diff --git a/custom_components/icloud3/ChangeLog.txt b/custom_components/icloud3/ChangeLog.txt index 3537f33..9195333 100644 --- a/custom_components/icloud3/ChangeLog.txt +++ b/custom_components/icloud3/ChangeLog.txt @@ -1,4 +1,11 @@ -rc7 - Release Candidate 6 (10/7/2023) +rc7.1 - Release Candidate 7.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 - Release Candidate 7 (10/15/2023) ............................... 1. yaml Zones - Fixed a problem where zones configured using yaml were not being loaded when iCloud3 started. 2. 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: diff --git a/custom_components/icloud3/config_flow.py b/custom_components/icloud3/config_flow.py index 833b629..5e689c7 100644 --- a/custom_components/icloud3/config_flow.py +++ b/custom_components/icloud3/config_flow.py @@ -197,6 +197,9 @@ def dict_value_to_list(key_value_dict): '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": "═══════════════════════════════════════" @@ -1168,6 +1171,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): ''' @@ -1921,7 +1967,22 @@ async def async_step_icloud_account(self, user_input=None, errors=None, called_f 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': + # user_input = self.user_input_multi_form + 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['url_suffix_china']): + self.user_input_multi_form = user_input.copy() + + return await self.async_step_confirm_action(user_input, + action_items = ['confirm_return','confirm_save'], + called_from_step_id='icloud_account') return await self.async_step_menu() # Data Source is iOS App only, iCloud was not selected @@ -2069,7 +2130,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' @@ -3467,7 +3528,11 @@ 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 > '): action_item = 'next_page_items' @@ -3476,6 +3541,8 @@ def _action_text_to_item(self, user_input): 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') + + else: action_item = None @@ -3777,12 +3844,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 +3881,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[-1] + + 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' @@ -4431,17 +4508,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 d3771fd..81737ff 100644 --- a/custom_components/icloud3/const.py +++ b/custom_components/icloud3/const.py @@ -4,7 +4,7 @@ # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -VERSION = '3.0.rc7' +VERSION = '3.0.rc7.1' DOMAIN = 'icloud3' ICLOUD3 = 'iCloud3' diff --git a/custom_components/icloud3/device.py b/custom_components/icloud3/device.py index 641d0d6..18076e2 100644 --- a/custom_components/icloud3/device.py +++ b/custom_components/icloud3/device.py @@ -85,9 +85,9 @@ def __init__(self, devicename, conf_device): 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 @@ -247,7 +247,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' 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 0000000..501444f Binary files /dev/null and b/custom_components/icloud3/event_log_card/icloud3-event-log-card.js.gz differ diff --git a/custom_components/icloud3/global_variables.py b/custom_components/icloud3/global_variables.py index 6367354..933363f 100644 --- a/custom_components/icloud3/global_variables.py +++ b/custom_components/icloud3/global_variables.py @@ -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/icloud3_main.py b/custom_components/icloud3/icloud3_main.py index 2991741..c600be4 100644 --- a/custom_components/icloud3/icloud3_main.py +++ b/custom_components/icloud3/icloud3_main.py @@ -427,7 +427,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) @@ -777,7 +777,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: @@ -849,7 +854,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 @@ -859,6 +864,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() @@ -914,7 +920,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 @@ -1026,6 +1032,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) #------------------------------------------------------------------------------ # @@ -1051,8 +1083,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 == []: @@ -1074,74 +1104,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_cnt_by_zone = self._zones_cnt_by_zone(zone_selected, Device.loc_data_zone) - 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') - # _trace(f"zonsel={zone_selected} devzon={Device.loc_data_zone} {zones_cnt_by_zone=}") - # _trace(f"{zones_cnt_summary_msg}") - - zones_distance_list.sort() - zones_distance_msg = zones_cnt_msg = '' - for zone_distance_list in zones_distance_list: - zdl_items = zone_distance_list.split('|') - _zone = zdl_items[1] - _zone_dist = zdl_items[2] - - zones_distance_msg += f"{_zone_dist}, " - if zones_cnt_by_zone.get(_zone, 0) > 0: - zones_distance_msg += f" ({zones_cnt_by_zone[_zone]})" - zones_cnt_msg +=(f"{_zone_dist.split('^')[0] }" - f" ({zones_cnt_by_zone[_zone]}), ") - del zones_cnt_by_zone[zone_selected] - zones_distance_msg = zones_distance_msg.replace('^', '-') - - if display_zone_msg: - selected_zone_msg = other_zones_msg = gps_accuracy_msg = '' - - # Format the Zone Selected Msg (ZoneName (#)) - if ZoneSelected.radius_m > 0: - selected_zone_msg = f"-{format_dist_m(zone_selected_dist_m)}" - if zone_selected in zones_cnt_by_zone: - selected_zone_msg += f" ({zones_cnt_by_zone[zone_selected]})" - del zones_cnt_by_zone[zone_selected] - - # Format the Zone Not Selected Msg (ZoneName-#km (#)) - if (zone_selected == NOT_HOME - or (is_statzone(zone_selected) and isnot_statzone(Device.loc_data_zone))): - other_zones_msg = f"{zones_distance_msg}" - else: - other_zones_msg = zones_cnt_msg - - # 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()] - other_zones_msg += list_to_str(zones_cnt_summary).replace('──', 'NotSet') - - if other_zones_msg: other_zones_msg = f" > {other_zones_msg}" - - if zone_selected_dist_m > ZoneSelected.radius_m: - gps_accuracy_msg = f", AccuracyAdjustment-{gps_accuracy_adj}m" - - # _trace(f"{selected_zone_msg=} " - # f"{other_zones_msg=} " - # f"{zones_cnt_summary_msg=} ") - 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}") - - 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 @@ -1152,16 +1116,21 @@ 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 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) + if display_zone_msg: + self._post_zone_selected_msg(Device, ZoneSelected, zone_selected, + zone_selected_dist_m, zones_distance_list) return ZoneSelected, zone_selected @@ -1213,6 +1182,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 @@ -1232,54 +1204,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 - zones_cnt_by_zone = self._zones_cnt_by_zone(zone_selected, Device.loc_data_zone) - # _trace(f"zonsel={zone_selected} devzon={Device.loc_data_zone} {zones_cnt_by_zone=}") - # 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}|" - f"{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 _zones_cnt_by_zone(zone_selected, device_zone): - # Get a list of all the zones, their distance, size and display_as + 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)} - # Adjust the zone counts based on the zone selected and the devices current - # zone since the device ha s not been updated yet - zone_selected = NOT_HOME if zone_selected == '' else zone_selected - if device_zone == zone_selected: - return zones_cnt_by_zone + 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: - zones_cnt_by_zone[zone_selected] += 1 - else: - zones_cnt_by_zone[zone_selected] = 1 - if device_zone != zone_selected: - zones_cnt_by_zone[device_zone] -= 1 - if zones_cnt_by_zone[device_zone] == 0: - del zones_cnt_by_zone[device_zone] - else: - zones_cnt_by_zone[device_zone] = 1 + zone_selected_msg += f" ({zones_cnt_by_zone[zone_selected]})" - return zones_cnt_by_zone + # 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()] -#-------------------------------------------------------------------- - def _format_zone_info(self, zone_data): - ''' - Format each device's zone information (display_as, distancee). It is used - to build the info about the zone that was not selected. + # 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, ") - return (f"{zone_data[ZD_NAME]}|" - f"{zone_data[ZD_DISPLAY_AS]}^{format_dist_m(zone_data[ZD_DIST_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): @@ -1563,8 +1566,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/icloud3_v3.0.rc7.zip b/custom_components/icloud3/icloud3_v3.0.rc7.zip deleted file mode 100644 index bfde3b9..0000000 Binary files a/custom_components/icloud3/icloud3_v3.0.rc7.zip and /dev/null differ diff --git a/custom_components/icloud3/strings.json b/custom_components/icloud3/strings.json index d51ffdc..0776d81 100644 --- a/custom_components/icloud3/strings.json +++ b/custom_components/icloud3/strings.json @@ -155,6 +155,12 @@ "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.", 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/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/pyicloud_ic3.py b/custom_components/icloud3/support/pyicloud_ic3.py index 7ee07c9..e430f80 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, @@ -1035,7 +1035,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/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 c4e2746..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): ''' @@ -1963,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 2179f00..a04b155 100644 --- a/custom_components/icloud3/support/stationary_zone.py +++ b/custom_components/icloud3/support/stationary_zone.py @@ -241,12 +241,12 @@ 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 + 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 - redcoat the monitored device as Away + 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(): event_msg = "" @@ -265,7 +265,9 @@ def _trigger_monitored_device_update(StatZone, Device, action): continue if event_msg: - Gb.force_icloud_update_flag = True + # 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]) diff --git a/custom_components/icloud3/translations/en.json b/custom_components/icloud3/translations/en.json index d51ffdc..0776d81 100644 --- a/custom_components/icloud3/translations/en.json +++ b/custom_components/icloud3/translations/en.json @@ -155,6 +155,12 @@ "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.",