From 91d89d43e8fcbd953ddc568f72fcc71225338e2c Mon Sep 17 00:00:00 2001 From: gcobb321 Date: Wed, 25 Oct 2023 17:08:56 -0400 Subject: [PATCH] iCloud3 v3.0.rc8 --- README.md | 2 +- custom_components/icloud3/ChangeLog.txt | 28 ++ custom_components/icloud3/config_flow.py | 293 ++++++++++++------ custom_components/icloud3/const.py | 3 +- custom_components/icloud3/device.py | 17 +- custom_components/icloud3/device_tracker.py | 54 ++-- .../icloud3-event-log-card.js.gz | Bin 0 -> 22114 bytes custom_components/icloud3/global_variables.py | 7 +- .../icloud3/helpers/time_util.py | 11 + custom_components/icloud3/icloud3_main.py | 212 +++++++++---- custom_components/icloud3/sensor.py | 4 +- custom_components/icloud3/strings.json | 103 +++--- .../icloud3/support/config_file.py | 37 ++- .../icloud3/support/determine_interval.py | 8 +- .../icloud3/support/icloud_data_handler.py | 13 +- .../icloud3/support/iosapp_data_handler.py | 20 +- .../icloud3/support/pyicloud_ic3.py | 16 +- .../icloud3/support/pyicloud_ic3_interface.py | 8 +- .../icloud3/support/service_handler.py | 7 +- .../icloud3/support/start_ic3.py | 24 +- .../icloud3/support/stationary_zone.py | 44 ++- .../icloud3/translations/en.json | 103 +++--- custom_components/icloud3/zone.py | 14 +- 23 files changed, 673 insertions(+), 355 deletions(-) create mode 100644 custom_components/icloud3/event_log_card/icloud3-event-log-card.js.gz diff --git a/README.md b/README.md index b5bf721..c1cb060 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ------ -### Prerelease Version - v3.0-pr1.4 (9/22/2023) +###Release Candidate 8 - v3.0.rc8 (10/25/2023) ------ diff --git a/custom_components/icloud3/ChangeLog.txt b/custom_components/icloud3/ChangeLog.txt index 9e20f06..e00e084 100644 --- a/custom_components/icloud3/ChangeLog.txt +++ b/custom_components/icloud3/ChangeLog.txt @@ -1,3 +1,31 @@ +rc8 - 10/25/2023 +............................... +1. Configure Settings - Made the following changes: + 1. Minor text changes to field names and screens. + 2. Improved the error messages when experiencing a problem logging into the iCloud Account. + 3. Fixed a bug when selecting the Enter Verification Code screen before logging into the iCloud account. +2. Old Location Error Notifications - Fixed a problem where Old Location notifications were constantly being sent to a phone when the maximum number of old location errors (20) had been encountered. +3. iOSApp Location Time - If the location time is older than 3-hours, it's age is displayed (4.5 hrs ago) instead of the time (6:15:00p). + +rc7.1/rc7.1.1 - 10/15/2023 +............................... +1. Zone-Device Count bug fix - Fixed a bug where the device counts were not being displayed correctly. +2. Exit Zone for Devices without the iOSApp (Watch) - When a Device exits a zone, all other devices that were in the same zone that do not have the iOS App installed will be updated immediately. They were being updated when their next update timer was reached. Hopefully, this will make Watch zone exit updates to be done when they happen. +3. Apple account password - When iCloud3 starts, the password is checked to see if it is encoded in the configuration parameter file. If it is not and it should be, it will be encoded and the configuration file will be updated. Previously, there were times when the file was not being updated. +4. iCloud Account username/password changes - When the username/password is changed, the Apple account is logged into. If you select 'Save' the configuration file is updated. If you select 'Return', the updated username/password is not saved and the menu is displayed. This can lead to login problems the next time iCloud3 starts if you really wanted to save them but didn't. An additional Confirmation Screen is now displayed that lets you save them or not save them. + + +rc7 - 10/15/2023 +............................... +1. yaml Zones - Fixed a problem where zones configured using yaml were not being loaded when iCloud3 started. +2. Stationary Zones - Minor changes to the handling of deleting a stationary zone when all devices had exited from it. +3. Zone-Devices Count - New feature - The number of the devices within a zone is displayed with the tracking results on the Event Log. The counts are the numbers (x) after the zone name. For Example: + _Zone > Away (2) > Home-2.45km, IndRivShores-6.53km, School-8.47km (1), Publix-10.3km, ThePoint-11.0km, Quail-12.0km, Warehouse-16.5km (1), GPS-(/±47m)_ + + An item is also posted to the Event Log when another device changes it's zone: + _Zone-Device Counts > Home (4), School (1), Warehouse (1)_ + + rc6 - Release Candidate 6 (10/7/2023) ............................... 1. Bug Fix - Fixed the error causing the "AttributeError: 'iCloud3_Device' object has no attribute 'interval_secs' message at line 680, in determine_interval_after_error" error. diff --git a/custom_components/icloud3/config_flow.py b/custom_components/icloud3/config_flow.py index 833b629..8eeaa67 100644 --- a/custom_components/icloud3/config_flow.py +++ b/custom_components/icloud3/config_flow.py @@ -106,23 +106,25 @@ def dict_value_to_list(key_value_dict): return value_list #----------------------------------------------------------------------------------------- MENU_KEY_TEXT = { - 'icloud_account': 'iCLOUD ACCOUNT & iOS APP ᐳ –Set iCloud Account Username/Password, –Set Location Data Sources', - 'device_list': 'ICLOUD3 DEVICES ᐳ –Add, Change and Delete tracked and monitored devices', - 'verification_code': 'ENTER/REQUEST AN APPLE ID VERIFICATION CODE ᐳ –Enter or Request the 6-digit Apple ID Verification Code', - 'change_device_order': 'CHANGE DEVICE ORDER ᐳ –Change the tracking order of the Devices and their display sequence on the Event Log', - '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', - - 'format_settings': 'FORMAT SETTINGS ᐳ –Log Level, –Zones Display Format, –DeviceTracker State, –Unit of Measure, –Time & Distance, Display GPS Coordinates', - 'display_text_as': 'DISPLAY TEXT AS ᐳ –Event Log Custom Card Text Replacement', - 'waze': 'WAZE ROUTE DISTANCE, TIME & HISTORY ᐳ –Set Route Server Location, Min/Max Intervals, etc. –Set Waze History Database Parameters and Controls', - 'inzone_intervals': 'INZONE DEFAULT INTERVALS ᐳ –Default inZone intervals for different device types and the iOS App, –Other inZone Controls ', - 'special_zones': 'SPECIAL ZONES ᐳ –Enter Zone Delay, –Stationary Zone, –Track From Zone', - 'tracking_parameters': 'TRACKING & OTHER PARAMETERS ᐳ –Set Nearby Device Info, Accuracy Thresholds & Other Location Request Intervals, –Set Event Log Custom Card Directory, etc.', + 'icloud_account': 'iCLOUD ACCOUNT & iOS APP ᐳ •Set iCloud Account Username/Password, •Set Location Data Sources', + # 'icloud_account': 'iCLOUD ACCOUNT & iOS APP ᐳ •Set iCloud Account Username/Password, •Set Location Data Sources', + 'device_list': 'ICLOUD3 DEVICES ᐳ •Add, Change and Delete tracked and monitored devices', + # 'device_list': 'ICLOUD3 DEVICES ᐳ •Add, Change and Delete tracked and monitored devices', + 'verification_code': 'ENTER/REQUEST AN APPLE ID VERIFICATION CODE ᐳ •Enter or Request the 6-digit Apple ID Verification Code', + 'change_device_order': 'CHANGE DEVICE ORDER ᐳ •Change the tracking order of the Devices and their display sequence on the Event Log', + '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', + + 'format_settings': 'FORMAT SETTINGS ᐳ •Log Level, •Zones Display Format, •DeviceTracker State, •Unit of Measure, •Time & Distance, Display GPS Coordinates', + 'display_text_as': 'DISPLAY TEXT AS ᐳ •Event Log Custom Card Text Replacement', + 'waze': 'WAZE ROUTE DISTANCE, TIME & HISTORY ᐳ •Set Route Server Location, Min/Max Intervals, etc. •Set Waze History Database Parameters and Controls', + 'inzone_intervals': 'INZONE DEFAULT INTERVALS ᐳ •Default inZone intervals for different device types and the iOS App, •Other inZone Controls ', + 'special_zones': 'SPECIAL ZONES ᐳ •Enter Zone Delay, •Stationary Zone, •Track From Zone', + 'tracking_parameters': 'TRACKING & OTHER PARAMETERS ᐳ •Set Nearby Device Info, Accuracy Thresholds & Other Location Request Intervals, •Set Event Log Custom Card Directory, etc.', 'select': 'SELECT ᐳ Select the parameter update form', - 'next_page_0': 'PAGE 1, DEVICES & SENSORS ᐳ –iCloud Account & iOS App, –iCloud3 Devices, –Enter & Request Verification Code, –Change Device Order, –Sensors, –Action Commands', - 'next_page_1': 'PAGE 2, GENERAL PARAMETERS ᐳ –Format Parameters, –Display Text As, –Waze Route Distance, Time & History, –inZone Intervals, –Special Zones, – Other Parameters', + 'next_page_0': 'PAGE 1, DEVICES & SENSORS ᐳ •iCloud Account & iOS App, •iCloud3 Devices, •Enter & Request Verification Code, •Change Device Order, •Sensors, •Action Commands', + 'next_page_1': 'PAGE 2, GENERAL PARAMETERS ᐳ •Format Parameters, •Display Text As, •Waze Route Distance, Time & History, •inZone Intervals, •Special Zones, • Other Parameters', 'exit': f'EXIT AND RESTART ICLOUD3 {".. "*22}(Version: {Gb.version})' } MENU_PAGE_0_INITIAL_ITEM = 1 @@ -169,9 +171,9 @@ def dict_value_to_list(key_value_dict): 'delete_device': 'DELETE DEVICE(S), OTHER DEVICE MAINTENANCE ᐳ Delete the device(s) from the tracked device list, clear the FamShr/FmF/iOS App selection fields', 'change_device_order': 'CHANGE DEVICE ORDER ᐳ Change the tracking order of the Devices and their display sequence on the Event Log', - 'delete_this_device': 'DELETE THIS DEVICE ᐳ Delete this device from the iCloud3 tracked devices list', + 'delete_this_device': 'DELETE THIS DEVICE ᐳ Delete this device → ', 'delete_all_devices': 'DELETE ALL DEVICES ᐳ Delete all devices from the iCloud3 tracked devices list', - 'delete_icloud_iosapp_info':'CLEAR FAMSHR/FMF/IOSAPP INFO ᐳ Reset the FamShr/FmF/iOS App seletion fields on all devices', + 'delete_icloud_iosapp_info':'CLEAR FAMSHR/IOSAPP INFO ᐳ Reset the FamShr/iOS App seletion fields on all devices', 'delete_device_cancel': 'CANCEL ᐳ Return to the Device List screen', 'inactive_to_track': 'TRACK ALL OR SELECTED ᐳ Change the \'Tracking Mode\' of all of the devices (or the selected devices) from \'Inactive\' to \Tracked\'', @@ -193,10 +195,13 @@ def dict_value_to_list(key_value_dict): 'move_down': 'MOVE DOWN ᐳ Move the Device down in the list', 'save': 'SAVE ᐳ Update Configuration File, Return to the menu screen', - 'cancel': 'RETURN ᐳ Return to the previous screen. Cancel any changes that are not already saved', + 'cancel': 'RETURN ᐳ Return to the previous screen. Cancel any unsaved changes', 'exit': 'EXIT ᐳ Exit the iCloud3 Configurator', 'return': 'RETURN ᐳ Return to the Main Menu', + 'confirm_return': 'RETURN WITHOUT SAVING CONFIGURATION CHANGES ᐳ Return to the Main Menu without saving any changes', + 'confirm_save': 'SAVE THE CONFIGURATION CHANGES ᐳ Save any changes, then return to the Main Menu', + "divider1": "═══════════════════════════════════════", "divider2": "═══════════════════════════════════════", "divider3": "═══════════════════════════════════════" @@ -407,6 +412,29 @@ def dict_value_to_list(key_value_dict): for key, text in ACTIONS_SCREEN_ITEMS_KEY_TEXT.items() if key.startswith('divider') is False} +ACTIONS_IC3_ITEMS = { + "restart": "RESTART ᐳ Restart iCloud3", + "pause": "PAUSE ᐳ Pause polling on all devices", + "resume": "RESUME ᐳ Resume Polling on all devices, Refresh all locations", +} +ACTIONS_DEBUG_ITEMS = { + "debug_start": "START DEBUG LOGGING ᐳ Start or stop debug logging", + "debug_stop": "STOP DEBUG LOGGING ᐳ Start or stop debug logging", + "rawdata_start": "START RAWDATA LOGGING ᐳ Start or stop debug rawdata logging", + "rawdata_stop": "STOP RAWDATA LOGGING ᐳ Start or stop debug rawdata logging", + "commit": "COMMIT DEBUG LOG RECORDS ᐳ Verify all debug log file records are written", +} +ACTIONS_OTHER_ITEMS = { + "evlog_export": "EXPORT EVENT LOG ᐳ Export Event Log data", + "wazehist_maint": "WAZE HIST DATABASE ᐳ Recalc time/distance data at midnight", + "wazehist_track": "WAZE HIST MAP TRACK ᐳ Load route locations for map display", +} +ACTIONS_ACTION_ITEMS = { + "restart_ha": "RESTART HA, RELOAD ICLOUD3 ᐳ Restart HA or Reload iCloud3", + "return": "MAIN MENU ᐳ Return to the Main Menu" +} + + WAZE_USED_HEADER = ("The Waze Route Service provides the travel time and distance information from your " "current location to the Home or another tracked from zone. This information is used to determine " "when the next location request should be made") @@ -1168,6 +1196,49 @@ async def async_step_change_device_order(self, user_input=None, errors=None, cal data_schema=self.form_schema(self.step_id), errors=self.errors) +#------------------------------------------------------------------------------------------- + async def async_step_confirm_action(self, user_input=None, action_items=None, + called_from_step_id=None): + ''' + Confirm an action - This will display a screen containing the action_items. + + Parameters: + action_items - The action_item keys in the ACTION_LIST_ITEMS_KEY_TEXT dictionary. + The last key is the default item on the confirm actions screen. + called_from_step_id - The name of the step to return to. + + Notes: + Before calling this function, set the self.user_input_multi_form to the user_input. + This will preserve all parameter changes in the calling screen. They are + returned to the called from step on exit. + Action item - The action_item selected on this screen is added to the + self.user_input_multi_form variable returned. It is resolved in the calling + step in the self._action_text_to_item function in the calling step. + On Return - Set the function to return to for the called_from_step_id. + ''' + self.step_id = 'confirm_action' + self.errors = {} + self.errors_user_input = {} + self.called_from_step_id_1 = called_from_step_id or self.called_from_step_id_1 or 'menu' + + if action_items is not None: + actions_list = [] + for action_item in action_items: + actions_list.append(ACTION_LIST_ITEMS_KEY_TEXT[action_item]) + + return self.async_show_form(step_id=self.step_id, + data_schema=self.form_schema(self.step_id, + actions_list=actions_list), + errors=self.errors) + + user_input, action_item = self._action_text_to_item(user_input) + self.user_input_multi_form['action_item'] = action_item + + if self.called_from_step_id_1 == 'icloud_account': + return await self.async_step_icloud_account(user_input=self.user_input_multi_form) + + return await self.async_step_menu() + #------------------------------------------------------------------------------------------- def _set_example_zone_name(self): ''' @@ -1512,40 +1583,41 @@ async def async_step_actions(self, user_input=None, errors=None): # Get key for item selected ("RESTART" --> "restart") and then # process the requested action - if instr(user_input.get('action_items'), ' >'): - action_item_text = user_input['action_items'] - action_item = ACTIONS_SCREEN_ITEMS_KEY_BY_TEXT[action_item_text] - user_input.pop('action_items') - - # await self._process_action_request(action_item) - if action_item == 'return': - return await self.async_step_menu() + if user_input['action_items']: + action_item =user_input['action_items'][0] + elif user_input['ic3_actions']: + action_item = user_input['ic3_actions'][0] + elif user_input['debug_actions']: + action_item = user_input['debug_actions'][0] + elif user_input['other_actions']: + action_item = user_input['other_actions'][0] + else: + action_item = 'return' - elif action_item in [ 'restart', 'pause', 'resume', - 'wazehist_maint', 'wazehist_track', - 'evlog_export', ]: - service_handler.update_service_handler(action_item) + if action_item == 'return': + return await self.async_step_menu() - elif action_item.startswith('debug'): - service_handler.handle_action_log_level('debug', change_conf_log_level=False) + elif action_item in [ 'restart', 'pause', 'resume', + 'wazehist_maint', 'wazehist_track', + 'evlog_export', ]: + service_handler.update_service_handler(action_item) - elif action_item.startswith('rawdata'): - service_handler.handle_action_log_level('rawdata', change_conf_log_level=False) + elif action_item.startswith('debug'): + service_handler.handle_action_log_level('debug', change_conf_log_level=False) - elif action_item == 'commit': - close_reopen_ic3_log_file(closed_by='Configurator') + elif action_item.startswith('rawdata'): + service_handler.handle_action_log_level('rawdata', change_conf_log_level=False) - elif action_item == 'restart_ha': - return await self.async_step_restart_ha_ic3() + elif action_item == 'commit': + close_reopen_ic3_log_file(closed_by='Configurator') - if self.header_msg is None: - self.header_msg = 'action_completed' + elif action_item == 'restart_ha': + return await self.async_step_restart_ha_ic3() - return await self.async_step_menu() + if self.header_msg is None: + self.header_msg = 'action_completed' - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), - errors=self.errors) + return await self.async_step_menu() #-------------------------------------------------------------------------------- async def _process_action_request(self, action_item): @@ -1607,7 +1679,7 @@ async def async_restart_ha_ic3(self, user_input, errors): await Gb.hass.services.async_call( "homeassistant", "reload_config_entry", - {'device_id': Gb.dr_device_id_by_devicename[ICLOUD3]}, + {'device_id': Gb.ha_device_id_by_devicename[ICLOUD3]}, ) # {"entry_id": Gb.entry_id} @@ -1915,13 +1987,28 @@ async def async_step_icloud_account(self, user_input=None, errors=None, called_f user_input, action_item = self._action_text_to_item(user_input) user_input = self._strip_spaces(user_input, [CONF_USERNAME, CONF_PASSWORD]) user_input = self._strip_spaces(user_input) + user_input['endpoint_suffix'] = 'cn' if user_input['url_suffix_china'] is True else 'None' log_user_input = user_input.copy() if CONF_USERNAME in log_user_input: log_user_input[CONF_USERNAME] = obscure_field(log_user_input[CONF_USERNAME]) if CONF_PASSWORD in log_user_input: log_user_input[CONF_PASSWORD] = obscure_field(log_user_input[CONF_PASSWORD]) log_debug_msg(f"{self.step_id} ({action_item}) > UserInput-{log_user_input}, Errors-{errors}") - if action_item == 'cancel': + if action_item == 'confirm_save': + action_item = 'save' + + elif action_item == 'confirm_return': + return await self.async_step_menu() + + elif action_item == 'cancel': + if (Gb.username != user_input[CONF_USERNAME] + or Gb.password != user_input[CONF_PASSWORD] + or Gb.icloud_server_endpoint_suffix != user_input['endpoint_suffix']): + self.user_input_multi_form = user_input.copy() + + return await self.async_step_confirm_action(user_input, + action_items = ['confirm_save', 'confirm_return'], + called_from_step_id='icloud_account') return await self.async_step_menu() # Data Source is iOS App only, iCloud was not selected @@ -1931,7 +2018,10 @@ async def async_step_icloud_account(self, user_input=None, errors=None, called_f return await self.async_step_menu() if action_item == 'verification_code': - return await self.async_step_reauth(called_from_step_id='icloud_account') + if self.PyiCloud or Gb.PyiCloud: + return await self.async_step_reauth(called_from_step_id='icloud_account') + else: + action_item = 'login_icloud_account' if user_input[CONF_USERNAME] == '': self.errors[CONF_USERNAME] = 'required_field' @@ -1950,8 +2040,6 @@ async def async_step_icloud_account(self, user_input=None, errors=None, called_f Gb.conf_data_source_FMF = instr(user_input[CONF_DATA_SOURCE], FMF) Gb.primary_data_source_ICLOUD = Gb.conf_data_source_FAMSHR or Gb.conf_data_source_FMF - user_input['endpoint_suffix'] = 'cn' if user_input['url_suffix_china'] is True else 'None' - # Action Login or Save will login into the account if the username changed if (action_item in ['login_icloud_account', 'save']): @@ -2034,10 +2122,13 @@ async def async_step_reauth(self, user_input=None, errors=None, called_from_step user_input, action_item = self._action_text_to_item(user_input) log_debug_msg(f"{self.step_id} ({action_item}) > UserInput-{user_input}, Errors-{errors}") - if self.username == Gb.PyiCloud.username and self.password == Gb.PyiCloud.password: + if Gb.PyiCloud and self.username == Gb.PyiCloud.username and self.password == Gb.PyiCloud.password: PyiCloud = Gb.PyiCloud - else: + elif self.PyiCloud: PyiCloud = self.PyiCloud + else: + self.errors = 'icloud_acct_not_logged_into' + action_item = 'cancel' if action_item == 'send_verification_code' and user_input.get(CONF_VERIFICATION_CODE, '') == '': action_item = 'cancel' @@ -2069,7 +2160,7 @@ async def async_step_reauth(self, user_input=None, errors=None, called_from_step post_event(f"{EVLOG_NOTICE}iCLOUD ALERT > Apple ID Verification complete") Gb.EvLog.clear_alert() - Gb.force_icloud_update_flag = True + Gb.icloud_force_update_flag = True PyiCloud.new_2fa_code_already_requested_flag = False self.errors['base'] = self.header_msg = 'verification_code_accepted' @@ -2157,8 +2248,10 @@ async def _log_into_icloud_account(self, user_input, called_from_step_id=None, r event_msg = f"{EVLOG_NOTICE}Requesting Apple ID Verification Code" else: event_msg =(f"{EVLOG_NOTICE}Logging into iCloud Account with Configure Settings, " - f"{CRLF_DOT}New iCloud Account > {obscure_field(self.username)}, " - f"{CRLF_DOT}iCloud Account Currently Used > {obscure_field(Gb.username)}") + f"{CRLF_DOT}iCloud Account Currently Used > {obscure_field(Gb.username)}" + f"{CRLF_DOT}New iCloud Account > {obscure_field(self.username)}") + if self.endpoint_suffix != 'None': + event_msg += f", AppleServerURLSuffix-{self.endpoint_suffix}" post_event(event_msg) try: @@ -2173,13 +2266,20 @@ async def _log_into_icloud_account(self, user_input, called_from_step_id=None, r except (PyiCloudFailedLoginException) as err: + err = str(err) + self.PyiCloud = None self.endpoint_suffix = Gb.icloud_server_endpoint_suffix = \ Gb.conf_tracking[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] if called_from_step_id == 'icloud_account': - error_msg = 'icloud_acct_login_error' + if err.endswith('302'): + error_msg = 'icloud_acct_login_error_connection' + elif err.endswith('400'): + error_msg = 'icloud_acct_login_error_user_pw' + else: + error_msg = 'icloud_acct_login_error_other' else: - error_msg = 'icloud_acct_not_available' + error_msg = 'icloud_acct_login_error_other' self.errors = {'base': error_msg} _CF_LOGGER.error(f"Error logging into iCloud service: {err}") @@ -3467,15 +3567,22 @@ def _action_text_to_item(self, user_input): return None, None action_text = None - if 'action_items' in user_input: + if 'action_item' in user_input: + action_item = user_input['action_item'] + user_input.pop('action_item') + + elif 'action_items' in user_input: action_text = user_input['action_items'] - if action_text.startswith('NEXT PAGE ITEMS > '): + + if action_text.startswith('NEXT PAGE ITEMS'): action_item = 'next_page_items' else: action_text_len = 25 if len(action_text) > 25 else len(action_text) action_item = [k for k, v in ACTION_LIST_ITEMS_KEY_TEXT.items() if v.startswith(action_text[:action_text_len])][0] - user_input.pop('action_items') + if 'actions_items' in user_input: + user_input.pop('action_items') + else: action_item = None @@ -3777,12 +3884,12 @@ def _discard_changes(self, user_input): # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - def form_schema(self, step_id): + def form_schema(self, step_id, actions_list=None, actions_list_default=None): ''' Return the step_id form schema for the data entry forms ''' schema = {} - self.actions_list = ACTION_LIST_ITEMS_BASE.copy() + self.actions_list = actions_list or ACTION_LIST_ITEMS_BASE.copy() if step_id == 'menu': menu_action_items = MENU_ACTION_ITEMS.copy() @@ -3814,8 +3921,18 @@ def form_schema(self, step_id): return schema #------------------------------------------------------------------------ - elif step_id == 'restart_icloud3': + elif step_id.startswith('confirm_action'): + actions_list_default = actions_list_default or self.actions_list[0] + return vol.Schema({ + vol.Required('action_items', + default=actions_list_default): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + + #------------------------------------------------------------------------ + elif step_id == 'restart_icloud3': self.actions_list = [] restart_default='restart_ic3_now' @@ -4107,12 +4224,11 @@ def form_schema(self, step_id): #------------------------------------------------------------------------ elif step_id == 'delete_device': self.actions_list = DELETE_DEVICE_ACTIONS.copy() - device_info = ( f"Delete this device (" - f"{self.conf_device_selected[CONF_IC3_DEVICENAME]}, " - f"{self.conf_device_selected[CONF_FNAME]})") + device_info = ( f"{self.conf_device_selected[CONF_IC3_DEVICENAME]}, " + f"{self.conf_device_selected[CONF_FNAME]}") # The first item is 'Delete this device, add the selected device's info - self.actions_list[0] = self.actions_list[0].replace('Delete this device', device_info) + self.actions_list[0] = f"{self.actions_list[0]}{device_info}" return vol.Schema({ vol.Required('action_items', @@ -4123,28 +4239,36 @@ def form_schema(self, step_id): #------------------------------------------------------------------------ elif step_id == 'actions': - action_screen_items_key_text = ACTIONS_SCREEN_ITEMS_KEY_TEXT.copy() + debug_items_key_text = ACTIONS_DEBUG_ITEMS.copy() if Gb.log_debug_flag: - action_screen_items_key_text.pop('debug_start') + debug_items_key_text.pop('debug_start') else: - action_screen_items_key_text.pop('debug_stop') + debug_items_key_text.pop('debug_stop') if Gb.log_rawdata_flag: - action_screen_items_key_text.pop('rawdata_start') + debug_items_key_text.pop('rawdata_start') else: - action_screen_items_key_text.pop('rawdata_stop') - self.actions_list = [text for text in action_screen_items_key_text.values()] + debug_items_key_text.pop('rawdata_stop') return vol.Schema({ - vol.Required('action_items', - default=self.action_default_text('return', action_itemss_key_text=action_screen_items_key_text)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), + vol.Optional('ic3_actions', default=[]): + cv.multi_select(ACTIONS_IC3_ITEMS), + # selector.SelectSelector(selector.SelectSelectorConfig( + # options=dict_value_to_list(ACTIONS_IC3_ITEMS), mode='list')), + vol.Optional('debug_actions', default=[]): + cv.multi_select(debug_items_key_text), + # selector.SelectSelector(selector.SelectSelectorConfig( + # options=dict_value_to_list(debug_items_key_text), mode='list')), + vol.Optional('other_actions', default=[]): + cv.multi_select(ACTIONS_OTHER_ITEMS), + # selector.SelectSelector(selector.SelectSelectorConfig( + # options=dict_value_to_list(ACTIONS_OTHER_ITEMS), mode='list')), + vol.Optional('action_items', default=[]): + cv.multi_select(ACTIONS_ACTION_ITEMS), + # selector.SelectSelector(selector.SelectSelectorConfig( + # options=dict_value_to_list(ACTIONS_ACTION_ITEMS), mode='list')), }) #------------------------------------------------------------------------ elif step_id == 'format_settings': - # self.actions_list = [ACTION_LIST_ITEMS_KEY_TEXT['change_device_order']] - # self.actions_list.extend(ACTION_LIST_ITEMS_BASE) - self._set_example_zone_name() return vol.Schema({ vol.Required(CONF_LOG_LEVEL, @@ -4431,17 +4555,6 @@ def form_schema(self, step_id): default=Gb.conf_general[CONF_STAT_ZONE_INZONE_INTERVAL]): selector.NumberSelector(selector.NumberSelectorConfig( min=5, max=60, unit_of_measurement='minutes')), - - # vol.Optional('base_offset_header', - # default=sbzh_default): - # cv.multi_select([STAT_ZONE_BASE_HEADER]), - # vol.Required(CONF_STAT_ZONE_BASE_LATITUDE, - # default=Gb.conf_general[CONF_STAT_ZONE_BASE_LATITUDE]): - # selector.NumberSelector(selector.NumberSelectorConfig(min=-90, max=90)), - # vol.Required(CONF_STAT_ZONE_BASE_LONGITUDE, - # default=Gb.conf_general[CONF_STAT_ZONE_BASE_LONGITUDE]): - # selector.NumberSelector(selector.NumberSelectorConfig(min=-180, max=180)), - vol.Optional('track_from_zone_header', default=tfzh_default): cv.multi_select([TRK_FROM_HOME_ZONE_HEADER]), diff --git a/custom_components/icloud3/const.py b/custom_components/icloud3/const.py index 2842e60..6a7c6b3 100644 --- a/custom_components/icloud3/const.py +++ b/custom_components/icloud3/const.py @@ -4,7 +4,7 @@ # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -VERSION = '3.0.rc6' +VERSION = '3.0.rc8' DOMAIN = 'icloud3' ICLOUD3 = 'iCloud3' @@ -328,6 +328,7 @@ 'Not_Home': 'Away', 'not_set': '──', 'Not_Set': '──', + '──': 'NotSet', # 'stationary': 'Stationary', # 'Stationary': 'Stationary', STATIONARY: STATIONARY_FNAME, diff --git a/custom_components/icloud3/device.py b/custom_components/icloud3/device.py index 605263e..ab1f53f 100644 --- a/custom_components/icloud3/device.py +++ b/custom_components/icloud3/device.py @@ -79,15 +79,15 @@ class iCloud3_Device(TrackerEntity): def __init__(self, devicename, conf_device): self.conf_device = conf_device self.devicename = devicename - self.dr_device_id = '' # ha device_registry device_id + self.ha_device_id = '' # ha device_registry device_id self.fname = devicename.title() self.StatZone = None # The StatZone this Device is in or None if not in a StatZone #self.stationary_zonename = (f"{self.devicename}_{STATIONARY}") - self.FromZones_by_zone = {} # DeviceFmZones objects for the track_from_zones parameter for this Device - self.FromZone_Home = None # DeviceFmZone object for the Home zone - self.from_zone_names = [] # List of the from_zones in the FromZones_by_zone dictionary + self.FromZones_by_zone = {} # DeviceFmZones objects for the track_from_zones parameter for this Device + self.FromZone_Home = None # DeviceFmZone object for the Home zone + self.from_zone_names = [] # List of the from_zones in the FromZones_by_zone dictionary self.only_track_from_home = True # Track from only Home (True) or also track from other zones (False) self.FromZone_BeingUpdated = None # DeviceFmZone object being updated in determine_interval for EvLog TfZ info self.FromZone_NextToUpdate = None # Set to the DeviceFmZone when it's next_update_time is reached @@ -149,6 +149,8 @@ def initialize(self): # Trigger & Update variables self.trigger = 'iCloud3' + self.interval_secs = 0 + self.interval_str = '' self.next_update_secs = 0 self.seen_this_device_flag = False self.iosapp_zone_enter_secs = 0 @@ -247,7 +249,7 @@ def initialize(self): self.last_iosapp_trigger = '' # iCloud data update control variables - # self.icloud_update_needed_flag = False + self.icloud_force_update_flag = False # Bypass all update needed checks and force an iCloud update self.icloud_devdata_useable_flag = False self.icloud_acct_error_flag = False # An error occured from the iCloud account update request self.icloud_update_reason = 'Trigger > Initial Locate' @@ -430,8 +432,8 @@ def _link_device_entities_sensor_device_tracker(self): self.DeviceTracker = Gb.DeviceTrackers_by_devicename[self.devicename] self.DeviceTracker.Device = self try: - self.DeviceTracker.device_id = Gb.dr_device_id_by_devicename[self.devicename] - self.DeviceTracker.area_id = Gb.dr_area_id_by_devicename[self.devicename] + self.DeviceTracker.device_id = Gb.ha_device_id_by_devicename[self.devicename] + self.DeviceTracker.area_id = Gb.ha_area_id_by_devicename[self.devicename] except: pass @@ -1308,7 +1310,6 @@ def calculate_distance_moved(self): self.loc_data_time_moved_from = self.sensors[LAST_LOCATED_DATETIME] self.loc_data_time_moved_to = self.loc_data_datetime - #-------------------------------------------------------------------- def distance_m(self, to_latitude, to_longitude): to_gps = (to_latitude, to_longitude) diff --git a/custom_components/icloud3/device_tracker.py b/custom_components/icloud3/device_tracker.py index 8ac7b69..2161cb6 100644 --- a/custom_components/icloud3/device_tracker.py +++ b/custom_components/icloud3/device_tracker.py @@ -78,10 +78,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e area_reg = ar.async_get(hass) Gb.area_id_personal_device = area_reg.async_get_or_create('Personal Device').id - _get_dr_device_ids_from_device_registry(hass) + _get_ha_device_ids_from_device_registry(hass) - if (ICLOUD3 in Gb.dr_area_id_by_devicename - and Gb.dr_area_id_by_devicename[ICLOUD3] in [None, 'unknown', '']): + if (ICLOUD3 in Gb.ha_area_id_by_devicename + and Gb.ha_area_id_by_devicename[ICLOUD3] in [None, 'unknown', '']): _update_icloud3_integration_area_id() except Exception as err: @@ -111,11 +111,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e if NewDeviceTrackers != []: async_add_entities(NewDeviceTrackers, True) - _get_dr_device_ids_from_device_registry(hass) + _get_ha_device_ids_from_device_registry(hass) _HA_LOGGER.info(f"iCloud3 Device Tracker entities: {Gb.device_trackers_cnt}") Devices_no_area = [Device for Device in Gb.DeviceTrackers_by_devicename.values() \ - if Device.dr_area_id in [None, 'unknown', '']] + if Device.ha_area_id in [None, 'unknown', '']] if Devices_no_area != []: for Device in Devices_no_area: @@ -131,7 +131,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e log_error_msg(log_msg) #------------------------------------------------------------------------------------------- -def _get_dr_device_ids_from_device_registry(hass): +def _get_ha_device_ids_from_device_registry(hass): ''' Cycle thru the ha device registry, extract the iCloud3 entries and associate the ha device_id with the ic3_devicename parameters @@ -144,12 +144,12 @@ def _get_dr_device_ids_from_device_registry(hass): icloud3_dev_reg = {device: device_entry for device, device_entry in dev_reg.deleted_devices.items() if _icloud3_dev_reg_item(device_entry)} for device, device_entry in icloud3_dev_reg.items(): - _get_dr_device_id_from_device_entry(hass, device, device_entry) + _get_ha_device_id_from_device_entry(hass, device, device_entry) icloud3_dev_reg = {device: device_entry for device, device_entry in dev_reg.devices.items() if _icloud3_dev_reg_item(device_entry)} for device, device_entry in icloud3_dev_reg.items(): - _get_dr_device_id_from_device_entry(hass, device, device_entry) + _get_ha_device_id_from_device_entry(hass, device, device_entry) except Exception as err: log_exception(err) @@ -167,7 +167,7 @@ def _icloud3_dev_reg_item(device_entry): return False #------------------------------------------------------------------------------------------- -def _get_dr_device_id_from_device_entry(hass, device, device_entry): +def _get_ha_device_id_from_device_entry(hass, device, device_entry): ''' For each entry in the device registry, determine if it is an iCloud3 entry (iCloud3 is in the device_entry.identifiers field. If so, check the other items, determine if one is a @@ -188,8 +188,8 @@ def _get_dr_device_id_from_device_entry(hass, device, device_entry): ''' try: if device_entry.name in [DOMAIN, ICLOUD3, 'iCloud3 Integration']: - Gb.dr_device_id_by_devicename[ICLOUD3] = device_entry.id - Gb.dr_area_id_by_devicename[ICLOUD3] = device_entry.area_id + Gb.ha_device_id_by_devicename[ICLOUD3] = device_entry.id + Gb.ha_area_id_by_devicename[ICLOUD3] = device_entry.area_id return except: pass @@ -200,8 +200,8 @@ def _get_dr_device_id_from_device_entry(hass, device, device_entry): for item in de_identifiers: if item in Gb.conf_devicenames: - Gb.dr_device_id_by_devicename[item] = device_entry.id - Gb.dr_area_id_by_devicename[item] = device_entry.area_id + Gb.ha_device_id_by_devicename[item] = device_entry.id + Gb.ha_area_id_by_devicename[item] = device_entry.area_id return except Exception as err: @@ -214,11 +214,11 @@ def _update_icloud3_integration_area_id(): try: kwargs = {} kwargs['area_id'] = Gb.area_id_personal_device - Gb.dr_area_id_by_devicename[ICLOUD3] = Gb.area_id_personal_device + Gb.ha_area_id_by_devicename[ICLOUD3] = Gb.area_id_personal_device - device_id = Gb.dr_device_id_by_devicename[ICLOUD3] + ha_device_id = Gb.ha_device_id_by_devicename[ICLOUD3] device_registry = dr.async_get(Gb.hass) - dr_entry = device_registry.async_update_device(device_id, **kwargs) + dr_entry = device_registry.async_update_device(ha_device_id, **kwargs) log_debug_msg( "Device Tracker entity changed: device_tracker.icloud3, " "iCloud3, Personal Device") @@ -238,9 +238,9 @@ def __init__(self, devicename, conf_device, data=None): self.devicename = devicename self.Device = None # Filled in after Device object has been created in start_ic3 self.entity_id = f"device_tracker.{devicename}" - self.dr_device_id = Gb.dr_device_id_by_devicename.get(self.devicename) - self.dr_area_id = Gb.dr_area_id_by_devicename.get(self.devicename) - # if self.dr_area_id in ['unknown', '']: self_area_id = None + self.ha_device_id = Gb.ha_device_id_by_devicename.get(self.devicename) + self.ha_area_id = Gb.ha_area_id_by_devicename.get(self.devicename) + # if self.ha_area_id in ['unknown', '']: self_area_id = None self.device_fname = conf_device[FNAME] self.device_type = conf_device[CONF_DEVICE_TYPE] @@ -298,8 +298,8 @@ def get_device_id(self): @property def get_area_id(self): """Return the area_id of the device.""" - # if self.dr_area_id is None: - # self.dr_area_id = Gb.dr_area_id_by_devicename[self.devicename] = \ + # if self.ha_area_id is None: + # self.ha_area_id = Gb.ha_area_id_by_devicename[self.devicename] = \ # Gb.area_id_personal_device return self.area_id @@ -479,22 +479,22 @@ def _get_attribute_value(self, attribute): def update_entity_attribute(self, new_fname=None, area_id=None): """ Update entity definition attributes """ - device_id = Gb.dr_device_id_by_devicename.get(self.devicename) - if device_id is None: + ha_device_id = Gb.ha_device_id_by_devicename.get(self.devicename) + if ha_device_id is None: return if new_fname is None and area_id is None: return try: - area_id = area_id or self.dr_area_id or Gb.area_id_personal_device + area_id = area_id or self.ha_area_id or Gb.area_id_personal_device area_reg = ar.async_get(Gb.hass) area_name = area_reg.async_get_area(area_id).name except: area_id = area_name = None self.device_fname = new_fname or self.device_fname - self.dr_area_id = Gb.dr_area_id_by_devicename[self.devicename] = \ + self.ha_area_id = Gb.ha_area_id_by_devicename[self.devicename] = \ area_id log_debug_msg(f"Device Tracker entity changed: device_tracker.{self.devicename}, " @@ -533,10 +533,10 @@ def update_entity_attribute(self, new_fname=None, area_id=None): kwargs = {} kwargs['name'] = f"{self.device_fname} ({self.devicename})" kwargs['name_by_user'] = "" - kwargs['area_id'] = self.dr_area_id + kwargs['area_id'] = self.ha_area_id device_registry = dr.async_get(Gb.hass) - dr_entry = device_registry.async_update_device(self.dr_device_id, **kwargs) + dr_entry = device_registry.async_update_device(self.ha_device_id, **kwargs) #------------------------------------------------------------------------------------------- def remove_device_tracker(self): diff --git a/custom_components/icloud3/event_log_card/icloud3-event-log-card.js.gz b/custom_components/icloud3/event_log_card/icloud3-event-log-card.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..501444fa72fa4ec3a2b99c38d59f21c4d2524f10 GIT binary patch literal 22114 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%0A*VU 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 literal 0 HcmV?d00001 diff --git a/custom_components/icloud3/global_variables.py b/custom_components/icloud3/global_variables.py index 6367354..7eb0d1c 100644 --- a/custom_components/icloud3/global_variables.py +++ b/custom_components/icloud3/global_variables.py @@ -147,8 +147,8 @@ class GlobalVariables(object): Sensors_by_devicename = {} # HA sensor.[devicename]_[sensor_name]_[from_zone] objects Sensors_by_devicename_from_zone = {} # HA sensor.[devicename]_[sensor_name]_[from_zone] objects Sensor_EventLog = None # Event Log sensor object - dr_device_id_by_devicename = {} # HA device_registry device_id - dr_area_id_by_devicename = {} # HA device_registry area_id + ha_device_id_by_devicename = {} # HA device_registry device_id + ha_area_id_by_devicename = {} # HA device_registry area_id # Event Log operational fields evlog_card_directory = '' @@ -234,7 +234,7 @@ class GlobalVariables(object): device_trackers_cnt = 0 # Number of device_trackers that will be creted (__init__.py) device_trackers_created_cnt = 0 # Number of device_trackers that have been set up (incremented in device_tracker.py) area_id_personal_device = None - + # restore_state file restore_state_file_data = {} restore_state_profile = {} @@ -312,6 +312,7 @@ class GlobalVariables(object): used_data_source_FAMSHR = False used_data_source_FMF = False used_data_source_IOSAPP = False + iosapp_monitor_any_devices_false_flag = False # Primary data source being used that can be turned off if errors primary_data_source_ICLOUD = conf_data_source_ICLOUD diff --git a/custom_components/icloud3/helpers/time_util.py b/custom_components/icloud3/helpers/time_util.py index f4fcbe3..124288f 100644 --- a/custom_components/icloud3/helpers/time_util.py +++ b/custom_components/icloud3/helpers/time_util.py @@ -72,6 +72,13 @@ def secs_to_time_str(secs): return time_str +#-------------------------------------------------------------------- +def secs_to_hrs_str(secs): + if secs < 1: + return '0 hrs' + else: + return f"{secs/3600:.1f} hrs" + #-------------------------------------------------------------------- def mins_to_time_str(mins): """ Create the time string from seconds """ @@ -446,6 +453,10 @@ def format_age(secs): return f"{secs_to_time_str(secs)} ago" +#-------------------------------------------------------------------- +def format_age_hrs(secs): + return f"{secs_to_hrs_str(secs_since(secs))} ago" + #-------------------------------------------------------------------- def format_age_ts(time_secs): if time_secs < 1: diff --git a/custom_components/icloud3/icloud3_main.py b/custom_components/icloud3/icloud3_main.py index ea88c84..68c1730 100644 --- a/custom_components/icloud3/icloud3_main.py +++ b/custom_components/icloud3/icloud3_main.py @@ -35,7 +35,7 @@ from .global_variables import GlobalVariables as Gb from .const import (VERSION, - HOME, NOT_HOME, NOT_SET, HIGH_INTEGER, RARROW, RARROW2, CRLF, + HOME, NOT_HOME, NOT_SET, NOT_SET_FNAME, HIGH_INTEGER, STATIONARY, TOWARDS, AWAY_FROM, EVLOG_IC3_STAGE_HDR, ICLOUD, ICLOUD_FNAME, TRACKING_NORMAL, CMD_RESET_PYICLOUD_SESSION, NEAR_DEVICE_DISTANCE, @@ -59,7 +59,7 @@ from .support import determine_interval as det_interval from .helpers.common import (instr, is_zone, is_statzone, isnot_statzone, isnot_zone, - zone_display_as, ) + list_to_str,) from .helpers.messaging import (broadcast_info_msg, post_event, post_error_msg, post_monitor_msg, post_internal_error, open_ic3_log_file, post_alert, clear_alert, @@ -78,6 +78,7 @@ ZD_NAME = 2 ZD_RADIUS = 3 ZD_DISPLAY_AS = 4 +ZD_CNT = 5 #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> class iCloud3: @@ -275,6 +276,15 @@ def _polling_loop_5_sec_device(self, ha_timer_secs): self._display_secs_to_next_update_info_msg(Device) self._clear_loop_control_device() + # Remove all StatZones from HA flagged for removal in StatZone module + # Removing them after the devices have been updated lets HA process the + # statzone 'leave' automation trigger associated with a device before + # the zone is deleted. + if Gb.StatZones_to_delete: + for StatZone in Gb.StatZones_to_delete: + StatZone.remove_ha_zone() + Gb.StatZones_to_delete = [] + #<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>> # UPDATE MONITORED DEVICES @@ -328,14 +338,6 @@ def _polling_loop_5_sec_device(self, ha_timer_secs): Gb.dist_to_other_devices_update_sensor_list = set() - # Remove all StatZones from HA flagged for removal in StatZone module - # Removing them after all devices have been updated lets HA process the statzone 'leave' - # automation trigger associated with a device before the zone is deleted. - if Gb.StatZones_to_delete: - for StatZone in Gb.StatZones_to_delete: - StatZone.remove_ha_zone() - Gb.StatZones_to_delete = [] - Gb.trace_prefix = '' @@ -369,7 +371,7 @@ def _main_5sec_loop_update_tracked_devices_iosapp(self, Device): post_event(devicename, event_msg) return - # If the iOS App is the primary data source next_update time is reached, get the + # If the iOS App is the primary and data source next_update time is reached, get the # old location threshold. Send a location request to the iosapp device if the # data is older than the threshold, the next_update is newer than the iosapp data # and the next_update and data time is after the last request was sent. @@ -405,7 +407,9 @@ def _main_5sec_loop_update_tracked_devices_iosapp(self, Device): event_msg = f"Trigger > {Device.iosapp_data_change_reason}" post_event(devicename, event_msg) - # If entering a zone, check the passthru time, exit if it is now set + # If using the passthru zone delay: + # If entering a zone, set it if it is not set + # If exiting, reset it if Gb.is_passthru_zone_used: if instr(Device.iosapp_data_change_reason, ENTER_ZONE): if Device.set_passthru_zone_delay(IOSAPP, @@ -425,7 +429,7 @@ def _main_5sec_loop_update_tracked_devices_iosapp(self, Device): and Device.StatZone and Device.iosapp_zone_exit_dist_m < Device.StatZone.radius_m): - event_msg =(f"{EVLOG_ALERT}Trigger Changed > {xiosapp_data_change_reason}, " + event_msg =(f"{EVLOG_ALERT}Trigger Changed > {Device.iosapp_data_change_reason}, " f"Distance less than zone size " f"{Device.StatZone.display_as} {Device.iosapp_zone_exit_dist_m} < {Device.StatZone.radius_m}") post_event(devicename, event_msg) @@ -466,7 +470,7 @@ def _main_5sec_loop_update_tracked_devices_icloud(self, Device): if (Device.is_tracking_resumed or icloud_data_handler.is_icloud_update_needed_timers(Device) or icloud_data_handler.is_icloud_update_needed_general(Device)): - pass + Device.tracking_status = TRACKING_NORMAL else: return @@ -502,7 +506,6 @@ def _main_5sec_loop_update_tracked_devices_icloud(self, Device): self._post_before_update_monitor_msg(Device) self.process_updated_location_data(Device, ICLOUD_FNAME) - Device.tracking_status = TRACKING_NORMAL # Refresh the EvLog if this is an initial locate if self.initial_locate_complete_flag == False: @@ -529,15 +532,17 @@ def _main_5sec_loop_update_monitored_devices(self, Device): if Device.iosapp_monitor_flag and Gb.conf_data_source_IOSAPP: iosapp_data_handler.check_iosapp_state_trigger_change(Device) - if Device.is_next_update_time_reached is False: + if Device.is_tracking_resumed: + Device.tracking_status = TRACKING_NORMAL + elif Device.is_next_update_time_reached is False: Device.calculate_distance_moved() if Device.loc_data_dist_moved_km < .05: return - - if Device.loc_data_latitude == 0: + elif Device.loc_data_latitude == 0: return Device.update_sensors_flag = True + Device.icloud_initial_locate_done = True Device.icloud_update_reason = 'Monitored Device Update' event_msg =(f"Trigger > Moved {format_dist_km(Device.loc_data_dist_moved_km)}") #{Gb.any_device_was_updated_reason}") @@ -774,7 +779,12 @@ def _validate_new_icloud_data(self, Device): post_event(Device.devicename, offline_msg) # 'Verify Location' update reason overrides all other checks and forces an iCloud update - if Device.icloud_update_reason == 'Verify Location': + # if Device.icloud_update_reason == 'Verify Location': + # pass + + # Bypass all update needed checks and force an iCloud update + if Device.icloud_force_update_flag: + # Device.icloud_force_update_flag = False pass elif Device.icloud_devdata_useable_flag is False or Device.icloud_acct_error_flag: @@ -835,6 +845,7 @@ def _validate_new_icloud_data(self, Device): def process_updated_location_data(self, Device, update_requested_by): try: devicename = Gb.devicename = Device.devicename + # Device.tracking_status = TRACKING_NORMAL # Makw sure the Device iosapp_state is set to the statzone if the device is in a statzone # and the Device iosapp state value is not_nome. The Device state value can be out of sync @@ -845,7 +856,7 @@ def process_updated_location_data(self, Device, update_requested_by): # Location is good or just setup the StatZone. Determine next update time and update interval, # next_update_time values and sensors with the good data if Device.update_sensors_flag: - self._update_all_tracking_sensors(Device, update_requested_by) + self._get_tracking_results_and_update_sensors(Device, update_requested_by) else: # Old location, poor gps etc. Determine the next update time to request new location info @@ -855,6 +866,7 @@ def process_updated_location_data(self, Device, update_requested_by): and (Device.old_loc_poor_gps_cnt % 4) == 0): iosapp_interface.request_location(Device) + Device.icloud_force_update_flag = False Device.write_ha_sensors_state() Device.write_ha_device_from_zone_sensors_state() Device.write_ha_device_tracker_state() @@ -910,7 +922,7 @@ def _started_passthru_zone_delay(self, Device): return False #---------------------------------------------------------------------------- - def _update_all_tracking_sensors(self, Device, update_requested_by): + def _get_tracking_results_and_update_sensors(self, Device, update_requested_by): ''' All sensor update checked passed and an update is needed. Get the latest icloud data, verify it's usability, and update the location data, determine the next @@ -1022,6 +1034,32 @@ def _determine_interval_and_next_update(self, Device): return True +#------------------------------------------------------------------------------ + def _request_update_devices_no_iosapp_same_zone_on_exit(self, Device): + ''' + The Device is exiting a zone. Check all other Devices that were in the same + zone that do not have the iosapp installed and set the next update time to + 5-seconds to see if that device also exited instead of waiting for the other + devices inZone interval time to be reached. + + Check the next update time to make sure it has not already been updated when + the device without the iOS app is with several devices that left the zone. + ''' + devices_to_update = [_Device + for _Device in Gb.Devices_by_devicename_tracked.values() + if (Device is not _Device + and _Device.is_data_source_IOSAPP is False + and _Device.loc_data_zone == Device.loc_data_zone + and secs_to(_Device.FromZone_Home.next_update_secs) > 60)] + + if devices_to_update == []: + return + + for _Device in devices_to_update: + _Device.icloud_force_update_flag = True + det_interval.update_all_device_fm_zone_sensors_interval(_Device, 15) + event_msg = f"Trigger > Check Zone Exit, GeneratedBy-{Device.fname}" + post_event(_Device.devicename, event_msg) #------------------------------------------------------------------------------ # @@ -1047,8 +1085,6 @@ def _update_current_zone(self, Device, display_zone_msg=True): calling hass on all polls ''' - gps_accuracy_adj = int(Device.loc_data_gps_accuracy / 2) - # Zone selected may have been done when determing if the device just entered a zone # during the passthru check. If so, use it and then reset it if Device.selected_zone_results == []: @@ -1070,36 +1106,8 @@ def _update_current_zone(self, Device, display_zone_msg=True): # In a zone but if not in a track from zone and was in a Stationary Zone, # reset the stationary zone elif Device.is_in_statzone and isnot_statzone(zone_selected): - statzone.exit_statzone(Device) - - zones_distance_list.sort() - zones_distance_list = ', '.join([v.split('|')[1] for v in zones_distance_list]) - - if display_zone_msg: - selected_zone_msg = other_zones_msg = gps_accuracy_msg = '' - if ZoneSelected.radius_m > 0: - selected_zone_msg = f"-{format_dist_m(zone_selected_dist_m)}" - if (zone_selected == NOT_HOME - or (is_statzone(zone_selected) and isnot_statzone(Device.loc_data_zone))): - other_zones_msg = f" > {zones_distance_list}" - if zone_selected_dist_m > ZoneSelected.radius_m: - gps_accuracy_msg = f", AccuracyAdjustment-{gps_accuracy_adj}m" - - zones_msg =(f"Zone > " - f"{ZoneSelected.display_as}" - f"{selected_zone_msg}" - f"{other_zones_msg}" - f"{gps_accuracy_msg}" - f", GPS-{Device.loc_data_fgps}") - if ZoneSelected in Gb.StatZones: - zones_msg += f", DevicesInStatZone-{statzone.devices_in_statzone_count(ZoneSelected)}" - post_event(Device.devicename, zones_msg) + statzone.exit_statzone(Device) - if other_zones_msg == '': - zones_msg =(f"Zone > " - f"{ZoneSelected.display_as} > " - f"{zones_distance_list}") - post_monitor_msg(Device.devicename, zones_msg) # Get distance between zone selected and current zone to see if they overlap. # If so, keep the current zone @@ -1110,10 +1118,22 @@ def _update_current_zone(self, Device, display_zone_msg=True): # The zone changed elif Device.loc_data_zone != zone_selected: + # See if any device without the iosapp was in this zone. If so, request a + # location update since it was running on the inzone timer instead of + # exit triggers from the ios app + if (Gb.iosapp_monitor_any_devices_false_flag + and zone_selected == NOT_HOME + and Device.loc_data_zone != NOT_HOME): + self._request_update_devices_no_iosapp_same_zone_on_exit(Device) + Device.loc_data_zone = zone_selected Device.zone_change_datetime = datetime_now() Device.zone_change_secs = time_now_secs() + if display_zone_msg: + self._post_zone_selected_msg(Device, ZoneSelected, zone_selected, + zone_selected_dist_m, zones_distance_list) + return ZoneSelected, zone_selected #-------------------------------------------------------------------- @@ -1137,7 +1157,7 @@ def _select_zone(self, Device, latitude=None, longitude=None): gps_accuracy_adj = int(Device.loc_data_gps_accuracy / 2) # [distance from zone, Zone, zone_name, redius, display_as] - zone_data_selected = [HIGH_INTEGER, None, '', HIGH_INTEGER, ''] + zone_data_selected = [HIGH_INTEGER, None, '', HIGH_INTEGER, '', 1] # Exit if no location data is available if Device.no_location_data: @@ -1153,7 +1173,6 @@ def _select_zone(self, Device, latitude=None, longitude=None): and Device.StatZone.distance_m(latitude, longitude) > Device.StatZone.radius_m): statzone.exit_statzone(Device) - # Get a list of all the zones, their distance, size and display_as zones_data = [[Zone.distance_m(latitude, longitude), Zone, Zone.zone, Zone.radius_m, Zone.display_as] for Zone in Gb.Zones @@ -1165,6 +1184,9 @@ def _select_zone(self, Device, latitude=None, longitude=None): inzone_zones = [zone_data for zone_data in zones_data if zone_data[ZD_DIST_M] <= zone_data[ZD_RADIUS] + gps_accuracy_adj] + # if Device.devicename == 'gary_iphone': + # inzone_zones = [] + for zone_data in inzone_zones: if zone_data[ZD_RADIUS] <= zone_data_selected[ZD_RADIUS]: zone_data_selected = zone_data @@ -1184,21 +1206,85 @@ def _select_zone(self, Device, latitude=None, longitude=None): Device.iosapp_zone_enter_time = Gb.this_update_time Device.iosapp_zone_enter_zone = zone_selected - # [f"{int(zone_data[ZD_DIST_M]):08}| {zone_data[ZD_DISPLAY_AS]}-{format_dist_m(zone_data[ZD_DIST_M])}" + # Build an item for each zone (dist-from-zone|zone_name|display_name-##km) zones_distance_list = \ - [f"{int(zone_data[ZD_DIST_M]):08}| {self._format_zone_info(zone_data)}" + [(f"{int(zone_data[ZD_DIST_M]):08}|{zone_data[ZD_NAME]}|{zone_data[ZD_DIST_M]}") for zone_data in zones_data if zone_data[ZD_NAME] != zone_selected] return ZoneSelected, zone_selected, zone_selected_dist_m, zones_distance_list #-------------------------------------------------------------------- @staticmethod - def _format_zone_info(zone_data): - statzone_msg = '' - if zone_data[ZD_ZONE] in Gb.StatZones: - statzone_msg = f" ({statzone.devices_in_statzone_count(zone_data[ZD_ZONE])})" - return (f"{zone_data[ZD_DISPLAY_AS]}-{format_dist_m(zone_data[ZD_DIST_M])}" - f"{statzone_msg}") + def _post_zone_selected_msg(Device, ZoneSelected, zone_selected, + zone_selected_dist_m, zones_distance_list): + + device_zones = [_Device.loc_data_zone for _Device in Gb.Devices] + zones_cnt_by_zone = {zone:device_zones.count(zone) for zone in set(device_zones)} + + zones_cnt_summary = [f"{Gb.zone_display_as[_zone]} ({cnt}), " + for _zone, cnt in zones_cnt_by_zone.items()] + zones_cnt_summary_msg = list_to_str(zones_cnt_summary).replace('──', 'NotSet') + + zones_distance_msg = '' + zones_displayed = [zone_selected] + if (zone_selected == NOT_HOME + or (is_statzone(zone_selected) + and isnot_statzone(Device.loc_data_zone))): + zones_distance_list.sort() + for zone_distance_list in zones_distance_list: + zdl_items = zone_distance_list.split('|') + _zone = zdl_items[1] + _zone_dist = float(zdl_items[2]) + + zones_displayed.append(_zone) + zones_distance_msg += f"{Gb.zone_display_as[_zone]}-{format_dist_m(_zone_dist)} " + if zones_cnt_by_zone.get(_zone, 0) > 0: + zones_distance_msg += f" ({zones_cnt_by_zone[_zone]}), " + else: + zones_distance_msg += ", " + + zones_cnt_list = [f"{Gb.zone_display_as[_zone]} ({zones_cnt_by_zone[_zone]}), " + for _zone, cnt in zones_cnt_by_zone.items() + if _zone not in zones_displayed] + zones_cnt_msg = list_to_str(zones_cnt_list) + if zones_cnt_msg: zones_cnt_msg += ', ' + + # if display_zone_msg: + # Format the Zone Selected Msg (ZoneName (#)) + zone_selected_msg = Gb.zone_display_as[zone_selected] + + if ZoneSelected.radius_m > 0: + zone_selected_msg += f"-{format_dist_m(zone_selected_dist_m)}" + if zone_selected in zones_cnt_by_zone: + zone_selected_msg += f" ({zones_cnt_by_zone[zone_selected]})" + + # Format the Zones with devices when in a zone (ZoneName (#)) + zones_cnt_summary = [f"{Gb.zone_display_as[_zone]} ({cnt}), " + for _zone, cnt in zones_cnt_by_zone.items()] + + # if zones_distance_msg: zones_distance_msg = f" > {zones_distance_msg}" + if zones_cnt_msg: zones_cnt_msg = f"{zones_cnt_msg.replace('──', 'NotSet')}" + + gps_accuracy_msg = '' + if zone_selected_dist_m > ZoneSelected.radius_m: + gps_accuracy_msg = (f"AccuracyAdjustment-" + f"{int(Device.loc_data_gps_accuracy / 2)}m, ") + + zones_msg =(f"Zone > " + f"{zone_selected_msg} > " + f"{zones_distance_msg}" + f"{zones_cnt_msg}" + f"{gps_accuracy_msg}" + f"GPS-{Device.loc_data_fgps}") + post_event(Device.devicename, zones_msg) + + if Device.loc_data_zone != Device.sensors[ZONE]: + if NOT_SET not in zones_cnt_by_zone: + # if 'xxx' not in zones_cnt_by_zone: + for _Device in Gb.Devices: + if Device is not _Device: + event_msg = f"Zone-Device Counts > {zones_cnt_summary_msg}" + post_event(_Device.devicename, event_msg) #-------------------------------------------------------------------- def _move_into_statzone_if_timer_reached(self, Device): @@ -1482,8 +1568,6 @@ def _check_old_loc_poor_gps(self, Device): Device.old_loc_poor_gps_msg = f"Poor GPS > {cnt_msg}, Accuracy-±{Device.loc_data_gps_accuracy:.0f}m" else: Device.old_loc_poor_gps_msg = f"Locaton > Unknown {cnt_msg}, {secs_to_age_str(Device.loc_data_secs)}" - # if Device.old_loc_poor_gps_cnt > 2: - # Device.old_loc_poor_gps_msg += f", Threshold-{secs_to_time_str(Device.old_loc_threshold_secs)}" except Exception as err: log_exception(err) diff --git a/custom_components/icloud3/sensor.py b/custom_components/icloud3/sensor.py index 0c57a85..920b6dd 100644 --- a/custom_components/icloud3/sensor.py +++ b/custom_components/icloud3/sensor.py @@ -410,7 +410,7 @@ def __init__(self, devicename, sensor_base, conf_device, from_zone=None): self.entity_name = f"{devicename}_{self.sensor}" self.entity_id = f"sensor.{self.entity_name}" - self.device_id = Gb.dr_device_id_by_devicename.get(ICLOUD3) + self.device_id = Gb.ha_device_id_by_devicename.get(ICLOUD3) self.Device = Gb.Devices_by_devicename.get(devicename) if self.Device and from_zone: @@ -1158,7 +1158,7 @@ def __init__(self, fname, entity_name): self.entity_id = f"sensor.{self.entity_name}" self._unsub_dispatcher = None self._device = DOMAIN - # self.ic3_device_id = Gb.ic3_device_id = Gb.dr_device_id_by_devicename.get(DOMAIN) + # self.ic3_device_id = Gb.ic3_device_id = Gb.ha_device_id_by_devicename.get(DOMAIN) self.current_state_value = '' self.history_exclude_flag = True diff --git a/custom_components/icloud3/strings.json b/custom_components/icloud3/strings.json index d51ffdc..ecaec50 100644 --- a/custom_components/icloud3/strings.json +++ b/custom_components/icloud3/strings.json @@ -34,7 +34,7 @@ "description": "Enter the 6-digit verification code you just received from Apple", "data": { "verification_code": "ICLOUD ACCOUNT VERIFICATION CODE", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════" } }, "restart_ha": { @@ -62,10 +62,12 @@ "icloud_acct_logging_into": "Logging into iCloud Account", "icloud_acct_logged_into": "Successfully Logged into the iCloud Account", "icloud_acct_already_logged_into": "Already Logged into the iCloud Account", - "icloud_acct_login_error": "iCloud Account Login Failed - Review Username/Password (or China '.cn' suffix)", - "icloud_acct_not_available": "iCloud Account is not Available, Login Unsuccessful", + "icloud_acct_login_error_user_pw": "Login Failed, Invalid Username or Password (err-400)", + "icloud_acct_login_error_other": "Login Failed, Other Error or iCloud is not Available", + "icloud_acct_login_error_connection": "Login Error, Failed to Connect to iCloud Server (err-302)", + "icloud_acct_not_available": "Login Error, iCloud Account is not Available", "icloud_acct_not_logged_into": "iCloud Account is not Logged Into", - "icloud_acct_not_set_up": "The iCloud Account Username or Password needs to be entered", + "icloud_acct_not_set_up": "iCloud Account Username or Password needs to be entered", "icloud_acct_no_data_source": "No Data Source has been selected", "verification_code_requested": "The Apple ID Verification Code was requested, BROWSER REFRESH MAY BE NEEDED", @@ -131,7 +133,7 @@ "title": "iCloud3 Configure Settings", "data": { "menu_items": "", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════" } }, "restart_icloud3": { @@ -155,16 +157,22 @@ "action_items": "" } }, + "confirm_action": { + "title": "Confirm Selected Action", + "data": { + "action_items": "" + } + }, "icloud_account": { "title": "iCloud Account & iOS App Data Sources", "description": "The data sources (iCloud Account Web Services and the iOS App) are selected on this screen. The Apple iCloud Account Username/Password must be specified here to access the iCloud Account. The HA Companion App (iOS App) must be installed on the iDevice to use is as a data source for that device. Each iDevice you are tracking is associated with the iCloud3 device on the Update Devices screen.", "data": { - "data_source_icloud": "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ICLOUD ACCOUNT - Location data is provided by the Apple iCloud account", + "data_source_icloud": "═════════════════════════════════════════════════════ ICLOUD ACCOUNT - Location data is provided by the Apple iCloud account", "username": "APPLE ID (USERNAME) - The email address used to sign in to the iCloud Account", "password": "PASSWORD - The iCloud Account Password", "url_suffix_china": "CHINA USERS - Add '.cn' to use the Apple iCloud Web Servers located in China", - "data_source_iosapp": "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ iOS APP - Location data is provided by the HA iOS App", - "action_items": "═══════════════════════════════════════════════════" + "data_source_iosapp": "═════════════════════════════════════════════════════ iOS APP - Location data is provided by the HA iOS App", + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" }, "data_description": { } @@ -174,15 +182,15 @@ "description": "Enter the 6-digit verification code you just received from Apple", "data": { "verification_code": "ICLOUD ACCOUNT VERIFICATION CODE", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" } }, "device_list": { "title": "iCloud3 Device Tracker Entities", "description": "All of the devices that are tracked or monitored by iCloud3 are listed on this screen. You can add new devices and update or delete existing devices.", "data": { - "devices": "", - "action_items": "═══════════════════════════════════════════════════" + "devices": "═════════════════════════════════════════════════════ ICLOUD3 DEVICES", + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" } }, "add_device": { @@ -193,7 +201,7 @@ "device_type": "DEVICE TYPE - iPhone, iPad, Watch, etc.", "tracking_mode": "TRACKING MODE - How location requests should be done (Full tracking, Monitor, Inactive)", "iosapp": "iOS APP INSTALLED - The HA iOS App is installed on this device ", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" }, "data_description": { } @@ -213,7 +221,7 @@ "track_from_base_zone": "PRIMARY HOME ZONE - Use another zone if you are away from Home for an extended period", "picture": "PICTURE - Photo image of the person normally using this device (40x40 pixels is best size)", "inzone_interval": "INZONE INTERVAL", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" }, "data_description": { "inzone_interval": "Time between location requests when in a zone", @@ -223,22 +231,23 @@ "delete_device": { "title": "Delete Device(s), Other Device Maintenance", "data": { - "action_items": "" + "action_items": "═════════════════════════════════════════════════════ DELETE OPTIONS" } }, "review_inactive_devices": { "title": "Review Untracked (Inactive) Devices", "description": "The 'Tracking Mode' of devices are set to 'Inactive' and will not be located or tracked.", "data": { - "inactive_devices": "", - "action_items": "═══════════════════════════════════════════════════" + "inactive_devices": "═════════════════════════════════════════════════════ INACTIVE ICLOUD3 DEVICES", + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" } }, "change_device_order": { "title": "Event Log Device Display Sequence", + "description": "The devices are displayed in the Event Log heading area and in various Event Log messages in the sequence below. This screen lets you change the order of the devices.", "data": { - "device_desc": "DEVICES - The devices are displayed in the Event Log heading area and in various Event Log messages in this sequence", - "action_items": "═══════════════════════════════════════════════════" + "device_desc": "═════════════════════════════════════════════════════ ICLOUD3 DEVICES", + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" } }, "format_settings": { @@ -252,7 +261,7 @@ "unit_of_measurement": "UNIT OF MEASUREMENT - How distance fieldsare displayed in sensors and in the Event Log", "display_gps_lat_long": "DISPLAY GPS COORDINATES - Display the GPS (Latitude, Longitude/±Accuracy) or only the GPS (/±Accuracy) in the Event Log", "display_gps_lat_long2": "DISPLAY GPS COORDINATES - Display the GPS Latitude/Longitude/Accuracy (22.32771, -76.33073/±35m) or only the GPS accuracy (/±35m) in the Event Log", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" }, "data_description": { "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." @@ -262,8 +271,8 @@ "title": "Event Log 'Display Text As'", "description": "There may be some text fields that are displayed on the Event Log screen that may be sensitive in nature. Some examples include email addresses or phone numbers. With this screen, you can select the Original Text and what should be displayed instead. For example, you can replace 'geekstergary@apple.com' with 'gary@email.com'", "data": { - "display_text_as": "Text Replacement Fields", - "action_items": "═══════════════════════════════════════════════════" + "display_text_as": "═════════════════════════════════════════════════════ TEXT REPLACEMENT FIELDS - [Actual text > Displayed text]", + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" } }, "display_text_as_update": { @@ -271,7 +280,7 @@ "data": { "text_from": "ORIGINAL TEXT - Text to be replaced (example: gary_real_email@gmail.com)", "text_to": "DISPLAYED TEXT- Text to be displayed (display: gary@email.com)", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" }, "data_description": { } @@ -279,7 +288,10 @@ "actions": { "title": "iCloud3 Action Commands", "data": { - "action_items": "" + "ic3_actions": "═════════════════════════════════════════════════════ ICLOUD3 GENERAL CONTROL ACTIONS", + "debug_actions": "═════════════════════════════════════════════════════ DEBUG LOG ACTIONS", + "other_actions": "═════════════════════════════════════════════════════ OTHER ACTIONS", + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" } }, "tracking_parameters": { @@ -299,12 +311,12 @@ "travel_time_factor": "TRAVEL TIME INTERVAL MULTIPLIER", "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_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": "See if there are any other devices that are near the device being updated. If there is, use that device's location results instead of going through the calculation process. This can speed up the update since Waze travel time and distance information does not have to be requested", + "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", @@ -328,7 +340,7 @@ "discard_poor_gps_inzone": "Discard Location Updates with Poor GPS Accuracy when in a Zone", "distance_between_devices": "Determine the distance between devices. Use a near by device's tracking results", "center_in_zone": "Change Device's Location to the Zone's Center when in a Zone", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" }, "data_description": { "no_iosapp": "Default interval if the iOS App is not used for location monitoring and zone enter/exit triggers", @@ -338,15 +350,15 @@ "waze_main": { "title": "Waze - Route Service Travel Time/Distance", "data": { - "waze_used": "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ WAZE ROUTE SERVICE", + "waze_used": "═════════════════════════════════════════════════════ WAZE ROUTE SERVICE", "waze_region": "ROUTE SERVER LOCATION - Location of the Waze Route Server for your area", "waze_realtime": "USE REAL TIME DATA - Waze should consider traffic delays when determining travel time", "waze_min_distance": "WAZE MINIMUM DISTANCE", "waze_max_distance": "WAZE MAXIMUM DISTANCE", - "waze_history_database_used": "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ WAZE HISTORY DATABASE", + "waze_history_database_used": "═════════════════════════════════════════════════════ WAZE HISTORY DATABASE", "waze_history_track_direction": "GENERAL TRAVEL DIRECTION - Used to display 'Map Trace Lines' between saved locations", "waze_history_max_distance": "HISTORY MAX DISTANCE", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" }, "data_description": { "waze_min_distance": "Use the Waze Route Service when the zone distance is greater than this value", @@ -356,17 +368,18 @@ }, "special_zones": { "title": "Special Zones - Enter Zone Delay, Stationary Zone, Primay Home Base Zone", + "description": "This screen lets you configure an enter zone delay time, stationary zones and set up a temporary 'home' zone at another location", "data": { - "passthru_zone_header": "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ENTER ZONE DELAY", + "passthru_zone_header": "═════════════════════════════════════════════════════ ENTER ZONE DELAY", "passthru_zone_time": "ENTER ZONE DELAY TIME ", - "stat_zone_header": "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ STATIONARY ZONE", + "stat_zone_header": "═════════════════════════════════════════════════════ STATIONARY ZONE", "stat_zone_still_time": "NO MOVEMENT TIME ", "stat_zone_inzone_interval": "INZONE INTERVAL ", "stat_zone_fname": "FRIENDLY NAME BASE - Name to display when in a Stationary Zone (StatZone)", - "track_from_zone_header": "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ PRIMARY BASE ZONE = HOME", + "track_from_zone_header": "═════════════════════════════════════════════════════ PRIMARY BASE ZONE = HOME", "track_from_base_zone": "PRIMARY 'HOME' ZONE - Use another zone if you are away from Home for an extended period (Global Override)", "track_from_home_zone": "ALSO TRACK FROM HOME ZONE - Continue to track from the Home zone when the Primary Home Zone is not Home", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" }, "data_description": { "passthru_zone_time": "Delay processing an Enter Zone Trigger that you may be driving through and not actually entering", @@ -379,17 +392,17 @@ "title": "Device and Tracking Sensors created by iCloud3", "description": "Many sensors are used to display tracking results and other information for a device. This screen is used to select the sensors that should be created.", "data": { - "monitored_devices": "MONITORED DEVICE SENSORS - Select the type of sensors to create for a Monitored Device", - "device": "DEVICE SENSORS - Device status and information", - "tracking_update": "LOCATION UPDATE SENSORS - Device location update times", - "tracking_time": "TIME SENSORS - Device tracking timers", - "tracking_distance": "DISTANCE SENSORS - Device tracking distances", - "track_from_zones": "TRACK FROM MULTIPLE ZONE SENSORS - Used when tracking from more than one zone (not needed for tracking only from the Home zone)", - "tracking_other": "OTHER TRACKING SENSORS - Not normally used but available", - "zone": "ZONE SENSORS - Device zone status and information", - "other": "OTHER SENSORS - Sensors not in the above areas", - "excluded_sensors": "EXCLUDED SENSORS - Sensors that will not be created when iCloud3 starts", - "action_items": "═══════════════════════════════════════════════════" + "monitored_devices": "═════════════════════════════════════════════════════ MONITORED DEVICE SENSORS - Select the type of sensors to create for a Monitored Device", + "device": "═════════════════════════════════════════════════════ DEVICE SENSORS - Device status and information", + "tracking_update": "═════════════════════════════════════════════════════ LOCATION UPDATE SENSORS - Device location update times", + "tracking_time": "═════════════════════════════════════════════════════ TIME SENSORS - Device tracking timers", + "tracking_distance": "═════════════════════════════════════════════════════ DISTANCE SENSORS - Device tracking distances", + "track_from_zones": "═════════════════════════════════════════════════════ TRACK FROM MULTIPLE ZONE SENSORS - Used when tracking from more than one zone (not needed for tracking only from the Home zone)", + "tracking_other": "═════════════════════════════════════════════════════ OTHER TRACKING SENSORS - Not normally used but available", + "zone": "═════════════════════════════════════════════════════ ZONE SENSORS - Device zone status and information", + "other": "═════════════════════════════════════════════════════ OTHER SENSORS - Sensors not in the above areas", + "excluded_sensors": "═════════════════════════════════════════════════════ EXCLUDED SENSORS - Sensors that will not be created when iCloud3 starts", + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" } }, "exclude_sensors": { @@ -399,7 +412,7 @@ "excluded_sensors": "EXCLUDED SENSORS - Sensors that will not be created when iCloud3 starts", "filter": "FILTER DISPLAYED SENSORS - Select the Sensors that should be displayed", "filtered_sensors": "ICLOUD3 SENSORS - A list of Sensors that are created when iCloud3 starts", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" } } } diff --git a/custom_components/icloud3/support/config_file.py b/custom_components/icloud3/support/config_file.py index 6b5eca7..e81470f 100644 --- a/custom_components/icloud3/support/config_file.py +++ b/custom_components/icloud3/support/config_file.py @@ -408,12 +408,15 @@ def write_storage_icloud3_configuration_file(filename_suffix=''): Gb.conf_file_data['profile'] = Gb.conf_profile Gb.conf_file_data['data'] = Gb.conf_data - decoded_password = Gb.conf_tracking[CONF_PASSWORD] + # The Gb.conf_tracking[CONF_PASSWORD] field contains the real password + # while iCloud3 is running. This makes it easier logging into PyiCloud + # and in config_flow. Save it, then put the encoded password in the file + # update the file and then restore the real password Gb.conf_tracking[CONF_PASSWORD] = encode_password(Gb.conf_tracking[CONF_PASSWORD]) json.dump(Gb.conf_file_data, f, indent=4, ensure_ascii=False) - Gb.conf_tracking[CONF_PASSWORD] = decoded_password + Gb.conf_tracking[CONF_PASSWORD] = decode_password(Gb.conf_tracking[CONF_PASSWORD]) close_reopen_ic3_log_file() @@ -440,7 +443,7 @@ def encode_password(password): except Exception as err: log_exception(err) - password = password.replace('««', '').replace('»»', '') + password = password.replace('«', '').replace('»', '') return password def base64_encode(string): @@ -450,9 +453,16 @@ def base64_encode(string): # encoded = base64.urlsafe_b64encode(string) # return encoded.rstrip("=") - string_bytes = string.encode('ascii') - base64_bytes = base64.b64encode(string_bytes) - return base64_bytes.decode('ascii') + try: + string_bytes = string.encode('ascii') + base64_bytes = base64.b64encode(string_bytes) + return base64_bytes.decode('ascii') + + except Exception as err: + log_exception(err) + password = password.replace('«', '').replace('»', '') + return password + #-------------------------------------------------------------------- def decode_password(password): @@ -463,13 +473,24 @@ def decode_password(password): Decoded password ''' try: + # If the password in the configuration file is not encoded (no '««' or '»»') + # and it should be encoded, save the configuration file which will encode it + if (Gb.encode_password_flag + and password != '' + and (password.startswith('««') is False + or password.endswith('»»') is False)): + password = password.replace('«', '').replace('»', '') + Gb.conf_tracking[CONF_PASSWORD] = password + write_storage_icloud3_configuration_file() + + # Decode password if it is encoded and has the '««password»»' format if (password.startswith('««') or password.endswith('»»')): - password = password.replace('««', '').replace('»»', '') + password = password.replace('«', '').replace('»', '') return base64_decode(password) except Exception as err: log_exception(err) - password = password.replace('««', '').replace('»»', '') + password = password.replace('«', '').replace('»', '') return password diff --git a/custom_components/icloud3/support/determine_interval.py b/custom_components/icloud3/support/determine_interval.py index 3dfb13e..c4d575c 100644 --- a/custom_components/icloud3/support/determine_interval.py +++ b/custom_components/icloud3/support/determine_interval.py @@ -654,15 +654,15 @@ def determine_interval_after_error(Device, counter=OLD_LOC_POOR_GPS_CNT): interval_secs, error_cnt, max_error_cnt = get_error_retry_interval(Device, counter) # Pause tracking when the max count is exceeded, send paused msg when the cnt is reached - if error_cnt >= max_error_cnt: + if error_cnt >= max_error_cnt and Device.is_tracking_paused is False: Device.pause_tracking() if error_cnt == max_error_cnt: message = { "title": "iCloud3 Tracking Exception", - "message": (f"Old Location or Poor GPS Accuracy Error " - f"Count exceeded (#{error_cnt}). Event Log > Actions > " - f"Resume to restart tracking."), + "message": (f"Old Location Count exceeded (#{error_cnt}). " + f"iCloud Web Services is Paused. " + f"Event Log > Actions > Resume to restart tracking."), "data": {"subtitle": "Tracking has been Paused"}} iosapp_interface.send_message_to_device(Device, message) diff --git a/custom_components/icloud3/support/icloud_data_handler.py b/custom_components/icloud3/support/icloud_data_handler.py index 92b2b82..3ebd561 100644 --- a/custom_components/icloud3/support/icloud_data_handler.py +++ b/custom_components/icloud3/support/icloud_data_handler.py @@ -78,7 +78,8 @@ def is_icloud_update_needed_timers(Device): #---------------------------------------------------------------------------- def is_icloud_update_needed_general(Device): - if Gb.force_icloud_update_flag: + if Gb.icloud_force_update_flag: + Device.icloud_force_update_flag = True Device.icloud_update_reason = 'Immediate Update Requested' elif Device.is_tracking_resumed: @@ -86,6 +87,7 @@ def is_icloud_update_needed_general(Device): elif Device.outside_no_exit_trigger_flag: Device.outside_no_exit_trigger_flag = False + Device.icloud_force_update_flag = True Device.icloud_update_reason = "Verify Location" elif (Device.loc_data_secs < Device.last_update_loc_secs @@ -127,7 +129,7 @@ def request_icloud_data_update(Device): devicename = Device.devicename try: - if Device.icloud_update_reason: + if Device.icloud_update_reason or Device.icloud_force_update_flag: Device.display_info_msg("Requesting iCloud Location Update") Device.icloud_devdata_useable_flag = update_PyiCloud_RawData_data(Device) @@ -425,8 +427,8 @@ def is_PyiCloud_RawData_data_useable(Device, results_msg_flag=True): fmf_secs, fmf_gps_accuracy, \ fmf_time = _get_devdata_useable_status(Device, FMF) - if Gb.force_icloud_update_flag: - Gb.force_icloud_update_flag = False + if Gb.icloud_force_update_flag or Device.icloud_force_update_flag: + Gb.icloud_force_update_flag = False is_useable_flag = False useable_msg = 'Update Required' return False @@ -495,9 +497,10 @@ def _get_devdata_useable_status(Device, data_source): if device_id is None or RawData is None: return False, False, False, 0, 0, '' + # v3.0.rc7.1 Added icloud_force_update_flag check loc_secs = RawData.location_secs loc_age_secs = secs_since(loc_secs) - loc_time_ok = (loc_age_secs <= Device.old_loc_threshold_secs) + loc_time_ok = ((loc_age_secs <= Device.old_loc_threshold_secs) and Device.icloud_force_update_flag is False) # If loc time is under threshold, check to see if the loc time is older than the interval # The interval may be < 15 secs if just trying to force a quick update with the current data. If so, do not check it diff --git a/custom_components/icloud3/support/iosapp_data_handler.py b/custom_components/icloud3/support/iosapp_data_handler.py index eeef20c..b6c97c9 100644 --- a/custom_components/icloud3/support/iosapp_data_handler.py +++ b/custom_components/icloud3/support/iosapp_data_handler.py @@ -15,7 +15,7 @@ from ..helpers.messaging import (post_event, post_monitor_msg, log_debug_msg, log_exception, log_error_msg, log_rawdata, _trace, _traceha, ) -from ..helpers.time_util import (secs_to_time, secs_since, format_time_age, format_age, ) +from ..helpers.time_util import (secs_to_time, secs_since, format_time_age, format_age, format_age_hrs, ) from ..helpers.dist_util import (format_dist_km, format_dist_m, ) from ..helpers import entity_io from ..support import iosapp_interface @@ -63,12 +63,19 @@ def check_iosapp_state_trigger_change(Device): iosapp_data_trigger_time = device_trkr_attrs[f"trigger_{TIMESTAMP_TIME}"] = secs_to_time(iosapp_data_trigger_secs) # Get the latest of the state time or trigger time for the new data + # if iosapp_data_state_secs > iosapp_data_trigger_secs: + # iosapp_data_secs = device_trkr_attrs[TIMESTAMP_SECS] = iosapp_data_trigger_secs + # iosapp_data_time = device_trkr_attrs[TIMESTAMP_TIME] = iosapp_data_trigger_time + # else: + # iosapp_data_secs = device_trkr_attrs[TIMESTAMP_SECS] = iosapp_data_state_secs + # iosapp_data_time = device_trkr_attrs[TIMESTAMP_TIME] = iosapp_data_state_time + if iosapp_data_state_secs > iosapp_data_trigger_secs: - iosapp_data_secs = device_trkr_attrs[TIMESTAMP_SECS] = iosapp_data_trigger_secs - iosapp_data_time = device_trkr_attrs[TIMESTAMP_TIME] = iosapp_data_trigger_time - else: iosapp_data_secs = device_trkr_attrs[TIMESTAMP_SECS] = iosapp_data_state_secs iosapp_data_time = device_trkr_attrs[TIMESTAMP_TIME] = iosapp_data_state_time + else: + iosapp_data_secs = device_trkr_attrs[TIMESTAMP_SECS] = iosapp_data_trigger_secs + iosapp_data_time = device_trkr_attrs[TIMESTAMP_TIME] = iosapp_data_trigger_time if Gb.log_rawdata_flag: change_msg = '' @@ -248,6 +255,11 @@ def check_iosapp_state_trigger_change(Device): else: Device.iosapp_data_reject_reason = "Failed Update Tests" + # If data time is very old, change it to it's age + if secs_since(Device.iosapp_data_secs) >= 10800: + Device.iosapp_data_time = device_trkr_attrs[TIMESTAMP_TIME] = \ + format_age_hrs(Device.iosapp_data_secs) + # Display iOSApp Monitor info message if the state or trigger changed if (Gb.this_update_time.endswith('00:00') or iosapp_msg != Device.last_iosapp_msg): diff --git a/custom_components/icloud3/support/pyicloud_ic3.py b/custom_components/icloud3/support/pyicloud_ic3.py index 7ee07c9..0745c2a 100644 --- a/custom_components/icloud3/support/pyicloud_ic3.py +++ b/custom_components/icloud3/support/pyicloud_ic3.py @@ -26,7 +26,7 @@ FMF, FAMSHR, FMF_FNAME, FAMSHR_FNAME, NAME, ID, APPLE_SPECIAL_ICLOUD_SERVER_COUNTRY_CODE, ICLOUD_HORIZONTAL_ACCURACY, - LOCATION, TIMESTAMP, LOCATION_TIME, DATA_SOURCE, + LOCATION, TIMESTAMP, LOCATION_TIME, DATA_SOURCE, ICLOUD_BATTERY_LEVEL, ICLOUD_BATTERY_STATUS, BATTERY_STATUS_CODES, ICLOUD_DEVICE_STATUS, CONF_PASSWORD, CONF_MODEL_DISPLAY_NAME, CONF_RAW_MODEL, @@ -580,13 +580,13 @@ def authenticate(self, refresh_session=False, service=None): self.authenticate_method += ", Password" else: login_successful = False - msg = "Login Error (Invalid username/password)/593" + msg = f"Login Error (Invalid username/password)/593, err={self.response_code}" raise PyiCloudFailedLoginException(msg) except PyiCloudAPIResponseException as error: login_successful = False - msg = "Login Error (Invalid username/password)/599" + msg = f"Login Error (Invalid username/password)/599, err={self.response_code}" raise PyiCloudFailedLoginException(msg) if self._authenticate_with_token(): @@ -595,7 +595,10 @@ def authenticate(self, refresh_session=False, service=None): if login_successful == False: self.authenticate_method += ", ERROR-Invalid username/password" - msg = "Login Error (Invalid username/password)/609" + if self.response_code == 302: + msg = f"Login Error, iCloud Server Connection Error/606, err={self.response_code}" + else: + msg = f"Login Error, Invalid username/password)/609, err={self.response_code}" raise PyiCloudFailedLoginException(msg) self.requires_2fa = self.requires_2fa or self._check_2fa_needed @@ -996,6 +999,10 @@ def trusted_devices(self): def new_log_in_needed(self, username): return username != self.apple_id + @property + def response_code(self): + return self.Session.response_status_code + #---------------------------------------------------------------------------- def send_verification_code(self, device): '''Requests that a verification code is sent to the given device.''' @@ -1035,7 +1042,6 @@ def validate_2fa_code(self, code): log_exception(err) return False - # _trace(f"{len(req)=} {req=}") try: data = req.json() except ValueError: diff --git a/custom_components/icloud3/support/pyicloud_ic3_interface.py b/custom_components/icloud3/support/pyicloud_ic3_interface.py index 6b1287b..41d1df1 100644 --- a/custom_components/icloud3/support/pyicloud_ic3_interface.py +++ b/custom_components/icloud3/support/pyicloud_ic3_interface.py @@ -225,7 +225,11 @@ def display_authentication_msg(PyiCloud): if instr(authentication_method, 'Password') is False: event_msg += f" ({format_age(last_authenticated_age)})" - post_monitor_msg(event_msg) + + if instr(authentication_method, 'Password'): + post_event(event_msg) + else: + post_monitor_msg(event_msg) #-------------------------------------------------------------------- def is_authentication_2fa_code_needed(PyiCloud, initial_setup=False): @@ -395,7 +399,7 @@ def delete_pyicloud_cookies_session_files(cookie_filename=None): post_monitor_msg(delete_msg) #-------------------------------------------------------------------- -def create_PyiCloudService_secondary(username, password, +def create_PyiCloudService_secondary(username, password, endpoint_suffix, called_from, verify_password, request_verification_code=False): ''' diff --git a/custom_components/icloud3/support/service_handler.py b/custom_components/icloud3/support/service_handler.py index cc907c3..aa0fbfc 100644 --- a/custom_components/icloud3/support/service_handler.py +++ b/custom_components/icloud3/support/service_handler.py @@ -501,8 +501,8 @@ def _handle_action_device_locate(Device, action_option): if Device.old_loc_poor_gps_cnt > 3: post_event(Device.devicename, "Location request canceled. Old Location Retry is " "handling Location Updates") - ost_event(Device.devicename, "iCloud Location Tracking is not available") - Gb.force_icloud_update_flag = False + post_event(Device.devicename, "iCloud Location Tracking is not available") + Gb.icloud_force_update_flag = False return try: @@ -512,7 +512,8 @@ def _handle_action_device_locate(Device, action_option): except: interval_secs = 5 - Gb.force_icloud_update_flag = True + Gb.icloud_force_update_flag = True + Device.icloud_force_update_flag = True det_interval.update_all_device_fm_zone_sensors_interval(Device, interval_secs) Device.icloud_update_reason = f"Location Requested@{time_now()}" post_event(Device.devicename, f"Location will be updated at {Device.sensors[NEXT_UPDATE_TIME]}") diff --git a/custom_components/icloud3/support/start_ic3.py b/custom_components/icloud3/support/start_ic3.py index 9124b92..cd2a0ed 100644 --- a/custom_components/icloud3/support/start_ic3.py +++ b/custom_components/icloud3/support/start_ic3.py @@ -243,6 +243,7 @@ def initialize_global_variables(): Gb.used_data_source_FMF = False Gb.used_data_source_FAMSHR = False Gb.used_data_source_IOSAPP = False + Gb.any_data_source_IOSAPP_none = False initialize_on_initial_load() @@ -403,7 +404,7 @@ def initialize_icloud_data_source(): Gb.primary_data_source_ICLOUD = Gb.conf_data_source_ICLOUD Gb.primary_data_source = ICLOUD if Gb.primary_data_source_ICLOUD else IOSAPP Gb.devices = Gb.conf_devices - Gb.force_icloud_update_flag = False + Gb.icloud_force_update_flag = False Gb.stage_4_no_devices_found_cnt = 0 @@ -420,19 +421,6 @@ def icloud_server_endpoint_suffix(endpoint_suffix): return '' - # endpoint_msg = '' - # if endpoint_suffix.startswith('-'): - # endpoint_msg = f"Overridden, Not Used ({endpoint_suffix}) " - # endpoint_suffix = '' - # elif endpoint_suffix != '': - # endpoint_suffix = endpoint_msg = f".{endpoint_suffix}" - # if endpoint_msg != '': - # post_event(f"iCloud Web Server Country Suffix > {endpoint_msg}") - -#------------------------------------------------------------------------------ -# def initialize_PyiCloud(): -# Gb.PyiCloud = None - #------------------------------------------------------------------------------ def set_primary_data_source(data_source): ''' @@ -894,6 +882,7 @@ def create_Zones_object(): zone_entities = Gb.hass.states.entity_ids(ZONE) er_zones, zone_entity_data = entity_io.get_entity_registry_data(platform=ZONE) + yaml_zones = [zone for zone in zone_entities if zone.replace('zone.', '') not in er_zones] Gb.state_to_zone = STATE_TO_ZONE_BASE.copy() OldZones_by_zone = Gb.Zones_by_zone.copy() @@ -920,7 +909,9 @@ def create_Zones_object(): # Add HA zones that are saved in the HA Entity Registry. This does not include # current Stationary Zones - for zone in er_zones: + # for zone in er_zones: + for raw_zone in zone_entities: + zone = raw_zone.replace('zone.', '') zone_entity_name = f"zone.{zone}" zone_data = entity_io.get_attributes(zone_entity_name) if (zone_entity_name in zone_entity_data @@ -1960,6 +1951,9 @@ def setup_trackable_devices(): Gb.used_data_source_FMF = True event_msg += f"{CRLF_DOT}FmF Device: {Device.conf_fmf_email}" + # Set a flag indicating there is a tracked device that does not use the ios app + if Device.iosapp_monitor_flag is False and Device.is_tracked: + Gb.iosapp_monitor_any_devices_false_flag = True # Initialize iosapp state & location fields if Device.iosapp_monitor_flag: diff --git a/custom_components/icloud3/support/stationary_zone.py b/custom_components/icloud3/support/stationary_zone.py index eaae3dc..a04b155 100644 --- a/custom_components/icloud3/support/stationary_zone.py +++ b/custom_components/icloud3/support/stationary_zone.py @@ -46,10 +46,9 @@ def move_device_into_statzone(Device): if _is_too_close_to_another_zone(Device): return # ''' End of commented out code to test of moving device into a statzone while home - _ha_statzones = ha_statzones() - # Cycle thru existing ic3 StatZones looking for one that can be recreated at a # new location. + _ha_statzones = ha_statzones() for StatZone in Gb.StatZones: if StatZone.passive and StatZone.zone not in _ha_statzones: event_msg = f"Reusing Stationary Zone > {StatZone.fname_id}" @@ -67,10 +66,8 @@ def move_device_into_statzone(Device): StatZone.radius_m = Gb.statzone_radius_m StatZone.passive = False - still_since_secs = Device.statzone_timer - Gb.statzone_still_time_secs _clear_statzone_timer_distance(Device, create_statzone_flag=True) - StatZone.away_attrs[LATITUDE] = latitude StatZone.away_attrs[LONGITUDE] = longitude @@ -82,11 +79,9 @@ def move_device_into_statzone(Device): Device.into_zone_datetime = datetime_now() Device.selected_zone_results = [] - # event_msg =(f"Assigned Stationary Zone > {StatZone.display_as}, > " - # f"StationarySince-{format_time_age(still_since_secs)}") - # post_event(Device.devicename, event_msg) - iosapp_interface.request_location(Device) + + # Move monitored devices into the new StatZone if they should be in it _trigger_monitored_device_update(StatZone, Device, ENTER_ZONE) return True @@ -244,23 +239,38 @@ def ha_statzones(): #-------------------------------------------------------------------- def _trigger_monitored_device_update(StatZone, Device, action): + ''' + When a StatZone is being created, see if any monitored devices are close enough to + the device creating it and, if so, trigger a locate update so they will move into + it. + + When the last device in a StatZone exited from it and there are monitored devices in + it, move all monitored devices in that StatZone out of it. Then trigger an update + to reset the monitored device as Away + ''' for _Device in Gb.Devices_by_devicename_monitored.values(): - if action == ENTER_ZONE: - dist_m = _Device.distance_m(Device.loc_data_latitude, Device.loc_data_longitude) - event_msg = f"Trigger > Create Stationary Zone > {StatZone.display_as}" - post_event(_Device.devicename, event_msg) + event_msg = "" + if action == ENTER_ZONE and _Device.StatZone is None: + dist_apart_m = _Device.distance_m(Device.loc_data_latitude, Device.loc_data_longitude) + if dist_apart_m <= Gb.statzone_radius_m: + event_msg = f"Trigger > Enter New Stationary Zone > {StatZone.display_as}" + post_event(_Device.devicename, event_msg) elif action == EXIT_ZONE and _Device.StatZone is StatZone: - event_msg = f"Trigger > Remove Stationary Zone > {StatZone.display_as}" + _Device.StatZone = None + event_msg = f"Trigger > Exit Removed Stationary Zone > {StatZone.display_as}" post_event(_Device.devicename, event_msg) else: continue - Gb.force_icloud_update_flag = True - det_interval.update_all_device_fm_zone_sensors_interval(_Device, 5) - _Device.icloud_update_reason = event_msg - _Device.write_ha_sensors_state([NEXT_UPDATE, INTERVAL]) + if event_msg: + # v3.0.rc7.1 Change Global force_update to the actual device needing it + # Gb.icloud_force_update_flag = True + _Device.icloud_force_update_flag = True + det_interval.update_all_device_fm_zone_sensors_interval(_Device, 5) + _Device.icloud_update_reason = event_msg + _Device.write_ha_sensors_state([NEXT_UPDATE, INTERVAL]) #-------------------------------------------------------------------- def devices_in_statzone_count(StatZone): diff --git a/custom_components/icloud3/translations/en.json b/custom_components/icloud3/translations/en.json index d51ffdc..ecaec50 100644 --- a/custom_components/icloud3/translations/en.json +++ b/custom_components/icloud3/translations/en.json @@ -34,7 +34,7 @@ "description": "Enter the 6-digit verification code you just received from Apple", "data": { "verification_code": "ICLOUD ACCOUNT VERIFICATION CODE", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════" } }, "restart_ha": { @@ -62,10 +62,12 @@ "icloud_acct_logging_into": "Logging into iCloud Account", "icloud_acct_logged_into": "Successfully Logged into the iCloud Account", "icloud_acct_already_logged_into": "Already Logged into the iCloud Account", - "icloud_acct_login_error": "iCloud Account Login Failed - Review Username/Password (or China '.cn' suffix)", - "icloud_acct_not_available": "iCloud Account is not Available, Login Unsuccessful", + "icloud_acct_login_error_user_pw": "Login Failed, Invalid Username or Password (err-400)", + "icloud_acct_login_error_other": "Login Failed, Other Error or iCloud is not Available", + "icloud_acct_login_error_connection": "Login Error, Failed to Connect to iCloud Server (err-302)", + "icloud_acct_not_available": "Login Error, iCloud Account is not Available", "icloud_acct_not_logged_into": "iCloud Account is not Logged Into", - "icloud_acct_not_set_up": "The iCloud Account Username or Password needs to be entered", + "icloud_acct_not_set_up": "iCloud Account Username or Password needs to be entered", "icloud_acct_no_data_source": "No Data Source has been selected", "verification_code_requested": "The Apple ID Verification Code was requested, BROWSER REFRESH MAY BE NEEDED", @@ -131,7 +133,7 @@ "title": "iCloud3 Configure Settings", "data": { "menu_items": "", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════" } }, "restart_icloud3": { @@ -155,16 +157,22 @@ "action_items": "" } }, + "confirm_action": { + "title": "Confirm Selected Action", + "data": { + "action_items": "" + } + }, "icloud_account": { "title": "iCloud Account & iOS App Data Sources", "description": "The data sources (iCloud Account Web Services and the iOS App) are selected on this screen. The Apple iCloud Account Username/Password must be specified here to access the iCloud Account. The HA Companion App (iOS App) must be installed on the iDevice to use is as a data source for that device. Each iDevice you are tracking is associated with the iCloud3 device on the Update Devices screen.", "data": { - "data_source_icloud": "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ICLOUD ACCOUNT - Location data is provided by the Apple iCloud account", + "data_source_icloud": "═════════════════════════════════════════════════════ ICLOUD ACCOUNT - Location data is provided by the Apple iCloud account", "username": "APPLE ID (USERNAME) - The email address used to sign in to the iCloud Account", "password": "PASSWORD - The iCloud Account Password", "url_suffix_china": "CHINA USERS - Add '.cn' to use the Apple iCloud Web Servers located in China", - "data_source_iosapp": "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ iOS APP - Location data is provided by the HA iOS App", - "action_items": "═══════════════════════════════════════════════════" + "data_source_iosapp": "═════════════════════════════════════════════════════ iOS APP - Location data is provided by the HA iOS App", + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" }, "data_description": { } @@ -174,15 +182,15 @@ "description": "Enter the 6-digit verification code you just received from Apple", "data": { "verification_code": "ICLOUD ACCOUNT VERIFICATION CODE", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" } }, "device_list": { "title": "iCloud3 Device Tracker Entities", "description": "All of the devices that are tracked or monitored by iCloud3 are listed on this screen. You can add new devices and update or delete existing devices.", "data": { - "devices": "", - "action_items": "═══════════════════════════════════════════════════" + "devices": "═════════════════════════════════════════════════════ ICLOUD3 DEVICES", + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" } }, "add_device": { @@ -193,7 +201,7 @@ "device_type": "DEVICE TYPE - iPhone, iPad, Watch, etc.", "tracking_mode": "TRACKING MODE - How location requests should be done (Full tracking, Monitor, Inactive)", "iosapp": "iOS APP INSTALLED - The HA iOS App is installed on this device ", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" }, "data_description": { } @@ -213,7 +221,7 @@ "track_from_base_zone": "PRIMARY HOME ZONE - Use another zone if you are away from Home for an extended period", "picture": "PICTURE - Photo image of the person normally using this device (40x40 pixels is best size)", "inzone_interval": "INZONE INTERVAL", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" }, "data_description": { "inzone_interval": "Time between location requests when in a zone", @@ -223,22 +231,23 @@ "delete_device": { "title": "Delete Device(s), Other Device Maintenance", "data": { - "action_items": "" + "action_items": "═════════════════════════════════════════════════════ DELETE OPTIONS" } }, "review_inactive_devices": { "title": "Review Untracked (Inactive) Devices", "description": "The 'Tracking Mode' of devices are set to 'Inactive' and will not be located or tracked.", "data": { - "inactive_devices": "", - "action_items": "═══════════════════════════════════════════════════" + "inactive_devices": "═════════════════════════════════════════════════════ INACTIVE ICLOUD3 DEVICES", + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" } }, "change_device_order": { "title": "Event Log Device Display Sequence", + "description": "The devices are displayed in the Event Log heading area and in various Event Log messages in the sequence below. This screen lets you change the order of the devices.", "data": { - "device_desc": "DEVICES - The devices are displayed in the Event Log heading area and in various Event Log messages in this sequence", - "action_items": "═══════════════════════════════════════════════════" + "device_desc": "═════════════════════════════════════════════════════ ICLOUD3 DEVICES", + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" } }, "format_settings": { @@ -252,7 +261,7 @@ "unit_of_measurement": "UNIT OF MEASUREMENT - How distance fieldsare displayed in sensors and in the Event Log", "display_gps_lat_long": "DISPLAY GPS COORDINATES - Display the GPS (Latitude, Longitude/±Accuracy) or only the GPS (/±Accuracy) in the Event Log", "display_gps_lat_long2": "DISPLAY GPS COORDINATES - Display the GPS Latitude/Longitude/Accuracy (22.32771, -76.33073/±35m) or only the GPS accuracy (/±35m) in the Event Log", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" }, "data_description": { "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." @@ -262,8 +271,8 @@ "title": "Event Log 'Display Text As'", "description": "There may be some text fields that are displayed on the Event Log screen that may be sensitive in nature. Some examples include email addresses or phone numbers. With this screen, you can select the Original Text and what should be displayed instead. For example, you can replace 'geekstergary@apple.com' with 'gary@email.com'", "data": { - "display_text_as": "Text Replacement Fields", - "action_items": "═══════════════════════════════════════════════════" + "display_text_as": "═════════════════════════════════════════════════════ TEXT REPLACEMENT FIELDS - [Actual text > Displayed text]", + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" } }, "display_text_as_update": { @@ -271,7 +280,7 @@ "data": { "text_from": "ORIGINAL TEXT - Text to be replaced (example: gary_real_email@gmail.com)", "text_to": "DISPLAYED TEXT- Text to be displayed (display: gary@email.com)", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" }, "data_description": { } @@ -279,7 +288,10 @@ "actions": { "title": "iCloud3 Action Commands", "data": { - "action_items": "" + "ic3_actions": "═════════════════════════════════════════════════════ ICLOUD3 GENERAL CONTROL ACTIONS", + "debug_actions": "═════════════════════════════════════════════════════ DEBUG LOG ACTIONS", + "other_actions": "═════════════════════════════════════════════════════ OTHER ACTIONS", + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" } }, "tracking_parameters": { @@ -299,12 +311,12 @@ "travel_time_factor": "TRAVEL TIME INTERVAL MULTIPLIER", "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_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": "See if there are any other devices that are near the device being updated. If there is, use that device's location results instead of going through the calculation process. This can speed up the update since Waze travel time and distance information does not have to be requested", + "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", @@ -328,7 +340,7 @@ "discard_poor_gps_inzone": "Discard Location Updates with Poor GPS Accuracy when in a Zone", "distance_between_devices": "Determine the distance between devices. Use a near by device's tracking results", "center_in_zone": "Change Device's Location to the Zone's Center when in a Zone", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" }, "data_description": { "no_iosapp": "Default interval if the iOS App is not used for location monitoring and zone enter/exit triggers", @@ -338,15 +350,15 @@ "waze_main": { "title": "Waze - Route Service Travel Time/Distance", "data": { - "waze_used": "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ WAZE ROUTE SERVICE", + "waze_used": "═════════════════════════════════════════════════════ WAZE ROUTE SERVICE", "waze_region": "ROUTE SERVER LOCATION - Location of the Waze Route Server for your area", "waze_realtime": "USE REAL TIME DATA - Waze should consider traffic delays when determining travel time", "waze_min_distance": "WAZE MINIMUM DISTANCE", "waze_max_distance": "WAZE MAXIMUM DISTANCE", - "waze_history_database_used": "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ WAZE HISTORY DATABASE", + "waze_history_database_used": "═════════════════════════════════════════════════════ WAZE HISTORY DATABASE", "waze_history_track_direction": "GENERAL TRAVEL DIRECTION - Used to display 'Map Trace Lines' between saved locations", "waze_history_max_distance": "HISTORY MAX DISTANCE", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" }, "data_description": { "waze_min_distance": "Use the Waze Route Service when the zone distance is greater than this value", @@ -356,17 +368,18 @@ }, "special_zones": { "title": "Special Zones - Enter Zone Delay, Stationary Zone, Primay Home Base Zone", + "description": "This screen lets you configure an enter zone delay time, stationary zones and set up a temporary 'home' zone at another location", "data": { - "passthru_zone_header": "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ENTER ZONE DELAY", + "passthru_zone_header": "═════════════════════════════════════════════════════ ENTER ZONE DELAY", "passthru_zone_time": "ENTER ZONE DELAY TIME ", - "stat_zone_header": "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ STATIONARY ZONE", + "stat_zone_header": "═════════════════════════════════════════════════════ STATIONARY ZONE", "stat_zone_still_time": "NO MOVEMENT TIME ", "stat_zone_inzone_interval": "INZONE INTERVAL ", "stat_zone_fname": "FRIENDLY NAME BASE - Name to display when in a Stationary Zone (StatZone)", - "track_from_zone_header": "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ PRIMARY BASE ZONE = HOME", + "track_from_zone_header": "═════════════════════════════════════════════════════ PRIMARY BASE ZONE = HOME", "track_from_base_zone": "PRIMARY 'HOME' ZONE - Use another zone if you are away from Home for an extended period (Global Override)", "track_from_home_zone": "ALSO TRACK FROM HOME ZONE - Continue to track from the Home zone when the Primary Home Zone is not Home", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" }, "data_description": { "passthru_zone_time": "Delay processing an Enter Zone Trigger that you may be driving through and not actually entering", @@ -379,17 +392,17 @@ "title": "Device and Tracking Sensors created by iCloud3", "description": "Many sensors are used to display tracking results and other information for a device. This screen is used to select the sensors that should be created.", "data": { - "monitored_devices": "MONITORED DEVICE SENSORS - Select the type of sensors to create for a Monitored Device", - "device": "DEVICE SENSORS - Device status and information", - "tracking_update": "LOCATION UPDATE SENSORS - Device location update times", - "tracking_time": "TIME SENSORS - Device tracking timers", - "tracking_distance": "DISTANCE SENSORS - Device tracking distances", - "track_from_zones": "TRACK FROM MULTIPLE ZONE SENSORS - Used when tracking from more than one zone (not needed for tracking only from the Home zone)", - "tracking_other": "OTHER TRACKING SENSORS - Not normally used but available", - "zone": "ZONE SENSORS - Device zone status and information", - "other": "OTHER SENSORS - Sensors not in the above areas", - "excluded_sensors": "EXCLUDED SENSORS - Sensors that will not be created when iCloud3 starts", - "action_items": "═══════════════════════════════════════════════════" + "monitored_devices": "═════════════════════════════════════════════════════ MONITORED DEVICE SENSORS - Select the type of sensors to create for a Monitored Device", + "device": "═════════════════════════════════════════════════════ DEVICE SENSORS - Device status and information", + "tracking_update": "═════════════════════════════════════════════════════ LOCATION UPDATE SENSORS - Device location update times", + "tracking_time": "═════════════════════════════════════════════════════ TIME SENSORS - Device tracking timers", + "tracking_distance": "═════════════════════════════════════════════════════ DISTANCE SENSORS - Device tracking distances", + "track_from_zones": "═════════════════════════════════════════════════════ TRACK FROM MULTIPLE ZONE SENSORS - Used when tracking from more than one zone (not needed for tracking only from the Home zone)", + "tracking_other": "═════════════════════════════════════════════════════ OTHER TRACKING SENSORS - Not normally used but available", + "zone": "═════════════════════════════════════════════════════ ZONE SENSORS - Device zone status and information", + "other": "═════════════════════════════════════════════════════ OTHER SENSORS - Sensors not in the above areas", + "excluded_sensors": "═════════════════════════════════════════════════════ EXCLUDED SENSORS - Sensors that will not be created when iCloud3 starts", + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" } }, "exclude_sensors": { @@ -399,7 +412,7 @@ "excluded_sensors": "EXCLUDED SENSORS - Sensors that will not be created when iCloud3 starts", "filter": "FILTER DISPLAYED SENSORS - Select the Sensors that should be displayed", "filtered_sensors": "ICLOUD3 SENSORS - A list of Sensors that are created when iCloud3 starts", - "action_items": "═══════════════════════════════════════════════════" + "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" } } } diff --git a/custom_components/icloud3/zone.py b/custom_components/icloud3/zone.py index 7a2cb47..247e350 100644 --- a/custom_components/icloud3/zone.py +++ b/custom_components/icloud3/zone.py @@ -20,6 +20,7 @@ ZONE, TITLE, FNAME, NAME, ID, FRIENDLY_NAME, ICON, LATITUDE, LONGITUDE, RADIUS, PASSIVE, STATZONE_RADIUS_1M, ZONE, ) +from .support import iosapp_interface from .helpers.common import (instr, is_statzone, format_gps, zone_display_as,) from .helpers.messaging import (post_event, post_error_msg, post_monitor_msg, log_exception, log_rawdata,_trace, _traceha, ) @@ -70,7 +71,7 @@ def __init__(self, zone, zone_data): self.passive = zone_data.get(PASSIVE, True) self.is_real_zone = (self.radius_m > 0) self.isnot_real_zone = not self.is_real_zone # (Description only zones/Away, not_home, not_set, etc) - self.dist_time_history = [] #Entries are a list - [lat, long, distance, travel time] + self.dist_time_history = [] # Entries are a list - [lat, long, distance, travel time] self.er_zone_id = zone_data.get(ID, zone.lower()) # HA entity_registry id self.entity_id = self.er_zone_id[:6] @@ -190,7 +191,7 @@ def __init__(self, statzone_id): self.fname = f"StatZon{self.statzone_id}" self.fname_id = self.display_as = Gb.zone_display_as[self.zone] = self.fname - #base_attrs is used to move the stationary zone back to it's base + # base_attrs is used to move the stationary zone back to it's base self.base_attrs[NAME] = self.zone self.base_attrs[RADIUS] = STATZONE_RADIUS_1M self.base_attrs[PASSIVE] = True @@ -220,7 +221,7 @@ def __init__(self, statzone_id): self.write_ha_zone_state(self.base_attrs) self.name = self.title = self.display_as - #away_attrs is used to move the stationary zone back to it's base + # away_attrs is used to move the stationary zone back to it's base self.away_attrs = self.base_attrs.copy() self.away_attrs[RADIUS] = Gb.statzone_radius_m self.away_attrs[PASSIVE] = False @@ -233,8 +234,8 @@ def initialize_updatable_items(self): if instr(Gb.statzone_fname, '#') is False: self.fname_id = f"{self.fname} (..._{self.statzone_id})" - self.base_latitude = 0 #Gb.statzone_base_latitude - self.base_longitude = 0 #Gb.statzone_base_longitude + self.base_latitude = 0 + self.base_longitude = 0 self.base_attrs[FRIENDLY_NAME] = self.fname self.base_attrs[LATITUDE] = self.base_latitude @@ -289,11 +290,12 @@ def remove_ha_zone(self): try: Gb.hass.states.async_remove(f"zone.{self.zone}") + Gb.hass.services.call(ZONE, "reload") post_event(f"Removed HA Zone > {self.fname_id}") except Exception as err: + log_exception(err) pass - # log_exception(err) #--------------------------------------------------------------------