From 51dea27ce0fb691cbc122a6886eb59e9938dc6b9 Mon Sep 17 00:00:00 2001 From: gcobb321 Date: Tue, 7 May 2024 14:45:35 -0400 Subject: [PATCH] iCloud3 v3.0.4 --- README.md | 4 +- custom_components/icloud3/ChangeLog.txt | 24 +- custom_components/icloud3/__init__.py | 5 +- custom_components/icloud3/config_flow.py | 442 +++++++++++------- custom_components/icloud3/const.py | 42 +- custom_components/icloud3/const_sensor.py | 23 +- custom_components/icloud3/device.py | 280 ++++++----- custom_components/icloud3/device_fm_zone.py | 65 ++- custom_components/icloud3/device_tracker.py | 21 +- .../icloud3-event-log-card.js.gz | Bin 0 -> 22553 bytes custom_components/icloud3/global_variables.py | 5 +- custom_components/icloud3/hacs-copy.json | 2 +- custom_components/icloud3/helpers/common.py | 36 +- .../icloud3/helpers/dist_util.py | 16 +- .../icloud3/helpers/entity_io.py | 18 +- .../icloud3/helpers/messaging.py | 195 ++++---- .../icloud3/helpers/time_util.py | 140 ++++-- custom_components/icloud3/icloud3_main.py | 99 ++-- custom_components/icloud3/sensor.py | 25 +- .../icloud3/support/config_file.py | 16 +- .../icloud3/support/determine_interval.py | 150 +++--- .../icloud3/support/event_log.py | 163 ++++--- .../icloud3/support/icloud_data_handler.py | 2 +- .../icloud3/support/mobapp_data_handler.py | 17 +- .../icloud3/support/mobapp_interface.py | 24 +- .../icloud3/support/pyicloud_ic3.py | 74 +-- .../icloud3/support/pyicloud_ic3_interface.py | 29 +- .../icloud3/support/restore_state.py | 9 +- .../icloud3/support/service_handler.py | 20 +- .../icloud3/support/start_ic3.py | 228 +++++---- .../icloud3/support/start_ic3_control.py | 55 ++- .../icloud3/support/stationary_zone.py | 14 +- .../icloud3/support/zone_handler.py | 13 +- .../icloud3/translations/en.json | 25 +- 34 files changed, 1398 insertions(+), 883 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 d32308b..9621215 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ ------ -[![CurrentVersion](https://img.shields.io/badge/Current_Version-v3.0.2-blue.svg)](https://github.com/gcobb321/icloud3_v3) [![Type](https://img.shields.io/badge/Type-Custom_Component-orange.svg)](https://github.com/gcobb321/icloud3_v3) [![HACS](https://img.shields.io/badge/HACS-Custom_Repository-orange.svg)](https://github.com/gcobb321/icloud3_v3) +[![CurrentVersion](https://img.shields.io/badge/Current_Version-v3.0.3-blue.svg)](https://github.com/gcobb321/icloud3_v3) [![Type](https://img.shields.io/badge/Type-Custom_Component-orange.svg)](https://github.com/gcobb321/icloud3_v3) [![HACS](https://img.shields.io/badge/HACS-Standard_Repository-orange.svg)](https://github.com/gcobb321/icloud3_v3) -[![ProjectStage](https://img.shields.io/badge/Project_Stage-Almost_General_Release-forestgreen.svg)](https://github/gcobb321/icloud3_v3) [![Released](https://img.shields.io/badge/Released-April,_2024-forestgreen.svg)](https://github.com/gcobb321/icloud3_v3) +[![ProjectStage](https://img.shields.io/badge/Project_Stage-General_Availability-forestgreen.svg)](https://github/gcobb321/icloud3_v3) [![Released](https://img.shields.io/badge/Released-May,_2024-forestgreen.svg)](https://github.com/gcobb321/icloud3_v3) diff --git a/custom_components/icloud3/ChangeLog.txt b/custom_components/icloud3/ChangeLog.txt index e0ab723..aa2a785 100644 --- a/custom_components/icloud3/ChangeLog.txt +++ b/custom_components/icloud3/ChangeLog.txt @@ -2,7 +2,29 @@ - iCloud3 Documentation is [here](https://gcobb321.github.io/icloud3_v3_docs/#/chapters/0.1-introduction) - The *Installing iCloud3* chapter describes migrating from iCloud3 v2 and installing iCloud3 for the first time -### Change Log + + +v3.0.4 +....................... +### Change Log - v3.0.4 (not released) +1. ADD DEVICE (Fixed) - An 'Out of Range' error message was encountered adding the first device. +2. DIRECTION OF TRAVEL (Improvement) - Tweaked the AwayFrom direction override when approaching Home after the previous directions were Towards. +3. Event Type DEPRECIATED EOORO MESSAGE (Fixed) - This was a warning about the removal of the EventType from HA next year. It has been removed. +4. RAWDATA LOGGING - Changed some formatting of the log to better filter device messages. + + +v3.0.3 +....................... +### Change Log - v3.0.3 (5/1/2024) +1. ALERTS (New) - An alert message is displayed on the Event Log and in the _alert_ attribute on the device's device_tracker and badge entities until it has been resolved. Examples of alerts are a startup error, no gps data, the device is offline, the battery below 20%, and tracking is paused. The alert attribute can be used in an automation to trigger sending a message to any device using the Mobile App. See the _Reference > Devices and Other Alerts_ chapter in the iCloud3 docs [here](#/chapters/7.6-alerts) for more information and example automations. +2. BATTERY (Improvement) - The battery information attribute has been added to the device's device_tracker and badge entity. It shows the battery level, charging state, when the information was updated and the source of the data. The charging status text has been changed to 'Charged', 'Charging', 'NotCharging', 'Low' and 'Unknown'. +3. UPDATE SENSOR (Fixed) - An 'AttributeError' message has been fixed. It was caused by trying to update the sensor before the sensor had been set up. +4. CONFIGURE SETTINGS > ICLOUD ACCOUNT AND MOBILE APP screen (Fixed) - Changing iCloud account information (Username or password) was not being saved correctly so restarting iCloud3 would still use the old account. A Log Off option was added to initialize the iCloud Account username/password fields. +5. DIRECTION OF TRAVEL (Improvement) - When Driving towards Home, the calculated straight line distance is used to determine the travel direction ('Towards'). The direction would momentarily change to 'AwayFrom' if the distance from Home increased due to a curve in the road or you were stopped at an intersection. It would then change back to 'Towards' on the next update. In this case, the direction will not be changed and will remain 'Towards'. +6. Other minor code changes, tuning and code cleanup. + + + v3.0.2 - 3/30/2023 ....................... 1. ICLOUD SERVER ERROR MESSAGE (Fixed) - When the iCloud servers did not respond with location information, the 'no response from iCloud servers' error was displayed correctly. This was followed by another unrelated error which should have not been displayed. diff --git a/custom_components/icloud3/__init__.py b/custom_components/icloud3/__init__.py index b2e657b..af83499 100644 --- a/custom_components/icloud3/__init__.py +++ b/custom_components/icloud3/__init__.py @@ -75,7 +75,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: config_file.load_storage_icloud3_configuration_file() if Gb.conf_profile[CONF_VERSION] == 1: - Gb.HALogger.info(f"Initializing iCloud3 v{VERSION} - Remove Platform: iCloud3 statement") + Gb.HALogger.warning(f"Starting iCloud3 v{VERSION} > " + "Detected a `platform: icloud3` statement in the " + "configuration.yaml file. This is depreciated and " + "should be removed.") except Exception as err: # log_exception(err) diff --git a/custom_components/icloud3/config_flow.py b/custom_components/icloud3/config_flow.py index 8a4b901..8c0e986 100644 --- a/custom_components/icloud3/config_flow.py +++ b/custom_components/icloud3/config_flow.py @@ -18,7 +18,7 @@ from .global_variables import GlobalVariables as Gb from .const import (DOMAIN, ICLOUD3, DATETIME_FORMAT, - RARROW, RARROW2, CRLF_DOT, DOT, HDOT, CIRCLE_STAR, YELLOW_ALERT, + RARROW, RARROW2, CRLF_DOT, DOT, HDOT, CIRCLE_STAR, YELLOW_ALERT, RED_ALERT, EVLOG_NOTICE, EVLOG_ALERT, IPHONE_FNAME, IPHONE, IPAD, WATCH, AIRPODS, ICLOUD, FAMSHR, FMF, OTHER, HOME, DEVICE_TYPES, DEVICE_TYPE_FNAME, DEVICE_TRACKER_DOT, @@ -40,7 +40,7 @@ CONF_MAX_INTERVAL, CONF_OFFLINE_INTERVAL, CONF_EXIT_ZONE_INTERVAL, CONF_MOBAPP_ALIVE_INTERVAL, CONF_GPS_ACCURACY_THRESHOLD, CONF_OLD_LOCATION_THRESHOLD, CONF_OLD_LOCATION_ADJUSTMENT, CONF_TRAVEL_TIME_FACTOR, CONF_TFZ_TRACKING_MAX_DISTANCE, - CONF_PASSTHRU_ZONE_TIME, CONF_LOG_LEVEL, + CONF_PASSTHRU_ZONE_TIME, CONF_LOG_LEVEL, CONF_LOG_LEVEL_DEVICES, CONF_DISPLAY_ZONE_FORMAT, CONF_DEVICE_TRACKER_STATE_SOURCE, CONF_DISPLAY_GPS_LAT_LONG, CONF_CENTER_IN_ZONE, CONF_DISCARD_POOR_GPS_INZONE, CONF_DISTANCE_BETWEEN_DEVICES, @@ -67,8 +67,10 @@ ) from .const_sensor import (SENSOR_GROUPS ) from .helpers.common import (instr, isnumber, obscure_field, list_to_str, str_to_list, - is_statzone, zone_dname, isbetween, list_del, list_add, ) -from .helpers.messaging import (log_exception, log_debug_msg, _traceha, _trace, + is_statzone, zone_dname, isbetween, list_del, list_add, + sort_dict_by_values, ) +from .helpers.messaging import (log_exception, log_debug_msg, log_info_msg, + _traceha, _trace, post_event, post_monitor_msg, ) from .helpers import entity_io from . import sensor as ic3_sensor @@ -84,6 +86,10 @@ _CF_LOGGER = logging.getLogger("icloud3-cf") DATA_ENTRY_ALERT_CHAR = '⛔' DATA_ENTRY_ALERT = f" {DATA_ENTRY_ALERT_CHAR} " +DEVICE_NON_TRACKING_FIELDS = [CONF_FNAME, CONF_PICTURE, CONF_DEVICE_TYPE, CONF_INZONE_INTERVAL, + CONF_FIXED_INTERVAL, CONF_LOG_ZONES, + CONF_AWAY_TIME_ZONE_1_OFFSET, CONF_AWAY_TIME_ZONE_1_DEVICES, + CONF_AWAY_TIME_ZONE_2_OFFSET, CONF_AWAY_TIME_ZONE_2_DEVICES] #----------------------------------------------------------------------------------------- def dict_value_to_list(key_value_dict): @@ -170,7 +176,8 @@ def ensure_six_item_dict(dict_item): 'next_page_waze': 'NEXT PAGE ᐳ Waze History Database parameters', 'select_form': 'SELECT ᐳ Select the parameter update form', - 'login_icloud_account': 'LOG INTO ANOTHER ICLOUD ACCOUNT ᐳ Log into an iCloud Account with a different username/password. Currently logged into > ^msg', + 'login_icloud_account': 'LOG INTO AN ICLOUD ACCOUNT ᐳ Log into an iCloud Account that will provide FamShr location data', + 'logout_icloud_account': 'LOG OUT OF ICLOUD ACCOUNT ᐳ Log out of the iCloud Account used for FamShr location data (^msg)', 'verification_code': 'ENTER/REQUEST AN APPLE ID VERIFICATION CODE ᐳ Enter (or Request) the 6-digit Apple ID Verification Code', 'send_verification_code': 'SEND THE VERIFICATION CODE TO APPLE ᐳ Send the 6-digit Apple ID Verification Code back to Apple to approve access to iCloud account', @@ -230,11 +237,12 @@ def ensure_six_item_dict(dict_item): UNKNOWN_DEVICE_TEXT = ' > ⛔ UNKNOWN/NOT FOUND > NEEDS REVIEW' SERVICE_NOT_AVAILABLE = ' > This Data Source/Web Location Service is not available' SERVICE_NOT_STARTED_YET = ' > This Data Source/Web Location Svc has not finished starting. Exit and Retry.' -LOGGED_INTO_MSG_ACTION_LIST_IDX = 0 # Index number of the Action list item containing the username/password +LOGGED_INTO_MSG_ACTION_LIST_IDX = 1 # Index number of the Action list item containing the username/password # Action List Items for all screens ICLOUD_ACCOUNT_ACTIONS = [ ACTION_LIST_ITEMS_KEY_TEXT['login_icloud_account'], + ACTION_LIST_ITEMS_KEY_TEXT['logout_icloud_account'], ACTION_LIST_ITEMS_KEY_TEXT['verification_code']] REAUTH_CONFIG_FLOW_ACTIONS = [ ACTION_LIST_ITEMS_KEY_TEXT['send_verification_code'], @@ -291,7 +299,7 @@ def ensure_six_item_dict(dict_item): 'none': 'Use normal Apple iCloud Servers', 'cn': 'China - Use Apple iCloud Servers located in China' } -MOBAPP_DEVICE_SEARCH_TEXT = 'Scan for mobile app device_tracker ᐳ ' +MOBAPP_DEVICE_SEARCH_TEXT = '⚡ Scan for mobile app device_tracker ᐳ ' MOBAPP_DEVICE_NONE_ITEMS_KEY_TEXT = { 'None': 'None - The Mobile App is not installed on this device', } @@ -792,8 +800,8 @@ def initialize_options(self): self.called_from_step_id_1 = '' # Form/Fct to return to when verifying the icloud auth code self.called_from_step_id_1_2 = '' # Form/Fct to return to when verifying the icloud auth code - self.actions_list = [] # Actions list at the bottom of the screen - self.actions_list_default = '' # Default action_itemss to reassign on screen redisplay + self.actions_list = [] # Actions list at the bottom of the screen + self.actions_list_default = '' # Default action_items to reassign on screen redisplay self.config_flow_updated_parms = {''} # Stores the type of parameters that were updated, used to reinitialize parms self._description_placeholders = None self.code_to_schema_pass_value = None @@ -841,7 +849,8 @@ def initialize_options(self): self.devicename_by_famshr_fmf = {} self.mobapp_search_for_devicename = 'None' - self.inactive_devices_key_text = {} + self.inactive_devices_key_text = {} + self.log_level_devices_key_text = {} self._verification_code = None @@ -878,15 +887,7 @@ def initialize_options(self): # in case the username/password is changed and another account is accessed. These will not # intefer with ones already in use by iC3. The Global Gb variables will be set to the local # variables if they were changes and a iC3 Restart was selected when finishing the config setup - self.PyiCloud = None - if Gb.PyiCloud: self.PyiCloud = Gb.PyiCloud - self.username = Gb.username or Gb.conf_tracking[CONF_USERNAME] - self.password = Gb.password or Gb.conf_tracking[CONF_PASSWORD] - self.obscure_username = '' - self.obscure_password = '' - self.show_username_password = False - self.endpoint_suffix = Gb.icloud_server_endpoint_suffix or \ - Gb.conf_tracking[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] + self._initialize_self_PyiCloud_fields_from_Gb() async def async_step_init(self, user_input=None): if self.initialize_options_required_flag: @@ -898,6 +899,22 @@ async def async_step_init(self, user_input=None): return await self.async_step_menu_0() +#------------------------------------------------------------------------------------------- + def _traceui(self, user_input): + _traceha(f"{user_input=} {self.errors=} ") + +#------------------------------------------------------------------------------------------- + def _initialize_self_PyiCloud_fields_from_Gb(self): + self.PyiCloud = Gb.PyiCloud if Gb.PyiCloud else None + self.username = Gb.username or Gb.conf_tracking[CONF_USERNAME] + self.password = Gb.password or Gb.conf_tracking[CONF_PASSWORD] + self.obscure_username = obscure_field(self.username) or 'NoUsername' + self.obscure_password = obscure_field(self.password) or 'NoPassword' + self.show_username_password = False + self.data_source = Gb.conf_tracking[CONF_DATA_SOURCE] + self.endpoint_suffix = Gb.icloud_server_endpoint_suffix or \ + Gb.conf_tracking[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] + #------------------------------------------------------------------------------------------- def _set_initial_icloud3_device_tracker_area_id(self): ''' @@ -933,8 +950,10 @@ async def async_step_menu(self, user_input=None, errors=None): '''Main Menu displays different screens for parameter entry''' Gb.trace_prefix = 'CONFIG' Gb.config_flow_flag = True - if self.PyiCloud is None and Gb.PyiCloud is not None: + + if Gb.PyiCloud and self.PyiCloud is None: self.PyiCloud = Gb.PyiCloud + Gb.PyiCloudConfigFlow = self.PyiCloud self.step_id = f"menu_{self.menu_page_no}" self.called_from_step_id_1 = self.called_from_step_id_2 ='' @@ -1038,30 +1057,22 @@ async def async_step_restart_icloud3(self, user_input=None, errors=None): if action_item == 'cancel': return await self.async_step_menu_0() - elif action_item == 'restart_ic3_later': if 'restart' in self.config_flow_updated_parms: self.config_flow_updated_parms.remove('restart') Gb.config_flow_updated_parms = self.config_flow_updated_parms + # If the polling loop has been set up, set the restart flag to trigger a restart when # no devices are being updated. Otherwise, there were probably no devices to track # when first loaded and a direct restart must be done. elif action_item == 'restart_ic3_now': - if 'restart' in self.config_flow_updated_parms: - self.config_flow_updated_parms.remove('restart') - Gb.restart_icloud3_request_flag = True - if (self.PyiCloud is not None - and (self.username != Gb.username - or self.password != Gb.password - or self.endpoint_suffix != Gb.icloud_server_endpoint_suffix)): - Gb.PyiCloud = self.PyiCloud - Gb.username = self.username - Gb.password = self.password - Gb.icloud_server_endpoint_suffix = self.endpoint_suffix + Gb.config_flow_updated_parms = self.config_flow_updated_parms + #if 'restart' in self.config_flow_updated_parms: + # self.config_flow_updated_parms.remove('restart') + #Gb.restart_icloud3_request_flag = True elif action_item.startswith('restart_ha'): - #return await self.step_restart_ha() await Gb.hass.services.async_call("homeassistant", "restart") return self.async_abort(reason="ha_restarting") @@ -1280,9 +1291,6 @@ async def async_step_away_time_zone(self, user_input=None, errors=None): if self._any_errors(): self.errors['action_items'] = 'update_aborted' - # if self.away_time_zone_hours_key_text == {}: - - return self.async_show_form(step_id=self.step_id, data_schema=self.form_schema(self.step_id), errors=self.errors) @@ -1312,7 +1320,7 @@ def _build_away_time_zone_hours_list(self): time_str = f"{away_hh:02}{Gb.this_update_time[2:]}" if away_hh == ha_time: - time_str = " " #+= f" (Home Time Zone)" + time_str = f"Home Time Zone" elif hh < ha_time: time_str += f" (-{abs(hh-ha_time):} hours)" else: @@ -1322,18 +1330,23 @@ def _build_away_time_zone_hours_list(self): #------------------------------------------------------------------------------------------- def _build_away_time_zone_devices_list(self): - self.away_time_zone_devices_key_text = {'none': 'Home time zone is used for all devices'} - devices = {conf_device[CONF_IC3_DEVICENAME]: conf_device[CONF_FNAME] - for conf_device in Gb.conf_devices - if conf_device[CONF_TRACKING_MODE] != INACTIVE_DEVICE} + #self.away_time_zone_devices_key_text = {'none': 'Home time zone is used for all devices'} + self.away_time_zone_devices_key_text = {'none': 'None - All devices are at Home'} + self.away_time_zone_devices_key_text.update(self._devices_selection_list()) + self.away_time_zone_devices_key_text = ensure_six_item_dict(self.away_time_zone_devices_key_text) + +#------------------------------------------------------------------------------------------- + def _build_log_level_devices_list(self): - self.away_time_zone_devices_key_text.update(devices) + self.log_level_devices_key_text = {'all': 'All devices should be logged to the log file'} + self.log_level_devices_key_text.update(self._devices_selection_list()) + self.log_level_devices_key_text = ensure_six_item_dict(self.log_level_devices_key_text) - self.away_time_zone_devices_key_text = ensure_six_item_dict(self.away_time_zone_devices_key_text) - # dummy_key = '' - # for i in range(6 - len(self.away_time_zone_devices_key_text)): - # dummy_key += '.' - # self.away_time_zone_devices_key_text[dummy_key] = '.' +#------------------------------------------------------------------------------------------- + def _devices_selection_list(self): + return {conf_device[CONF_IC3_DEVICENAME]: conf_device[CONF_FNAME] + for conf_device in Gb.conf_devices + if conf_device[CONF_TRACKING_MODE] != INACTIVE_DEVICE} #------------------------------------------------------------------------------------------- async def async_step_confirm_action(self, user_input=None, action_items=None, @@ -1866,7 +1879,8 @@ def _update_configuration_file(self, user_input): if 'special_zones' in self.step_id: updated_parms.update(['zone_formats']) if 'away_time_zone' in self.step_id: - updated_parms.update(['restart']) + updated_parms.update(['devices']) + #updated_parms.update(['restart']) if 'waze' in self.step_id: updated_parms.update(['waze']) @@ -1908,6 +1922,12 @@ def _validate_format_settings(self, user_input): user_input = self._option_text_to_parm(user_input, CONF_TIME_FORMAT, TIME_FORMAT_ITEMS_KEY_TEXT) user_input = self._option_text_to_parm(user_input, CONF_LOG_LEVEL, LOG_LEVEL_ITEMS_KEY_TEXT) + if (user_input[CONF_LOG_LEVEL_DEVICES] == [] + or len(user_input[CONF_LOG_LEVEL_DEVICES]) >= len(Gb.Devices)): + user_input[CONF_LOG_LEVEL_DEVICES] = ['all'] + elif len(user_input[CONF_LOG_LEVEL_DEVICES]) > 1: + list_del(user_input[CONF_LOG_LEVEL_DEVICES], 'all') + if (Gb.display_zone_format != user_input[CONF_DISPLAY_ZONE_FORMAT]): self.config_flow_updated_parms.update(['zone_formats']) @@ -2096,7 +2116,8 @@ def _set_inactive_devices_header_msg(self): Return none, few, some, most, all based on the number of inactive devices ''' - if instr(Gb.conf_tracking[CONF_DATA_SOURCE], FAMSHR): + # if instr(Gb.conf_tracking[CONF_DATA_SOURCE], FAMSHR): + if instr(self.data_source, FAMSHR): if (Gb.conf_tracking[CONF_USERNAME] == '' or Gb.conf_tracking[CONF_PASSWORD] == ''): self.header_msg = 'icloud_acct_not_set_up' @@ -2157,53 +2178,60 @@ async def async_step_icloud_account(self, user_input=None, errors=None, called_f self.step_id = 'icloud_account' self.errors = errors or {} self.errors_user_input = {} + self.actions_list_default = '' action_item = '' self.called_from_step_id_2 = called_from_step_id or self.called_from_step_id_2 or 'menu_0' try: if user_input is None: + if self.username == '' or self.password == '': + self.actions_list_default = 'login_icloud_account' + + elif (self.username != '' and self.password != '' + and instr(self.data_source, FAMSHR) is False): + self.actions_list_default = 'login_icloud_account' + self.errors['base'] = 'icloud_acct_data_source_warning' + return self.async_show_form(step_id=self.step_id, data_schema=self.form_schema(self.step_id), errors=self.errors) - user_input[CONF_DATA_SOURCE] = (f"{list_to_str(user_input['data_source_icloud'], ',')}," - f"{list_to_str(user_input['data_source_mobapp'], ',')}") - user_input[CONF_USERNAME] = user_input[CONF_USERNAME].lower() 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}") + user_input[CONF_USERNAME] = user_input[CONF_USERNAME].lower() + user_input['endpoint_suffix'] = 'cn' if user_input['url_suffix_china'] is True else 'None' + if user_input[CONF_USERNAME] == '' or user_input[CONF_PASSWORD] == '': + user_input['data_source_icloud'] = [] + user_input = self._set_data_source(user_input) - if action_item == 'confirm_save': - action_item = 'save' + if Gb.log_debug_flag: + 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]) + log_debug_msg(f"{self.step_id} ({action_item}) > UserInput-{log_user_input}, Errors-{errors}") - elif action_item == 'confirm_return': + if action_item == 'cancel': + self._initialize_self_PyiCloud_fields_from_Gb() 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() + if (action_item == 'save' + and (self.username != user_input[CONF_USERNAME] + or self.password != user_input[CONF_PASSWORD])): + action_item = 'login_icloud_account' # Data Source is Mobile App only, iCloud was not selected - if user_input[CONF_DATA_SOURCE] == MOBAPP: + if user_input[CONF_USERNAME] == '' and user_input[CONF_PASSWORD] == '': + user_input['data_source_icloud'] = [] + user_input = self._set_data_source(user_input) + self._update_configuration_file(user_input) self.PyiCloud = None return await self.async_step_menu() if action_item == 'verification_code': - if self.PyiCloud or Gb.PyiCloud: + if self.PyiCloud: return await self.async_step_reauth(called_from_step_id='icloud_account') else: action_item = 'login_icloud_account' @@ -2220,40 +2248,45 @@ async def async_step_icloud_account(self, user_input=None, errors=None, called_f self.errors['data_source_mobapp'] = 'icloud_acct_no_data_source' if self.errors == {}: - # Set the data_source so pyicloud will get all the devices - Gb.conf_data_source_FAMSHR = instr(user_input[CONF_DATA_SOURCE], FAMSHR) - 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 + if action_item == 'login_icloud_account': + user_input['data_source_icloud'] = [FAMSHR] + user_input = self._set_data_source(user_input) - # Action Login or Save will login into the account if the username changed - if (action_item in ['login_icloud_account', 'save']): # if already logged in and no changes, do not login again if (self.PyiCloud - and self.PyiCloud.username == self.username - and self.PyiCloud.password == self.password): + and self.PyiCloud.username == user_input[CONF_USERNAME] + and self.PyiCloud.password == user_input[CONF_PASSWORD]): pass else: await self._log_into_icloud_account(user_input, called_from_step_id='icloud_account') - if action_item == 'save' and self.PyiCloud != Gb.PyiCloud: - Gb.PyiCloud = Gb.PyiCloudInit = self.PyiCloud - Gb.username = self.username - Gb.password = self.password - Gb.icloud_server_endpoint_suffix = self.endpoint_suffix + if (self.PyiCloud and self.PyiCloud.requires_2fa): + errors = {'base': 'verification_code_needed'} + return await self.async_step_reauth(user_input=None, + errors={'base': 'verification_code_needed'}, + called_from_step_id='icloud_account') + + if self.PyiCloud is None: + self.actions_list_default = 'login_icloud_account' + if self.errors.get('base', '') == 'icloud_acct_login_error_user_pw': + self.actions_list_default = 'login_icloud_account' + self.errors[CONF_USERNAME] = 'icloud_acct_username_password_error' + - await self._build_device_form_selection_lists() - self._prepare_device_selection_list() - if (self.PyiCloud and self.PyiCloud.requires_2fa): - errors = {'base': 'verification_code_needed'} - return await self.async_step_reauth(user_input=None, - errors={'base': 'verification_code_needed'}, - called_from_step_id='icloud_account') + elif action_item == 'logout_icloud_account': + user_input = self._initialize_pyicloud_username_password(user_input) + + elif (action_item == 'save' + and (self.errors == {} + or self.errors.get('base', '') == 'icloud_acct_logged_into') + or self.errors.get('base', '') == 'icloud_acct_not_logged_into'): + + await self._build_update_device_selection_lists() + self._prepare_device_selection_list() - # Save the login username/password - if (action_item == 'save' - and (self.errors == {} or self.errors.get('base', '') == 'icloud_acct_logged_into')): user_input[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] = self.endpoint_suffix + user_input[CONF_DATA_SOURCE] = self.data_source self._update_configuration_file(user_input) @@ -2269,7 +2302,32 @@ async def async_step_icloud_account(self, user_input=None, errors=None, called_f data_schema=self.form_schema(self.step_id), errors=self.errors) +#------------------------------------------------------------------------------------------- + def _initialize_pyicloud_username_password(self, user_input): + ''' + Logging out of the iCloud account - Reset all of the login variables + ''' + self.PyiCloud = None + self.username = '' + self.password = '' + self.endpoint_suffix = '' + + user_input[CONF_USERNAME] = '' + user_input[CONF_PASSWORD] = '' + user_input['data_source_icloud'] = [] + user_input = self._set_data_source(user_input) + return user_input + +#------------------------------------------------------------------------------------------- + def _set_data_source(self, user_input): + data_source = '' + if user_input['data_source_icloud']: data_source += f"{FAMSHR}, " + if user_input['data_source_mobapp']: data_source += f"{MOBAPP}, " + data_source = data_source[:-2] if data_source else ',' + user_input[CONF_DATA_SOURCE] = self.data_source = data_source + + return user_input #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -2314,11 +2372,7 @@ async def async_step_reauth(self, user_input=None, errors=None, called_from_step user_input = self._strip_spaces(user_input, [CONF_VERIFICATION_CODE]) log_debug_msg(f"{self.step_id} ({action_item}) > UserInput-{user_input}, Errors-{errors}") - if Gb.PyiCloud and self.username == Gb.PyiCloud.username and self.password == Gb.PyiCloud.password: - PyiCloud = Gb.PyiCloud - elif self.PyiCloud: - PyiCloud = self.PyiCloud - else: + if self.PyiCloud is None: self.errors = 'icloud_acct_not_logged_into' action_item = 'cancel' @@ -2333,7 +2387,8 @@ async def async_step_reauth(self, user_input=None, errors=None, called_from_step if action_item == 'request_verification_code': await Gb.hass.async_add_executor_job( pyicloud_ic3_interface.pyicloud_reset_session, - PyiCloud) + self.PyiCloud) + # PyiCloud) self.errors['base'] = 'verification_code_requested2' @@ -2341,7 +2396,7 @@ async def async_step_reauth(self, user_input=None, errors=None, called_from_step and CONF_VERIFICATION_CODE in user_input and user_input[CONF_VERIFICATION_CODE]): valid_code = await Gb.hass.async_add_executor_job( - Gb.PyiCloud.validate_2fa_code, + self.PyiCloud.validate_2fa_code, user_input[CONF_VERIFICATION_CODE]) # Do not restart iC3 right now if the username/password was changed on the @@ -2353,7 +2408,7 @@ async def async_step_reauth(self, user_input=None, errors=None, called_from_step Gb.EvLog.clear_evlog_greenbar_msg() Gb.icloud_force_update_flag = True - PyiCloud.new_2fa_code_already_requested_flag = False + self.PyiCloud.new_2fa_code_already_requested_flag = False self.errors['base'] = self.header_msg = 'verification_code_accepted' @@ -2393,11 +2448,6 @@ async def _log_into_icloud_account(self, user_input, called_from_step_id=None, r Exception: The self.PyiCloud.requres_2fa must be checked after a login to see if the account access needs to be verified. If so, the verification code entry form must be displayed. - await self._log_into_icloud_account(user_input, self.step_id) - - if (self.PyiCloud - and self.PyiCloud.requires_2fa): - return await self.async_step_icloud_verification_code() Returns: Gb.Pyicloud object @@ -2408,24 +2458,27 @@ async def _log_into_icloud_account(self, user_input, called_from_step_id=None, r used in the tracking configuration devices icloud_device parameter ''' called_from_step_id = called_from_step_id or 'icloud_account' + log_debug_msg( f"Logging into iCloud Acct > UserInput-{user_input}, " + f"Errors-{self.errors}, Step-{self.step_id}, CalledFrom-{called_from_step_id}") if CONF_USERNAME in user_input: self.username = user_input[CONF_USERNAME].lower() self.password = user_input[CONF_PASSWORD] self.endpoint_suffix = user_input['endpoint_suffix'] - verify_password = self.username != Gb.conf_tracking[CONF_USERNAME] + verify_password = (self.username != Gb.conf_tracking[CONF_USERNAME]) else: self.username = Gb.conf_tracking[CONF_USERNAME] self.password = Gb.conf_tracking[CONF_PASSWORD] self.endpoint_suffix = Gb.conf_tracking[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] verify_password = False - # If using same username/password as primary PyiCloud, use the primary + # If using same username/password as primary PyiCloud, we are already logged in if (Gb.PyiCloud - and Gb.PyiCloud.username == self.username - and Gb.PyiCloud.password == self.password - and Gb.PyiCloud.endpoint_suffix == self.endpoint_suffix): - self.PyiCloud = Gb.PyiCloud + and self.PyiCloud + and self.PyiCloud == Gb.PyiCloud + and self.username == Gb.PyiCloud.username + and self.password == Gb.PyiCloud.password + and self.endpoint_suffix == Gb.PyiCloud.endpoint_suffix): return # Already logged in with same username/password @@ -2437,14 +2490,14 @@ async def _log_into_icloud_account(self, user_input, called_from_step_id=None, r return if request_verification_code: - event_msg = f"{EVLOG_NOTICE}Requesting Apple ID Verification Code" + event_msg = f"{EVLOG_NOTICE}Configure Settings > Requesting Apple ID Verification Code" else: - event_msg =(f"{EVLOG_NOTICE}Logging into iCloud Account with Configure Settings, " + event_msg =(f"{EVLOG_NOTICE}Configure Settings > Logging into iCloud Account, " 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) + log_info_msg(event_msg) try: self.PyiCloud = await Gb.hass.async_add_executor_job( @@ -2456,13 +2509,14 @@ async def _log_into_icloud_account(self, user_input, called_from_step_id=None, r verify_password, request_verification_code) - 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] + + err = str(err) + _CF_LOGGER.error(f"Error logging into iCloud service: {err}") + if called_from_step_id == 'icloud_account': if err.endswith('302'): error_msg = 'icloud_acct_login_error_connection' @@ -2474,22 +2528,24 @@ async def _log_into_icloud_account(self, user_input, called_from_step_id=None, r error_msg = 'icloud_acct_login_error_other' self.errors = {'base': error_msg} - _CF_LOGGER.error(f"Error logging into iCloud service: {err}") - return self.async_show_form(step_id=called_from_step_id, data_schema=self.form_schema(called_from_step_id), errors=self.errors) except Exception as err: log_exception(err) + _CF_LOGGER.error(f"Error logging into iCloud service: {err}") + + self.errors = {'base': 'icloud_acct_login_error_other'} - await self._build_device_form_selection_lists() + return self.async_show_form(step_id=called_from_step_id, + data_schema=self.form_schema(called_from_step_id), + errors=self.errors) + + await self._build_update_device_selection_lists() - if Gb.PyiCloud is None: - Gb.PyiCloud = Gb.PyiCloudInit = self.PyiCloud - Gb.username = self.username - Gb.password = self.password - Gb.icloud_server_endpoint_suffix = self.endpoint_suffix + self.obscure_username = obscure_field(self.username) or 'NoUsername' + self.obscure_password = obscure_field(self.password) or 'NoPassword' if self.PyiCloud.requires_2fa or request_verification_code: return @@ -2508,18 +2564,18 @@ def _set_action_list_item_username_password(self): into the Action Item selection list item ''' - #if self.username or self.password: - obscure_username = obscure_field(self.username) or 'NoUsername' - obscure_password = obscure_field(self.password) or 'NoPassword' - username_password = f"{obscure_username}/{obscure_password}" - - if self.PyiCloud or Gb.PyiCloud: - logged_into_msg = f"{username_password}" - - else: - logged_into_msg = f"None ({username_password})" + if (self.username== '' or self.password == '' + or self.PyiCloud is None ): #or Gb.PyiCloud is None): if 'base' not in self.errors: self.errors = {'base': 'icloud_acct_not_logged_into'} + logged_into_msg = 'NOT LOGGED IN' + else: + if (self.username == Gb.conf_tracking[CONF_USERNAME] + and self.password == Gb.conf_tracking[CONF_PASSWORD]): + logged_into_msg = f"Logged into: {self.obscure_username}" + else: + logged_into_msg = (f"New iCloud Acct: {self.obscure_username} " + f"{RED_ALERT}SAVE CHANGES{RED_ALERT}") self.actions_list[LOGGED_INTO_MSG_ACTION_LIST_IDX] = \ self.actions_list[LOGGED_INTO_MSG_ACTION_LIST_IDX].replace('^msg', logged_into_msg) @@ -2547,10 +2603,10 @@ async def async_step_device_list(self, user_input=None, errors=None): self.sensor_entity_attrs_changed = {} return await self.async_step_menu() - if self.PyiCloud is None and Gb.PyiCloud is not None: + if Gb.PyiCloud and self.PyiCloud is None: self.PyiCloud = Gb.PyiCloud - if instr(Gb.conf_tracking[CONF_DATA_SOURCE], FAMSHR): + if instr(self.data_source, FAMSHR): if (Gb.conf_tracking[CONF_USERNAME] == '' or Gb.conf_tracking[CONF_PASSWORD] == ''): self.header_msg = 'icloud_acct_not_set_up' @@ -2572,12 +2628,12 @@ async def async_step_device_list(self, user_input=None, errors=None): device_cnt = len(Gb.conf_devices) if user_input is None: - await self._build_device_form_selection_lists() + await self._build_update_device_selection_lists() if user_input is not None: if (action_item in ['update_device', 'delete_device'] and CONF_DEVICES not in user_input): - await self._build_device_form_selection_lists() + await self._build_update_device_selection_lists() action_item = '' if action_item == 'return': @@ -2621,9 +2677,6 @@ async def async_step_device_list(self, user_input=None, errors=None): self._prepare_device_selection_list() self.sensor_entity_attrs_changed = {} - #pr1.4 - #if Gb.async_add_entities_device_tracker is None: - # self.errors = {'base': 'no_add_entities_device_tracker_fct'} self.step_id = 'device_list' @@ -2870,6 +2923,7 @@ async def async_step_update_device(self, user_input=None, errors=None): if change_flag: ui_devicename = user_input[CONF_IC3_DEVICENAME] + only_non_tracked_field_updated = self._is_only_non_tracked_field_updated(user_input) self.conf_device_selected.update(user_input) # Update the configuration file @@ -2910,7 +2964,10 @@ async def async_step_update_device(self, user_input=None, errors=None): self._update_changed_sensor_entities() self.header_msg = 'conf_updated' - self.config_flow_updated_parms.update(['tracking', 'restart']) + if only_non_tracked_field_updated: + self.config_flow_updated_parms.update(['devices']) + else: + self.config_flow_updated_parms.update(['tracking', 'restart']) return await self.async_step_device_list() @@ -2922,6 +2979,29 @@ async def async_step_update_device(self, user_input=None, errors=None): errors=self.errors, last_step=True) +#------------------------------------------------------------------------------------------- + def _is_only_non_tracked_field_updated(self, user_input): + ''' + Cycle through the fields in the working fields dictionary for the device and see if + only non-tracked fields were updated. + + Update the device's fields if only non-tracked fields were updated + Restart iCloud3 if a tracked field was updated + ''' + + try: + if Gb.conf_devices == []: + return False + + for pname, pvalue in user_input.items(): + if (Gb.conf_devices[self.conf_device_selected_idx][pname] != pvalue + and pname not in DEVICE_NON_TRACKING_FIELDS): + return False + except: + return False + + return True + #------------------------------------------------------------------------------------------- def _validate_devicename(self, user_input): ''' @@ -3058,7 +3138,7 @@ def _validate_update_device(self, user_input): track_from_zones.append(self.conf_device_selected[CONF_TRACK_FROM_BASE_ZONE]) user_input[CONF_TRACK_FROM_ZONES] = track_from_zones - if isbetween(user_input[CONF_FIXED_INTERVAL], 0, 3): + if isbetween(user_input[CONF_FIXED_INTERVAL], 1, 2): user_input[CONF_FIXED_INTERVAL] = 3 self.errors[CONF_FIXED_INTERVAL] = 'fixed_interval_invalid_range' @@ -3163,7 +3243,7 @@ def _update_changed_sensor_entities(self): # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - async def _build_device_form_selection_lists(self): + async def _build_update_device_selection_lists(self): """ Setup the option lists used to select device parameters """ self._build_picture_filename_list() @@ -3176,18 +3256,30 @@ async def _build_device_form_selection_lists(self): #---------------------------------------------------------------------- async def _build_famshr_devices_list(self): - """ Cycle through famshr data and get devices that can be tracked for the - icloud device selection list - """ + ''' + Create the FamShr object if it does not exist. This will create the famshr_info_by_famshr_fname + that contains the fname and device info dictionary. Then sort this by the lower case fname values + so the uppercase items (Watch) are not listed before the lower case ones (iPhone). + + This creates the list of devices used on the update devices screen + ''' self.famshr_list_text_by_fname_base = NONE_DICT_KEY_TEXT.copy() - if self.PyiCloud is None or self.PyiCloud.FamilySharing is None: + if self.PyiCloud is None: return + if self.PyiCloud.FamilySharing is None: + config_flow_login = True + _FamShr = await Gb.hass.async_add_executor_job( + pyicloud_ic3_interface.create_FamilySharing_secondary, + self.PyiCloud, + config_flow_login) + if _FamShr := self.PyiCloud.FamilySharing: self._check_finish_v2v3conversion_for_famshr_fname() - self.famshr_list_text_by_fname_base.update(_FamShr.device_info_by_famshr_fname) + sorted_famshr_info_by_famshr_fname = sort_dict_by_values(_FamShr.device_info_by_famshr_fname) + self.famshr_list_text_by_fname_base.update(sorted_famshr_info_by_famshr_fname) self.famshr_list_text_by_fname = self.famshr_list_text_by_fname_base.copy() #---------------------------------------------------------------------- @@ -3279,13 +3371,16 @@ def _build_devicename_by_famshr_fmf(self, current_devicename=None): self.famshr_list_text_by_fname = self.famshr_list_text_by_fname_base.copy() for famshr_devicename, famshr_text in self.famshr_list_text_by_fname_base.items(): devicename_msg = '' + devicename_msg_prefix = '' try: if current_devicename != self.devicename_by_famshr_fmf[famshr_devicename]: + devicename_msg_prefix = f"{YELLOW_ALERT} " devicename_msg = ( f"{RARROW}ASSIGNED TO-" f"{self.devicename_by_famshr_fmf[famshr_devicename]}") except: pass - self.famshr_list_text_by_fname[famshr_devicename] = f"{famshr_text}{devicename_msg}" + self.famshr_list_text_by_fname[famshr_devicename] = \ + f"{devicename_msg_prefix}{famshr_text}{devicename_msg}" self.fmf_list_text_by_email = self.fmf_list_text_by_email_base.copy() for fmf_email, fmf_text in self.fmf_list_text_by_email_base.items(): @@ -3315,24 +3410,26 @@ def _build_mobapp_entity_list(self): entity_io.get_entity_registry_data(platform='mobile_app', domain='device_tracker') self.mobapp_list_text_by_entity_id = MOBAPP_DEVICE_NONE_ITEMS_KEY_TEXT.copy() - # Add `Devices` options - self.mobapp_list_text_by_entity_id.update( - {entity_io._base_entity_id(dev_trkr_entity): ( + # Get `Devices` items + mobapp_devices = {f"{entity_io._base_entity_id(dev_trkr_entity)}": ( f"{self._mobapp_fname(entity_attrs)} (" f"{DEVICE_TRACKER_DOT}{entity_io._base_entity_id(dev_trkr_entity)} " f"({entity_attrs[CONF_RAW_MODEL]})") - for dev_trkr_entity, entity_attrs in mobapp_entity_data.items()}) + for dev_trkr_entity, entity_attrs in mobapp_entity_data.items()} - # Add `Search` options + # Get `Search` items try: - self.mobapp_list_text_by_entity_id.update( - {f"Search: {slugify(self._mobapp_fname(entity_attrs))}": + search_mobapp_devices = \ + {f"Search: {slugify(self._mobapp_fname(entity_attrs))}": ( f"{MOBAPP_DEVICE_SEARCH_TEXT}{self._mobapp_fname(entity_attrs)} " - f"({slugify(self._mobapp_fname(entity_attrs))})" - for dev_trkr_entity, entity_attrs in mobapp_entity_data.items()}) + f"({slugify(self._mobapp_fname(entity_attrs))})") + for dev_trkr_entity, entity_attrs in mobapp_entity_data.items()} except: pass + self.mobapp_list_text_by_entity_id.update(sort_dict_by_values(mobapp_devices)) + self.mobapp_list_text_by_entity_id.update(sort_dict_by_values(search_mobapp_devices)) + return #------------------------------------------------------------------------------------------- @@ -3614,7 +3711,7 @@ def remove_track_fm_zone_sensor_entity(self, devicename, remove_tfz_zones_list): if remove_tfz_zones_list == []: return - device_tfz_sensors = Gb.Sensors_by_devicename_from_zone.get(devicename) + device_tfz_sensors = Gb.Sensors_by_devicename_from_zone.get(devicename).copy() if device_tfz_sensors is None: return @@ -4298,9 +4395,13 @@ def form_schema(self, step_id, actions_list=None, actions_list_default=None): data_source_icloud_list = [] data_source_mobapp_list = [] - if instr(Gb.conf_tracking[CONF_DATA_SOURCE], FAMSHR): data_source_icloud_list.append(FAMSHR) - if instr(Gb.conf_tracking[CONF_DATA_SOURCE], FMF): data_source_icloud_list.append(FMF) - if instr(Gb.conf_tracking[CONF_DATA_SOURCE], MOBAPP): data_source_mobapp_list.append(MOBAPP) + if instr(self.data_source, FAMSHR): data_source_icloud_list.append(FAMSHR) + # if instr(self.data_source, FMF): data_source_icloud_list.append(FMF) + if instr(self.data_source, MOBAPP): data_source_mobapp_list.append(MOBAPP) + + default_action = self.actions_list_default if self.actions_list_default else 'save' + self.actions_list_default = '' + if start_ic3.check_mobile_app_integration() is False: self.errors['data_source_mobapp'] = 'mobile_app_error' @@ -4324,7 +4425,7 @@ def form_schema(self, step_id, actions_list=None, actions_list_default=None): cv.multi_select(DATA_SOURCE_MOBAPP_ITEMS_KEY_TEXT), vol.Required('action_items', - default=self.action_default_text('save')): + default=self.action_default_text(default_action)): selector.SelectSelector(selector.SelectSelectorConfig( options=self.actions_list, mode='list')), }) @@ -4579,7 +4680,12 @@ def form_schema(self, step_id, actions_list=None, actions_list_default=None): #------------------------------------------------------------------------ elif step_id == 'format_settings': self._set_example_zone_name() + self._build_log_level_devices_list() + return vol.Schema({ + vol.Required(CONF_LOG_LEVEL_DEVICES, + default=Gb.conf_general[CONF_LOG_LEVEL_DEVICES]): + cv.multi_select(self.log_level_devices_key_text), vol.Required(CONF_LOG_LEVEL, default=self._option_parm_to_text(CONF_LOG_LEVEL, LOG_LEVEL_ITEMS_KEY_TEXT)): selector.SelectSelector(selector.SelectSelectorConfig( diff --git a/custom_components/icloud3/const.py b/custom_components/icloud3/const.py index c86006a..9f7d359 100644 --- a/custom_components/icloud3/const.py +++ b/custom_components/icloud3/const.py @@ -4,7 +4,7 @@ # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -VERSION = '3.0.2' +VERSION = '3.0.4' #----------------------------------------- DOMAIN = 'icloud3' ICLOUD3 = 'iCloud3' @@ -83,7 +83,6 @@ UTC_TIME = True LOCAL_TIME = False NUMERIC = True -NEW_LINE = '\n' WAZE = 'waze' CALC = 'calc' DIST = 'dist' @@ -233,7 +232,7 @@ EVLOG_NOTICE = '^5^' -EVLOG_TRACE = '^6^' +EVLOG_TRACE = '^3^' EVLOG_DEBUG = '^6^' EVLOG_MONITOR = '^6^' # SETTINGS_INTEGRATIONS_MSG, INTEGRATIONS_IC3_CONFIG_MSG, @@ -252,8 +251,8 @@ lite_circled_letters = "Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ" dark_circled_letters = "🅐 🅑 🅒 🅓 🅔 🅕 🅖 🅗 🅘 🅙 🅚 🅛 🅜 🅝 🅞 🅟 🅠 🅡 🅢 🅣 🅤 🅥 🅦 🅧 🅨 🅩 ✪" Symbols = ±▪•●▬⮾ ⊗ ⊘✓×ø¦ ▶◀ ►◄▲▼ ∙▪ »« oPhone=►▶→⟾➤➟➜➔➤🡆🡪🡺⟹🡆➔ᐅ◈🝱☒☢⛒⊘Ɵ⊗ⓧⓍ⛒🜔 -Important = ❗❌⚠️❓🛑⛔⚡⭐⭕ⓘ• ⍰ ‶″“”‘’‶″ - — –ᗒ ⁃ » ━▶ ━➤🡺 —> > > ❯↦ … 🡪ᗕ ᗒ ᐳ ─🡢 ──ᗒ 🡢 ─ᐅ ↣ ➙ →《》◆◈◉● +Important = ❗❌⚠️❓🛑⛔⚡⭐⭕ⓘ• ⍰ ‶″“”‘’‶″ 🕓 + — –ᗒ ⁃ » ━▶ ━➤🡺 —> > > ❯↦ … ⋮ 🡪ᗕ ᗒ ᐳ ─🡢 ──ᗒ 🡢 ─ᐅ ↣ ➙ →《》◆◈◉● ▐‖ ▹▻▷◁◅◃‖╠ᐅ🡆▶▐🡆▐▶‖➤▐➤➜➔❰❰❱❱ ⠤ ² ⣇⠈⠉⠋⠛⠟⠿⡿⣿ https://www.fileformat.info/info/unicode/block/braille_patterns/utf8test.htm ''' @@ -264,6 +263,8 @@ NBSP5 = '⠟' #'     ' NBSP6 = '⠿' #'      ' CRLF = '⣇' #'
' +NL = '\n' +CLOCK_FACE = '🕓' CHECK_MARK = '✓ ' RED_X = '❌' YELLOW_ALERT = '⚠️' @@ -277,16 +278,19 @@ CIRCLE_SLASH = '⊘' CIRCLE_X = 'ⓧ' DOT = '• ' -DOT2 = '•' +PDOT = '•' SQUARE_DOT = '▪' HDOT = '◦ ' -HDOT2 = '◦' +PHDOT = '◦' LT = '<' GT = '>' LTE = '≤' GTE = '≥' PLUS_MINUS = '±' +LDOT2 = f'•{NBSP2}' CRLF_DOT = f'{CRLF}{NBSP3}•{NBSP2}' +CRLF_LDOT = f'{CRLF}•{NBSP2}' +NL_DOT = f'{NL} • ' CRLF_XD = f'{CRLF}{NBSP2}×{NBSP2}' CRLF_X = f'{CRLF}{NBSP3}×{NBSP2}' CRLF_HDOT = f'{CRLF}{NBSP6}◦{NBSP2}' @@ -494,6 +498,8 @@ ID = 'id' LAST_CHANGED_SECS = 'last_changed_secs' LAST_CHANGED_TIME = 'last_changed_time' +LAST_UPDATED_SECS = 'last_updated_secs' +LAST_UPDATED_TIME = 'last_updated_time' STATE = 'state' # device tracker attributes @@ -525,8 +531,9 @@ BATTERY_SOURCE = 'battery_data_source' BATTERY_LEVEL = 'battery_level' BATTERY_UPDATE_TIME = 'battery_level_updated' -BATTERY_FAMSHR = 'battery_last_famshr_data' -BATTERY_MOBAPP = 'battery_last_mobapp_data' +BATTERY_FAMSHR = 'famshr_battery_info' +BATTERY_MOBAPP = 'mobapp_battery_info' +BATTERY_LATEST = 'battery_info' WAZE_METHOD = 'waze_method' MAX_DISTANCE = 'max_distance' WENT_3KM = 'went_3km' @@ -536,6 +543,7 @@ TRACKING = 'tracking' DEVICENAME_MOBAPP = 'mobapp_device' AUTHENTICATED = 'authenticated' +ALERT = 'alert' LAST_UPDATE_TIME = 'last_update_time' LAST_UPDATE_DATETIME = 'last_updated_date/time' @@ -572,8 +580,8 @@ '204': 'Unregistered', '0': 'Unknown', } - -DEVICE_STATUS_ONLINE = ['Online', 'Pending', 'Unknown', 'unknown', ''] +BATTERY_LEVEL_LOW = 20 +DEVICE_STATUS_ONLINE = ['Online', 'Pending', 'Unknown', 'unknown', ''] DEVICE_STATUS_OFFLINE = ['Offline'] DEVICE_STATUS_PENDING = ['Pending'] #<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> @@ -640,6 +648,7 @@ CONF_TFZ_TRACKING_MAX_DISTANCE = 'tfz_tracking_max_distance' CONF_PASSTHRU_ZONE_TIME = 'passthru_zone_time' CONF_LOG_LEVEL = 'log_level' +CONF_LOG_LEVEL_DEVICES = 'log_level_devices' CONF_DISPLAY_GPS_LAT_LONG = 'display_gps_lat_long' # Zone Parameters @@ -863,6 +872,7 @@ DEFAULT_GENERAL_CONF = { CONF_LOG_LEVEL: 'debug-auto-reset', + CONF_LOG_LEVEL_DEVICES: ['all'], # General Configuration Parameters CONF_UNIT_OF_MEASUREMENT: 'mi', @@ -935,16 +945,6 @@ CONF_TRAVEL_TIME_FACTOR: [.1, 1, .1, ''], CONF_PASSTHRU_ZONE_TIME: [0, 5], - # inZone Configuration Parameters - # CONF_INZONE_INTERVALS: { - # IPHONE: [5, 480], - # IPAD: [5, 480], - # WATCH: [5, 480], - # AIRPODS: [5, 480], - # NO_MOBAPP: [5, 480], - # OTHER: [5, 480], - # }, - # Waze Configuration Parameters CONF_WAZE_MIN_DISTANCE: [0, 1000, 5, 'km'], CONF_WAZE_MAX_DISTANCE: [0, 1000, 5, 'km'], diff --git a/custom_components/icloud3/const_sensor.py b/custom_components/icloud3/const_sensor.py index d3a0480..e6a5e6f 100644 --- a/custom_components/icloud3/const_sensor.py +++ b/custom_components/icloud3/const_sensor.py @@ -3,16 +3,15 @@ from .const import (DISTANCE_TO_DEVICES, - BLANK_SENSOR_FIELD, - NAME, - BADGE, + BLANK_SENSOR_FIELD, UNKNOWN, + NAME, BADGE, ALERT, TRIGGER, FROM_ZONE, ZONE_INFO, NEAR_DEVICE_USED, ZONE, ZONE_DNAME, ZONE_NAME, ZONE_FNAME, ZONE_DATETIME, LAST_ZONE, LAST_ZONE_DNAME, LAST_ZONE_NAME, LAST_ZONE_FNAME, LAST_ZONE_DATETIME, - INTERVAL, + INTERVAL, LOCATION_SOURCE, BATTERY_SOURCE, BATTERY, BATTERY_STATUS, BATTERY_UPDATE_TIME, - BATTERY_FAMSHR, BATTERY_MOBAPP, + BATTERY_FAMSHR, BATTERY_MOBAPP, BATTERY_LATEST, DISTANCE, ZONE_DISTANCE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, HOME_DISTANCE, MAX_DISTANCE,CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD, TRAVEL_TIME, TRAVEL_TIME_MIN, TRAVEL_TIME_HHMM, ARRIVAL_TIME, DIR_OF_TRAVEL, @@ -152,12 +151,12 @@ 'Badge', 'badge', 'mdi:shield-account', - [NAME, BATTERY, ZONE, ZONE_FNAME, - HOME_DISTANCE, - MAX_DISTANCE, TRAVEL_TIME, DIR_OF_TRAVEL, INTERVAL, - DISTANCE_TO_DEVICES, - ZONE_DATETIME, LAST_LOCATED_DATETIME, LAST_UPDATE_DATETIME, - DEVICE_STATUS, ], + [NAME, BATTERY_LATEST, ZONE, ZONE_FNAME, ALERT, LOCATION_SOURCE, + HOME_DISTANCE, MAX_DISTANCE, + TRAVEL_TIME, DIR_OF_TRAVEL, INTERVAL, + DISTANCE_TO_DEVICES, + ZONE_DATETIME, LAST_LOCATED_DATETIME, LAST_UPDATE_DATETIME, + DEVICE_STATUS, ], BLANK_SENSOR_FIELD], BATTERY: [ 'Battery', @@ -172,7 +171,7 @@ 'text, title', 'mdi:battery-outline', [BATTERY, BATTERY_SOURCE], - ''], + UNKNOWN], INFO: [ 'Info', 'info, ha_history_exclude', diff --git a/custom_components/icloud3/device.py b/custom_components/icloud3/device.py index e529ef2..dfe36bb 100644 --- a/custom_components/icloud3/device.py +++ b/custom_components/icloud3/device.py @@ -9,30 +9,29 @@ DISTANCE_TO_OTHER_DEVICES, DISTANCE_TO_OTHER_DEVICES_DATETIME, HOME, HOME_FNAME, NOT_HOME, NOT_SET, UNKNOWN, NOT_HOME_ZONES, DOT, RED_X, RARROW, INFO_SEPARATOR, YELLOW_ALERT, CRLF_DOT, CRLF_HDOT, + EVLOG_ALERT, BLANK_SENSOR_FIELD, TOWARDS, AWAY, AWAY_FROM, INZONE, STATIONARY, STATIONARY_FNAME, TOWARDS_HOME, AWAY_FROM_HOME, INZONE_HOME, INZONE_STATZONE, STATE_TO_ZONE_BASE, DEVICE_TRACKER_STATE, PAUSED, PAUSED_CAPS, RESUMING, DATETIME_ZERO, HHMMSS_ZERO,HHMM_ZERO, HIGH_INTEGER, TRACKING_NORMAL, TRACKING_PAUSED, TRACKING_RESUMED, - LAST_CHANGED_SECS, LAST_CHANGED_TIME, STATE, - EVLOG_ALERT, - BLANK_SENSOR_FIELD, - ICLOUD, FMF, FAMSHR, FMF_FNAME, FAMSHR_FNAME, - MOBAPP, MOBAPP_FNAME, + LAST_CHANGED_SECS, LAST_CHANGED_TIME, LAST_UPDATED_SECS, LAST_UPDATED_TIME, + STATE, + ICLOUD, FMF, FAMSHR, FMF_FNAME, FAMSHR_FNAME, MOBAPP, MOBAPP_FNAME, DATA_SOURCE_FNAME, TRACK_DEVICE, MONITOR_DEVICE, INACTIVE_DEVICE, TRACKING_MODE_FNAME, NAME, DEVICE_TYPE_FNAME, ICLOUD_HORIZONTAL_ACCURACY, ICLOUD_VERTICAL_ACCURACY, ICLOUD_BATTERY_STATUS, ICLOUD_BATTERY_LEVEL, ICLOUD_DEVICE_CLASS, ICLOUD_DEVICE_STATUS, ICLOUD_LOW_POWER_MODE, ID, - FRIENDLY_NAME, PICTURE, ICON, BADGE, + FRIENDLY_NAME, PICTURE, ICON, BADGE, ALERT, LATITUDE, LONGITUDE, LOCATION, LOCATION_SOURCE, TRIGGER, TRACKING, NEAR_DEVICE_USED, FROM_ZONE, INTERVAL, ZONE, ZONE_DNAME, ZONE_NAME, ZONE_FNAME, ZONE_DATETIME, LAST_ZONE, LAST_ZONE_DNAME, LAST_ZONE_NAME, LAST_ZONE_FNAME, LAST_ZONE_DATETIME, - BATTERY_SOURCE, BATTERY, BATTERY_LEVEL, BATTERY_STATUS, - BATTERY_FAMSHR, BATTERY_MOBAPP, + BATTERY_SOURCE, BATTERY, BATTERY_LEVEL, BATTERY_STATUS, BATTERY_LEVEL_LOW, + BATTERY_FAMSHR, BATTERY_MOBAPP, BATTERY_LATEST, BATTERY_STATUS_CODES, BATTERY_STATUS_FNAME, BATTERY_UPDATE_TIME, ZONE_DISTANCE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, HOME_DISTANCE, MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD, @@ -61,11 +60,12 @@ from .helpers.common import (instr, is_zone, isnot_zone, is_statzone, list_add, list_del, circle_letter, format_gps, zone_dname, ) -from .helpers.messaging import (post_event, post_error_msg, post_monitor_msg, post_evlog_greenbar_msg, clear_evlog_greenbar_msg, - log_exception, log_debug_msg, log_error_msg, +from .helpers.messaging import (post_event, post_error_msg, post_monitor_msg, + post_evlog_greenbar_msg, clear_evlog_greenbar_msg, + log_exception, log_debug_msg, log_error_msg, log_rawdata, post_startup_alert, post_internal_error, _trace, _traceha, ) -from .helpers.time_util import (time_now_secs, secs_to_time, time_now, datetime_now, +from .helpers.time_util import (time_now_secs, secs_to_time, s2t, time_now, datetime_now, secs_since, mins_since, secs_to, mins_to, secs_to_hhmm, format_timer, format_secs_since, time_to_12hrtime, secs_to_datetime, @@ -127,6 +127,7 @@ def initialize(self): self.data_source = None self.tracking_status = TRACKING_NORMAL self.tracking_mode = TRACK_DEVICE #normal, monitor, inactive + self.alert = '' self.last_data_update_secs = time_now_secs() self.last_evlog_msg_secs = time_now_secs() self.last_update_msg_secs = time_now_secs() @@ -188,13 +189,6 @@ def initialize(self): self.statzone_dist_moved_km = 0.0 self.statzone_setup_secs = 0 # Time the statzone was set up - self.away_time_zone_offset = Gb.away_time_zone_1_offset \ - if self.devicename in Gb.away_time_zone_1_devices \ - else Gb.away_time_zone_2_offset \ - if self.devicename in Gb.away_time_zone_2_devices \ - else 0 - - # iCloud3 configration fields self.conf_famshr_name = None self.conf_famshr_devicename = None @@ -246,6 +240,7 @@ def initialize(self): self.mobapp_request_loc_last_secs = 0 # Used for checking if alive and user request self.mobapp_request_loc_cnt = 0 self.mobapp_request_loc_sent_secs = 0 # Used for tracking in 5-sec loop when the data source is mobapp + self.mobapp_request_sensor_update_secs = 0 # MobApp state variables self.update_mobapp_data_monitor_msg= '' @@ -343,21 +338,21 @@ def initialize_on_initial_load(self): # return self.mobapp_data_battery_level = 0 - self.mobapp_data_battery_status = '' + self.mobapp_data_battery_status = UNKNOWN self.mobapp_data_battery_update_secs = 0 self.dev_data_battery_source = '' self.dev_data_battery_level = 0 - self.dev_data_battery_status = '' + self.dev_data_battery_status = UNKNOWN self.dev_data_battery_update_secs = 0 self.dev_data_battery_level_last = 0 - self.dev_data_battery_status_last = '' + self.dev_data_battery_status_last = UNKNOWN self.last_battery_msg = '0%, not_set' self.last_battery_msg_secs = 0 # rc9 Added battery_info sensors to display last battery data for famshr # & mobapp sensor.battery attributes - self.battery_info = {FAMSHR: '', MOBAPP: ''} + self.battery_info = {FAMSHR_FNAME: '', MOBAPP_FNAME: ''} #------------------------------------------------------------------------------ def initialize_sensors(self): @@ -376,9 +371,10 @@ def initialize_sensors(self): self.sensors[BADGE] = '' self.sensors[LOW_POWER_MODE] = '' self.sensors[INFO] = '' + self.sensors[ALERT] = '' self.sensors[BATTERY] = 0 - self.sensors[BATTERY_STATUS] = '' + self.sensors[BATTERY_STATUS] = UNKNOWN self.sensors[BATTERY_SOURCE] = '' self.sensors[BATTERY_UPDATE_TIME] = HHMMSS_ZERO self.sensors['mobapp_sensor-battery_level'] = '' @@ -387,6 +383,7 @@ def initialize_sensors(self): # rc9 Added battery_famshr & battery_mobapp to display battery_info data self.sensors[BATTERY_FAMSHR] = '' self.sensors[BATTERY_MOBAPP] = '' + self.sensors[BATTERY_LATEST] = '' # Location related items self.sensors[GPS] = (0, 0) @@ -454,12 +451,15 @@ def initialize_sensors(self): self.sensors[LAST_ZONE_NAME] = NOT_SET self.sensors[LAST_ZONE_DATETIME] = DATETIME_ZERO - # Initialize the Device sensors[xxx] value from the restore_state file if # the sensor is in the file. Otherwise, initialize to this value. This will preserve # non-tracking sensors across restarts self._restore_sensors_from_restore_state_file() + self.sensors[DISTANCE_TO_OTHER_DEVICES] = {} + self.sensors[DISTANCE_TO_OTHER_DEVICES_DATETIME] = HHMMSS_ZERO + self.sensors[ALERT] = '' + #------------------------------------------------------------------------------ def _link_device_entities_sensor_device_tracker(self): # The DeviceTracker & Sensors entities are created before the Device object @@ -493,11 +493,8 @@ def configure_device(self, conf_device): # Change Monitored to tracked if primary data source is MOBAPP since # a monitored device only monitors the iCloud data aand iOS Data may be available self.tracking_mode = conf_device.get(CONF_TRACKING_MODE, 'track') - self.fname = conf_device.get(CONF_FNAME, self.devicename.title()) - self.sensors[NAME] = self.fname_devicename self.sensors['dev_id'] = self.devicename - self.sensors['host_name'] = self.fname - self.evlog_fname_alert_char = '' # Character added to the fmame in the EvLog (❗❌⚠️) + self.evlog_fname_alert_char = '' # Character added to the fname in the EvLog (❗❌⚠️) # mobapp device_tracker/sensor entity ids self.mobapp = { @@ -508,28 +505,14 @@ def configure_device(self, conf_device): NOTIFY: '', } - self.sensor_badge_attrs[FRIENDLY_NAME] = self.fname - self.sensor_badge_attrs[ICON] = 'mdi:account-circle-outline' - self._initialize_data_source_fields(conf_device) - - self.device_type = conf_device.get(CONF_DEVICE_TYPE, 'iphone') - self.raw_model = conf_device.get(CONF_RAW_MODEL, self.device_type) # iPhone15,2 - self.model = conf_device.get(CONF_MODEL, self.device_type) # iPhone - self.model_display_name = conf_device.get(CONF_MODEL_DISPLAY_NAME, self.device_type) # iPhone 14 Pro - - picture = conf_device.get(CONF_PICTURE, 'None').replace('www/', '/local/') - if picture: - self.sensors[PICTURE] = picture if instr(picture, '/') else (f"/local/{picture}") - self.sensor_badge_attrs[PICTURE] = self.sensors[PICTURE] - - self.inzone_interval_secs = conf_device.get(CONF_INZONE_INTERVAL, 30) * 60 - self.fixed_interval_secs = conf_device.get(CONF_FIXED_INTERVAL, 0) * 60 - self.statzone_inzone_interval_secs = min(self.inzone_interval_secs, Gb.statzone_inzone_interval_secs) - + self.initialize_non_tracking_config_fields(conf_device) self._validate_zone_parameters() - self.log_zones = conf_device.get(CONF_LOG_ZONES, ['none']) - self.track_from_zones = conf_device.get(CONF_TRACK_FROM_ZONES, [HOME]).copy() + + self.raw_model = conf_device.get(CONF_RAW_MODEL, self.device_type) # iPhone15,2 + self.model = conf_device.get(CONF_MODEL, self.device_type) # iPhone + self.model_display_name = conf_device.get(CONF_MODEL_DISPLAY_NAME, self.device_type) # iPhone 14 Pro + self.track_from_zones = conf_device.get(CONF_TRACK_FROM_ZONES, [HOME]).copy() self.track_from_base_zone = conf_device.get(CONF_TRACK_FROM_BASE_ZONE, HOME) try: @@ -564,6 +547,42 @@ def _extract_devicename(self, device_field): return device_name +#------------------------------------------------------------------------------ + def initialize_non_tracking_config_fields(self, conf_device): + ''' + Set the device's fields to the configuration for fields not related to device + selection, data source, track_from zones or tracking related fields that require + an iCloud3 restart + + DEVICE_NON_TRACKING_FIELDS = [CONF_FNAME, CONF_PICTURE, CONF_DEVICE_TYPE, CONF_INZONE_INTERVAL, + CONF_FIXED_INTERVAL, CONF_LOG_ZONES, + CONF_AWAY_TIME_ZONE_1_OFFSET, CONF_AWAY_TIME_ZONE_1_DEVICES, + CONF_AWAY_TIME_ZONE_2_OFFSET, CONF_AWAY_TIME_ZONE_2_DEVICES] + ''' + + self.fname = conf_device.get(CONF_FNAME, self.devicename.title()) + self.sensors[NAME] = self.fname_devicename + self.sensors['host_name'] = self.fname + + self.sensor_badge_attrs[FRIENDLY_NAME] = self.fname + self.sensor_badge_attrs[ICON] = 'mdi:account-circle-outline' + + picture = conf_device.get(CONF_PICTURE, 'None').replace('www/', '/local/') + if picture: + self.sensors[PICTURE] = picture if instr(picture, '/') else (f"/local/{picture}") + self.sensor_badge_attrs[PICTURE] = self.sensors[PICTURE] + + self.statzone_inzone_interval_secs = min(self.inzone_interval_secs, Gb.statzone_inzone_interval_secs) + self.inzone_interval_secs = conf_device.get(CONF_INZONE_INTERVAL, 30) * 60 + self.fixed_interval_secs = conf_device.get(CONF_FIXED_INTERVAL, 0) * 60 + self.device_type = conf_device.get(CONF_DEVICE_TYPE, 'iphone') + self.log_zones = conf_device.get(CONF_LOG_ZONES, ['none']) + + self.away_time_zone_offset = \ + Gb.away_time_zone_1_offset if self.devicename in Gb.away_time_zone_1_devices else \ + Gb.away_time_zone_2_offset if self.devicename in Gb.away_time_zone_2_devices else \ + 0 + #-------------------------------------------------------------------- def _initialize_data_source_fields(self, conf_device): @@ -727,8 +746,7 @@ def remove_zone_from_settings(self, zone): ''' try: if (zone == HOME - or Gb.start_icloud3_inprocess_flag - or Gb.restart_icloud3_request_flag): + or Gb.start_icloud3_inprocess_flag): return conf_file_updated_flag = False @@ -947,6 +965,7 @@ def format_battery_level(self): @property def format_battery_status(self): + return self.dev_data_battery_status return f"{BATTERY_STATUS_FNAME.get(self.dev_data_battery_status, self.dev_data_battery_status.title())}" @property @@ -1063,9 +1082,9 @@ def is_approaching_tracked_zone(self): if (secs_to(self.next_update_secs) <= 15 and secs_since(self.loc_data_secs > 15) and self.FromZone_TrackFrom.is_going_towards - and self.FromZone_TrackFrom.zone_dist < 1 and self.went_3km): return True + # and self.FromZone_TrackFrom.zone_dist < 1 return False @property @@ -1305,12 +1324,7 @@ def is_tracking_paused(self): True Device is paused False Device not pause ''' - try: - return (self.tracking_status == TRACKING_PAUSED) - - except Exception as err: - log_exception(err) - return False + return (self.tracking_status == TRACKING_PAUSED) #-------------------------------------------------------------------- def resume_tracking(self, interval_secs=0): @@ -1424,16 +1438,20 @@ def set_passthru_zone_delay(self, data_source, zone_entered=None, zone_entered_s False - Zone was reset and should proceed with an update ''' # Passthru zone is not used or already set up + passthru_not_used_reason = '' if (zone_entered == self.passthru_zone or zone_entered == self.loc_data_zone): return True # Entering a zone not subject to a delay - if (zone_entered in self.FromZones_by_zone - or is_statzone(zone_entered) - or zone_entered is None - or (data_source == ICLOUD and self.is_location_old_or_gps_poor)): - return False + if zone_entered in self.FromZones_by_zone: + passthru_not_used_reason = 'TrackedFrom Zone' + elif is_statzone(zone_entered): + passthru_not_used_reason = 'Stat Zone' + elif zone_entered is None: + passthru_not_used_reason = 'Unknown Zone' + elif (data_source == ICLOUD and self.is_location_old_or_gps_poor): + passthru_not_used_reason = 'Old Location' # Not set and next update not reached, set it below elif (self.is_passthru_timer_set is False @@ -1443,14 +1461,17 @@ def set_passthru_zone_delay(self, data_source, zone_entered=None, zone_entered_s # Time for an update, reset it elif self.is_next_update_time_reached: self.reset_passthru_zone_delay() - - return False + passthru_not_used_reason = 'Next Update Time Reached' # Passthru expire is set, if before enter zone time or this update time, reset it elif (self.is_passthru_timer_set and (zone_entered_secs > self.passthru_zone_timer or Gb.this_update_secs >= self.passthru_zone_timer)): self.reset_passthru_zone_delay() + passthru_not_used_reason = 'Timer Expired' + + if passthru_not_used_reason: + post_event(self.devicename, f"Zone Enter Not Delayed > {passthru_not_used_reason}") return False # Activate Passthru zone @@ -1672,10 +1693,9 @@ def badge_sensor_value(self): #-------------------------------------------------------------------- def _update_battery_sensors(self, update_sensors_list): - if BATTERY not in update_sensors_list: # or BATTERY not in self.sensors: + if BATTERY not in update_sensors_list: return update_sensors_list - # rc9 Added check to display battery info when starting ic3 if (self.dev_data_battery_level < 1 or (self.dev_data_battery_level == self.sensors[BATTERY] and self.format_battery_status == self.sensors[BATTERY_STATUS]) @@ -1685,14 +1705,28 @@ def _update_battery_sensors(self, update_sensors_list): update_sensors_list.pop(BATTERY_SOURCE, None) return update_sensors_list + self._set_battery_sensor_values() + + # Battery info in in the Badge sensor. Make sure it is updated too + # if BADGE not in update_sensors_list: + # list_add(update_sensors_list, BADGE) + + return update_sensors_list + + def _set_battery_sensor_values(self): self.sensors[BATTERY] = self.dev_data_battery_level self.sensors[BATTERY_STATUS] = self.format_battery_status self.sensors[BATTERY_SOURCE] = self.dev_data_battery_source self.sensors[BATTERY_UPDATE_TIME] = self.format_battery_time - self.sensors[BATTERY_FAMSHR] = self.battery_info[FAMSHR] - self.sensors[BATTERY_MOBAPP] = self.battery_info[MOBAPP] + self.sensors[BATTERY_FAMSHR] = self.battery_info[FAMSHR_FNAME] + self.sensors[BATTERY_MOBAPP] = self.battery_info[MOBAPP_FNAME] + if self.dev_data_battery_source == FAMSHR_FNAME: + self.sensors[BATTERY_LATEST] = f"(FamShr) {self.sensors[BATTERY_FAMSHR]}" + self.sensors[BATTERY_FAMSHR] = f"(Latest) {self.sensors[BATTERY_FAMSHR]}" + elif self.dev_data_battery_source == MOBAPP_FNAME: + self.sensors[BATTERY_LATEST] = f"(MobApp) {self.sensors[BATTERY_MOBAPP]}" + self.sensors[BATTERY_MOBAPP] = f"(Latest) {self.sensors[BATTERY_MOBAPP]}" - return update_sensors_list #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -1813,8 +1847,6 @@ def update_distance_to_other_devices(self): update_at_time = secs_to_hhmm(self.loc_data_secs) self.dist_to_other_devices_secs = self.loc_data_secs - # for _devicename, _Device in Gb.Devices_by_devicename_tracked.items(): - for _devicename, _Device in Gb.Devices_by_devicename.items(): if _Device is self: continue @@ -1867,28 +1899,47 @@ def update_battery_data_from_mobapp(self): try: battery_level_attrs = entity_io.get_attributes(self.mobapp[BATTERY_LEVEL]) - if STATE not in battery_level_attrs: - return False + if STATE not in battery_level_attrs: return False - battery_level = int(battery_level_attrs[STATE]) - battery_status = 'charging' \ - if battery_level < 100 and instr(battery_level_attrs['icon'], 'charging') \ - else 'not charging' - battery_update_secs = battery_level_attrs[LAST_CHANGED_SECS] + battery_update_secs = \ + max(battery_level_attrs[LAST_UPDATED_SECS], battery_level_attrs[LAST_CHANGED_SECS]) except Exception as err: - #log_exception(err) + log_exception(err) return False - self._update_battery_data_fields( battery_level, battery_status, - battery_update_secs, MOBAPP_FNAME) + battery_level = int(battery_level_attrs[STATE]) + if (Gb.this_update_time.endswith('00:00') + or battery_update_secs != self.mobapp_data_battery_update_secs): + log_rawdata(f"MobApp Battery Level - <{self.devicename}>", battery_level_attrs) + + if battery_level > 99: + battery_status = 'Charged' + elif instr(battery_level_attrs['icon'], 'charging'): + battery_status = 'Charging' + elif battery_level > 0 and battery_level < BATTERY_LEVEL_LOW: + battery_status = 'Low' + else: + battery_status = 'NotCharging' + + if (battery_level == self.mobapp_data_battery_level + and battery_status == self.mobapp_data_battery_status): + return False + + self.mobapp_data_battery_update_secs = battery_update_secs + self.mobapp_data_battery_level = battery_level + self.mobapp_data_battery_status = battery_status + + self._update_battery_data_and_sensors( + MOBAPP_FNAME, battery_update_secs, battery_level, battery_status) - self.write_ha_sensors_state([BATTERY, BATTERY_STATUS]) + # self.write_ha_sensors_state([BATTERY, BATTERY_STATUS]) return True #------------------------------------------------------------------- - def _update_battery_data_fields(self, battery_level, battery_status, battery_update_secs, data_source): + def _update_battery_data_and_sensors(self, data_source, battery_update_secs, + battery_level, battery_status): ''' Update the dev_data_battery and mobapp_battery fields with the battery data if this data is newer @@ -1897,23 +1948,15 @@ def _update_battery_data_fields(self, battery_level, battery_status, battery_upd if battery_level < 1 or battery_status == '': return - self.battery_info[data_source.lower()] = f"{battery_level}@{secs_to_time(battery_update_secs)}, {battery_status}" + self.battery_info[data_source] = f"{battery_level}@{secs_to_time(battery_update_secs)}, {battery_status}" - if battery_status != self.dev_data_battery_status: - pass - elif battery_update_secs <= self.dev_data_battery_update_secs: - return - - if battery_level == 100 and data_source == FAMSHR_FNAME and self.PyiCloud_RawData_famshr: - battery_status = self.PyiCloud_RawData_famshr.device_data[ICLOUD_BATTERY_STATUS] - - if (battery_update_secs > self.dev_data_battery_update_secs - or battery_status != self.dev_data_battery_status): + if battery_update_secs > self.dev_data_battery_update_secs: + self.dev_data_battery_update_secs = battery_update_secs self.dev_data_battery_source = data_source - self.dev_data_battery_level = self.mobapp_data_battery_level = battery_level - if battery_status: - self.dev_data_battery_status = self.mobapp_data_battery_status = battery_status - self.dev_data_battery_update_secs = self.mobapp_data_battery_update_secs = battery_update_secs + self.dev_data_battery_level = battery_level + self.dev_data_battery_status = battery_status + + self.write_ha_sensors_state([BATTERY, BATTERY_STATUS, BADGE]) #------------------------------------------------------------------- def display_battery_info_msg(self, force_display=False): @@ -1968,9 +2011,9 @@ def update_dev_loc_data_from_raw_data_MOBAPP(self, RawData=None): self.dev_data_device_status = "Online" self.dev_data_device_status_code = 200 - self._update_battery_data_fields( self.mobapp_data_battery_level, - self.mobapp_data_battery_status, - self.mobapp_data_battery_update_secs, MOBAPP_FNAME) + self._update_battery_data_and_sensors( + MOBAPP_FNAME, self.mobapp_data_battery_update_secs, + self.mobapp_data_battery_level, self.mobapp_data_battery_status) self.loc_data_latitude = self.mobapp_data_latitude self.loc_data_longitude = self.mobapp_data_longitude @@ -1985,7 +2028,8 @@ def update_dev_loc_data_from_raw_data_MOBAPP(self, RawData=None): if self.is_location_gps_good: self.old_loc_cnt = 0 self.calculate_distance_moved() self.update_distance_to_other_devices() - self.write_ha_sensor_state(LAST_LOCATED, self.loc_data_time) + #self.write_ha_sensor_state(LAST_LOCATED, self.loc_data_time) + self.write_ha_sensors_state([LAST_LOCATED, NEXT_UPDATE, LAST_UPDATE]) self.display_update_location_msg() #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> @@ -2022,8 +2066,6 @@ def update_dev_loc_data_from_raw_data_FAMSHR_FMF(self, RawData, requesting_devic self.dev_data_low_power_mode = RawData.device_data.get(ICLOUD_LOW_POWER_MODE, "") if RawData.device_data.get(ICLOUD_BATTERY_LEVEL): - # icloud_rawdata_battery_level = round(RawData.device_data.get(ICLOUD_BATTERY_LEVEL, 0) * 100) - # icloud_rawdata_battery_status = RawData.device_data.get(ICLOUD_BATTERY_STATUS, UNKNOWN) icloud_rawdata_battery_level = RawData.battery_level icloud_rawdata_battery_status = RawData.battery_status else: @@ -2031,9 +2073,9 @@ def update_dev_loc_data_from_raw_data_FAMSHR_FMF(self, RawData, requesting_devic icloud_rawdata_battery_status = UNKNOWN if RawData.is_data_source_FAMSHR: - self._update_battery_data_fields( icloud_rawdata_battery_level, - icloud_rawdata_battery_status, - location_secs, FAMSHR_FNAME) + self._update_battery_data_and_sensors( + FAMSHR_FNAME, location_secs, + icloud_rawdata_battery_level, icloud_rawdata_battery_status) self.dev_data_device_status_code = RawData.device_data.get(ICLOUD_DEVICE_STATUS, 0) self.dev_data_device_status = DEVICE_STATUS_CODES.get(self.dev_data_device_status_code, UNKNOWN) @@ -2053,18 +2095,20 @@ def update_dev_loc_data_from_raw_data_FAMSHR_FMF(self, RawData, requesting_devic if self.is_location_gps_good: self.old_loc_cnt = 0 self.calculate_distance_moved() self.update_distance_to_other_devices() - self.write_ha_sensor_state(LAST_LOCATED, self.loc_data_time) + #self.write_ha_sensor_state(LAST_LOCATED, self.loc_data_time) + self.write_ha_sensors_state([LAST_LOCATED, NEXT_UPDATE, LAST_UPDATE]) if requesting_device_flag or self.is_monitored: self.display_update_location_msg() #------------------------------------------------------------------- def display_update_location_msg(self): + return if self.loc_data_time_gps == self.last_loc_data_time_gps: return if self.isnotin_zone or self.loc_data_dist_moved_km > .015: - event_msg =(f"SinceLast > " + event_msg =(f"Selected > " f"{self.last_loc_data_time_gps}" f"{RARROW}{self.dev_data_source}-{self.loc_data_time_gps}") post_event(self.devicename,event_msg) @@ -2089,12 +2133,7 @@ def update_sensor_values_from_data_fields(self): # Initialize Batttery if not set up. Then Update in _update_battery_sensors if self.sensors[BATTERY] < 1 and self.dev_data_battery_level >= 1: - self.sensors[BATTERY] = self.dev_data_battery_level - self.sensors[BATTERY_STATUS] = self.format_battery_status - self.sensors[BATTERY_SOURCE] = self.dev_data_battery_source - self.sensors[BATTERY_UPDATE_TIME] = self.format_battery_time - self.sensors[BATTERY_FAMSHR] = self.battery_info[FAMSHR] - self.sensors[BATTERY_MOBAPP] = self.battery_info[MOBAPP] + self._set_battery_sensor_values() # Device related sensors self.sensors[DEVICE_STATUS] = self.device_status @@ -2123,6 +2162,7 @@ def update_sensor_values_from_data_fields(self): self.sensors[MOVED_TIME_TO] = self.loc_data_time_moved_to self.sensors[ZONE_DATETIME] = secs_to_datetime(self.zone_change_secs) + if self.FromZone_NextToUpdate is None: self.FromZone_NextToUpdate = self.FromZone_Home self.interval_secs = self.FromZone_NextToUpdate.interval_secs self.interval_str = self.FromZone_NextToUpdate.interval_str self.next_update_secs = self.FromZone_NextToUpdate.next_update_secs @@ -2131,6 +2171,7 @@ def update_sensor_values_from_data_fields(self): self.sensors[NEXT_UPDATE_TIME] = self.FromZone_NextToUpdate.sensors[NEXT_UPDATE_TIME] self.sensors[NEXT_UPDATE] = self.FromZone_NextToUpdate.sensors[NEXT_UPDATE] + if self.FromZone_TrackFrom is None: self.FromZone_TrackFrom = self.FromZone_Home self.sensors[FROM_ZONE] = self.FromZone_TrackFrom.from_zone self.sensors[LAST_UPDATE_DATETIME] = self.FromZone_TrackFrom.sensors[LAST_UPDATE_DATETIME] self.sensors[LAST_UPDATE_TIME] = self.FromZone_TrackFrom.sensors[LAST_UPDATE_TIME] @@ -2147,14 +2188,11 @@ def update_sensor_values_from_data_fields(self): self.sensors[WAZE_DISTANCE] = self.FromZone_TrackFrom.sensors[WAZE_DISTANCE] self.sensors[WAZE_METHOD] = self.FromZone_TrackFrom.sensors[WAZE_METHOD] self.sensors[CALC_DISTANCE] = self.FromZone_TrackFrom.sensors[CALC_DISTANCE] + self.sensors[HOME_DISTANCE] = self.FromZone_Home.sensors[ZONE_DISTANCE] self.FromZone_TrackFrom.dir_of_travel = dir_of_travel = \ self.FromZone_TrackFrom.sensors[DIR_OF_TRAVEL] - # if dir_of_travel == INZONE: - # self.sensors[DIR_OF_TRAVEL] = f"@{zone_dname(self.loc_data_zone)[:8]}" - # else: - # self.sensors[DIR_OF_TRAVEL] = dir_of_travel self.sensors[DIR_OF_TRAVEL] = dir_of_travel # Update the last zone info if the device was in a zone and now not in a zone or went immediatelly from @@ -2387,10 +2425,6 @@ def display_info_msg(self, info_msg=None, new_base_msg=False): # PassThru zone msg has priority over all other messages if self.is_passthru_zone_delay_active and instr(info_msg, 'PassThru') is False: return - # if new_base_msg is False: - # return - - #info_msg = info_msg if new_base_msg else f"《{info_msg}》{self.info_msg}" try: self.write_ha_sensor_state(INFO, info_msg) diff --git a/custom_components/icloud3/device_fm_zone.py b/custom_components/icloud3/device_fm_zone.py index 6127ec7..3c6749b 100644 --- a/custom_components/icloud3/device_fm_zone.py +++ b/custom_components/icloud3/device_fm_zone.py @@ -58,6 +58,7 @@ def __init__(self, Device, from_zone): except Exception as err: log_exception(err) +#-------------------------------------------------------------------- def initialize(self): try: self.interval_secs = 0 @@ -67,6 +68,7 @@ def initialize(self): self.last_distance_str = '' self.last_distance_km = 0 self.dir_of_travel = NOT_SET + self.dir_of_travel_awayfrom_override = False self.dir_of_travel_history = '' self.last_update_time = HHMMSS_ZERO self.last_update_secs = 0 @@ -92,6 +94,7 @@ def initialize(self): except: post_internal_error(traceback.format_exc) +#-------------------------------------------------------------------- def initialize_sensors(self): self.sensors_um = {} self.sensors = {} @@ -129,9 +132,11 @@ def initialize_sensors(self): for sensor, Sensor in from_this_zone_sensors.items(): Sensor.FromZone = self +#-------------------------------------------------------------------- def __repr__(self): return (f"") +#-------------------------------------------------------------------- @property def zone_distance_str(self): return ('' if self.zone_dist == 0 else (f"{km_to_um(self.zone_dist)}")) @@ -160,48 +165,36 @@ def is_going_awayfrom(self): def isnot_going_awayfrom(self): return self.dir_of_travel != AWAY_FROM - # @property - # def format_dir_of_travel_history(self): - # ''' Format the dir_of_travel_history into groups. ''' - # if self.dir_of_travel_history == '': - # return - - # self.dir_of_travel_history = self.dir_of_travel_history.replace('i', 'Z') - # dir_codes = list(self.dir_of_travel_history[-36:]) - # hist_disp = '' - # cnt = 0 - # for dir_code in dir_codes: - # hist_disp += dir_code - # cnt += 1 - # if cnt == 10: - # hist_disp += ',' - # cnt = 0 - # if hist_disp.endswith(','): hist_disp = hist_disp[:-1] - - # return hist_disp - +#-------------------------------------------------------------------- def update_dir_of_travel_history(self, dir_of_travel): ''' - Update and Format the dir_of_travel_history into groups of 5 - ZZZZA,AA+AA,AASSS,SSTTT,TTTHH + Update and Format the dir_of_travel_history. + Away-A and Towards-T directions are stored with the first-letter code (A, T) + Other directions (inZone (Home-H, Stationary-S, OtherZone-Z, FarAway-F & + Unknown-U) are stored with the number of occurances (H09, S04) ''' - dir_code = INZONE_CODES.get(dir_of_travel) - if dir_code is None: dir_code = dir_of_travel[0:1] - dir_code_repeat = f"{dir_code}{dir_code}+{dir_code}{dir_code}" - - if self.dir_of_travel_history.endswith(dir_code_repeat): - return - if self.dir_of_travel_history.endswith(dir_code*5): - self.dir_of_travel_history = \ - self.dir_of_travel_history[:len(self.dir_of_travel_history)-5] \ - + dir_code_repeat + dir_code = INZONE_CODES.get(dir_of_travel, dir_of_travel[0:1]) + if dir_code not in ['A', 'T']: + if self.dir_of_travel_history.endswith(dir_code): + self.dir_of_travel_history += '02' + elif self.dir_of_travel_history[-3:-2] == dir_code: + try: + cnt = int(self.dir_of_travel_history[-2:]) + 1 + if cnt > 99: cnt = 99 + except: + cnt = 1 + self.dir_of_travel_history = self.dir_of_travel_history[:-2] + f"{cnt:0>2}" + else: + self.dir_of_travel_history += dir_code return - if len(self.dir_of_travel_history) == 29: - self.dir_of_travel_history = self.dir_of_travel_history[-23:] - if len(self.dir_of_travel_history) in [5, 11, 17, 23]: - self.dir_of_travel_history += ',' + if self.dir_of_travel_awayfrom_override: + dir_code = dir_code.lower() + self.dir_of_travel_history += dir_code + if len(self.dir_of_travel_history) > 40: + self.dir_of_travel_history = self.dir_of_travel_history[-30:] + return diff --git a/custom_components/icloud3/device_tracker.py b/custom_components/icloud3/device_tracker.py index d09ce39..9a458c0 100644 --- a/custom_components/icloud3/device_tracker.py +++ b/custom_components/icloud3/device_tracker.py @@ -1,16 +1,14 @@ """Support for tracking for iCloud devices.""" from .global_variables import GlobalVariables as Gb -from .const import (DOMAIN, ICLOUD3, CONF_VERSION, +from .const import (DOMAIN, ICLOUD3, DISTANCE_TO_DEVICES, - NOT_SET, NOT_SET_FNAME, HOME, NOT_HOME, RARROW, - DEVICE_TYPE_ICONS, DEVICE_TYPE_FNAME, - BLANK_SENSOR_FIELD, STATIONARY_FNAME, DEVICE_TRACKER_STATE, - TRACK_DEVICE, INACTIVE_DEVICE, - NAME, FNAME, - PICTURE, - LATITUDE, LONGITUDE, GPS, - LOCATION_SOURCE, TRIGGER, + NOT_SET, HOME, + DEVICE_TYPE_ICONS, + BLANK_SENSOR_FIELD, DEVICE_TRACKER_STATE, + INACTIVE_DEVICE, + NAME, FNAME, PICTURE, ALERT, + LATITUDE, LONGITUDE, GPS, LOCATION_SOURCE, TRIGGER, ZONE, ZONE_DATETIME, LAST_ZONE, FROM_ZONE, ZONE_FNAME, BATTERY, BATTERY_LEVEL, MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, @@ -363,6 +361,8 @@ def _get_extra_attributes(self): extra_attrs[GPS] = f"({self.latitude}, {self.longitude})" extra_attrs[LOCATED] = self._get_sensor_value(LAST_LOCATED_DATETIME) + alert = self._get_sensor_value(ALERT) + extra_attrs[ALERT] = alert if alert != BLANK_SENSOR_FIELD else '' extra_attrs[f"{'-'*40}"] = f"{'-'*35}" extra_attrs['integration'] = ICLOUD3 extra_attrs[NAME] = self._get_sensor_value(NAME) @@ -373,8 +373,9 @@ def _get_extra_attributes(self): extra_attrs['away_time_zone_offset'] = self.extra_attrs_away_time_zone_offset extra_attrs[f"{'-'*41}"] = f"{'-'*35}" - extra_attrs['data_source'] = f"{self._get_sensor_value(LOCATION_SOURCE)}" + # extra_attrs['data_source'] = f"{self._get_sensor_value(LOCATION_SOURCE)}" extra_attrs[DEVICE_STATUS] = self._get_sensor_value(DEVICE_STATUS) + extra_attrs[LOCATION_SOURCE]= f"{self._get_sensor_value(LOCATION_SOURCE)}" extra_attrs[TRIGGER] = self._get_sensor_value(TRIGGER) extra_attrs[ZONE] = self._get_sensor_value(ZONE) extra_attrs[LAST_ZONE] = self._get_sensor_value(LAST_ZONE) 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..148598d1827ccdb2936e010959213d3fb29032d7 GIT binary patch literal 22553 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 A^8f$< literal 0 HcmV?d00001 diff --git a/custom_components/icloud3/global_variables.py b/custom_components/icloud3/global_variables.py index a482f31..fc10ed6 100644 --- a/custom_components/icloud3/global_variables.py +++ b/custom_components/icloud3/global_variables.py @@ -38,7 +38,7 @@ CONF_WAZE_HISTORY_TRACK_DIRECTION, CONF_STAT_ZONE_FNAME, CONF_STAT_ZONE_BASE_LATITUDE, CONF_STAT_ZONE_BASE_LONGITUDE, - CONF_STAT_ZONE_INZONE_INTERVAL, CONF_LOG_LEVEL, + CONF_STAT_ZONE_INZONE_INTERVAL, CONF_LOG_LEVEL, CONF_LOG_LEVEL_DEVICES, CONF_MOBAPP_REQUEST_LOC_MAX_CNT, CONF_DISTANCE_BETWEEN_DEVICES, CONF_PASSTHRU_ZONE_TIME, CONF_TRACK_FROM_BASE_ZONE_USED, CONF_TRACK_FROM_BASE_ZONE, CONF_TRACK_FROM_HOME_ZONE, @@ -180,6 +180,7 @@ class GlobalVariables(object): restart_ha_flag = False # HA needs to be restarted any_device_was_updated_reason = '' startup_alerts = [] + startup_alerts_str = '' startup_stage_status_controls = [] # A general list used by various modules for noting startup progress debug_log = {} # Log variable and dictionsry field/values to icloud3-0.log file @@ -273,6 +274,7 @@ class GlobalVariables(object): config_flow_updated_parms = {''} distance_method_waze_flag = True + icloud_force_update_flag = False max_interval_secs = DEFAULT_GENERAL_CONF[CONF_MAX_INTERVAL] * 60 offline_interval_secs = DEFAULT_GENERAL_CONF[CONF_OFFLINE_INTERVAL] * 60 exit_zone_interval_secs = DEFAULT_GENERAL_CONF[CONF_EXIT_ZONE_INTERVAL] * 60 @@ -287,6 +289,7 @@ class GlobalVariables(object): gps_accuracy_threshold = DEFAULT_GENERAL_CONF[CONF_GPS_ACCURACY_THRESHOLD] travel_time_factor = DEFAULT_GENERAL_CONF[CONF_TRAVEL_TIME_FACTOR] log_level = DEFAULT_GENERAL_CONF[CONF_LOG_LEVEL] + log_level_devices = DEFAULT_GENERAL_CONF[CONF_LOG_LEVEL_DEVICES] device_tracker_state_source = DEFAULT_GENERAL_CONF[CONF_DEVICE_TRACKER_STATE_SOURCE] display_gps_lat_long_flag = DEFAULT_GENERAL_CONF[CONF_DISPLAY_GPS_LAT_LONG] diff --git a/custom_components/icloud3/hacs-copy.json b/custom_components/icloud3/hacs-copy.json index 3518a07..8a6a9e3 100644 --- a/custom_components/icloud3/hacs-copy.json +++ b/custom_components/icloud3/hacs-copy.json @@ -1,5 +1,5 @@ { "name": "iCloud3 v3 iDevice Tracker", - "homeassistant": "2023.6.0b0", + "homeassistant": "2024.3.0", "render_readme": true } diff --git a/custom_components/icloud3/helpers/common.py b/custom_components/icloud3/helpers/common.py index ce65f32..44f8c1c 100644 --- a/custom_components/icloud3/helpers/common.py +++ b/custom_components/icloud3/helpers/common.py @@ -7,7 +7,7 @@ #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # -# DATA VERIFICATION FUNCTIONS +# DICTION & LIST UTILITY FUNCTIONS # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> def combine_lists(parm_lists): @@ -33,8 +33,8 @@ def list_to_str(list_value, separator=None): ''' if list_value == []: return '' separator_str = separator if separator else ', ' - if None in list_value: - list_value = [lv for lv in list_value if lv is not None] + if None in list_value or '' in list_value: + list_value = [lv for lv in list_value if lv is not None and lv != ''] list_str = separator_str.join(list_value) if list_value else 'None' if separator_str.startswith(CRLF): @@ -77,6 +77,28 @@ def delete_from_list(list_value, item): return list_value #-------------------------------------------------------------------- +def sort_dict_by_values(dict_value): + ''' + Return a dictionary sorted by the item values + ''' + if (type(dict_value) is not dict + or dict_value == {}): + return {} + + dict_value_lower = {k: v.lower() for k, v in dict_value.items()} + sorted_dict_value_lower = sorted(dict_value_lower.items(), key=lambda x:x[1]) + keys_sorted_dict_value_lower = dict(sorted_dict_value_lower).keys() + sorted_dict_value = {k: dict_value[k] for k in keys_sorted_dict_value_lower} + + return sorted_dict_value + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# DATA VERIFICATION FUNCTIONS +# +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + def instr(string, substring): ''' Fine a substring or a list of substrings strings in a string @@ -112,8 +134,12 @@ def isnumber(string): return False #-------------------------------------------------------------------- -def isbetween(number, greater_than, less_than_equal): - return (less_than_equal > number > greater_than) +def isbetween(number, min_value, max_value): + ''' + Return True if the the number is between the other two numbers + including the min_value and max_value number) + ''' + return (max_value+1 > number > min_value-1) #-------------------------------------------------------------------- def inlist(string, list_items): diff --git a/custom_components/icloud3/helpers/dist_util.py b/custom_components/icloud3/helpers/dist_util.py index 1ff0c80..1cbd27e 100644 --- a/custom_components/icloud3/helpers/dist_util.py +++ b/custom_components/icloud3/helpers/dist_util.py @@ -100,24 +100,30 @@ def format_dist_m(dist_m): #-------------------------------------- def format_dist_km(dist_km): + if dist_km < 0: + dist_km = abs(dist_km) + if dist_km >= 100: return f"{dist_km:.0f}km" if dist_km >= 10: return f"{dist_km:.1f}km" if dist_km >= 1: return f"{dist_km:.2f}km" + dist_m = dist_km * 1000 + if dist_m >= 1: return f"{dist_m:.0f}m" if round_to_zero(dist_km) == 0: return f"0.0km" - return f"{dist_km*1000:.1f}m" + return f"{dist_m:.1f}m" #-------------------------------------------------------------------- def format_dist_mi(dist_mi): + if dist_mi < 0: + dist_mi = abs(dist_mi) + if dist_mi >= 100: return f"{dist_mi:.0f}mi" if dist_mi >= 10: return f"{dist_mi:.1f}mi" if dist_mi >= 1: return f"{dist_mi:.1f}mi" if dist_mi >= .0947: return f"{dist_mi:.2f}mi" - if round_to_zero(dist_mi) == 0: return f"0.0mi" - dist_ft = dist_mi * 5280 - if dist_ft > 1: return f"{dist_ft:.1f}ft" - # if dist_ft > 1: return f"{int(dist_ft)}ft" + if dist_ft >= 1: return f"{dist_ft:.1f}ft" + if round_to_zero(dist_mi) == 0: return f"0.0mi" return f"{dist_ft:.2f}ft" #-------------------------------------------------------------------- diff --git a/custom_components/icloud3/helpers/entity_io.py b/custom_components/icloud3/helpers/entity_io.py index 3ead8b6..9fa83bc 100644 --- a/custom_components/icloud3/helpers/entity_io.py +++ b/custom_components/icloud3/helpers/entity_io.py @@ -4,8 +4,9 @@ HOME, ZONE, UTC_TIME, MOBAPP_TRIGGER_ABBREVIATIONS, TRACE_ICLOUD_ATTRS_BASE, TRACE_ATTRS_BASE, BATTERY_LEVEL, BATTERY_STATUS, BATTERY_STATUS_CODES, - LAST_CHANGED_SECS, LAST_CHANGED_TIME, STATE, - LOCATION, ATTRIBUTES, TRIGGER, RAW_MODEL) + LAST_CHANGED_SECS, LAST_CHANGED_TIME, + LAST_UPDATED_SECS, LAST_UPDATED_TIME, + STATE, LOCATION, ATTRIBUTES, TRIGGER, RAW_MODEL) from .common import (instr, ) from .messaging import (log_debug_msg, log_exception, log_debug_msg, log_error_msg, log_rawdata, _trace, _traceha, ) @@ -72,10 +73,16 @@ def get_attributes(entity_id): entity_attrs = entity_data.attributes.copy() last_changed_secs = int(entity_data.last_changed.timestamp()) + last_updated_secs = int(entity_data.last_updated.timestamp()) + last_reported_secs = int(entity_data.last_reported.timestamp()) - entity_attrs[STATE] = entity_state entity_attrs[LAST_CHANGED_SECS] = last_changed_secs entity_attrs[LAST_CHANGED_TIME] = secs_to_time(last_changed_secs) + entity_attrs[LAST_UPDATED_SECS] = last_updated_secs + entity_attrs[LAST_UPDATED_TIME] = secs_to_time(last_updated_secs) + entity_attrs['last_reported_secs'] = last_reported_secs + entity_attrs['last_reported_time'] = secs_to_time(last_reported_secs) + entity_attrs[STATE] = entity_state if BATTERY_STATUS in entity_attrs: battery_status = entity_attrs[BATTERY_STATUS].lower() @@ -109,9 +116,6 @@ def get_last_changed_time(entity_id): last_changed = States.last_changed last_updated = States.last_updated - timestamp_utc = str(last_changed).split(".")[0] - # time_secs_old = datetime_to_secs(timestamp_utc, UTC_TIME) - if lc := last_changed.timestamp(): time_secs = int(lc) elif lu := last_updated.timestamp(): @@ -379,7 +383,7 @@ def trace_device_attributes(Device, description, fct_name, attrs): log_msg = (f"{description} Attrs-{trace_attrs}{trace_attrs_in_attrs}") log_debug_msg(Device.devicename, log_msg) - log_rawdata(f"iCloud Rawdata - {Device.devicename}--{description}", attrs) + log_rawdata(f"FamShr iCloud Rawdata - <{Device.devicename}> {description}", attrs) except Exception as err: pass diff --git a/custom_components/icloud3/helpers/messaging.py b/custom_components/icloud3/helpers/messaging.py index 49b5f76..c6cc812 100644 --- a/custom_components/icloud3/helpers/messaging.py +++ b/custom_components/icloud3/helpers/messaging.py @@ -4,11 +4,12 @@ from ..const import (DOT, ICLOUD3_ERROR_MSG, EVLOG_DEBUG, EVLOG_ERROR, EVLOG_INIT_HDR, EVLOG_MONITOR, EVLOG_TIME_RECD, EVLOG_UPDATE_HDR, EVLOG_UPDATE_START, EVLOG_UPDATE_END, EVLOG_ALERT, EVLOG_WARNING, EVLOG_HIGHLIGHT, EVLOG_IC3_STARTING,EVLOG_IC3_STAGE_HDR, - IC3LOG_FILENAME, EVLOG_TIME_RECD, + IC3LOG_FILENAME, EVLOG_TIME_RECD, EVLOG_TRACE, CRLF, CRLF_DOT, NBSP, NBSP2, NBSP3, NBSP4, NBSP5, NBSP6, CRLF_INDENT, DASH_50, DASH_DOTTED_50, TAB_11, RED_ALERT, RED_STOP, RED_CIRCLE, YELLOW_ALERT, DATETIME_FORMAT, DATETIME_ZERO, NEXT_UPDATE_TIME, INTERVAL, + FAMSHR_FNAME, MOBAPP_FNAME, CONF_IC3_DEVICENAME, CONF_FNAME, CONF_LOG_LEVEL, CONF_PASSWORD, CONF_USERNAME, CONF_DEVICES, LATITUDE, LONGITUDE, LOCATION_SOURCE, TRACKING_METHOD, @@ -53,7 +54,8 @@ LAST_LOCATED_TIME, LAST_LOCATED_DATETIME, INFO, GPS_ACCURACY, GPS, POLL_COUNT, VERT_ACCURACY, ALTITUDE, ICLOUD_LOST_MODE_CAPABLE, 'ResponseCode', 'reason', - 'id', 'firstName', 'lastName', 'name', 'fullName', 'appleId', 'emails', 'phones', + 'id', 'firstName', 'lastName', 'name', 'fullName', CONF_IC3_DEVICENAME, + 'appleId', 'emails', 'phones', 'deviceStatus', 'batteryStatus', 'batteryLevel', 'membersInfo', 'deviceModel', 'rawDeviceModel', 'deviceDisplayName', 'modelDisplayName', 'deviceClass', 'isOld', 'isInaccurate', 'timeStamp', 'altitude', 'location', 'latitude', 'longitude', @@ -66,40 +68,31 @@ 'items', 'userInfo', 'prsId', 'dsid', 'dsInfo', 'webservices', 'locations', 'devices', 'content', 'followers', 'following', 'contactDetails', ] -# TABS_BOX_DEBUG = "\t\t\t\t\t\t\t\t\t\t " -# TABS_BOX_INFO = "\t\t\t\t\t " -# TABS_BOX_EVLOG_EXPORT = "\t\t\t" -SP50 = ' '*50 -SP = { - 4: SP50[1:4], - 5: SP50[1:5], - 6: SP50[1:6], - 8: SP50[1:8], - 10: SP50[1:10], - 12: SP50[1:12], - 16: SP50[1:16], - 22: SP50[1:22], - 28: SP50[1:28], - 26: SP50[1:26], - 44: SP50[1:44], - 48: SP50[1:48], - 50: SP50, + +SP_str = ' '*50 +SP_dict = { + 4: SP_str[1:4], + 5: SP_str[1:5], + 6: SP_str[1:6], + 8: SP_str[1:8], + 9: SP_str[1:9], + 10: SP_str[1:10], + 11: SP_str[1:11], + 12: SP_str[1:12], + 13: SP_str[1:13], + 14: SP_str[1:14], + 16: SP_str[1:16], + 22: SP_str[1:22], + 28: SP_str[1:28], + 26: SP_str[1:26], + 44: SP_str[1:44], + 48: SP_str[1:48], + 50: SP_str, } -# SP = { -# 4: ' '*4, -# 5: ' '*5, -# 6: ' '*6, -# 8: ' '*8, -# 10: ' '*10, -# 12: ' '*12, -# 16: ' '*16, -# 22: ' '*22, -# 28: ' '*28, -# 26: ' '*26, -# 44: ' '*44, -# 48: ' '*48, -# 50: ' '*50, -# } +def SP(space_cnt): + if space_cnt in SP_dict: return SP_dict[space_cnt] + if space_cnt < len(SP_str): return SP_str[1:space_cnt] + return ' '*space_cnt #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -113,7 +106,6 @@ def broadcast_info_msg(info_msg): if INFO not in Gb.conf_sensors['device']: return - Gb.broadcast_info_msg = f"{info_msg}" try: @@ -289,8 +281,6 @@ def more_info(key): # ICLOUD3-DEBUG.LOG FILE ROUTINES # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - - def open_ic3log_file_init(): ''' Entry point for async_add_executor_job in __init__.py @@ -352,8 +342,8 @@ def check_ic3log_file_exists(ic3logger_file): open_ic3log_file(new_log_file=True) log_msg = f"{EVLOG_IC3_STARTING}Recreated iCloud3 Log File: {ic3logger_file}" - log_msg = f"{format_startup_header_box(log_msg)}" - log_msg = f"{format_header_box_indent(log_msg, 4).replace('⡇', '⛔')}" + log_msg = f"{format_startup_header_box(log_msg, 20)}" + log_msg = log_msg.replace('⡇', '⛔') Gb.iC3Logger.info(log_msg) return True @@ -365,7 +355,6 @@ def check_ic3log_file_exists(ic3logger_file): return False - #-------------------------------------------------------------------- def archive_ic3log_file(): ''' @@ -402,7 +391,7 @@ def write_config_file_to_ic3log(): conf_tracking_recd[CONF_DEVICES] = f"{len(Gb.conf_devices)}" Gb.trace_prefix = '_INIT_' - indent = SP[44] if Gb.log_debug_flag else SP[26] + indent = SP(44) if Gb.log_debug_flag else SP(26) log_msg = ( f"iCloud3 v{Gb.version}, " f"{dt_util.now().strftime('%A')}, " f"{dt_util.now().strftime(DATETIME_FORMAT)[:19]}") @@ -455,7 +444,7 @@ def log_info_msg(module_name, log_msg='+'): log_msg = format_msg_line(log_msg) write_ic3log_recd(log_msg) - log_msg = log_msg.replace(' > +', f" > ...\n{SP[22]}+") + log_msg = log_msg.replace(' > +', f" > ...\n{SP(22)}+") Gb.HALogger.debug(log_msg) #-------------------------------------------------------------------- @@ -487,6 +476,7 @@ def log_exception(err): #-------------------------------------------------------------------- def log_debug_msg(devicename_or_Device, log_msg='+', msg_prefix=None): + if Gb.log_debug_flag is False: return if devicename_or_Device and log_msg == '': return devicename, log_msg = _resolve_devicename_log_msg(devicename_or_Device, log_msg) @@ -495,11 +485,9 @@ def log_debug_msg(devicename_or_Device, log_msg='+', msg_prefix=None): log_msg = f"{dn_str}{str(log_msg).replace(CRLF, ', ')}" log_msg = format_msg_line(log_msg) - if Gb.log_debug_flag: - write_ic3log_recd(log_msg) - + write_ic3log_recd(log_msg) - log_msg = log_msg.replace(' > +', f" > ...\n{SP[22]}+") + log_msg = log_msg.replace(' > +', f" > ...\n{SP(22)}+") Gb.HALogger.debug(log_msg) #-------------------------------------------------------------------- @@ -513,7 +501,7 @@ def log_start_finish_update_banner(start_finish, devicename, Device = Gb.Devices_by_devicename[devicename] text = (f"{devicename}, {method}, " f"CurrZone-{Device.sensor_zone}, {update_reason} ") - log_msg = format_header_box(text, start_finish) + log_msg = format_header_box(text, indent=43, start_finish=start_finish) log_info_msg(log_msg) @@ -551,12 +539,9 @@ def format_msg_line(log_msg, area=None): Gb.trace_prefix source = f"{_called_from()}{program_area}" log_msg = format_startup_header_box(log_msg) - log_msg = format_header_box_indent(log_msg, len(source)) msg_prefix= ' ' if log_msg.startswith('⡇') else \ ' ⡇ ' if Gb.trace_group else \ ' ' - # ' ▹ ' - # ' > ' log_msg = filter_special_chars(log_msg) log_msg = f"{source}{msg_prefix}{log_msg}" @@ -572,9 +557,9 @@ def filter_special_chars(recd, evlog_export=False): Filter out EVLOG_XXX control fields ''' - indent =SP[16] if evlog_export else \ - SP[48] if Gb.log_debug_flag else \ - SP[28] + indent =SP(16) if evlog_export else \ + SP(48) if Gb.log_debug_flag else \ + SP(28) if recd.startswith('^'): recd = recd[3:] recd = recd.replace(EVLOG_MONITOR, '') @@ -627,34 +612,27 @@ def format_startup_header_box(log_msg): return log_msg #-------------------------------------------------------------------- -def format_header_box(recd, start_finish=None, evlog_export=False): +def format_header_box(recd, indent=None, start_finish=None, evlog_export=False): ''' Format a box around this item ''' start_pos = recd.find('^') if start_pos == -1: start_pos = 0 + # Default indent for icloud3-0.log file is 43 + if indent is None: indent = 43 + top_char = bot_char = DASH_50 if start_finish == 'start': bot_char = f"{'⠂'*37}" Gb.trace_group = True elif start_finish == 'finish': top_char = f"{'⠂'*37}" - # top_char = f"{'⠐'*37}" Gb.trace_group = False - # return (f"⡇{top_char}\n" return (f"⡇{top_char}\n" - f"▹⡇{SP[4]}{recd[start_pos:].upper()}\n" - f"▹⡇{bot_char}") - # f"▹⡇{bot_char}") - -#-------------------------------------------------------------------- -def format_header_box_indent(log_msg, indent): - if instr(log_msg, '▹⡇') is False: - return log_msg - - return log_msg.replace('▹', f"{' '*(16+indent)}") + f"{SP(indent)}⡇{SP(4)}{recd[start_pos:].upper()}\n" + f"{SP(indent)}⡇{bot_char}") #------------------------------------------------------------------------------------------- def _resolve_devicename_log_msg(devicename_or_Device, event_msg): @@ -681,6 +659,34 @@ def _resolve_module_name_log_msg(module_name, log_msg): # RAWDATA LOGGING ROUTINES # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def log_rawdata_unfiltered(title, rawdata): + try: + rawdata_copy = rawdata['raw'].copy() if 'raw' in rawdata else rawdata.copy() + + except: + log_info_msg(f"{'─'*8} {title.upper()} {'─'*8}\n{rawdata}") + return + + devices_data = {} + + if 'content' in rawdata_copy: + for device_data in rawdata_copy['content']: + devices_data[device_data['name']] = device_data + + rawdata_copy['content'] = 'DeviceData...' + + log_info_msg(f"{'─'*8} {title.upper()} {'─'*8}\n{rawdata_copy}") + + for device_data in devices_data: + log_msg = ( f"FamShr PyiCloud Data (unfiltered -- " + f"{device_data['name'], }" + f"{device_data['deviceDisplayName']} " + f"({device_data['rawDeviceModel']})" + f"\n" + f"{device_data}") + log_info_msg(log_msg) + +#-------------------------------------------------------------------- def log_rawdata(title, rawdata, log_rawdata_flag=False): ''' Add raw data records to the HA log file for debugging purposes. @@ -697,26 +703,58 @@ def log_rawdata(title, rawdata, log_rawdata_flag=False): if Gb.log_rawdata_flag is False or rawdata is None: return + # log_info_msg(f"RAWDATA 706 {title=} {Gb.log_level_devices}") + # if Gb.log_rawdata_flag_unfiltered: + # log_rawdata_unfiltered(title, rawdata) + # return + + if (Gb.start_icloud3_inprocess_flag + or 'all' in Gb.log_level_devices + or Gb.log_level_devices == []): + pass + elif (Gb.log_level_devices + and (instr(title, FAMSHR_FNAME) + or instr(title, MOBAPP_FNAME) + or instr(title, 'iCloud') + or instr(title, 'Mobile'))): + + log_level_devices = [devicename for devicename in Gb.log_level_devices if instr(title, devicename)] + if log_level_devices == []: + return filtered_dicts = {} filtered_lists = {} - filtered_data = {} - rawdata_data = {} + filtered_data = {} + rawdata_data = {} try: + if type(rawdata) is not dict: + log_info_msg(f"{'─'*8} {title.upper()} {'─'*8}\n{rawdata}") + return + + if Gb.log_rawdata_flag_unfiltered: + log_rawdata_unfiltered(title, rawdata) + return + if 'raw' in rawdata or log_rawdata_flag: log_info_msg(f"{'─'*8} {title.upper()} {'─'*8}\n{rawdata}") return - rawdata_items = {k: v for k, v in rawdata['filter'].items() - if type(v) not in [dict, list]} - rawdata_data['filter'] = {k: v for k, v in rawdata['filter'].items() - if k in FILTER_FIELDS} + rawdata_items = {k: v for k, v in rawdata['filter'].items() + if type(v) not in [dict, list]} + if Gb.log_rawdata_flag_unfiltered: + rawdata_data['filter'] = rawdata['filter'] + else: + rawdata_data['filter'] = {k: v for k, v in rawdata['filter'].items() + if k in FILTER_FIELDS} except: - rawdata_items = {k: v for k, v in rawdata.items() - if type(v) not in [dict, list]} - rawdata_data['filter'] = {k: v for k, v in rawdata.items() - if k in FILTER_FIELDS} + rawdata_items = {k: v for k, v in rawdata.items() + if type(v) not in [dict, list]} + if Gb.log_rawdata_flag_unfiltered: + rawdata_data['filter'] = rawdata + else: + rawdata_data['filter'] = {k: v for k, v in rawdata.items() + if k in FILTER_FIELDS} rawdata_data['filter']['items'] = rawdata_items if rawdata_data['filter']: @@ -906,7 +944,6 @@ def post_internal_error(err_text, traceback_format_exec_obj='+'): #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> def dummy_trace(): _trace(None, None) - _traceha(None, None) #-------------------------------------------------------------------- def _trace(devicename_or_Device, items='+'): @@ -922,7 +959,7 @@ def _trace(devicename_or_Device, items='+'): #rc9 Reworked post_event and write_config_file to call modules directly if Gb.EvLog: - Gb.EvLog.post_event(devicename, f"^3^{called_from} {items}") + Gb.EvLog.post_event(devicename, f"{EVLOG_TRACE}{called_from} {items}") write_ic3log_recd(f"{called_from}⛔.⛔ . . . {devicename} > {items}") #-------------------------------------------------------------------- diff --git a/custom_components/icloud3/helpers/time_util.py b/custom_components/icloud3/helpers/time_util.py index 3af094f..3ba5140 100644 --- a/custom_components/icloud3/helpers/time_util.py +++ b/custom_components/icloud3/helpers/time_util.py @@ -11,51 +11,36 @@ #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # -# Time conversion and formatting functions +# Current Time conversion and formatting functions # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -def time_now(): - ''' now --> epoch/unix 10:23:45 ''' - return (dt_util.now().strftime(DATETIME_FORMAT)[11:19]) - -#-------------------------------------------------------------------- def time_now_secs(): ''' now --> epoch/unix secs ''' return int(time.time()) #-------------------------------------------------------------------- -def time_secs(): - ''' now --> epoch/unix secs ''' - return time_now_secs() - -#-------------------------------------------------------------------- -def datetime_now_ymd_hms(): - ''' now --> epoch/unix yyy-mm-dd 10:23:45 ''' - return (dt_util.now().strftime(DATETIME_FORMAT)[0:19]) +def time_now(): + ''' now --> epoch/unix 10:23:45 ''' + return str(datetime.fromtimestamp(int(time.time())))[11:19] #-------------------------------------------------------------------- def datetime_now(): ''' now --> epoch/unix yyy-mm-dd 10:23:45 ''' - return secs_to_datetime(time_now_secs()) + return str(datetime.fromtimestamp(int(time.time()))) #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # # Time conversion and formatting functions # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -def isnot_valid(secs): - ''' - Not valid if before 1/1/2020, = 9999999999 or None - ''' - try: - return secs < 1 or secs == HIGH_INTEGER or secs is None - except: - return True - -#-------------------------------------------------------------------- def secs_local(secs_utc): return secs_utc + Gb.time_zone_offset_secs +#-------------------------------------------------------------------- +def time_local(secs_utc): + ''' secs_utc --> 10:23:45 ''' + return datetime_local(secs_utc)[11:19] + #-------------------------------------------------------------------- def datetime_local(secs_utc): ''' secs_utc --> 2024-03-15 10:23:45 ''' @@ -64,16 +49,22 @@ def datetime_local(secs_utc): return str(datetime.fromtimestamp(secs_utc)) #-------------------------------------------------------------------- -def time_local(secs_utc): - ''' secs_utc --> 10:23:45 ''' - - return datetime_local(secs_utc)[11:19] - +def isnot_valid(secs): + ''' + Not valid if before 1/1/2020, = 9999999999 or None + ''' + try: + return secs < 1 or secs == HIGH_INTEGER or secs is None + except: + return True #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # # Time conversion and formatting functions # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def s2t(secs_utc): + return secs_to_time(secs_utc) + def secs_to_time(secs_utc): ''' secs --> 10:23:45/h:mm:ssa ''' @@ -122,9 +113,7 @@ def mins_to(secs): #-------------------------------------------------------------------- def time_to_12hrtime(hhmmss, ampm=True): - ''' 10:23:45 --> (h)h:mm:ssa or (h)h:mm:ssp - - ''' + ''' 10:23:45 --> (h)h:mm:ssa or (h)h:mm:ssp ''' try: if hhmmss == HHMMSS_ZERO: @@ -158,6 +147,27 @@ def time_to_12hrtime(hhmmss, ampm=True): pass return hhmmss + +#-------------------------------------------------------------------- +def time_to_24hrtime(hhmmss): + ''' (h)h:mm:ssa or (h)h:mm:ssp --> hh:mm:ss ''' + + hhmm_colon = hhmmss.find(':') + if hhmm_colon == -1: return hhmmss + + ap = hhmmss[-1].lower() # Get last character of time (#, a, p).lower() + if ap not in ['a', 'p']: + return hhmmss + + hh = int(hhmmss[:hhmm_colon]) + if hh == 12 and ap == 'a': + hh = 0 + elif hh <= 11 and ap == 'p': + hh += 12 + + hhmmss24 = f"{hh:0>2}{hhmmss[hhmm_colon:-1]}" + + return hhmmss24 #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # # FORMAT TIMER & AGE FUNCTIONS @@ -471,6 +481,7 @@ def extract_time_fields(msg_str): hhmm_colon += 4 return list(times_found) + #-------------------------------------------------------------------------------- def adjust_time_hour_value(hhmmss, hh_adjustment): ''' @@ -492,22 +503,53 @@ def adjust_time_hour_value(hhmmss, hh_adjustment): hhmm_colon = hhmmss.find(':') if hhmm_colon == -1: return hhmmss - ap = hhmmss_ap = hhmmss[-1] # Get last character of time (#, a, p) - ap = f"{ap.lower()}m" if ap in ['a', 'p', 'A', 'P'] else '' # Reformat (pm or '') - hh = hhmmss[:hhmm_colon] + ap # Create Hometime zonehours field (3pm, 15) - - if hh.endswith('m'): - dt12 = datetime.strptime(hh, '%I%p') # Get 12-hour datetime (1900-01-01 03:00:00pm) - h24 = dt12.strftime('%H') # Convert to 24 hour time (03pm -> 15) - dt24 = datetime.strptime(h24, '%H') # Get datetime value of 24-hour time (1900-01-01 15:00:00) - dt24 += timedelta(hours=hh_adjustment) # Add time zone offset (15-2=13) - ap = dt24.strftime("%p")[0].lower() # Get a/p for new time (PM -> p) - hh_away_zone = dt24.strftime("%-I") # Get Away time zone 12-hour value (1) - - else: - dt24 = datetime.strptime(hh, '%H') # Get datetime value of 24-hour time (15) - dt24 += timedelta(hours=hh_adjustment) # Add time zone offset (15-2=13) - hh_away_zone = dt24.strftime("%H") # Get Away time zone 12-hour value (15) + hhmmss24 = time_to_24hrtime(hhmmss) + hh = int(hhmmss24[0:2]) + hh_adjustment + if hh <= 0: + hh += 24 + elif hh >= 24: + hh -=24 + + hhmmss24 = f"{hh:0>2}{hhmmss24[2:8]}" + + return time_to_12hrtime(hhmmss24) + +#-------------------------------------------------------------------------------- +def xadjust_time_hour_value(hhmmss, hh_adjustment): + ''' + All times are based on the HA server time. When the device is in another time + zone, convert the HA server time to the device's local time so the local time + can be displayed on the Event Log and in time-based sensors. + + Input: + hhmmss - HA server time (hh:mm, hh:mm:ss, hh:mm(a/p), hh:mm:ss(a/p)) + hh_adjustment - Number of hours between the HA server time and the + local time (-12 to 12) + Return: + new time value in the same format as the Input hhmmss time + ''' + + if hh_adjustment == 0 or hhmmss == HHMMSS_ZERO: + return hhmmss + + hhmm_colon = hhmmss.find(':') + if hhmm_colon == -1: return hhmmss + + ap = hhmmss_ap = hhmmss[-1].lower() # Get last character of time (#, a, p).lower() + h24 = int(hhmmss[:hhmm_colon]) + + if ap in ['a', 'p']: # 12-hour time (7:23:45p) + if (h24 == 12 and ap == 'a'): h24 = 0 # Convert to 24-hour time + elif ap == 'p': h24 += 12 + dt24 = datetime.strptime(str(h24), '%H') # Get datetime value of 24-hour time (19) + dt24 += timedelta(hours=hh_adjustment) # Add time zone offset (3, -3) = (16, 22) + ap = dt24.strftime("%p")[0].lower() # Get a/p for new time (PM -> p) (p) + hh_away_zone = dt24.strftime("%-I") # Get Away time zone 12-hour value (4, 10) + + else: # 24-hour time (19:23:45) + dt24 = datetime.strptime(str(h24), '%H') # Get datetime value of 24-hour time (19) + dt24 += timedelta(hours=hh_adjustment) # Add time zone offset (3, -3) = (16, 22) + hh_away_zone = dt24.strftime("%H") # Get Away time zone value (16, 22) hhmmss = f"{hh_away_zone}{hhmmss[hhmm_colon:]}" if ap: hhmmss = hhmmss.replace(hhmmss_ap, ap) diff --git a/custom_components/icloud3/icloud3_main.py b/custom_components/icloud3/icloud3_main.py index 6a0b94a..44ade92 100644 --- a/custom_components/icloud3/icloud3_main.py +++ b/custom_components/icloud3/icloud3_main.py @@ -35,10 +35,11 @@ from .global_variables import GlobalVariables as Gb from .const import (VERSION, - HOME, NOT_HOME, NOT_SET, HIGH_INTEGER, RARROW, LT, + HOME, NOT_HOME, NOT_SET, HIGH_INTEGER, RARROW, LT, NBSP3, CLOCK_FACE, + CRLF, DOT, LDOT2, CRLF_DOT, CRLF_LDOT, CRLF_HDOT, CRLF_X, NL, NL_DOT, TOWARDS, EVLOG_IC3_STAGE_HDR, ICLOUD, ICLOUD_FNAME, TRACKING_NORMAL, FNAME, - IPHONE, IPAD, WATCH, AIRPODS, IPOD, + IPHONE, IPAD, WATCH, AIRPODS, IPOD, ALERT, CMD_RESET_PYICLOUD_SESSION, NEAR_DEVICE_DISTANCE, DISTANCE_TO_OTHER_DEVICES, DISTANCE_TO_OTHER_DEVICES_DATETIME, OLD_LOCATION_CNT, AUTH_ERROR_CNT, @@ -59,7 +60,7 @@ from .support import service_handler from .support import zone_handler from .support import determine_interval as det_interval -from .helpers.common import (instr, is_zone, is_statzone, isnot_statzone, list_to_str,) +from .helpers.common import (instr, is_zone, is_statzone, isnot_statzone, list_to_str, isbetween, ) from .helpers.messaging import (broadcast_info_msg, post_event, post_error_msg, post_monitor_msg, post_internal_error, post_evlog_greenbar_msg, clear_evlog_greenbar_msg, @@ -144,14 +145,12 @@ def start_icloud3(self): self.initial_locate_complete_flag = False self.startup_log_msgs = '' self.startup_log_msgs_prefix = '' + Gb.start_icloud3_inprocess_flag = True + Gb.restart_icloud3_request_flag = False + Gb.all_tracking_paused_flag = False start_ic3_control.stage_1_setup_variables() start_ic3_control.stage_2_prepare_configuration() - if Gb.polling_5_sec_loop_running is False: - broadcast_info_msg("Set Up 5-sec Polling Cycle") - Gb.polling_5_sec_loop_running = True - track_utc_time_change(Gb.hass, self._polling_loop_5_sec_device, - second=[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]) start_ic3_control.stage_3_setup_configured_devices() stage_4_success = start_ic3_control.stage_4_setup_data_sources() @@ -170,6 +169,13 @@ def start_icloud3(self): Gb.startup_stage_status_controls = [] Gb.broadcast_info_msg = None + if Gb.polling_5_sec_loop_running is False: + broadcast_info_msg("Set Up 5-sec Polling Cycle") + Gb.polling_5_sec_loop_running = True + track_utc_time_change(Gb.hass, self._polling_loop_5_sec_device, + second=[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]) + + return True except Exception as err: @@ -189,13 +195,15 @@ def _polling_loop_5_sec_device(self, ha_timer_secs): Gb.this_update_secs = time_now_secs() Gb.this_update_time = dt_util.now().strftime('%H:%M:%S') + if Gb.start_icloud3_inprocess_flag: + return + if Gb.config_flow_updated_parms != {''}: start_ic3.process_config_flow_parameter_updates() # Restart iCloud via service call from EvLog or config_flow if Gb.restart_icloud3_request_flag: self.start_icloud3() - Gb.restart_icloud3_request_flag = False # Exit 5-sec loop if no devices, updating a device now, or restarting iCloud3 info_msg = '' @@ -246,6 +254,7 @@ def _polling_loop_5_sec_device(self, ha_timer_secs): # End of uncommented out code to test of moving device into a statzone while home if Gb.all_tracking_paused_flag: + post_evlog_greenbar_msg('All Devices > Tracking Paused') return #<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>> @@ -304,7 +313,7 @@ def _polling_loop_5_sec_device(self, ha_timer_secs): except Exception as err: log_exception(err) - self._display_device_error_evlog_greenbar_msg() + self._display_device_alert_evlog_greenbar_msg() Gb.any_device_was_updated_reason = '' self.initialize_5_sec_loop_control_flags() self._display_clear_authentication_needed_msg() @@ -615,6 +624,9 @@ def _main_5sec_loop_special_time_control(self): Device.log_data_fields() for devicename, Device in Gb.Devices_by_devicename.items(): + # if devicename == 'gary_iphone': + # mobapp_interface.request_sensor_update(Device) + if Device.dist_apart_msg: device_time = secs_to_hhmm(Device.dist_to_other_devices_secs) event_msg =(f"Nearby Devices " @@ -778,7 +790,6 @@ def _validate_new_icloud_data(self, Device): # Bypass all update needed checks and force an iCloud update elif Device.icloud_force_update_flag: - # Device.icloud_force_update_flag = False pass # elif Device.is_offline or Device.no_location_data: @@ -1150,7 +1161,7 @@ def _display_icloud_acct_error_msg(self, Device): post_error_msg(log_msg) #---------------------------------------------------------------------------- - def _display_device_error_evlog_greenbar_msg(self): + def _display_device_alert_evlog_greenbar_msg(self): ''' Check to see if any startup alerts of device issues exist. If so, display them in the green alert bar at the to p of the EvLog. @@ -1158,35 +1169,62 @@ def _display_device_error_evlog_greenbar_msg(self): Tracked device screen displayed - Show all alert messages Monitored device screen displayed - Show only that devices alerts ''' - evlog_greenbar_msg = '' + evlog_greenbar_msg = evlog_startup_alert_attr = '' + evlog_tracked_alert_attr = evlog_monitored_alert_attr = '' show_on_displayed_evlog_screen_flag = False if Gb.startup_alerts != []: - evlog_greenbar_msg += "Errors Starting iCloud3, " + evlog_greenbar_msg = f"{LDOT2}Errors Starting iCloud3" + evlog_startup_alert_attr = Gb.startup_alerts_str + show_on_displayed_evlog_screen_flag = True for Device in Gb.Devices: - device_msg = '' + alert_msg = '' + if (Device.verified_flag is False + or (Device.is_data_source_ICLOUD is False and Device.is_data_source_MOBAPP is False)): + alert_msg = "Not Verified, No Data Source, " if Device.no_location_data: - device_msg = "No GPS Data, " - elif Device.is_offline: - device_msg = "Offline, " - elif mins_since(Device.loc_data_secs) > 300: - device_msg = f"Location Very Old (>{format_age(Device.loc_data_secs)}), " + alert_msg += "No GPS Data, " + if Device.is_offline: + alert_msg += "Offline, " + + if alert_msg: + alert_msg = alert_msg[:-2] elif Device.is_tracking_paused: - device_msg = "Paused, " + alert_msg = "Tracking Paused" + elif mins_since(Device.loc_data_secs) > 300: + alert_msg = f"Location Very Old (>{format_age(Device.loc_data_secs)})" + elif isbetween(Device.dev_data_battery_level, 1, 19): + alert_msg = f"Low Battery (< 20%)" + + if alert_msg: + fname_alert_msg = f"{Device.fname} > {alert_msg}" + crlf = CRLF_LDOT if evlog_greenbar_msg else LDOT2 + evlog_greenbar_msg += f"{crlf}{fname_alert_msg}" + if Device.is_tracked: + nldot = NL_DOT if evlog_tracked_alert_attr else DOT + evlog_tracked_alert_attr += f"{nldot}{fname_alert_msg}" + else: + nldot = NL_DOT if evlog_monitored_alert_attr else DOT + evlog_monitored_alert_attr += f"{nldot}{fname_alert_msg}" - if device_msg: - evlog_greenbar_msg += f"{Device.fname} > {device_msg}" - #if (Device.is_tracked - # or Gb.EvLog.evlog_attrs["fname"] == f"{Device.fname} 🅜"): if (Device.device_type in [IPHONE, IPAD, WATCH] or Gb.EvLog.evlog_attrs[FNAME].startswith(Device.fname)): show_on_displayed_evlog_screen_flag = True - if evlog_greenbar_msg and show_on_displayed_evlog_screen_flag: - post_evlog_greenbar_msg(evlog_greenbar_msg[:-2]) - else: + if alert_msg != Device.alert: + Device.alert = Device.sensors[ALERT] = alert_msg + Device.write_ha_device_tracker_state() + + Gb.EvLog.evlog_attrs['alert_startup'] = Gb.EvLog.alert_attr_filter(evlog_startup_alert_attr) + Gb.EvLog.evlog_attrs['alert_tracked'] = Gb.EvLog.alert_attr_filter(evlog_tracked_alert_attr) + Gb.EvLog.evlog_attrs['alert_monitored'] = Gb.EvLog.alert_attr_filter(evlog_monitored_alert_attr) + + if (evlog_greenbar_msg != Gb.EvLog.greenbar_alert_msg + and show_on_displayed_evlog_screen_flag): + post_evlog_greenbar_msg(evlog_greenbar_msg) + elif evlog_greenbar_msg == '' and Gb.EvLog.greenbar_alert_msg: clear_evlog_greenbar_msg() #---------------------------------------------------------------------------- @@ -1196,11 +1234,11 @@ def _display_clear_authentication_needed_msg(self): pass elif (Gb.PyiCloud.requires_2fa - and Gb.EvLog.alert_message != 'iCloud Account authentication is needed'): + and Gb.EvLog.greenbar_alert_msg != 'iCloud Account authentication is needed'): post_evlog_greenbar_msg('iCloud Account authentication is needed') elif (Gb.PyiCloud.requires_2fa is False - and Gb.EvLog.alert_message == 'iCloud Account authentication is needed'): + and Gb.EvLog.greenbar_alert_msg == 'iCloud Account authentication is needed'): clear_evlog_greenbar_msg() #-------------------------------------------------------------------- @@ -1320,6 +1358,7 @@ def _set_old_location_status(self, Device): elif Device.is_offline: Device.old_loc_msg = f"Device > Offline {cnt_msg}" Device.update_sensors_flag = False + statzone.clear_statzone_timer_distance(Device) elif Device.is_location_old: Device.old_loc_msg = f"Location > Old {cnt_msg}, {format_age(Device.loc_data_secs)}" elif Device.is_gps_poor: diff --git a/custom_components/icloud3/sensor.py b/custom_components/icloud3/sensor.py index 1e34397..d22c006 100644 --- a/custom_components/icloud3/sensor.py +++ b/custom_components/icloud3/sensor.py @@ -20,7 +20,7 @@ SENSOR_EVENT_LOG_NAME, SENSOR_WAZEHIST_TRACK_NAME, HOME, HOME_FNAME, NOT_SET, NOT_SET_FNAME, NONE_FNAME, DATETIME_ZERO, HHMMSS_ZERO, - BLANK_SENSOR_FIELD, DOT, HDOT, HDOT2, UM_FNAME, NBSP, + BLANK_SENSOR_FIELD, DOT, HDOT, UM_FNAME, NBSP, TRACK_DEVICE, MONITOR_DEVICE, INACTIVE_DEVICE, NAME, FNAME, BADGE, FROM_ZONE, ZONE, ZONE_DISTANCE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, @@ -70,6 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e # Save the hass `add_entities` call object for use in config_flow for adding new sensors Gb.hass = hass Gb.async_add_entities_sensor = async_add_entities + Gb.sensors_created_cnt = 0 try: if Gb.conf_file_data == {}: @@ -128,7 +129,6 @@ def create_tracked_device_sensors(devicename, conf_device, new_sensors_list=None ''' try: NewSensors = [] - Gb.sensors_created_cnt = 0 if new_sensors_list is None: new_sensors_list = [] @@ -422,6 +422,7 @@ def __init__(self, devicename, sensor_base, conf_device, from_zone=None): self.sensor_base = sensor_base self.sensor_type = self._get_sensor_definition(sensor_base, SENSOR_TYPE).replace(' ', '') + self.sensor_empty = self._get_sensor_definition(sensor_base, SENSOR_DEFAULT) self.sensor_fname = (f"{conf_device[FNAME]} " f"{self._get_sensor_definition(sensor_base, SENSOR_FNAME)}" f"{self.from_zone_fname}") @@ -602,8 +603,8 @@ def _get_device_sensor_value(self, sensor): #------------------------------------------------------------------------------------------- def _get_restore_or_default_value(self, sensor): ''' - Get a default value that is used when iCloud3 has not started or the Device for the - sensor has not veen created. + Get a default value that is used when iCloud3 has not started, the Device for the + sensor has not veen created or the type is text and the value is ''. ''' try: if self.from_zone: @@ -848,9 +849,6 @@ class Sensor_Text(SensorBase): def native_value(self): sensor_value = self._get_sensor_value(self.sensor) - # if instr(self.sensor_type, 'title'): - # sensor_value = sensor_value.title().replace('_', ' ') - if instr(self.sensor_type, 'time'): if instr(sensor_value, ' '): text_um_parts = sensor_value.split(' ') @@ -861,7 +859,7 @@ def native_value(self): # Set to space if empty if sensor_value.strip() == '': - sensor_value = BLANK_SENSOR_FIELD + sensor_value = self.sensor_empty #BLANK_SENSOR_FIELD if self.Device and self.Device.away_time_zone_offset != 0: sensor_value = adjust_time_hour_values(sensor_value, self.Device.away_time_zone_offset) @@ -919,10 +917,14 @@ class Sensor_Timestamp(SensorBase): @property def native_value(self): sensor_value = self._get_sensor_value(self.sensor) + state_value=sensor_value + sensor_value = time_to_12hrtime(sensor_value) + state_value_12=sensor_value + if self.Device and self.Device.away_time_zone_offset != 0: sensor_value = adjust_time_hour_value(sensor_value, self.Device.away_time_zone_offset) - + state_value_adj=sensor_value try: # Drop the 'a' or 'p' so the field will fit on an iPhone if int(sensor_value.split(':')[0]) >= 10: @@ -934,6 +936,8 @@ def native_value(self): @property def extra_state_attributes(self): + attrs = self._get_extra_attributes(self.sensor) + return attrs return self._get_extra_attributes(self.sensor) @@ -1253,12 +1257,13 @@ def should_poll(self): #------------------------------------------------------------------------------------------- def async_update_sensor(self): """Update the entity's state.""" + if Gb.hass is None: return try: # self.async_write_ha_state() self.schedule_update_ha_state() - except RuntimeError: + except (RuntimeError, AttributeError): # Catch a 'RuntimeError: Attribute hass is None for ' pass except Exception as err: diff --git a/custom_components/icloud3/support/config_file.py b/custom_components/icloud3/support/config_file.py index 4a6f56e..2d3ef09 100644 --- a/custom_components/icloud3/support/config_file.py +++ b/custom_components/icloud3/support/config_file.py @@ -11,7 +11,7 @@ CONF_IC3_VERSION, VERSION, CONF_EVLOG_CARD_DIRECTORY, CONF_EVLOG_CARD_PROGRAM, CONF_TRAVEL_TIME_FACTOR, CONF_UPDATE_DATE, CONF_VERSION_INSTALL_DATE, CONF_PASSWORD, CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX, CONF_DEVICES, CONF_IC3_DEVICENAME, CONF_SETUP_ICLOUD_SESSION_EARLY, - CONF_UNIT_OF_MEASUREMENT, CONF_TIME_FORMAT, CONF_LOG_LEVEL, + CONF_UNIT_OF_MEASUREMENT, CONF_TIME_FORMAT, CONF_LOG_LEVEL, CONF_LOG_LEVEL_DEVICES, CONF_DATA_SOURCE, CONF_DISPLAY_GPS_LAT_LONG, CONF_LOG_ZONES, CONF_FAMSHR_DEVICENAME, CONF_FMF_EMAIL, CONF_MOBILE_APP_DEVICE, CONF_IOSAPP_DEVICE, @@ -38,7 +38,7 @@ from ..support import waze from ..helpers.common import (instr, ordereddict_to_dict, isbetween, ) from ..helpers.messaging import (log_exception, _trace, _traceha, log_info_msg, ) -from ..helpers.time_util import (datetime_now, datetime_now_ymd_hms, ) +from ..helpers.time_util import (datetime_now, ) import os import json @@ -187,11 +187,11 @@ def config_file_check_new_ic3_version(): update_config_file_flag = False if Gb.conf_profile[CONF_IC3_VERSION] != VERSION: Gb.conf_profile[CONF_IC3_VERSION] = VERSION - Gb.conf_profile[CONF_VERSION_INSTALL_DATE] = datetime_now_ymd_hms() + Gb.conf_profile[CONF_VERSION_INSTALL_DATE] = datetime_now() update_config_file_flag = True elif Gb.conf_profile[CONF_VERSION_INSTALL_DATE] == DATETIME_ZERO: - Gb.conf_profile[CONF_VERSION_INSTALL_DATE] = datetime_now_ymd_hms() + Gb.conf_profile[CONF_VERSION_INSTALL_DATE] = datetime_now() update_config_file_flag = True if update_config_file_flag: @@ -338,6 +338,10 @@ def config_file_add_new_parameters(): update_config_file_flag = (_add_config_file_parameter(Gb.conf_general, CONF_TRACK_FROM_BASE_ZONE_USED, False) or update_config_file_flag) + # Add log level devices to select the devices that should be used when rawdata is selected (v3.0.3) + update_config_file_flag = (_add_config_file_parameter(Gb.conf_general, CONF_LOG_LEVEL_DEVICES, ['all']) + or update_config_file_flag) + # Add general.CONF_DEVICE_TRACKER_STATE_SOURCE, b20 update_config_file_flag = (_add_config_file_parameter(Gb.conf_general, CONF_DEVICE_TRACKER_STATE_SOURCE, 'ic3_fname') or update_config_file_flag) @@ -442,7 +446,7 @@ def config_file_check_devices(): if conf_device[CONF_TRACK_FROM_ZONES] == []: conf_device[CONF_TRACK_FROM_ZONES] = [HOME] update_configuration_flag = True - if isbetween(conf_device[CONF_FIXED_INTERVAL], 0, 3): + if isbetween(conf_device[CONF_FIXED_INTERVAL], 1, 2): conf_device[CONF_FIXED_INTERVAL] = 3.0 update_configuration_flag = True @@ -513,7 +517,7 @@ def write_storage_icloud3_configuration_file(filename_suffix=''): # 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]) - Gb.conf_profile[CONF_UPDATE_DATE] = datetime_now_ymd_hms() + Gb.conf_profile[CONF_UPDATE_DATE] = datetime_now() Gb.conf_data['tracking']['devices'] = Gb.conf_devices Gb.conf_data['tracking'] = Gb.conf_tracking diff --git a/custom_components/icloud3/support/determine_interval.py b/custom_components/icloud3/support/determine_interval.py index afd747a..64faaa8 100644 --- a/custom_components/icloud3/support/determine_interval.py +++ b/custom_components/icloud3/support/determine_interval.py @@ -43,7 +43,7 @@ # from ..support import mobapp_interface # from ..support import stationary_zone as statzone -from ..helpers.common import (instr, round_to_zero, is_zone, is_statzone, isnot_zone, +from ..helpers.common import (instr, isbetween, round_to_zero, is_zone, is_statzone, isnot_zone, zone_dname, ) from ..helpers.messaging import (post_event, post_error_msg, post_evlog_greenbar_msg, clear_evlog_greenbar_msg, @@ -68,6 +68,7 @@ LD_WAZE_TIME = 5 LD_MOVED = 6 LD_DIRECTION = 7 +LD_AWAYFROM_OVERRIDE = 8 #waze_from_zone fields WAZ_STATUS = 0 @@ -124,7 +125,8 @@ def determine_interval(Device, FromZone): Device.display_info_msg(Device.format_info_msg, new_base_msg=True) #-------------------------------------------------------------------------------- - Device.write_ha_sensor_state(LAST_LOCATED, Device.loc_data_time) + #Device.write_ha_sensor_state(LAST_LOCATED, Device.loc_data_time) + Device.write_ha_sensors_state([LAST_LOCATED, NEXT_UPDATE, LAST_UPDATE]) if used_near_device_results(Device, FromZone): return FromZone.sensors @@ -146,11 +148,16 @@ def determine_interval(Device, FromZone): waze_time_from_zone = location_data[LD_WAZE_TIME] dist_moved_km = location_data[LD_MOVED] dir_of_travel = location_data[LD_DIRECTION] - - log_msg = ( f"DistFmZome-{dist_from_zone_km}, Moved-{dist_moved_km}, " - f"Waze-{waze_dist_from_zone_km}, Calc-{calc_dist_from_zone_km}, " - f"TravTime-{waze_time_from_zone}, Dir-{dir_of_travel}, " - f"DirHist-{FromZone.dir_of_travel_history}") + dir_of_travel_awayfrom_override = location_data[LD_AWAYFROM_OVERRIDE] + + awayfrom_override_star = '*' if dir_of_travel_awayfrom_override else '' + log_msg = ( f"DistFmZome-{dist_from_zone_km}, " + f"Moved-{dist_moved_km}, " + f"Waze-{waze_dist_from_zone_km}, " + f"Calc-{calc_dist_from_zone_km}, " + f"TravTime-{waze_time_from_zone}, " + f"Dir-{dir_of_travel}{awayfrom_override_star}, " + f"DirHist-{FromZone.dir_of_travel_history[-40:]}") log_debug_msg(devicename, log_msg) @@ -173,7 +180,7 @@ def determine_interval(Device, FromZone): Device.statzone_clear_timer waze_time_msg = 'NotUsed' - calc_interval_secs = round(km_to_mi(dist_from_zone_km) / 1.5) * 60 + calc_interval_secs = round(km_to_mi(dist_from_zone_km) * Gb.travel_time_factor) * 60 if Gb.Waze.is_status_USED: waze_interval_secs = round(waze_time_from_zone * 60 * Gb.travel_time_factor , 0) else: @@ -313,13 +320,15 @@ def determine_interval(Device, FromZone): interval_method = '3.Calc' interval_secs = calc_interval_secs - if (dir_of_travel in ('', ' ', '___', AWAY_FROM) - and interval_secs < 180 - and interval_secs > 30): - interval_method += '+6.AwayFm+<3min' - interval_secs = 180 + # if (dir_of_travel in ('', ' ', '___', AWAY_FROM) + # and isbetween(interval_secs, 30, 180)): + # interval_method += '+6.AwayFm+<3min' + # interval_secs = 180 - elif (dir_of_travel == AWAY_FROM + if (dir_of_travel == AWAY_FROM + and calc_dist_from_zone_km >= 3 + and Device.state_change_flag is False + and Device.is_gps_good and not Gb.Waze.distance_method_waze_flag and Device.fixed_interval_secs == 0): interval_method += '+6.AwayFm+Calc' @@ -342,12 +351,12 @@ def determine_interval(Device, FromZone): interval_secs = 180 #if changed zones on this poll reset multiplier - if Device.state_change_flag: - interval_multiplier = 1 + # if Device.state_change_flag: + # interval_multiplier = 1 #Check accuracy again to make sure nothing changed, update counter - if Device.is_gps_poor: - interval_multiplier = 1 + # if Device.is_gps_poor: + # interval_multiplier = 1 try: #Real close, final check to make sure interval_secs is not adjusted @@ -389,6 +398,12 @@ def determine_interval(Device, FromZone): interval_method = "9.Max" interval_secs = Gb.max_interval_secs + # if moving, make sure interval is not larger than the travtime*factor + if (dir_of_travel in [AWAY_FROM, TOWARDS] + and waze_interval_secs > 0 + and interval_secs > (waze_interval_secs * (Gb.travel_time_factor * 1.5))): + interval_secs = waze_interval_secs * Gb.travel_time_factor + interval_str = format_timer(interval_secs) if interval_multiplier > 1: @@ -425,8 +440,10 @@ def determine_interval(Device, FromZone): FromZone.interval_method = interval_method FromZone.dir_of_travel = dir_of_travel + FromZone.dir_of_travel_awayfrom_override = dir_of_travel_awayfrom_override FromZone.update_dir_of_travel_history(dir_of_travel) - monitor_msg = (f"DirHist-{FromZone.dir_of_travel_history}") + + monitor_msg = f"DirHist-{FromZone.dir_of_travel_history[-40:]}" post_monitor_msg(devicename, monitor_msg) except Exception as err: @@ -542,10 +559,13 @@ def post_results_message_to_event_log(Device, FromZone): event_msg += f"Arrive-{FromZone.sensors[ARRIVAL_TIME]}, " event_msg += f"NextUpdate-{FromZone.next_update_time}, " + event_msg += f"Moved-{km_to_um(Device.loc_data_dist_moved_km)} " - if Device.loc_data_dist_moved_km > 0: - event_msg += (f"Moved-{km_to_um(Device.loc_data_dist_moved_km)} " - f"({FromZone.dir_of_travel}), ") + if Device.isin_zone: + event_msg += ', ' + else: + awayfrom_override_star = '*' if FromZone.dir_of_travel_awayfrom_override else '' + event_msg += f"({FromZone.dir_of_travel}{awayfrom_override_star}), " if Device.is_statzone_timer_set and Device.is_tracked and Gb.is_statzone_used: event_msg += f"IntoStatZone-{secs_to_time(Device.statzone_timer)}, " @@ -565,41 +585,28 @@ def post_results_message_to_event_log(Device, FromZone): and secs_since(Device.mobapp_data_secs) > 3600): event_msg += f"MobAppLocated-{format_age(Device.mobapp_data_secs)}, " - # if FromZone.zone_dist > 0: - # event_msg += ( f"TravTime-{FromZone.last_travel_time}, " - # f"Distance-{km_to_um(FromZone.zone_dist)}, ") - # if FromZone.dir_of_travel == STATIONARY_FNAME: - # event_msg += STATIONARY_FNAME + ", " - # if FromZone.dir_of_travel not in [INZONE, '_', '___', ' ', '']: - # event_msg += f"Direction-{FromZone.dir_of_travel}, " - - # if Device.statzone_dist_moved_km > 0: - # event_msg += f"Moved-{km_to_um(Device.statzone_dist_moved_km)}, " - - #if Device.is_monitored: - # event_msg += f"Source-{Device.dev_data_source}, " - post_event(Device, event_msg[:-2]) - log_msg = ( f"RESULTS: From-{FromZone.from_zone_dname} > " - f"MobAppZone-{Device.mobapp_data_state}, " - f"iC3Zone-{Device.loc_data_zone}, " - f"Interval-{FromZone.interval_str}, " + f"MobApp-{Device.mobapp_data_state}, " + f"iC3-{Device.loc_data_zone}, " + f"Intrvl-{FromZone.interval_str}, " f"TravTime-{FromZone.last_travel_time}, " - f"Dist-{km_to_um(FromZone.zone_dist)}, " - f"NextUpdt-{FromZone.next_update_time}, " - f"MaxDist-{km_to_um(FromZone.max_dist_km)}, " - f"Dir-{FromZone.dir_of_travel}, " + f"Dist-{format_dist_km(FromZone.zone_dist)}, " + f"MaxDist-{format_dist_km(FromZone.max_dist_km)}, " f"Moved-{format_dist_km(Device.statzone_dist_moved_km)}, " - f"Battery-{Device.dev_data_battery_level}%, " - f"LastDataUpdate-{secs_to_time(Device.last_data_update_secs)}, " - f"GPSAccuracy-{Device.loc_data_gps_accuracy}m, " + f"Calc/WazeDist={FromZone.sensors[CALC_DISTANCE]}/{FromZone.sensors[WAZE_DISTANCE]}, " + f"Dir-{FromZone.dir_of_travel}, " + f"Batt-{Device.dev_data_battery_level}%, " + f"NextUpdt-{FromZone.next_update_time}, " + f"LastUpdt-{secs_to_time(Device.last_data_update_secs)}, " + f"GPSAccur-{Device.loc_data_gps_accuracy}m, " f"LocAge-{format_age(Device.loc_data_secs)}, " - f"OldThreshold-{format_timer(Device.old_loc_threshold_secs)}, " + f"OldThresh-{format_timer(Device.old_loc_threshold_secs)}, " f"LastEvLogMsg-{secs_to_time(Device.last_evlog_msg_secs)}, " f"Method-{FromZone.interval_method}") - log_info_msg(Device.devicename, log_msg) + #log_info_msg(Device, log_msg) + post_monitor_msg(Device, log_msg) #-------------------------------------------------------------------------------- def post_zone_time_dist_event_msg(Device, FromZone): @@ -614,7 +621,7 @@ def post_zone_time_dist_event_msg(Device, FromZone): mobapp_state = 'NotUsed' else: mobapp_state = zone_dname(Device.mobapp_data_state) - ic3_zone = zone_dname(Device.loc_data_zone) + ic3_zone = zone_dname(Device.loc_data_zone) if Device.loc_data_zone == NOT_SET: interval_str = travel_time = 0 @@ -959,7 +966,8 @@ def _get_distance_data(Device, FromZone): calc_dist_from_zone_km, # calc_dist_from_zone_km, 0, # waze_time_from_zone, dist_moved_km, # dist moved - dir_of_travel] # direction + dir_of_travel, # direction + False] # direction away_from override return distance_data @@ -1026,7 +1034,9 @@ def _get_distance_data(Device, FromZone): post_event(Device, event_msg) #-------------------------------------------------------------------------------- - dir_of_travel = '___' + # Get direction of travel + + dir_of_travel = UNKNOWN time_change_secs = 0 if waze_time_from_zone == 0 \ else int(waze_time_from_zone * 60) - int(FromZone.waze_time * 60) dist_from_zone_moved_m = int(dist_from_zone_m - FromZone.zone_dist_m) @@ -1043,8 +1053,8 @@ def _get_distance_data(Device, FromZone): elif time_change_secs == 0 and dist_from_zone_moved_m == 0: dir_of_travel = Device.sensors[DIR_OF_TRAVEL] - # Far Away if dist > 400km/250mi - elif (calc_dist_from_zone_km > 400): + # Far Away if dist > 150km/100mi + elif (calc_dist_from_zone_km > 150): dir_of_travel = FAR_AWAY # Towards if the last zone distance > than this zone distance @@ -1058,6 +1068,19 @@ def _get_distance_data(Device, FromZone): #didn't move far enough to tell current direction dir_of_travel = Device.sensors[DIR_OF_TRAVEL] + # Override AWAY_FROM if last 3 directions were TOWARDS and close to the zone + if (dir_of_travel == AWAY_FROM + and Device.went_3km + and dist_from_zone_km < 2 + and FromZone.dir_of_travel_history[-3:] in \ + ['TTT', 'TTt', 'TtT', 'tTT', 'Ttt', 'ttT']): + dir_of_travel = TOWARDS + dir_of_travel_awayfrom_override = True + + else: + dir_of_travel_awayfrom_override = False + + if Device.loc_data_zone == NOT_HOME: if Gb.is_statzone_used is False: pass @@ -1085,7 +1108,8 @@ def _get_distance_data(Device, FromZone): calc_dist_from_zone_km, waze_time_from_zone, dist_moved_km, - dir_of_travel] + dir_of_travel, + dir_of_travel_awayfrom_override] return distance_data @@ -1174,12 +1198,12 @@ def copy_near_device_results(Device, FromZone): The Device is near the NearDevice for the FromZone zone results. Copy the NearDevice variables to Device since everything is the same. ''' - NearDevice = Device.NearDevice - from_zone = FromZone.from_zone + NearDevice = Device.NearDevice + from_zone = FromZone.from_zone NearFromZone = Device.NearDevice.FromZones_by_zone[from_zone] - Device.loc_data_zone = NearDevice.loc_data_zone - Device.zone_change_secs = NearDevice.zone_change_secs + Device.loc_data_zone = NearDevice.loc_data_zone + Device.zone_change_secs = NearDevice.zone_change_secs FromZone.zone_dist = NearFromZone.zone_dist FromZone.waze_dist = NearFromZone.waze_dist @@ -1192,9 +1216,11 @@ def copy_near_device_results(Device, FromZone): FromZone.last_distance_km = NearFromZone.last_distance_km FromZone.last_distance_str = NearFromZone.last_distance_str - FromZone.dir_of_travel = dir_of_travel = NearFromZone.dir_of_travel + FromZone.dir_of_travel = NearFromZone.dir_of_travel + FromZone.dir_of_travel_awayfrom_override = \ + NearFromZone.dir_of_travel_awayfrom_override FromZone.update_dir_of_travel_history(NearFromZone.dir_of_travel) - monitor_msg = (f"DirHist-{FromZone.dir_of_travel_history}") + monitor_msg = (f"DirHist-{FromZone.dir_of_travel_history[-40:]}") post_monitor_msg(Device.devicename, monitor_msg) FromZone.sensors.update(NearFromZone.sensors) @@ -1213,7 +1239,7 @@ def copy_near_device_results(Device, FromZone): Device.statzone_dist_moved_km = NearDevice.statzone_dist_moved_km Device.mobapp_request_loc_sent_secs = Gb.this_update_secs - log_rawdata(f"{Device.devicename} - {from_zone}", FromZone.sensors) + log_rawdata(f"{Device.data_source} - <{Device.devicename}> - {from_zone}", FromZone.sensors) return FromZone.sensors diff --git a/custom_components/icloud3/support/event_log.py b/custom_components/icloud3/support/event_log.py index 6b96b4d..b33be5f 100644 --- a/custom_components/icloud3/support/event_log.py +++ b/custom_components/icloud3/support/event_log.py @@ -12,22 +12,23 @@ from ..global_variables import GlobalVariables as Gb from ..const import (HOME, HOME_FNAME, TOWARDS, - HHMMSS_ZERO, HIGH_INTEGER, NONE, MOBAPP, DOT2, + HHMMSS_ZERO, HIGH_INTEGER, NONE, MOBAPP, RED_X, YELLOW_ALERT, + NL, NL_DOT, LDOT2, + CRLF, CRLF_DOT, CRLF_CHK, RARROW, DOT, LT, GT, DASH_50, + NBSP, NBSP2, NBSP3, NBSP4, NBSP5, NBSP6, CLOCK_FACE, EVENT_RECDS_MAX_CNT_BASE, EVENT_LOG_CLEAR_SECS, EVENT_LOG_CLEAR_CNT, EVENT_RECDS_MAX_CNT_ZONE, EVLOG_BTN_URLS, - EVLOG_TIME_RECD, EVLOG_HIGHLIGHT, EVLOG_MONITOR, + EVLOG_TIME_RECD, EVLOG_HIGHLIGHT, EVLOG_MONITOR, EVLOG_TRACE, EVLOG_ERROR, EVLOG_ALERT, EVLOG_UPDATE_START, EVLOG_UPDATE_END, - NBSP, NBSP2, NBSP3, NBSP4, NBSP5, NBSP6, - CRLF, CRLF_DOT, CRLF_CHK, RARROW, DOT, LT, GT, DASH_50, CONF_EVLOG_BTNCONFIG_URL, EVLOG_ERROR, EVLOG_ALERT, EVLOG_WARNING, EVLOG_INIT_HDR, EVLOG_HIGHLIGHT,EVLOG_IC3_STARTING, EVLOG_IC3_STAGE_HDR, ) -from ..helpers.common import instr, circle_letter, str_to_list, list_to_str +from ..helpers.common import instr, circle_letter, str_to_list, list_to_str, isbetween from ..helpers.messaging import (SP, log_exception, log_info_msg, log_warning_msg, _traceha, _trace, - filter_special_chars, format_header_box, format_header_box_indent, ) + filter_special_chars, format_header_box, ) from ..helpers.time_util import (time_to_12hrtime, datetime_now, time_now_secs, datetime_for_filename, adjust_time_hour_value, adjust_time_hour_values, ) @@ -122,18 +123,23 @@ def initialize(self): self.evlog_btn_urls['btnConfig'] = Gb.evlog_btnconfig_url self.evlog_attrs = {} + self.evlog_attrs["update_time"] = '' + self.evlog_attrs["alert"] = "" + self.evlog_attrs["alerts"] = "" + self.evlog_attrs["alert_startup"] = "" + self.evlog_attrs["alert_tracked"] = "" + self.evlog_attrs["alert_monitored"] = "" + self.evlog_attrs["user_message"] = self.user_message + self.evlog_attrs["devicename"] = '' + self.evlog_attrs["fname"] = '' + self.evlog_attrs["fnames"] = {'Setup': 'Initializing iCloud3'} + self.evlog_attrs["filtername"] = 'Initialize' self.evlog_attrs["version_ic3"] = Gb.version self.evlog_attrs["version_evlog"] = Gb.version_evlog self.evlog_attrs["versionEvLog"] = Gb.version_evlog self.evlog_attrs["log_level_debug"] = '' self.evlog_attrs["run_mode"] = 'Initialize' self.evlog_attrs["evlog_btn_urls"] = self.evlog_btn_urls - self.evlog_attrs["user_message"] = self.user_message - self.evlog_attrs["update_time"] = '' - self.evlog_attrs["devicename"] = '' - self.evlog_attrs["fname"] = '' - self.evlog_attrs["fnames"] = {'Setup': 'Initializing iCloud3'} - self.evlog_attrs["filtername"] = 'Initialize' self.evlog_attrs["name"] = {"Browser Refresh is Required to load v3": "Browser Refresh is Required to load v3"} self.evlog_attrs["names"] = v2_v3_browser_refresh_msg @@ -185,17 +191,26 @@ def setup_event_log_trackable_device_info(self): self.evlog_attrs["name"] = '' self.evlog_attrs["names"] = '' - self.evlog_btn_urls['btnConfig'] = Gb.evlog_btnconfig_url - self.evlog_attrs["version_ic3"] = Gb.version - self.evlog_attrs["version_evlog"] = Gb.version_evlog - self.evlog_attrs["versionEvLog"] = Gb.version_evlog - self.evlog_attrs["run_mode"] = "Initialize" - self.evlog_attrs["evlog_btn_urls"] = self.evlog_btn_urls self.evlog_attrs["update_time"] = "setup" + self.evlog_attrs["alert"] = self.greenbar_alert_msg + + if Gb.start_icloud3_inprocess_flag is False: + self.evlog_attrs["alerts"] = self.alert_attr_filter() + self.evlog_attrs["alert_startup"] = "" + self.evlog_attrs["alert_tracked"] = "" + self.evlog_attrs["alert_monitored"]= "" + + self.evlog_attrs["user_message"] = self.user_message self.evlog_attrs["devicename"] = self.devicename self.evlog_attrs["fname"] = self.fname_selected self.evlog_attrs["fnames"] = self.fnames_by_devicename self.evlog_attrs["filtername"] = 'Initialize' + self.evlog_attrs["version_ic3"] = Gb.version + self.evlog_attrs["version_evlog"] = Gb.version_evlog + self.evlog_attrs["versionEvLog"] = Gb.version_evlog + self.evlog_attrs["run_mode"] = "Initialize" + self.evlog_btn_urls['btnConfig'] = Gb.evlog_btnconfig_url + self.evlog_attrs["evlog_btn_urls"] = self.evlog_btn_urls Gb.EvLogSensor.async_update_sensor() @@ -271,6 +286,7 @@ def post_event(self, devicename_or_Device, event_text='+'): if (v[ELR_DEVICENAME] == devicename \ and v[ELR_TEXT] == event_text \ and v[ELR_TEXT].startswith(EVLOG_TIME_RECD) is False + and v[ELR_TEXT].startswith(EVLOG_TRACE) is False and v[ELR_TEXT].startswith('Battery') is False)] if in_last_few_recds != []: return @@ -508,6 +524,8 @@ def update_evlog_sensor(self): self.evlog_attrs["update_time"] = self.log_update_time() self.evlog_attrs["user_message"] = self.user_message + self.evlog_attrs["alert"] = self.greenbar_alert_msg + self.evlog_attrs["alerts"] = self.alert_attr_filter() # Update EvLog sensor to display all log records if Gb.EvLogSensor: @@ -515,6 +533,20 @@ def update_evlog_sensor(self): self.last_refresh_secs = time_now_secs() +#------------------------------------------------------ + def alert_attr_filter(self, alert_msg=None): + if alert_msg is None: + alert_msg = self.greenbar_alert_msg + if alert_msg == '': + return '' + + alert_msg = alert_msg.replace(NBSP2, ' ') + alert_msg = alert_msg.replace(NBSP3, ' ') + alert_msg = alert_msg.replace(NBSP4, ' ') + alert_msg = alert_msg.replace(CRLF, NL) + + return alert_msg + #------------------------------------------------------ def _add_recd_to_event_recds(self, event_recd): """Add the event recd into the event table""" @@ -627,6 +659,7 @@ def _update_event_recds_device_cnt(self, elr_recd): #------------------------------------------------------ def clear_evlog_greenbar_msg(self): + self.greenbar_alert_msg = '' #------------------------------------------------------ @@ -670,8 +703,8 @@ def _extract_filtered_evlog_recds(self, devicename): the resulting list to be passed to the Event Log ''' if devicename == 'startup_log': - self.greenbar_alert_msg=(f"Start up log, alerts and èrrors are displayed" - f"{RARROW}Refresh to close") + self.greenbar_alert_msg=( f"Start up log, alerts and èrrors are displayed" + f"{RARROW}Refresh to close") el_recds = [el_recd[1:3] for el_recd in self.startup_event_recds if (el_recd[ELR_TEXT].startswith(EVLOG_MONITOR) is False or Gb.evlog_trk_monitors_flag)] @@ -770,18 +803,32 @@ def _apply_gps_filter(elr_time_text): @staticmethod def _apply_home_to_away_time_zone_update(elr_time_text, away_time_zone_offset): ''' - Change the Home zone time in the elr_text to the Away Zone time if needed + Change the Home zone time in the elr_text to the Away Zone time if needed. + A clock face 🕓 is added to the end of the 'Results >' in icloud3_main at the end of + the tracking update with the HA-Time: offset to show an Away Time Zone is being displayed. + Remove it from the event recd if it is being reset. Return [elr_time, elr_text] ''' - - if away_time_zone_offset == 0: return elr_time_text + if away_time_zone_offset == 0: + return elr_time_text elr_time, elr_text = elr_time_text + if away_time_zone_offset == 0: + if instr(elr_text, CLOCK_FACE): + elr_text = elr_text.split(CLOCK_FACE)[0] + return [elr_time, elr_text] + elr_time = adjust_time_hour_value(elr_time, away_time_zone_offset) if instr(elr_text, (':')): elr_text = adjust_time_hour_values(elr_text, away_time_zone_offset) + # Add a note that the Away Time is displayed + if elr_text.startswith(EVLOG_UPDATE_END): + plus_sign = '+' if away_time_zone_offset > 0 else '-' + elr_text += ( f"{NBSP4}{CLOCK_FACE} Time: " + f"{plus_sign}{abs(away_time_zone_offset)}hrs") + return [elr_time, elr_text] @@ -798,17 +845,17 @@ def export_event_log(self): try: log_update_time = (f"{dt_util.now().strftime('%a, %m/%d')}, " f"{dt_util.now().strftime(Gb.um_time_strfmt)}") - hdr_recd = f"Time{SP[8]}Event\n{'-'*120}\n" + hdr_recd = f"Time{SP(8)}Event\n{'-'*120}\n" export_recd = (f"iCloud3 Event Log v{Gb.version}\n\n" f"Log Update Time: {log_update_time}\n" f"Tracked Devices:\n") export_recd += f"\nGeneral Configuration:\n" - export_recd += f"{SP[4]}{Gb.conf_general}\n" + export_recd += f"{SP(4)}{Gb.conf_general}\n" for devicename, Device in Gb.Devices_by_devicename.items(): - export_recd += (f"{SP[4]}{DOT}{Device.fname_devicename} >\n" - f"{SP[4]}{Device.conf_device}\n") + export_recd += (f"{SP(4)}{DOT}{Device.fname_devicename} >\n" + f"{SP(4)}{Device.conf_device}\n") #-------------------------------- # # Prepare Global '*' records. Reverse the list elements using [::-1] and make a string of the results @@ -833,7 +880,7 @@ def export_event_log(self): if (len(el_recd) == 3 and el_recd[ELR_DEVICENAME] == devicename)] - export_recd += self._export_ic3_event_log_reformat_recds(devicename, el_recds) + export_recd += self._export_ic3_event_log_reformat_recds(Device.fname_devicename, el_recds) #-------------------------------- # # Prepare Global '*' records. Reverse the list elements using [::-1] and make a string of the results @@ -867,25 +914,42 @@ def _export_ic3_event_log_reformat_recds(self, log_section, el_recds): record_str = '' startup_recds_flag = False el_recds.reverse() + last_tfz_zone = '' for record in el_recds: devicename = record[ELR_DEVICENAME] - time = record[ELR_TIME] if record[ELR_TIME] not in ['Debug', 'Rawdata'] else SP[4] + time = record[ELR_TIME] if record[ELR_TIME] not in ['Debug', 'Rawdata'] else SP(4) text = record[ELR_TEXT] + # iCloud3 Startup Records if log_section == 'startup': if text[0:3] in [EVLOG_IC3_STARTING, EVLOG_IC3_STAGE_HDR]: - text = f"{SP[12]}{format_header_box(text[3:], evlog_export=True)}" - text = format_header_box_indent(text, -4) + text = f"{SP(9)}{format_header_box(text[3:], indent=12, evlog_export=True)}" elif text.startswith('^'): - text = f"{SP[4]}{filter_special_chars(text[3:], evlog_export=True)}" + text = f"{SP(4)}{filter_special_chars(text[3:], evlog_export=True)}" else: - text = f"{SP[4]}{filter_special_chars(text, evlog_export=True)}" + text = f"{SP(4)}{filter_special_chars(text, evlog_export=True)}" + # Non-device related Records elif log_section == 'other': - text = f" {filter_special_chars(text, evlog_export=True)}" + text = f"{filter_special_chars(text, evlog_export=True)}" + # Device Records else: - text = self._reformat_device_recd(time, text) + time = (time + SP(8))[:8] + text = self._reformat_device_recd(log_section, time, text) + + if time.startswith('»Home'): + pass + + # Start of tfz group header + elif time.startswith('»') and time != last_tfz_zone: + text = f"{SP(4)}⡇{' ~ '*18}\n{time}{text}" + last_tfz_zone = time + + # End of tfz group trailer + elif last_tfz_zone.startswith('»') and time != last_tfz_zone: + text = f"{last_tfz_zone}{SP(4)}⡇{' ~ '*18}\n{time}{text}" + time = last_tfz_zone = '' if text != '': record_str += f"{time}{text}\n" @@ -898,47 +962,40 @@ def _export_ic3_event_log_reformat_recds(self, log_section, el_recds): return '' #-------------------------------------------------------------------- - def _reformat_device_recd(self, time, text): + def _reformat_device_recd(self, log_section, time, text): # Time-record = {mobapp_state},{ic3_zone},{interval},{travel_time},{distance) - line_prefix = SP[4] + if text.startswith(EVLOG_UPDATE_START): - text = 'Start Tracking Update' if text[3:] == '' else text[3:] - text = f">{format_header_box(text, start_finish='start', evlog_export=True)}" - text = f"{format_header_box_indent(text, -4)}" + text = f"Tracking Update ({log_section})" if text[3:] == '' else text[3:] + text = f"{SP(5)}{format_header_box(text, indent=12, start_finish='start', evlog_export=True)}" return text elif text.startswith(EVLOG_UPDATE_END): - text = f">{format_header_box(text[3:], start_finish='finish', evlog_export=True)}" - _traceha(f"{text=}") - text = f"{format_header_box_indent(text, -4)}" - _traceha(f"{text=}") - line_prefix = '' + text = f"{SP(4)}{format_header_box(text[3:], indent=12, start_finish='finish', evlog_export=True)}" return text elif text.startswith(EVLOG_TIME_RECD): + tfz_adj = ' ' if (time.startswith('»') and time.startswith('»Home')) is False else '' text = text[3:] item = text.split(',') - text = (f"{' '*(12-len(time))}⡇ MobApp-{item[0]}, " + text = (f"{' '*(11-len(time))}{tfz_adj}⡇ " + f"MobApp-{item[0]}, " f"iCloud3-{item[1]}, " f"Interval-{item[2]}, " f"TravTime-{item[3]}, " f"Dist-{item[4]}") return text - if time.startswith('»'): line_prefix = SP[5] - + tfz_adj = ' ' if time.startswith('»') else '' group_char= '' if text.startswith('⡇') else \ '⡇ ' if Gb.trace_group else \ '' - text = filter_special_chars(text) - - if instr(text, 'Results:') and instr(text, 'From-Home') is False: - text = f"{text}\n{SP[12]}⡇ {' ~ '*18}" + text = filter_special_chars(text, evlog_export=True) - return f"{line_prefix}{group_char}{text}" + return f"{SP(4)}{tfz_adj}{group_char}{text}" #-------------------------------------------------------------------- @staticmethod diff --git a/custom_components/icloud3/support/icloud_data_handler.py b/custom_components/icloud3/support/icloud_data_handler.py index 633fb76..0a72b32 100644 --- a/custom_components/icloud3/support/icloud_data_handler.py +++ b/custom_components/icloud3/support/icloud_data_handler.py @@ -306,7 +306,7 @@ def update_device_with_latest_raw_data(Device, all_devices=False): except Exception as err: rawdata_msg = 'No Location data' if _RawData: - log_rawdata(f"{rawdata_msg}-{_Device.devicename}/{_Device.is_data_source_FAMSHR_FMF}", + log_rawdata(f"iCloud - {rawdata_msg}-{_Device.devicename}/{_Device.is_data_source_FAMSHR_FMF}", {'filter': _RawData.device_data}) continue # log_exception(err) diff --git a/custom_components/icloud3/support/mobapp_data_handler.py b/custom_components/icloud3/support/mobapp_data_handler.py index c34d988..5eb2b6a 100644 --- a/custom_components/icloud3/support/mobapp_data_handler.py +++ b/custom_components/icloud3/support/mobapp_data_handler.py @@ -9,6 +9,7 @@ LATITUDE, LONGITUDE, TIMESTAMP_SECS, TIMESTAMP_TIME, TRIGGER, LAST_ZONE, ZONE, GPS_ACCURACY, VERT_ACCURACY, ALTITUDE, + CONF_IC3_DEVICENAME, ) from ..helpers.common import (instr, is_statzone, is_zone, zone_dname, ) @@ -56,15 +57,22 @@ def check_mobapp_state_trigger_change(Device): mobapp_data_state = device_trkr_attrs[DEVICE_TRACKER] mobapp_data_state_secs = device_trkr_attrs[f"state_{TIMESTAMP_SECS}"] mobapp_data_state_time = device_trkr_attrs[f"state_{TIMESTAMP_TIME}"] + mobapp_data_state_time = device_trkr_attrs[f"state_{TIMESTAMP_TIME}"] + # State change will create enter/exit Zone trigger if Device.mobapp_data_state != mobapp_data_state: + mobapp_data_trigger = device_trkr_attrs["trigger"] = EXIT_ZONE \ + if mobapp_data_state == NOT_HOME else ENTER_ZONE + mobapp_data_trigger_secs = device_trkr_attrs[f"trigger_{TIMESTAMP_SECS}"] = mobapp_data_state_secs + mobapp_data_trigger_time = device_trkr_attrs[f"trigger_{TIMESTAMP_TIME}"] = mobapp_data_state_time + event_msg =(f"MobApp State Changed > " f"{zone_dname(Device.mobapp_data_state)}{RARROW}{zone_dname(mobapp_data_state)}") post_event(Device, event_msg) # Get the trigger data - entity_id = Device.mobapp[TRIGGER] - if entity_id: + elif Device.mobapp[TRIGGER]: + entity_id = Device.mobapp[TRIGGER] mobapp_data_trigger = device_trkr_attrs["trigger"] = entity_io.get_state(entity_id) mobapp_data_trigger_secs = device_trkr_attrs[f"trigger_{TIMESTAMP_SECS}"] = entity_io.get_last_changed_time(entity_id) mobapp_data_trigger_time = device_trkr_attrs[f"trigger_{TIMESTAMP_TIME}"] = secs_to_time(mobapp_data_trigger_secs) @@ -81,7 +89,7 @@ def check_mobapp_state_trigger_change(Device): Device.mobapp_data_trigger_time = mobapp_data_time = mobapp_data_trigger_time = Gb.this_update_time mobapp_data_change_flag = True - if mobapp_data_state_secs > mobapp_data_trigger_secs: + if mobapp_data_state_secs >= mobapp_data_trigger_secs: mobapp_data_from = f" (State), Trig-{mobapp_data_trigger_time}" mobapp_data_secs = device_trkr_attrs[TIMESTAMP_SECS] = mobapp_data_state_secs mobapp_data_time = device_trkr_attrs[TIMESTAMP_TIME] = mobapp_data_state_time @@ -437,6 +445,7 @@ def get_mobapp_device_trkr_entity_attrs(Device): f"It may be asleep, offline or not available.") return None + device_trkr_attrs[CONF_IC3_DEVICENAME] = Device.devicename device_trkr_attrs[f"state_{TIMESTAMP_SECS}"] = entity_io.get_last_changed_time(entity_id) device_trkr_attrs[f"state_{TIMESTAMP_TIME}"] = secs_to_time(device_trkr_attrs[f"state_{TIMESTAMP_SECS}"]) @@ -475,7 +484,7 @@ def update_mobapp_data_from_entity_attrs(Device, device_trkr_attrs): if Device.mobapp_data_secs >= mobapp_data_secs or gps_accuracy > Gb.gps_accuracy_threshold: return - log_rawdata(f"Mobile App - {Device.devicename}", device_trkr_attrs) + log_rawdata(f"MobApp Attrs - <{Device.devicename}>", device_trkr_attrs) Device.mobapp_data_state = device_trkr_attrs.get(DEVICE_TRACKER, NOT_SET) Device.mobapp_data_state_secs = device_trkr_attrs.get(f"state_{TIMESTAMP_SECS}", 0) diff --git a/custom_components/icloud3/support/mobapp_interface.py b/custom_components/icloud3/support/mobapp_interface.py index 30b25b5..a667b96 100644 --- a/custom_components/icloud3/support/mobapp_interface.py +++ b/custom_components/icloud3/support/mobapp_interface.py @@ -6,8 +6,8 @@ from ..helpers.common import (instr, list_add, ) from ..helpers.messaging import (post_event, post_error_msg, post_evlog_greenbar_msg, log_info_msg, log_exception, log_rawdata, _trace, _traceha, ) -from ..helpers.time_util import (secs_to_time, secs_since, secs_to_time, format_time_age, - format_timer, ) +from ..helpers.time_util import (secs_to_time, secs_since, mins_since, secs_to_time, format_time_age, + format_timer, time_now_secs) from homeassistant.helpers import entity_registry as er, device_registry as dr import json @@ -64,7 +64,7 @@ def get_entity_registry_mobile_app_devices(): f"{dev_trkr_entity['entity_id']}") post_evlog_greenbar_msg(alert_msg) - log_title = (f"mobapp entity_registry entry -- {mobapp_devicename})") + log_title = (f"MobApp entity_registry entry - <{mobapp_devicename}>)") log_rawdata(log_title, dev_trkr_entity, log_rawdata_flag=True) raw_model = 'Unknown' @@ -73,7 +73,7 @@ def get_entity_registry_mobile_app_devices(): # Get raw_model from HA device_registry device_reg_data = device_registry.async_get(device_id) - log_title = (f"mobapp device_registry entry -- {mobapp_devicename})") + log_title = (f"MobApp device_registry entry - <{mobapp_devicename}>)") log_rawdata(log_title, str(device_reg_data), log_rawdata_flag=True) raw_model = device_reg_data.model @@ -230,8 +230,7 @@ def send_message_to_device(Device, service_data): #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # # Using the mobapp tracking method or iCloud is disabled -# so trigger the osapp to send a -# location transaction +# Trigger the mobapp to send a location request transaction # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> def request_location(Device, is_alive_check=False, force_request=False): @@ -300,3 +299,16 @@ def request_location(Device, is_alive_check=False, force_request=False): error_msg = (f"iCloud3 Error > An error occurred sending a location request > " f"Device-{Device.fname_devicename}, Error-{err}") post_error_msg(devicename, error_msg) + +#----------------------------------------------------------------------------------------------------- +def request_sensor_update(Device): + ''' + Request the mobapp to update it's sensors + ''' + #if mins_since(Device.mobapp_request_sensor_update_secs) > 15: + Device.mobapp_request_sensor_update_secs = time_now_secs() + + message = {"message": "command_update_sensors"} + message_sent_ok = send_message_to_device(Device, message) + + return message_sent_ok diff --git a/custom_components/icloud3/support/pyicloud_ic3.py b/custom_components/icloud3/support/pyicloud_ic3.py index f775fa0..a24650d 100644 --- a/custom_components/icloud3/support/pyicloud_ic3.py +++ b/custom_components/icloud3/support/pyicloud_ic3.py @@ -28,7 +28,7 @@ ICLOUD_HORIZONTAL_ACCURACY, LOCATION, TIMESTAMP, LOCATION_TIME, DATA_SOURCE, ICLOUD_BATTERY_LEVEL, ICLOUD_BATTERY_STATUS, BATTERY_STATUS_CODES, - BATTERY_LEVEL, BATTERY_STATUS, + BATTERY_LEVEL, BATTERY_STATUS, BATTERY_LEVEL_LOW, ICLOUD_DEVICE_STATUS, CONF_PASSWORD, CONF_MODEL_DISPLAY_NAME, CONF_RAW_MODEL, CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX, @@ -40,7 +40,7 @@ secs_since, format_age ) from ..helpers.messaging import (post_event, post_monitor_msg, post_startup_alert, post_internal_error, _trace, _traceha, more_info, - log_info_msg, log_error_msg, log_debug_msg, log_warning_msg, log_rawdata, log_exception) + log_info_msg, log_error_msg, log_debug_msg, log_warning_msg, log_rawdata, log_exception, log_rawdata_unfiltered) from .config_file import (encode_password, decode_password) from uuid import uuid1 @@ -162,7 +162,7 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ if Gb.log_rawdata_flag: log_msg = (f"{secs_to_time(time_now_secs())}, {method}, {url}, {self.prefilter_rawdata(kwargs)}") - log_rawdata("PyiCloud_ic3 iCloud Request", {'raw': log_msg}) + log_rawdata(f"PyiCloud_ic3 iCloud Request ({obscure_field(self.Service.username)})", {'raw': log_msg}) try: response = None @@ -172,7 +172,7 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ #++++++++++++++++ REQUEST ICLOUD DATA +++++++++++++++ except Exception as err: - # log_exception(err) + log_exception(err) self._raise_error(-2, "Failed to establish a new connection") if response is None: return @@ -196,8 +196,8 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ try: if Gb.log_rawdata_flag_unfiltered: - log_rawdata("PyiCloud_ic3 iCloud Response-Header (Unfiltered)", {'raw': log_msg}) - log_rawdata("PyiCloud_ic3 iCloud Response-Data (Unfiltered)", {'raw': data}) + log_rawdata_unfiltered("PyiCloud_ic3 iCloud Response-Header (Unfiltered)", {'raw': log_msg}) + log_rawdata_unfiltered("PyiCloud_ic3 iCloud Response-Data (Unfiltered)", {'raw': data}) elif data and ('userInfo' in data is False or 'webservices' in data): log_rawdata("PyiCloud_ic3 iCloud Response-Data", {'filter': self.prefilter_rawdata(data)}) @@ -1181,15 +1181,18 @@ def famshr_devices(self): self.create_FamilySharing_object() #---------------------------------------------------------------------------- - def create_FamilySharing_object(self): + def create_FamilySharing_object(self, config_flow_login=False): ''' Initializes the Family Sharing object, refresh the iCloud device data for all devices and create the PyiCloud_RawData object containing the data for all locatible devices. + + config_flow_create indicates another iCloud acct is being logged into and a new FamShr object + should be created instead of using the existing FamShr object created when iC3 started ''' try: if self.FamilySharing is not None: return - if Gb.PyiCloud and Gb.PyiCloud.FamilySharing is not None: + if config_flow_login is False and Gb.PyiCloud and Gb.PyiCloud.FamilySharing is not None: self.PyiCloud = Gb.PyiCloud return @@ -1198,10 +1201,13 @@ def create_FamilySharing_object(self): self.Session, self.params, self.with_family) + return self.FamilySharing except Exception as err: log_exception(err) + return None + #---------------------------------------------------------------------------- @property def refresh_famshr_data(self): @@ -1294,11 +1300,11 @@ def __init__(self, PyiCloud, except Exception as err: log_exception(err) - if Gb.conf_data_source_FAMSHR is False: - self._set_service_available(False) - return + # if Gb.conf_data_source_FAMSHR is False: + # self._set_service_available(False) + # return - if self.is_service_not_available: + if self.is_service_not_available: log_msg = ( f"{EVLOG_ALERT}iCLOUD ALERT > Family Sharing Data Source is not available. " f"The web url providing location data returned a Service Not Available error " f"({self.PyiCloud.called_from})") @@ -1470,7 +1476,6 @@ def update_device_location_data(self, requested_by_devicename=None, devices_data # else: monitor_msg +=\ self._create_RawData_famshr_object(device_id, device_data_name, device_data) - continue # Non-tracked devices are not updated @@ -1745,19 +1750,21 @@ def __init__(self, PyiCloud, self.device_form_icloud_fmf_list = [] self.devices_without_location_data = [] - self.is_service_available = True - self.is_service_not_available = False - self._set_service_available(service_root is not None) + # self.is_service_available = True + # self.is_service_not_available = False + # self._set_service_available(service_root is not None) - if Gb.conf_data_source_FMF is False: - self._set_service_available(False) - return + # if Gb.conf_data_source_FMF is False: + # self._set_service_available(False) + # return + self.is_service_available = False + self.is_service_not_available = True if self.is_service_not_available: - log_msg = ( f"{EVLOG_ALERT}iCLOUD ALERT > Find-my-Friends Data Source is not available. " - f"The web url providing location data returned a Service Not Available error " - f"({self.PyiCloud.called_from})") - post_event(log_msg) + # log_msg = ( f"{EVLOG_ALERT}iCLOUD ALERT > Find-my-Friends Data Source is not available. " + # f"The web url providing location data returned a Service Not Available error " + # f"({self.PyiCloud.called_from})") + # post_event(log_msg) return self._friend_endpoint = f"{self._service_root}/fmipservice/client/fmfWeb/initClient" @@ -2113,7 +2120,8 @@ def __init__(self, device_id, self.fname_dup_suffix= '' # Suffix added to fname if duplicates self.evlog_alert_char= '' - # self.Device = Gb.Devices_by_icloud_device_id.get(device_id) + self.Device = Gb.Devices_by_icloud_device_id.get(device_id) + self.ic3_devicename = self.Device.devicename if self.Device else '' self.update_secs = time_now_secs() self.location_secs = 0 self.location_time = HHMMSS_ZERO @@ -2131,6 +2139,8 @@ def __init__(self, device_id, self.set_located_time_battery_info() self.device_data[DATA_SOURCE] = self.data_source + self.device_data[CONF_IC3_DEVICENAME] = self.ic3_devicename + #---------------------------------------------------------------------- @property @@ -2316,13 +2326,14 @@ def is_location_data_available(self): def set_located_time_battery_info(self): try: + self.device_data[CONF_IC3_DEVICENAME] = self.ic3_devicename + if self.is_location_data_available: self.device_data[LOCATION][TIMESTAMP] = int(self.device_data[LOCATION][self.timestamp_field] / 1000) self.device_data[LOCATION][LOCATION_TIME] = secs_to_time(self.device_data[LOCATION][TIMESTAMP]) self.location_secs = self.device_data[LOCATION][TIMESTAMP] self.location_time = self.device_data[LOCATION][LOCATION_TIME] else: - _trace self.device_data[LOCATION] = {self.timestamp_field: 0} self.device_data[LOCATION][TIMESTAMP] = 0 self.device_data[LOCATION][LOCATION_TIME] = HHMMSS_ZERO @@ -2352,13 +2363,18 @@ def set_located_time_battery_info(self): # Reformat and convert batteryStatus and batteryLevel try: - battery_status = self.device_data[ICLOUD_BATTERY_STATUS].lower() - self.battery_status = BATTERY_STATUS_CODES.get(battery_status, battery_status) self.battery_level = self.device_data.get(ICLOUD_BATTERY_LEVEL, 0) self.battery_level = round(self.battery_level*100) + self.battery_status = self.device_data[ICLOUD_BATTERY_STATUS] + if self.battery_level > 99: + self.battery_status = 'Charged' + elif self.battery_status in ['Charging', 'Unknown']: + pass + elif self.battery_level > 0 and self.battery_level < BATTERY_LEVEL_LOW: + self.battery_status = 'Low' - self.device_data[BATTERY_LEVEL] = self.battery_level - self.device_data[BATTERY_STATUS] = BATTERY_STATUS_CODES.get(battery_status, battery_status) + self.device_data[BATTERY_LEVEL] = self.battery_level + self.device_data[BATTERY_STATUS] = self.battery_status except: pass diff --git a/custom_components/icloud3/support/pyicloud_ic3_interface.py b/custom_components/icloud3/support/pyicloud_ic3_interface.py index 83ec68d..9366c48 100644 --- a/custom_components/icloud3/support/pyicloud_ic3_interface.py +++ b/custom_components/icloud3/support/pyicloud_ic3_interface.py @@ -5,6 +5,7 @@ CRLF, CRLF_DOT, DASH_20, ICLOUD, FAMSHR, SETTINGS_INTEGRATIONS_MSG, INTEGRATIONS_IC3_CONFIG_MSG, + CONF_USERNAME ) from ..support import start_ic3 as start_ic3 @@ -14,7 +15,7 @@ from ..helpers.common import (instr, list_to_str, delete_file, ) from ..helpers.messaging import (post_event, post_error_msg, post_monitor_msg, post_startup_alert, log_debug_msg, log_info_msg, log_exception, log_error_msg, internal_error_msg2, _trace, _traceha, ) -from ..helpers.time_util import (time_secs, secs_to_time, format_age, +from ..helpers.time_util import (time_now_secs, secs_to_time, format_age, format_time_age, ) import os @@ -62,7 +63,7 @@ def create_PyiCloudService(PyiCloud, called_from='unknown'): #-------------------------------------------------------------------- def verify_pyicloud_setup_status(): ''' - The PyiCloud Servicesinterface set up was started in __init__ + The PyiCloud Services interface set up was started in __init__ via create_PyiCloudService_executor_job above. The following steps are done to set up PyiCloudService: 1. Initialize the variables and authenticate the account. @@ -80,6 +81,14 @@ def verify_pyicloud_setup_status(): the PyiCloud session data requests must be run in the event loop. ''' + # The verify can be requested during started or after a restart request before + # the restart has begun + if (Gb.PyiCloudInit + and Gb.restart_icloud3_request_flag + and Gb.start_icloud3_inprocess_flag): + Gb.PyiCloudInit.init_step_needed = ['FamShr', 'FmF'] + Gb.PyiCloudInit.init_step_complete = ['Setup', 'Authenticate'] + init_step_needed = list_to_str(Gb.PyiCloudInit.init_step_needed) init_step_complete = list_to_str(Gb.PyiCloudInit.init_step_complete) @@ -137,7 +146,7 @@ def authenticate_icloud_account(PyiCloud, called_from='unknown', initial_setup=F this_fct_error_flag = True try: - Gb.pyicloud_auth_started_secs = time_secs() + Gb.pyicloud_auth_started_secs = time_now_secs() if PyiCloud and 'Complete' in Gb.PyiCloudInit.init_step_complete: PyiCloud.authenticate(refresh_session=True, service='find') @@ -212,9 +221,9 @@ def display_authentication_msg(PyiCloud): last_authenticated_time = Gb.authenticated_time # last_authenticated_time = last_authenticated_age = Gb.authenticated_time # if last_authenticated_time > 0: - # last_authenticated_age = time_secs() - last_authenticated_time + # last_authenticated_age = time_now_secs() - last_authenticated_time - Gb.authenticated_time = time_secs() + Gb.authenticated_time = time_now_secs() Gb.pyicloud_authentication_cnt += 1 event_msg =(f"iCloud Acct Auth " @@ -305,7 +314,6 @@ def new_2fa_authentication_code_requested(PyiCloud, initial_setup=False): elif Gb.restart_icloud3_request_flag: # via service call event_msg =("iCloud Restarting, Reset command issued") post_error_msg(event_msg) - Gb.restart_icloud3_request_flag = True if PyiCloud is None: event_msg =("iCloud Authentication Required, will retry") @@ -405,10 +413,17 @@ def create_PyiCloudService_secondary(username, password, authentication test routines. This is used by config_flow to open a second PyiCloud session ''' - return PyiCloudService( username, password, + PyiCloud = PyiCloudService( username, password, cookie_directory=Gb.icloud_cookies_dir, session_directory=(f"{Gb.icloud_cookies_dir}/session"), endpoint_suffix=endpoint_suffix, called_from=called_from, verify_password=verify_password, request_verification_code=request_verification_code) + + return PyiCloud + +def create_FamilySharing_secondary(PyiCloud, config_flow_login): + + FamShr = PyiCloud.create_FamilySharing_object(config_flow_login) + return FamShr diff --git a/custom_components/icloud3/support/restore_state.py b/custom_components/icloud3/support/restore_state.py index 54c50b4..6b3fc94 100644 --- a/custom_components/icloud3/support/restore_state.py +++ b/custom_components/icloud3/support/restore_state.py @@ -3,7 +3,7 @@ from ..global_variables import GlobalVariables as Gb from ..const import (RESTORE_STATE_FILE, DISTANCE_TO_OTHER_DEVICES, DISTANCE_TO_OTHER_DEVICES_DATETIME, - HHMMSS_ZERO, AWAY, AWAY_FROM, NOT_SET, NOT_HOME, STATIONARY, STATIONARY_FNAME, + HHMMSS_ZERO, AWAY, AWAY_FROM, NOT_SET, NOT_HOME, STATIONARY, STATIONARY_FNAME, ALERT, ZONE, ZONE_DNAME, ZONE_FNAME, ZONE_NAME, ZONE_INFO, LAST_ZONE, LAST_ZONE_DNAME, LAST_ZONE_FNAME, LAST_ZONE_NAME, DIR_OF_TRAVEL, ) @@ -77,8 +77,10 @@ def clear_devices(): #------------------------------------------------------------------------------------------- def read_storage_icloud3_restore_state_file(): ''' - Read the config/.storage/.icloud3.restore_state file and extract the - data into the Global Variables + Read the config/.storage/.icloud3.restore_state file. + - Extract the data into the Global Variables. + - Restoreeach device's sensors values + - Reinitialize sensors that should not be restored ''' try: @@ -91,6 +93,7 @@ def read_storage_icloud3_restore_state_file(): sensors = devicename_data['sensors'] sensors[DISTANCE_TO_OTHER_DEVICES] = {} sensors[DISTANCE_TO_OTHER_DEVICES_DATETIME] = HHMMSS_ZERO + sensors[ALERT] = '' _reset_statzone_values_to_away(sensors) diff --git a/custom_components/icloud3/support/service_handler.py b/custom_components/icloud3/support/service_handler.py index d870726..8560f48 100644 --- a/custom_components/icloud3/support/service_handler.py +++ b/custom_components/icloud3/support/service_handler.py @@ -30,8 +30,6 @@ _trace, _traceha, ) from ..helpers.time_util import (secs_to_time, time_str_to_secs, datetime_now, secs_since, time_now_secs, time_now, ) -# from ..config_flow import ActionSettingsFlowManager -# from .. import config_flow # EvLog Action Commands CMD_ERROR = 'error' @@ -275,9 +273,7 @@ def update_service_handler(action_entry=None, action_fname=None, devicename=None if action == f"{CMD_REFRESH_EVENT_LOG}+clear_evlog_greenbar_msgs": action = CMD_REFRESH_EVENT_LOG - clear_evlog_greenbar_msg() - clear_evlog_greenbar_msg() if (action == CMD_REFRESH_EVENT_LOG and Gb.EvLog.secs_since_refresh <= 2 and Gb.EvLog.last_refresh_devicename == devicename): @@ -321,7 +317,7 @@ def update_service_handler(action_entry=None, action_fname=None, devicename=None if action == CMD_PAUSE: if devicename is None: Gb.all_tracking_paused_flag = True - Gb.EvLog.display_user_message('Tracking is Paused', alert=True) + # Gb.EvLog.display_user_message('Tracking is Paused', alert=True) for Device in Devices: Device.pause_tracking() @@ -481,19 +477,16 @@ def _handle_action_device_locate(Device, action_option): _handle_action_device_location_mobapp(Device) return else: - post_event(Device, - "Mobile App Location Tracking is not available") + post_event(Device, "Mobile App Location Tracking is not available") if (Gb.primary_data_source_ICLOUD is False or (Device.device_id_famshr is None and Device.device_id_fmf is None) or Device.is_data_source_ICLOUD is False): - post_event(Device, - "iCloud Location Tracking is not available") + post_event(Device, "iCloud Location Tracking is not available") return elif Device.is_offline: - post_event(Device, - "The device is offline, iCloud Location Tracking is not available") + post_event(Device, "The device is offline, iCloud Location Tracking is not available") return try: @@ -503,6 +496,11 @@ def _handle_action_device_locate(Device, action_option): except: interval_secs = 5 + if Device.is_tracking_paused: + Gb.all_tracking_paused_flag = False + Gb.EvLog.display_user_message('', clear_evlog_greenbar_msg=True) + Device.resume_tracking() + Gb.icloud_force_update_flag = True Device.icloud_force_update_flag = True Device.reset_tracking_fields(interval_secs) diff --git a/custom_components/icloud3/support/start_ic3.py b/custom_components/icloud3/support/start_ic3.py index e4a234c..03b7139 100644 --- a/custom_components/icloud3/support/start_ic3.py +++ b/custom_components/icloud3/support/start_ic3.py @@ -37,7 +37,7 @@ CONF_TFZ_TRACKING_MAX_DISTANCE, CONF_TRACK_FROM_BASE_ZONE_USED, CONF_TRACK_FROM_BASE_ZONE, CONF_TRACK_FROM_HOME_ZONE, CONF_TRAVEL_TIME_FACTOR, CONF_PASSTHRU_ZONE_TIME, CONF_DISTANCE_BETWEEN_DEVICES, - CONF_LOG_LEVEL, + CONF_LOG_LEVEL, CONF_LOG_LEVEL_DEVICES, CONF_DISPLAY_ZONE_FORMAT, CONF_DEVICE_TRACKER_STATE_SOURCE, DEVICE_TRACKER_STATE_SOURCE_DESC, CONF_DISPLAY_GPS_LAT_LONG, CONF_CENTER_IN_ZONE, CONF_DISCARD_POOR_GPS_INZONE, @@ -85,7 +85,6 @@ import traceback from datetime import timedelta, date, datetime from collections import OrderedDict -from homeassistant.helpers.typing import ConfigType, EventType from homeassistant.helpers import event from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback from homeassistant.util import slugify @@ -122,7 +121,74 @@ def initialize_directory_filenames(): if not os.path.exists(Gb.icloud_cookies_dir): os.makedirs(Gb.icloud_cookies_dir) +#------------------------------------------------------------------------------ +# +# ICLOUD3 CONFIGURATION PARAMETERS WERE UPDATED VIA CONFIG_FLOW +# +# Determine the type of parameters that were updated and reset the variables or +# devices based on the type of changes. +# +#------------------------------------------------------------------------------ +def process_config_flow_parameter_updates(): + + if Gb.config_flow_updated_parms == {''}: + return + + # Make copy and Reinitialize so it will not be run again from the 5-secs loop + config_flow_updated_parms = Gb.config_flow_updated_parms + Gb.config_flow_updated_parms = {''} + + event_msg =(f"Configuration Loading > " + f"Type-{list_to_str(config_flow_updated_parms).title()}") + post_event(event_msg) + + if 'restart' in config_flow_updated_parms: + initialize_icloud_data_source() + Gb.restart_icloud3_request_flag = True + return + + post_event(f"{EVLOG_IC3_STAGE_HDR}") + if 'general' in config_flow_updated_parms: + set_global_variables_from_conf_parameters() + + if 'zone_formats' in config_flow_updated_parms: + set_zone_display_as() + + if 'evlog' in Gb.config_flow_updated_parms: + post_event('Processing Event Log Settings Update') + Gb.evlog_btnconfig_url = Gb.conf_profile[CONF_EVLOG_BTNCONFIG_URL].strip() + Gb.hass.loop.create_task(update_lovelace_resource_event_log_js_entry()) + check_ic3_event_log_file_version() + Gb.EvLog.setup_event_log_trackable_device_info() + + # stage_title = f'Configuration Changes Loaded' + # post_event(f"{EVLOG_IC3_STAGE_HDR}{stage_title}") + + if 'reauth' in config_flow_updated_parms: + Gb.evlog_action_request = CMD_RESET_PYICLOUD_SESSION + + if 'waze' in config_flow_updated_parms: + set_waze_conf_parameters() + if 'tracking' in config_flow_updated_parms: + post_event("Tracking parameters updated") + initialize_icloud_data_source() + + elif 'devices' in config_flow_updated_parms: + post_event("Device parameters updated") + initialize_icloud_data_source() + update_devices_non_tracked_fields() + Gb.EvLog.setup_event_log_trackable_device_info() + + _refresh_all_devices_sensors() + _display_all_devices_config_info() + + stage_title = f'Device Configuration Summary' + post_event(f"{EVLOG_IC3_STAGE_HDR}{stage_title}") + Gb.EvLog.display_user_message('') + + post_event(f"{EVLOG_IC3_STAGE_HDR}Configuration Changes Applied") + Gb.config_flow_updated_parms = {''} #------------------------------------------------------------------------------ # @@ -359,6 +425,7 @@ def set_global_variables_from_conf_parameters(evlog_msg=True): Gb.away_time_zone_2_devices = Gb.conf_general[CONF_AWAY_TIME_ZONE_2_DEVICES].copy() Gb.log_level = Gb.conf_general[CONF_LOG_LEVEL] + Gb.log_level_devices = Gb.conf_general[CONF_LOG_LEVEL_DEVICES] # update the interval time for each of the interval types (i.e., ipad: 2 hrs, no_mobapp: 15 min) inzone_intervals = Gb.conf_general[CONF_INZONE_INTERVALS] @@ -739,57 +806,6 @@ def set_zone_display_as(): log_msg = (f"Set up Zones > zone, Display ({Gb.display_zone_format})") post_event(f"{log_msg}{zone_msg}") -#------------------------------------------------------------------------------ -# -# ICLOUD3 CONFIGURATION PARAMETERS WERE UPDATED VIA CONFIG_FLOW -# -# Determine the type of parameters that were updated and reset the variables or -# devices based on the type of changes. -# -#------------------------------------------------------------------------------ -def process_config_flow_parameter_updates(): - - if Gb.config_flow_updated_parms == {''}: - return - - if 'restart' in Gb.config_flow_updated_parms: - Gb.config_flow_updated_parms = {''} - initialize_icloud_data_source() - Gb.restart_icloud3_request_flag = True - return - - post_event(f"{EVLOG_IC3_STAGE_HDR}") - if 'general' in Gb.config_flow_updated_parms: - set_global_variables_from_conf_parameters() - - if 'zone_formats' in Gb.config_flow_updated_parms: - set_zone_display_as() - - if 'evlog' in Gb.config_flow_updated_parms: - post_event('Processing Event Log Settings Update') - Gb.evlog_btnconfig_url = Gb.conf_profile[CONF_EVLOG_BTNCONFIG_URL].strip() - Gb.hass.loop.create_task(update_lovelace_resource_event_log_js_entry()) - check_ic3_event_log_file_version() - Gb.EvLog.setup_event_log_trackable_device_info() - - if 'reauth' in Gb.config_flow_updated_parms: - Gb.evlog_action_request = CMD_RESET_PYICLOUD_SESSION - - if 'waze' in Gb.config_flow_updated_parms: - set_waze_conf_parameters() - - if 'tracking' in Gb.config_flow_updated_parms: - post_event("Tracking parameters initialized") - initialize_icloud_data_source() - - elif 'devices' in Gb.config_flow_updated_parms: - post_event("Device parameters initialized") - initialize_icloud_data_source() - pass - - post_event(f"{EVLOG_IC3_STAGE_HDR}Configuration Changes Applied") - Gb.config_flow_updated_parms = {''} - #------------------------------------------------------------------------------ # # LOAD HA CONFIGURATION.YAML FILE @@ -1283,6 +1299,13 @@ def create_Devices_object(): return +#-------------------------------------------------------------------- +def update_devices_non_tracked_fields(): + for conf_device in Gb.conf_devices: + devicename = conf_device[CONF_IC3_DEVICENAME] + if Device := Gb.Devices_by_devicename.get(devicename): + Device.initialize_non_tracking_config_fields(conf_device) + #-------------------------------------------------------------------- def _verify_away_time_zone_devicenames(): ''' @@ -1434,8 +1457,8 @@ def _check_conf_famshr_devices_not_set_up(_FamShr): Gb.debug_log['_.devices_not_set_up'] = devices_not_set_up devices_not_set_up_str = list_to_str(devices_not_set_up, CRLF_X) - post_startup_alert( f"FamShr Device Config Error > " - f"Device Not found{devices_not_set_up_str.replace(CRLF_X, CRLF_HDOT)}") + post_startup_alert( f"FamShr Config Error > Device not found" + f"{devices_not_set_up_str.replace(CRLF_X, CRLF_DOT)}") log_msg = ( f"{EVLOG_ALERT}FAMSHR DEVICES ERROR > Your Apple iCloud Account Family Sharing List did " f"not return any information for some of configured devices. FamShr will not be used " f"to track these devices." @@ -1479,6 +1502,8 @@ def _display_devices_verification_status(PyiCloud, _FamShr): famshr_name = conf_device.get(CONF_FAMSHR_DEVICENAME) Gb.devicenames_x_famshr_devices[devicename] = famshr_name Gb.devicenames_x_famshr_devices[famshr_name] = devicename + _RawData.ic3_devicename = devicename + _RawData.Device = Gb.Devices_by_devicename.get(devicename) exception_msg = '' if devicename is None: @@ -1519,7 +1544,7 @@ def _display_devices_verification_status(PyiCloud, _FamShr): log_title = ( f"FamShr PyiCloud Data (device_data -- " f"{devicename}/{famshr_fname}), " f"{model_display_name} ({raw_model})") - log_rawdata(log_title, {'data': _RawData.device_data}) + log_rawdata(log_title, {'filter': _RawData.device_data}) # if devicename not in Gb.Devices_by_devicename: Device = Gb.Devices_by_devicename.get(devicename) @@ -1547,8 +1572,8 @@ def _display_devices_verification_status(PyiCloud, _FamShr): Gb.PairedDevices_by_paired_with_id[Device.paired_with_id] = [Device] Gb.Devices_by_icloud_device_id[device_id] = Device + Gb.famshr_device_verified_cnt += 1 - #ᗒ> event_msg += ( f"{CRLF_CHK}" f"{famshr_fname}, {model_display_name} ({raw_model}) >" @@ -2230,7 +2255,7 @@ def setup_tracked_devices_for_mobapp(): tracked_msg += (f"{crlf_sym}{mobapp_fname}, {mobapp_devicename} {mobapp_dev_type} >") if Device: Device.set_fname_alert(YELLOW_ALERT) - tracked_msg += (f"{CRLF_SP8_HDOT}{Device.devicename}, {Device.fname}") + tracked_msg += (f"{CRLF_SP8_HDOT}{Device.fname}, {Device.devicename}") tracked_msg += f"{CRLF_SP8_HDOT}{device_msg}{duplicate_msg}" post_event(tracked_msg) @@ -2267,7 +2292,8 @@ def _search_for_mobapp_device(devicename, Device, mobapp_id_by_mobapp_devicename elif len(matched_mobapp_devices) > 1: mobapp_devicename = matched_mobapp_devices[-1] - post_startup_alert(f"Mobile App device_tracker entity scan found several devices: {Device.fname_devicename}") + post_startup_alert( f"Mobile App device_tracker entity scan found several devices: " + f"{Device.fname_devicename}") alert_msg =(f"{EVLOG_ALERT}DUPLICATE MOBAPP DEVICES FOUND > More than one Device Tracker Entity " f"was found during the scan of the HA Device Registry." @@ -2279,7 +2305,7 @@ def _search_for_mobapp_device(devicename, Device, mobapp_id_by_mobapp_devicename f"{CRLF}Monitored-{mobapp_devicename}") post_event(alert_msg) - log_error_msg(f"iCloud3 Error > Mobile App Device Config Error > Dev Trkr Entity not found > " + log_error_msg(f"iCloud3 Error > Mobile App Config Error > Dev Trkr Entity not found " f"during Search {conf_mobapp_device}_???. " f"See iCloud3 Event Log > Startup Stage 4 for more info.") return mobapp_devicename @@ -2293,8 +2319,8 @@ def _display_any_mobapp_errors( mobapp_error_mobile_app_msg, mobapp_error_not_found_msg): if mobapp_error_mobile_app_msg: - post_startup_alert( f"Mobile App Integration (Mobile App) is missing trigger or battery " - f"sensors: {mobapp_error_mobile_app_msg}") + post_startup_alert( f"Mobile App Integration missing trigger or battery sensors" + f"{mobapp_error_mobile_app_msg.replace(CRLF_X, CRLF_DOT)}") alert_msg =(f"{EVLOG_ALERT}MOBILE APP INTEGRATION (Mobile App) SET UP PROBLEM > The Mobile App " f"Integration `last_update_trigger` and `battery_level` sensors are disabled or " @@ -2310,8 +2336,8 @@ def _display_any_mobapp_errors( mobapp_error_mobile_app_msg, f"{mobapp_error_mobile_app_msg}") if mobapp_error_search_msg: - post_startup_alert( f"Mobile App Device Config Error > Device Tracker Entity was not found:" - f"{mobapp_error_search_msg}") + post_startup_alert( f"Mobile App Config Error > Device Tracker Entity not found" + f"{mobapp_error_search_msg.replace(CRLF_X, CRLF_DOT)}") alert_msg =(f"{EVLOG_ALERT}MOBAPP DEVICE NOT FOUND > An MobApp device_tracker " f"entity was not found in the `Scan for mobile_app devices` in the HA Device Registry." @@ -2319,14 +2345,14 @@ def _display_any_mobapp_errors( mobapp_error_mobile_app_msg, f"{more_info('mobapp_error_search_msg')}") post_event(alert_msg) - log_error_msg(f"iCloud3 Error > Mobile App Device Tracker Entity was not found " + log_error_msg(f"iCloud3 Error > Mobile App Device Tracker Entity not found " f"in the HA Devices List." f"{mobapp_error_search_msg}. " f"See iCloud3 Event Log > Startup Stage 4 for more info.") if mobapp_error_not_found_msg: - post_startup_alert( f"Mobile App Device Config Error > Device Tracker Entity was " - f"not found: {mobapp_error_not_found_msg}") + post_startup_alert( f"Mobile App Config Error > Device Tracker Entity not found" + f"{mobapp_error_not_found_msg.replace(CRLF_X, CRLF_DOT)}") alert_msg =(f"{EVLOG_ALERT}MOBAPP DEVICE NOT FOUND > The device tracker entity " f"was not found during the scan of the HA Device Registry." @@ -2340,8 +2366,8 @@ def _display_any_mobapp_errors( mobapp_error_mobile_app_msg, f"See iCloud3 Event Log > Startup Stage 4 for more info.") if mobapp_error_disabled_msg: - post_startup_alert( f"Mobile App Device Config Error > Device Tracker Entity was " - f"disabled {mobapp_error_disabled_msg}") + post_startup_alert( f"Mobile App Config Error > Device Tracker Entity disabled" + f"{mobapp_error_disabled_msg.replace(CRLF_X, CRLF_DOT)}") alert_msg =(f"{EVLOG_ALERT}MOBILE APP DEVICE DISABLED > The device tracker entity " f"is disabled so the mobile_app last_update_trigger and bettery_level sensors " @@ -2475,6 +2501,14 @@ def identify_tracked_monitored_devices(): Gb.debug_log['Gb.Devices_by_devicename_tracked'] = Gb.Devices_by_devicename_tracked Gb.debug_log['Gb.Devices_by_devicename_monitored'] = Gb.Devices_by_devicename_monitored +#------------------------------------------------------------------------------ +def _refresh_all_devices_sensors(): + ''' + Rewrite all device sensors in case anything changed diuring config update + ''' + for Device in Gb.Devices: + Device.write_ha_sensors_state() + #------------------------------------------------------------------------------ def setup_trackable_devices(): ''' @@ -2492,6 +2526,35 @@ def setup_trackable_devices(): except: pass + _display_all_devices_config_info() + + # Initialize distance_to_other_devices, add other devicenames to this Device's field + for devicename, Device in Gb.Devices_by_devicename_tracked.items(): + for _devicename, _Device in Gb.Devices_by_devicename.items(): + if devicename != _devicename: + Device.dist_to_other_devices[_devicename] = [0, 0, 0, '0m/00:00:00'] + Device.near_device_distance = 0 # Distance to the NearDevice device + Device.near_device_checked_secs = 0 # When the nearby devices were last updated + Device.dist_apart_msg = '' # Distance to all other devices msg set in icloud3_main + Device.dist_apart_msg_by_devicename = {} + + if Device.mobapp_monitor_flag: + Gb.used_data_source_MOBAPP = True + mobapp_attrs = mobapp_data_handler.get_mobapp_device_trkr_entity_attrs(Device) + if mobapp_attrs: + mobapp_data_handler.update_mobapp_data_from_entity_attrs(Device, mobapp_attrs) + + if Gb.primary_data_source_ICLOUD is False: + if Gb.conf_data_source_ICLOUD: + post_event("iCloud Location Tracking is not available") + else: + post_event("iCloud Location Tracking is not being used") + + if Gb.conf_data_source_MOBAPP is False: + post_event("Mobile App Location Tracking is not being used") + +#------------------------------------------------------------------------------ +def _display_all_devices_config_info(): for devicename, Device in Gb.Devices_by_devicename.items(): Device.display_info_msg(f"Set Trackable Devices > {devicename}") if Device.verified_flag: @@ -2571,31 +2634,6 @@ def setup_trackable_devices(): post_event(event_msg) - # Initialize distance_to_other_devices, add other devicenames to this Device's field - for device, Device in Gb.Devices_by_devicename_tracked.items(): - for _devicename, _Device in Gb.Devices_by_devicename.items(): - if devicename != _devicename: - Device.dist_to_other_devices[_devicename] = [0, 0, 0, '0m/00:00:00'] - Device.near_device_distance = 0 # Distance to the NearDevice device - Device.near_device_checked_secs = 0 # When the nearby devices were last updated - Device.dist_apart_msg = '' # Distance to all other devices msg set in icloud3_main - Device.dist_apart_msg_by_devicename = {} - - if Device.mobapp_monitor_flag: - Gb.used_data_source_MOBAPP = True - mobapp_attrs = mobapp_data_handler.get_mobapp_device_trkr_entity_attrs(Device) - if mobapp_attrs: - mobapp_data_handler.update_mobapp_data_from_entity_attrs(Device, mobapp_attrs) - - if Gb.primary_data_source_ICLOUD is False: - if Gb.conf_data_source_ICLOUD: - post_event("iCloud Location Tracking is not available") - else: - post_event("iCloud Location Tracking is not being used") - - if Gb.conf_data_source_MOBAPP is False: - post_event("Mobile App Location Tracking is not being used") - #------------------------------------------------------------------------------ def display_inactive_devices(): ''' @@ -2613,7 +2651,7 @@ def display_inactive_devices(): return event_msg = f"Inactive/Untracked Devices > " - event_msg+= list_to_str(inactive_devices, separator=CRLF_X) + event_msg+= list_to_str(inactive_devices, separator=CRLF_DOT) post_event(event_msg) if len(inactive_devices) == len(Gb.conf_devices): diff --git a/custom_components/icloud3/support/start_ic3_control.py b/custom_components/icloud3/support/start_ic3_control.py index 48ab189..19ac422 100644 --- a/custom_components/icloud3/support/start_ic3_control.py +++ b/custom_components/icloud3/support/start_ic3_control.py @@ -1,9 +1,10 @@ from ..global_variables import GlobalVariables as Gb from ..const import (NOT_SET, IC3LOG_FILENAME, - NEW_LINE, CRLF, CRLF_DOT, + CRLF, CRLF_DOT, CRLF_HDOT, CRLF_X, NL, NL_DOT, EVLOG_ALERT, EVLOG_IC3_STARTING, EVLOG_IC3_STAGE_HDR, SETTINGS_INTEGRATIONS_MSG, INTEGRATIONS_IC3_CONFIG_MSG, CONF_VERSION, ICLOUD_FNAME, ZONE_DISTANCE, + FAMSHR_FNAME, FMF_FNAME, MOBAPP_FNAME, ) from ..support import start_ic3 @@ -42,16 +43,16 @@ def stage_1_setup_variables(): broadcast_info_msg(stage_title) #check to see if restart is in process - if Gb.start_icloud3_inprocess_flag: - return + #if Gb.start_icloud3_inprocess_flag: + # return Gb.EvLog.display_user_message(f'iCloud3 v{Gb.version} > Initializiing') try: + # Gb.start_icloud3_inprocess_flag = True + # Gb.restart_icloud3_request_flag = False + # Gb.all_tracking_paused_flag = False Gb.this_update_secs = time_now_secs() - Gb.start_icloud3_inprocess_flag = True - Gb.restart_icloud3_request_flag = False - Gb.all_tracking_paused_flag = False Gb.startup_alerts = [] Gb.EvLog.alert_message = '' Gb.config_track_devices_change_flag = False @@ -63,10 +64,11 @@ def stage_1_setup_variables(): Gb.EvLog.startup_event_save_recd_flag = True post_event( f"{EVLOG_IC3_STARTING}iCloud3 v{Gb.version} > Restarting, " f"{dt_util.now().strftime('%A, %b %d')}") - config_file.load_storage_icloud3_configuration_file() - write_config_file_to_ic3log() - start_ic3.initialize_global_variables() - start_ic3.set_global_variables_from_conf_parameters() + + config_file.load_storage_icloud3_configuration_file() + write_config_file_to_ic3log() + start_ic3.initialize_global_variables() + start_ic3.set_global_variables_from_conf_parameters() start_ic3.define_tracking_control_fields() @@ -157,6 +159,13 @@ def stage_3_setup_configured_devices(): broadcast_info_msg(stage_title) # Make sure a full restart is done if all of the devices were not found in the iCloud data + data_sources = '' + if Gb.conf_data_source_FAMSHR: data_sources += f"{FAMSHR_FNAME}, " + if Gb.conf_data_source_FMF : data_sources += f"{FMF_FNAME}, " + if Gb.conf_data_source_MOBAPP: data_sources += f"{MOBAPP_FNAME}, " + data_sources = data_sources[:-2] if data_sources else 'NONE' + post_event(f"Data Sources > {data_sources}") + if Gb.config_track_devices_change_flag: pass elif (Gb.conf_data_source_FMF @@ -283,7 +292,7 @@ def _are_all_devices_verified(retry=False): def stage_5_configure_tracked_devices(): Gb.trace_prefix = 'STAGE5' - stage_title = f'Stage 5 > Tracked Devices Configuration Summary' + stage_title = f'Stage 5 > Device Configuration Summary' log_info_msg(f"* > {EVLOG_IC3_STAGE_HDR}{stage_title}") try: @@ -323,15 +332,23 @@ def stage_6_initialization_complete(): try: start_ic3.display_object_lists() - item_no = 1 - if Gb.startup_alerts != []: - Gb.EvLog.alert_message = 'Problems occured during startup up that should be reviewed' - alert_msg = (f"{EVLOG_ALERT}The following issues were detected when starting iCloud3. " - f"Scroll through the Startup Log for more information:") - + if Gb.startup_alerts: + item_no = 1 + alert_msg = '' for alert in Gb.startup_alerts: alert_msg += f"{CRLF}{item_no}. {alert}" item_no += 1 + + # Build alert msg for the evlog.attrs['alert_startup'] attribute for display + alerts_str = alert_msg.replace(CRLF_HDOT, NL_DOT) + alerts_str = alerts_str.replace(CRLF_X, NL_DOT) + alerts_str = alerts_str.replace(CRLF, NL) + Gb.startup_alerts_str = alerts_str + + Gb.EvLog.alert_message = 'Problems occured during startup up that should be reviewed' + alert_msg = (f"{EVLOG_ALERT}The following issues were detected when starting iCloud3. " + f"Scroll through the Startup Log for more information: " + f"{alert_msg}") post_event(alert_msg) except Exception as err: @@ -378,7 +395,9 @@ def stage_7_initial_locate(): else: continue - post_event(Device, 'Trigger > Initial Locate') + event_msg =(f"{Device.dev_data_source} Trigger > Initial Locate@" + f"{Device.loc_data_time_gps}") + post_event(Device, event_msg) if Device.no_location_data: event_msg = f"{EVLOG_ALERT}NO GPS DATA RETURNED FROM ICLOUD LOCATION SERVICE" diff --git a/custom_components/icloud3/support/stationary_zone.py b/custom_components/icloud3/support/stationary_zone.py index 98fa3bc..b901964 100644 --- a/custom_components/icloud3/support/stationary_zone.py +++ b/custom_components/icloud3/support/stationary_zone.py @@ -80,7 +80,7 @@ def move_device_into_statzone(Device): and Zone.distance_m(latitude, longitude) <= Zone.radius_m)] if available_zones != []: - _clear_statzone_timer_distance(Device) + clear_statzone_timer_distance(Device) return False if _is_too_close_to_another_zone(Device): return False @@ -101,7 +101,7 @@ def move_device_into_statzone(Device): event_msg = f"Created Stationary Zone > {StatZone.fname_id}, SetupBy-{Device.fname}" post_event(event_msg) - _clear_statzone_timer_distance(Device, create_statzone_flag=True) + clear_statzone_timer_distance(Device, create_statzone_flag=True) StatZone.attrs[LATITUDE] = latitude StatZone.attrs[LONGITUDE] = longitude @@ -231,7 +231,7 @@ def move_statzone_to_device_location(Device, latitude=None, longitude=None): if _is_too_close_to_another_zone(Device): return - _clear_statzone_timer_distance(Device) + clear_statzone_timer_distance(Device) StatZone.attrs[LATITUDE] = latitude StatZone.attrs[LONGITUDE] = longitude @@ -258,7 +258,7 @@ def remove_statzone(StatZone, Device=None): Gb.StatZones_to_delete.append(StatZone) if Device: - _clear_statzone_timer_distance(Device) + clear_statzone_timer_distance(Device) event_msg =(f"Exited Stationary Zone > {StatZone.dname}, " f"DevicesRemaining-{devices_in_statzone_count(StatZone)}") post_event(Device, event_msg) @@ -335,7 +335,6 @@ def _is_too_close_to_another_zone(Device): Device.loc_data_gps_accuracy))] if CloseZones == []: return False - CloseZone = CloseZones[0] if is_statzone(CloseZone.zone): log_msg = ( f"{Device.devicename} > StatZone not created, too close to " @@ -346,7 +345,8 @@ def _is_too_close_to_another_zone(Device): return True #-------------------------------------------------------------------- -def _clear_statzone_timer_distance(Device, create_statzone_flag=False): +def clear_statzone_timer_distance(Device, create_statzone_flag=False): + Device.statzone_timer = 0 - Device.statzone_moved_dist = 0 + Device.statzone_moved_dist = 0.0 Device.statzone_setup_secs = Gb.this_update_secs if create_statzone_flag else 0 \ No newline at end of file diff --git a/custom_components/icloud3/support/zone_handler.py b/custom_components/icloud3/support/zone_handler.py index 5eed3bd..d47fa70 100644 --- a/custom_components/icloud3/support/zone_handler.py +++ b/custom_components/icloud3/support/zone_handler.py @@ -12,7 +12,6 @@ import os import homeassistant.util.dt as dt_util -from homeassistant.helpers.typing import EventType from homeassistant.helpers import event from homeassistant.core import callback @@ -205,6 +204,7 @@ def select_zone(Device, latitude=None, longitude=None): zones_distance_list = \ [(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] + zones_distance_list.sort() return ZoneSelected, zone_selected, zone_selected_dist_m, zones_distance_list @@ -225,7 +225,6 @@ def post_zone_selected_msg(Device, ZoneSelected, zone_selected, # Format distance msg zones_dist_msg = '' zones_displayed = [zone_selected] - zones_distance_list.sort() for zone_distance_list in zones_distance_list: zdl_items = zone_distance_list.split('|') _zone = zdl_items[1] @@ -233,7 +232,6 @@ def post_zone_selected_msg(Device, ZoneSelected, zone_selected, zones_dist_msg += ( f"{zone_dname(_zone)}" f"-{m_to_um(_zone_dist)}") - # zones_dist_msg += f"-r{int(Gb.Zones_by_zone[_zone].radius_m)}m" zones_dist_msg += ", " gps_accuracy_msg = '' @@ -468,8 +466,7 @@ def request_update_devices_no_mobapp_same_zone_on_exit(Device): #------------------------------------------------------------------------------ @callback -#def _async_add_zone_entity_id(event: EventType[event.EventStateChangedData]) -> None: -def ha_added_zone_entity_id(event: EventType[event.EventStateChangedData]) -> None: +def ha_added_zone_entity_id(event): """Add zone entity ID.""" zone_entity_id = event.data['entity_id'] @@ -490,8 +487,7 @@ def ha_added_zone_entity_id(event: EventType[event.EventStateChangedData]) -> No #------------------------------------------------------------------------------ @callback -#def _async_remove_zone_entity_id(event: EventType[event.EventStateChangedData]) -> None: -def ha_removed_zone_entity_id(event: EventType[event.EventStateChangedData]) -> None: +def ha_removed_zone_entity_id(event): """Remove zone entity ID.""" try: zone_entity_id = event.data['entity_id'] @@ -499,8 +495,7 @@ def ha_removed_zone_entity_id(event: EventType[event.EventStateChangedData]) -> if (zone == HOME or zone not in Gb.HAZones_by_zone - or Gb.start_icloud3_inprocess_flag - or Gb.restart_icloud3_request_flag): + or Gb.start_icloud3_inprocess_flag): return Zone = Gb.HAZones_by_zone[zone] diff --git a/custom_components/icloud3/translations/en.json b/custom_components/icloud3/translations/en.json index 7fe4f17..94183a1 100644 --- a/custom_components/icloud3/translations/en.json +++ b/custom_components/icloud3/translations/en.json @@ -60,16 +60,18 @@ "conf_updated": "iCloud3 Configuration Parameters were updated successfully", "conf_reloaded": "iCloud3 Configuration File was Reloaded", "icloud_acct_logging_into": "Logging into iCloud Account", - "icloud_acct_logged_into": "Successfully Logged into the iCloud Account", + "icloud_acct_logged_into": "Logged into the new iCloud Account. Select SAVE to save the changes and restart iCloud3", "icloud_acct_already_logged_into": "Already Logged into the iCloud Account", - "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_login_error_user_pw": "Login Error, Invalid Username or Password", + "icloud_acct_login_error_other": "Login Error, Other Error or iCloud is not Available", + "icloud_acct_login_error_connection": "Login Error, Failed to Connect to iCloud Server", + "icloud_acct_username_password_error": "Entry Error, Invalid Username or Password", + "icloud_acct_not_available": "Login Failed, iCloud Account is not Available", + "icloud_acct_not_logged_into": "Warning: iCloud Account is not Logged Into", + "icloud_acct_data_source_warning": "Warning: iCloud Account is not selected as a data source but username/password is setup", "icloud_acct_not_set_up": "iCloud Account Username or Password needs to be entered", "icloud_acct_no_data_source": "No Data Source (iCloud or Mobile App) has been selected", - "mobile_app_error": "The Mobile App Integration is not installed. The Mobile App will not be used as a data source; location data and zone enter/exit triggers will not be monitored", + "mobile_app_error": "Error, The Mobile App Integration is not installed. The Mobile App will not be used as a data source; location data and zone enter/exit triggers will not be monitored", "verification_code_requested": "The Apple ID Verification Code was requested, BROWSER REFRESH MAY BE NEEDED", "verification_code_requested2": "The Apple ID Verification Code was requested", @@ -278,10 +280,10 @@ "title": "Display Location Time Zone when Away", "description": "The time displayed in the Event Log and Sensors show the time an event took place using the Home 'time zone' from your Home Assistant computer. When you are away from Home and in another time zone, your tracking events are still based on the time at your Home 'time zone', not time in your current location.\n\nThis screen lets you display time events using your current location's time zone.", "data": { - "away_time_zone_1_devices": "Devices that will display time events based on this location time", - "away_time_zone_1_offset": "Current Location Time", - "away_time_zone_2_devices": "Devices that will display time events based on this location time", - "away_time_zone_2_offset": "Current Location Time", + "away_time_zone_1_devices": "Devices in Away Time Zone #1", + "away_time_zone_1_offset": "Time & Time Zone Adjustment at Current Location #1", + "away_time_zone_2_devices": "Devices in Away Time Zone #2", + "away_time_zone_2_offset": "Time & Time Zone Adjustment at Current Location #2", "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" }, "data_description": { @@ -294,6 +296,7 @@ "description": "Tracking activity, results and information messages are displayed in the Event log, sensors and device_tracker entities for tracked and monitored devices.\n\nThis screen us used to specify how these results should be displayed.", "data": { "log_level": "LOG LEVEL - The type of messages that are added to the HA log file by iCloud3", + "log_level_devices": "LOG LEVEL RAWDATA DEVICES FILTER - Dump rawdata for only these devices to log file", "display_zone_format": "EVENT LOG ZONE DISPLAY NAME - How the Zone name is displayed in sensors and the Event Log", "device_tracker_state_source": "DEVICE TRACKER STATE VALUE - How the device's device_tracker entity state value is determined", "time_format": "TIME FORMAT - How time fields are displayed in sensors and in the Event Log",