diff --git a/custom_components/icloud3/Release Notes.txt b/custom_components/icloud3/Release Notes.txt index 64e0351..2cab5ed 100644 --- a/custom_components/icloud3/Release Notes.txt +++ b/custom_components/icloud3/Release Notes.txt @@ -3,6 +3,25 @@ **iCloud3 v3 Documentation** - iCloud3 User Guide can be found [here](https://gcobb321.github.io/icloud3_v3_docs/#/) **Migrating from v2.4.7_** - See [here](https://gcobb321.github.io/icloud3_v3_docs/#/chapters/3.1-migrating-v2-to-v3) for instructions on migrating from from an older version. +v3.1.5b1 Removed, Replaced with v3.1.5b2 [here](https://github.com/gcobb321/icloud3_v3/issues/399#issuecomment-2620099037) + + + +### Release Notes - v3.1.5b3 (1/31/2025) +#### 🐛 Bug Fixes +- A location request was sent incorrectly to Apple when logging into or adding a new Apple Account. This caused an incorrect _Invalid Username/Password_ message to be displayed and communication with the Apple account stopped. +- The _Enter/Request Verification Code_ screen was not displaying correctly if there were no Apple accounts that needed to be verified or the Apple account was deleted. +- Waze History time/distance recalculation will no longer run again at midnight if it was run immediately. An update that is running can now be terminated. +- Fixed a problem creating a 'Server Error 500' loading the iCloud3 integration for the first time. + +#### 🎉 Improvements and New Features +- Improved - Internet connections errors are now detected and monitored. iCloud3 will pause all tracking until the Internet is back up. A status message is displayed in the Event log and a message will be sent to an iCloud3 device using the Mobile App that Home Assistant is Offline. +- Improved - Renamed and reorganized the _Configure > Tracking Parameters_ screen and _Field Formats & Event Log Parmaeters_ screens. +- New - The _Picture Directory Filter_ can now be updated from the _Update Device_ screen _Picture Selection List_. +- New - If no picture is selected for the Device (=None), the Device's icon that will be displayed can now be selected from Home Assistant's 'mdi:' icon image list (default=mdi:account). + + + 3.1.4.3 ### Release Notes - v3.1.4.3 (1/18/2025) #### General Updates diff --git a/custom_components/icloud3/__init__.py b/custom_components/icloud3/__init__.py index 58b021a..0e7888d 100644 --- a/custom_components/icloud3/__init__.py +++ b/custom_components/icloud3/__init__.py @@ -72,8 +72,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # has set up the integration start_ic3.initialize_directory_filenames() await Gb.hass.async_add_executor_job( - config_file.load_storage_icloud3_configuration_file) - # await config_file.async_load_storage_icloud3_configuration_file() + config_file.load_icloud3_configuration_file) + # await config_file.async_load_icloud3_configuration_file() if Gb.conf_profile[CONF_VERSION] == 1: Gb.HALogger.warning(f"Starting iCloud3 v{VERSION}{VERSION_BETA} > " @@ -135,16 +135,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): Gb.entry_id = entry.entry_id Gb.operating_mode = MODE_INTEGRATION Gb.PyiCloud = None - ic3_device_tracker.get_ha_device_ids_from_device_registry(Gb.hass) + ic3_device_tracker.get_ha_device_ids_from_device_registry(Gb.hass) start_ic3.initialize_directory_filenames() - - await Gb.hass.async_add_executor_job( - config_file.load_storage_icloud3_configuration_file) - + await Gb.hass.async_add_executor_job( config_file.load_icloud3_configuration_file) start_ic3.set_log_level(Gb.log_level) - - # Setup iCloud Log File (icloud3.log) await Gb.hass.async_add_executor_job(open_ic3log_file_init) Gb.evlog_btnconfig_url = Gb.conf_profile[CONF_EVLOG_BTNCONFIG_URL].strip() @@ -163,10 +158,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return False await async_get_ha_location_info(hass) - start_ic3.initialize_data_source_variables() - await Gb.hass.async_add_executor_job( - restore_state.load_storage_icloud3_restore_state_file) + await Gb.hass.async_add_executor_job(restore_state.load_icloud3_restore_state_file) # config_file.count_lines_of_code(Gb.icloud3_directory) @@ -183,10 +176,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): Gb.PyiCloudValidateAppleAcct = PyiCloudValidateAppleAcct() Gb.username_valid_by_username = {} if Gb.use_data_source_ICLOUD: - await Gb.hass.async_add_executor_job( - move_icloud_cookies_to_icloud3_apple_acct) - await Gb.hass.async_add_executor_job( - pyicloud_ic3_interface.check_all_apple_accts_valid_upw) + # v3.0 --> v3.1 file location change + await Gb.hass.async_add_executor_job(move_icloud_cookies_to_icloud3_apple_acct) + await Gb.hass.async_add_executor_job(pyicloud_ic3_interface.check_all_apple_accts_valid_upw) # set_up_default_area_id() diff --git a/custom_components/icloud3/config_flow.py b/custom_components/icloud3/config_flow.py index 1938763..fa5cebc 100644 --- a/custom_components/icloud3/config_flow.py +++ b/custom_components/icloud3/config_flow.py @@ -53,7 +53,7 @@ CONF_SENSORS_OTHER, CONF_EXCLUDED_SENSORS, CONF_PARAMETER_TIME_STR, CONF_PARAMETER_FLOAT, CF_PROFILE, CF_TRACKING, CF_GENERAL, CF_DATA, CF_SENSORS, - DEFAULT_DEVICE_CONF, DEFAULT_GENERAL_CONF, DEFAULT_APPLE_ACCOUNTS_CONF, DEFAULT_DATA_CONF, + DEFAULT_DEVICE_CONF, DEFAULT_GENERAL_CONF, DEFAULT_APPLE_ACCOUNT_CONF, DEFAULT_DATA_CONF, DEFAULT_DEVICE_APPLE_ACCT_DATA_SOURCE, DEFAULT_DEVICE_MOBAPP_DATA_SOURCE, DEFAULT_TRACKING_CONF, DEFAULT_SENSORS_CONF, ) @@ -163,7 +163,7 @@ async def async_step_user(self, user_input=None): Gb.hass = self.hass start_ic3.initialize_directory_filenames() - await config_file.async_load_storage_icloud3_configuration_file() + await config_file.async_load_icloud3_configuration_file() start_ic3.initialize_data_source_variables() await file_io.async_make_directory(Gb.icloud_session_directory) @@ -231,6 +231,18 @@ async def async_step_reauth(self, user_input=None, errors=None, called_from_step action_item = '' reauth_username = None + # if (Gb.internet_connection_error + # and Gb.PyiCloudValidateAppleAcct.is_internet_available + # and Gb.internet_connection_test is False): + # reset_internet_connection_error() + # Gb.internet_connection_error = False + # Gb.internet_connection_error_secs = 0 + # list_add(self.config_parms_update_control, 'restart') + # list_add(self.config_parms_update_control, 'restart') + + if Gb.internet_connection_error: + self.errors['base'] = 'internet_connection_err' + if user_input is None: return self.async_show_form(step_id='reauth', data_schema=form_reauth(_OptFlow), @@ -266,7 +278,8 @@ async def async_step_reauth(self, user_input=None, errors=None, called_from_step _OptFlow.apple_acct_reauth_username = username if _OptFlow.PyiCloud is None: - self.errors['base'] = 'icloud_acct_not_logged_into' + self.errors['base'] = 'icloud_acct_not_logged_into' if Gb.internet_connection_error is False else \ + 'internet_connection_err' action_item = 'goto_previous' elif (action_item == 'send_verification_code' @@ -349,7 +362,7 @@ async def async_migrate_v2_config_to_v3(self): v2v3_config_migration.convert_v2_config_files_to_v3() v2v3_config_migration.remove_ic3_devices_from_known_devices_yaml_file() - config_file.async_load_storage_icloud3_configuration_file() + config_file.async_load_icloud3_configuration_file() Gb.v2v3_config_migrated = True if Gb.restart_ha_flag: @@ -440,7 +453,7 @@ def initialize_options(self): self.mobapp_list_text_by_entity_id = {} # mobile_app device_tracker info used in devices form for mobapp selection self.mobapp_list_text_by_entity_id = MOBAPP_DEVICE_NONE_OPTIONS.copy() self.picture_by_filename = {} - self.picture_by_filename_base = NONE_DICT_KEY_TEXT.copy() + self.picture_by_filename_base = PICTURE_NONE_KEY_TEXT.copy() self.zone_name_key_text = {} self.opt_picture_file_name_list = [] @@ -511,7 +524,13 @@ def _initialize_self_PyiCloud_fields_from_Gb(self): self.username = conf_apple_acct[CONF_USERNAME] self.password = conf_apple_acct[CONF_PASSWORD] - self.PyiCloud = Gb.PyiCloud_by_username.get(self.username) + self.PyiCloud = Gb.PyiCloud_by_username.get(self.username) + + if instr(Gb.conf_tracking[CONF_DATA_SOURCE], 'famshr'): + Gb.conf_tracking[CONF_DATA_SOURCE] = Gb.conf_tracking[CONF_DATA_SOURCE].replace('famshr', ICLOUD) + if instr(Gb.conf_tracking[CONF_DATA_SOURCE], 'mobapp'): + Gb.conf_tracking[CONF_DATA_SOURCE] = Gb.conf_tracking[CONF_DATA_SOURCE].replace('mobapp', MOBAPP) + self.data_source = Gb.conf_tracking[CONF_DATA_SOURCE] self.endpoint_suffix = Gb.conf_tracking[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] or \ Gb.icloud_server_endpoint_suffix @@ -554,7 +573,7 @@ async def async_step_menu(self, user_input=None, errors=None): self.step_id = f"menu_{self.menu_page_no}" self.called_from_step_id_1 = self.called_from_step_id_2 = '' self.errors = errors or {} - await self._async_write_storage_icloud3_configuration_file() + await self._async_write_icloud3_configuration_file() Gb.trace_prefix = 'CONFIG' Gb.config_flow_flag = True @@ -562,12 +581,12 @@ async def async_step_menu(self, user_input=None, errors=None): if (self.username != '' and self.password != '' and instr(Gb.conf_tracking[CONF_DATA_SOURCE], ICLOUD) is False): self.header_msg = 'icloud_acct_data_source_warning' - + elif Gb.internet_connection_error: + self.header_msg = 'internet_connection_err' elif self.PyiCloud is None and self.username: self.header_msg = 'icloud_acct_not_logged_into' - elif self.is_verification_code_needed: - self.errors['base'] ='verification_code_needed' + self.header_msg ='verification_code_needed' if user_input is None: self._set_inactive_devices_header_msg() @@ -664,7 +683,7 @@ async def async_step_restart_icloud3(self, user_input=None, errors=None): self.step_id = 'restart_icloud3' self.errors = errors or {} self.errors_user_input = {} - await self._async_write_storage_icloud3_configuration_file() + await self._async_write_icloud3_configuration_file() user_input, action_item = self._action_text_to_item(user_input) self.log_step_info(user_input, action_item) @@ -820,37 +839,6 @@ def common_form_handler(self, user_input=None, action_item=None, errors=None): # Display the config data entry form, any errors will be redisplayed and highlighted return False - -#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -# FORMAT SETTINGS -#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - async def async_step_format_settings(self, user_input=None, errors=None): - self.step_id = 'format_settings' - user_input, action_item = self._action_text_to_item(user_input) - - if self.common_form_handler(user_input, action_item, errors): - return await self.async_step_menu() - - if self.errors != {} and self.errors.get('base') != 'conf_updated': - self.errors['action_items'] = 'update_aborted' - - return self.async_show_form(step_id='format_settings', - data_schema=form_format_settings(self), - errors=self.errors) - -#------------------------------------------------------------------------------------------- - @staticmethod - def _format_device_text_hdr(conf_device): - device_text = ( f"{conf_device[CONF_FNAME]} " - f"({conf_device[CONF_IC3_DEVICENAME]})") - if conf_device[CONF_TRACKING_MODE] == MONITOR_DEVICE: - device_text += ", MONITOR" - elif conf_device[CONF_TRACKING_MODE] == INACTIVE_DEVICE: - device_text += ", ✪ INACTIVE" - - return device_text - - #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # CONFIRM ACTION #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> @@ -942,6 +930,26 @@ 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.common_form_handler(user_input, action_item, errors): + return await self.async_step_menu() + + + if self._any_errors(): + self.errors['action_items'] = 'update_aborted' + + return self.async_show_form(step_id='tracking_parameters', + data_schema=form_tracking_parameters(self), + errors=self.errors) + + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# FORMAT SETTINGS +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + async def async_step_format_settings(self, user_input=None, errors=None): + self.step_id = 'format_settings' + user_input, action_item = self._action_text_to_item(user_input) + if self.www_directory_list == []: start_dir = 'www' self.www_directory_list = await Gb.hass.async_add_executor_job( @@ -955,14 +963,25 @@ async def async_step_tracking_parameters(self, user_input=None, errors=None): await self._build_picture_filename_selection_list() return await self.async_step_menu() - - if self._any_errors(): + if self.errors != {} and self.errors.get('base') != 'conf_updated': self.errors['action_items'] = 'update_aborted' - return self.async_show_form(step_id='tracking_parameters', - data_schema=form_tracking_parameters(self), + return self.async_show_form(step_id='format_settings', + data_schema=form_format_settings(self), errors=self.errors) +#------------------------------------------------------------------------------------------- + @staticmethod + def _format_device_text_hdr(conf_device): + device_text = ( f"{conf_device[CONF_FNAME]} " + f"({conf_device[CONF_IC3_DEVICENAME]})") + if conf_device[CONF_TRACKING_MODE] == MONITOR_DEVICE: + device_text += ", MONITOR" + elif conf_device[CONF_TRACKING_MODE] == INACTIVE_DEVICE: + device_text += ", ✪ INACTIVE" + + return device_text + #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # INZONE INTERVALS @@ -1026,7 +1045,7 @@ async def async_step_sensors(self, user_input=None, errors=None): self.step_id = 'sensors' self.errors = errors or {} - await self._async_write_storage_icloud3_configuration_file() + await self._async_write_icloud3_configuration_file() user_input, action_item = self._action_text_to_item(user_input) self.log_step_info(user_input, action_item) @@ -1085,7 +1104,7 @@ async def async_step_exclude_sensors(self, user_input=None, errors=None): self.step_id = 'exclude_sensors' self.errors = errors or {} - await self._async_write_storage_icloud3_configuration_file() + await self._async_write_icloud3_configuration_file() user_input, action_item = self._action_text_to_item(user_input) if self.excluded_sensors == []: @@ -1224,6 +1243,7 @@ async def async_step_display_text_as(self, user_input=None, errors=None): async def async_step_display_text_as_update(self, user_input=None, errors=None): self.step_id = 'display_text_as_update' user_input, action_item = self._action_text_to_item(user_input) + self.log_step_info(user_input, action_item) if action_item == 'cancel_goto_menu': return await self.async_step_display_text_as() @@ -1250,6 +1270,38 @@ async def async_step_display_text_as_update(self, user_input=None, errors=None): data_schema=form_display_text_as_update(self), errors=self.errors) +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# PICTURE DIRECTORY FILTER +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + async def async_step_picture_dir_filter(self, user_input=None, errors=None): + self.step_id = 'picture_dir_filter' + user_input, action_item = self._action_text_to_item(user_input) + self.log_step_info(user_input, action_item) + + if action_item == 'cancel_goto_menu': + return await self.async_step_update_device() + + if self.www_directory_list == []: + self.www_directory_list = \ + await Gb.hass.async_add_executor_job(file_io.get_directory_list, 'www') + + if action_item == 'save': + Gb.picture_www_dirs = [] + for group_no in ['1', '2', '3', '4', '5']: + ui_group_name = f"www_group_{group_no}" + if ui_group_name in user_input: + for dir in user_input[ui_group_name]: + list_add(Gb.picture_www_dirs, dir) + + Gb.conf_profile[CONF_PICTURE_WWW_DIRS] = Gb.picture_www_dirs + self.picture_by_filename = {} + await self._build_picture_filename_selection_list() + self._update_config_file_general(user_input) + return await self.async_step_update_device() + + return self.async_show_form(step_id='picture_dir_filter', + data_schema=form_picture_dir_filter(self), + errors=self.errors) #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # TOOLS @@ -1264,7 +1316,7 @@ async def async_step_tools(self, user_input=None, errors=None): self.errors = errors or {} self.errors_user_input = {} - await self._async_write_storage_icloud3_configuration_file() + await self._async_write_icloud3_configuration_file() if user_input is None or 'confrm_action_item' in user_input: return self.async_show_form(step_id='tools', @@ -1462,7 +1514,7 @@ async def async_step_restart_ha_reload_icloud3(self, user_input=None, errors=Non elif action_item == 'reload_icloud3': post_event("RELOAD ICLOUD3") write_config_file_to_ic3log() - await config_file.async_write_storage_icloud3_configuration_file() + await config_file.async_write_icloud3_configuration_file() close_ic3_log_file() await Gb.hass.services.async_call( @@ -1480,7 +1532,7 @@ async def async_step_restart_ha_reload_icloud3(self, user_input=None, errors=Non # VALIDATE DATA AND UPDATE CONFIG FILE # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - async def _async_write_storage_icloud3_configuration_file(self): + async def _async_write_icloud3_configuration_file(self): ''' Write the updated configuration file to .storage/icloud3/configuration The config file updates are done by setting the commit_updates flag in @@ -1489,7 +1541,7 @@ async def _async_write_storage_icloud3_configuration_file(self): while the update fcts are not. ''' if self.config_file_commit_updates: - await config_file.async_write_storage_icloud3_configuration_file() + await config_file.async_write_icloud3_configuration_file() self.config_file_commit_updates = False self.header_msg = 'conf_updated' @@ -1635,13 +1687,7 @@ def _validate_format_settings(self, user_input): user_input = self._option_text_to_parm(user_input, CONF_DEVICE_TRACKER_STATE_SOURCE, DEVICE_TRACKER_STATE_SOURCE_OPTIONS) user_input = self._option_text_to_parm(user_input, CONF_UNIT_OF_MEASUREMENT, UNIT_OF_MEASUREMENT_OPTIONS) user_input = self._option_text_to_parm(user_input, CONF_TIME_FORMAT, TIME_FORMAT_OPTIONS) - user_input = self._option_text_to_parm(user_input, CONF_LOG_LEVEL, LOG_LEVEL_OPTIONS) - - if (user_input[CONF_LOG_LEVEL_DEVICES] == [] - or len(user_input[CONF_LOG_LEVEL_DEVICES]) >= len(Gb.Devices)): - user_input[CONF_LOG_LEVEL_DEVICES] = ['all'] - elif len(user_input[CONF_LOG_LEVEL_DEVICES]) > 1: - list_del(user_input[CONF_LOG_LEVEL_DEVICES], 'all') + user_input = self._strip_spaces(user_input, [CONF_EVLOG_BTNCONFIG_URL]) if (Gb.display_zone_format != user_input[CONF_DISPLAY_ZONE_FORMAT]): list_add(self.config_parms_update_control, 'special_zone') @@ -1697,7 +1743,6 @@ def _validate_tracking_parameters(self, user_input): Update the profile parameters ''' user_input = self._option_text_to_parm(user_input, CONF_TRAVEL_TIME_FACTOR, TRAVEL_TIME_INTERVAL_MULTIPLIER_KEY_TEXT) - user_input[CONF_EVLOG_BTNCONFIG_URL] = user_input[CONF_EVLOG_BTNCONFIG_URL].strip() return user_input @@ -1857,18 +1902,19 @@ def _set_inactive_devices_header_msg(self): inactive_pct = inactive_device_cnt / device_cnt if device_cnt == inactive_device_cnt: - inactive_msg = 'all' - elif inactive_pct > .66: - inactive_msg = 'most' - elif inactive_pct > .34: - inactive_msg = 'some' - else: - return 'none' + self.header_msg = f'inactive_all_devices' + # inactive_msg = 'all' + # elif inactive_pct > .66: + # inactive_msg = 'most' + # elif inactive_pct > .34: + # inactive_msg = 'some' + # else: + return 'none' # inactive_msg = 'few' - self.header_msg = f'inactive_{inactive_msg}_devices' + # self.header_msg = f'inactive_{inactive_msg}_devices' - return inactive_msg + # return inactive_msg #------------------------------------------------------------------------------------------- @staticmethod @@ -1908,7 +1954,19 @@ async def async_step_data_source( self, user_input=None, errors=None, self.add_apple_acct_flag = False self.actions_list_default = '' action_item = '' - await self._async_write_storage_icloud3_configuration_file() + + # if (Gb.internet_connection_error + # and Gb.PyiCloudValidateAppleAcct.is_internet_available + # and Gb.internet_connection_test is False): + # reset_internet_connection_error() + # Gb.internet_connection_error = False + # Gb.internet_connection_error_secs = 0 + # list_add(self.config_parms_update_control, 'restart') + + if Gb.internet_connection_error: + self.errors['base'] = 'internet_connection_err' + + await self._async_write_icloud3_configuration_file() user_input, action_item = self._action_text_to_item(user_input) if self._is_apple_acct_setup() is False: self.errors['apple_accts'] = 'icloud_acct_not_set_up' @@ -1929,7 +1987,7 @@ async def async_step_data_source( self, user_input=None, errors=None, if user_input['apple_accts'].startswith('➤ ADD'): self.add_apple_acct_flag = True action_item = 'update_apple_acct' - self.conf_apple_acct = DEFAULT_APPLE_ACCOUNTS_CONF.copy() + self.conf_apple_acct = DEFAULT_APPLE_ACCOUNT_CONF.copy() return await self.async_step_update_apple_acct() if user_input['apple_accts'].startswith('➤ OTHER'): @@ -2027,14 +2085,24 @@ def _is_apple_acct_setup(self): # APPLE USERNAME PASSWORD #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_update_apple_acct(self, user_input=None, errors=None): - self.step_id = 'update_apple_acct' self.errors = errors or {} self.multi_form_user_input = {} self.errors_user_input = user_input or {} self.actions_list_default = '' action_item = '' - await self._async_write_storage_icloud3_configuration_file() + await self._async_write_icloud3_configuration_file() + + # if (Gb.internet_connection_error + # and Gb.PyiCloudValidateAppleAcct.is_internet_available + # and Gb.internet_connection_test is False): + # reset_internet_connection_error() + # Gb.internet_connection_error = False + # Gb.internet_connection_error_secs = 0 + # list_add(self.config_parms_update_control, 'restart') + + if Gb.internet_connection_error: + self.errors['base'] = 'internet_connection_err' user_input, action_item = self._action_text_to_item(user_input) @@ -2115,7 +2183,7 @@ async def async_step_update_apple_acct(self, user_input=None, errors=None): Gb.conf_tracking[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] and user_input[CONF_LOCATE_ALL] != conf_locate_all and Gb.PyiCloud_by_username.get(ui_username) is not None): - self.errors['base'] = 'icloud_acct_logged_into' + self.header_msg = 'icloud_acct_logged_into' action_item = '' if action_item == '': @@ -2157,13 +2225,20 @@ async def async_step_update_apple_acct(self, user_input=None, errors=None): self.actions_list_default = 'add_change_apple_acct' self.errors['base'] = '' self.errors[CONF_USERNAME] = 'icloud_acct_login_error_user_pw' + + # App Specific Password (ASP) format: msim-lwru-afiq-igwr + if (len(ui_password) == 19 + and ui_password[4:5] == '-' + and ui_password[9:10] == '-' + and ui_password[14:15] == '-'): + self.errors[CONF_PASSWORD] = 'password_asp_invalid' return await self.async_step_update_apple_acct( user_input=user_input, errors=self.errors) if aa_login_info_changed or other_flds_changed: self._update_conf_apple_accounts(self.aa_idx, user_input) - await self._async_write_storage_icloud3_configuration_file() + await self._async_write_icloud3_configuration_file() # Log into the account if (aa_login_info_changed @@ -2244,7 +2319,7 @@ def _update_conf_apple_accounts(self, aa_idx, user_input, remove_acct_flag=False if Gb.conf_tracking[CONF_USERNAME] == self.conf_apple_acct[CONF_USERNAME]: Gb.conf_tracking[CONF_USERNAME] = '' Gb.conf_tracking[CONF_PASSWORD] = '' - self.conf_apple_acct = DEFAULT_APPLE_ACCOUNTS_CONF.copy() + self.conf_apple_acct = DEFAULT_APPLE_ACCOUNT_CONF.copy() self.aa_idx = aa_idx - 1 if self.aa_idx < 0: self.aa_idx = 0 self.aa_page_item[self.aa_page_no] = '' @@ -2390,6 +2465,7 @@ def _delete_apple_acct(self, user_input): elif device_action == 'set_devices_inactive': conf_device[CONF_APPLE_ACCOUNT] = '' + conf_device[CONF_FAMSHR_DEVICENAME] ='None' conf_device[CONF_TRACKING_MODE] = INACTIVE_DEVICE updated_conf_devices.append(conf_device) @@ -2466,14 +2542,23 @@ async def async_step_reauth(self, user_input=None, errors=None, self.called_from_step_id_1 = called_from_step_id or self.called_from_step_id_1 or 'menu_0' reauth_username = None + # if (Gb.internet_connection_error + # and Gb.PyiCloudValidateAppleAcct.is_internet_available + # and Gb.internet_connection_test is False): + # reset_internet_connection_error() + # Gb.internet_connection_error = False + # Gb.internet_connection_error_secs = 0 + # list_add(self.config_parms_update_control, 'restart') + + if Gb.internet_connection_error: + self.errors['base'] = 'internet_connection_err' + elif self.PyiCloud and self.PyiCloud.requires_2fa: + self.errors['base'] ='verification_code_needed' + log_debug_msg( f"OF-{self.step_id.upper()} ({action_item}) > " f"FromForm-{called_from_step_id}, UserInput-{user_input}, Errors-{errors}") if user_input is None: - if self.PyiCloud and self.PyiCloud.requires_2fa: - self.errors['base'] ='verification_code_needed' - - # reauth_username=self.PyiCloud.username reauth_username = self.apple_acct_reauth_username return self.async_show_form(step_id='reauth', data_schema=form_reauth(self, reauth_username=reauth_username), @@ -2743,7 +2828,7 @@ async def async_step_device_list(self, user_input=None, errors=None): self.errors = errors or {} self.errors_user_input = {} self.add_device_flag = self.display_rarely_updated_parms = False - await self._async_write_storage_icloud3_configuration_file() + await self._async_write_icloud3_configuration_file() if user_input is None: await self._build_icloud_device_selection_list() @@ -2875,7 +2960,7 @@ def _get_conf_device_selected(self, user_input): # self.form_devices_list_all.pop(self.conf_device_selected_idx) # devicename = self.form_devices_list_devicename.pop(self.conf_device_selected_idx) - # config_file.write_storage_icloud3_configuration_file() + # config_file.write_icloud3_configuration_file() # device_cnt = len(self.form_devices_list_devicename) - 1 # if self.conf_device_selected_idx > device_cnt: @@ -3091,7 +3176,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 = {} - self.multi_form_user_input = {} + # self.multi_form_user_input = {} await self._build_update_device_selection_lists(self.conf_device[CONF_IC3_DEVICENAME]) log_debug_msg(f"{self.step_id.upper()} ( > UserInput-{user_input}, Errors-{errors}") @@ -3163,7 +3248,11 @@ async def async_step_update_device(self, user_input=None, errors=None): return self.async_show_form(step_id='update_device', data_schema=form_update_device(self), errors=self.errors) - # last_step=True) + + if user_input[CONF_PICTURE] == 'setup_dir_filter': + self.multi_form_user_input = user_input + self.multi_form_user_input[CONF_PICTURE] = self.conf_device[CONF_PICTURE] + return await self.async_step_picture_dir_filter() if change_flag is False: if ui_rarely_used_parms_changed and self.display_rarely_updated_parms: @@ -3171,6 +3260,10 @@ async def async_step_update_device(self, user_input=None, errors=None): return await self.async_step_device_list() + # Picture was changed to None, display Icon mdi: selector later on + picture_changed_to_none = (user_input[CONF_PICTURE] == 'None' \ + and self.conf_device[CONF_PICTURE] != 'None') + ui_devicename = user_input[CONF_IC3_DEVICENAME] only_non_tracked_field_updated = self._is_only_non_tracked_field_updated(user_input) @@ -3210,6 +3303,10 @@ async def async_step_update_device(self, user_input=None, errors=None): if ui_rarely_used_parms_changed: return await self.async_step_update_device() + # Picture was changed to None, display Icon mdi: selector + if picture_changed_to_none: + return await self.async_step_update_device() + return await self.async_step_device_list() #------------------------------------------------------------------------------------------- @@ -4214,9 +4311,8 @@ async def _build_picture_filename_selection_list(self): www_dir_idx += 1 self.picture_by_filename[f".www_dirs{www_dir_idx}"] = over_25_warning_msg - self.picture_by_filename['.www_dirs998'] = ("A directory filter can be set on the " - "`Tracking and Other Parameters` screen") self.picture_by_filename['.available'] = f"✅ ⋯⋯⋯ DEVICE PICTURE FILE NAMES {'⋯'*16} ✅" + self.picture_by_filename['setup_dir_filter'] = "➤ FILTER IMAGE DIRECTORIES > Select directories with the picture image files" self.picture_by_filename.update(self.picture_by_filename_base) for sorted_image_filename in sorted_image_filenames: @@ -4384,7 +4480,7 @@ async def log_into_icloud_account(self, user_input, called_from_step_id=None): endpoint_suffix = user_input.get(CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX, Gb.conf_tracking[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX]) - log_info_msg( f"Apple Acct > {username}, Logging in, )" + log_info_msg( f"Apple Acct > {username}, Logging in, " f"UserInput-{user_input}, Errors-{self.errors}, " f"Step-{self.step_id}, CalledFrom-{called_from_step_id}") @@ -4396,8 +4492,7 @@ async def log_into_icloud_account(self, user_input, called_from_step_id=None): self.PyiCloud = PyiCloud self.username = username self.password = password - self.errors['base'] = 'icloud_acct_logged_into' - self.header_msg = 'icloud_acct_logged_into' + self.header_msg = 'icloud_acct_logged_into' log_info_msg(f"Apple Acct > {username}, Already Logged in, {self.PyiCloud}") return True @@ -4431,16 +4526,19 @@ async def log_into_icloud_account(self, user_input, called_from_step_id=None): Gb.username_valid_by_username[username] = True log_info_msg(f"Apple Acct > {username}, Login successful, {self.PyiCloud}") - PyiCloud.refresh_icloud_data() - start_ic3.dump_startup_lists_to_log() + # try: + # await Gb.hass.async_add_executor_job(PyiCloud.refresh_icloud_data) + # log_info_msg(f"Apple Acct > {username}, Data Refreshed for all devices") + # except Exception as err: + # log_info_msg( f"Apple Acct > {username}, An error occurred refreshing the device data, " + # f"Error-{err}") - log_info_msg(f"Apple Acct > {username}, Location Data Refreshed, {self.PyiCloud}") + start_ic3.dump_startup_lists_to_log() if PyiCloud.requires_2fa or called_from_step_id is None: log_info_msg(f"Apple Acct > {username}, 2fa Verification Needed, {self.PyiCloud}") return True - self.errors['base'] = 'icloud_acct_logged_into' self.header_msg = 'icloud_acct_logged_into' if called_from_step_id is None: @@ -5316,4 +5414,4 @@ def form_schema(self, step_id, actions_list=None, actions_list_default=None): elif step_id == 'reauth': return form_reauth(self) else: - return {} + return {} \ No newline at end of file diff --git a/custom_components/icloud3/config_flow_forms.py b/custom_components/icloud3/config_flow_forms.py index ad22e71..b9e2798 100644 --- a/custom_components/icloud3/config_flow_forms.py +++ b/custom_components/icloud3/config_flow_forms.py @@ -13,13 +13,12 @@ DEVICE_TYPE_FNAME, DEVICE_TYPE_FNAMES, MOBAPP, NO_MOBAPP, INACTIVE_DEVICE, HOME_DISTANCE, PICTURE_WWW_STANDARD_DIRS, CONF_PICTURE_WWW_DIRS, - DEFAULT_DEVICE_CONF, CONF_EVLOG_CARD_DIRECTORY, CONF_EVLOG_BTNCONFIG_URL, CONF_APPLE_ACCOUNT, CONF_USERNAME, CONF_PASSWORD, CONF_LOCATE_ALL, CONF_TOTP_KEY, CONF_DATA_SOURCE, CONF_VERIFICATION_CODE, CONF_TRACK_FROM_ZONES, CONF_LOG_ZONES, CONF_TRACK_FROM_BASE_ZONE_USED, CONF_TRACK_FROM_BASE_ZONE, CONF_TRACK_FROM_HOME_ZONE, - CONF_PICTURE, CONF_DEVICE_TYPE, CONF_INZONE_INTERVALS, + CONF_PICTURE, CONF_ICON, CONF_DEVICE_TYPE, CONF_INZONE_INTERVALS, CONF_UNIT_OF_MEASUREMENT, CONF_TIME_FORMAT, CONF_MAX_INTERVAL, CONF_OFFLINE_INTERVAL, CONF_EXIT_ZONE_INTERVAL, CONF_MOBAPP_ALIVE_INTERVAL, CONF_GPS_ACCURACY_THRESHOLD, CONF_OLD_LOCATION_THRESHOLD, CONF_OLD_LOCATION_ADJUSTMENT, @@ -30,7 +29,7 @@ CONF_DISTANCE_BETWEEN_DEVICES, CONF_WAZE_USED, CONF_WAZE_SERVER, CONF_WAZE_MAX_DISTANCE, CONF_WAZE_MIN_DISTANCE, CONF_WAZE_REALTIME, CONF_WAZE_HISTORY_DATABASE_USED, CONF_WAZE_HISTORY_MAX_DISTANCE, - CONF_WAZE_HISTORY_TRACK_DIRECTION, + CONF_WAZE_HISTORY_TRACK_DIRECTION, CONF_STAT_ZONE_FNAME, CONF_STAT_ZONE_STILL_TIME, CONF_STAT_ZONE_INZONE_INTERVAL, CONF_DISPLAY_TEXT_AS, CONF_IC3_DEVICENAME, CONF_FNAME, CONF_FAMSHR_DEVICENAME, CONF_MOBILE_APP_DEVICE, @@ -331,9 +330,9 @@ def form_update_apple_acct(self): vol.Optional(CONF_PASSWORD , default=password): password_selector, - vol.Optional(CONF_TOTP_KEY, - default='For future use in supporting hardware keys (YubiKey)'): - selector.TextSelector(), + # vol.Optional(CONF_TOTP_KEY, + # default='For future use in supporting hardware keys (YubiKey)'): + # selector.TextSelector(), vol.Optional('locate_all', default=locate_all): cv.boolean, @@ -406,9 +405,20 @@ def form_reauth(self, reauth_username=None): self._build_apple_accounts_list() # No Apple accts are set up - if (is_empty(self.apple_acct_items_by_username) - or is_empty(Gb.conf_apple_accounts)): - self.apple_acct_reauth_username = '' + # No Apple acct is selected, get the first one or one that needs to be authenticated + if instr(str(self.apple_acct_items_by_username), 'AUTHENTICATION'): + self.apple_acct_reauth_username = [username + for username, acct_info in self.apple_acct_items_by_username.items() + if instr(acct_info, 'AUTHENTICATION')][0] + self.conf_apple_acct, self.aa_idx = \ + config_file.conf_apple_acct(self.apple_acct_reauth_username) + + elif (is_empty(self.apple_acct_items_by_username) + or is_empty(Gb.conf_apple_accounts) + or self.apple_acct_reauth_username not in self.apple_acct_items_by_username): + self.conf_apple_acct, self.aa_idx = \ + config_file.conf_apple_acct(0) + self.apple_acct_reauth_username = self.conf_apple_acct[CONF_USERNAME] # Requesting a new code will set the selected acct. Use it to deselect the acct elif reauth_username is not None and reauth_username != '': @@ -416,13 +426,6 @@ def form_reauth(self, reauth_username=None): self.conf_apple_acct, self.aa_idx = \ config_file.conf_apple_acct(reauth_username) - # No Apple acct is selected, get the first one or one that needs to be authenticated - elif instr(str(self.apple_acct_items_by_username), 'AUTHENTICATION'): - self.apple_acct_reauth_username = [username - for username, acct_info in self.apple_acct_items_by_username.items() - if instr(acct_info, 'AUTHENTICATION')][0] - self.conf_apple_acct, self.aa_idx = \ - config_file.conf_apple_acct(self.apple_acct_reauth_username) elif isnot_empty(self.conf_apple_acct): self.apple_acct_reauth_username = self.conf_apple_acct[CONF_USERNAME] @@ -465,7 +468,7 @@ def form_reauth(self, reauth_username=None): selector.SelectSelector(selector.SelectSelectorConfig( options=dict_value_to_list(self.apple_acct_items_by_username), mode='dropdown')), - vol.Optional(CONF_VERIFICATION_CODE, default=otp_code): + vol.Optional(CONF_VERIFICATION_CODE, default=' '): selector.TextSelector(), vol.Optional('action_items', default=self.action_default_text(action_list_default)): @@ -588,6 +591,9 @@ def form_add_device(self): default=default_picture_filename): selector.SelectSelector(selector.SelectSelectorConfig( options=dict_value_to_list(self.picture_by_filename), mode='dropdown')), + vol.Required(CONF_ICON, + default=self._parm_or_device(CONF_ICON)): + selector.IconSelector(), vol.Required(CONF_TRACKING_MODE, default=self._option_parm_to_text(CONF_TRACKING_MODE, TRACKING_MODE_OPTIONS)): selector.SelectSelector(selector.SelectSelectorConfig( @@ -696,11 +702,19 @@ def form_update_device(self): default=default_picture_filename): selector.SelectSelector(selector.SelectSelectorConfig( options=dict_value_to_list(_picture_by_filename), mode='dropdown')), + } + if self._parm_or_device(CONF_PICTURE) == 'None': + schema.update({ + vol.Required(CONF_ICON, + default=self._parm_or_device(CONF_ICON)): + selector.IconSelector(), + }) + schema.update({ vol.Required(CONF_TRACKING_MODE, default=self._option_parm_to_text(CONF_TRACKING_MODE, TRACKING_MODE_OPTIONS)): selector.SelectSelector(selector.SelectSelectorConfig( options=dict_value_to_list(TRACKING_MODE_OPTIONS), mode='dropdown')), - } + }) if self.display_rarely_updated_parms is False: schema.update({ @@ -829,21 +843,73 @@ def form_actions(self): #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -# FORMAT SETTINGS +# TRACKING PARAMETERS +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_tracking_parameters(self): + self.actions_list = ACTION_LIST_ITEMS_BASE.copy() + + return vol.Schema({ + vol.Required(CONF_DISTANCE_BETWEEN_DEVICES, + default=Gb.conf_general[CONF_DISTANCE_BETWEEN_DEVICES]): + selector.BooleanSelector(), + vol.Optional(CONF_DISCARD_POOR_GPS_INZONE, + default=Gb.conf_general[CONF_DISCARD_POOR_GPS_INZONE]): + selector.BooleanSelector(), + vol.Required(CONF_GPS_ACCURACY_THRESHOLD, + default=Gb.conf_general[CONF_GPS_ACCURACY_THRESHOLD]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=5, max=300, step=5, unit_of_measurement='m')), + vol.Required(CONF_OLD_LOCATION_THRESHOLD, + default=Gb.conf_general[CONF_OLD_LOCATION_THRESHOLD]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=1, max=60, step=1, unit_of_measurement='minutes')), + vol.Required(CONF_OLD_LOCATION_ADJUSTMENT, + default=Gb.conf_general[CONF_OLD_LOCATION_ADJUSTMENT]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=0, max=60, step=1, unit_of_measurement='minutes')), + vol.Required(CONF_MAX_INTERVAL, + default=Gb.conf_general[CONF_MAX_INTERVAL]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=15, max=480, step=5, unit_of_measurement='minutes')), + vol.Required(CONF_EXIT_ZONE_INTERVAL, + default=Gb.conf_general[CONF_EXIT_ZONE_INTERVAL]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=.5, max=10, step=.5, unit_of_measurement='minutes')), + vol.Required(CONF_MOBAPP_ALIVE_INTERVAL, + default=Gb.conf_general[CONF_MOBAPP_ALIVE_INTERVAL]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=15, max=240, step=5, unit_of_measurement='minutes')), + vol.Required(CONF_OFFLINE_INTERVAL, + default=Gb.conf_general[CONF_OFFLINE_INTERVAL]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=5, max=240, step=5, unit_of_measurement='minutes')), + vol.Required(CONF_TFZ_TRACKING_MAX_DISTANCE, + default=Gb.conf_general[CONF_TFZ_TRACKING_MAX_DISTANCE]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=1, max=100, unit_of_measurement='Km')), + vol.Required(CONF_TRAVEL_TIME_FACTOR, + default=self._option_parm_to_text(CONF_TRAVEL_TIME_FACTOR, TRAVEL_TIME_INTERVAL_MULTIPLIER_KEY_TEXT)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(TRAVEL_TIME_INTERVAL_MULTIPLIER_KEY_TEXT), mode='dropdown')), + + vol.Required('action_items', + default=self.action_default_text('save')): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# FORMAT SETTINGS & ICLOUD3 DIRECTORIES #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> def form_format_settings(self): self.actions_list = ACTION_LIST_ITEMS_BASE.copy() self._set_example_zone_name() - self._build_log_level_devices_list() + + self.picture_by_filename = {} + if PICTURE_WWW_STANDARD_DIRS in Gb.conf_profile[CONF_PICTURE_WWW_DIRS]: + Gb.conf_profile[CONF_PICTURE_WWW_DIRS] = [] return vol.Schema({ - vol.Required(CONF_LOG_LEVEL_DEVICES, - default=Gb.conf_general[CONF_LOG_LEVEL_DEVICES]): - cv.multi_select(six_item_dict(self.log_level_devices_key_text)), - vol.Required(CONF_LOG_LEVEL, - default=self._option_parm_to_text(CONF_LOG_LEVEL, LOG_LEVEL_OPTIONS)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(LOG_LEVEL_OPTIONS), mode='dropdown')), vol.Required(CONF_DISPLAY_ZONE_FORMAT, default=self._option_parm_to_text(CONF_DISPLAY_ZONE_FORMAT, DISPLAY_ZONE_FORMAT_OPTIONS)): selector.SelectSelector(selector.SelectSelectorConfig( @@ -860,11 +926,27 @@ def form_format_settings(self): default=self._option_parm_to_text(CONF_TIME_FORMAT, TIME_FORMAT_OPTIONS)): selector.SelectSelector(selector.SelectSelectorConfig( options=dict_value_to_list(TIME_FORMAT_OPTIONS), mode='dropdown')), + vol.Required(CONF_PICTURE_WWW_DIRS, + default=Gb.conf_profile[CONF_PICTURE_WWW_DIRS] or self.www_directory_list): + cv.multi_select(six_item_list(self.www_directory_list)), vol.Required(CONF_DISPLAY_GPS_LAT_LONG, default=Gb.conf_general[CONF_DISPLAY_GPS_LAT_LONG]): # cv.boolean, selector.BooleanSelector(), + vol.Required('evlog_header', + default=IC3_DIRECTORY_HEADER): + # cv.multi_select([IC3_DIRECTORY_HEADER]), + selector.SelectSelector(selector.SelectSelectorConfig( + options=[IC3_DIRECTORY_HEADER], mode='list')), + vol.Required(CONF_EVLOG_CARD_DIRECTORY, + default=self._parm_or_error_msg(CONF_EVLOG_CARD_DIRECTORY, conf_group=CF_PROFILE)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(self.www_directory_list), mode='dropdown')), + vol.Optional(CONF_EVLOG_BTNCONFIG_URL, + default=f"{self._parm_or_error_msg(CONF_EVLOG_BTNCONFIG_URL, conf_group=CF_PROFILE)} "): + selector.TextSelector(), + vol.Required('action_items', default=self.action_default_text('save')): selector.SelectSelector(selector.SelectSelectorConfig( @@ -892,6 +974,83 @@ def form_change_device_order(self): options=self.actions_list, mode='list')), }) +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# PICTURE IMAGE FILTER +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_picture_dir_filter(self): + self.actions_list = [ + ACTION_LIST_OPTIONS['save'], + ACTION_LIST_OPTIONS['goto_previous']] + + # self.picture_by_filename = {} + if PICTURE_WWW_STANDARD_DIRS in Gb.conf_profile[CONF_PICTURE_WWW_DIRS]: + Gb.conf_profile[CONF_PICTURE_WWW_DIRS] = [] + + www_group_1 = {} + www_group_2 = {} + www_group_3 = {} + www_group_4 = {} + www_group_5 = {} + conf_www_group_1 = [] + conf_www_group_2 = [] + conf_www_group_3 = [] + conf_www_group_4 = [] + conf_www_group_5 = [] + for dir in self.www_directory_list: + if len(www_group_1) < 5: + www_group_1[dir] = dir + if dir in Gb.conf_profile[CONF_PICTURE_WWW_DIRS]: + list_add(conf_www_group_1, dir) + elif len(www_group_2) < 5: + www_group_2[dir] = dir + if dir in Gb.conf_profile[CONF_PICTURE_WWW_DIRS]: + list_add(conf_www_group_2, dir) + elif len(www_group_3) < 5: + www_group_3[dir] = dir + if dir in Gb.conf_profile[CONF_PICTURE_WWW_DIRS]: + list_add(conf_www_group_3, dir) + elif len(www_group_4) < 5: + www_group_4[dir] = dir + if dir in Gb.conf_profile[CONF_PICTURE_WWW_DIRS]: + list_add(conf_www_group_4, dir) + elif len(www_group_5) < 5: + www_group_5[dir] = dir + if dir in Gb.conf_profile[CONF_PICTURE_WWW_DIRS]: + list_add(conf_www_group_5, dir) + + schema = { + vol.Required('www_group_1', + default=conf_www_group_1): + cv.multi_select(www_group_1)} + + if isnot_empty(www_group_2): + schema.update({ + vol.Required('www_group_2', + default=conf_www_group_2): + cv.multi_select(www_group_2),}) + if isnot_empty(www_group_3): + schema.update({ + vol.Required('www_group_3', + default=conf_www_group_3): + cv.multi_select(www_group_3),}) + if isnot_empty(www_group_4): + schema.update({ + vol.Required('www_group_4', + default=conf_www_group_4): + cv.multi_select(www_group_4),}) + if isnot_empty(www_group_5): + schema.update({ + vol.Required('www_group_5', + default=conf_www_group_5): + cv.multi_select(www_group_5),}) + + schema.update({ + vol.Required('action_items', + default=self.action_default_text('save')): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')),}) + + return vol.Schema(schema) #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # AWAY TIME ZONE @@ -988,76 +1147,6 @@ def form_display_text_as_update(self): options=self.actions_list, mode='list')), }) -#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -# TRACKING PARAMETERS -#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -def form_tracking_parameters(self): - self.actions_list = ACTION_LIST_ITEMS_BASE.copy() - - self.picture_by_filename = {} - if PICTURE_WWW_STANDARD_DIRS in Gb.conf_profile[CONF_PICTURE_WWW_DIRS]: - Gb.conf_profile[CONF_PICTURE_WWW_DIRS] = [] - - return vol.Schema({ - vol.Required(CONF_DISTANCE_BETWEEN_DEVICES, - default=Gb.conf_general[CONF_DISTANCE_BETWEEN_DEVICES]): - selector.BooleanSelector(), - vol.Required(CONF_GPS_ACCURACY_THRESHOLD, - default=Gb.conf_general[CONF_GPS_ACCURACY_THRESHOLD]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=5, max=300, step=5, unit_of_measurement='m')), - vol.Required(CONF_OLD_LOCATION_THRESHOLD, - default=Gb.conf_general[CONF_OLD_LOCATION_THRESHOLD]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=1, max=60, step=1, unit_of_measurement='minutes')), - vol.Required(CONF_OLD_LOCATION_ADJUSTMENT, - default=Gb.conf_general[CONF_OLD_LOCATION_ADJUSTMENT]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=0, max=60, step=1, unit_of_measurement='minutes')), - vol.Required(CONF_MAX_INTERVAL, - default=Gb.conf_general[CONF_MAX_INTERVAL]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=15, max=480, step=5, unit_of_measurement='minutes')), - vol.Required(CONF_EXIT_ZONE_INTERVAL, - default=Gb.conf_general[CONF_EXIT_ZONE_INTERVAL]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=.5, max=10, step=.5, unit_of_measurement='minutes')), - vol.Required(CONF_MOBAPP_ALIVE_INTERVAL, - default=Gb.conf_general[CONF_MOBAPP_ALIVE_INTERVAL]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=15, max=240, step=5, unit_of_measurement='minutes')), - vol.Required(CONF_OFFLINE_INTERVAL, - default=Gb.conf_general[CONF_OFFLINE_INTERVAL]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=5, max=240, step=5, unit_of_measurement='minutes')), - vol.Required(CONF_TFZ_TRACKING_MAX_DISTANCE, - default=Gb.conf_general[CONF_TFZ_TRACKING_MAX_DISTANCE]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=1, max=100, unit_of_measurement='Km')), - vol.Optional(CONF_DISCARD_POOR_GPS_INZONE, - default=Gb.conf_general[CONF_DISCARD_POOR_GPS_INZONE]): - selector.BooleanSelector(), - vol.Required(CONF_TRAVEL_TIME_FACTOR, - default=self._option_parm_to_text(CONF_TRAVEL_TIME_FACTOR, TRAVEL_TIME_INTERVAL_MULTIPLIER_KEY_TEXT)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(TRAVEL_TIME_INTERVAL_MULTIPLIER_KEY_TEXT), mode='dropdown')), - vol.Required(CONF_PICTURE_WWW_DIRS, - default=Gb.conf_profile[CONF_PICTURE_WWW_DIRS] or self.www_directory_list): - cv.multi_select(six_item_list(self.www_directory_list)), - vol.Required(CONF_EVLOG_CARD_DIRECTORY, - default=self._parm_or_error_msg(CONF_EVLOG_CARD_DIRECTORY, conf_group=CF_PROFILE)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(self.www_directory_list), mode='dropdown')), - vol.Optional(CONF_EVLOG_BTNCONFIG_URL, - default=f"{self._parm_or_error_msg(CONF_EVLOG_BTNCONFIG_URL, conf_group=CF_PROFILE)} "): - selector.TextSelector(), - - vol.Required('action_items', - default=self.action_default_text('save')): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # INZONE INTERVALS @@ -1143,7 +1232,9 @@ def form_waze_main(self): options=self.actions_list, mode='list')), }) -#------------------------------------------------------------------------ +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# SPECIAL ZONES +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> def form_special_zones(self): self.actions_list = ACTION_LIST_ITEMS_BASE.copy() diff --git a/custom_components/icloud3/const.py b/custom_components/icloud3/const.py index 8f966d6..cb11d3f 100644 --- a/custom_components/icloud3/const.py +++ b/custom_components/icloud3/const.py @@ -12,8 +12,8 @@ # from homeassistant.const import (Platform) -VERSION = '3.1.4.4' -VERSION_BETA = '' +VERSION = '3.1.5' +VERSION_BETA = 'b3' #----------------------------------------- DOMAIN = 'icloud3' PLATFORMS = ['sensor', 'device_tracker'] @@ -294,7 +294,7 @@ def DEVICE_TYPE_FNAME(device_type): ↺↻⟲⟳⭯⭮↺↻⥀⥁↶↷⮌⮍⮎⮏⤻⤸⤾⤿⤺⤼⤽⤹🗘⮔⤶⤷⃕⟳↻🔄🔁➡️🔃⬇️🔗✳🞺🞴🞸🞳 ═ ⎯ — –ᗒ⋮… ⁃ » ━▶ ━➤🡺 —> > ❯↦ …⋯⋮ ⋱⋰🡪ᗕᗒ ᐳ ─🡢 ⎯ ━ ──ᗒ 🡢 ─ᐅ ↣ ➙ →《》◆◈◉● ⟷•⟛⚯⧟⫗' '᚛᚜ 〉〈 ⦒⦑ ⟩⟨ ⓧ≻≺ ⸩⸨ ▐‖ ▹▻◁─▷◅◃‖╠ᐅ🡆▶▐🡆▐▶‖➤▐➤➜➔❰❰❱❱ ⠤ … ² ⚯⟗⟐⥄⥵⧴⧕⫘⧉⯏≷≶≳≲≪≫⋘⋙ ∮∯ ❪❫❴❵❮❯❰❱ - ⣇⠈⠉⠋⠛⠟⠿⡿⣿ ⠗⠺ ⠿ ⸩⸨⯎⯌⯏⯍✧ 🙾 🙿 ⲶⲼ+≈⟣⟢⟡ + ⣇⠈⠉⠋⠛⠟⠿⡿⣿ ⠗⠺ ⠿ ⸩⸨⯎⯌⯏⯍✧ 🙾 🙿 ⲶⲼ+≈⟣⟢⟡⯌ ≽≼≽ ⋞⋟≺≻ ≪≫≾≿⋘⋙ ⋖⋗ https://www.fileformat.info/info/unicode/block/braille_patterns/utf8test.htm https://www.htmlsymbols.xyz/unit-symbols @@ -739,6 +739,7 @@ def DEVICE_TYPE_FNAME(device_type): CONF_IOSAPP_DEVICE = 'iosapp_device' CONF_MOBILE_APP_DEVICE = 'mobile_app_device' CONF_PICTURE = 'picture' +CONF_ICON = 'icon' CONF_TRACKING_MODE = 'tracking_mode' CONF_TRACK_FROM_BASE_ZONE_USED = 'track_from_base_zone_used' # Primary Zone a device is tracking from, normally Home CONF_TRACK_FROM_BASE_ZONE = 'track_from_base_zone' # Primary Zone a device is tracking from, normally Home @@ -872,7 +873,7 @@ def DEVICE_TYPE_FNAME(device_type): CONF_DEVICES: [], } -DEFAULT_APPLE_ACCOUNTS_CONF = { +DEFAULT_APPLE_ACCOUNT_CONF = { CONF_USERNAME: '', CONF_PASSWORD: '', CONF_TOTP_KEY: '', @@ -883,6 +884,7 @@ def DEVICE_TYPE_FNAME(device_type): CONF_IC3_DEVICENAME: ' ', CONF_FNAME: '', CONF_PICTURE: 'None', + CONF_ICON: 'mdi:account', CONF_UNIQUE_ID: '', CONF_DEVICE_TYPE: 'iPhone', CONF_INZONE_INTERVAL: 120, diff --git a/custom_components/icloud3/const_config_flow.py b/custom_components/icloud3/const_config_flow.py index f1f990d..1131005 100644 --- a/custom_components/icloud3/const_config_flow.py +++ b/custom_components/icloud3/const_config_flow.py @@ -9,25 +9,24 @@ 'Parameters Menu' ] MENU_KEY_TEXT = { - 'data_source': 'DATA SOURCES > APPLE ACCOUNT & MOBILE APP > Select Location Data Sources; Apple Account Username/Password', - 'device_list': 'ICLOUD3 DEVICES > Add, Change and Delete Tracked and Monitored Devices', + 'data_source': 'DATA SOURCES > APPLE ACCOUNT & MOBILE APP > Select Location Data Sources, Apple Account Username/Password', + 'device_list': 'ICLOUD3 DEVICES > Add, Change and Delete Tracked and Monitored Devices', 'verification_code': 'ENTER/REQUEST AN APPLE ACCOUNT VERIFICATION CODE > Enter or Request the 6-digit Apple Account Verification Code', 'away_time_zone': 'AWAY TIME ZONE > Select the displayed time zone for devices away from Home', 'change_device_order': 'CHANGE DEVICE ORDER > Change the Event Log Device display and tracking update sequence', - 'sensors': 'SENSORS > Set Sensors created by iCloud3; Exclude Specific Sensors from being created', - 'actions': 'ACTION COMMANDS > Restart/Pause/Resume Polling; Debug Logging; Export Event Log; Waze Utilities', - 'tools': 'TOOLS > Set Log Level, Reset Device Apple Acct config, Delete Apple Accts & Devices, Delete Apple Acct Cookie and iCloud3 Config files, Repair sensor ‘_2’ entity name error', + 'sensors': 'SENSORS > Set Sensors created by iCloud3, Exclude Specific Sensors from being created', + 'tools': 'TOOLS > Log Level, Delete Apple Acct & Device Assignment, Delete Apple Acct Cookie & iCloud3 Config files, Repair sensor ‘_2’ entity name errors, Restart HA/Reload iCloud3', - 'format_settings': 'FORMAT SETTINGS > Log Level; Zone Display Format; Device Tracker State; Unit of Measure; Time & Distance, Display GPS Coordinates', - 'display_text_as': 'DISPLAY TEXT AS > Event Log Text Replacement, etc', - 'waze': 'WAZE ROUTE DISTANCE, TIME & HISTORY > Route Server and Parameters; Waze History Database Parameters and Controls', - 'inzone_intervals': 'INZONE INTERVALS > inZone Interval assigned to new devices', - 'special_zones': 'SPECIAL ZONES > Enter Zone Delay Time; Stationary Zone; Primary Track-from-Home Zone Override', - 'tracking_parameters': 'TRACKING & OTHER PARAMETERS > Set Nearby Device Info, Accuracy Thresholds & Other Location Request Intervals; Picture Image Directories; Event Log Custom Card Directory', + 'tracking_parameters': 'TRACKING PARAMETERS > Nearby Device Info, Accuracy Thresholds & Other Location Request Intervals', + 'format_settings': 'FIELD FORMATS & EVENT LOG CONGUG OVERRIDES > Zone Display & Device Tracker State format, Unit of Measure/Time & Distance format, Picture Dir Filters', + 'display_text_as': 'DISPLAY TEXT AS > Event Log Text Replacement', + 'waze': 'WAZE ROUTE DISTANCE, TIME & HISTORY > Route Server and Parameters, Waze History Database Parameters and Controls', + 'special_zones': 'SPECIAL ZONES > Enter Zone Delay Time. Stationary Zone. Primary Track-from-Home Zone Override', + 'inzone_intervals': 'DEFAULT INZONE INTERVALS > inZone Interval assigned to new devices', 'select': 'SELECT > Select the parameter update form', - 'next_page_0': f'{MENU_PAGE_TITLE[0].upper()} > iCloud Account & Mobile App; iCloud3 Devices; Enter & Request Verification Code; Change Device Order; Sensors; Action Commands', - 'next_page_1': f'{MENU_PAGE_TITLE[1].upper()} > Format Parameters; Display Text As; Waze Route Distance, Time & History; inZone Intervals; Special Zones; Other Parameters', + 'next_page_0': f'{MENU_PAGE_TITLE[0].upper()} > iCloud Account & Mobile App, iCloud3 Devices, Enter & Request Verification Code; Change Device Order; Sensors; Action Commands', + 'next_page_1': f'{MENU_PAGE_TITLE[1].upper()} > Tracking Parameters, Field Formats & Directories, Display Text As, Waze Route Distance/Time & History, Special Zones, Default inZone Intervals', 'exit': f'EXIT AND RESTART/RELOAD ICLOUD3 (Current version is v{Gb.version})' } @@ -41,11 +40,11 @@ ] MENU_PAGE_1_INITIAL_ITEM = 0 MENU_KEY_TEXT_PAGE_1 = [ + MENU_KEY_TEXT['tracking_parameters'], MENU_KEY_TEXT['format_settings'], MENU_KEY_TEXT['display_text_as'], MENU_KEY_TEXT['waze'], MENU_KEY_TEXT['special_zones'], - MENU_KEY_TEXT['tracking_parameters'], MENU_KEY_TEXT['inzone_intervals'], ] MENU_ACTION_ITEMS = [ @@ -62,7 +61,7 @@ 'select_form': 'SELECT > Select the parameter update form', 'update_apple_acct': 'SELECT APPLE ACCOUNT > Update the Username/Password of the selected Apple Account, Add a new Apple Account, Remove the Apple Account', - 'save_log_into_apple_acct': 'LOG INTO APPLE ACCT, SAVE ANY CHANGES > Log into the Apple Account, Save any configuration changes', + 'save_log_into_apple_acct': 'SAVE CHANGES, LOG INTO APPLE ACCT > Save any configuration changes, Log into the Apple Account', 'log_into_apple_acct': 'LOG INTO APPLE ACCT > Log into the Apple Account, Save any configuration changes', 'stop_using_apple_acct': 'STOP USING AN APPLE ACCOUNT > Stop using an Apple Account, Remove it from the Apple Accounts list and all devices using it', 'verification_code': 'ENTER/REQUEST AN APPLE ACCOUNT VERIFICATION CODE > Enter (or Request) the 6-digit Apple Account Verification Code', @@ -104,7 +103,7 @@ 'cancel_goto_select_device': 'BACK TO DEVICE SELECTION > Return to the Device Selection screen. Cancel any unsaved changes', 'exit': 'EXIT > Exit the iCloud3 Configure Parameters Settings, Return to HA', - 'save': 'SAVE > Update Configuration File, Return to the Menu screen', + 'save': 'SAVE > Update Configuration File, Return to the Previous screen', 'save_stay': 'SAVE > Update Configuration File', 'confirm_action_yes': 'YES > Complete the requested action', @@ -218,6 +217,7 @@ 'cn': 'China - Use Apple iCloud Servers located in China' } MOBAPP_DEVICE_NONE_OPTIONS = {'None': 'None - The Mobile App is not installed on this device'} +PICTURE_NONE_KEY_TEXT = {'None': 'None - Display the Device’s Icon instead of a picture'} LOG_ZONES_KEY_TEXT = { 'name-zone': ' → [year]-[zone].csv', 'name-device': ' → [year]-[device].csv', @@ -403,6 +403,7 @@ "location (vacation house, second home, parent's house, etc.). This is a global setting " "that overrides the Primary Track-from-Home Zone assigned to an individual Device on the Update " "Devices screen.") +IC3_DIRECTORY_HEADER = ("Change the directory containing the Event Log Custom Card File (event-log-card.js). Set the `Gear` URL for the `HA Devices & Svcs > iCloud3 Config screen`") DATA_SOURCE_ICLOUD_HDR = ("APPLE ACCOUNT > Location data is provided by devices in the Family Sharing list") DATA_SOURCE_MOBAPP_HDR = ("HA MOBILE APP > Location data and zone Enter/Exit triggers are provided by the Mobile App") diff --git a/custom_components/icloud3/device.py b/custom_components/icloud3/device.py index 6f79fce..435a74e 100644 --- a/custom_components/icloud3/device.py +++ b/custom_components/icloud3/device.py @@ -18,7 +18,7 @@ TRACKING_NORMAL, TRACKING_PAUSED, TRACKING_RESUMED, LAST_CHANGED_SECS, LAST_CHANGED_TIME, LAST_UPDATED_SECS, LAST_UPDATED_TIME, STATE, - ICLOUD, MOBAPP, + ICLOUD, MOBAPP, DEVICE_TYPE_ICONS, TRACK_DEVICE, MONITOR_DEVICE, INACTIVE_DEVICE, TRACKING_MODE_FNAME, NAME, DEVICE_TYPE_FNAME, ICLOUD_HORIZONTAL_ACCURACY, ICLOUD_VERTICAL_ACCURACY, ICLOUD_BATTERY_STATUS, @@ -44,7 +44,7 @@ DEVICE_STATUS_CODES, DEVICE_STATUS_OFFLINE, DEVICE_STATUS_PENDING, CONF_TRACK_FROM_BASE_ZONE, CONF_TRACK_FROM_ZONES, CONF_LOG_ZONES, CONF_FNAME, FRIENDLY_NAME, PICTURE, ICON, BADGE, - CONF_PICTURE, CONF_STAT_ZONE_FNAME, + CONF_PICTURE, CONF_ICON, CONF_STAT_ZONE_FNAME, CONF_DEVICE_TYPE, CONF_RAW_MODEL, CONF_MODEL, CONF_MODEL_DISPLAY_NAME, CONF_APPLE_ACCOUNTS, CONF_APPLE_ACCOUNT, CONF_FAMSHR_DEVICENAME, CONF_USERNAME, CONF_MOBILE_APP_DEVICE, @@ -379,6 +379,7 @@ def initialize_sensors(self): self.sensors[DEVICE_TRACKER_STATE] = None self.sensors[NAME] = '' self.sensors[PICTURE] = '' + self.sensors[ICON] = 'mdi:account' self.sensors[BADGE] = '' self.sensors[LOW_POWER_MODE] = '' self.sensors[INFO] = '' @@ -567,13 +568,21 @@ def initialize_non_tracking_config_fields(self, conf_device): self.sensors['host_name'] = self.fname self.sensor_badge_attrs[FRIENDLY_NAME] = self.fname - self.sensor_badge_attrs[ICON] = 'mdi:account-circle-outline' + # self.sensor_badge_attrs[ICON] = conf_device.get(CONF_ICON, 'mdi:account-circle-outline') + self.sensor_badge_attrs[ICON] = conf_device.get(CONF_ICON, 'mdi:account') - if conf_device.get(CONF_PICTURE, 'None') != 'None': - picture = conf_device.get(CONF_PICTURE, 'None').replace('www/', '/local/') - self.sensors[PICTURE] = picture if instr(picture, '/') else (f"/local/{picture}") - self.sensor_badge_attrs[PICTURE] = self.sensors[PICTURE] + picture = conf_device.get(CONF_PICTURE, 'None') + if picture == 'None': + self.sensors[PICTURE] = '' + else: + picture = picture.replace('www/', '/local/') + if picture.startswith('/local') is False: + picture = f"/local/{picture}" + self.sensors[PICTURE] = picture + self.sensor_badge_attrs[PICTURE] = picture + + self.sensors[ICON] = conf_device.get(CONF_ICON, 'mdi:account') self.inzone_interval_secs = conf_device.get(CONF_INZONE_INTERVAL, 30) * 60 self.statzone_inzone_interval_secs = min(self.inzone_interval_secs, Gb.statzone_inzone_interval_secs) self.fixed_interval_secs = conf_device.get(CONF_FIXED_INTERVAL, 0) * 60 @@ -616,12 +625,6 @@ def _initialize_data_source_fields(self, conf_device): self.tracking_mode = TRACK_DEVICE else: self.primary_data_source = None - # if self.conf_icloud_dname: - # self.primary_data_source = ICLOUD - # elif self.mobapp[DEVICE_TRACKER]: - # self.primary_data_source = MOBAPP - # else: - # self.primary_data_source = None #-------------------------------------------------------------------- def initialize_track_from_zones(self): @@ -726,7 +729,7 @@ def _validate_zone_parameters(self): self.conf_device[CONF_TRACK_FROM_BASE_ZONE] = HOME if lza_zones or tfz_zones or tfbz_zone: - config_file.write_storage_icloud3_configuration_file() + config_file.write_icloud3_configuration_file() self.set_fname_alert(RED_ALERT) @@ -801,7 +804,7 @@ def remove_zone_from_settings(self, zone): self.conf_device[CONF_TRACK_FROM_BASE_ZONE] = HOME if conf_file_updated_flag: - config_file.write_storage_icloud3_configuration_file() + config_file.write_icloud3_configuration_file() except Exception as err: log_exception(err) @@ -1315,8 +1318,10 @@ def pause_tracking(self): try: self.tracking_status = TRACKING_PAUSED - self.write_ha_sensor_state(NEXT_UPDATE, PAUSED) - self.display_info_msg(PAUSED) + msg = f'{RED_ALERT}OFFLINE' if Gb.internet_connection_error else PAUSED + + self.write_ha_sensor_state(NEXT_UPDATE, msg) + self.display_info_msg(msg) except Exception as err: log_exception(err) @@ -1336,6 +1341,9 @@ def is_tracking_paused(self): def resume_tracking(self, interval_secs=0): ''' Resume tracking ''' try: + self.write_ha_sensor_state(NEXT_UPDATE, RESUMING) + self.display_info_msg(RESUMING) + self.tracking_status = TRACKING_RESUMED Gb.all_tracking_paused_flag = False Gb.any_device_was_updated_reason = '' @@ -1348,9 +1356,6 @@ def resume_tracking(self, interval_secs=0): self.reset_tracking_fields(interval_secs) - self.write_ha_sensor_state(NEXT_UPDATE, RESUMING) - self.display_info_msg(RESUMING) - except Exception as err: log_exception(err) @@ -1681,7 +1686,7 @@ def _update_restore_state_values(self): for from_zone, FromZone in self.FromZones_by_zone.items(): Gb.restore_state_devices[self.devicename]['from_zone'][from_zone] = copy.deepcopy(FromZone.sensors) - restore_state.write_storage_icloud3_restore_state_file() + restore_state.write_icloud3_restore_state_file() #-------------------------------------------------------------------- def _restore_state_save_mobapp_items(self): @@ -2457,8 +2462,14 @@ def format_info_msg(self): elif self.zone_change_secs > 0: if self.isin_zone: - info_msg +=( f"{zone_dname(self.loc_data_zone)}-" - f"{self.sensors[ARRIVAL_TIME]}, ") + info_msg +=f"At {zone_dname(self.loc_data_zone)}-" + if self.sensors[ARRIVAL_TIME].startswith('@'): + info_msg += f"(Since-{self.sensors[ARRIVAL_TIME][1:]}), " + elif self.sensors[ARRIVAL_TIME].startswith('~'): + info_msg += f"(About-{self.sensors[ARRIVAL_TIME][1:]}), " + else: + info_msg += f"(Before-{secs_to_hhmm(Gb.started_secs)}), " + elif self.mobapp_zone_exit_zone != '': info_msg +=(f"Left-{zone_dname(self.mobapp_zone_exit_zone)}-" f"{format_time_age(self.mobapp_zone_exit_secs)}, ") diff --git a/custom_components/icloud3/device_tracker.py b/custom_components/icloud3/device_tracker.py index 7ccbf27..fa77e45 100644 --- a/custom_components/icloud3/device_tracker.py +++ b/custom_components/icloud3/device_tracker.py @@ -7,7 +7,7 @@ DEVICE_TYPE_ICONS, BLANK_SENSOR_FIELD, DEVICE_TRACKER_STATE, INACTIVE_DEVICE, - NAME, FNAME, PICTURE, ALERT, + NAME, FNAME, PICTURE, ICON, ALERT, DEVICE_TRACKER, LATITUDE, LONGITUDE, GPS, LOCATION_SOURCE, TRIGGER, ZONE, ZONE_DATETIME, LAST_ZONE, FROM_ZONE, ZONE_FNAME, BATTERY, BATTERY_LEVEL, @@ -65,8 +65,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e try: if Gb.conf_file_data == {}: start_ic3.initialize_directory_filenames() - # config_file.load_storage_icloud3_configuration_file() - await config_file.async_load_storage_icloud3_configuration_file() + # config_file.load_icloud3_configuration_file() + await config_file.async_load_icloud3_configuration_file() try: Gb.conf_devicenames = [conf_device[CONF_IC3_DEVICENAME] @@ -330,7 +330,7 @@ def source_type(self): @property def icon(self): """Return an icon based on the type of the device.""" - return DEVICE_TYPE_ICONS.get(self.device_type, "mdi:cellphone") + return self._get_sensor_value(ICON) @property def extra_state_attributes(self): @@ -385,11 +385,14 @@ def _get_extra_attributes(self): extra_attrs[NAME] = self._get_sensor_value(NAME) picture = self._get_sensor_value(PICTURE) - if instr(picture,'None'): + if picture in ['', 'None', BLANK_SENSOR_FIELD]: extra_attrs['picture_file'] = 'None' + extra_attrs.pop(PICTURE, None) else: extra_attrs[PICTURE] = picture extra_attrs['picture_file'] = self._get_sensor_value(PICTURE) + + extra_attrs['icon_mdi:imagename'] = self._get_sensor_value(ICON) extra_attrs['track_from_zones'] = self.extra_attrs_track_from_zones extra_attrs['primary_home_zone'] = self.extra_attrs_primary_home_zone extra_attrs['away_time_zone_offset'] = self.extra_attrs_away_time_zone_offset @@ -408,7 +411,6 @@ def _get_extra_attributes(self): extra_attrs[WAZE_DISTANCE] = self._get_sensor_value(WAZE_DISTANCE) extra_attrs[DISTANCE_TO_DEVICES] = self._get_sensor_value(DISTANCE_TO_DEVICES) extra_attrs[ZONE_DATETIME] = self._get_sensor_value(ZONE_DATETIME) - #extra_attrs[LAST_UPDATE] = self._get_sensor_value(LAST_UPDATE_DATETIME) extra_attrs[NEXT_UPDATE] = self._get_sensor_value(NEXT_UPDATE_DATETIME) extra_attrs['last_timestamp']= f"{self._get_sensor_value(LAST_LOCATED_SECS)}" diff --git a/custom_components/icloud3/global_variables.py b/custom_components/icloud3/global_variables.py index b2255c1..d236927 100644 --- a/custom_components/icloud3/global_variables.py +++ b/custom_components/icloud3/global_variables.py @@ -94,18 +94,20 @@ class GlobalVariables(object): HALogger = None iC3Logger = None iC3Logger_last_check_exist_secs = 0 - log_file_filter_items = [] # items to be filtered from the log file (passwords, etc) - log_file_hide_items = [] # email extensions filter from apple accounts to remove in the icloud3-0.log file (messaging.py) prestartup_log = '' - disable_log_filter = False iC3EntityPlatform = None # iCloud3 Entity Platform (homeassistant.helpers.entity_component) PyiCloud = None # iCloud Account service PyiCloudLoggingInto = None # PyiCloud being set up that can be used if the login fails PyiCloud_needing_reauth_via_ha = {} # Reauth needed sent to ha for notify msg display PyiCloudValidateAppleAcct = None # A session that can be used to verify the username/password + PyiCloud_by_username = {} PyiCloudSession_by_username = {} # Session object for a username, set in Session so exists on an error + username_pyicloud_503_connection_error = [] # Session object for a username, set in Session so exists on an error + log_file_filter_items = [] # items to be filtered from the log file (passwords, etc) + log_file_hide_items = [] # email extensions filter from apple accounts to remove in the icloud3-0.log file (messaging.py) + disable_log_filter = False Waze = None WazeHist = None @@ -158,8 +160,15 @@ class GlobalVariables(object): owner_Devices_by_username = {} # List of Devices in the owner Apple Acct (excludes those in the iCloud list) username_valid_by_username = {} # The username/password validation status - internet_connection_error_secs = 0 # Time https connection error received + internet_connection_test = False # Raise ConnectionError in PyiCloud_session, set in service_handler - Show/Hide Tracking Monitors internet_connection_error = False # Set in PyiCloud_session from an http connection error. Shuts down all PyiCloud requests + internet_connection_error_secs = 0 # Time https connection error received + internet_connection_progress_cnt = 0 # Progress display counter + internet_connection_status_request_cnt = 0 # Recheck counter + internet_connection_status_request_secs = 0 + internet_connection_status_requested = False + + last_PyiCloud_request_secs = 0 # Last time a request was sent in PyIcloud, > 1-min ago = internet is down PyiCloud_by_devicename = {} # PyiCloud object for each ic3 devicename PyiCloud_by_username = {} # PyiCloud object for each Apple acct username PyiCloud_password_by_username = {} # Password for each Apple acct username diff --git a/custom_components/icloud3/helpers/common.py b/custom_components/icloud3/helpers/common.py index d407e28..f071237 100644 --- a/custom_components/icloud3/helpers/common.py +++ b/custom_components/icloud3/helpers/common.py @@ -325,7 +325,7 @@ def strip_lead_comma(text): #-------------------------------------------------------------------- def get_username_base(username): return f"{username}@".split('@')[0] - + #-------------------------------------------------------------------- def format_cnt(desc, n): return f", {desc}(#{n})" if n > 1 else '' @@ -389,7 +389,7 @@ def decode_password(password): or password.endswith('»»') is False)): password = password.replace('«', '').replace('»', '') Gb.conf_tracking[CONF_PASSWORD] = password - #write_storage_icloud3_configuration_file() + #write_icloud3_configuration_file() # Decode password if it is encoded and has the '««password»»' format if (password.startswith('««') or password.endswith('»»')): diff --git a/custom_components/icloud3/helpers/messaging.py b/custom_components/icloud3/helpers/messaging.py index eee4f31..bd61224 100644 --- a/custom_components/icloud3/helpers/messaging.py +++ b/custom_components/icloud3/helpers/messaging.py @@ -67,8 +67,8 @@ 'familyEligible', 'findme', 'requestInfo', 'invitationSentToEmail', 'invitationAcceptedByEmail', 'invitationFromHandles', 'invitationFromEmail', 'invitationAcceptedHandles', - 'items', 'userInfo', 'prsId', 'dsid', 'dsInfo', 'webservices', 'locations', - 'devices', 'content', 'followers', 'following', 'contactDetails', + 'items', 'userInfo', 'prsId', 'dsid', 'dsInfo', 'webservices', 'locations', 'dslang', + 'devices', 'content', 'followers', 'following', 'contactDetails', 'countryCode', 'dsWebAuthToken', 'accountCountryCode', 'extended_login', 'trustToken', 'trustTokens', 'data', 'json', 'headers', 'params', 'url', 'retry_cnt', 'retried', 'retry', '#', 'code', 'ok', 'method', 'securityCode', 'fmly', 'shouldLocate', 'selectedDevice', @@ -325,6 +325,8 @@ def open_ic3log_file(new_log_file=False): handler.addFilter(LoggerFilter) Gb.iC3Logger.addHandler(handler) + + if isnot_empty(Gb.conf_general): Gb.iC3Logger.propagate = (Gb.conf_general[CONF_LOG_LEVEL] == 'debug-ha') #-------------------------------------------------------------------- @@ -424,8 +426,8 @@ def write_config_file_to_ic3log(): conf_tracking_recd = Gb.conf_tracking.copy() # conf_tracking_recd[CONF_PASSWORD] = obscure_field(conf_tracking_recd[CONF_PASSWORD]) - conf_tracking_recd[CONF_APPLE_ACCOUNTS] = len(Gb.conf_apple_accounts) - conf_tracking_recd[CONF_DEVICES] = len(Gb.conf_devices) + # conf_tracking_recd[CONF_APPLE_ACCOUNTS] = len(Gb.conf_apple_accounts) + # conf_tracking_recd[CONF_DEVICES] = len(Gb.conf_devices) Gb.trace_prefix = '_INIT_' indent = SP(44) if Gb.log_debug_flag else SP(26) diff --git a/custom_components/icloud3/helpers/time_util.py b/custom_components/icloud3/helpers/time_util.py index 5bff7bb..da1a0d0 100644 --- a/custom_components/icloud3/helpers/time_util.py +++ b/custom_components/icloud3/helpers/time_util.py @@ -100,6 +100,7 @@ def isnot_valid(secs): def s2t(secs_utc): return secs_to_time(secs_utc) +#-------------------------------------------------------------------- def secs_to_time(secs_utc): ''' secs --> 10:23:45/h:mm:ssa ''' @@ -107,11 +108,42 @@ def secs_to_time(secs_utc): return time_to_12hrtime(time_local(secs_utc)) +#-------------------------------------------------------------------- def secs_to_datetime(secs_utc, format_ymd=False): ''' secs --> 2024-03-16 12:55:03 ''' return datetime_local(secs_utc) +#-------------------------------------------------------------------- +def secs_to_even_min_secs(secs_utc_or_min, min=None): + ''' + secs --> secs for next even min interval + + Parameters: + secs_utc_or_min - utc_secs to adjust from + min - adjustment minutes (+ or -) + or: + secs_utc_or_min - adjustment minutes (+ or -) from utc time now + min - Not specified + ''' + + if min is None: + secs_utc = int(time.time()) + min_secs = secs_utc_or_min * 60 + else: + secs_utc = secs_utc_or_min + min_secs = min * 60 + + if min_secs > 0: + prev_secs_adj = 0 + else: + prev_secs_adj = min_secs + if prev_secs_adj <= -3600: + prev_secs_adj -= 3600 + min_secs = abs(min_secs) + + return secs_utc - (secs_utc % min_secs) + min_secs + prev_secs_adj + #-------------------------------------------------------------------- def secs_to_hhmm(secs_utc): ''' secs --> hh:mm or hh:mma or hh:mmp ''' @@ -218,7 +250,7 @@ def format_timer(secs): try: if secs < 1: - return '0 min' + return '0 secs' if secs >= 86400: time_str = f"{secs/86400:.1f} days" @@ -292,7 +324,7 @@ def format_age_hrs(secs): return f"{format_timer_hrs(secs_since(secs))} ago" #-------------------------------------------------------------------- -def format_time_age(secs): +def format_time_age(secs, xago=None): ''' secs --> 10:23:45 or h:mm:ssa/p (4.5 sec/mins/hrs ago) ''' if isnot_valid(secs): return 'Unknown' @@ -304,8 +336,9 @@ def format_time_age(secs): else: return f"{age_secs/86400:.1f} days ago" + ago = ' ago' if xago is None else '' return (f"{secs_to_time(secs)} " - f"({format_timer(age_secs)} ago)") + f"({format_timer(age_secs)}{ago})") #-------------------------------------------------------------------- def format_secs_since(secs): diff --git a/custom_components/icloud3/icloud3_main.py b/custom_components/icloud3/icloud3_main.py index 1a40b9b..265e87a 100644 --- a/custom_components/icloud3/icloud3_main.py +++ b/custom_components/icloud3/icloud3_main.py @@ -27,7 +27,7 @@ from .const import (VERSION, VERSION_BETA, HOME, NOT_HOME, NOT_SET, HIGH_INTEGER, RARROW, LT, NBSP3, CLOCK_FACE, LINK, CRLF, DOT, LDOT2, CRLF_DOT, CRLF_LDOT, CRLF_HDOT, CRLF_X, NL, NL_DOT, - EVLOG_IC3_STAGE_HDR, + EVLOG_IC3_STAGE_HDR, RED_ALERT, ICLOUD, TRACKING_NORMAL, FNAME, CONF_USERNAME, CONF_PASSWORD, CONF_TOTP_KEY, IPHONE, IPAD, WATCH, AIRPODS, IPOD, ALERT, @@ -37,7 +37,7 @@ MOBAPP_UPDATE, ICLOUD_UPDATE, ARRIVAL_TIME, TOWARDS, AWAY_FROM, EVLOG_UPDATE_START, EVLOG_UPDATE_END, EVLOG_ERROR, EVLOG_ALERT, EVLOG_NOTICE, ICLOUD, MOBAPP, - ENTER_ZONE, EXIT_ZONE, INTERVAL, NEXT_UPDATE, + ENTER_ZONE, EXIT_ZONE, INTERVAL, NEXT_UPDATE, NOTIFY, CONF_LOG_LEVEL, STATZONE_RADIUS_1M, ) from .const_sensor import (SENSOR_LIST_DISTANCE, ) @@ -64,7 +64,7 @@ _evlog, _log, ) from .helpers.time_util import (time_now, time_now_secs, secs_to, secs_since, mins_since, secs_to_time, secs_to_hhmm, secs_to_datetime, - calculate_time_zone_offset, + calculate_time_zone_offset, secs_to_even_min_secs, format_timer, format_age, format_time_age, format_secs_since, ) from .helpers.dist_util import (km_to_um, m_to_um_ft, ) @@ -136,7 +136,7 @@ def start_icloud3(self): service_handler.issue_ha_notification() - self.start_timer = time_now_secs() + self.startup_secs = time_now_secs() self.initial_locate_complete_flag = False self.startup_log_msgs = '' self.startup_log_msgs_prefix = '' @@ -199,13 +199,16 @@ def _polling_loop_5_sec_device(self, ha_timer_secs): if Gb.config_parms_update_control != {''}: start_ic3.handle_config_parms_update() - # If the internet is down and ha was offline, chedk the internet connection after 20-mins. - # Restart iCloud3 if it was never started or - # Resume tracking if it was started and all tracked devices are available - if (Gb.internet_connection_error is False - and Gb.internet_connection_error_secs == 0): - pass - else: + # An internet request was made more than 1-minute ago, assume it is down + if Gb.last_PyiCloud_request_secs > 0 and secs_since(Gb.last_PyiCloud_request_secs) > 60: + Gb.internet_connection_error = True + + # if (Gb.internet_connection_error is False + # and Gb.internet_connection_error_secs == 0): + # pass + # else: + if (Gb.internet_connection_error + or Gb.internet_connection_error_secs > 0): self._handle_internet_connection_error() # Restart iCloud via service call from EvLog or config_flow @@ -237,14 +240,6 @@ def _polling_loop_5_sec_device(self, ha_timer_secs): if Gb.evlog_action_request == '': pass - # elif Gb.evlog_action_request == CMD_RESET_PYICLOUD_SESSION: - # post_event(f"{EVLOG_ERROR}The `Action > Request Apple Verification Code` " - # f"is no longer available. This must be done using the " - # f"`Configuration > Enter/Request An Apple Account Verification " - # f"Code` screen") - # pyicloud_ic3_interface.pyicloud_reset_session() - # Gb.evlog_action_request = '' - try: #<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>> # CHECK TIMERS @@ -470,7 +465,6 @@ def _main_5sec_loop_update_tracked_devices_icloud(self, Device): ''' if (Device.PyiCloud is None): - # or Gb.internet_connection_error): return Gb.trace_prefix = 'ICLOUD' @@ -1192,32 +1186,25 @@ def _display_device_alert_evlog_greenbar_msg(self): or Gb.WazeHist.wazehist_recalculate_time_dist_running_flag): return - # post_evlog_greenbar_msg('Test Alert Message Display') - # return - general_alert_msg = startup_alert_attr = '' tracked_alert_attr = monitored_alert_attr = '' + if (Gb.internet_connection_error + or Gb.internet_connection_error_secs > 0): + return + if Gb.version_hacs: general_alert_msg += ( f"{CRLF_LDOT}iCloud3 {Gb.version_hacs} is available on HACS, " f"you are running v{Gb.version}") + if Gb.disable_log_filter: + general_alert_msg += f"{RED_ALERT}PASSWORD LOG FILTER DISABLED (Gb.disable_log_filter)" + if (Gb.startup_alerts # and is_empty(Gb.username_pyicloud_503_connection_error) and is_empty(Gb.usernames_setup_error_retry_list)): Gb.startup_alerts = [] - # if isnot_empty(Gb.username_pyicloud_503_connection_error): - # username_base = [get_username_base(username) - # for username in Gb.username_pyicloud_503_connection_error] - # general_alert_msg += (f"{CRLF_LDOT}Apple Login Failed > AutoRetry-" - # f"{list_to_str(username_base)}") - if Gb.internet_connection_error: - usernames_base = [username.split('@')[0] for username in Gb.conf_usernames] - general_alert_msg += ( f"{CRLF_LDOT}HOME ASST SERVER IS OFFLINE > Apple Acct not available:" - f"{CRLF_HDOT}{list_to_str(usernames_base)} > " - f"Retry at {secs_to_time(Gb.internet_connection_error_secs + 1200)}") - if isnot_empty(Gb.startup_alerts): startup_alert_attr = Gb.startup_alerts_str general_alert_msg += f"{CRLF_LDOT}Alerts starting iCloud3{RARROW}Review Event Log for more info" @@ -1348,54 +1335,113 @@ def _post_after_update_monitor_msg(self, Device): replace('%tage', Device.loc_data_time_age) post_monitor_msg(Device.devicename, device_monitor_msg) -#-------------------------------------------------------------------- +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# Internet Connection Error Handler +# +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> def _handle_internet_connection_error(self): ''' - Handle a internet connection issues + Handle all internet connection issues error time(secs) meaning True = 0 Internet just went down - pause tracking - True > 0 Age > 20-mins --> Resume trackn to se if internt is still down - True > 0 See if a mobapp update time > iternet down time, True means - the internet is back up, resume tracking False > 0 Internet is back up, resume tracking ''' - + # Internet just went down. Pause tracking and set timer if Gb.internet_connection_error_secs == 0: - Gb.internet_connection_error_secs = Gb.this_update_secs + Gb.internet_connection_error_secs = time_now_secs() + Gb.internet_connection_status_waiting_for_response = False + Gb.internet_connection_status_request_cnt = 0 + self._internet_connection_status_msg() + for Device in Gb.Devices: Device.pause_tracking() + post_event(f"{EVLOG_ALERT}HomeAsst Offline > Pause all tracking") + + # If the Mobile App is set up, send a message to the 1st Device that can use + # the notify service + if isnot_empty(Gb.mobapp_id_by_mobapp_dname): + Devices = [Device for Device in Gb.Devices + if Device.is_tracked and Device.mobapp[NOTIFY] != ''] + if isnot_empty(Devices): + message = {"message": "Home Asst Server is Offline due to an Internet " + f"Connection Error, {secs_to_time(time_now_secs())} " + "(iCloud3)"} + mobapp_interface.send_message_to_device(Devices[0], message) + return - if Gb.this_update_secs <= Gb.internet_connection_error_secs + 1200: - for Device in Gb.Devices: - if mobapp_data_handler.new_mobapp_data_data_available( - Device, Gb.internet_connection_error_secs): - Gb.internet_connection_error_secs = -1 - # post_event("MobApp location update received, Home Asst is online") - break + self._internet_connection_status_msg() - if ((Gb.internet_connection_error is False - and Gb.internet_connection_error_secs > 0) - or Gb.this_update_secs > Gb.internet_connection_error_secs + 1200): - if Gb.internet_connection_error_secs == -1: - post_event(f"{EVLOG_ALERT}HomeAsst Online > Resume tracking") - else: - post_event(f"{EVLOG_ALERT}HomeAsst Offline > Resume tracking, Check status") - Gb.internet_connection_error = False - Gb.internet_connection_error_secs = 0 + if Gb.internet_connection_error is False: + self.reset_internet_connection_error() + return - devices_not_setup = [Device.devicename for Device in Gb.Devices - if (Device.verified_flag - and Device.dev_data_source == NOT_SET)] + if Gb.internet_connection_status_request_cnt == 0: + pass + elif Gb.this_update_time[-2:] not in ['00' ,'15', '30', '45']: + return - if is_empty(devices_not_setup): - for Device in Gb.Devices: - Device.resume_tracking() - else: - Gb.restart_icloud3_request_flag = True + # See if internet is back up + # Gb.internet_connection_status_request_cnt += 1 + # Gb.internet_connection_status_request_secs = time_now_secs() + + is_internet_available = Gb.PyiCloudValidateAppleAcct.is_internet_available() + if is_internet_available: + self.reset_internet_connection_error() +#............................................................................... + @staticmethod + def reset_internet_connection_error(): + Gb.internet_connection_error = False + Gb.internet_connection_error_secs = 0 + Gb.internet_connection_status_request_cnt = 0 + Gb.internet_connection_progress_cnt = 0 + + data_source_not_set_Devices = [Device + for Device in Gb.Devices + if (Device.dev_data_source == NOT_SET)] + + # If no connection during startup, restart. Otherwiae, resume all tracking + #if isnot_empty(devices_not_setup): + notify_Device = None + if isnot_empty(data_source_not_set_Devices): + post_event(f"{EVLOG_ALERT}HomeAsst Back Online > Restarting iCloud3") + Gb.restart_icloud3_request_flag = True + else: + post_event(f"{EVLOG_ALERT}HomeAsst Back Online > Resume tracking") + for Device in Gb.Devices: + Device.resume_tracking() + if (notify_Device is None + and Device.mobapp[NOTIFY] != ''): + notify_Device = Device + + # If the Mobile App is set up, send a message to the 1st Device that can use + # the notify service + if notify_Device: + message = {"message": "Home Asst Server is back Online, " + f"{secs_to_time(time_now_secs())} (iCloud3)"} + mobapp_interface.send_message_to_device(notify_Device, message) + +#............................................................................... + def _internet_connection_status_msg(self): + ''' + Display the offline message. Show a progress bar that refreshes on 5-sec + interval while checking the status + ''' + if Gb.internet_connection_progress_cnt > 11: + Gb.internet_connection_progress_cnt = 1 + else: + Gb.internet_connection_progress_cnt += 1 + progress_bar = '🟡'*Gb.internet_connection_progress_cnt + evlog_msg =(f"HOME ASST SERVER IS OFFLINE > Since " + f"{format_time_age(Gb.internet_connection_error_secs, xago=True)}" + f"{CRLF}Checking-{secs_to_time(Gb.internet_connection_status_request_secs)} " + f"(#{Gb.internet_connection_status_request_cnt}) " + f"{progress_bar}") + post_evlog_greenbar_msg(evlog_msg) #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -1537,9 +1583,7 @@ def _display_secs_to_next_update_info_msg(self, Device): next_update_hhmmss = Device.sensors[NEXT_UPDATE] Device.sensors[NEXT_UPDATE] = f"{age_secs} secs" - Device.write_ha_sensors_state([NEXT_UPDATE]) - Device.sensors[NEXT_UPDATE] = next_update_hhmmss except Exception as err: log_exception(err) diff --git a/custom_components/icloud3/manifest.json b/custom_components/icloud3/manifest.json index 0eaf442..0b87e82 100644 --- a/custom_components/icloud3/manifest.json +++ b/custom_components/icloud3/manifest.json @@ -10,5 +10,5 @@ "issue_tracker": "https://github.com/gcobb321/icloud3/issues", "loggers": ["icloud3"], "requirements": ["srp"], - "version": "3.1.4.4" + "version": "3.1.5" } diff --git a/custom_components/icloud3/sensor.py b/custom_components/icloud3/sensor.py index df33ff3..5c370d8 100644 --- a/custom_components/icloud3/sensor.py +++ b/custom_components/icloud3/sensor.py @@ -24,7 +24,7 @@ TRACK_DEVICE, MONITOR_DEVICE, INACTIVE_DEVICE, NAME, FNAME, BADGE, FROM_ZONE, ZONE, ZONE_DISTANCE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, - HOME_DISTANCE, + HOME_DISTANCE, ICON, BATTERY, BATTERY_STATUS, WAZE_DISTANCE, WAZE_DISTANCE_ATTR, WAZE_METHOD, WAZE_USED, CALC_DISTANCE, CALC_DISTANCE_ATTR, @@ -80,8 +80,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e try: if Gb.conf_file_data == {}: start_ic3.initialize_directory_filenames() - # start_ic3.load_storage_icloud3_configuration_file() - await config_file.async_load_storage_icloud3_configuration_file() + # start_ic3.load_icloud3_configuration_file() + await config_file.async_load_icloud3_configuration_file() NewSensors = [] @@ -907,6 +907,10 @@ def extra_state_attributes(self): else: return None + @property + def icon(self): + return self._get_sensor_value(ICON) + #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> class Sensor_ZoneInfo(DeviceSensor_Base, SensorEntity): diff --git a/custom_components/icloud3/strings.json b/custom_components/icloud3/strings.json index 8f2a743..5eb4feb 100644 --- a/custom_components/icloud3/strings.json +++ b/custom_components/icloud3/strings.json @@ -94,6 +94,7 @@ "icloud_acct_no_data_source": "❌ No Data Source has been selected (Apple iCloud Account or Mobile App)", "ic3_icloud_same_name": "iCloud3 dev_trkr.entity_id and name on the device (Settings > General > About) can not be exactly the same (letters & case)", "mobile_app_error": "Error, The Mobile App Integration is not installed. The Mobile App will not be used as a data source; location data and zone enter/exit triggers will not be monitored", + "password_asp_invalid": "Apple does not support App Specific Passwords in WebAuth applications", "verification_code_requested": "The Apple Account Verification Code was requested, BROWSER REFRESH MAY BE NEEDED", "verification_code_requested2": "The Apple Account Verification Code was requested", @@ -133,6 +134,9 @@ "unknown_icloud_mobapp_picture": "Check the iCloud, Mobile App and Picture parameter values (Not found or Invalid)", "unknown_icloud_picture": "Check the iCloud and Picture parameter values (Not found or Invalid)", + "internet_connection_err": "Home Asst is Offline, No Internet Connection is Available", + "apple_acct_connection_err": "Internet is available but could not connect to Apple. Try again in a few minutes", + "tfz_selection_invalid": "The value must be a zone that is being tracked from", "time_factor_invalid_range": "The 'Travel Time Multiplier' must be between .1 and .9", "fixed_interval_invalid_range": "The 'Fixed Interval' must be 0 (not used) or >= 3 (> 5 recommended)", @@ -280,6 +284,7 @@ "famshr_devicename": "APPLE ACCOUNT iCLOUD DEVICE - Apple iCloud device providing location data", "mobile_app_device": "MOBILE APP DEVICE_TRACKER ENTITY - Mobile App device providing location data & zone triggers", "picture": "PICTURE - Image of the person normally using this device (44x44 pixels is a good size)", + "icon": "ICON - Icon to display when the Picture has not been selected (=None)", "device_type": "DEVICE TYPE - iPhone, iPad, Watch, etc.", "tracking_mode": "TRACKING MODE - Location request method (Tracked, Monitored, Inactive)", "mobapp": "MOBILE APP INSTALLED - HA Mobile App is installed on this device", @@ -298,6 +303,7 @@ "famshr_devicename": "APPLE ACCOUNT iCLOUD DEVICE - Apple iCloud device providing location data", "mobile_app_device": "MOBILE APP DEVICE_TRACKER ENTITY - Mobile App device providing location data & zone triggers", "picture": "PICTURE - Image of the person normally using this device (44x44 pixels is a good size)", + "icon": "ICON - Icon to display when the Picture has not been selected (=None)", "device_type": "DEVICE TYPE - iPhone, iPad, Watch, etc", "tracking_mode": "TRACKING MODE - How location requests should be done (Full tracking, Monitor, Inactive)", "inzone_interval": "INZONE INTERVAL", @@ -337,6 +343,18 @@ "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, + "picture_dir_filter": { + "title": "Set up Picture Directory Filter", + "description": "Select the \\www directorys to be searched for image (png, jpg) files", + "data": { + "www_group_1": "Group 1 - Directory 1-5", + "www_group_2": "Group 2 - Directory 6-10", + "www_group_3": "Group 3 - Directory 11-15", + "www_group_4": "Group 4 - Directory 16-20", + "www_group_5": "Group 5 - Directory 21-25", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" + } + }, "away_time_zone": { "title": "Display Location Time Zone when Away", "description": "The time displayed in the Event Log and Sensors show the time an event took place using the Home 'time zone' from your Home Assistant computer. When you are away from Home and in another time zone, your tracking events are still based on the time at your Home 'time zone', not time in your current location.\n\nThis screen lets you display time events using your current location's time zone.", @@ -364,23 +382,56 @@ "data_description": { } }, + "tracking_parameters": { + "title": "Tracking Parameters", + "description": "Many parameters are used when tracking a device and displaying the results. These include how poor GPS Accuracy should be handled, various polling interval values and zone distances used to set the time to locate the device. They are customized on this screen.", + "data": { + "gps_accuracy_threshold": "GPS ACCURACY THRESHOLD", + "old_location_threshold": "OLD LOCATION THRESHOLD", + "old_location_adjustment": "OLD LOCATION ADJUSTMENT", + "distance_between_devices": "USE LOCATION RESULTS FROM A NEAR-BY DEVICE", + "max_interval": "MAXIMUM INTERVAL", + "exit_zone_interval": "EXIT ZONE INTERVAL", + "mobapp_alive_interval": "REQUEST MOBILE APP LOCATION INTERVAL", + "tfz_tracking_max_distance": "TRACK-FROM-ZONE DISPLAY DISTANCE", + "offline_interval": "DEVICE OFFLINE INTERVAL", + "travel_time_factor": "TRAVEL TIME INTERVAL AND NEXT LOCATION UPDATE MULTIPLIER", + "discard_poor_gps_inzone": "DISCARD POOR RESULTS - Discard Location Updates with Poor GPS Accuracy when in a Zone", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" + }, + "data_description": { + "old_location_threshold": "Locations older than this value will be discarded", + "old_location_adjustment": "Add this to the time that determines if a location is old", + "distance_between_devices": "When tracking results are updated, any nearby devices are identified. When tracking results for those devices are updated, the tracking results of the one originally updated can be used instead. This improves performance since the Waze route time and distance are not requested again", + "gps_accuracy_threshold": "Locations with GPS Accuracy above this value will be discarded", + "tfz_tracking_max_distance": "Normally the Home zone's time and distance data is displayed on the Device's device_tracker and sensor entities. Display the Track-from-Zone instead when the Device is within this distance of the Track-from-Zone", + "max_interval": "The maximum time between location requests", + "exit_zone_interval": "The time to the first location request after exiting a zone", + "mobapp_alive_interval": "Send a location request to the Mobile App if there has been no contact after this amount of time. This will check to see if the Mobile App is responding to location requests or is asleep and not running.", + "offline_interval": "Location request interval when offline (Airplane mode, dead cell area, etc.)", + "travel_time_factor": "This is used to calculate the Interval and Next Location Time when going towards Home. A smaller value will reduce the interval time and increase the location requests, a larger value will increase the interval and reduce the location requests." + } + }, "format_settings": { - "title": "Format Settings", + "title": "FIELD FORMATS & EVENT LOG OVERRIDES", "description": "Tracking activity, results and information messages are displayed in the Event log, sensors and device_tracker entities for tracked and monitored devices.\n\nThis screen us used to specify how these results should be displayed.", "data": { - "log_level": "LOG LEVEL - The type of messages that are written to the iCloud3 Log file (icloud3.log}", - "log_level_devices": "RAWDATA LOG DEVICE FILTER - Write iCloud RawData to the log file for only these devices", "display_zone_format": "EVENT LOG ZONE DISPLAY NAME - How the Zone name is displayed in sensors and the Event Log", "device_tracker_state_source": "DEVICE TRACKER STATE VALUE - How the device's device_tracker entity state value is determined", "time_format": "TIME FORMAT - How time fields are displayed in sensors and in the Event Log", "unit_of_measurement": "UNIT OF MEASUREMENT - How distance fields are displayed in sensors and in the Event Log", "display_gps_lat_long2": "DISPLAY GPS COORDINATES - Display the GPS (Latitude, Longitude/±Accuracy) or only the GPS (/±Accuracy) in the Event Log", "display_gps_lat_long": "DISPLAY GPS COORDINATES - Display GPS-(22.32771, -76.33073/±35m) instead of GPS-/±35m in the Event Log", + "evlog_header": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ EVENT LOG SYSTEM OVERRIDES", + "picture_www_dirs": "PICTURE DIRECTORY FILTER - Select the directories containg Device image files (.png, .jpg)", + "event_log_card_directory": "EVENT LOG CARD LOVELACE RESOURCES DIRECTORY - Event Log custom card .js file directory", + "event_log_btnconfig_url": "EVENT LOG CONFIGURE BUTTON (GEAR) URL > Special URL that display's the HA Configure Settings screen", "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { - "log_level": "iCloud3 can log configuration parameters, startup activity and errors, tracking activity, error messages and the Apple Account requests for iCloud Device location RawData information (request and response). Log levels specify the type of records that should be written to the iCloud3 log file (`icloud3-0.log`) from basic (Info) to more detailed (Debug) to extremely detailed (RawData).", - "device_tracker_state_source": "HA uses the device's gps coordinates to determine the zone. The gps accuracy is not considered so the zone may be exited when the gps wanders out of the zone. iCloud3 does consider the gps accuracy and will not exit the zone when this occurs. iCloud3 will display the Zone's Friendly Name or zone name displayed on the Event Log." + "picture_www_dirs": "iCloud3 scans the /www for all picture files (.png, .jpg) to build a table for selecting the picture for a device. The picture can be hard to find if you have many image files in many directories. Specify the directories with your device image files here", + "device_tracker_state_source": "HA uses the device's gps coordinates to determine the zone. The gps accuracy is not considered so the zone may be exited when the gps wanders out of the zone. iCloud3 does consider the gps accuracy and will not exit the zone when this occurs. iCloud3 will display the Zone's Friendly Name or zone name displayed on the Event Log", + "event_log_btnconfig_url": "Normally, this is blank and iCloud3 will determine the URL for it's Configure Settings screen. However, if there is a problem caused by running HA in a virtual environment, docker or on another device and the actual URL can not be detemined, a 404 not found error may be encountered. If that happens, select it the normal way (HA Devices & Services > Integration > iCloud3 > Configure Settings gear) and copy the URL from the browser into this field." } }, "display_text_as": { @@ -401,15 +452,6 @@ "data_description": { } }, - "actions": { - "title": "iCloud3 Action Commands", - "data": { - "ic3_actions": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ICLOUD3 GENERAL CONTROL ACTIONS", - "debug_actions": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ DEBUG LOG ACTIONS", - "other_actions": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ OTHER ACTIONS", - "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" - } - }, "inzone_intervals": { "title": "inZone Parameters and Default Intervals", "description": "An inZone interval is the time between location requests when the Device is in a zone.\n\n This screen is used to set the default values for different types of devices. This value is assigned to a device when it is added.\n\nNote: The inZone Interval can be set to a different value on the Update Device screen for each device.", @@ -495,41 +537,6 @@ "filtered_sensors": "ICLOUD3 SENSORS - A list of Sensors that are created when iCloud3 starts", "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } - }, - "tracking_parameters": { - "title": "Tracking & Other Parameters", - "description": "Some parameters do not fall into any of the other general categories and are rarely changed.\n\nThis screen is used to specify those parameters.", - "data": { - "log_level": "LOG LEVEL - The type of messages that are added to the HA log file during iCloud3 operations", - "gps_accuracy_threshold": "GPS ACCURACY THRESHOLD", - "old_location_threshold": "OLD LOCATION THRESHOLD", - "old_location_adjustment": "OLD LOCATION ADJUSTMENT", - "distance_between_devices": "USE LOCATION RESULTS FROM A NEAR-BY DEVICE", - "max_interval": "MAXIMUM INTERVAL", - "exit_zone_interval": "EXIT ZONE INTERVAL", - "mobapp_alive_interval": "REQUEST MOBILE APP LOCATION INTERVAL", - "tfz_tracking_max_distance": "TRACK-FROM-ZONE DISPLAY DISTANCE", - "offline_interval": "DEVICE OFFLINE INTERVAL", - "travel_time_factor": "TRAVEL TIME INTERVAL AND NEXT LOCATION UPDATE MULTIPLIER", - "discard_poor_gps_inzone": "DISCARD POOR RESULTS - Discard Location Updates with Poor GPS Accuracy when in a Zone", - "picture_www_dirs": "WWW DIRECTORIES WITH PICTURE IMAGES - Filter for `Update Devices > Pictures` file locations", - "event_log_card_directory": "EVENT LOG CARD LOVELACE RESOURCES DIRECTORY - Event Log custom card .js file directory", - "event_log_btnconfig_url": "EVENT LOG CONFIGURE BUTTON (GEAR) URL > Special URL that display's the HA Configure Settings screen", - "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" - }, - "data_description": { - "old_location_threshold": "Locations older than this value will be discarded", - "old_location_adjustment": "Add this to the time that determines if a location is old", - "distance_between_devices": "When tracking results are updated, any nearby devices are identified. When tracking results for those devices are updated, the tracking results of the one originally updated can be used instead. This improves performance since the Waze route time and distance are not requested again", - "gps_accuracy_threshold": "Locations with GPS Accuracy above this value will be discarded", - "tfz_tracking_max_distance": "Normally the Home zone's time and distance data is displayed on the Device's device_tracker and sensor entities. Display the Track-from-Zone instead when the Device is within this distance of the Track-from-Zone", - "max_interval": "The maximum time between location requests", - "exit_zone_interval": "The time to the first location request after exiting a zone", - "mobapp_alive_interval": "Send a location request to the Mobile App if there has been no contact after this amount of time. This will check to see if the Mobile App is responding to location requests or is asleep and not running.", - "offline_interval": "Location request interval when offline (Airplane mode, dead cell area, etc.)", - "travel_time_factor": "This is used to calculate the Interval and Next Location Time when going towards Home. A smaller value will reduce the interval time and increase the location requests, a larger value will increase the interval and reduce the location requests.", - "event_log_btnconfig_url": "Normally, this is blank and iCloud3 will determine the URL for it's Configure Settings screen. However, if there is a problem caused by running HA in a virtual environment, docker or on another device and the actual URL can not be detemined, a 404 not found error may be encountered. If that happens, select it the normal way (HA Devices & Services > Integration > iCloud3 > Configure Settings gear) and copy the URL from the browser into this field." - } } } }, diff --git a/custom_components/icloud3/support/config_file.py b/custom_components/icloud3/support/config_file.py index c595e73..db0651c 100644 --- a/custom_components/icloud3/support/config_file.py +++ b/custom_components/icloud3/support/config_file.py @@ -26,7 +26,7 @@ DEFAULT_SENSORS_CONF, DEFAULT_DATA_CONF, RANGE_DEVICE_CONF, RANGE_GENERAL_CONF, MIN, MAX, STEP, RANGE_UM, CF_PROFILE, CF_DATA, CF_TRACKING, CF_GENERAL, CF_SENSORS, - CONF_DEVICES, CONF_APPLE_ACCOUNTS, DEFAULT_APPLE_ACCOUNTS_CONF, + CONF_DEVICES, CONF_APPLE_ACCOUNTS, DEFAULT_APPLE_ACCOUNT_CONF, IC3LOG_FILENAME, ) @@ -34,7 +34,8 @@ from ..support import waze from ..helpers import file_io from ..helpers.common import (instr, ordereddict_to_dict, isbetween, list_add, is_empty, ) -from ..helpers.messaging import (log_exception, _evlog, _log, log_info_msg, add_log_file_filter, ) +from ..helpers.messaging import (log_exception, _evlog, _log, log_info_msg, add_log_file_filter, + open_ic3log_file_init, ) from ..helpers.time_util import (datetime_now, ) import os @@ -51,10 +52,10 @@ # configuration file I/O # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -async def async_load_storage_icloud3_configuration_file(): - return await Gb.hass.async_add_executor_job(load_storage_icloud3_configuration_file) +async def async_load_icloud3_configuration_file(): + return await Gb.hass.async_add_executor_job(load_icloud3_configuration_file) -def load_storage_icloud3_configuration_file(): +def load_icloud3_configuration_file(): # Make the .storage/icloud3 directory if it does not exist file_io.make_directory(Gb.ha_storage_icloud3) @@ -63,30 +64,23 @@ def load_storage_icloud3_configuration_file(): _LOGGER.info(f"Creating Configuration File-{Gb.icloud3_config_filename}") initialize_icloud3_configuration_file() - # Gb.conf_file_data = CF_DEFAULT_IC3_CONF_FILE.copy() - # build_initial_config_file_structure() - # write_storage_icloud3_configuration_file() - success = read_storage_icloud3_configuration_file() + success = read_icloud3_configuration_file() if success: - write_storage_icloud3_configuration_file('_backup') + write_icloud3_configuration_file('_backup') else: _restore_config_file_from_backup() - _add_parms_and_check_config_file() _count_device_tracking_methods_configured() - if CONF_LOG_LEVEL in Gb.conf_general: - start_ic3.set_log_level(Gb.conf_general[CONF_LOG_LEVEL]) - Gb.www_evlog_js_directory = Gb.conf_profile[CONF_EVLOG_CARD_DIRECTORY] Gb.www_evlog_js_filename = f"{Gb.www_evlog_js_directory}/{Gb.conf_profile[CONF_EVLOG_CARD_PROGRAM]}" return #------------------------------------------------------------------------------------------- -def read_storage_icloud3_configuration_file(filename_suffix=''): +def read_icloud3_configuration_file(filename_suffix=''): ''' Read the config/.storage/.icloud3.configuration file and extract the data into the Global Variables @@ -111,8 +105,8 @@ def read_storage_icloud3_configuration_file(filename_suffix=''): Gb.conf_devices = Gb.conf_tracking.get(CONF_DEVICES, []) Gb.conf_general = Gb.conf_data[CF_GENERAL] Gb.conf_sensors = Gb.conf_data[CF_SENSORS] - Gb.log_level = Gb.conf_general[CONF_LOG_LEVEL] + Gb.log_level = Gb.conf_general[CONF_LOG_LEVEL] _add_parms_and_check_config_file() return True @@ -123,7 +117,7 @@ def read_storage_icloud3_configuration_file(filename_suffix=''): return False #-------------------------------------------------------------------- -def write_storage_icloud3_configuration_file(filename_suffix=None): +def write_icloud3_configuration_file(filename_suffix=None): ''' Update the config/.storage/.icloud3.configuration file @@ -150,7 +144,7 @@ def write_storage_icloud3_configuration_file(filename_suffix=None): return success #-------------------------------------------------------------------- -async def async_write_storage_icloud3_configuration_file(filename_suffix=None): +async def async_write_icloud3_configuration_file(filename_suffix=None): ''' Update the config/.storage/.icloud3.configuration file @@ -216,13 +210,13 @@ def _restore_config_file_from_backup(): f"Will restore from `configuration_backup` file") _LOGGER.warning(log_msg) file_io.rename_file(Gb.icloud3_config_filename, json_errors_filename) - success = read_storage_icloud3_configuration_file('_backup') + success = read_icloud3_configuration_file('_backup') if success: log_msg = ("Restore from backup configuration file was successful") _LOGGER.warning(log_msg) - write_storage_icloud3_configuration_file() + write_icloud3_configuration_file() else: _LOGGER.error(f"iCloud3{RARROW}Restore from backup configuration file failed") @@ -234,8 +228,8 @@ def _restore_config_file_from_backup(): def initialize_icloud3_configuration_file(): build_initial_config_file_structure() Gb.conf_file_data = CF_DEFAULT_IC3_CONF_FILE.copy() - write_storage_icloud3_configuration_file() - read_storage_icloud3_configuration_file() + write_icloud3_configuration_file() + read_icloud3_configuration_file() #-------------------------------------------------------------------- def _add_parms_and_check_config_file(): @@ -247,10 +241,12 @@ def _add_parms_and_check_config_file(): update_config_file_flag = _update_tracking_parameters() or update_config_file_flag update_config_file_flag = _update_device_parameters() or update_config_file_flag update_config_file_flag = _update_general_parameters() or update_config_file_flag - update_config_file_flag = _config_file_check_range_values() or update_config_file_flag - update_config_file_flag = _config_file_check_device_settings() or update_config_file_flag + + update_config_file_flag = _verify_general_parameter_values() or update_config_file_flag + update_config_file_flag = _verify_tracking_parameters_values() or update_config_file_flag + update_config_file_flag = _verify_device_parameters_values() or update_config_file_flag if update_config_file_flag: - write_storage_icloud3_configuration_file() + write_icloud3_configuration_file() decode_all_passwords() build_log_file_filters() @@ -372,7 +368,7 @@ def conf_apple_acct(idx_or_username): ''' try: if len(Gb.conf_apple_accounts) == 0: - return (DEFAULT_APPLE_ACCOUNTS_CONF.copy(), 0) + return (DEFAULT_APPLE_ACCOUNT_CONF.copy(), 0) # Get conf_apple_acct by it's index if type(idx_or_username) is int: @@ -382,14 +378,14 @@ def conf_apple_acct(idx_or_username): return (conf_apple_acct, idx_or_username) else: - return (DEFAULT_APPLE_ACCOUNTS_CONF.copy(), -1) + return (DEFAULT_APPLE_ACCOUNT_CONF.copy(), -1) # Get conf_apple_acct by it's username elif type(idx_or_username) is str: conf_apple_acct = [apple_acct for apple_acct in Gb.conf_apple_accounts if apple_acct[CONF_USERNAME] == idx_or_username] if is_empty(conf_apple_acct): - return (DEFAULT_APPLE_ACCOUNTS_CONF.copy(), -1) + return (DEFAULT_APPLE_ACCOUNT_CONF.copy(), -1) # Get it's index conf_apple_acct_usernames = [apple_acct[CONF_USERNAME] @@ -405,7 +401,7 @@ def conf_apple_acct(idx_or_username): log_exception(err) pass - return (DEFAULT_APPLE_ACCOUNTS_CONF.copy(), 0) + return (DEFAULT_APPLE_ACCOUNT_CONF.copy(), 0) #-------------------------------------------------------------------- @@ -455,7 +451,6 @@ def _count_device_tracking_methods_configured(): ''' try: Gb.conf_icloud_device_cnt = 0 - # Gb.conf_fmf_device_cnt = 0 Gb.conf_mobapp_device_cnt = 0 for conf_device in Gb.conf_devices: @@ -465,9 +460,6 @@ def _count_device_tracking_methods_configured(): if conf_device[CONF_FAMSHR_DEVICENAME].startswith(NONE_FNAME) is False: Gb.conf_icloud_device_cnt += 1 - # if conf_device[CONF_FMF_EMAIL].startswith(NONE_FNAME) is False: - # Gb.conf_fmf_device_cnt += 1 - if conf_device[CONF_MOBILE_APP_DEVICE].startswith(NONE_FNAME) is False: Gb.conf_mobapp_device_cnt += 1 @@ -504,7 +496,7 @@ def _delete_old_log_files(): file_io.delete_file(log_file_2) #-------------------------------------------------------------------- -def _config_file_check_range_values(): +def _verify_general_parameter_values(): ''' Check the min and max value of the items that have a range in config_flow to make sure the actual value in the config file is within the min-max range @@ -542,14 +534,28 @@ def _config_file_check_range_values(): if trav_time_factor != Gb.conf_general[CONF_TRAVEL_TIME_FACTOR]: update_configuration_flag = True - if update_configuration_flag: - write_storage_icloud3_configuration_file() + return update_configuration_flag except Exception as err: _LOGGER.exception(err) #-------------------------------------------------------------------- -def _config_file_check_device_settings(): +def _verify_tracking_parameters_values(): + ''' + Cycle thru the conf_devices and verify that the settings are valid + ''' + update_configuration_flag = False + + if instr(Gb.conf_tracking[CONF_DATA_SOURCE], 'famshr'): + Gb.conf_tracking[CONF_DATA_SOURCE] = Gb.conf_tracking[CONF_DATA_SOURCE].replace('famshr', ICLOUD) + Gb.conf_tracking[CONF_DATA_SOURCE] = Gb.conf_tracking[CONF_DATA_SOURCE].replace('mobapp', MOBAPP) + Gb.conf_tracking[CONF_DATA_SOURCE] = Gb.conf_tracking[CONF_DATA_SOURCE].replace(' ', '') + update_configuration_flag = True + + return update_configuration_flag + +#-------------------------------------------------------------------- +def _verify_device_parameters_values(): ''' Cycle thru the conf_devices and verify that the settings are valid ''' @@ -572,8 +578,7 @@ def _config_file_check_device_settings(): conf_device[CONF_FIXED_INTERVAL] = 3.0 update_configuration_flag = True - if update_configuration_flag: - write_storage_icloud3_configuration_file() + return update_configuration_flag #-------------------------------------------------------------------- def _convert_hhmmss_to_minutes(conf_group): @@ -605,60 +610,28 @@ def _update_tracking_parameters(): new_items = [item for item in DEFAULT_TRACKING_CONF if item not in Gb.conf_tracking] - # v3.1.0 - Change 'famshr' to 'iCloud' - if instr(Gb.conf_tracking[CONF_DATA_SOURCE], 'famshr'): - list_add(item, CONF_DATA_SOURCE) + log_info_msg(f"{new_items=}") + for item in DEFAULT_TRACKING_CONF: + log_info_msg(f"{item=} {item in Gb.conf_tracking=} {Gb.conf_tracking.get(item)=}") if is_empty(new_items): return False for item in new_items: - if item in [CONF_APPLE_ACCOUNTS, CONF_DATA_SOURCE]: - Gb.conf_tracking = _v310_tracking_parameter_updates() - - else: - Gb.conf_tracking = _insert_into_conf_dict_parameter( - Gb.conf_tracking, - item, DEFAULT_TRACKING_CONF[item], - before=CONF_DEVICES) + Gb.conf_tracking = _insert_into_conf_dict_parameter( + Gb.conf_tracking, + item, DEFAULT_TRACKING_CONF[item], + before=CONF_DEVICES) + + if (CONF_APPLE_ACCOUNTS in new_items + and Gb.conf_tracking[CONF_USERNAME] != ''): + conf_apple_acct = DEFAULT_APPLE_ACCOUNT_CONF.copy() + conf_apple_acct[CONF_USERNAME] = Gb.conf_tracking[CONF_USERNAME] + conf_apple_acct[CONF_PASSWORD] = Gb.conf_tracking[CONF_PASSWORD] + Gb.conf_tracking[CONF_APPLE_ACCOUNTS] = Gb.conf_apple_accounts = [conf_apple_acct] return True -#..................................................................... -def _v310_tracking_parameter_updates(): - ''' - v3.1.0 changes: - - add the 'apple_accounts' parameter using the username/password from the tracking fields - - Changed the data source field from 'famshr,mobapp' to 'iCloud, MobApp' - ''' - - Gb.conf_tracking[CONF_DATA_SOURCE] = Gb.conf_tracking[CONF_DATA_SOURCE].replace('famshr', ICLOUD) - Gb.conf_tracking[CONF_DATA_SOURCE] = Gb.conf_tracking[CONF_DATA_SOURCE].replace('mobapp', MOBAPP) - Gb.conf_tracking[CONF_DATA_SOURCE] = Gb.conf_tracking[CONF_DATA_SOURCE].replace(' ', '') - - # v3.1 Add Apple accounts list - try: - if (CONF_APPLE_ACCOUNTS not in Gb.conf_tracking): - Gb.conf_tracking = _insert_into_conf_dict_parameter( - Gb.conf_tracking, - CONF_APPLE_ACCOUNTS, [], - before=CONF_DEVICES) - - if Gb.conf_tracking[CONF_USERNAME] == '': - Gb.conf_apple_accounts = Gb.conf_tracking[CONF_APPLE_ACCOUNTS] = [] - else: - Gb.conf_apple_accounts = Gb.conf_tracking[CONF_APPLE_ACCOUNTS] = [ - { CONF_USERNAME: Gb.conf_tracking[CONF_USERNAME], - CONF_PASSWORD: encode_password(Gb.conf_tracking[CONF_PASSWORD]), - CONF_TOTP_KEY: '', - CONF_LOCATE_ALL: True, - }] - - except Exception as err: - _LOGGER.exception(err) - - return - #-------------------------------------------------------------------- def _update_device_parameters(): ''' @@ -727,14 +700,34 @@ def _update_general_parameters(): return False for item in new_items: + before_item = _place_item_before(item, DEFAULT_GENERAL_CONF, CONF_DISPLAY_TEXT_AS) + Gb.conf_data[CF_GENERAL][item] = DEFAULT_GENERAL_CONF[item] Gb.conf_general = _insert_into_conf_dict_parameter( Gb.conf_general, item, DEFAULT_GENERAL_CONF[item], - before=CONF_DISPLAY_TEXT_AS) + before= before_item) return True +#-------------------------------------------------------------------- +def _place_item_before(item, conf_dict, default_item): + # Cycle thru conf\dict (ex: DEFAULT_GENERAL_CONF) items and get the name of the next + # item to place the new one in the same position + + before_item = default_item + _item_found = False + + for _item in conf_dict: + if _item_found: + return _item + + if _item == item: + _item_found = True + + return default_item + + #-------------------------------------------------------------------- def _insert_into_conf_dict_parameter(dict_parameter, new_item=None, @@ -771,8 +764,6 @@ def _insert_into_conf_dict_parameter(dict_parameter, dict_parameter[new_item] = initial_value return dict_parameter - return dict_parameter - #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # # Password encode/decode functions @@ -780,10 +771,13 @@ def _insert_into_conf_dict_parameter(dict_parameter, #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> def encode_all_passwords(): - Gb.conf_tracking[CONF_PASSWORD] = encode_password(Gb.conf_tracking[CONF_PASSWORD]) + try: + Gb.conf_tracking[CONF_PASSWORD] = encode_password(Gb.conf_tracking[CONF_PASSWORD]) - for apple_acct in Gb.conf_apple_accounts: - apple_acct[CONF_PASSWORD] = encode_password(apple_acct[CONF_PASSWORD]) + for apple_acct in Gb.conf_apple_accounts: + apple_acct[CONF_PASSWORD] = encode_password(apple_acct[CONF_PASSWORD]) + except: + pass #-------------------------------------------------------------------- def decode_all_passwords(): @@ -810,6 +804,8 @@ def encode_password(password): or password.startswith('««') or password.endswith('»»')): return password + elif password is None: + return '' return f"««{base64_encode(password)}»»" @@ -850,7 +846,7 @@ def decode_password(password): and password != '' and (password.startswith('««') is False or password.endswith('»»') is False)): password = password.replace('«', '').replace('»', '') - write_storage_icloud3_configuration_file() + write_icloud3_configuration_file() # Decode password if it is encoded and has the '««password»»' format if (password.startswith('««') or password.endswith('»»')): diff --git a/custom_components/icloud3/support/fido2.py b/custom_components/icloud3/support/fido2.py new file mode 100644 index 0000000..b11bef0 --- /dev/null +++ b/custom_components/icloud3/support/fido2.py @@ -0,0 +1,97 @@ +from fido2.client import Fido2Client +from fido2.ctap2 import AttestationObject, AuthenticatorData +from fido2.server import Fido2Server +from fido2.utils import websafe_encode, websafe_decode +import json + +''' +ChatGPT question: +Create a Python program to authenticate access to an Apple account using Fido based hardware security keys + + +Key Points: + 1. Registration: The program generates a registration challenge, which the user completes with their hardware security key. + 2. Authentication: Once registered, the program can authenticate the user by verifying the hardware key’s response to an authentication challenge. + 3. Customization: Replace https://localhost with your actual client application origin, and ensure the RP ID matches the relying party’s domain (e.g., apple.com). + +Notes: + • This example assumes a local FIDO2 client and server setup. Real-world integration with Apple’s services may require working within their APIs or using a browser-based flow. + • You may need additional permissions or SDKs from Apple to fully integrate with Apple account authentication. +''' + + + +# FIDO2 Relying Party information +RP_ID = "apple.com" # Use Apple's domain for RP ID +RP_NAME = "Apple" +ORIGIN = "https://apple.com" + +# Create a FIDO2 server instance +server = Fido2Server({"id": RP_ID, "name": RP_NAME}) + + +def register_security_key(): + # Generate a registration challenge + registration_data, state = server.register_begin( + { + "id": b"user-id", + "name": "User Name", + "displayName": "User Display Name", + }, + user_verification="discouraged", + ) + + print("Registration Challenge:", json.dumps(registration_data, indent=2)) + + # Assume user interacts with a security key via a client + client = Fido2Client("https://localhost", origin=ORIGIN) + client_data, attestation_object = client.make_credential( + registration_data["publicKey"] + ) + + # Verify the registration response + auth_data = server.register_complete( + state, + client_data, + AttestationObject(attestation_object), + ) + + print("Registration successful!") + print("Authenticator Data:", websafe_encode(auth_data)) + + +def authenticate_with_security_key(): + # Generate an authentication challenge + auth_data, state = server.authenticate_begin([{ + "id": b"user-id", + "name": "User Name", + "displayName": "User Display Name", + }]) + + print("Authentication Challenge:", json.dumps(auth_data, indent=2)) + + # Assume user interacts with a security key via a client + client = Fido2Client("https://localhost", origin=ORIGIN) + client_data, auth_data = client.get_assertion(auth_data["publicKey"]) + + # Verify the authentication response + server.authenticate_complete( + state, + [AuthenticatorData(websafe_decode(auth_data))], + client_data, + [websafe_decode(auth_data)], + ) + + print("Authentication successful!") + + +if __name__ == "__main__": + print("FIDO2 Security Key Authentication for Apple Account") + choice = input("Enter '1' to register or '2' to authenticate: ").strip() + + if choice == "1": + register_security_key() + elif choice == "2": + authenticate_with_security_key() + else: + print("Invalid choice. Exiting.") \ No newline at end of file diff --git a/custom_components/icloud3/support/pyicloud_ic3.py b/custom_components/icloud3/support/pyicloud_ic3.py index 79647e7..1a7b709 100644 --- a/custom_components/icloud3/support/pyicloud_ic3.py +++ b/custom_components/icloud3/support/pyicloud_ic3.py @@ -40,7 +40,7 @@ encode_password, decode_password, get_username_base, ) from ..helpers.file_io import (delete_file, read_json_file, save_json_file, file_exists, ) from ..helpers.time_util import (time_now, time_now_secs, secs_to_time, s2t, - secs_since, format_age ) + secs_since, format_secs_since, format_age, format_time_age ) from ..helpers.messaging import (post_event, post_monitor_msg, post_startup_alert, post_error_msg, _evlog, _log, more_info, add_log_file_filter, log_info_msg, log_error_msg, log_debug_msg, log_warning_msg, @@ -96,18 +96,21 @@ CONNECTION_ERROR_503 = 503 HTTP_RESPONSE_CODES = { - 200: 'iCloud Server Responded', + -2: 'Apple Server not Available (Connection Error)', + 200: 'iCloud Server Response', + 201: 'Device Offline', 204: 'Verification Code Accepted', - 421: 'Verification Code May Be Needed', - 450: 'Verification Code May Be Needed', - 500: 'Verification Code May Be Needed', - 503: 'Apple Server Refused SRP Password Validation Request', + 302: 'Apple Server not Available (Connection Error)', 400: 'Invalid Verification Code', + 401: 'INVALID USERNAME/PASSWORD', 403: 'Verification Code Requested', 404: 'Apple http Error, Web Page not Found', - 201: 'Device Offline', - -2: 'Apple Server not Available (Connection Error)', - 302: 'Apple Server not Available (Connection Error)', + 409: 'Valid Username/Password', + 421: 'Verification Code May Be Needed', + 421.1: 'INVALID USERNAME/PASSWORD', + 450: 'Verification Code May Be Needed', + 500: 'Verification Code May Be Needed', + 503: 'Apple Server Refused Password Validation Request, Retry Later', } HTTP_RESPONSE_CODES_IDX = {str(code): code for code in HTTP_RESPONSE_CODES.keys()} @@ -181,6 +184,32 @@ def __init__(self): self.AUTH_ENDPOINT = "https://idmsa.apple.com/appleauth/auth" + self.response_code = 0 + self.last_response_code = 0 + + + +#---------------------------------------------------------------------------- + def is_internet_available(self): + try: + if Gb.internet_connection_status_waiting_for_response: + log_debug_msg( f"Wait for Internet Connection response, " + f"Sent-{format_age(Gb.internet_connection_status_request_secs)}") + return False + + Gb.internet_connection_status_request_cnt += 1 + Gb.internet_connection_status_request_secs = time_now_secs() + + is_internet_available = self.ValidationPyiCloudSession.is_internet_available() + + log_debug_msg( f"Received Internet Connection status, " + f"Available-{is_internet_available}") + + return is_internet_available + + except Exception as err: + log_exception(err) + return False #---------------------------------------------------------------------------- def validate_username_password(self, username, password): @@ -197,9 +226,9 @@ def validate_username_password(self, username, password): self.password = password self.username_base = get_username_base(self.username) - self.session_data = '' #Dummy statement for PyiCloudSession - self.session_id = '' #Dummy statement for PyiCloudSession - self.instance = '' #Dummy statement for PyiCloudSession + self.session_data = '' #Dummy statement for PyiCloudSession + self.session_id = '' #Dummy statement for PyiCloudSession + self.instance = '' #Dummy statement for PyiCloudSession log_debug_msg(f"{self.username_base}, Validate Username/Password") aa_cookie_files_exist = file_exists(self.ValidationPyiCloud.cookie_dir_filename) @@ -231,6 +260,8 @@ def validate_username_password(self, username, password): #---------------------------------------------------------------------------- def _validate_with_tokenpw_file(self, username, password): + return False + token_pw_data = self.ValidationPyiCloud.read_token_pw_file() if (isnot_empty(token_pw_data) @@ -273,6 +304,31 @@ def _log_pw(password): return f"{password[:4]}{DOTS}{password[4:]}" + + +# From GitHub iLoot iloot.py +# def download_backup(login, password, output_folder, types, chosen_snapshot_id, combined, itunes_style, domain, threads, keep_existing): +# print 'Working with %s : %s' % (login, password) +# print 'Output directory :', output_folder + +# auth = "Basic %s" % base64.b64encode("%s:%s" % (login, password)) +# authenticateResponse = plist_request("setup.icloud.com", "POST", "/setup/authenticate/$APPLE_ID$", "", {"Authorization": auth}) +# if not authenticateResponse: +# # There was an error authenticating the user. +# return + +# dsPrsID = authenticateResponse["appleAccountInfo"]["dsPrsID"] +# auth = "Basic %s" % base64.b64encode("%s:%s" % (dsPrsID, authenticateResponse["tokens"]["mmeAuthToken"])) + +# headers = { +# 'Authorization': auth, +# 'X-MMe-Client-Info': CLIENT_INFO, +# 'User-Agent': USER_AGENT_UBD +# } +# account_settings = plist_request("setup.icloud.com", "POST", "/setup/get_account_settings", "", headers) +# auth = "X-MobileMe-AuthToken %s" % base64.b64encode("%s:%s" % (dsPrsID, authenticateResponse["tokens"]["mmeAuthToken"])) + + #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # # PyiCloud_RawData Object - Store all of the data related to the device. It is created @@ -338,6 +394,9 @@ def __init__( self, username, password=None, self.requires_2fa = False # This is set during the authentication function self.response_code_pwsrp_err = 0 self.response_code = 0 + self.last_response_code = 0 + self.last_request_secs = 0 # Keep the last time an internet request was made. Check time since + # in icloud3_main. Longer than 1-minute indicates internet is down. self.token_pw_data = {} self.token_password = password self.account_locked = False # set from the locked data item when authenticating with a token @@ -358,10 +417,9 @@ def __init__( self, username, password=None, self.endpoint_suffix = '' elif endpoint_suffix in APPLE_SPECIAL_ICLOUD_SERVER_COUNTRY_CODE: self.endpoint_suffix = endpoint_suffix.lower() - self._setup_url_endpoint_suffix(self.endpoint_suffix) + self._setup_url_endpoint_suffix() else: - self.endpoint_suffix = '' - # self.endpoint_suffix = endpoint_suffix if endpoint_suffix else Gb.icloud_server_endpoint_suffix + self.endpoint_suffix = '' self.cookie_directory = cookie_directory or Gb.icloud_cookie_directory self.session_directory = session_directory or Gb.icloud_session_directory @@ -373,26 +431,32 @@ def __init__( self, username, password=None, self.DeviceSvc = None # PyiCloud_ic3 object for Apple Device Service used to refresh the device's location - self.session_data = {} - self.session_data_token = {} - self.dsid = '' - self.trust_token = '' - self.session_token = '' - self.session_id = '' - self.client_id = f"auth-{str(uuid1()).lower()}" + self.session_data = {} + self.session_data_token = {} + self.dsid = '' + self.trust_token = '' + self.session_token = '' + self.session_id = '' + self.client_id = f"auth-{str(uuid1()).lower()}" - # self._setup_password_filter(password) add_log_file_filter(password) self._setup_PyiCloudSession() + self._initialize_variables() + + # Done if setting up ValidatePyiCloud used to verify username/password if validate_aa_upw is True: return - Gb.PyiCloudLoggingInto = self # Identifies a partial login that failed + Gb.PyiCloudLoggingInto = self # Identifies a partial login that failed Gb.PyiCloud_by_username[username] = self - self.authenticate() - self.refresh_icloud_data(locate_all_devices=True) + login_successful = self.authenticate() + if login_successful: + self.refresh_icloud_data(locate_all_devices=True) + else: + post_event(f"Apple Acct > {self.username_base}, Login Failed, " + "Location Data not Refreshed") except Exception as err: log_exception(err) @@ -433,7 +497,6 @@ def _initialize_variables(self): self.device_model_info_by_fname = {} # {'Gary-iPhone': [raw_model, model, model_display_name]} self.dup_icloud_dname_cnt = {} # Used to create a suffix for duplicate devicenames # {'Gary-iPhone': ['iPhone15,2', 'iPhone', 'iPhone 14 Pro']} - #--------------------------------------------------------------------------- @property def is_DeviceSvc_setup_complete(self): @@ -443,7 +506,7 @@ def is_DeviceSvc_setup_complete(self): @property def response_code_desc(self): desc = HTTP_RESPONSE_CODES.get(self.response_code, 'Unknown Error') - return f"Error-{self.response_code} ({desc})" + return f"Error-{self.response_code}, {desc}" #--------------------------------------------------------------------------- @@ -517,39 +580,49 @@ def authenticate(self, refresh_session=False): and 'dsid' in self.params): log_info_msg(f"{self.username_base}, Validate Token") + self.auth_method = "ValidToken" login_successful = self._validate_token() - if login_successful: self.auth_method = "Token" + # if login_successful: self.auth_method = "Token" # Authenticate - Sign into Apple Account (POST=/signin) if login_successful is False: log_info_msg(f"{self.username_base}, Authenticate with Token") + self.auth_method = "GetNewToken" login_successful = self._authenticate_with_token() - if login_successful: self.auth_method = "Token" if login_successful is False: log_info_msg(f"{self.username_base}, Authenticate with Password") + self.auth_method = 'Password' login_successful = self.authenticate_with_password() - if login_successful: self.auth_method = 'Password' + # The Auth with Token is necessary to fill in the findme_url + if login_successful: + login_successful = self._authenticate_with_token() + + # if login_successful is False: + # log_info_msg(f"{self.username_base}, Authenticate with Password SRP") + # login_successful = self.authenticate_with_password_srp() - # if login_successful is False: - # log_info_msg(f"{self.username_base}, Authenticate with Password SRP") - # login_successful = self.authenticate_with_password_srp() + # self.response_code_pwsrp_err = self.response_code + # if login_successful: - # self.response_code_pwsrp_err = self.response_code - # if login_successful: self.auth_method = 'PasswordSRP' + # # The Auth with Token is necessary to fill in the findme_url + # if login_successful: + # self.auth_method = 'PasswordSRP' + # self._authenticate_with_token() - # The Auth with Token is necessary to fill in the findme_url - if login_successful: - self._authenticate_with_token() if login_successful is False: + if self.response_code == 200: + self.response_code = self.last_response_code + err_msg = f"{self.username_base}, Authentication Failed, " if self.response_code == 302: err_msg += f"iCloud Server Connection Error, " - elif self.response_code == 401: + elif (self.auth_method == 'PasswordSRP' + and self.response_code == 401): valid_upw = Gb.PyiCloudValidateAppleAcct.validate_username_password( self.username, self.password) if valid_upw: @@ -558,14 +631,21 @@ def authenticate(self, refresh_session=False): else: err_msg += "Authentication error, Invalid Username or Password, " - elif self.response_code == 503 or self.response_code_pwsrp_err == 503: + elif (self.auth_method == 'PasswordSRP' + and (self.response_code == 503 + or self.response_code_pwsrp_err == 503)): self.auth_failed_503 = True self.response_code = 503 list_add(Gb.username_pyicloud_503_connection_error, self.username) err_msg += self.response_code_desc elif self.response_code == 421: - err_msg += "Account will be Reauthenticated and login will continue, " + err_msg += f"AuthMethod-{self.auth_method}, " + if self.auth_method == 'Password': + self.response_code = 421.1 + err_msg += "Invalid Username or Password, " + else: + err_msg += ("Invalid Token, Verification Code May be Needed") else: err_msg += "An unknown error occurred, " @@ -573,8 +653,7 @@ def authenticate(self, refresh_session=False): err_msg += f"ErrorCode-{self.response_code}" #log_info_msg(err_msg) - log_info_msg( f"{self.username_base}, " - f"Authentication Failed, {err_msg}") + log_info_msg(f"{err_msg}") self.is_authenticated = False return False # raise PyiCloudFailedLoginException(err_msg) @@ -602,6 +681,8 @@ def authenticate(self, refresh_session=False): return self.is_authenticated + + #---------------------------------------------------------------------------- def _authenticate_with_token(self): '''Authenticate using session token. Return True if successful.''' @@ -615,6 +696,7 @@ def _authenticate_with_token(self): "trustToken": self.session_data.get("trust_token", ""), "appName": "iCloud3"} else: + self.response_code = 421 log_debug_msg( f"{self.username_base}, " f"Authenticate with Token > Failed, Invalid Session Data") return False @@ -624,6 +706,15 @@ def _authenticate_with_token(self): self.data = self.PyiCloudSession.post(url, params=self.params, data=data) + if 'items' in self.data: + if 'hsaTrustedBrowser' in self.data['items']: + self._update_token_pw_file('items', self.data['items']) + else: + log_debug_msg( f"{self.username_base}, " + "Authenticate with Token > Failed, " + "Invalid 2fa hsaTrustedBrowser/hsaChallengeRequired Items") + return False + if 'dsInfo' in self.data: if 'dsid' in self.data['dsInfo']: self.params['dsid'] = self.dsid = str(self.data['dsInfo']['dsid']) @@ -1120,6 +1211,20 @@ def is_trusted_browser(self): def trusted_devices(self): return '''Returns devices trusted for two-step authentication.''' + headers = self._get_auth_headers() + url = f"{self.SETUP_ENDPOINT}/listDevices" + + try: + data = self.PyiCloudSession.post(url, params=self.params, headers=headers,) + _log(f"{data=}") + _log(f"{data.get('devices')=}") + + return data.get('devices') + + except Exception as err: + log_exception(err) + + # try: # _log(f"BUILD IN PYICLOUD TRUSTED DEVICES {self.data}") # url = f"{self.SETUP_ENDPOINT}/listDevices" @@ -1132,9 +1237,7 @@ def trusted_devices(self): # # data=json.dumps(self.data)) # self.data = req.json() - # _log(f"{self.data=}") - # _log(f"{self.data.get('devices')=}") - # return self.data.get('devices') + # _session_request(self, method, url, **kwargs # request = self.PyiCloudSession.get(url, params=self.params) @@ -1281,7 +1384,7 @@ def create_DeviceSvc_object(self, config_flow_login=False): self.PyiCloudSession, self.params) - log_debug_msg(f"{self.username_base}, Create iCloud object {self.username_base})") + log_debug_msg(f"{self.username_base}, Create iCloud object {self.username_base}") return self.DeviceSvc diff --git a/custom_components/icloud3/support/pyicloud_ic3_interface.py b/custom_components/icloud3/support/pyicloud_ic3_interface.py index 3b8d90a..57a1d96 100644 --- a/custom_components/icloud3/support/pyicloud_ic3_interface.py +++ b/custom_components/icloud3/support/pyicloud_ic3_interface.py @@ -63,7 +63,7 @@ def create_all_PyiCloudServices(): Gb.username_valid_by_username[username] = username_password_valid if Gb.username_valid_by_username[username]: - log_into_apple_account(username, password, locate_all_devices) + PyiCloud = log_into_apple_account(username, password, locate_all_devices) else: event_msg =(f"Apple Acct > " f"{username.split('@')[0]}, Invalid Username or Password") @@ -111,9 +111,6 @@ def check_all_apple_accts_valid_upw(): This is done when iCloud3 starts in __init__ ''' - # if Gb.PyiCloudValidateAppleAcct is None: - # Gb.PyiCloudValidateAppleAcct = PyiCloudValidateAppleAcct() - results_msg = '' cnt = -1 alert_symb = '' @@ -124,7 +121,6 @@ def check_all_apple_accts_valid_upw(): password = Gb.PyiCloud_password_by_username[username] if is_empty(username) or is_empty(password): - # Gb.username_valid_by_username[f"AppleAcctNoUserPW-#{cnt}"] = False continue # Validate username/password so we know all future login attempts will be with valid apple accts @@ -132,7 +128,6 @@ def check_all_apple_accts_valid_upw(): Gb.username_valid_by_username[username]= valid_upw - # if valid_apple_acct is False: alert_symb = EVLOG_ALERT crlf_symb = CRLF_DOT if valid_upw else f"{CRLF_RED_ALERT}" results_msg += f"{crlf_symb}{username}, Valid-{valid_upw}" @@ -263,15 +258,17 @@ def log_into_apple_account(username, password, locate_all_devices=None): f"{PyiCloud.auth_method}") else: retry_at = secs_to_time(time_now_secs() + 900) - post_event( f"Apple Acct > {username_base}, Login Failed, " - f"{PyiCloud.response_code_desc}, " - f"Retry At-{retry_at}") + post_event( f"{EVLOG_ALERT}Apple Acct > {username_base}, Login Failed, " + f"{CRLF_DOT}{PyiCloud.response_code_desc}") verify_icloud_device_info_received(PyiCloud) is_authentication_2fa_code_needed(PyiCloud, initial_setup=True) #display_authentication_msg(PyiCloud) + # _log(f"PyiCloud.trusted_devices=") + # _log(f"{PyiCloud.trusted_devices=}") + return PyiCloud except PyiCloud2FARequiredException as err: diff --git a/custom_components/icloud3/support/pyicloud_session.py b/custom_components/icloud3/support/pyicloud_session.py index 67fafc4..caa32ee 100644 --- a/custom_components/icloud3/support/pyicloud_session.py +++ b/custom_components/icloud3/support/pyicloud_session.py @@ -22,15 +22,19 @@ from ..global_variables import GlobalVariables as Gb from ..helpers.common import (instr, is_empty, isnot_empty, list_add, list_del, ) from ..helpers.file_io import (save_json_file, ) -from ..helpers.time_util import (time_now, time_now_secs, ) +from ..helpers.time_util import (time_now, time_now_secs, secs_to_time, format_time_age, ) from ..helpers.messaging import (_log, _evlog, log_info_msg, log_error_msg, log_debug_msg, log_warning_msg, log_rawdata, log_exception, log_rawdata_unfiltered, filter_data_dict, ) from requests import Session, adapters +import requests +from requests.exceptions import ConnectionError from os import path import inspect import json +import random +import time # import http.cookiejar as cookielib HEADER_DATA = { @@ -144,15 +148,16 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ callee = inspect.stack()[2] module = inspect.getmodule(callee[0]) - if Gb.internet_connection_error: + if 'retry_cnt' in kwargs: + pass + elif Gb.internet_connection_error: return {} try: - # if data is a str, unconvert it from json format, - # it will be reconverted to json later + # If data is a str, unconvert it from json format, it will be reconverted to json later if 'data' in kwargs and type(kwargs['data']) is str: kwargs['data'] = json.loads(kwargs['data']) - retry_cnt = kwargs.get("retry_cnt", 0) + retry_cnt = kwargs.get('retry_cnt', 0) log_rawdata_flag = (url.endswith('refreshClient') is False) if Gb.log_rawdata_flag or log_rawdata_flag or Gb.initial_icloud3_loading_flag: @@ -179,6 +184,10 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ response = None #++++++++++++++++ REQUEST ICLOUD DATA ++++++++++++++++ + Gb.last_PyiCloud_request_secs = time_now_secs() + + if Gb.internet_connection_test: + raise requests.exceptions.ConnectionError response = Session.request(self, method, url, **kwargs) @@ -189,34 +198,43 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ # log_exception(err) data = {} + Gb.last_PyiCloud_request_secs = 0 #++++++++++++++++ REQUEST ICLOUD DATA +++++++++++++++ - test_err = 'connection' - # if a == b: - # c = True - - except Exception as err: - # log_exception(err) - - # Connection Error Message: - # "HTTPSConnectionPool(host='setup.icloud.com', port=443): - # Max retries exceeded with url: /setup/ws/1/accountLogin?clientBuildNumber=2021Project52 - # &clientMasteringNumber=2021B29&ckjsBuildVersion=17DProjectDev77 - # &clientId=3f97e836-b581-11ef-8115-2ccf674e40a8 - # (Caused by NewConnectionError(': - # Failed to establish a new connection: [Errno -3] Try again'))" - - # if instr(err, 'connection') or instr(test_err, 'connection'): - if instr(str(err), 'connection'): - log_error_msg("iCloud3 encountered an error connecting to the Internet, Home Assistant is Offline > HTTPSConnectionPool Error: Failed to establish a new connection: [Errno -3]") - Gb.internet_connection_error = True - # Gb.internet_connection_error_secs = time_now_secs() + except (requests.exceptions.RetryError, + requests.exceptions.ConnectionError, + requests.exceptions.HTTPError, + requests.exceptions.Timeout, + OSError, + requests.exceptions.SSLError, + requests.exceptions.ProxyError, + requests.exceptions.ConnectTimeout, + requests.exceptions.ReadTimeout, + requests.exceptions.URLRequired, + requests.exceptions.TooManyRedirects, + requests.exceptions.InvalidURL, + requests.exceptions.InvalidHeader, + requests.exceptions.InvalidProxyURL, + requests.exceptions.ChunkedEncodingError, + requests.exceptions.StreamConsumedError, + requests.exceptions.RetryError, + requests.exceptions.UnrewindableBodyError, + ) as err: + + log_error_msg( f"iCloud3 Error > An error occurred connecting to the Internet, " + f"Home Assistant may be Offline > " + f"Error-{err}") + + Gb.internet_connection_error = True + + self.response_code = -3 + self.PyiCloud.response_code = -3 + self.response_ok = False + return {} - self.response_code = -3 - self.PyiCloud.response_code = -3 - self.response_ok = False - return {} + except Exception as err: + log_exception(err) self._raise_error(-3, f"Error setting up iCloud Server Connection ({err})") return {} @@ -224,9 +242,10 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ content_type = response.headers.get("Content-Type", "").split(";")[0] json_mimetypes = ["application/json", "text/json"] - self.response_code = response.status_code - self.PyiCloud.response_code = response.status_code - self.response_ok = response.ok + self.PyiCloud.last_response_code = self.PyiCloud.response_code + self.PyiCloud.response_code = response.status_code + self.response_code = response.status_code + self.response_ok = response.ok log_rawdata_flag = (url.endswith('refreshClient') is False) or response.status_code != 200 if Gb.log_rawdata_flag or log_rawdata_flag or Gb.initial_icloud3_loading_flag: @@ -237,6 +256,8 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ if retry_cnt >= 2 or Gb.log_rawdata_flag_unfiltered: log_data['headers'] = response.headers logged = log_rawdata(log_hdr, log_data, log_rawdata_flag=log_rawdata_flag) + # _log(f"noYK-{log_hdr=}") + # _log(f"noYK-{data=}") # Validating the username/password, code=409 is valid, code=401 is invalid if (response.status_code in [401, 409] @@ -307,26 +328,72 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ log_exception(err) if content_type not in json_mimetypes: - # return response return data - # try: - # data = response.json() - - # except: - # if not response.ok: - # msg = (f"Error handling data returned from iCloud, {response}") - # request_logger.warning(msg) - # return response - error_code, error_reason = self._resolve_error_code_reason(data) if error_reason: self._raise_error(error_code, error_reason) - # return response return data +#------------------------------------------------------------------ + def is_internet_available(self): + ''' + Try to connect to Google servers to determine if the internet is really down + or there is an error connecting to Apple servers + ''' + + try: + # Prevent a new status check being requested while waiting for a response + if Gb.internet_connection_status_waiting_for_response: + return False + + Gb.internet_connection_status_waiting_for_response = True + + log_debug_msg( f"Request Internet Connection status " + f"(#{Gb.internet_connection_status_request_cnt}), " + f"Sent-{time_now()}") + + # Use TestCode in service_handler > Show Tracking Monitor + if Gb.internet_connection_test: + ri = random.randint(1, 61) + time.sleep(ri) + raise requests.exceptions.ConnectionError + + # Connect to google.com + Session.request(self, 'get', "https://8.8.8.8") + Gb.internet_connection_status_waiting_for_response = False + return True + + except (requests.exceptions.RetryError, + requests.exceptions.ConnectionError, + requests.exceptions.HTTPError, + requests.exceptions.Timeout, + OSError, + requests.exceptions.SSLError, + requests.exceptions.ProxyError, + requests.exceptions.ConnectTimeout, + requests.exceptions.ReadTimeout, + requests.exceptions.URLRequired, + requests.exceptions.TooManyRedirects, + requests.exceptions.InvalidURL, + requests.exceptions.InvalidHeader, + requests.exceptions.InvalidProxyURL, + requests.exceptions.ChunkedEncodingError, + requests.exceptions.StreamConsumedError, + requests.exceptions.RetryError, + requests.exceptions.UnrewindableBodyError, + ) as err: + pass + + except Exception as err: + log_exception(err) + pass + + Gb.internet_connection_status_waiting_for_response = False + return False + #------------------------------------------------------------------ @staticmethod def _resolve_error_code_reason(data): diff --git a/custom_components/icloud3/support/restore_state.py b/custom_components/icloud3/support/restore_state.py index 99e56be..ea893ff 100644 --- a/custom_components/icloud3/support/restore_state.py +++ b/custom_components/icloud3/support/restore_state.py @@ -29,28 +29,28 @@ # .STORAGE/ICLOUD3.RESTORE_STATE FILE ROUTINES # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -def load_storage_icloud3_restore_state_file(): +def load_icloud3_restore_state_file(): try: if file_exists(Gb.icloud3_restore_state_filename) is False: build_initial_restore_state_file_structure() - write_storage_icloud3_restore_state_file() + write_icloud3_restore_state_file() - success = read_storage_icloud3_restore_state_file() + success = read_icloud3_restore_state_file() if success is False: log_info_msg(f"Invalid icloud3.restore_state File-{Gb.icloud3_restore_state_filename}") build_initial_restore_state_file_structure() - write_storage_icloud3_restore_state_file() - read_storage_icloud3_restore_state_file() + write_icloud3_restore_state_file() + read_icloud3_restore_state_file() return except Exception as err: log_exception(err) build_initial_restore_state_file_structure() - write_storage_icloud3_restore_state_file() - read_storage_icloud3_restore_state_file() + write_icloud3_restore_state_file() + read_icloud3_restore_state_file() #-------------------------------------------------------------------- def build_initial_restore_state_file_structure(): @@ -81,7 +81,7 @@ def clear_devices(): Gb.restore_state_devices = {} #------------------------------------------------------------------------------------------- -def read_storage_icloud3_restore_state_file(): +def read_icloud3_restore_state_file(): ''' Read the config/.storage/.icloud3.restore_state file. - Extract the data into the Global Variables. @@ -123,7 +123,7 @@ def read_storage_icloud3_restore_state_file(): return False #-------------------------------------------------------------------- -def write_storage_icloud3_restore_state_file(): +def write_icloud3_restore_state_file(): ''' Update the config/.storage/.icloud3.restore_state file when the sensors for a device have changed. Since the multiple sensors are updated on one tracking @@ -144,12 +144,12 @@ def write_storage_icloud3_restore_state_file(): Gb.restore_state_commit_cnt = 0 async_track_point_in_time(Gb.hass, - _async_commit_storage_icloud3_restore_state_file_changes, + _async_commit_icloud3_restore_state_file_changes, datetime_plus(utcnow(), secs=10)) #-------------------------------------------------------------------- @callback -async def _async_commit_storage_icloud3_restore_state_file_changes(callback_datetime_struct): +async def _async_commit_icloud3_restore_state_file_changes(callback_datetime_struct): try: Gb.restore_state_profile['last_commit'] = datetime_now() Gb.restore_state_profile['recds_changed'] = Gb.restore_state_commit_cnt diff --git a/custom_components/icloud3/support/service_handler.py b/custom_components/icloud3/support/service_handler.py index aa31e22..e5377ed 100644 --- a/custom_components/icloud3/support/service_handler.py +++ b/custom_components/icloud3/support/service_handler.py @@ -10,7 +10,7 @@ from ..global_variables import GlobalVariables as Gb from ..const import (DOMAIN, - RED_ALERT, EVLOG_ALERT, EVLOG_ERROR, + RED_ALERT, EVLOG_ALERT, EVLOG_ERROR, CRLF_DOT, CMD_RESET_PYICLOUD_SESSION, LOCATION, NEXT_UPDATE_TIME, NEXT_UPDATE, INTERVAL, CONF_DEVICENAME, CONF_ZONE, CONF_COMMAND, CONF_LOG_LEVEL, @@ -192,7 +192,7 @@ def resolve_action_devicename_values(action, devicename): if action in ACTION_FNAME_TO_ACTION: action = ACTION_FNAME_TO_ACTION[action] if devicename == 'startup_log': - return action, devicename + return action, devicename if devicename in Gb.Devices_by_ha_device_id: devicename = Gb.Devices_by_ha_device_id[devicename].devicename @@ -386,9 +386,9 @@ def _handle_global_action(global_action, action_option): # This will be handled in the 5-second ic3 loop # Gb.evlog_action_request = CMD_RESET_PYICLOUD_SESSION post_event(f"{EVLOG_ERROR}The `Action > Request Apple Verification Code` " - f"is no longer available. This must be done using the " - f"`Configuration > Enter/Request An Apple Account Verification " - f"Code` screen") + "is no longer available. This must be done using the " + "`Configuration > Enter/Request An Apple Account Verification " + "Code` screen") return elif global_action == CMD_LOG_LEVEL: @@ -396,15 +396,21 @@ def _handle_global_action(global_action, action_option): return elif global_action == CMD_WAZEHIST_MAINTENANCE: - event_msg = f"{EVLOG_ALERT}Waze History > Recalculate Route Time/Dist. " - if Gb.wazehist_recalculate_time_dist_flag: - event_msg += "Starting Immediately" + event_msg = f"{EVLOG_ALERT}Waze History > Recalculate Route Time/Dist, " + if Gb.WazeHist.wazehist_recalculate_time_dist_running_flag: + Gb.WazeHist.wazehist_recalculate_time_dist_abort_flag = True + event_msg+="Stopped" post_event(event_msg) + elif Gb.wazehist_recalculate_time_dist_flag: + event_msg+=("Starting Immediately" + f"{CRLF_DOT}SELECT AGAIN TO STOP") + post_event(event_msg) + Gb.wazehist_recalculate_time_dist_flag = False Gb.WazeHist.wazehist_recalculate_time_dist_all_zones() else: Gb.wazehist_recalculate_time_dist_flag = True - event_msg+=(f"Scheduled to run tonight at Midnight. " - "SELECT AGAIN TO RUN IMMEDIATELY") + event_msg+=(f"Scheduled to run tonight at Midnight " + f"{CRLF_DOT}SELECT AGAIN TO RUN IMMEDIATELY") post_event(event_msg) elif global_action == CMD_WAZEHIST_TRACK: @@ -417,13 +423,15 @@ def _handle_global_action(global_action, action_option): #-------------------------------------------------------------------- def handle_action_log_level(action_option, change_conf_log_level=True): - # Shor/Hide Tracking Monitors + # Show/Hide Tracking Monitors if instr(action_option, 'monitor'): - Gb.evlog_trk_monitors_flag = (not Gb.evlog_trk_monitors_flag) + Gb.evlog_trk_monitors_flag = not Gb.evlog_trk_monitors_flag # Test trigger for Internet connection error - # Gb.internet_connection_error = Gb.evlog_trk_monitors_flag - # _evlog(f"{RED_ALERT}Internet Connection Error-{Gb.internet_connection_error}") + # Gb.internet_connection_error = not Gb.internet_connection_error + # Gb.internet_connection_test = not Gb.internet_connection_test + # Gb.evlog_trk_monitors_flag = False + # _evlog(f"{RED_ALERT}TEST INTERNET CONNECTION ERROR-{Gb.internet_connection_error}") return new_log_debug_flag = Gb.log_debug_flag diff --git a/custom_components/icloud3/support/start_ic3.py b/custom_components/icloud3/support/start_ic3.py index cc406e7..7d08536 100644 --- a/custom_components/icloud3/support/start_ic3.py +++ b/custom_components/icloud3/support/start_ic3.py @@ -44,7 +44,7 @@ CONF_CENTER_IN_ZONE, CONF_DISCARD_POOR_GPS_INZONE, CONF_WAZE_USED, CONF_WAZE_REGION, CONF_WAZE_MAX_DISTANCE, CONF_WAZE_MIN_DISTANCE, CONF_WAZE_REALTIME, CONF_WAZE_HISTORY_DATABASE_USED, CONF_WAZE_HISTORY_MAX_DISTANCE, - CONF_WAZE_HISTORY_TRACK_DIRECTION, + CONF_WAZE_HISTORY_TRACK_DIRECTION, CONF_STAT_ZONE_FNAME, CONF_STAT_ZONE_STILL_TIME, CONF_STAT_ZONE_INZONE_INTERVAL, CONF_STAT_ZONE_BASE_LATITUDE, CONF_STAT_ZONE_BASE_LONGITUDE, CONF_DISPLAY_TEXT_AS, @@ -352,6 +352,9 @@ def initialize_on_initial_load(): if Gb.initial_icloud3_loading_flag is False: return + # Gb.internet_connection_error = True + # _evlog(f"{Gb.internet_connection_error=}") + Gb.log_level = 'info' # Gb.username_valid_by_username = {} @@ -515,8 +518,12 @@ def initialize_data_source_variables(): Gb.internet_connection_error_secs = 0 Gb.internet_connection_error = False - Gb.conf_data_source_ICLOUD = instr(Gb.conf_tracking[CONF_DATA_SOURCE], ICLOUD) \ - or instr(Gb.conf_tracking[CONF_DATA_SOURCE], 'icloud') + if instr(Gb.conf_tracking[CONF_DATA_SOURCE], 'famshr'): + Gb.conf_tracking[CONF_DATA_SOURCE] = Gb.conf_tracking[CONF_DATA_SOURCE].replace('famshr', ICLOUD) + if instr(Gb.conf_tracking[CONF_DATA_SOURCE], 'mobapp'): + Gb.conf_tracking[CONF_DATA_SOURCE] = Gb.conf_tracking[CONF_DATA_SOURCE].replace('mobapp', MOBAPP) + + Gb.conf_data_source_ICLOUD = instr(Gb.conf_tracking[CONF_DATA_SOURCE], ICLOUD) Gb.conf_data_source_ICLOUD = Gb.conf_data_source_ICLOUD Gb.conf_data_source_MOBAPP = instr(Gb.conf_tracking[CONF_DATA_SOURCE], MOBAPP) @@ -649,7 +656,7 @@ def set_log_level(log_level): #------------------------------------------------------------------------------ def update_conf_file_log_level(log_level): Gb.conf_general[CONF_LOG_LEVEL] = log_level - config_file.write_storage_icloud3_configuration_file() + config_file.write_icloud3_configuration_file() #------------------------------------------------------------------------------ # @@ -813,7 +820,7 @@ def check_ic3_event_log_file_version(): if Gb.evlog_version != www_version_text: Gb.evlog_version = Gb.conf_profile['event_log_version'] = www_version_text - config_file.write_storage_icloud3_configuration_file() + config_file.write_icloud3_configuration_file() return @@ -822,7 +829,7 @@ def check_ic3_event_log_file_version(): copy_file(ic3_evlog_js_filename, www_evlog_js_filename) Gb.evlog_version = Gb.conf_profile['event_log_version'] = www_version_text - config_file.write_storage_icloud3_configuration_file() + config_file.write_icloud3_configuration_file() post_startup_alert('Event Log was updated. Browser refresh needed') event_msg =(f"{EVLOG_ALERT}" @@ -1298,7 +1305,7 @@ def _verify_away_time_zone_devicenames(): Gb.conf_general[CONF_AWAY_TIME_ZONE_2_OFFSET] = 0 if update_conf_file_flag: - config_file.write_storage_icloud3_configuration_file() + config_file.write_icloud3_configuration_file() #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -1331,7 +1338,7 @@ def setup_data_source_ICLOUD(retry=False): not_tracked = [Device.fname for Device in Gb.Devices if Device.conf_apple_acct_username == username] not_tracked_msg = 'None' if is_empty(not_tracked) else list_to_str(not_tracked) - post_event( f"{EVLOG_ALERT}Apple Acct > {username}, Login Failed, " + post_event( f"{EVLOG_ALERT}Apple Acct > {get_username_base(username)}, Login Failed, " f"{CRLF_DOT}Devices-{not_tracked_msg}") # Now that all devices are set up, cycle through them again and display the @@ -1440,7 +1447,7 @@ def _check_renamed_find_devices(PyiCloud): PyiCloud.device_id_by_icloud_dname[new_icloud_devicename] = conf_device[CONF_FAMSHR_DEVICE_ID] PyiCloud.device_id_by_icloud_dname.pop(old_icloud_devicename, None) - config_file.write_storage_icloud3_configuration_file() + config_file.write_icloud3_configuration_file() except Exception as err: log_exception(err) @@ -1594,6 +1601,8 @@ def _set_any_Device_alerts(): for Device in Gb.Devices: # Device's Apple acct error + if (Device.conf_apple_acct_username == '' or Device.conf_icloud_dname == '' or Device.verified_flag is False): + continue if Device.conf_apple_acct_username not in Gb.PyiCloud_by_username: Device.set_fname_alert(YELLOW_ALERT) apple_acct_not_found_msg += ( @@ -1841,7 +1850,7 @@ def _find_icloud_conf_device(PyiCloud, pyicloud_dname, device_id): if update_config_flag: conf_device = _check_changed_apple_device_info(PyiCloud, pyicloud_dname, device_id, conf_device) - config_file.write_storage_icloud3_configuration_file() + config_file.write_icloud3_configuration_file() post_event( f"{EVLOG_ALERT}ICLOUD DEVICE NAME CHANGED > " f"The configured name was not found in any Apple Accounts. A candidate was found" f"{CRLF_DOT}{conf_device[CONF_IC3_DEVICENAME]} > " @@ -1869,7 +1878,7 @@ def _check_changed_apple_device_info(PyiCloud, pyicloud_dname, device_id, conf_d conf_device[CONF_MODEL] = model conf_device[CONF_MODEL_DISPLAY_NAME] = model_display_name - config_file.write_storage_icloud3_configuration_file() + config_file.write_icloud3_configuration_file() return conf_device @@ -1961,7 +1970,7 @@ def _update_config_with_icloud_device_fields(conf_device, _RawData): conf_device[CONF_MODEL] = model conf_device[CONF_MODEL_DISPLAY_NAME] = model_display_name - config_file.write_storage_icloud3_configuration_file() + config_file.write_icloud3_configuration_file() post_event( f"{EVLOG_NOTICE}Device Config Updated > {conf_device[CONF_FNAME]} " f"({conf_device[CONF_IC3_DEVICENAME]}), " @@ -2162,7 +2171,7 @@ def setup_tracked_devices_for_mobapp(): Device.model = conf_device[CONF_MODEL] = model if model_display_name: Device.model_display_name = conf_device[CONF_MODEL_DISPLAY_NAME] = model_display_name - config_file.write_storage_icloud3_configuration_file() + config_file.write_icloud3_configuration_file() break # Setup mobapp entities with data or to be monitored @@ -2451,7 +2460,6 @@ def setup_trackable_devices(): Gb.used_data_source_MOBAPP = True mobapp_attrs = mobapp_data_handler.get_mobapp_device_trkr_entity_attrs(Device) - # _log(f"{Device.devicename} {mobapp_attrs=}") if mobapp_attrs: mobapp_data_handler.update_mobapp_data_from_entity_attrs(Device, mobapp_attrs) diff --git a/custom_components/icloud3/support/start_ic3_control.py b/custom_components/icloud3/support/start_ic3_control.py index 07d9429..4879cd8 100644 --- a/custom_components/icloud3/support/start_ic3_control.py +++ b/custom_components/icloud3/support/start_ic3_control.py @@ -58,7 +58,7 @@ def stage_1_setup_variables(): Gb.reinitialize_icloud_devices_flag = False # Set when no devices are tracked and iC3 needs to automatically restart Gb.reinitialize_icloud_devices_cnt = 0 - config_file.load_storage_icloud3_configuration_file() + config_file.load_icloud3_configuration_file() write_config_file_to_ic3log() start_ic3.initialize_global_variables() start_ic3.set_global_variables_from_conf_parameters() @@ -176,8 +176,8 @@ def stage_3_setup_configured_devices(): for username, valid_upw in Gb.username_valid_by_username.items(): if valid_upw is False: - post_event( f"{EVLOG_ALERT}Apple Acct > {username}, Login failed, " - f"INVALID USERNAME/PASSWORD") + post_event( f"{EVLOG_ALERT}Apple Acct > {get_username_base(username)}, Login failed, " + f"{CRLF_DOT}INVALID USERNAME/PASSWORD") if Gb.config_track_devices_change_flag: pass @@ -268,7 +268,7 @@ def stage_4_setup_data_sources(): for username in Gb.username_pyicloud_503_connection_error] retry_at = secs_to_time(time_now_secs() + 900) post_event( f"{EVLOG_ALERT}Apple Acct > {list_to_str(username_base)}, Login Failed, " - f"Error-503 (Apple Server Refused SRP Password Validation Request), " + f"{CRLF_DOT}Error-503 (Apple Server Refused SRP Password Validation Request), " f"Retry At-{retry_at}") except Exception as err: diff --git a/custom_components/icloud3/support/v2v3_config_migration.py b/custom_components/icloud3/support/v2v3_config_migration.py index 1eab93d..a564feb 100644 --- a/custom_components/icloud3/support/v2v3_config_migration.py +++ b/custom_components/icloud3/support/v2v3_config_migration.py @@ -39,7 +39,7 @@ CONF_TRACK_FROM_ZONES, CONF_DEVICE_TYPE, CONF_INZONE_INTERVAL, CONF_NAME, NAME, BADGE, BATTERY, BATTERY_STATUS, INFO, - DEFAULT_DEVICE_CONF, DEFAULT_GENERAL_CONF, DEFAULT_APPLE_ACCOUNTS_CONF, + DEFAULT_DEVICE_CONF, DEFAULT_GENERAL_CONF, DEFAULT_APPLE_ACCOUNT_CONF, WAZE_SERVERS_BY_COUNTRY_CODE, ) @@ -189,7 +189,7 @@ def convert_v2_config_files_to_v3(self): log_warning_msg('iCloud3 - Migration Complete') - config_file.write_storage_icloud3_configuration_file() + config_file.write_icloud3_configuration_file() self.migration_log_file.close() log_info_msg(f"Profile:\n{DEBUG_LOG_LINE_TABS}{Gb.conf_profile}") @@ -538,7 +538,7 @@ def _set_data_fields_from_config_parameter_dictionary(self): Gb.conf_profile[CONF_EVLOG_CARD_PROGRAM] = self.conf_parm_general.get(CONF_EVLOG_CARD_PROGRAM, EVLOG_CARD_WWW_JS_PROG) # Convert iCloud Account Parameters - conf_apple_account = DEFAULT_APPLE_ACCOUNTS_CONF.copy() + conf_apple_account = DEFAULT_APPLE_ACCOUNT_CONF.copy() conf_apple_account[CONF_USERNAME] = self.conf_parm_tracking[CONF_USERNAME] conf_apple_account[CONF_PASSWORD] = self.conf_parm_tracking[CONF_PASSWORD] Gb.conf_apple_accounts = conf_apple_account diff --git a/custom_components/icloud3/support/waze_history.py b/custom_components/icloud3/support/waze_history.py index 5f7e796..174eeb9 100644 --- a/custom_components/icloud3/support/waze_history.py +++ b/custom_components/icloud3/support/waze_history.py @@ -757,6 +757,7 @@ def wazehist_recalculate_time_dist(self, all_zones_flag=False): self.wazehist_recalculate_time_dist_running_flag = False self.wazehist_recalculate_time_dist_abort_flag = False + Gb.wazehist_recalculate_time_dist_flag = False #-------------------------------------------------------------------- def _cycle_through_wazehist_records(self, zone_id, zone_dname, zone_from_loc): diff --git a/custom_components/icloud3/translations/en.json b/custom_components/icloud3/translations/en.json index 8f2a743..5eb4feb 100644 --- a/custom_components/icloud3/translations/en.json +++ b/custom_components/icloud3/translations/en.json @@ -94,6 +94,7 @@ "icloud_acct_no_data_source": "❌ No Data Source has been selected (Apple iCloud Account or Mobile App)", "ic3_icloud_same_name": "iCloud3 dev_trkr.entity_id and name on the device (Settings > General > About) can not be exactly the same (letters & case)", "mobile_app_error": "Error, The Mobile App Integration is not installed. The Mobile App will not be used as a data source; location data and zone enter/exit triggers will not be monitored", + "password_asp_invalid": "Apple does not support App Specific Passwords in WebAuth applications", "verification_code_requested": "The Apple Account Verification Code was requested, BROWSER REFRESH MAY BE NEEDED", "verification_code_requested2": "The Apple Account Verification Code was requested", @@ -133,6 +134,9 @@ "unknown_icloud_mobapp_picture": "Check the iCloud, Mobile App and Picture parameter values (Not found or Invalid)", "unknown_icloud_picture": "Check the iCloud and Picture parameter values (Not found or Invalid)", + "internet_connection_err": "Home Asst is Offline, No Internet Connection is Available", + "apple_acct_connection_err": "Internet is available but could not connect to Apple. Try again in a few minutes", + "tfz_selection_invalid": "The value must be a zone that is being tracked from", "time_factor_invalid_range": "The 'Travel Time Multiplier' must be between .1 and .9", "fixed_interval_invalid_range": "The 'Fixed Interval' must be 0 (not used) or >= 3 (> 5 recommended)", @@ -280,6 +284,7 @@ "famshr_devicename": "APPLE ACCOUNT iCLOUD DEVICE - Apple iCloud device providing location data", "mobile_app_device": "MOBILE APP DEVICE_TRACKER ENTITY - Mobile App device providing location data & zone triggers", "picture": "PICTURE - Image of the person normally using this device (44x44 pixels is a good size)", + "icon": "ICON - Icon to display when the Picture has not been selected (=None)", "device_type": "DEVICE TYPE - iPhone, iPad, Watch, etc.", "tracking_mode": "TRACKING MODE - Location request method (Tracked, Monitored, Inactive)", "mobapp": "MOBILE APP INSTALLED - HA Mobile App is installed on this device", @@ -298,6 +303,7 @@ "famshr_devicename": "APPLE ACCOUNT iCLOUD DEVICE - Apple iCloud device providing location data", "mobile_app_device": "MOBILE APP DEVICE_TRACKER ENTITY - Mobile App device providing location data & zone triggers", "picture": "PICTURE - Image of the person normally using this device (44x44 pixels is a good size)", + "icon": "ICON - Icon to display when the Picture has not been selected (=None)", "device_type": "DEVICE TYPE - iPhone, iPad, Watch, etc", "tracking_mode": "TRACKING MODE - How location requests should be done (Full tracking, Monitor, Inactive)", "inzone_interval": "INZONE INTERVAL", @@ -337,6 +343,18 @@ "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, + "picture_dir_filter": { + "title": "Set up Picture Directory Filter", + "description": "Select the \\www directorys to be searched for image (png, jpg) files", + "data": { + "www_group_1": "Group 1 - Directory 1-5", + "www_group_2": "Group 2 - Directory 6-10", + "www_group_3": "Group 3 - Directory 11-15", + "www_group_4": "Group 4 - Directory 16-20", + "www_group_5": "Group 5 - Directory 21-25", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" + } + }, "away_time_zone": { "title": "Display Location Time Zone when Away", "description": "The time displayed in the Event Log and Sensors show the time an event took place using the Home 'time zone' from your Home Assistant computer. When you are away from Home and in another time zone, your tracking events are still based on the time at your Home 'time zone', not time in your current location.\n\nThis screen lets you display time events using your current location's time zone.", @@ -364,23 +382,56 @@ "data_description": { } }, + "tracking_parameters": { + "title": "Tracking Parameters", + "description": "Many parameters are used when tracking a device and displaying the results. These include how poor GPS Accuracy should be handled, various polling interval values and zone distances used to set the time to locate the device. They are customized on this screen.", + "data": { + "gps_accuracy_threshold": "GPS ACCURACY THRESHOLD", + "old_location_threshold": "OLD LOCATION THRESHOLD", + "old_location_adjustment": "OLD LOCATION ADJUSTMENT", + "distance_between_devices": "USE LOCATION RESULTS FROM A NEAR-BY DEVICE", + "max_interval": "MAXIMUM INTERVAL", + "exit_zone_interval": "EXIT ZONE INTERVAL", + "mobapp_alive_interval": "REQUEST MOBILE APP LOCATION INTERVAL", + "tfz_tracking_max_distance": "TRACK-FROM-ZONE DISPLAY DISTANCE", + "offline_interval": "DEVICE OFFLINE INTERVAL", + "travel_time_factor": "TRAVEL TIME INTERVAL AND NEXT LOCATION UPDATE MULTIPLIER", + "discard_poor_gps_inzone": "DISCARD POOR RESULTS - Discard Location Updates with Poor GPS Accuracy when in a Zone", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" + }, + "data_description": { + "old_location_threshold": "Locations older than this value will be discarded", + "old_location_adjustment": "Add this to the time that determines if a location is old", + "distance_between_devices": "When tracking results are updated, any nearby devices are identified. When tracking results for those devices are updated, the tracking results of the one originally updated can be used instead. This improves performance since the Waze route time and distance are not requested again", + "gps_accuracy_threshold": "Locations with GPS Accuracy above this value will be discarded", + "tfz_tracking_max_distance": "Normally the Home zone's time and distance data is displayed on the Device's device_tracker and sensor entities. Display the Track-from-Zone instead when the Device is within this distance of the Track-from-Zone", + "max_interval": "The maximum time between location requests", + "exit_zone_interval": "The time to the first location request after exiting a zone", + "mobapp_alive_interval": "Send a location request to the Mobile App if there has been no contact after this amount of time. This will check to see if the Mobile App is responding to location requests or is asleep and not running.", + "offline_interval": "Location request interval when offline (Airplane mode, dead cell area, etc.)", + "travel_time_factor": "This is used to calculate the Interval and Next Location Time when going towards Home. A smaller value will reduce the interval time and increase the location requests, a larger value will increase the interval and reduce the location requests." + } + }, "format_settings": { - "title": "Format Settings", + "title": "FIELD FORMATS & EVENT LOG OVERRIDES", "description": "Tracking activity, results and information messages are displayed in the Event log, sensors and device_tracker entities for tracked and monitored devices.\n\nThis screen us used to specify how these results should be displayed.", "data": { - "log_level": "LOG LEVEL - The type of messages that are written to the iCloud3 Log file (icloud3.log}", - "log_level_devices": "RAWDATA LOG DEVICE FILTER - Write iCloud RawData to the log file for only these devices", "display_zone_format": "EVENT LOG ZONE DISPLAY NAME - How the Zone name is displayed in sensors and the Event Log", "device_tracker_state_source": "DEVICE TRACKER STATE VALUE - How the device's device_tracker entity state value is determined", "time_format": "TIME FORMAT - How time fields are displayed in sensors and in the Event Log", "unit_of_measurement": "UNIT OF MEASUREMENT - How distance fields are displayed in sensors and in the Event Log", "display_gps_lat_long2": "DISPLAY GPS COORDINATES - Display the GPS (Latitude, Longitude/±Accuracy) or only the GPS (/±Accuracy) in the Event Log", "display_gps_lat_long": "DISPLAY GPS COORDINATES - Display GPS-(22.32771, -76.33073/±35m) instead of GPS-/±35m in the Event Log", + "evlog_header": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ EVENT LOG SYSTEM OVERRIDES", + "picture_www_dirs": "PICTURE DIRECTORY FILTER - Select the directories containg Device image files (.png, .jpg)", + "event_log_card_directory": "EVENT LOG CARD LOVELACE RESOURCES DIRECTORY - Event Log custom card .js file directory", + "event_log_btnconfig_url": "EVENT LOG CONFIGURE BUTTON (GEAR) URL > Special URL that display's the HA Configure Settings screen", "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { - "log_level": "iCloud3 can log configuration parameters, startup activity and errors, tracking activity, error messages and the Apple Account requests for iCloud Device location RawData information (request and response). Log levels specify the type of records that should be written to the iCloud3 log file (`icloud3-0.log`) from basic (Info) to more detailed (Debug) to extremely detailed (RawData).", - "device_tracker_state_source": "HA uses the device's gps coordinates to determine the zone. The gps accuracy is not considered so the zone may be exited when the gps wanders out of the zone. iCloud3 does consider the gps accuracy and will not exit the zone when this occurs. iCloud3 will display the Zone's Friendly Name or zone name displayed on the Event Log." + "picture_www_dirs": "iCloud3 scans the /www for all picture files (.png, .jpg) to build a table for selecting the picture for a device. The picture can be hard to find if you have many image files in many directories. Specify the directories with your device image files here", + "device_tracker_state_source": "HA uses the device's gps coordinates to determine the zone. The gps accuracy is not considered so the zone may be exited when the gps wanders out of the zone. iCloud3 does consider the gps accuracy and will not exit the zone when this occurs. iCloud3 will display the Zone's Friendly Name or zone name displayed on the Event Log", + "event_log_btnconfig_url": "Normally, this is blank and iCloud3 will determine the URL for it's Configure Settings screen. However, if there is a problem caused by running HA in a virtual environment, docker or on another device and the actual URL can not be detemined, a 404 not found error may be encountered. If that happens, select it the normal way (HA Devices & Services > Integration > iCloud3 > Configure Settings gear) and copy the URL from the browser into this field." } }, "display_text_as": { @@ -401,15 +452,6 @@ "data_description": { } }, - "actions": { - "title": "iCloud3 Action Commands", - "data": { - "ic3_actions": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ICLOUD3 GENERAL CONTROL ACTIONS", - "debug_actions": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ DEBUG LOG ACTIONS", - "other_actions": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ OTHER ACTIONS", - "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" - } - }, "inzone_intervals": { "title": "inZone Parameters and Default Intervals", "description": "An inZone interval is the time between location requests when the Device is in a zone.\n\n This screen is used to set the default values for different types of devices. This value is assigned to a device when it is added.\n\nNote: The inZone Interval can be set to a different value on the Update Device screen for each device.", @@ -495,41 +537,6 @@ "filtered_sensors": "ICLOUD3 SENSORS - A list of Sensors that are created when iCloud3 starts", "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } - }, - "tracking_parameters": { - "title": "Tracking & Other Parameters", - "description": "Some parameters do not fall into any of the other general categories and are rarely changed.\n\nThis screen is used to specify those parameters.", - "data": { - "log_level": "LOG LEVEL - The type of messages that are added to the HA log file during iCloud3 operations", - "gps_accuracy_threshold": "GPS ACCURACY THRESHOLD", - "old_location_threshold": "OLD LOCATION THRESHOLD", - "old_location_adjustment": "OLD LOCATION ADJUSTMENT", - "distance_between_devices": "USE LOCATION RESULTS FROM A NEAR-BY DEVICE", - "max_interval": "MAXIMUM INTERVAL", - "exit_zone_interval": "EXIT ZONE INTERVAL", - "mobapp_alive_interval": "REQUEST MOBILE APP LOCATION INTERVAL", - "tfz_tracking_max_distance": "TRACK-FROM-ZONE DISPLAY DISTANCE", - "offline_interval": "DEVICE OFFLINE INTERVAL", - "travel_time_factor": "TRAVEL TIME INTERVAL AND NEXT LOCATION UPDATE MULTIPLIER", - "discard_poor_gps_inzone": "DISCARD POOR RESULTS - Discard Location Updates with Poor GPS Accuracy when in a Zone", - "picture_www_dirs": "WWW DIRECTORIES WITH PICTURE IMAGES - Filter for `Update Devices > Pictures` file locations", - "event_log_card_directory": "EVENT LOG CARD LOVELACE RESOURCES DIRECTORY - Event Log custom card .js file directory", - "event_log_btnconfig_url": "EVENT LOG CONFIGURE BUTTON (GEAR) URL > Special URL that display's the HA Configure Settings screen", - "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" - }, - "data_description": { - "old_location_threshold": "Locations older than this value will be discarded", - "old_location_adjustment": "Add this to the time that determines if a location is old", - "distance_between_devices": "When tracking results are updated, any nearby devices are identified. When tracking results for those devices are updated, the tracking results of the one originally updated can be used instead. This improves performance since the Waze route time and distance are not requested again", - "gps_accuracy_threshold": "Locations with GPS Accuracy above this value will be discarded", - "tfz_tracking_max_distance": "Normally the Home zone's time and distance data is displayed on the Device's device_tracker and sensor entities. Display the Track-from-Zone instead when the Device is within this distance of the Track-from-Zone", - "max_interval": "The maximum time between location requests", - "exit_zone_interval": "The time to the first location request after exiting a zone", - "mobapp_alive_interval": "Send a location request to the Mobile App if there has been no contact after this amount of time. This will check to see if the Mobile App is responding to location requests or is asleep and not running.", - "offline_interval": "Location request interval when offline (Airplane mode, dead cell area, etc.)", - "travel_time_factor": "This is used to calculate the Interval and Next Location Time when going towards Home. A smaller value will reduce the interval time and increase the location requests, a larger value will increase the interval and reduce the location requests.", - "event_log_btnconfig_url": "Normally, this is blank and iCloud3 will determine the URL for it's Configure Settings screen. However, if there is a problem caused by running HA in a virtual environment, docker or on another device and the actual URL can not be detemined, a 404 not found error may be encountered. If that happens, select it the normal way (HA Devices & Services > Integration > iCloud3 > Configure Settings gear) and copy the URL from the browser into this field." - } } } },