diff --git a/README.md b/README.md index 0bdb508..9ebddda 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ ------ -[![CurrentVersion](https://img.shields.io/badge/Current_Version-v3.0.5-blue.svg)](https://github.com/gcobb321/icloud3) [![Type](https://img.shields.io/badge/Type-Custom_Component-orange.svg)](https://github.com/gcobb321/icloud3) [![HACS](https://img.shields.io/badge/HACS-Standard_Repository-orange.svg)](https://github.com/gcobb321/icloud3) +[![CurrentVersion](https://img.shields.io/badge/Current_Version-v3.1-blue.svg)](https://github.com/gcobb321/icloud3) [![Type](https://img.shields.io/badge/Type-Custom_Component-orange.svg)](https://github.com/gcobb321/icloud3) [![HACS](https://img.shields.io/badge/HACS-Standard_Repository-orange.svg)](https://github.com/gcobb321/icloud3) -[![ProjectStage](https://img.shields.io/badge/Project_Stage-General_Availability-forestgreen.svg)](https://github/gcobb321/icloud3) [![Released](https://img.shields.io/badge/Released-June,_2024-forestgreen.svg)](https://github.com/gcobb321/icloud3) +[![ProjectStage](https://img.shields.io/badge/Project_Stage-General_Availability-forestgreen.svg)](https://github/gcobb321/icloud3) [![Released](https://img.shields.io/badge/Released-November,_2024-forestgreen.svg)](https://github.com/gcobb321/icloud3) @@ -21,7 +21,8 @@ Although Home Assistant has it's own official iCloud component, iCloud3 goes far - **HA Integration** - iCloud3 is a Home Assistant custom integration that is set up and configured from the *HA Settings > Devices & Services > Integrations* screen. - **Configuration Settings** - Configuration parameters are updated online using various screens and take effect immediately without restarting HA. - **Track iPhones, iPads and Apple Watches** - Track or monitor your iDevices. -- **Location data sources** - Location data comes from the iCloud Account and the HA Companion App (Mobile App). +- **Location data sources** - Location data comes from the Apple Account and the HA Companion App (Mobile App). +- **Multiple Apple Accounts** - Devices from several Apple Accounts can be tracked (yours, your spouse, children, friend, relative, etc.) - **Actively track a device** - The device will request it's location on a regular interval based on its distance from Home or another zone. - **Passively monitor a device** - The device does not request it's location. It is updated when another tracked device requests theirs. - **Waze Route Service** - The travel time and distance to Home or another tracked zone is provided by Waze. @@ -56,7 +57,7 @@ The screens below are an example of how the many tracking sensors can be display ### iCloud3 Documentation - Introduces the many features and components of iCloud3 -- Describes how to migration from v2.4.7 to v3.0 +- Describes how to migration from v2.4.7 to v3.1 - Provides step-by-step to install and configure iCloud3, it's components and it's supporting components (iCloud Account and the Mobile App) - Highlights the configuration screens and parameters - Provides example screens, automations and scripts diff --git a/custom_components/icloud3/ChangeLog.txt b/custom_components/icloud3/ChangeLog.txt index 966cf0a..7a90fc3 100644 --- a/custom_components/icloud3/ChangeLog.txt +++ b/custom_components/icloud3/ChangeLog.txt @@ -3,44 +3,26 @@ **Installing for the first time_** - See [here](https://gcobb321.github.io/icloud3_v3_docs/#/chapters/3.2-installing-and-configuring) for instructions on installing as a New Installation **iCloud3 v3 Documentation** - iCloud3 User Guide can be found [here](https://gcobb321.github.io/icloud3_v3_docs/#/) - -3.0.5.9 -### Change Log - v3.0.5.9 (9/9/2024) -1. CONFIGURATION - UPDATE DEVICES (Fixed) - An error would occur on the _Configure > Update Devices_ screen if the Mobile App Integration had not been set up or if the name had not been set up on one of the Mobile App devices was missing. This occured predominately when adding the first device to iCloud3. -2. DEVICE_TRACKER ATTRIBUTES (Fix/Update) - There are 3 sections on thedevice attribute's list to group similar attributes together. A title has been added to the section's dividing lines to prevent problems with other AddOns that use the attribute's name. Corrected a spelling error on one of the attribute names created in 3.0.5.8. - -3.0.5.8 -....................... -### Change Log - v3.0.5.8 (9/8/2024) -1. CONFIGURATION - UPDATE DEVICES (Fixed) - An error would occur on the _Configure > Update Devices_ screen if the Mobile App Integration had not been set up or if the name had not been set up on one of the Mobile App devices was missing. This occured predominately when adding the first device to iCloud3. -2. DEVICE_TRACKER ATTRIBUTES (Update) - There are 3 sections on thedevice attribute's list to group similar attributes together. A title has been added to the section's dividing lines to prevent problems with other AddOns that use the attribute's name. - - -3.0.5.7 -....................... -### Change Log - v3.0.5.6 (7/15/2024) -1. ICLOUD3 PROBLEMS WITH HA 2024.7.4 - Fixed -2. ADD/UPDATE DEVICE CONFIGURATION (Fixed) - This was probably caused by HA 2024.7.4 Loading issues. -3. MOBILE APP NOTIFY MESSAGE (Fixed) - A warning message about not being able to send a notification to a device was displayed in the Event Log when the device was not using the Mobile App. - - -3.0.5.6 -....................... -### Change Log - v3.0.5.6 (7/15/2024) -1. ICLOUD3 BUG FIXES - Fixes the following errors: - ```AttributeError: 'NoneType' object has no attribute 'init_step_complete'``` - ```AttributeError: module 'custom_components.icloud3.sensor' has no attribute _setup_recorder_exclude_sensor_filter'``` -2. HA ERROR/WARNING MESSAGES - Fixed a problem where some I/O getting directory and filename list for the Update Devices configuration screen was being done outside of the HA Event Loop - - -3.0.5.5 +3.1 ....................... -### Change Log - v3.0.5.5 (7/15/2024) -1. ICLOUD3 FAILED TO LOAD (Fix) - iCloud3 injected special code into the HA history recorder to exclude various sensor entities with large text fields from being added to the history recorder. This caused problems with HA in the 2024.7 release. The HA 2024.7.2 included special code that blocked iCloud3 from loading. A temporary patch was posted on the iCloud3 GitHub repository to disable the recorder injection. This update is a permant fix. - - All sensor attributes not related to the battery, distance and timer sensors are being added to the HA recorder history database. Text base sensor attributes are not being added (info sensors, Event Log sensor, badge, tracking update, zone, etc.).. +### Change Log - v3.1 +1. APPLE ACCOUNT LOGIN (Fixed) - Apple changed the method of logging into the Apple Account to use Secure Remote Password verification where a hash-key is calculated from the password by both iCloud3 and Apple. The hash-key, instead of the password, is sent over the internet. Apple then compares the hash-key sent by iCloud3 to it's hash-key to determine the validity of the password. Note: In a few cases, the hash-key algorithm used by iCloud3 may not be the same as Apple for a valid password and you will need to change your password. +1. MULTIPLE APPLE ACCOUNTS (New) - iCloud3 now supports tracking devices from more than one Apple Account. The accounts are setup on the 'Configuration > Data Sources' screen and devices can be tracked from any apple account and the d if the device is set up as an account owners deviceThe Primary account belongs to the person with the main Family Sharing list as it always has. Secondary accounts can also be configured and devices from those accounts can be tracked. The Apple Account for the specific iCloud3 device is selected on the Update Devices screen as it always has. +2. FAMSHR - Everything called FamShr has been changed to iCloud. +2. THE APPLE ACCOUNT (Improved) - Logging into the Apple Account is now started before the beginning the HA process that creates the device_tracker and sensor entities. Both processes now run concurrently, eliminating (or reducing) any delays waiting for the Apple Account device data to be returned. The result is iCloud3 starts up faster. +3. APPLE ACCOUNT & MOBILE APP DEVICE ASSIGNMENT (Improved) - The results of matching the Apple Account iCloud devices and the Mobile App devices during startup are displayed in the Event Log. This has been simplified and is easier to read. +4. CONFIGURATION SCREENS (Improved) - + - Saving changes is faster. + - The iCloud3 Devices screen indicates if there is a problem with the iCloud or Mobile App device selection. + - Update Devices screen - Impoved selection of Apple Account and Mobile App devices. +5. MISSING APPLE ACCOUNT DEVICES (Improved) - Sometimes, an iCloud device's information is not returned from iCloud when iCloud3 starts. The error recovery and data request retry routines have been rewritten to only retry setting up devices with the error. This greatly simplifies and reduces the steps needed to retrieve the data from iCloud. +6. WAZE & WAZE HISTORY (Improved) - Improved the error checking, added retry on error conditions and updated the History database recalculation routines to better support concurrent operations. +7. APPLE ACCOUNT DEVICE STARTUP ERRORS (Enhanced) - Improved the handling and reporting duplicate and missing Apple Account devices. +8 STARTING/RESTARTING ICLOUD3 - + - Device Sensors (Fixed) - They were being reinitialized when Restarting iCloud3 when they shouldn't have been. + - Mobile App entities (Fixed) - They are no longer being reinitialized when iCloud3 is restarted on a configuration change or Event Log restart request. This includes the device_tracker, battery, trigger and notify entities.. + - Improved messaging and fixed miscellaneous bugs - **SPECIAL NOTE**: If you get an HA error that a sensor attribute is not available, create an issue on the iCloud3 GitHub repository. Let me know the sensor name and attribute name that is causing the error. I will remove it from the exclusion list in the next update. 3.0.5.2 @@ -58,7 +40,7 @@ ### Change Log - v3.0.5 (5/18/2024) 1. HACS UPUDATE ALERT (New) - The HACS Integration information will be check on a regular basis to see if a newer version of iCloud3 is available. -2. ICLOUD ACCOUNT AUTHENTICATION/FAMSHR DEVICES LIST (Fixed) - During startup ("Stage 4), the iCloud Account access is set up and the devices in the Family Sharing List is read. If a problem occurred, iCloud3 would retry this 10-times to see if the error was corrected. However, the FamShr data was not being reread and the old data was being used. The FamShr data is now reread correctly when trying to recover from this error. +2. ICLOUD ACCOUNT AUTHENTICATION/FAMSHR DEVICES LIST (Fixed) - During startup ("Stage 4), the iCloud Account access is set up and the devices in the Family Sharing List is read. If a problem occurred, iCloud3 would retry this 10-times to see if the error was corrected. However, the iCloud data was not being reread and the old data was being used. The iCloud data is now reread correctly when trying to recover from this error. 3. UPDATE DEVICES SCREEN (Fixed) - When upgrading a device (iPhone, iPad, Watch) and both the old and new devices are still in the Family Sharing List, the new device was being set back to the old device the next time iCloud3 was started. 4. LOCKED ICLOUD ACCOUNT (New) - An error message is displayed in the HA logs and on the Event Log if the iCloud account is locked. 5. EVENT LOG (Fix) - An 'Unbound event_recd' error would display when the length of the the event text > 2000 characters (@ehendrix23). diff --git a/custom_components/icloud3/__init__.py b/custom_components/icloud3/__init__.py index c0e5449..d6c51fb 100644 --- a/custom_components/icloud3/__init__.py +++ b/custom_components/icloud3/__init__.py @@ -1,9 +1,6 @@ """GitHub Custom Component.""" import asyncio - -# from homeassistant.components import history -# from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE - +from re import match from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP @@ -14,11 +11,11 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util import homeassistant.util.location as ha_location_info -import os import logging -from .const import (DOMAIN, PLATFORMS, ICLOUD3, MODE_PLATFORM, MODE_INTEGRATION, CONF_VERSION, +from .const import (ICLOUD3, DOMAIN, ICLOUD3_VERSION_MSG, + PLATFORMS, ICLOUD3, MODE_PLATFORM, MODE_INTEGRATION, CONF_VERSION, CONF_SETUP_ICLOUD_SESSION_EARLY, CONF_EVLOG_BTNCONFIG_URL, SENSOR_EVENT_LOG_NAME, SENSOR_WAZEHIST_TRACK_NAME, EVLOG_IC3_STARTING, VERSION, VERSION_BETA,) @@ -26,11 +23,15 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) from .global_variables import GlobalVariables as Gb -from .helpers.messaging import (_trace, _traceha, open_ic3log_file_init, +from .helpers.messaging import (_evlog, _log, open_ic3log_file_init, post_monitor_msg, post_evlog_greenbar_msg, post_startup_alert, log_info_msg, log_debug_msg, log_error_msg, log_exception_HA, log_exception) from .helpers.time_util import (time_now_secs, ) +from .helpers.file_io import (async_make_directory, async_directory_exists, async_copy_file, + async_rename_file, async_delete_directory, + make_directory, directory_exists, copy_file, file_exists, + rename_file, move_files, ) from .support.v2v3_config_migration import iCloud3_v2v3ConfigMigration from .support import start_ic3 from .support import config_file @@ -40,6 +41,7 @@ from .support import event_log from .icloud3_main import iCloud3 + Gb.HARootLogger = logging.getLogger("") Gb.HALogger = logging.getLogger(__name__) @@ -52,7 +54,7 @@ #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>><><><><><><><><><> async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Old way of setting up the iCloud tracker with platform: icloud3 statement.""" + """Old way of setting up the iCloud tracker with platform: icloud3 statem a ent.""" hass.data.setdefault(DOMAIN, {}) Gb.hass = hass Gb.config = config @@ -65,10 +67,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: Gb.ha_config_platform_stmt = True Gb.operating_mode = MODE_PLATFORM - # Initialize the config/.storage/icloud3/configuration file before the config_glow + # Initialize the config/.storage/icloud3/configuration file before the config_flow # has set up the integration start_ic3.initialize_directory_filenames() - config_file.load_storage_icloud3_configuration_file() + await Gb.hass.async_add_executor_job( + config_file.load_storage_icloud3_configuration_file) + # await config_file.async_load_storage_icloud3_configuration_file() if Gb.conf_profile[CONF_VERSION] == 1: Gb.HALogger.warning(f"Starting iCloud3 v{VERSION}{VERSION_BETA} > " @@ -81,7 +85,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # log_exception_HA(err) pass - return True #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> @@ -92,17 +95,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def start_icloud3(event=None): Gb.initial_icloud3_loading_flag = True log_debug_msg(f'START iCloud3 Initial Load Executor Job (iCloud3.start_icloud3)') - icloud3_started = await Gb.hass.async_add_executor_job(Gb.iCloud3.start_icloud3) + icloud3_started = await Gb.hass.async_add_executor_job( + Gb.iCloud3.start_icloud3) if icloud3_started: - log_msg =(f"iCloud3 v{Gb.version} Started") + log_msg =(f"{ICLOUD3_VERSION_MSG} Started") log_info_msg(log_msg) - Gb.HALogger.info(f"Gb.HALogger-Setting up iCloud3 v{Gb.version}") + Gb.HALogger.info(f"Gb.HALogger-Setting up {ICLOUD3_VERSION_MSG}") else: - log_msg =(f"iCloud3 v{Gb.version} - Failed to Start") + log_msg =(f"{ICLOUD3_VERSION_MSG} - Failed to Start") log_error_msg(log_msg) - Gb.HALogger.info(f"Setting up iCloud3 v{Gb.version}, Failed") + Gb.HALogger.info(f"Setting up {ICLOUD3_VERSION_MSG}, Failed") #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>><><><><><><><><><> # @@ -135,7 +139,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): start_ic3.initialize_directory_filenames() await Gb.hass.async_add_executor_job( - config_file.load_storage_icloud3_configuration_file) + config_file.load_storage_icloud3_configuration_file) start_ic3.set_log_level(Gb.log_level) @@ -143,11 +147,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): Gb.evlog_version = Gb.conf_profile['event_log_version'] Gb.EvLog = event_log.EventLog(Gb.hass) + Gb.EvLog.post_event(f"{EVLOG_IC3_STARTING}{ICLOUD3_VERSION_MSG} > Loading, " + f"{dt_util.now().strftime('%A, %b %d')}") + + # Setup iCloud Log File (icloud3-0.log) await Gb.hass.async_add_executor_job( - open_ic3log_file_init) + open_ic3log_file_init) - Gb.HALogger.info(f"Setting up iCloud3 v{Gb.version}{VERSION_BETA}") - log_info_msg(f"Setting up iCloud3 v{VERSION}{VERSION_BETA}") + Gb.HALogger.info(f"Setting up {ICLOUD3_VERSION_MSG}{VERSION_BETA}") + log_info_msg(f"Setting up {ICLOUD3} v{VERSION}{VERSION_BETA}") if Gb.restart_ha_flag: log_error_msg("iCloud3 > Waiting for HA restart to remove legacy \ @@ -156,32 +164,41 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await async_get_ha_location_info(hass) - start_ic3.initialize_icloud_data_source() + start_ic3.initialize_data_source_variables() await Gb.hass.async_add_executor_job( - restore_state.load_storage_icloud3_restore_state_file) + restore_state.load_storage_icloud3_restore_state_file) + # config_file.count_lines_of_code(Gb.icloud3_directory) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ try: - pass except Exception as err: log_exception(err) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Gb.EvLog.display_user_message(f"Starting {ICLOUD3_VERSION_MSG}") + if Gb.use_data_source_ICLOUD: + await _rename_icloud_v30_cookie_directory() + Gb.hass.async_add_executor_job( + pyicloud_ic3_interface.verify_all_apple_accounts) + # pyicloud_ic3_interface.create_all_PyiCloudServices) + # set_up_default_area_id() # Create device_tracker entities if devices have been configure # Otherwise, this is done in config_flow when the first device is set up if Gb.conf_devices != []: await Gb.hass.config_entries.async_forward_entry_setups( - entry, ['device_tracker']) + entry, + ['device_tracker']) # Create sensor entities await Gb.hass.config_entries.async_forward_entry_setups( - entry, ['sensor']) + entry, + ['sensor']) # Do not start if loading/initialization failed if successful_startup is False: @@ -190,6 +207,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): log_error_msg("Verify the configuration file and delete it manually if necessary") return False + Gb.EvLog.post_event(f"{EVLOG_IC3_STARTING}{ICLOUD3_VERSION_MSG} > Starting, " + f"{dt_util.now().strftime('%A, %b %d')}") + # Store a reference to the unsubscribe function to cleanup if an entry is unloaded. unsub_options_update_listener = entry.add_update_listener(options_update_listener) hass_data["unsub_options_update_listener"] = unsub_options_update_listener @@ -204,9 +224,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # SETUP PROCESS TO START ICLOUD3 # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - Gb.EvLog.display_user_message(f"Starting iCloud3 v{Gb.version}") - Gb.EvLog.post_event(f"{EVLOG_IC3_STARTING}iCloud3 v{Gb.version} > Starting, " - f"{dt_util.now().strftime('%A, %b %d')}") Gb.iCloud3 = iCloud3() @@ -218,26 +235,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): EVENT_HOMEASSISTANT_STOP, start_ic3.ha_stopping) Gb.EvLog.post_event('Start iCloud3 Services Executor Job') - Gb.hass.async_add_executor_job(register_icloud3_services) - - if (Gb.primary_data_source_ICLOUD and Gb.conf_tracking[CONF_SETUP_ICLOUD_SESSION_EARLY]): - Gb.EvLog.post_event('Start iCloud Account Session Executor Job') - Gb.hass.async_add_executor_job( - pyicloud_ic3_interface.create_PyiCloudService_executor_job) + Gb.hass.async_add_executor_job( + register_icloud3_services) Gb.initial_icloud3_loading_flag = True log_debug_msg('START iCloud3 Initial Load Executor Job (iCloud3.start_icloud3)') - icloud3_started = \ - await Gb.hass.async_add_executor_job( - Gb.iCloud3.start_icloud3) + icloud3_started = await Gb.hass.async_add_executor_job( + Gb.iCloud3.start_icloud3) if icloud3_started: - log_info_msg(f"iCloud3 v{Gb.version} Startup Complete") - Gb.HALogger.info(f"Setting up iCloud3 v{Gb.version}, Complete") + log_info_msg(f"{ICLOUD3_VERSION_MSG} Startup Complete") + Gb.HALogger.info(f"Setting up {ICLOUD3_VERSION_MSG}, Complete") else: - log_error_msg(f"iCloud3 v{Gb.version} Initialization Failed") - Gb.HALogger.info(f"Setting up iCloud3 v{Gb.version}, Failed") + log_error_msg(f"{ICLOUD3_VERSION_MSG} Initialization Failed") + Gb.HALogger.info(f"Setting up {ICLOUD3_VERSION_MSG}, Failed") return True @@ -285,6 +297,75 @@ async def async_get_ha_location_info(hass): log_exception(err) pass +#------------------------------------------------------------------------------------------- +async def _rename_icloud_v30_cookie_directory(): + ''' + iCloud3 v3.1 uses the '.../apple_acct.ic3' cookie directory instead of the '.../icloud' + directory to avoid conflicts with Apple Account integrations (iCloud, HomeKit, etc) + + Rename the '.../icloud' cookie directory to '.../apple_acct.ic3' if the old + '.../icloud/session/username.tpw' file exists (this is unique to iCloud3). + + if the '.../icloapple_acct.ic3/' directory does not exist. create it. + ''' + try: + # v30_icloud_config_dir = Gb.ha_storage_icloud3.replace('.config', '') + # v31_ic3_config_dir_exists = await async_directory_exists(Gb.ha_storage_icloud3) + # if v31_ic3_config_dir_exists is False: + # Gb.HALogger.info(f"{v30_icloud_config_dir} --> {Gb.ha_storage_icloud3}") + # move_files(v30_icloud_config_dir, Gb.ha_storage_icloud3) + + v31_cookie_dir = Gb.icloud_cookie_directory + v31_cookie_dir_exists = await async_directory_exists(v31_cookie_dir) + if v31_cookie_dir_exists: + return + + await async_make_directory(v31_cookie_dir) + v30_cookie_dir = Gb.hass.config.path(Gb.ha_storage_directory, 'icloud') + v30_cookie_dir_exists = await async_directory_exists(v30_cookie_dir) + Gb.HALogger.info(f"{v30_cookie_dir=} {v30_cookie_dir_exists=}") + + if v30_cookie_dir_exists is False: + return + + username, _password, _locate_all = config_file.apple_acct_username_password(0) + if username == "": + return + + cookie_filename = "".join([c for c in username if match(r"\w", c)]) + Gb.HALogger.info(f"{cookie_filename=}") + + v30_cookie_filename = f"{v30_cookie_dir}/{cookie_filename}" + v30_cookie_tpw_filename = f"{v30_cookie_dir}/session/{cookie_filename}.tpw" + v30_session_filename = f"{v30_cookie_dir}/session/{cookie_filename}" + v30_tpw_file_exists = await async_directory_exists(v30_cookie_tpw_filename) + + Gb.HALogger.info(f"{v30_cookie_filename =}") + Gb.HALogger.info(f"{v30_cookie_tpw_filename =}") + Gb.HALogger.info(f"{v30_session_filename=}") + + if v30_tpw_file_exists is False: + return + + Gb.HALogger.info(f"{v30_cookie_filename} --> {v31_cookie_dir}/{cookie_filename}.session") + await async_copy_file(v30_cookie_filename, f"{v31_cookie_dir}/{cookie_filename}") + + Gb.HALogger.info(f"{v30_session_filename} --> {v31_cookie_dir}/{cookie_filename}.session") + await async_copy_file(v30_session_filename, f"{v31_cookie_dir}/{cookie_filename}.session") + # await async_rename_file(v30_session_filename, f"{v30_cookie_dir}/{cookie_filename}.session") + + Gb.HALogger.info(f"{v30_session_filename}.tpw --> {v31_cookie_dir}/{cookie_filename}.tpw") + await async_copy_file(f"{v30_session_filename}.tpw", f"{v31_cookie_dir}/{cookie_filename}.tpw") + # await async_rename_file(f"{v30_session_filename}.tpw", f"{v30_cookie_dir}/{cookie_filename}.tpw") + + post_monitor_msg(f"Cookie Directory > Directory and files were copied " + f"from `{v30_cookie_dir}` to `{v31_cookie_dir}`") + + except Exception as err: + log_exception(err) + pass + +#------------------------------------------------------------------------------------------- # def set_up_default_area_id(): # Get Personal Devices area id # area_reg = ar.async_get(hass) diff --git a/custom_components/icloud3/config_flow.py b/custom_components/icloud3/config_flow.py index 9c78b29..9aa4e80 100644 --- a/custom_components/icloud3/config_flow.py +++ b/custom_components/icloud3/config_flow.py @@ -1,5 +1,3 @@ -import os -import time from homeassistant import config_entries, data_entry_flow from homeassistant.config_entries import ConfigEntry as config_entry @@ -12,68 +10,64 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util import voluptuous as vol - - - +from re import match +import time +from datetime import datetime +import pyotp from .global_variables import GlobalVariables as Gb -from .const import (DOMAIN, ICLOUD3, DATETIME_FORMAT, - 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, +from .const import (DOMAIN, ICLOUD3, DATETIME_FORMAT, STORAGE_DIR, + NBSP, RARROW, PHDOT, CRLF_DOT, DOT, HDOT, PHDOT, CIRCLE_STAR, RED_X, + YELLOW_ALERT, RED_ALERT, EVLOG_NOTICE, EVLOG_ALERT, EVLOG_ERROR, LINK, LLINK, RLINK, + IPHONE_FNAME, IPHONE, IPAD, WATCH, AIRPODS, ICLOUD, OTHER, HOME, FAMSHR, DEVICE_TYPES, DEVICE_TYPE_FNAME, DEVICE_TRACKER_DOT, - MOBAPP, NO_MOBAPP, APPLE_SPECIAL_ICLOUD_SERVER_COUNTRY_CODE, + MOBAPP, NO_MOBAPP, TRACK_DEVICE, MONITOR_DEVICE, INACTIVE_DEVICE, NAME, FRIENDLY_NAME, FNAME, TITLE, BATTERY, - ZONE, HOME_DISTANCE, WAZE_SERVERS_FNAME, - PICTURE_WWW_STANDARD_DIRS, CONF_PICTURE_WWW_DIRS, + ZONE, HOME_DISTANCE, + CONF_PICTURE_WWW_DIRS, CONF_VERSION, CONF_EVLOG_CARD_DIRECTORY, CONF_EVLOG_BTNCONFIG_URL, + CONF_APPLE_ACCOUNTS, CONF_APPLE_ACCOUNT, CONF_TOTP_KEY, CONF_USERNAME, CONF_PASSWORD, CONF_DEVICES, CONF_SETUP_ICLOUD_SESSION_EARLY, - CONF_DATA_SOURCE, CONF_VERIFICATION_CODE, + CONF_DATA_SOURCE, CONF_VERIFICATION_CODE, CONF_LOCATE_ALL, CONF_TRACK_FROM_ZONES, CONF_TRACK_FROM_BASE_ZONE_USED, CONF_TRACK_FROM_BASE_ZONE, CONF_TRACK_FROM_HOME_ZONE, CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX, CONF_LOG_ZONES, CONF_PICTURE, CONF_DEVICE_TYPE, CONF_INZONE_INTERVALS, CONF_RAW_MODEL, CONF_MODEL, CONF_MODEL_DISPLAY_NAME, CONF_FAMSHR_DEVICE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_TIME_FORMAT, - CONF_MAX_INTERVAL, CONF_OFFLINE_INTERVAL, CONF_EXIT_ZONE_INTERVAL, CONF_MOBAPP_ALIVE_INTERVAL, - CONF_GPS_ACCURACY_THRESHOLD, CONF_OLD_LOCATION_THRESHOLD, CONF_OLD_LOCATION_ADJUSTMENT, - CONF_TRAVEL_TIME_FACTOR, CONF_TFZ_TRACKING_MAX_DISTANCE, + CONF_TRAVEL_TIME_FACTOR, 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, - CONF_WAZE_USED, CONF_WAZE_SERVER, CONF_WAZE_MAX_DISTANCE, CONF_WAZE_MIN_DISTANCE, - CONF_WAZE_REALTIME, CONF_WAZE_HISTORY_DATABASE_USED, CONF_WAZE_HISTORY_MAX_DISTANCE, + CONF_WAZE_USED, CONF_WAZE_SERVER, + CONF_WAZE_REALTIME, CONF_WAZE_HISTORY_DATABASE_USED, CONF_WAZE_HISTORY_TRACK_DIRECTION, - - CONF_STAT_ZONE_FNAME, CONF_STAT_ZONE_STILL_TIME, CONF_STAT_ZONE_INZONE_INTERVAL, - CONF_STAT_ZONE_BASE_LATITUDE, - CONF_STAT_ZONE_BASE_LONGITUDE, CONF_DISPLAY_TEXT_AS, + CONF_STAT_ZONE_FNAME, CONF_STAT_ZONE_STILL_TIME, CONF_DISPLAY_TEXT_AS, CONF_IC3_DEVICENAME, CONF_FNAME, CONF_FAMSHR_DEVICENAME, CONF_MOBILE_APP_DEVICE, CONF_FMF_EMAIL, CONF_TRACKING_MODE, CONF_INZONE_INTERVAL, CONF_FIXED_INTERVAL, CONF_AWAY_TIME_ZONE_1_OFFSET, CONF_AWAY_TIME_ZONE_1_DEVICES, CONF_AWAY_TIME_ZONE_2_OFFSET, CONF_AWAY_TIME_ZONE_2_DEVICES, - CONF_SENSORS_MONITORED_DEVICES, - CONF_SENSORS_DEVICE, - CONF_SENSORS_TRACKING_UPDATE, CONF_SENSORS_TRACKING_TIME, CONF_SENSORS_TRACKING_DISTANCE, - CONF_SENSORS_TRACK_FROM_ZONES, CONF_SENSORS_TRACKING_OTHER, CONF_SENSORS_ZONE, + CONF_SENSORS_TRACKING_TIME, CONF_SENSORS_TRACKING_DISTANCE, + CONF_SENSORS_TRACK_FROM_ZONES, CONF_SENSORS_OTHER, CONF_EXCLUDED_SENSORS, CONF_PARAMETER_TIME_STR, CONF_PARAMETER_FLOAT, - CF_PROFILE, CF_DATA_TRACKING, CF_DATA_GENERAL, - DEFAULT_DEVICE_CONF, DEFAULT_GENERAL_CONF, + CF_PROFILE, CF_TRACKING, CF_GENERAL, + DEFAULT_DEVICE_CONF, DEFAULT_GENERAL_CONF, DEFAULT_APPLE_ACCOUNTS_CONF, DEFAULT_DEVICE_REINITIALIZE_CONF, ) from .const_sensor import (SENSOR_GROUPS ) -from .helpers.common import (instr, isnumber, obscure_field, list_to_str, str_to_list, +from .const_config_flow import * +from .config_flow_forms import * +from .helpers.common import (instr, isnumber, is_empty, isnot_empty, list_to_str, str_to_list, is_statzone, zone_dname, isbetween, list_del, list_add, - get_file_list, get_directory_list, - sort_dict_by_values, ) + sort_dict_by_values, + encode_password, decode_password, ) from .helpers.messaging import (log_exception, log_debug_msg, log_info_msg, - _traceha, _trace, + _log, _evlog, post_event, post_monitor_msg, ) from .helpers import entity_io +from .helpers import file_io from . import sensor as ic3_sensor from . import device_tracker as ic3_device_tracker from .support import start_ic3 @@ -81,7 +75,8 @@ from .support import service_handler from .support import pyicloud_ic3_interface from .support.v2v3_config_migration import iCloud3_v2v3ConfigMigration -from .support.pyicloud_ic3 import (PyiCloudService, PyiCloudException, PyiCloudFailedLoginException, +from .support.pyicloud_ic3 import (PyiCloudService, PyiCloudValidateAppleAcct, + PyiCloudException, PyiCloudFailedLoginException, PyiCloudServiceNotActivatedException, PyiCloudNoDevicesException, ) import logging _CF_LOGGER = logging.getLogger("icloud3-cf") @@ -92,407 +87,8 @@ 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): - """ Make a drop down list from a list """ - - if type(key_value_dict) is dict: - value_list = [v for v in key_value_dict.values() if v.startswith('.') is False] - else: - value_list = list(key_value_dict) - - return value_list - -#----------------------------------------------------------------------------------------- -def ensure_six_item_list(list_item): - for i in range(6 - len(list_item)): - list_item.append('.') - - return list_item - -#----------------------------------------------------------------------------------------- -def ensure_six_item_dict(dict_item): - dummy_key = '' - for i in range(6 - len(dict_item)): - dummy_key += '.' - dict_item[dummy_key] = '.' - - return dict_item - -#----------------------------------------------------------------------------------------- -MENU_PAGE_0_INITIAL_ITEM = 1 -MENU_PAGE_TITLE = [ - 'Menu > Configure Devices and Sensor Menu', - 'Menu > Configure Parameters Menu' - ] -MENU_KEY_TEXT = { - 'icloud_account': 'iCLOUD ACCOUNT & MOBILE APP ᐳ •Set iCloud Account Username/Password, •Set Location Data Sources', - 'device_list': 'ICLOUD3 DEVICES ᐳ •Add, Change and Delete tracked and monitored devices', - 'verification_code': 'ENTER/REQUEST AN APPLE ID VERIFICATION CODE ᐳ •Enter or Request the 6-digit Apple ID Verification Code', - 'away_time_zone': 'AWAY TIME ZONE ᐳ •Select the time zone used to display time based tracking events for a device when in another time zone', - 'change_device_order': 'CHANGE DEVICE ORDER ᐳ •Change the tracking order of the Devices and their display sequence on the Event Log', - 'sensors': 'SENSORS ᐳ •Set Sensors created by iCloud3, •Exclude Specific Sensors from being created', - 'actions': 'ACTION COMMANDS ᐳ •Restart/Pause/Resume Polling, •Debug Logging, •Export Event Log, •Waze Utilities', - - 'format_settings': 'FORMAT SETTINGS ᐳ •Log Level, •Zones Display Format, •DeviceTracker State, •Unit of Measure, •Time & Distance, Display GPS Coordinates', - 'display_text_as': 'DISPLAY TEXT AS ᐳ •Event Log Custom Card Text Replacement', - 'waze': 'WAZE ROUTE DISTANCE, TIME & HISTORY ᐳ •Set Route Server Location, Min/Max Intervals, etc. •Set Waze History Database Parameters and Controls', - 'inzone_intervals': 'INZONE DEFAULT INTERVALS ᐳ •Default inZone intervals for different device types and the Mobile App, •Other inZone Controls ', - 'special_zones': 'SPECIAL ZONES ᐳ •Enter Zone Delay, •Stationary Zone, •Primary Track-from-Home Zone Override', - 'tracking_parameters': 'TRACKING & OTHER PARAMETERS ᐳ •Set Nearby Device Info, Accuracy Thresholds & Other Location Request Intervals, •Picture Image Directories, •Event Log Custom Card Directory, etc.', - - 'select': 'SELECT ᐳ Select the parameter update form', - 'next_page_0': f'{MENU_PAGE_TITLE[0].upper()} ᐳ •iCloud Account & Mobile App, •iCloud3 Devices, •Enter & Request Verification Code, •Change Device Order, •Sensors, •Action Commands', - 'next_page_1': f'{MENU_PAGE_TITLE[1].upper()} ᐳ •Format Parameters, •Display Text As, •Waze Route Distance, Time & History, •inZone Intervals, •Special Zones, • Other Parameters', - 'exit': f'EXIT AND RESTART ICLOUD3 v{Gb.version}' -} - -MENU_KEY_TEXT_PAGE_0 = [ - MENU_KEY_TEXT['icloud_account'], - MENU_KEY_TEXT['device_list'], - MENU_KEY_TEXT['verification_code'], - MENU_KEY_TEXT['away_time_zone'], - MENU_KEY_TEXT['sensors'], - MENU_KEY_TEXT['actions'], - ] -MENU_PAGE_1_INITIAL_ITEM = 0 -MENU_KEY_TEXT_PAGE_1 = [ - MENU_KEY_TEXT['format_settings'], - MENU_KEY_TEXT['display_text_as'], - MENU_KEY_TEXT['waze'], - MENU_KEY_TEXT['special_zones'], - MENU_KEY_TEXT['tracking_parameters'], - MENU_KEY_TEXT['inzone_intervals'], - ] -MENU_ACTION_ITEMS = [ - MENU_KEY_TEXT['select'], - MENU_KEY_TEXT['next_page_1'], - MENU_KEY_TEXT['exit'] - ] - -ACTION_LIST_ITEMS_KEY_TEXT = { - 'next_page_items': 'NEXT PAGE ITEMS ᐳ ^info_field^', - 'next_page': 'NEXT PAGE ᐳ Save changes. Display the next page', - 'next_page_device': 'NEXT PAGE ᐳ Friendly Name, Track-from-Zones, Other Setup Fields', - 'next_page_waze': 'NEXT PAGE ᐳ Waze History Database parameters', - 'select_form': 'SELECT ᐳ Select the parameter update form', - - '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', - "request_verification_code":'REQUEST A NEW APPLE ID VERIFICATION CODE ᐳ Reset iCloud Interface and request a new Apple ID Verification Code', - 'cancel_verification_entry':'CANCEL ᐳ Cancel the Verification Code Entry and Close this screen', - - 'update_device': 'UPDATE DEVICE ᐳ Update the selected device', - 'add_device': 'ADD DEVICE ᐳ Add a device to be tracked by iCloud3', - 'delete_device': 'DELETE DEVICE(S), OTHER DEVICE MAINTENANCE ᐳ Delete the device(s) from the tracked device list, clear the FamShr/FmF/Mobile App selection fields', - 'change_device_order': 'CHANGE DEVICE ORDER ᐳ Change the tracking order of the Devices and their display sequence on the Event Log', - - 'delete_this_device': 'DELETE THIS DEVICE ᐳ Delete this device → ', - 'delete_all_devices': 'DELETE ALL DEVICES ᐳ Delete all devices from the iCloud3 tracked devices list', - 'delete_icloud_mobapp_info':'CLEAR FAMSHR/MOBAPP INFO ᐳ Reset the FamShr/Mobile App seletion fields on all devices', - 'delete_device_cancel': 'CANCEL ᐳ Return to the Device List screen', - - 'inactive_to_track': 'TRACK ALL OR SELECTED ᐳ Change the `Tracking Mode‘ of all of the devices (or the selected devices) from `Inactive‘ to `Tracked‘', - 'inactive_keep_inactive': 'DO NOT TRACK, KEEP INACTIVE ᐳ None of these devices should be `Tracked‘ and should remain `Inactive‘', - - 'restart_ha': 'RESTART HOME ASSISTANT ᐳ Restart HA and reload iCloud3', - 'restart_ic3_now': 'RESTART NOW ᐳ Restart iCloud3 now to load the updated configuration', - 'restart_ic3_later': 'RESTART LATER ᐳ The configuration changes have been saved. Load the updated configuration the next time iCloud3 is started', - 'reload_icloud3': 'RELOAD ICLOUD3 ᐳ Reload & Restart iCloud3 (This does not load a new version)', - 'review_inactive_devices': 'REVIEW INACTIVE DEVICES ᐳ Some Devices are `Inactive` and will not be located or tracked', - - 'select_text_as': 'SELECT ᐳ Update selected `Display Text As‘ field', - 'clear_text_as': 'CLEAR ᐳ Remove `Display Text As‘ entry', - - 'exclude_sensors': 'EXCLUDE SENSORS ᐳ Select specific Sensors that should not be created', - 'filter_sensors': 'FILTER SENSORS ᐳ Select Sensors that should be displayed', - - 'move_up': 'MOVE UP ᐳ Move the Device up in the list', - 'move_down': 'MOVE DOWN ᐳ Move the Device down in the list', - - 'save': 'SAVE ᐳ Update Configuration File, Return to the Menu', - 'return': 'RETURN ᐳ Return to the Menu', - - 'cancel': 'RETURN ᐳ Return to the previous screen. Cancel any unsaved changes', - 'exit': 'EXIT ᐳ Exit the iCloud3 Configurator', - - 'confirm_return': 'RETURN WITHOUT SAVING CONFIGURATION CHANGES ᐳ Return to the Main Menu without saving any changes', - 'confirm_save': 'SAVE THE CONFIGURATION CHANGES ᐳ Save any changes, then return to the Main Menu', - - "divider1": "═══════════════════════════════════════", - "divider2": "═══════════════════════════════════════", - "divider3": "═══════════════════════════════════════" - } - -ACTION_LIST_ITEMS_KEY_BY_TEXT = {text: key for key, text in ACTION_LIST_ITEMS_KEY_TEXT.items()} - -ACTION_LIST_ITEMS_BASE = [ - ACTION_LIST_ITEMS_KEY_TEXT['save'], - ACTION_LIST_ITEMS_KEY_TEXT['cancel'] - ] - -NONE_DICT_KEY_TEXT = {'None': 'None'} -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 = 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'], - ACTION_LIST_ITEMS_KEY_TEXT['request_verification_code'], - ACTION_LIST_ITEMS_KEY_TEXT['cancel_verification_entry']] -REAUTH_ACTIONS = [ - ACTION_LIST_ITEMS_KEY_TEXT['send_verification_code'], - ACTION_LIST_ITEMS_KEY_TEXT['request_verification_code'], - ACTION_LIST_ITEMS_KEY_TEXT['cancel']] -DEVICE_LIST_ACTIONS = [ - ACTION_LIST_ITEMS_KEY_TEXT['update_device'], - ACTION_LIST_ITEMS_KEY_TEXT['add_device'], - ACTION_LIST_ITEMS_KEY_TEXT['delete_device'], - ACTION_LIST_ITEMS_KEY_TEXT['change_device_order'], - ACTION_LIST_ITEMS_KEY_TEXT['return']] -DEVICE_LIST_ACTIONS_ADD = [ - ACTION_LIST_ITEMS_KEY_TEXT['add_device'], - ACTION_LIST_ITEMS_KEY_TEXT['return']] -DEVICE_LIST_ACTIONS_NO_ADD = [ - ACTION_LIST_ITEMS_KEY_TEXT['update_device'], - ACTION_LIST_ITEMS_KEY_TEXT['delete_device'], - ACTION_LIST_ITEMS_KEY_TEXT['change_device_order'], - ACTION_LIST_ITEMS_KEY_TEXT['return']] -DELETE_DEVICE_ACTIONS = [ - ACTION_LIST_ITEMS_KEY_TEXT['delete_this_device'], - ACTION_LIST_ITEMS_KEY_TEXT['delete_all_devices'], - ACTION_LIST_ITEMS_KEY_TEXT['delete_icloud_mobapp_info'], - ACTION_LIST_ITEMS_KEY_TEXT['delete_device_cancel']] -REVIEW_INACTIVE_DEVICES = [ - ACTION_LIST_ITEMS_KEY_TEXT['inactive_to_track'], - ACTION_LIST_ITEMS_KEY_TEXT['inactive_keep_inactive']] -RESTART_NOW_LATER_ACTIONS = [ - ACTION_LIST_ITEMS_KEY_TEXT['restart_ha'], - ACTION_LIST_ITEMS_KEY_TEXT['reload_icloud3'], - ACTION_LIST_ITEMS_KEY_TEXT['restart_ic3_now'], - ACTION_LIST_ITEMS_KEY_TEXT['restart_ic3_later'], - ACTION_LIST_ITEMS_KEY_TEXT['review_inactive_devices']] - - -# Parameter List Selections Items -# DATA_SOURCE_ITEMS_KEY_TEXT = { -# 'icloud,mobapp': 'ICLOUD & MOBAPP - iCloud account and Mobile App are used for location data', -# 'icloud': 'ICLOUD ONLY - Mobile App is not monitored on any tracked device', -# 'mobapp': 'Mobile App ONLY - iCloud account is not used for location data on any tracked device' -# } -DATA_SOURCE_ICLOUD_ITEMS_KEY_TEXT = { - 'famshr': 'Family Sharing List members from the iCloud Account (FamShr)', - # 'fmf': 'Friends/Contacts who are sharing their location with you (FmF)' - } -DATA_SOURCE_MOBAPP_ITEMS_KEY_TEXT = { - 'mobapp': 'HA Mobile App device_tracker and sensor entities are monitored (MobApp)' - } -ICLOUD_SERVER_ENDPOINT_SUFFIX_ITEMS_KEY_TEXT = { - '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_NONE_ITEMS_KEY_TEXT = { - 'None': 'None - The Mobile App is not installed on this device', - } -LOG_ZONES_KEY_TEXT = { - # '.fmf': f"{'-'*10} File Name Formats {'-'*10}", - 'name-zone': ' → [year]-[zone].csv', - 'name-device': ' → [year]-[device].csv', - 'name-device-zone': ' → [year]-[device]-[zone].csv', - 'name-zone-device': ' → [year]-[zone]-[device].csv', - } -TRACKING_MODE_ITEMS_KEY_TEXT = { - 'track': 'Track - Request Location and track the device', - 'monitor': 'Monitor - Report location only when another tracked device is updated', - 'inactive': 'INACTIVE - Device is inactive and will not be tracked' - } -DATA_SOURCE_ITEMS_KEY_TEXT = { - 'famshr': 'iCloud - Family Sharing List members from the iCloud Account (FamShr)', - # 'fmf': 'iCloud - Friends/Contacts who are sharing their location with you (FmF)', - 'mobapp': 'Mobile App - HA Mobile App device_tracker and sensor entities are monitored (MobApp)' - } -UNIT_OF_MEASUREMENT_ITEMS_KEY_TEXT = { - 'mi': 'Imperial (mi, ft)', - 'km': 'Metric (km, m)' - } -TIME_FORMAT_ITEMS_KEY_TEXT = { - '12-hour': '12-hour Time Format (9:05:30a, 4:40:15p)', - '24-hour': '24-hour Time Format (09:05:30, 16:40:15)' - } -TRAVEL_TIME_INTERVAL_MULTIPLIER_KEY_TEXT = { - .25: 'Shortest Interval Time - 1/4 TravelTime (¼ × 8 mins = Next Locate in 2m)', - .33: 'Shorter Interval Time - 1/3 TravelTime (⅓ × 8 mins = Next Locate in 2m40s)', - .50: 'Half Way (Default) - 1/2 TravelTime (½ × 8 mins = Next Locate in 4m)', - .66: 'Longer Interval Time - 2/3 TravelTime (⅔ × 8 mins = Next Locate in 5m20s', - .75: 'Longest Interval Time - 3/4 TravelTime (¾ × 8 mins = Next Locate in 6m)' - } -DISPLAY_ZONE_FORMAT_ITEMS_KEY_TEXT = {} -DISPLAY_ZONE_FORMAT_ITEMS_KEY_TEXT_BASE = { - 'fname': 'HA Zone Friendly Name (Home, Away, TheShores) →→→ PREFERRED', - 'zone': 'HA Zone entity_id (home, not_home, the_shores)', - 'name': 'iCloud3 reformated Zone entity_id (zone.the_shores → TheShores)', - 'title': 'iCloud3 reformated Zone entity_id (zone.the_shores → The Shores)' - } -DEVICE_TRACKER_STATE_SOURCE_ITEMS_KEY_TEXT = { - 'ic3_evlog': 'iCloud3 Zone - Use EvLog Zone Display Name (gps & accuracy) →→→ PREFERRED', - 'ic3_fname': 'iCloud3 Zone - Use Zone Friendly Name (gps & accuracy)', - 'ha_gps': 'HA Zone - Use gps coordinates to determine the zone (except Stationary Zones)' -} -LOG_LEVEL_ITEMS_KEY_TEXT = { - 'info': 'Info - Log General Information and Event Log messages', - 'debug': 'Debug - Info + Other Internal Tracking Monitors', - 'debug-ha': 'Debug (HALog) - Also add log records to the `home-assistant.log` file', - 'debug-auto-reset': 'Debug (AutoReset) - Debug logging that resets to Info at midnight', - 'rawdata': 'Rawdata - Debug + Raw Data (filtered) received from iCloud Location Servers', - 'rawdata-auto-reset': 'Rawdata (AutoReset) - RawData logging that resets to Info at midnight', - 'unfiltered': 'Rawdata (Unfiltered) - Raw Data (everything) received from iCloud Location Servers', - } -DISTANCE_METHOD_ITEMS_KEY_TEXT = { - 'waze': 'Waze - Waze Route Service provides travel time & distance information', - 'calc': 'Calc - Distance is calculated using a `straight line` formula' - } -WAZE_SERVER_ITEMS_KEY_TEXT = { - 'us': WAZE_SERVERS_FNAME['us'], - 'il': WAZE_SERVERS_FNAME['il'], - 'row': WAZE_SERVERS_FNAME['row'] - } -WAZE_HISTORY_TRACK_DIRECTION_ITEMS_KEY_TEXT = { - 'north_south': 'North-South - You generally travel in North-to-South direction', - 'east_west': 'East-West - You generally travel in East-West direction' - } - -CONF_SENSORS_MONITORED_DEVICES_KEY_TEXT = { - 'md_badge': '_badge ᐳ Badge sensor - A badge showing the Zone Name or distance from the Home zone. Attributes include location related information', - 'md_battery': '_battery, battery_status ᐳ Create Battery (65%) and Battery Status (Charging, Low, etc) sensors', - 'md_location_sensors': 'Location related sensors ᐳ Name, zone, distance, travel_time, etc. (_name, _zone, _zone_fname, _zone_name, _zone_datetime, _home_distance, _travel_time, _travel_time_min, _last_located, _last_update)', - } -CONF_SENSORS_DEVICE_KEY_TEXT = { - NAME: '_name ᐳ iCloud3 Device Name', - 'badge': '_badge ᐳ A badge showing the Zone Name or distance from the Home zone', - BATTERY: '_battery, _battery_status ᐳ Create Battery Level (65%) and Battery Status (Charging, Low, etc) sensors', - 'info': '_info ᐳ An information message containing status, alerts and errors related to device location updates, data accuracy, etc', - } -CONF_SENSORS_TRACKING_UPDATE_KEY_TEXT = { - 'interval': '_interval ᐳ Time between location requests', - 'last_update': '_last_update ᐳ Last time the location was updated', - 'next_update': '_next_update ᐳ Next time the location will be updated', - 'last_located': '_last_located ᐳ Last time the was located using iCloud or Mobile App location', - } -CONF_SENSORS_TRACKING_TIME_KEY_TEXT = { - 'travel_time': '_travel_time ᐳ Waze Travel time to Home or closest Track-from-Zone zone', - 'travel_time_min': '_travel_time_min ᐳ Waze Travel time to Home or closest Track-from-Zone zone in minutes', - 'travel_time_hhmm': '_travel_time_hhmm ᐳ Waze Travel time to a Zone in hours:minutes', - 'arrival_time': '_arrival_time ᐳ Home Zone arrival time based on Waze Travel time', - } -CONF_SENSORS_TRACKING_DISTANCE_KEY_TEXT = { - 'home_distance': '_home_distance ᐳ Distance to the Home zone', - 'zone_distance': '_zone_distance ᐳ Distance to the Home or closest Track-from-Zone zone', - 'dir_of_travel': '_dir_of_travel ᐳ Direction of Travel for the Home zone or closest Track-from-Zone zone (Towards, AwayFrom, inZone, etc)', - 'moved_distance': '_moved_distance ᐳ Distance moved from the last location', - } -CONF_SENSORS_TRACK_FROM_ZONES_KEY_TEXT = { - 'general_sensors': 'Include General Sensors (_zone_info)', - 'time_sensors': 'Include Travel Time Sensors (_travel_time, _travel_time_mins, _travel_time_hhmm, _arrival_time', - 'distance_sensors': 'Include Zone Distance Sensors (_zone_distance, _distance, _dir_of_travel)', - } -CONF_SENSORS_TRACK_FROM_ZONES_KEYS = ['general_sensors', 'time_sensors', 'distance_sensors'] -CONF_SENSORS_TRACKING_OTHER_KEY_TEXT = { - 'trigger': '_trigger ᐳ Last action that triggered a location update', - 'waze_distance': '_waze_distance ᐳ Waze distance from a TrackFrom zone', - 'calc_distance': '_calc_distance ᐳ Calculated straight line distance from a TrackFrom zone', - } -CONF_SENSORS_ZONE_KEY_TEXT = { - 'zone_fname': '_zone_fname ᐳ HA Zone entity Friendly Name (HA Config > Areas & Zones > Zones > Name)', - 'zone': '_zone ᐳ HA Zone entity_id (`the_shores`)', - 'zone_name': '_zone_name ᐳ Reformat the Zone entity_id, capitalize and remove `_`s (`the_shores` → `TheShores`)', - 'zone_datetime': '_zone_datetime ᐳ The time the Device entered the Zone', - 'last_zone': '_last_zone_[...] ᐳ Create the same sensors for the device`s last HA Zone', - } -CONF_SENSORS_OTHER_KEY_TEXT = { - 'gps_accuracy': '_gps_accuracy ᐳ GPS acuracy of the last location coordinates', - 'vertical_accuracy':'_vertical_accuracy ᐳ Vertical (Elevation) Accuracy', - 'altitude': '_altitude ᐳ Altitude/Elevation', - } - -ACTIONS_SCREEN_ITEMS_KEY_TEXT = { - "divider1": "═════════════ ICLOUD3 CONTROL ACTIONS ══════════════", - "restart": "RESTART ᐳ Restart iCloud3", - "pause": "PAUSE ᐳ Pause polling on all devices", - "resume": "RESUME ᐳ Resume Polling on all devices, Refresh all locations", - "divider2": "════════════════ DEBUG LOG ACTIONS ══════════════", - "debug_start": "START DEBUG LOGGING ᐳ Start or stop debug logging", - "debug_stop": "STOP DEBUG LOGGING ᐳ Start or stop debug logging", - "rawdata_start": "START RAWDATA LOGGING ᐳ Start or stop debug rawdata logging", - "rawdata_stop": "STOP RAWDATA LOGGING ᐳ Start or stop debug rawdata logging", - "commit": "COMMIT DEBUG LOG RECORDS ᐳ Verify all debug log file records are written", - "divider3": "════════════════ OTHER COMMANDS ═══════════════", - "evlog_export": "EXPORT EVENT LOG ᐳ Export Event Log data", - "wazehist_maint": "WAZE HIST DATABASE ᐳ Recalc time/distance data at midnight", - "wazehist_track": "WAZE HIST MAP TRACK ᐳ Load route locations for map display", - "divider4": "═══════════════════════════════════════════════", - "restart_ha": "RESTART HA, RELOAD ICLOUD3 ᐳ Restart HA or Reload iCloud3", - "return": "MAIN MENU ᐳ Return to the Main Menu" - } -ACTIONS_SCREEN_ITEMS_TEXT = [text for text in ACTIONS_SCREEN_ITEMS_KEY_TEXT.values()] -ACTIONS_SCREEN_ITEMS_KEY_BY_TEXT = {text: key - for key, text in ACTIONS_SCREEN_ITEMS_KEY_TEXT.items() - if key.startswith('divider') is False} - -ACTIONS_IC3_ITEMS = { - "restart": "RESTART ᐳ Restart iCloud3", - "pause": "PAUSE ᐳ Pause polling on all devices", - "resume": "RESUME ᐳ Resume Polling on all devices, Refresh all locations", -} -ACTIONS_DEBUG_ITEMS = { - "debug_start": "START DEBUG LOGGING ᐳ Start or stop debug logging", - "debug_stop": "STOP DEBUG LOGGING ᐳ Start or stop debug logging", - "rawdata_start": "START RAWDATA LOGGING ᐳ Start or stop debug rawdata logging", - "rawdata_stop": "STOP RAWDATA LOGGING ᐳ Start or stop debug rawdata logging", - "commit": "COMMIT DEBUG LOG RECORDS ᐳ Verify all debug log file records are written", -} -ACTIONS_OTHER_ITEMS = { - "evlog_export": "EXPORT EVENT LOG ᐳ Export Event Log data", - "wazehist_maint": "WAZE HIST DATABASE ᐳ Recalc time/distance data at midnight", - "wazehist_track": "WAZE HIST MAP TRACK ᐳ Load route locations for map display", -} -ACTIONS_ACTION_ITEMS = { - "restart_ha": "RESTART HA, RELOAD ICLOUD3 ᐳ Restart HA or Reload iCloud3", - "return": "MAIN MENU ᐳ Return to the Main Menu" -} - -WAZE_USED_HEADER = ("The Waze Route Service provides the travel time and distance information from your " - "current location to the Home or another tracked from zone. This information is used to determine " - "when the next location request should be made") -WAZE_HISTORY_USED_HEADER = ("The Waze History Data base stores 'close to zone' travel time and distance information " - "for a GPS location (100m radius). It reduces the number of internet requests to the Waze Servers " - "after it has been in use for a while and speed up response time when in a poor cell area") -PASSTHRU_ZONE_HEADER = ("You may be driving through a non-tracked zone but not stopping at tne zone. The Mobile " - "App issues an Enter Zone trigger when the device enters the zone and changes the " - "device_tracker entity state to the Zone. iCloud3 does not process the Enter Zone " - "trigger until the delay time has passed. This prevents processing a Zone Enter " - "trig[er that is immediately followed by an Exit Zone trigger.") -STAT_ZONE_HEADER = ("A Stationary Zone is automatically created if the device remains in the same location " - "(store, friends house, doctor`s office, etc.) for an extended period of time") -TRK_FROM_HOME_ZONE_HEADER =("Normally, the Home zone is used as the primary track-from-zone for the tracking results " - "(travel time, distance, etc). However, a different zone can be used as the base location " - "if you are away from Home for an extended period or the device is normally at another " - "location (vacation house, second home, parent's house, etc.). This is a global setting " - "that overrides the Primary Track-from-Home Zone assigned to an individual Device on the Update " - "Devices screen.") - - -#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # # ICLOUD3 CONFIG FLOW - INITIAL SETUP # @@ -506,12 +102,14 @@ def __init__(self): self.step_id = '' # step_id for the window displayed self.errors = {} # Errors en.json error key self.OptFlow = None + self.PyiCloud = None + self.apple_acct_reauth_username = None def form_msg(self): return f"Form-{self.step_id}, Errors-{self.errors}" - def _traceui(self, user_input): - _traceha(f"{user_input=} {self.errors=} ") + def _evlogui(self, user_input): + _log(f"{user_input=} {self.errors=} ") #---------------------------------------------------------------------- @staticmethod @@ -519,16 +117,15 @@ def _traceui(self, user_input): def async_get_options_flow(config_entry): ''' Create the options flow handler for iCloud3. This is called when the iCloud3 > Configure - is selected on the Devices & Services screen, not when HA or iCloud3 is loaed + is selected on the Devices & Services screen, not when HA or iCloud3 is loaded ''' Gb.OptionsFlowHandler = iCloud3_OptionsFlowHandler() return Gb.OptionsFlowHandler + #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -# -# CONFIG_FLOW FORMS -# +# USER #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_user(self, user_input=None): ''' @@ -543,18 +140,12 @@ async def async_step_user(self, user_input=None): disabled_by = config_entry.disabled_by added_datetime = config_entry.data.get('added') - # for config_entry in config_entries: - # _CF_LOGGER.info(f"ic3 config_entry {config_entry.disabled_by=}") - # _CF_LOGGER.info(f"ic3 config_entry {config_entry.data=}") - # _CF_LOGGER.info(f"ic3 config_entry {config_entry.data.get('added')=}") - except Exception as err: pass errors = {} await self.async_set_unique_id(DOMAIN) - # self._abort_if_unique_id_configured() if disabled_by: _CF_LOGGER.info(f"Aborting iCloud3 Integration, Already set up but Disabled") @@ -571,15 +162,17 @@ async def async_step_user(self, user_input=None): Gb.hass = self.hass start_ic3.initialize_directory_filenames() - config_file.load_storage_icloud3_configuration_file() - start_ic3.initialize_icloud_data_source() + await config_file.async_load_storage_icloud3_configuration_file() + start_ic3.initialize_data_source_variables() + + await file_io.async_make_directory(Gb.icloud_session_directory) # Convert the .storage/icloud3.configuration file if it is at a default # state or has never been updated via config_flow using 'HA Integrations > iCloud3' if Gb.conf_profile[CONF_VERSION] == -1: - self.migrate_v2_config_to_v3() + await self.async_migrate_v2_config_to_v3() - _CF_LOGGER.info(f"Config_Flow Added Integration-{Gb.ha_device_id_by_devicename=} {Gb.ha_area_id_by_devicename=}") + _CF_LOGGER.info(f"Config_Flow Added Integration-{Gb.ha_device_id_by_devicename=}") if user_input is not None: _CF_LOGGER.info(f"Added iCloud3 Integration") @@ -598,13 +191,16 @@ async def async_step_user(self, user_input=None): data_schema=schema, errors=errors) -#---------------------------------------------------------------------- - async def async_step_reauth(self, user_input=None, errors=None): + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# REAUTH +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + async def async_step_reauth(self, user_input=None, errors=None, called_from_step_id=None): ''' - Ask the verification code to the user. + Ask for the verification code from the user. The iCloud account needs to be verified. Show the code entry form, get the - code from the user, send the code back to Apple iCloud via pyicloud and get + code from the user, send the code back to Apple ID iCloud via pyicloud and get a valid code indicator or invalid code error. If the code is valid, either: @@ -622,11 +218,11 @@ async def async_step_reauth(self, user_input=None, errors=None): = None if the rquest is from another regular function during the normal tracking operation. ''' - # Config_flow is only set up on the initial add. This reauth uses some of the OptionsFlowHandler # functions so we need to set up that link when a reauth is needed if Gb.OptionsFlowHandler is None: Gb.OptionsFlowHandler = iCloud3_OptionsFlowHandler() + _OptFlow = Gb.OptionsFlowHandler self.step_id = 'reauth' self.errors = errors or {} @@ -634,55 +230,69 @@ async def async_step_reauth(self, user_input=None, errors=None): action_item = '' if user_input is None: - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='reauth', + data_schema=form_reauth(_OptFlow), errors=self.errors) - user_input, action_item = Gb.OptionsFlowHandler._action_text_to_item(user_input) - log_debug_msg(f"{self.step_id} ({action_item}) > UserInput-{user_input}, Errors-{errors}") + user_input, action_item = _OptFlow._action_text_to_item(user_input) + user_input = _OptFlow._strip_spaces(user_input, [CONF_VERIFICATION_CODE]) + user_input = _OptFlow._option_text_to_parm(user_input, 'account_selected', _OptFlow.apple_acct_items_by_username) - if (action_item == 'cancel_verification_entry' - or (action_item == 'send_verification_code' and user_input.get(CONF_VERIFICATION_CODE, '') == '')): - return self.async_abort(reason="verification_code_cancelled") + log_debug_msg(f"CF-{self.step_id.upper()} ({action_item}) > UserInput-{user_input}, Errors-{errors}") - if action_item == 'request_verification_code': - await Gb.hass.async_add_executor_job( - pyicloud_ic3_interface.pyicloud_reset_session, - Gb.PyiCloud) - self.errors['base'] = 'verification_code_requested2' + if 'account_selected' in user_input: + _OptFlow._get_conf_apple_acct_selected(user_input['account_selected']) + username = _OptFlow.conf_apple_acct[CONF_USERNAME] + password = _OptFlow.conf_apple_acct[CONF_PASSWORD] + _OptFlow.PyiCloud = Gb.PyiCloud_by_username.get(username) + else: + # When iCloud3 creates the PyiCloud object for the Apple account during startup, + # a 2fa needed check is made. If it is needed, a reauthentication is needed executive + # job is run that tells HA to issue a notification. The PyiCloud object is saved + # to be used here + user_input = None + username = Gb.PyiCloud_needing_reauth_via_ha[CONF_USERNAME] + password = Gb.PyiCloud_needing_reauth_via_ha[CONF_PASSWORD] + acct_owner = Gb.PyiCloud_needing_reauth_via_ha['account_owner'] - elif (action_item == 'send_verification_code' - and CONF_VERIFICATION_CODE in user_input - and user_input[CONF_VERIFICATION_CODE]): + _OptFlow.apple_acct_reauth_username = username - valid_code = await Gb.hass.async_add_executor_job( - Gb.PyiCloud.validate_2fa_code, - user_input[CONF_VERIFICATION_CODE]) + if _OptFlow.PyiCloud is None: + self.errors['base'] = 'icloud_acct_not_logged_into' + action_item = 'cancel_return' - # Do not restart iC3 right now if the username/password was changed on the - # iCloud setup screen. If it was changed, another account is being logged into - # and it will be restarted when exiting the configurator. - if valid_code: - post_event( f"{EVLOG_NOTICE}The Verification Code was accepted ({user_input[CONF_VERIFICATION_CODE]})") - post_event(f"{EVLOG_NOTICE}iCLOUD ALERT > Apple ID Verification complete") + elif (action_item == 'send_verification_code' + and user_input.get(CONF_VERIFICATION_CODE, '') == ''): + action_item = 'cancel_return' - Gb.EvLog.clear_evlog_greenbar_msg() - Gb.EvLog.update_event_log_display("") - start_ic3.set_primary_data_source(FAMSHR) - Gb.PyiCloud.new_2fa_code_already_requested_flag = False + if action_item == 'cancel_return': + _OptFlow.clear_PyiCloud_2fa_flags() + return self.async_abort(reason="verification_code_cancelled") - Gb.authenticated_time = time.time() - return self.async_abort(reason="verification_code_accepted") + if action_item == 'send_verification_code': + valid_code = await _OptFlow.reauth_send_verification_code_handler(_OptFlow, user_input) + if valid_code: + self.errors['base'] = 'verification_code_accepted' + if instr(str(_OptFlow.apple_acct_items_by_username), 'AUTHENTICATION'): + _OptFlow.conf_apple_acct = '' + else: + _OptFlow.clear_PyiCloud_2fa_flags() + return self.async_abort(reason="verification_code_accepted") else: - post_event( f"{EVLOG_ALERT}The Apple ID Verification Code is invalid " - f"({user_input[CONF_VERIFICATION_CODE]})") self.errors[CONF_VERIFICATION_CODE] = 'verification_code_invalid' - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + elif action_item == 'request_verification_code': + await _OptFlow.async_pyicloud_reset_session(username, password) + + return self.async_show_form(step_id='reauth', + data_schema=form_reauth(_OptFlow), errors=self.errors) -#------------------------------------------------------------------------------------------- + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# RESTART HA +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_restart_ha(self, user_input=None, errors=None): ''' A restart is required if there were devicenames in known_devices.yaml @@ -703,13 +313,13 @@ async def async_step_restart_ha(self, user_input=None, errors=None): data = {'added': dt_util.now().strftime(DATETIME_FORMAT)[0:19]} return self.async_create_entry(title="iCloud3", data=data) - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema('restart_ha'), + return self.async_show_form(step_id='restart_ha', + data_schema=form_restart_ha_ic3(self), errors=self.errors, last_step=False) #------------------------------------------------------------------------------------------- - def migrate_v2_config_to_v3(self): + async def async_migrate_v2_config_to_v3(self): ''' Migrate v2 to v3 if needed @@ -719,8 +329,9 @@ def migrate_v2_config_to_v3(self): ''' # if a platform: icloud3 statement or config_ic3.yaml, migrate the files if Gb.ha_config_platform_stmt: - config_file.load_icloud3_ha_config_yaml(Gb.config) - elif os.path.exists(Gb.hass.config.path('config_ic3.yaml')): + await Gb.hass.async_add_executor_job(config_file.load_icloud3_ha_config_yaml, Gb.config) + + elif file_io.file_exists(Gb.hass.config.path('config_ic3.yaml')): pass else: return @@ -729,42 +340,12 @@ def migrate_v2_config_to_v3(self): v2v3_config_migration.convert_v2_config_files_to_v3() v2v3_config_migration.remove_ic3_devices_from_known_devices_yaml_file() - config_file.load_storage_icloud3_configuration_file() + config_file.async_load_storage_icloud3_configuration_file() Gb.v2v3_config_migrated = True if Gb.restart_ha_flag: pass -#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - def form_schema(self, step_id): - if step_id == 'reauth': - self.actions_list = REAUTH_CONFIG_FLOW_ACTIONS.copy() - - return vol.Schema({ - vol.Optional(CONF_VERIFICATION_CODE): - selector.TextSelector(), - vol.Required('action_items', - default=Gb.OptionsFlowHandler.action_default_text('send_verification_code')): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - -#------------------------------------------------------------------------ - elif step_id == 'restart_ha': - - restart_default = 'restart_ha' - self.actions_list = [] - self.actions_list.append(ACTION_LIST_ITEMS_KEY_TEXT['restart_ha']) - self.actions_list.append(ACTION_LIST_ITEMS_KEY_TEXT['restart_ic3_later']) - - actions_list_default = Gb.OptionsFlowHandler.action_default_text(restart_default) - - return vol.Schema({ - vol.Required('action_items', - default=actions_list_default): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -779,6 +360,7 @@ def __init__(self, settings=False): self.step_id = '' # step_id for the window displayed self.errors = {} # Errors en.json error key self.errors_entered_value = {} + self.config_file_commit_updates = False # The config file has been updated and needs to be written self.initialize_options() if settings: @@ -791,72 +373,71 @@ def initialize_options(self): self.v2v3_migrated_flag = False # Set to True when the conf_profile[VERSION] = -1 when first loaded self.errors = {} # Errors en.json error key - self.user_input_multi_form = {} # Saves the user_input from form #1 on a multi-form update + self.multi_form_hdr = '' # multi-form - text string displayed on the called form + self.multi_form_user_input = {} # multi-form - user_input to be restored when returning to calling form self.errors_user_input = {} # user_input text for a value with an error self.step_id = '' # step_id for the window displayed self.menu_item_selected = [ MENU_KEY_TEXT_PAGE_0[MENU_PAGE_0_INITIAL_ITEM], MENU_KEY_TEXT_PAGE_1[MENU_PAGE_1_INITIAL_ITEM]] self.menu_page_no = 0 # Menu currently displayed self.header_msg = None # Message displayed on menu after update - 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.called_from_step_id_1 = '' # Form/Fct to return to when verifying the icloud auth code + self.called_from_step_id_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_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.config_parms_update_control = [] # Stores the type of parameters that were updated, used to reinitialize parms self.code_to_schema_pass_value = None # Variables used for icloud_account update forms self.logging_into_icloud_flag = False - self._existing_entry = None # Variables used for device selection and update on the device_list and device_update forms - self.form_devices_list_all = [] # List of the devices in the Gb.conf_tracking[DEVICES] parameter - self.form_devices_list_displayed = [] # List of the devices displayed on the device_list form - self.form_devices_list_devicename = [] # List of the devicenames in the Gb.conf_tracking[DEVICES] parameter - self.next_page_devices_list = [] - self.device_list_page_no = 0 # Devices List form page number, starting with 0 - self.device_list_page_selected_idx = \ - [idx for idx in range(0, len(Gb.conf_devices)+10, 5)] # Device selected on each display page + self.device_items_by_devicename = {} # List of the apple_accts in the Gb.conf_tracking[apple_accts] parameter + self.device_items_displayed = [] # List of the apple_accts displayed on the device_list form + self.dev_page_item = ['', '', '', '', ''] # Device's devicename last displayed on each page + self.dev_page_no = 0 # apple_accts List form page number, starting with 0 + self.display_rarely_updated_parms = False # Display the fixed interval & track from zone parameters + self.ic3_devicename_being_updated = '' # Devicename currently being updated - self.conf_device_selected = {} - self.conf_device_selected_idx = 0 - self.sensor_entity_attrs_changed = {} # Contains info regarding update_device and needed entity changes + self.conf_device = {} + self.conf_device_idx = 0 + self.conf_device_update_control = {} # Contains info regarding update_device and needed entity changes self.device_list_control_default = 'select' # Select the Return to main menu as the default self.add_device_flag = False self.add_device_enter_devicename_form_part_flag = False # Add device started, True = form top part only displayed - self.all_famshr_devices = True - self.devicename_device_info_famshr = {} - self.devicename_device_id_famshr = {} - self.devicename_device_info_fmf = {} - self.devicename_device_id_fmf = {} - self.device_id_devicename_fmf = {} - self.device_trkr_by_entity_id_all = {} # other platform device_tracker used to validate the ic3 entity is not used - - # Option selection lists on the Update devices screen - self.famshr_list_text_by_fname = {} - self.famshr_list_text_by_fname_base = NONE_DICT_KEY_TEXT.copy() - self.fmf_list_text_by_email = {} - self.fmf_list_text_by_email_base = NONE_DICT_KEY_TEXT.copy() + self.device_trkr_by_entity_id_all = {} # other platform device_tracker used to validate the ic3 entity is not used + + # Option selection lists on the Update apple_accts screen + self.apple_acct_items_list = [] # List of the apple_accts in the Gb.conf_tracking[apple_accts] parameter + self.apple_acct_items_displayed = [] # List of the apple_accts displayed on the device_list form + self.aa_page_item = ['', '', '', '', ''] # Apple acct username last displayed on each page + self.aa_page_no = 0 # apple_accts List form page number, starting with 0 + self.conf_apple_acct = '' # apple acct item selected + self.apple_acct_items_by_username = {} # Selection list for the apple accounts on data_sources and reauth screens + self.aa_idx = 0 + self.apple_acct_reauth_username = None + self.add_apple_acct_flag = False + + self.icloud_list_text_by_fname = {} + self.icloud_list_text_by_fname2 = {} + self.icloud_list_text_by_fname_base = NONE_DICT_KEY_TEXT.copy() self.mobapp_list_text_by_entity_id = {} # mobile_app device_tracker info used in devices form for mobapp selection - self.mobapp_list_text_by_entity_id = MOBAPP_DEVICE_NONE_ITEMS_KEY_TEXT.copy() - self.picture_by_filename = {} - self.picture_by_filename_base = NONE_DICT_KEY_TEXT.copy() - self.zone_name_key_text = {} + self.mobapp_list_text_by_entity_id = MOBAPP_DEVICE_NONE_OPTIONS.copy() + self.picture_by_filename = {} + self.picture_by_filename_base = NONE_DICT_KEY_TEXT.copy() + self.zone_name_key_text = {} + self.opt_picture_file_name_list = [] - self.opt_picture_file_name_list = [] - - self.devicename_by_famshr_fmf = {} - self.mobapp_search_for_devicename = 'None' + self.mobapp_scan_for_for_devicename = 'None' self.inactive_devices_key_text = {} self.log_level_devices_key_text = {} - self._verification_code = None + self.is_verification_code_needed = False # Variables used for the display_text_as update - self.dta_selected_idx = -1 # Current conf index being updated + self.dta_selected_idx = UNSELECTED # Current conf index being updated self.dta_selected_idx_page = [0, 5] # Selected idx to display on each page self.dta_page_no = 0 # Current page being displayed self.dta_working_copy = {0: '', 1: '', 2: '', 3: '', 4: '', 5: '', 6: '', 7: '', 8: '', 9: '',} @@ -889,7 +470,12 @@ def initialize_options(self): # 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._initialize_self_PyiCloud_fields_from_Gb() + self._build_apple_accounts_list() + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# INIT +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_init(self, user_input=None): if self.initialize_options_required_flag: self.initialize_options() @@ -901,20 +487,19 @@ 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 _evlogui(self, user_input): + _log(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] + conf_apple_acct, _idx = config_file.conf_apple_acct(0) + self.username = conf_apple_acct[CONF_USERNAME] + self.password = conf_apple_acct[CONF_PASSWORD] + + self.PyiCloud = Gb.PyiCloud_by_username.get(self.username) + self.data_source = Gb.conf_tracking[CONF_DATA_SOURCE] + self.endpoint_suffix = Gb.conf_tracking[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] or \ + Gb.icloud_server_endpoint_suffix #------------------------------------------------------------------------------------------- def _set_initial_icloud3_device_tracker_area_id(self): @@ -938,7 +523,10 @@ def _set_initial_icloud3_device_tracker_area_id(self): self.update_area_id_personal_device(ICLOUD3) -#------------------------------------------------------------------------------------------- + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# MENU +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_menu_0(self, user_input=None, errors=None): self.menu_page_no = 0 return await self.async_step_menu(user_input, errors) @@ -948,27 +536,31 @@ async def async_step_menu_1(self, user_input=None, errors=None): return await self.async_step_menu(user_input, errors) async def async_step_menu(self, user_input=None, errors=None): - '''Main Menu displays different screens for parameter entry''' + self.step_id = f"menu_{self.menu_page_no}" + self.called_from_step_id_1 = self.called_from_step_id_2 = '' + self.errors = errors or {} + await self._async_write_storage_icloud3_configuration_file() + Gb.trace_prefix = 'CONFIG' Gb.config_flow_flag = True - if Gb.PyiCloud and self.PyiCloud is None: - self.PyiCloud = Gb.PyiCloud - Gb.PyiCloudConfigFlow = self.PyiCloud + if (self.username != '' and self.password != '' + and instr(Gb.conf_tracking[CONF_DATA_SOURCE], ICLOUD) is False): + self.header_msg = 'icloud_acct_data_source_warning' - if self.PyiCloud is None and self.username: + elif self.PyiCloud is None and self.username: self.header_msg = 'icloud_acct_not_logged_into' - self.step_id = f"menu_{self.menu_page_no}" - self.called_from_step_id_1 = self.called_from_step_id_2 ='' - self.errors = {} + elif self.is_verification_code_needed: + self.header_msg = 'verification_code_needed' + if user_input is None: self._set_inactive_devices_header_msg() self._set_header_msg() return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + data_schema=form_menu(self), errors=self.errors) self.menu_item_selected[self.menu_page_no] = user_input['menu_items'] @@ -987,19 +579,18 @@ async def async_step_menu(self, user_input=None, errors=None): if Gb.conf_profile[CONF_VERSION] <= 0: self.v2v3_migrated_flag = (Gb.conf_profile[CONF_VERSION] == 0) Gb.conf_profile[CONF_VERSION] = 1 - config_file.write_storage_icloud3_configuration_file() + self._update_config_file_tracking(user_input) - Gb.config_flow_updated_parms = self.config_flow_updated_parms - if ('restart' in self.config_flow_updated_parms + if ('restart' in self.config_parms_update_control or self._set_inactive_devices_header_msg() in ['all', 'most']): return await self.async_step_restart_icloud3() else: - self.config_flow_updated_parms = {''} - data = {} - data = {'updated': dt_util.now().strftime(DATETIME_FORMAT)[0:19]} - log_debug_msg(f"Exit Configure Settings, UpdateParms-{Gb.config_flow_updated_parms}") + Gb.config_parms_update_control = self.config_parms_update_control.copy() + self.config_parms_update_control = [] + log_debug_msg(f"Exit Configure Settings, UpdateParms-{list_to_str(Gb.config_parms_update_control)}") + data = {'updated': dt_util.now().strftime(DATETIME_FORMAT)[0:19]} return self.async_create_entry(title="iCloud3", data={}) elif menu_action_item == 'next_page_0': @@ -1011,14 +602,12 @@ async def async_step_menu(self, user_input=None, errors=None): elif 'menu_item' == '': pass - elif menu_item == 'icloud_account': - return await self.async_step_icloud_account() + elif menu_item == 'data_source': + return await self.async_step_data_source() elif menu_item == 'verification_code': return await self.async_step_reauth() elif menu_item == 'device_list': return await self.async_step_device_list() - elif menu_item == 'change_device_order': - return await self.async_step_change_device_order() elif menu_item == 'away_time_zone': return await self.async_step_away_time_zone() elif menu_item == 'format_settings': @@ -1042,71 +631,74 @@ async def async_step_menu(self, user_input=None, errors=None): self._set_header_msg() return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + data_schema=form_menu(self), errors=self.errors, last_step=False) -#------------------------------------------------------------------------------------------- + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# RESTART ICLOUD3 +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_restart_icloud3(self, user_input=None, errors=None): ''' A restart is required due to tracking, devices or sensors changes. Ask if this should be done now or later. ''' + self.step_id = 'restart_icloud3' self.errors = errors or {} self.errors_user_input = {} + await self._async_write_storage_icloud3_configuration_file() user_input, action_item = self._action_text_to_item(user_input) if user_input is not None or action_item is not 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': - 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'): await Gb.hass.services.async_call("homeassistant", "restart") return self.async_abort(reason="ha_restarting") elif action_item == 'review_inactive_devices': - self.called_from_step_id_1 = self.step_id + self.called_from_step_id_1 = 'restart_icloud3' return await self.async_step_review_inactive_devices() + if action_item == 'restart_ic3_now': + Gb.config_parms_update_control = self.config_parms_update_control.copy() + + elif action_item == 'restart_ic3_later': + if 'restart' in self.config_parms_update_control: + list_del(self.config_parms_update_control, 'restart') + Gb.config_parms_update_control = self.config_parms_update_control.copy() + self.config_parms_update_control = [] - self.config_flow_updated_parms = {''} data = {} data = {'added': dt_util.now().strftime(DATETIME_FORMAT)[0:19]} - log_debug_msg(f"Exit Configure Settings, UpdateParms-{Gb.config_flow_updated_parms}") + log_debug_msg(f"Exit Configure Settings, UpdateParms-{Gb.config_parms_update_control}") + # 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. return self.async_create_entry(title="iCloud3", data={}) self._set_inactive_devices_header_msg() self._set_header_msg() - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema('restart_icloud3'), + return self.async_show_form(step_id='restart_icloud3', + data_schema=form_restart_icloud3(self), errors=self.errors, last_step=False) -#------------------------------------------------------------------------------------------- + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# REVIEW INACTIVE DEVICES +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_review_inactive_devices(self, user_input=None, errors=None): ''' There are inactive devices. Display the list of devices and confirm they should remain active. - ACTION_LIST_ITEMS_KEY_TEXT['inactive_to_track'], - ACTION_LIST_ITEMS_KEY_TEXT['inactive_keep_inactive']] + ACTION_LIST_OPTIONS['inactive_to_track'], + ACTION_LIST_OPTIONS['inactive_keep_inactive']] ''' self.step_id = 'review_inactive_devices' self.errors = errors or {} @@ -1124,8 +716,7 @@ async def async_step_review_inactive_devices(self, user_input=None, errors=None) if conf_device[CONF_IC3_DEVICENAME] in devicename_list: conf_device[CONF_TRACKING_MODE] = TRACK_DEVICE - config_file.write_storage_icloud3_configuration_file() - self.config_flow_updated_parms.update(['tracking', 'restart']) + self._update_config_file_tracking(user_input) self.header_msg = 'action_completed' if self.called_from_step_id_1 == 'restart_icloud3': @@ -1133,8 +724,8 @@ async def async_step_review_inactive_devices(self, user_input=None, errors=None) return await self.async_step_menu() - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema('review_inactive_devices'), + return self.async_show_form(step_id='review_inactive_devices', + data_schema=form_review_inactive_devices(self), errors=self.errors, last_step=False) @@ -1163,7 +754,7 @@ def common_form_handler(self, user_input=None, action_item=None, errors=None): if action_item == 'cancel': return True - elif self.step_id == 'icloud_account': + elif self.step_id == 'data_source': pass elif self.step_id == 'device_list': user_input = self._get_conf_device_selected(user_input) @@ -1184,10 +775,10 @@ def common_form_handler(self, user_input=None, action_item=None, errors=None): elif self.step_id == "sensors": self._remove_and_create_sensors(user_input) - log_debug_msg(f"{self.step_id} ({action_item}) > UserInput-{user_input}, Errors-{errors}") + self.log_step_info(user_input, action_item) post_event(f"Configuration Changed > Type-{self.step_id.replace('_', ' ').title()}") - self._update_configuration_file(user_input) + self._update_config_file_general(user_input) # Redisplay the menu if there were no errors if not self.errors: @@ -1196,7 +787,10 @@ def common_form_handler(self, user_input=None, action_item=None, errors=None): # Display the config data entry form, any errors will be redisplayed and highlighted return False -#------------------------------------------------------------------------------------------- + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# FORMAT SETTINGS +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_format_settings(self, user_input=None, errors=None): self.step_id = 'format_settings' user_input, action_item = self._action_text_to_item(user_input) @@ -1207,168 +801,47 @@ async def async_step_format_settings(self, user_input=None, errors=None): if self.errors != {} and self.errors.get('base') != 'conf_updated': self.errors['action_items'] = 'update_aborted' - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='format_settings', + data_schema=form_format_settings(self), errors=self.errors) #------------------------------------------------------------------------------------------- @staticmethod - def _format_device_info(conf_device): - device_info = ( f"{conf_device[CONF_FNAME]}{RARROW}" - f"{conf_device[CONF_IC3_DEVICENAME]}, " - f"{DEVICE_TYPE_FNAME.get(conf_device[CONF_DEVICE_TYPE])}") + def _format_device_text_hdr(conf_device): + device_text = ( f"{conf_device[CONF_FNAME]} " + f"({conf_device[CONF_IC3_DEVICENAME]})") if conf_device[CONF_TRACKING_MODE] == MONITOR_DEVICE: - device_info += ", MONITOR" + device_text += ", MONITOR" elif conf_device[CONF_TRACKING_MODE] == INACTIVE_DEVICE: - device_info += ", INACTIVE" - - return device_info - -#------------------------------------------------------------------------------------------- - async def async_step_change_device_order(self, user_input=None, errors=None, called_from_step_id=None): - self.step_id = 'change_device_order' - user_input, action_item = self._action_text_to_item(user_input) - self.called_from_step_id_1 = called_from_step_id or self.called_from_step_id_1 or 'menu_0' - - if user_input is None: - log_debug_msg(f"{self.step_id} ({action_item}) > UserInput-{user_input}, Errors-{errors}") - self.cdo_devicenames = [self._format_device_info(conf_device) - for conf_device in Gb.conf_devices] - self.cdo_new_order_idx = [x for x in range(0, len(Gb.conf_devices))] - self.actions_list_default = 'move_down' - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), - errors=self.errors) - - if action_item == 'save': - new_conf_devices = [] - for idx in self.cdo_new_order_idx: - new_conf_devices.append(Gb.conf_devices[idx]) - - Gb.conf_devices = new_conf_devices - config_file.set_conf_devices_index_by_devicename() - config_file.write_storage_icloud3_configuration_file() - self.config_flow_updated_parms.update(['restart', 'profile']) - self.errors['base'] = 'conf_updated' - - action_item = 'cancel' - - if action_item == 'cancel': - return self.async_show_form(step_id=self.called_from_step_id_1, - data_schema=self.form_schema(self.called_from_step_id_1), - errors=self.errors) - - self.cdo_curr_idx = self.cdo_devicenames.index(user_input['device_desc']) - - new_idx = self.cdo_curr_idx - if action_item == 'move_up': - if new_idx > 0: - new_idx = new_idx - 1 - if action_item == 'move_down': - if new_idx < len(self.cdo_devicenames) - 1: - new_idx = new_idx + 1 - self.actions_list_default = action_item - - if new_idx != self.cdo_curr_idx: - self.cdo_devicenames[self.cdo_curr_idx], self.cdo_devicenames[new_idx] = \ - self.cdo_devicenames[new_idx], self.cdo_devicenames[self.cdo_curr_idx] - self.cdo_new_order_idx[self.cdo_curr_idx], self.cdo_new_order_idx[new_idx] = \ - self.cdo_new_order_idx[new_idx], self.cdo_new_order_idx[self.cdo_curr_idx] - - self.cdo_curr_idx = new_idx - - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), - errors=self.errors) - -#------------------------------------------------------------------------------------------- - async def async_step_away_time_zone(self, user_input=None, errors=None): - self.step_id = 'away_time_zone' - user_input, action_item = self._action_text_to_item(user_input) - - self._build_away_time_zone_devices_list() - self._build_away_time_zone_hours_list() - - if self.common_form_handler(user_input, action_item, errors): - return await self.async_step_menu() - - if self._any_errors(): - self.errors['action_items'] = 'update_aborted' - - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), - errors=self.errors) - -#------------------------------------------------------------------------------------------- - def _build_away_time_zone_hours_list(self): - if self.away_time_zone_hours_key_text != {}: - return - - ha_time = int(Gb.this_update_time[0:2]) - for hh in range(ha_time-12, ha_time+13): - away_hh = hh + 24 if hh < 0 else hh - - if away_hh == 0: ap_hh = 12; ap = 'a' - elif away_hh < 12: ap_hh = away_hh; ap = 'a' - elif away_hh == 12: ap_hh = 12; ap = 'p' - else: ap_hh = away_hh - 12; ap = 'p' - - if away_hh >= 24: - away_hh -= 24 - if ap_hh == 12: ap = 'a' - elif ap_hh >= 13: ap_hh -= 12; ap = 'a' - - if Gb.time_format_12_hour: - time_str = f"{ap_hh:}{Gb.this_update_time[2:]}{ap}" - else: - time_str = f"{away_hh:02}{Gb.this_update_time[2:]}" - - if away_hh == ha_time: - time_str = f"Home Time Zone" - elif hh < ha_time: - time_str += f" (-{abs(hh-ha_time):} hours)" - else: - time_str += f" (+{abs(ha_time-hh):} hours)" - self.away_time_zone_hours_key_text[hh-ha_time] = time_str - -#------------------------------------------------------------------------------------------- - def _build_away_time_zone_devices_list(self): - - #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): + device_text += ", ✪ INACTIVE" - 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) + return device_text -#------------------------------------------------------------------------------------------- - 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, - called_from_step_id=None): +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# CONFIRM ACTION +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + async def async_step_confirm_action(self, user_input=None, + extra_action_items=None, + actions_list_default=None, + confirm_action_form_hdr=None, + called_from_step_id=None): ''' Confirm an action - This will display a screen containing the action_items. Parameters: - action_items - The action_item keys in the ACTION_LIST_ITEMS_KEY_TEXT dictionary. + action_items - The action_item keys in the ACTION_LIST_OPTIONS dictionary. The last key is the default item on the confirm actions screen. called_from_step_id - The name of the step to return to. + self.multi_form_user_input['confirm_msg'] = Text message to be displayed on the + Confirm Actions screen Notes: - Before calling this function, set the self.user_input_multi_form to the user_input. + Before calling this function, set the self.multi_form_user_input to the user_input. This will preserve all parameter changes in the calling screen. They are returned to the called from step on exit. Action item - The action_item selected on this screen is added to the - self.user_input_multi_form variable returned. It is resolved in the calling + self.multi_form_user_input variable returned. It is resolved in the calling step in the self._action_text_to_item function in the calling step. On Return - Set the function to return to for the called_from_step_id. ''' @@ -1377,21 +850,22 @@ async def async_step_confirm_action(self, user_input=None, action_items=None, self.errors_user_input = {} self.called_from_step_id_1 = called_from_step_id or self.called_from_step_id_1 or 'menu_0' - if action_items is not None: - actions_list = [] - for action_item in action_items: - actions_list.append(ACTION_LIST_ITEMS_KEY_TEXT[action_item]) - - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id, - actions_list=actions_list), - errors=self.errors) + if user_input is None: + return self.async_show_form(step_id='confirm_action', + data_schema=form_confirm_action(self, + extra_action_items, + actions_list_default, + confirm_action_form_hdr), + errors=self.errors) user_input, action_item = self._action_text_to_item(user_input) - self.user_input_multi_form['action_item'] = action_item - if self.called_from_step_id_1 == 'icloud_account': - return await self.async_step_icloud_account(user_input=self.user_input_multi_form) + if action_item == 'confirm_action': + if self.called_from_step_id_1 == 'data_source/remove_account': + self._delete_apple_acct(self.multi_form_user_input) + + if self.called_from_step_id_1 == 'data_source/remove_account': + return await self.async_step_data_source() return await self.async_step_menu() @@ -1403,15 +877,11 @@ def _set_example_zone_name(self): 'name': 'iCloud3 reformated Zone entity_id (zone.the_shores → TheShores)', 'title': 'iCloud3 reformated Zone entity_id (zone.the_shores → The Shores)' ''' - DISPLAY_ZONE_FORMAT_ITEMS_KEY_TEXT.update(DISPLAY_ZONE_FORMAT_ITEMS_KEY_TEXT_BASE) + DISPLAY_ZONE_FORMAT_OPTIONS.update(DISPLAY_ZONE_FORMAT_OPTIONS_BASE) - # Zone = [Zone for zone, Zone in Gb.Zones_by_zone.items() - # if Zone.radius_m > 1 and instr(Zone.zone, '_')] Zone = [Zone for zone, Zone in Gb.HAZones_by_zone.items() if instr(Zone.zone, '_')] if Zone == []: - # Zone = [Zone for zone, Zone in Gb.Zones_by_zone.items() - # if Zone.radius_m > 1 and zone != 'home'] Zone = [Zone for zone, Zone in Gb.HAZones_by_zone.items() if zone != 'home'] if Zone != []: @@ -1426,18 +896,22 @@ def _set_example_zone_name(self): self._dzf_set_example_zone_name_text(TITLE, 'The Shores', exZone.title) def _dzf_set_example_zone_name_text(self, key, example_text, real_text): - if key in DISPLAY_ZONE_FORMAT_ITEMS_KEY_TEXT: - DISPLAY_ZONE_FORMAT_ITEMS_KEY_TEXT[key] = \ - DISPLAY_ZONE_FORMAT_ITEMS_KEY_TEXT[key].replace(example_text, real_text) -#------------------------------------------------------------------------------------------- + if key in DISPLAY_ZONE_FORMAT_OPTIONS: + DISPLAY_ZONE_FORMAT_OPTIONS[key] = \ + DISPLAY_ZONE_FORMAT_OPTIONS[key].replace(example_text, real_text) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# TRACKING PARAMETERS +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_tracking_parameters(self, user_input=None, errors=None): self.step_id = 'tracking_parameters' user_input, action_item = self._action_text_to_item(user_input) if self.www_directory_list == []: - directory_list, start_dir, file_filter = [True, 'www', []] + start_dir = 'www' self.www_directory_list = await Gb.hass.async_add_executor_job( - get_directory_list, + file_io.get_directory_list, start_dir) if self.common_form_handler(user_input, action_item, errors): @@ -1450,43 +924,14 @@ async def async_step_tracking_parameters(self, user_input=None, errors=None): if self._any_errors(): self.errors['action_items'] = 'update_aborted' - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='tracking_parameters', + data_schema=form_tracking_parameters(self), errors=self.errors) -#------------------------------------------------------------------------------------------- - async def xasync_step_tracking_parameters(self, user_input=None, errors=None): - self.step_id = 'tracking_parameters' - user_input, action_item = self._action_text_to_item(user_input) - - if self.www_directory_list == []: - dir_filters = ['/.', 'deleted', '/x-'] - path_config_base = f"{Gb.ha_config_directory}/" - back_slash = '\\' - - for path, dirs, files in os.walk(f"{path_config_base}www"): - www_sub_directory = path.replace(path_config_base, '') - in_filter_cnt = len([filter for filter in dir_filters if instr(www_sub_directory, filter)]) - if in_filter_cnt > 0 or www_sub_directory.count('/') > 4 or www_sub_directory.count(back_slash) > 4: - continue - - self.www_directory_list.append(www_sub_directory) - - if self.common_form_handler(user_input, action_item, errors): - if action_item == 'save': - Gb.picture_www_dirs = Gb.conf_profile[CONF_PICTURE_WWW_DIRS].copy() - self.picture_by_filename = {} - return await self.async_step_menu() - - - if self._any_errors(): - self.errors['action_items'] = 'update_aborted' - - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), - errors=self.errors) -#------------------------------------------------------------------------------------------- +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# INZONE INTERVALS +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_inzone_intervals(self, user_input=None, errors=None): self.step_id = 'inzone_intervals' user_input, action_item = self._action_text_to_item(user_input) @@ -1497,11 +942,14 @@ async def async_step_inzone_intervals(self, user_input=None, errors=None): if self._any_errors(): self.errors['action_items'] = 'update_aborted' - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='inzone_intervals', + data_schema=form_inzone_intervals(self), errors=self.errors) -#------------------------------------------------------------------------------------------- + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# WAZE MAIN +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_waze_main(self, user_input=None, errors=None): self.step_id = 'waze_main' user_input, action_item = self._action_text_to_item(user_input) @@ -1512,16 +960,19 @@ async def async_step_waze_main(self, user_input=None, errors=None): if self._any_errors(): self.errors['action_items'] = 'update_aborted' - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='waze_main', + data_schema=form_waze_main(self), errors=self.errors, last_step=True) -#------------------------------------------------------------------------------------------- + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# SPECIAL ZONES +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_special_zones(self, user_input=None, errors=None): self.step_id = 'special_zones' user_input, action_item = self._action_text_to_item(user_input) - await self._build_zone_list() + await self._build_zone_selection_list() if self.common_form_handler(user_input, action_item, errors): return await self.async_step_menu() @@ -1529,21 +980,26 @@ async def async_step_special_zones(self, user_input=None, errors=None): if self._any_errors(): self.errors['action_items'] = 'update_aborted' - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='special_zones', + data_schema=form_special_zones(self), errors=self.errors) -#------------------------------------------------------------------------------------------- +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# SENSORS +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_sensors(self, user_input=None, errors=None): + self.step_id = 'sensors' + self.errors = errors or {} + await self._async_write_storage_icloud3_configuration_file() user_input, action_item = self._action_text_to_item(user_input) if Gb.conf_sensors[CONF_EXCLUDED_SENSORS] == []: Gb.conf_sensors[CONF_EXCLUDED_SENSORS] = ['None'] if user_input is None: - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='sensors', + data_schema=form_sensors(self), errors=self.errors) if HOME_DISTANCE not in user_input[CONF_SENSORS_TRACKING_DISTANCE]: @@ -1571,14 +1027,19 @@ async def async_step_sensors(self, user_input=None, errors=None): if self._any_errors(): self.errors['action_items'] = 'update_aborted' - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='sensors', + data_schema=form_sensors(self), errors=self.errors) -#------------------------------------------------------------------------------------------- + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# EXCLUDE SENSORS +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_exclude_sensors(self, user_input=None, errors=None): + self.step_id = 'exclude_sensors' - self.errors = {} + self.errors = errors or {} + await self._async_write_storage_icloud3_configuration_file() user_input, action_item = self._action_text_to_item(user_input) if self.excluded_sensors == []: @@ -1594,11 +1055,11 @@ async def async_step_exclude_sensors(self, user_input=None, errors=None): self.sensors_fname_list.sort() if user_input is None: - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='exclude_sensors', + data_schema=form_exclude_sensors(self), errors=self.errors) - log_debug_msg(f"{self.step_id} ({action_item}) > UserInput-{user_input}, Errors-{errors}") + self.log_step_info(user_input, action_item) sensors_list_filter = user_input['filter'].lower().replace('?', '').strip() if (self.sensors_list_filter == sensors_list_filter @@ -1608,23 +1069,23 @@ async def async_step_exclude_sensors(self, user_input=None, errors=None): else: self.sensors_list_filter = sensors_list_filter or '?' - if action_item == 'cancel': + if action_item == 'cancel_return': return await self.async_step_sensors() - if (action_item == 'save' + if (action_item == 'save_stay' or user_input[CONF_EXCLUDED_SENSORS] != self.excluded_sensors or user_input['filtered_sensors'] != []): - self._update_excluded_sensors(user_input) + user_input = self._update_excluded_sensors(user_input) if Gb.conf_sensors[CONF_EXCLUDED_SENSORS] != self.excluded_sensors: Gb.conf_sensors[CONF_EXCLUDED_SENSORS] = self.excluded_sensors.copy() - config_file.write_storage_icloud3_configuration_file() + self._update_config_file_general(user_input, update_config_flag=True) self.errors['excluded_sensors'] = 'excluded_sensors_ha_restart' - self.config_flow_updated_parms.update(['restart_ha', 'restart']) + list_add(self.config_parms_update_control, ['restart_ha', 'restart']) - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='exclude_sensors', + data_schema=form_exclude_sensors(self), errors=self.errors) #------------------------------------------------------------------------------------------- @@ -1648,35 +1109,37 @@ def _update_excluded_sensors(self, user_input): elif len(self.excluded_sensors) > 1 and 'None' in self.excluded_sensors: self.excluded_sensors.remove('None') + # Add filtered sensors just selected/unselected + user_input[CONF_EXCLUDED_SENSORS] = self.excluded_sensors.copy() + return user_input + + #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -# -# DISPLAY_TEXT_AS HANDLER -# +# DISPLAY TEXT AS #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - async def async_step_display_text_as(self, user_input=None, errors=None): self.step_id = 'display_text_as' user_input, action_item = self._action_text_to_item(user_input) # Reinitialize everything - if self.dta_selected_idx == -1: + if self.dta_selected_idx == UNSELECTED: self.dta_selected_idx = 0 self.dta_selected_idx_page = [0, 5] self.dta_page_no = 0 - idx = -1 + idx = UNSELECTED for dta_text in Gb.conf_general[CONF_DISPLAY_TEXT_AS]: idx += 1 self.dta_working_copy[idx] = dta_text if user_input is None: - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='display_text_as', + data_schema=form_display_text_as(self), errors=self.errors) user_input = self._option_text_to_parm(user_input, CONF_DISPLAY_TEXT_AS, self.dta_working_copy) self.dta_selected_idx = int(user_input[CONF_DISPLAY_TEXT_AS]) self.dta_selected_idx_page[self.dta_page_no] = self.dta_selected_idx - log_debug_msg(f"{self.step_id} ({action_item}) > UserInput-{user_input}, Errors-{errors}") + self.log_step_info(user_input, action_item) if action_item == 'next_page_items': self.dta_page_no = 1 if self.dta_page_no == 0 else 0 @@ -1685,12 +1148,11 @@ async def async_step_display_text_as(self, user_input=None, errors=None): return await self.async_step_display_text_as_update(user_input) elif action_item == 'cancel': - self.dta_selected_idx = -1 + self.dta_selected_idx = UNSELECTED return await self.async_step_menu() elif action_item == 'save': - idx = -1 - self.dta_selected_idx = -1 + idx = self.dta_selected_idx = UNSELECTED dta_working_copy_list = DEFAULT_GENERAL_CONF[CONF_DISPLAY_TEXT_AS].copy() for temp_dta_text in self.dta_working_copy.values(): if instr(temp_dta_text,'>'): @@ -1699,18 +1161,21 @@ async def async_step_display_text_as(self, user_input=None, errors=None): user_input[CONF_DISPLAY_TEXT_AS] = dta_working_copy_list - self._update_configuration_file(user_input) + self._update_config_file_general(user_input) return await self.async_step_menu() if self._any_errors(): self.errors['action_items'] = 'update_aborted' - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='display_text_as', + data_schema=form_display_text_as(self), errors=self.errors) -#------------------------------------------------------------------------------------------- + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# DISPLAY TEXT AS UPDATE +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_display_text_as_update(self, user_input=None, errors=None): self.step_id = 'display_text_as_update' user_input, action_item = self._action_text_to_item(user_input) @@ -1736,16 +1201,15 @@ async def async_step_display_text_as_update(self, user_input=None, errors=None): if self._any_errors(): self.errors['action_items'] = 'update_aborted' - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='display_text_as_update', + data_schema=form_display_text_as_update(self), errors=self.errors) #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -# -# ACTION MENU HANDLER -# +# ACTIONS #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + ''' "divider1": "━━━━━━━━━━ ICLOUD3 CONTROL ACTIONS ━━━━━━━━━━ ", "restart": "RESTART > Restart iCloud3", @@ -1772,8 +1236,8 @@ async def async_step_actions(self, user_input=None, errors=None): self.errors_user_input = {} if user_input is None: - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='actions', + data_schema=form_actions(self), errors=self.errors) # Get key for item selected ("RESTART" --> "restart") and then @@ -1813,7 +1277,6 @@ async def async_step_actions(self, user_input=None, errors=None): #-------------------------------------------------------------------------------- async def _process_action_request(self, action_item): - #update_service_handler(action=None, action_fname=None, devicename=None):# self.header_msg = None if action_item == 'return': @@ -1833,44 +1296,49 @@ async def _process_action_request(self, action_item): if self.header_msg is None: self.header_msg = 'action_completed' -#------------------------------------------------------------------------------------------- + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# RESTART HA IC3LOAD ERROR +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_restart_ha_ic3_load_error(self, user_input=None, errors=None): self.step_id = 'restart_ha_ic3_load_error' return await self.async_restart_ha_ic3(user_input, errors) -#------------------------------------------------------------------------------------------- + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# RESTART HA IC3 +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_restart_ha_ic3(self, user_input=None, errors=None): self.step_id = 'restart_ha_ic3' return await self.async_restart_ha_ic3(user_input, errors) # return await self.async_step_menu() -#------------------------------------------------------------------------------------------- + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# RESTART HA IC3 +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_restart_ha_ic3(self, user_input, errors): ''' A restart HA or reload iCloud3 ''' - # self.step_id = 'restart_ha_ic3' + self.step_id = 'restart_ha_ic3' self.errors = errors or {} self.errors_user_input = {} user_input, action_item = self._action_text_to_item(user_input) if user_input is None or action_item is None: - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='restart_ha_ic3', + data_schema=form_restart_ha_ic3(self), errors=self.errors) if action_item == 'restart_ha': await Gb.hass.services.async_call("homeassistant", "restart") return self.async_abort(reason="ha_restarting") - elif action_item == 'reload_icloud3': - await Gb.hass.services.async_call( - "homeassistant", - "reload_config_entry", - {'device_id': Gb.ha_device_id_by_devicename[ICLOUD3]}, - ) - - return self.async_abort(reason="ic3_reloading") + elif action_item == 'restart_icloud3': + # Gb.config_parms_update_control = ['restart'] + list_add(self.config_parms_update_control, 'restart') + return self.async_abort(reason="ic3_restarting") return await self.async_step_menu() @@ -1880,11 +1348,40 @@ async def async_restart_ha_ic3(self, user_input, errors): # VALIDATE DATA AND UPDATE CONFIG FILE # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + async def _async_write_storage_icloud3_configuration_file(self): + ''' + Write the updated configuration file to .storage/icloud3/configuration + The config file updates are done by setting the commit_updates flag in + the update routines and adding a call to this fct on screen changes so they + are done using async updates. The screen handlers are run in async made + while the update fcts are not. + ''' + if self.config_file_commit_updates: + await config_file.async_write_storage_icloud3_configuration_file() + self.config_file_commit_updates = False + self.header_msg = 'conf_updated' + self.errors['base'] = 'conf_updated' - def _update_configuration_file(self, user_input): +#------------------------------------------------------------------------------------------- + def _update_config_file_general(self, user_input, update_config_flag=None): ''' Update the configuration parameters and write to the icloud3.configuration file + + Parameters: + update_config_flag - The config_tracking, conf_devices or conf_apple_accounts + has already been updated. Make sure the changes are saved. ''' + # The username/password may be in the user_input from the update_data_sources form + # or it's subforms. If so, make sure it is set the the primary username/password + update_config_flag = update_config_flag or False + + if CONF_USERNAME in user_input: + conf_apple_acct, _idx = config_file.conf_apple_acct(0) + conf_username = conf_apple_acct[CONF_USERNAME] + conf_password = conf_apple_acct[CONF_PASSWORD] + user_input[CONF_USERNAME] = conf_username + user_input[CONF_PASSWORD] = encode_password(conf_password) + updated_parms = {''} for pname, pvalue in user_input.items(): if type(pvalue) is str: @@ -1899,19 +1396,18 @@ def _update_configuration_file(self, user_input): if pname in Gb.conf_tracking: if Gb.conf_tracking[pname] != pvalue: Gb.conf_tracking[pname] = pvalue - updated_parms.update(['tracking', 'restart']) + list_add(self.config_parms_update_control, ['tracking', 'restart']) if pname in Gb.conf_general: if Gb.conf_general[pname] != pvalue: Gb.conf_general[pname] = pvalue - updated_parms.update(['general']) + list_add(self.config_parms_update_control, 'general') if 'special_zones' in self.step_id: - updated_parms.update(['zone_formats']) + list_add(self.config_parms_update_control, 'special_zone') if 'away_time_zone' in self.step_id: - updated_parms.update(['devices']) - #updated_parms.update(['restart']) + list_add(self.config_parms_update_control, 'devices') if 'waze' in self.step_id: - updated_parms.update(['waze']) + list_add(self.config_parms_update_control, 'waze') if pname == CONF_LOG_LEVEL: Gb.conf_general[CONF_LOG_LEVEL] = pvalue @@ -1920,36 +1416,82 @@ def _update_configuration_file(self, user_input): if pname in Gb.conf_sensors: if Gb.conf_sensors[pname] != pvalue: Gb.conf_sensors[pname] = pvalue - updated_parms.update(['sensors']) + list_add(self.config_parms_update_control, 'sensors') if pname in Gb.conf_profile: if Gb.conf_profile[pname] != pvalue: Gb.conf_profile[pname] = pvalue - updated_parms.update(['profile', 'evlog']) #, 'restart']) + list_add(self.config_parms_update_control, ['profile', 'evlog']) - if updated_parms != {''}: + if self.config_parms_update_control or update_config_flag: # If default or converted file, update version so the # ic3 parameters are now handled by config_flow if Gb.conf_profile[CONF_VERSION] <= 0: Gb.conf_profile[CONF_VERSION] = 1 - self.config_flow_updated_parms.update(updated_parms) - config_file.write_storage_icloud3_configuration_file() - - self.header_msg = 'conf_updated' + self.config_file_commit_updates = True return +#------------------------------------------------------------------------------------------- + def _update_config_file_tracking(self, user_input=None, update_config_flag=None): + ''' + Update the configuration parameters and write to the icloud3.configuration file + + This is used for updating the devices, Apple account, and some profile items + in the config file That requires a n iCloud3 restart + + Parameters: + update_config_flag - The config_tracking, conf_devices or conf_apple_accounts + has already been updated. Make sure the changes are saved. + ''' + if user_input is None: + user_input = {} + + # The username/password may be in the user_input from the update_data_sources form + # or it's subforms. If so, make sure it is set the the primary username/password + if CONF_USERNAME in user_input: + conf_apple_acct, _idx = config_file.conf_apple_acct(0) + conf_username_0 = conf_apple_acct[CONF_USERNAME] + conf_password_0 = conf_apple_acct[CONF_PASSWORD] + conf_password_0_encoded = encode_password(conf_password_0) + + updated_parms = {''} + update_config_flag = update_config_flag or False + for pname, pvalue in user_input.items(): + if pname not in Gb.conf_tracking: + continue + if type(pvalue) is str: + pvalue = pvalue.strip() + if pvalue == '.': + continue + + if (pname not in self.errors and pname in CONF_PARAMETER_FLOAT): + pvalue = float(pvalue) + + elif Gb.conf_tracking[pname] != pvalue: + Gb.conf_tracking[pname] = pvalue + update_config_flag = True + + if update_config_flag: + if Gb.conf_apple_accounts: + Gb.conf_tracking[CONF_USERNAME] = Gb.conf_apple_accounts[0][CONF_USERNAME] + Gb.conf_tracking[CONF_PASSWORD] = Gb.conf_apple_accounts[0][CONF_PASSWORD] + Gb.conf_tracking[CONF_APPLE_ACCOUNTS] = Gb.conf_apple_accounts + Gb.conf_tracking[CONF_DEVICES] = Gb.conf_devices + list_add(self.config_parms_update_control, ['tracking', 'restart']) + self.config_file_commit_updates = True + #------------------------------------------------------------------------------------------- def _validate_format_settings(self, user_input): ''' The display_zone_format may contain '(Example: ...). If so, strip it off. ''' - user_input = self._option_text_to_parm(user_input, CONF_DISPLAY_ZONE_FORMAT, DISPLAY_ZONE_FORMAT_ITEMS_KEY_TEXT) - user_input = self._option_text_to_parm(user_input, CONF_DEVICE_TRACKER_STATE_SOURCE, DEVICE_TRACKER_STATE_SOURCE_ITEMS_KEY_TEXT) - user_input = self._option_text_to_parm(user_input, CONF_UNIT_OF_MEASUREMENT, UNIT_OF_MEASUREMENT_ITEMS_KEY_TEXT) - 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) + user_input = self._option_text_to_parm(user_input, CONF_DISPLAY_ZONE_FORMAT, DISPLAY_ZONE_FORMAT_OPTIONS) + user_input = self._option_text_to_parm(user_input, CONF_DEVICE_TRACKER_STATE_SOURCE, DEVICE_TRACKER_STATE_SOURCE_OPTIONS) + user_input = self._option_text_to_parm(user_input, CONF_UNIT_OF_MEASUREMENT, UNIT_OF_MEASUREMENT_OPTIONS) + user_input = self._option_text_to_parm(user_input, CONF_TIME_FORMAT, TIME_FORMAT_OPTIONS) + user_input = self._option_text_to_parm(user_input, CONF_LOG_LEVEL, LOG_LEVEL_OPTIONS) if (user_input[CONF_LOG_LEVEL_DEVICES] == [] or len(user_input[CONF_LOG_LEVEL_DEVICES]) >= len(Gb.Devices)): @@ -1958,7 +1500,7 @@ def _validate_format_settings(self, user_input): 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']) + list_add(self.config_parms_update_control, 'special_zone') return user_input @@ -1995,9 +1537,8 @@ def _validate_away_time_zone(self, user_input): if devicename in user_input[CONF_AWAY_TIME_ZONE_1_DEVICES] and devicename != 'none'] if dup_devices != [] : self.errors[CONF_AWAY_TIME_ZONE_2_DEVICES] = 'away_time_zone_dup_devices_2' - - return user_input + #------------------------------------------------------------------------------------------- def _validate_tracking_parameters(self, user_input): ''' @@ -2043,9 +1584,9 @@ def _validate_waze_main(self, user_input): ''' Validate the Waze numeric fields ''' - user_input = self._option_text_to_parm(user_input, CONF_WAZE_SERVER, WAZE_SERVER_ITEMS_KEY_TEXT) + user_input = self._option_text_to_parm(user_input, CONF_WAZE_SERVER, WAZE_SERVER_OPTIONS) user_input = self._validate_numeric_field(user_input) - user_input = self._option_text_to_parm(user_input, CONF_WAZE_HISTORY_TRACK_DIRECTION, WAZE_HISTORY_TRACK_DIRECTION_ITEMS_KEY_TEXT) + user_input = self._option_text_to_parm(user_input, CONF_WAZE_HISTORY_TRACK_DIRECTION, WAZE_HISTORY_TRACK_DIRECTION_OPTIONS) user_input = self._validate_numeric_field(user_input) user_input[CONF_WAZE_USED] = False if user_input[CONF_WAZE_USED] == [] else True @@ -2083,7 +1624,7 @@ def _validate_special_zones(self, user_input): if (user_input[CONF_TRACK_FROM_BASE_ZONE_USED] != Gb.conf_general[CONF_TRACK_FROM_BASE_ZONE_USED] or user_input[CONF_TRACK_FROM_BASE_ZONE] != Gb.conf_general[CONF_TRACK_FROM_BASE_ZONE] or user_input[CONF_TRACK_FROM_HOME_ZONE] != Gb.conf_general[CONF_TRACK_FROM_HOME_ZONE]): - self.config_flow_updated_parms.update(['restart']) + list_add(self.config_parms_update_control, 'restart') if 'stat_zone_header' in user_input: if user_input['stat_zone_header'] == []: @@ -2118,8 +1659,6 @@ def _strip_special_text_from_user_input(self, user_input, pname): pvalue = pvalue.split(DATA_ENTRY_ALERT_CHAR)[0] elif instr(pvalue, '(Example:'): pvalue = pvalue.split('(Example:')[0] - # elif instr(pvalue, '>'): - # pvalue = pvalue.split('>')[0] except Exception as err: log_exception(err) @@ -2145,10 +1684,12 @@ 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(self.data_source, FAMSHR): - if (Gb.conf_tracking[CONF_USERNAME] == '' - or Gb.conf_tracking[CONF_PASSWORD] == ''): + + if instr(self.data_source, ICLOUD): + if (Gb.conf_apple_accounts == [] + or Gb.conf_apple_accounts[0] == [] + or Gb.conf_apple_accounts[0].get(CONF_USERNAME, '') == '' + or Gb.conf_apple_accounts[0].get(CONF_PASSWORD, '') == ''): self.header_msg = 'icloud_acct_not_set_up' return 'none' @@ -2170,7 +1711,8 @@ def _set_inactive_devices_header_msg(self): elif inactive_pct > .34: inactive_msg = 'some' else: - inactive_msg = 'few' + return 'none' + # inactive_msg = 'few' self.header_msg = f'inactive_{inactive_msg}_devices' @@ -2198,170 +1740,471 @@ def _device_cnt(): #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -# -# ICLOUD ACCOUNT FUNCTIONS -# +# DATA SOURCE #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + async def async_step_data_source( self, user_input=None, errors=None, + called_from_step_id=None): + ''' + Updata Data Sources form enables/disables finddev and mobile app datasources and + adds/updates/removes an Apple account using the Update Username/Password screen + ''' - async def async_step_icloud_account(self, user_input=None, errors=None, called_from_step_id=None): - self.step_id = 'icloud_account' + self.step_id = 'data_source' self.errors = errors or {} self.errors_user_input = {} + self.add_apple_acct_flag = False self.actions_list_default = '' action_item = '' + await self._async_write_storage_icloud3_configuration_file() + user_input, action_item = self._action_text_to_item(user_input) 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' + if user_input is None: + self.actions_list_default = 'update_apple_acct' + return self.async_show_form(step_id='data_source', + data_schema=form_data_source(self), + errors=self.errors) - 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' + self.log_step_info(user_input, action_item) - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), - errors=self.errors) + # Set add or display next page now since they are not in apple_acct_items_by_idx + if user_input['apple_accts'].startswith('➤ OTHER'): + self.aa_page_no += 1 + if self.aa_page_no > int(len(self.apple_acct_items_list)/5): + self.aa_page_no = 0 + return await self.async_step_data_source() - 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) + if action_item == 'cancel': + self._initialize_self_PyiCloud_fields_from_Gb() + return await self.async_step_menu() - 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 user_input['apple_accts'].startswith('➤ ADD'): + self.add_apple_acct_flag = True + action_item = 'update_apple_acct' + self.conf_apple_acct = DEFAULT_APPLE_ACCOUNTS_CONF.copy() + self.username = self.password = '' + self.PyiCloud = None + else: + user_input = self._option_text_to_parm(user_input, 'apple_accts', self.apple_acct_items_by_username) + self._get_conf_apple_acct_selected(user_input['apple_accts']) - 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}") + self.log_step_info(user_input, action_item) - if action_item == 'cancel': - self._initialize_self_PyiCloud_fields_from_Gb() - return await self.async_step_menu() + if (self.username != user_input.get('apple_accts', self.username) + and action_item == 'save'): + action_item = 'update_apple_acct' - if (action_item == 'save' - and (self.username != user_input[CONF_USERNAME] - or self.password != user_input[CONF_PASSWORD])): - action_item = 'login_icloud_account' + self.username = self.conf_apple_acct[CONF_USERNAME] + self.password = self.conf_apple_acct[CONF_PASSWORD] + self.PyiCloud = Gb.PyiCloud_by_username.get(self.username) - # Data Source is Mobile App only, iCloud was not selected - if user_input[CONF_USERNAME] == '' and user_input[CONF_PASSWORD] == '': - user_input['data_source_icloud'] = [] - user_input = self._set_data_source(user_input) + if Gb.log_debug_flag: + log_user_input = user_input.copy() + log_debug_msg(f"{self.step_id.upper()} ({action_item}) > UserInput-{log_user_input}, Errors-{errors}") + + if action_item == 'update_apple_acct': + self.aa_page_item[self.aa_page_no] = self.conf_apple_acct[CONF_USERNAME] + return await self.async_step_update_apple_acct() + + if action_item == 'verification_code': + return await self.async_step_reauth(called_from_step_id='data_source') + + if user_input[CONF_DATA_SOURCE] == ',': + self.errors['base'] = 'icloud_acct_no_data_source' + + if self.errors == {}: + if action_item == 'add_change_apple_acct': + action_item == 'save' + user_input[CONF_DATA_SOURCE] = list_add(user_input[CONF_DATA_SOURCE], ICLOUD) + + if action_item == 'save': + self.data_source = list_to_str(user_input[CONF_DATA_SOURCE], ',') + user_input[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] = self.endpoint_suffix + user_input[CONF_DATA_SOURCE] = self.data_source + self._update_config_file_general(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: - return await self.async_step_reauth(called_from_step_id='icloud_account') + self.step_id = 'data_source' + return self.async_show_form(step_id='data_source', + data_schema=form_data_source(self), + errors=self.errors) + +#..........................................................................................- + def _get_conf_apple_acct_selected(self, username): + ''' + Cycle through the devices listed on the device_list screen. If one was selected, + get it's device name and position in the Gb.config_tracking[DEVICES] parameter. + If Found, tThe position was saved in conf_device_idx + Returns: + - True = The devicename was found. + - False = The devicename was not found. + ''' + self.conf_apple_acct, self.aa_idx = config_file.conf_apple_acct(username) + return + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# APPLE USERNAME PASSWORD +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + async def async_step_update_apple_acct(self, user_input=None, errors=None): + + self.step_id = 'update_apple_acct' + self.errors = errors or {} + self.errors_user_input = user_input or {} + self.actions_list_default = '' + action_item = '' + await self._async_write_storage_icloud3_configuration_file() + user_input, action_item = self._action_text_to_item(user_input) + + if user_input is None or CONF_USERNAME in self.errors: + return self.async_show_form(step_id='update_apple_acct', + data_schema=form_update_apple_acct(self), + errors=self.errors) + + user_input = self._option_text_to_parm(user_input, 'account_selected', self.apple_acct_items_by_username) + user_input[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] = 'cn' \ + if user_input.get('url_suffix_china') is True else 'None' + user_input = self._strip_spaces(user_input, [CONF_USERNAME, CONF_PASSWORD, CONF_TOTP_KEY]) + + user_input[CONF_USERNAME] = user_input[CONF_USERNAME].lower() + user_input[CONF_TOTP_KEY] = user_input[CONF_TOTP_KEY].upper() + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + ui_apple_acct ={CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_TOTP_KEY: user_input[CONF_TOTP_KEY], + CONF_LOCATE_ALL: user_input[CONF_LOCATE_ALL]} + + #conf_apple_acct, _idx = config_file.conf_apple_acct(self.aa_idx) + conf_username = self.conf_apple_acct[CONF_USERNAME] + conf_password = self.conf_apple_acct[CONF_PASSWORD] + conf_locate_all = self.conf_apple_acct[CONF_LOCATE_ALL] + + if Gb.log_debug_flag: + log_user_input = user_input.copy() + log_debug_msg(f"{self.step_id.upper()} ({action_item}) > UserInput-{log_user_input}, Errors-{errors}") + + if action_item == 'cancel_return': + self.username = self.conf_apple_acct[CONF_USERNAME] + self.password = self.conf_apple_acct[CONF_PASSWORD] + self.PyiCloud = Gb.PyiCloud_by_username.get(self.username) + return await self.async_step_data_source(user_input=None) + + if username == '' or password == '': + action_item = '' + + # Adding an Apple Account but it already exists + if (self.add_apple_acct_flag + and username in Gb.PyiCloud_by_username): + self.errors[CONF_USERNAME] = 'icloud_acct_dup_username_error' + action_item = '' + + # Changing a username and the old one is being used and no devices are + # using the old one, it's ok to change the name + elif (username != conf_username + and username in Gb.PyiCloud_by_username + and instr(self.apple_acct_items_by_username[username], ' 0 of ') is False): + user_input[CONF_USERNAME] = conf_username + user_input[CONF_PASSWORD] = conf_password + self.errors[CONF_USERNAME] = 'icloud_acct_username_inuse_error' + action_item = '' + + # Saving an existing account with same password, no change, nothing to do + if (action_item == 'log_into_apple_acct' + and ui_apple_acct == self.conf_apple_acct + and user_input[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] ==\ + Gb.conf_tracking[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] + and Gb.PyiCloud_by_username.get(username) is not None): + self.errors['base'] = 'icloud_acct_logged_into' + action_item = '' + + # Display the Confirm Actions form which will execute the remove_apple... function + if action_item == 'stop_using_apple_acct': + # Drop the tracked/untracked part from the current heading (user_input['account_selected']) + # Ex: account_selected = 'GaryCobb (gcobb321) -> 4 of 7 iCloud Devices Tracked, Tracked-(Gary-iPad ...' + confirm_action_form_hdr = ( f"Remove Apple Account - " + f"{user_input['account_selected']}") + if self.PyiCloud: + confirm_action_form_hdr += f", Devices-{list_to_str(self.PyiCloud.icloud_dnames)}" + self.multi_form_user_input = user_input.copy() + + return await self.async_step_delete_apple_acct(user_input=user_input) + + username_password_valid = True + aa_login_info_changed = False + self.errors = {} + if action_item == 'log_into_apple_acct': + # Apple acct login info changed, validate it without logging in + if (conf_username != user_input[CONF_USERNAME] + or conf_password != user_input[CONF_PASSWORD] + or user_input[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] \ + != Gb.conf_tracking[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] + or username not in Gb.PyiCloud_by_username + or Gb.PyiCloud_by_username.get(username) is None): + aa_login_info_changed = True + + if aa_login_info_changed: + username_password_valid = \ + await self._async_validate_username_password(username, password) + + if username_password_valid is False: + self.actions_list_default = 'add_change_apple_acct' + self.errors['base'] = '' + self.errors[CONF_USERNAME] = 'icloud_acct_username_password_error' + return await self.async_step_update_apple_acct( + user_input=user_input, + errors=self.errors) + + if aa_login_info_changed: + self._update_conf_apple_accounts(self.aa_idx, user_input) + await self._async_write_storage_icloud3_configuration_file() + + # Log into the account + if (username not in Gb.PyiCloud_by_username + or Gb.PyiCloud_by_username.get(username) is None): + successful_login = await self.log_into_icloud_account( + user_input, + called_from_step_id='update_apple_acct') + + if successful_login: + Gb.PyiCloud_by_username[user_input[CONF_USERNAME]] = \ + self.PyiCloud or Gb.PyiCloudLoggingInto + Gb.PyiCloud_password_by_username[user_input[CONF_USERNAME]] = \ + user_input[CONF_PASSWORD] + + if (aa_login_info_changed and + username in Gb.username_pyicloud_503_connection_error): + self.errors['base'] = 'icloud_acct_updated_not_logged_into' + + if self.PyiCloud.requires_2fa: + action_item = 'verification_code' else: - action_item = 'login_icloud_account' - - if user_input[CONF_USERNAME] == '': - self.errors[CONF_USERNAME] = 'required_field' - self.errors_user_input[CONF_USERNAME] = '' - if user_input[CONF_PASSWORD] == '': - self.errors[CONF_PASSWORD] = 'required_field' - self.errors_user_input[CONF_PASSWORD] = '' - - if user_input[CONF_DATA_SOURCE] == ',': - self.errors['data_source_icloud'] = 'icloud_acct_no_data_source' - self.errors['data_source_mobapp'] = 'icloud_acct_no_data_source' - - if self.errors == {}: - if action_item == 'login_icloud_account': - user_input['data_source_icloud'] = [FAMSHR] - user_input = self._set_data_source(user_input) - - # if already logged in and no changes, do not login again - if (self.PyiCloud - 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') + return await self.async_step_data_source(user_input=None) + + if action_item == 'verification_code': + return await self.async_step_reauth(called_from_step_id='update_apple_acct') + + return self.async_show_form(step_id='update_apple_acct', + data_schema=form_update_apple_acct(self), + errors=self.errors) - 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') +#------------------------------------------------------------------------------------------- + def _set_data_source(self, user_input): - 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' + # No Apple Accounts set up, don't use it as a data source + if len(Gb.conf_apple_accounts) == 1: + #conf_apple_acct, _idx = config_file.conf_apple_acct(0) + conf_username = self.conf_apple_acct[CONF_USERNAME] + conf_password = self.conf_apple_acct[CONF_PASSWORD] + if conf_username == '' and conf_password == '': + user_input['data_source_icloud'] = [] - elif action_item == 'logout_icloud_account': - user_input = self._initialize_pyicloud_username_password(user_input) + data_source = [ user_input['data_source_icloud'], + user_input['data_source_mobapp']] + user_input[CONF_DATA_SOURCE] = self.data_source = list_to_str(data_source, ',') - 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'): + return user_input - # await self._build_update_device_selection_lists() - self._prepare_device_selection_list() +#------------------------------------------------------------------------------------------- + def _update_conf_apple_accounts(self, aa_idx, user_input, remove_acct_flag=False): + ''' + Update the apple accounts config entry with the new values. - user_input[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] = self.endpoint_suffix - user_input[CONF_DATA_SOURCE] = self.data_source + Input (user_input): + - username = Updated username + - password = Updated password + - apple_account = + - # = Index number of the username being updated in the conf_apple_accounts list + - -1 = Add this username/password to the conf_apple_accounts list - self._update_configuration_file(user_input) + If the apple_account is the index ('#') and the username & password are empty, that item + is deleted if it is not the primary account (1st entry). - return self.async_show_form(step_id=self.called_from_step_id_2, - data_schema=self.form_schema(self.called_from_step_id_2), - errors=self.errors) + ''' + # Updating the account + if remove_acct_flag is False: + self.conf_apple_acct[CONF_USERNAME] = user_input[CONF_USERNAME] + self.conf_apple_acct[CONF_PASSWORD] = encode_password(user_input[CONF_PASSWORD]) + self.conf_apple_acct[CONF_TOTP_KEY] = user_input[CONF_TOTP_KEY] + self.conf_apple_acct[CONF_LOCATE_ALL] = user_input[CONF_LOCATE_ALL] + + # Delete an existing account + if remove_acct_flag: + Gb.conf_apple_accounts.pop(aa_idx) + self.aa_idx = aa_idx - 1 + if self.aa_idx < 0: self.aa_idx = 0 + self.aa_page_item[self.aa_page_no] = '' + + # Add a new account + elif self.add_apple_acct_flag: + if Gb.conf_apple_accounts[0][CONF_USERNAME] == '': + Gb.conf_apple_accounts[0] = self.conf_apple_acct.copy() + self.aa_idx = 0 + else: + Gb.conf_apple_accounts.append(self.conf_apple_acct.copy()) + self.aa_idx = len(Gb.conf_apple_accounts) - 1 - except Exception as err: - log_exception(err) + self.aa_page_no = int(self.aa_idx / 5) + self.aa_page_item[self.aa_page_no] = self.conf_apple_acct[CONF_USERNAME] - self.step_id = 'icloud_account' - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), - errors=self.errors) + # Set the account being updated to the new value + else: + Gb.conf_apple_accounts[aa_idx] = self.conf_apple_acct.copy() + self.aa_idx = aa_idx + self.aa_page_item[self.aa_page_no] = self.conf_apple_acct[CONF_USERNAME] + + max_aa_idx = len(Gb.conf_apple_accounts) - 1 + if self.aa_idx > max_aa_idx: + self.aa_idx = max_aa_idx + elif self.aa_idx < 0: + self.aa_idx = 0 + + user_input['account_selected'] = self.aa_idx + self._update_config_file_tracking(update_config_flag=True) + self._build_apple_accounts_list() + self._build_devices_list() + config_file.build_log_file_filters() #------------------------------------------------------------------------------------------- - def _initialize_pyicloud_username_password(self, user_input): + def tracked_untracked_form_msg(self, username): ''' - Logging out of the iCloud account - Reset all of the login variables + This is used in the config_flow_forms to fill in the tracked and untracked devices + on the username password form ''' - 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) + PyiCloud = Gb.PyiCloud_by_username.get(username) + icloud_dnames = PyiCloud.icloud_dnames if PyiCloud else [] - return user_input + devicenames_by_username, icloud_dnames_by_username = \ + self.get_conf_device_names_by_username(username) + tracked_devices = [icloud_dname + for icloud_dname in icloud_dnames + if icloud_dname in icloud_dnames_by_username] + untracked_devices = [icloud_dname + for icloud_dname in icloud_dnames + if icloud_dname not in icloud_dnames_by_username] + + return (f"Tracked-({list_to_str(tracked_devices)}), " + f"Untracked-({list_to_str(untracked_devices)})") + +#-------------------------------------------------------------------- + def get_conf_device_names_by_username(self, username): + ''' + Cycle through the conf_devices and build a list of device names by the + apple account usernames + + Parameter: + username + Return: + {devicenames_by_username}, {icloud_dnames_by_username} + ''' + devicenames_by_username = [conf_device[CONF_IC3_DEVICENAME] + for conf_device in Gb.conf_devices + if conf_device[CONF_APPLE_ACCOUNT] == username] + + icloud_dnames_by_username = [conf_device[CONF_FAMSHR_DEVICENAME] + for conf_device in Gb.conf_devices + if conf_device[CONF_APPLE_ACCOUNT] == username] + + devicenames_by_username.sort() + icloud_dnames_by_username.sort() + + return devicenames_by_username, icloud_dnames_by_username + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# DELETE APPLE ACCOUNT +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + async def async_step_delete_apple_acct(self, user_input=None, errors=None): + ''' + 1. Delete the device from the tracking devices list and adjust the device index + 2. Delete all devices + 3. Clear the iCloud, Mobile App and track_from_zone fields from all devices + ''' + self.step_id = 'delete_apple_acct' + self.errors = errors or {} + self.errors_user_input = {} + + user_input, action_item = self._action_text_to_item(user_input) + user_input = self._option_text_to_parm(user_input, 'device_action', + DELETE_APPLE_ACCT_DEVICE_ACTION_OPTIONS) + self.log_step_info(user_input, action_item) + + if user_input is None or action_item is None: + return self.async_show_form(step_id='delete_apple_acct', + data_schema=form_delete_apple_acct(self), + errors=self.errors) + + if action_item == 'cancel_return': + return await self.async_step_update_apple_acct() + + device_action = user_input['device_action'] + self._delete_apple_acct(user_input) + + list_add(self.config_parms_update_control, ['tracking', 'restart']) + self.header_msg = 'action_completed' + + return await self.async_step_data_source() #------------------------------------------------------------------------------------------- - 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 + def _delete_apple_acct(self, user_input): + ''' + Remove Apple Account from the Apple Accounts config list + and from all devices using it. + ''' + + # Cycle through the devices and see if it is assigned to the acct being removed. + # If it is, see if the device is in the primary (1st) username Apple acct. + # If it is, reassign the device to that Apple acct. Otherwise, remove it + + primary_username = Gb.conf_apple_accounts[0][CONF_USERNAME] + device_action = user_input['device_action'] + #conf_apple_acct, _idx = config_file.conf_apple_acct(self.aa_idx) + conf_username = self.conf_apple_acct[CONF_USERNAME] + + # Cycle thru config and get the username being deleted. Delete or update it + updated_conf_devices = [] + for conf_device in Gb.conf_devices: + if conf_device[CONF_APPLE_ACCOUNT] != conf_username: + updated_conf_devices.append(conf_device) + + elif device_action == 'delete_devices': + devicename = conf_device[CONF_IC3_DEVICENAME] + self._remove_device_tracker_entity(devicename) + + elif device_action == 'set_devices_inactive': + conf_device[CONF_APPLE_ACCOUNT] = '' + conf_device[CONF_TRACKING_MODE] = INACTIVE_DEVICE + updated_conf_devices.append(conf_device) + + elif device_action == 'reassign_devices': + icloud_dname = conf_device[CONF_FAMSHR_DEVICENAME] + other_apple_acct = [PyiCloud.username + for username, PyiCloud in Gb.PyiCloud_by_username.items() + if icloud_dname in PyiCloud.device_id_by_icloud_dname] + if other_apple_acct == []: + conf_device[CONF_APPLE_ACCOUNT] = '' + conf_device[CONF_TRACKING_MODE] = INACTIVE_DEVICE + else: + conf_device[CONF_APPLE_ACCOUNT] = other_apple_acct[0] + updated_conf_devices.append(conf_device) + + Gb.conf_devices = updated_conf_devices + self._update_config_file_tracking(user_input={}, update_config_flag=True) + + # Remove the apple acct from the PyiCloud dict and delete it's instance + PyiCloud = Gb.PyiCloud_by_username.pop(conf_username, None) + if PyiCloud: del PyiCloud + + self._update_conf_apple_accounts(self.aa_idx, user_input, remove_acct_flag=True) return user_input #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -# -# ICLOUD VERIFICATION CODE ENTRY FORM -# +# REAUTH #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - async def async_step_reauth(self, user_input=None, errors=None, called_from_step_id=None): + async def async_step_reauth(self, user_input=None, errors=None, + called_from_step_id=None, ha_reauth_username=None): ''' Ask the verification code to the user. @@ -2390,67 +2233,123 @@ async def async_step_reauth(self, user_input=None, errors=None, called_from_step action_item = '' self.called_from_step_id_1 = called_from_step_id or self.called_from_step_id_1 or 'menu_0' + log_debug_msg( f"OF-{self.step_id.upper()} ({action_item}) > " + f"FromForm-{called_from_step_id}, UserInput-{user_input}, Errors-{errors}") + if user_input is None: - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + if self.PyiCloud and self.PyiCloud.requires_2fa: + self.errors['base'] ='verification_code_needed' + + return self.async_show_form(step_id='reauth', + data_schema=form_reauth(self), errors=self.errors) + try: + user_input, action_item = self._action_text_to_item(user_input) + user_input = self._strip_spaces(user_input, [CONF_VERIFICATION_CODE]) + user_input = self._option_text_to_parm(user_input, 'account_selected', self.apple_acct_items_by_username) + log_debug_msg(f"OF-{self.step_id.upper()} ({action_item}) > UserInput-{user_input}, Errors-{errors}") + + if user_input['account_selected']: + self._get_conf_apple_acct_selected(user_input['account_selected']) + username = self.conf_apple_acct[CONF_USERNAME] + password = self.conf_apple_acct[CONF_PASSWORD] + else: + self.aa_idx = 0 + username = self.conf_apple_acct[CONF_USERNAME] = user_input[CONF_USERNAME] + password = self.conf_apple_acct[CONF_PASSWORD] = user_input[CONF_PASSWORD] - user_input, action_item = self._action_text_to_item(user_input) - user_input = self._strip_spaces(user_input, [CONF_VERIFICATION_CODE]) - log_debug_msg(f"{self.step_id} ({action_item}) > UserInput-{user_input}, Errors-{errors}") + self.apple_acct_reauth_username = username + self.PyiCloud = Gb.PyiCloud_by_username.get(username, self.PyiCloud) - if self.PyiCloud is None: - self.errors = 'icloud_acct_not_logged_into' - action_item = 'cancel' + if action_item == 'log_into_apple_acct': + await self.log_into_icloud_account(user_input, called_from_step_id='reauth') - if action_item == 'send_verification_code' and user_input.get(CONF_VERIFICATION_CODE, '') == '': - action_item = 'cancel' + elif (action_item == 'send_verification_code' + and user_input.get(CONF_VERIFICATION_CODE, '') == ''): + action_item = 'cancel_return' - if action_item == 'cancel': - return self.async_show_form(step_id=self.called_from_step_id_1, - data_schema=self.form_schema(self.called_from_step_id_1), + if action_item == 'cancel_return': + self.clear_PyiCloud_2fa_flags() + return self.async_show_form(step_id=self.called_from_step_id_1, + data_schema=self.form_schema(self.called_from_step_id_1), + errors=self.errors) + + if action_item == 'send_verification_code': + # if self.conf_apple_acct[CONF_TOTP_KEY]: + # OTP = pyotp.TOTP(conf_apple_acct[CONF_TOTP_KEY].replace('-', '')) + # otp_code = OTP.now() + # user_input[CONF_VERIFICATION_CODE] = otp_code + valid_code = await self.reauth_send_verification_code_handler(self, user_input) + + if valid_code: + if instr(str(self.apple_acct_items_by_username), 'AUTHENTICATION'): + self.conf_apple_acct = '' + else: + self.clear_PyiCloud_2fa_flags() + return self.async_show_form(step_id=self.called_from_step_id_1, + data_schema=self.form_schema(self.called_from_step_id_1), + errors=self.errors) + + elif action_item == 'request_verification_code': + await self.async_pyicloud_reset_session(username, password) + + return self.async_show_form(step_id='reauth', + data_schema=form_reauth(self), errors=self.errors) + except Exception as err: + log_exception(err) - if action_item == 'request_verification_code': - await Gb.hass.async_add_executor_job( - pyicloud_ic3_interface.pyicloud_reset_session, - self.PyiCloud) - # PyiCloud) +#.............................................................................................. + def clear_PyiCloud_2fa_flags(self): + for username, PyiCloud in Gb.PyiCloud_by_username.items(): + if PyiCloud.requires_2fa: + PyiCloud.new_2fa_code_already_requested_flag = False - self.errors['base'] = 'verification_code_requested2' +#.............................................................................................. + @staticmethod + async def reauth_send_verification_code_handler(caller_self, user_input): + ''' + Handle the send_verification_code action. This is called from the ConfigFlow and OptionFlow + reauth steps in each Flow. This provides this function with the appropriate data and return objects. - elif (action_item == 'send_verification_code' - and CONF_VERIFICATION_CODE in user_input - and user_input[CONF_VERIFICATION_CODE]): + Parameters: + - caller_self = 'self' for OptionFlow and the _OptFlow object when calling from ConfigFlow + OptionFlow --> valid_code = await self.reauth_send_verification_code_handler( + self, user_input) + ConfigFlow --> valid_code = await _OptFlow.reauth_send_verification_code_handler( + _OptFlow, user_input) + - user_input = user_input dictionary + ''' + try: valid_code = await Gb.hass.async_add_executor_job( - self.PyiCloud.validate_2fa_code, + caller_self.PyiCloud.validate_2fa_code, user_input[CONF_VERIFICATION_CODE]) # Do not restart iC3 right now if the username/password was changed on the # iCloud setup screen. If it was changed, another account is being logged into # and it will be restarted when exiting the configurator. if valid_code: - post_event( f"{EVLOG_NOTICE}The Verification Code was accepted ({user_input[CONF_VERIFICATION_CODE]})") - post_event(f"{EVLOG_NOTICE}iCLOUD ALERT > Apple ID Verification complete") + post_event(f"{EVLOG_NOTICE}Apple Acct > {caller_self.PyiCloud.account_owner}, " + f"Code accepted, Verification completed") + + await caller_self._build_icloud_device_selection_list() Gb.EvLog.clear_evlog_greenbar_msg() Gb.icloud_force_update_flag = True - self.PyiCloud.new_2fa_code_already_requested_flag = False + caller_self.PyiCloud.new_2fa_code_already_requested_flag = False + caller_self._build_apple_accounts_list() - self.errors['base'] = self.header_msg = 'verification_code_accepted' - - return self.async_show_form(step_id=self.called_from_step_id_1, - data_schema=self.form_schema(self.called_from_step_id_1), - errors=self.errors) + caller_self.errors['base'] = caller_self.header_msg = 'verification_code_accepted' else: - post_event( f"{EVLOG_NOTICE}The Apple ID Verification Code is invalid " + post_event( f"{EVLOG_NOTICE}The Apple Account Verification Code is invalid " f"({user_input[CONF_VERIFICATION_CODE]})") - self.errors[CONF_VERIFICATION_CODE] = 'verification_code_invalid' + caller_self.errors[CONF_VERIFICATION_CODE] = 'verification_code_invalid' - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), - errors=self.errors) + return valid_code + + except Exception as err: + log_exception(err) #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -2458,7 +2357,7 @@ async def async_step_reauth(self, user_input=None, errors=None, called_from_step # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - async def _log_into_icloud_account(self, user_input, called_from_step_id=None, request_verification_code=False): + async def log_into_icloud_account(self, user_input, called_from_step_id=None): ''' Log into the icloud account and check to see if a verification code is needed. If so, show the verification form, get the code from the user, verify it and @@ -2477,142 +2376,328 @@ async def _log_into_icloud_account(self, user_input, called_from_step_id=None, r access needs to be verified. If so, the verification code entry form must be displayed. Returns: - Gb.Pyicloud object - self.PyiCloud_FamilySharing object + self.Pyicloud object + self.PyiCloud_DeviceSvc object self.PyiCloud_FindMyFriends object - self.opt_famshr_devicename_list & self.device_form_icloud_famf_list = + self.opt_icloud_dname_list & self.device_form_icloud_famf_list = A dictionary with the devicename and identifiers 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}, " + self.errors = {} + called_from_step_id = called_from_step_id or 'update_apple_acct' + log_debug_msg( f"Logging into Apple 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]) - 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, we are already logged in - if (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): + # The username may be changed to assign a new account, if so, log into the new one + if CONF_USERNAME not in user_input or CONF_PASSWORD not in user_input: return - # Already logged in with same username/password - if (self.PyiCloud - and request_verification_code is False - and self.username == self.PyiCloud.username - and self.password == self.PyiCloud.password - and self.endpoint_suffix == self.PyiCloud.endpoint_suffix): - return + username = user_input[CONF_USERNAME].lower() + password = user_input[CONF_PASSWORD] + list_add(Gb.log_file_filter, password) + endpoint_suffix = user_input.get(CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX, + Gb.conf_tracking[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX]) + + # Already logged in and no changes + PyiCloud = Gb.PyiCloud_by_username.get(username) + if (PyiCloud + and password == PyiCloud.password + and endpoint_suffix == PyiCloud.endpoint_suffix): + self.PyiCloud = PyiCloud + self.username = username + self.password = password + self.errors['base'] = 'icloud_acct_logged_into' + self.header_msg = 'icloud_acct_logged_into' - if request_verification_code: - event_msg = f"{EVLOG_NOTICE}Configure Settings > Requesting Apple ID Verification Code" - else: - 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}" + return True + + # Validate the account before actually logging in + username_password_valid = await self._async_validate_username_password(username, password) + if username_password_valid is False: + self.errors['base'] = 'icloud_acct_login_error_user_pw' + + return False + + event_msg =(f"{EVLOG_NOTICE}Configure Settings > Logging into Apple Account {username}") + if endpoint_suffix != 'None': event_msg += f", AppleServerURLSuffix-{endpoint_suffix}" log_info_msg(event_msg) try: - self.PyiCloud = await Gb.hass.async_add_executor_job( - pyicloud_ic3_interface.create_PyiCloudService_secondary, - self.username, - self.password, - self.endpoint_suffix, - 'config', - verify_password, - request_verification_code) + await file_io.async_make_directory(Gb.icloud_cookie_directory) - except (PyiCloudFailedLoginException) as err: - self.PyiCloud = None - self.endpoint_suffix = Gb.icloud_server_endpoint_suffix = \ - Gb.conf_tracking[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] + PyiCloud = None + PyiCloud = await Gb.hass.async_add_executor_job( + self.create_PyiCloudService_config_flow, + username, + password, + endpoint_suffix) - err = str(err) - _CF_LOGGER.error(f"Error logging into iCloud service: {err}") + # Successful login, set PyiCloud fields + self.PyiCloud = PyiCloud + self.username = username + self.password = password + self.endpoint_suffix = endpoint_suffix + Gb.username_valid_by_username[username] = True - if called_from_step_id == 'icloud_account': - if err.endswith('302'): - error_msg = 'icloud_acct_login_error_connection' - elif err.endswith('400'): - error_msg = 'icloud_acct_login_error_user_pw' - else: - error_msg = 'icloud_acct_login_error_other' - else: - error_msg = 'icloud_acct_login_error_other' - self.errors = {'base': error_msg} + if PyiCloud.DeviceSvc: + PyiCloud.DeviceSvc.refresh_client() - return self.async_show_form(step_id=called_from_step_id, - data_schema=self.form_schema(called_from_step_id), - errors=self.errors) + start_ic3.dump_startup_lists_to_log() - except Exception as err: - log_exception(err) - _CF_LOGGER.error(f"Error logging into iCloud service: {err}") + if PyiCloud.requires_2fa or called_from_step_id is None: + return True - self.errors = {'base': 'icloud_acct_login_error_other'} + self.errors['base'] = 'icloud_acct_logged_into' + self.header_msg = 'icloud_acct_logged_into' + + if called_from_step_id is None: + return True 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() + except (PyiCloudFailedLoginException) as err: + err = str(err) + _CF_LOGGER.error(f"Error logging into Apple Acct: {err}") + + # if called_from_step_id == 'update_apple_acct': + if True is True: + response_code = Gb.PyiCloudLoggingInto.response_code + if Gb.PyiCloudLoggingInto.response_code_pwsrp_err == 503: + list_add(Gb.username_pyicloud_503_connection_error, username) + error_msg = 'icloud_acct_login_error_503' + elif response_code == 400: + error_msg = 'icloud_acct_login_error_user_pw' + elif response_code == 401 and instr(err, 'Python SRP'): + error_msg = 'icloud_acct_login_error_srp_401' + elif response_code == 401: + error_msg = 'icloud_acct_login_error_user_pw' + else: + error_msg = 'icloud_acct_login_error_other' + else: + error_msg = 'icloud_acct_login_error_other' + self.errors['base'] = error_msg - self.obscure_username = obscure_field(self.username) or 'NoUsername' - self.obscure_password = obscure_field(self.password) or 'NoPassword' + except Exception as err: + log_exception(err) + _CF_LOGGER.error(f"Error logging into Apple Account: {err}") + self.errors['base'] = 'icloud_acct_login_error_other' - if self.PyiCloud.requires_2fa or request_verification_code: - return + start_ic3.dump_startup_lists_to_log() + return False - self.errors = {'base': 'icloud_acct_logged_into'} - self.header_msg = 'icloud_acct_logged_into' - return self.async_show_form(step_id=called_from_step_id, - data_schema=self.form_schema(called_from_step_id), - errors=self.errors) +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# CREATE PYICLOUD SERVICE OBJECTS +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + @staticmethod + async def _async_validate_username_password(username, password): + ''' + Verify the username and password are valid using the Apple Acct validate + routine before logging into the Apple Acct. This uses one PyiCloud call to + validate instead of logging in and having it fail later in the process. + ''' + if Gb.PyiCloudValidateAppleAcct is None: + Gb.PyiCloudValidateAppleAcct = PyiCloudValidateAppleAcct() -#------------------------------------------------------------------------------------------- - def _set_action_list_item_username_password(self): + valid_apple_acct = await Gb.hass.async_add_executor_job( + Gb.PyiCloudValidateAppleAcct.validate_username_password, + username, + password) + + return valid_apple_acct + +#-------------------------------------------------------------------- + @staticmethod + def create_PyiCloudService_config_flow(username, password, endpoint_suffix): ''' - Insert the username/password of the iCloud account currently logged into - into the Action Item selection list item + Create the PyiCloudService object without going through the error checking and + authentication test routines. This is used by config_flow to open a second + PyiCloud session ''' + PyiCloud = PyiCloudService( username, + password, + cookie_directory=Gb.icloud_cookie_directory, + session_directory=Gb.icloud_session_directory, + endpoint_suffix=endpoint_suffix, + config_flow_login=True) - 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}" + log_debug_msg( f"Apple Acct > {PyiCloud.account_owner}, Login Successful, " + f"Update Connfiguration") + + start_ic3.dump_startup_lists_to_log() + return PyiCloud + +#-------------------------------------------------------------------- + @staticmethod + def create_DeviceSvc_config_flow(PyiCloud): + + iCloud = PyiCloud.create_DeviceSvc_object(config_flow_login=True) + start_ic3.dump_startup_lists_to_log() + return iCloud + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# RESET PYICLOUD SESSION, GENERATE VERIFICATION CODE +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + async def async_pyicloud_reset_session(self, username, password): + ''' + Reset the current session and authenticate to restart pyicloud_ic3 + and enter a new verification code + + The username & password are specified in case the Apple acct is not logged + into because of an error + ''' + try: + PyiCloud = self.PyiCloud + if PyiCloud: + post_event(f"{EVLOG_NOTICE}Apple Acct > {PyiCloud.account_owner}, Authentication needed") + + await self.async_delete_pyicloud_cookies_session_files(PyiCloud.username) + + if PyiCloud.authentication_alert_displayed_flag is False: + PyiCloud.authentication_alert_displayed_flag = True + + await Gb.hass.async_add_executor_job(PyiCloud.__init__, + PyiCloud.username, + PyiCloud.password, + Gb.icloud_cookie_directory, + Gb.icloud_session_directory) + + # Initialize PyiCloud object to force a new one that will trigger the 2fa process + PyiCloud.verification_code = None + + # The Apple acct is not logged into. There may have been some type Of error. + # Delete the session files four the username selected on the request form and + # try to login + elif username and password: + post_event(f"{EVLOG_NOTICE}Apple Acct > {username}, Authentication needed") + + await self.async_delete_pyicloud_cookies_session_files(username) + + user_input = {} + user_input[CONF_USERNAME] = username + user_input[CONF_PASSWORD] = password + + await self.log_into_icloud_account(user_input) + + PyiCloud = self.PyiCloud + + if PyiCloud: + post_event( f"{EVLOG_NOTICE}Apple Acct > {PyiCloud.account_owner}, " + f"Waiting for 6-digit Verification Code to be entered") + return + + except PyiCloudFailedLoginException as err: + login_err = str(err) + login_err + ", Will retry logging into the Apple Account later" + + except Exception as err: + login_err = str(err) + log_exception(err) + + if instr(login_err, '-200') is False: + PyiCloudSession = Gb.PyiCloudSession_by_username.get(username) + if PyiCloudSession and PyiCloudSession.response_code == 503: + list_add(Gb.username_pyicloud_503_connection_error, username) + self.errors['base'] = 'icloud_acct_login_error_503' + + if PyiCloudSession.response_code == 503: + post_event( f"{EVLOG_ERROR}Apple Acct > {username}, " + f"Apple is delaying displaying a new Verification code to " + f"prevent Suspicious Activity, probably due to too many requests. " + f"It should be displayed in about 20-30 minutes. " + f"{CRLF_DOT}The Apple Acct login will be retried within 15-mins. " + f"The Verification Code will be displayed then if successful") else: - logged_into_msg = (f"New iCloud Acct: {self.obscure_username} " - f"{RED_ALERT}SAVE CHANGES{RED_ALERT}") + post_event( f"{EVLOG_ERROR}Apple Acct > {username}, " + f"An Error was encountered requesting the 6-digit Verification Code, " + f"{login_err}") + - self.actions_list[LOGGED_INTO_MSG_ACTION_LIST_IDX] = \ - self.actions_list[LOGGED_INTO_MSG_ACTION_LIST_IDX].replace('^msg', logged_into_msg) +#-------------------------------------------------------------------- + async def async_delete_pyicloud_cookies_session_files(self, username=None): + ''' + Delete the cookies and session files as part of the reset_session and request_verification_code + This is called from config_flow/setp_reauth and pyicloud_reset_session + + ''' + post_event(f"{EVLOG_NOTICE}Apple Acct > Resetting Cookie/Session Files") + + if username is None: username = self.username + cookie_directory = Gb.icloud_cookie_directory + cookie_filename = "".join([c for c in username if match(r"\w", c)]) + + delete_msg = f"Apple Acct > Deleting Session Files > ({cookie_directory})" + + delete_msg += f"{CRLF_DOT}Session ({cookie_filename}.session)" + await file_io.async_delete_file_with_msg( + 'Apple Acct Session', cookie_directory, f"{cookie_filename}.session", delete_old_sv_file=True) + delete_msg += f"{CRLF_DOT}Token Password ({cookie_filename}.tpw)" + await file_io.async_delete_file_with_msg( + 'Apple Acct Tokenpw', cookie_directory, f"{cookie_filename}.tpw") + post_monitor_msg(delete_msg) #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -# -# TRACKED DEVICE MENU - DEVICE LIST, DEVICE UPDATE FORMS -# +# TRUSTED DEVICES #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + async def async_step_trusted_device(self, user_input=None, errors=None): + """We need a trusted device.""" + return {} + + if errors is None: + errors = {} + + trusted_devices_key_text = await self._build_trusted_devices_list() + trusted_devices = await(Gb.hass.async_add_executor_job( + self.PyiCloud.trusted_devices) + ) + trusted_devices_for_form = {} + for i, device in enumerate(trusted_devices): + trusted_devices_for_form[i] = device.get( + "deviceName", f"SMS to {device.get('phoneNumber')}" + ) + + # if user_input is None: + # return await self._show_trusted_device_form( + # trusted_devices_for_form, user_input, errors + # ) + + # self._trusted_device = trusted_devices[int(user_input[CONF_TRUSTED_DEVICE])] + + # if not await self.hass.async_add_executor_job( + # self.api.send_verification_code, self._trusted_device + # ): + # _LOGGER.error("Failed to send verification code") + # self._trusted_device = None + # errors[CONF_TRUSTED_DEVICE] = "send_verification_code" + + # return await self._show_trusted_device_form( + # trusted_devices_for_form, user_input, errors + # ) + + # return await self.async_step_verification_code() + + # async def _show_trusted_device_form( + # self, trusted_devices, user_input=None, errors=None + # ): + # """Show the trusted_device form to the user.""" + + # return self.async_show_form( + # step_id=CONF_TRUSTED_DEVICE, + # data_schema=vol.Schema( + # { + # vol.Required(CONF_TRUSTED_DEVICE): vol.All( + # vol.Coerce(int), vol.In(trusted_devices) + # ) + # } + # ), + # errors=errors or {}, + # ) +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# DEVICE LIST +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_device_list(self, user_input=None, errors=None): ''' Display the list of devices form and the function to be performed @@ -2621,146 +2706,120 @@ async def async_step_device_list(self, user_input=None, errors=None): self.step_id = 'device_list' self.errors = errors or {} self.errors_user_input = {} - self.add_device_flag = False + self.add_device_flag = self.display_rarely_updated_parms = False + await self._async_write_storage_icloud3_configuration_file() + + if user_input is None: + await self._build_icloud_device_selection_list() + await self._build_mobapp_entity_selection_list() + self._set_inactive_devices_header_msg() + self._set_header_msg() + self._build_devices_list() + self.conf_device_update_control = {} + + return self.async_show_form(step_id='device_list', + data_schema=form_device_list(self), + errors=self.errors) user_input, action_item = self._action_text_to_item(user_input) - log_debug_msg(f"{self.step_id} ({action_item}) > UserInput-{user_input}, Errors-{errors}") + self.log_step_info(user_input, action_item) if action_item == 'return': - self.sensor_entity_attrs_changed = {} + self.conf_device_update_control = {} return await self.async_step_menu() - if Gb.PyiCloud and self.PyiCloud is None: - self.PyiCloud = Gb.PyiCloud - - if instr(self.data_source, FAMSHR): - if (Gb.conf_tracking[CONF_USERNAME] == '' - or Gb.conf_tracking[CONF_PASSWORD] == ''): + if instr(self.data_source, ICLOUD): + if (Gb.conf_apple_accounts == [] + or Gb.conf_apple_accounts[0] == [] + or Gb.conf_apple_accounts[0].get(CONF_USERNAME, '') == '' + or Gb.conf_apple_accounts[0].get(CONF_PASSWORD, '') == ''): self.header_msg = 'icloud_acct_not_set_up' errors = {'base': 'icloud_acct_not_set_up'} - return await self.async_step_icloud_account(user_input=None, - errors=errors, - called_from_step_id='device_list') - elif (self.PyiCloud is None - and Gb.conf_tracking[CONF_USERNAME] - and Gb.conf_tracking[CONF_PASSWORD]): - await self._log_into_icloud_account({}, called_from_step_id='device_list') + device_cnt = len(Gb.conf_devices) - if (self.PyiCloud and self.PyiCloud.requires_2fa): - errors = {'base': 'verification_code_needed'} - return await self.async_step_reauth(user_input=None, - errors=errors, - called_from_step_id='device_list') + if (action_item in ['update_device', 'delete_device'] + and CONF_DEVICES not in user_input): + action_item = '' - device_cnt = len(Gb.conf_devices) - # if user_input is None: - # await self._build_update_device_selection_lists() + if action_item == 'return': + self.conf_device_update_control = {} + return await self.async_step_menu() - if user_input is not None: - if (action_item in ['update_device', 'delete_device'] - and CONF_DEVICES not in user_input): - # await self._build_update_device_selection_lists() - action_item = '' + if action_item == 'change_device_order': + self.cdo_devicenames = [self._format_device_text_hdr(conf_device) + for conf_device in Gb.conf_devices] + self.cdo_new_order_idx = [x for x in range(0, len(Gb.conf_devices))] + self.actions_list_default = 'move_down' + return await self.async_step_change_device_order() - if action_item == 'return': - self.sensor_entity_attrs_changed = {} - return await self.async_step_menu() + if user_input['devices'].startswith('➤ OTHER'): + if action_item == 'delete_device': + action_item = 'update_device' + self.dev_page_no += 1 + if self.dev_page_no > int(len(self.device_items_list)/5): + self.dev_page_no = 0 + return await self.async_step_device_list() - if action_item == 'update_device': - self.sensor_entity_attrs_changed['update_device'] = True - if self._get_conf_device_selected(user_input): - return await self.async_step_update_device() + if user_input['devices'].startswith('➤ ADD'): + self.conf_device_update_control['add_device'] = True + self.conf_device = DEFAULT_DEVICE_CONF.copy() + return await self.async_step_add_device() - if action_item == 'add_device': - self.sensor_entity_attrs_changed['add_device'] = True - self.conf_device_selected = DEFAULT_DEVICE_CONF.copy() - return await self.async_step_add_device() + user_input = self._option_text_to_parm(user_input, 'devices', self.device_items_by_devicename) + user_input[CONF_IC3_DEVICENAME] = user_input['devices'] + self.log_step_info(user_input, action_item) + self._get_conf_device_selected(user_input) - if action_item == 'delete_device': - self.sensor_entity_attrs_changed['delete_device'] = True - if self._get_conf_device_selected(user_input): - return await self.async_step_delete_device() - - if action_item == 'next_page_items': - if device_cnt == 0: - self.sensor_entity_attrs_changed = {} - return await self.async_step_menu() - elif device_cnt > 5: - self.device_list_page_no += 1 - if self.device_list_page_no > int(device_cnt/5): - self.device_list_page_no = 0 - self.conf_device_selected_idx = self.device_list_page_no * 5 - - if action_item == 'change_device_order': - self.cdo_devicenames = [self._format_device_info(conf_device) - for conf_device in Gb.conf_devices] - self.cdo_new_order_idx = [x for x in range(0, len(Gb.conf_devices))] - self.actions_list_default = 'move_down' - return await self.async_step_change_device_order(called_from_step_id='device_list') + if action_item == 'update_device': + self.conf_device_update_control['update_device'] = True + return await self.async_step_update_device() + + if action_item == 'delete_device': + self.conf_device_update_control['delete_device'] = True + return await self.async_step_delete_device() + await self._build_icloud_device_selection_list() + await self._build_mobapp_entity_selection_list() self._set_inactive_devices_header_msg() self._set_header_msg() - self._prepare_device_selection_list() - self.sensor_entity_attrs_changed = {} + self._build_devices_list() + self.conf_device_update_control = {} - self.step_id = 'device_list' - - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='device_list', + data_schema=form_device_list(self), errors=self.errors, last_step=False) -#------------------------------------------------------------------------------------------- +#..........................................................................................- def _get_conf_device_selected(self, user_input): ''' Cycle through the devices listed on the device_list screen. If one was selected, get it's device name and position in the Gb.config_tracking[DEVICES] parameter. - - If it is deleted, pop it from the config parameter and return. - If it is being added, add a default entry to the config parameter and return that entry. - If it is being updated, return that entry. - + If Found, tThe position was saved in conf_device_idx Returns: - - True = The device is being added or updated. Display the device update form. - - False = The device was deleted. Rebuild the list and redisplay the screen. - + - True = The devicename was found. + - False = The devicename was not found. ''' - # Displayed info is devicename > Name, FamShr device info, FmF device info, - # MobApp device. Get devicename. - if CONF_DEVICES in user_input: - devicename_selected = user_input[CONF_DEVICES] - else: - self.ic3_devicename_being_updated = '' - self.conf_device_selected = {} - self.conf_device_selected_idx = 0 - self.device_list_page_no = 0 - return False - - first_space_pos = devicename_selected.find(' ') - if first_space_pos > 0: - devicename_selected = devicename_selected[:first_space_pos] - - for form_devices_list_index, devicename in enumerate(self.form_devices_list_devicename): - if devicename_selected == devicename: - self.conf_device_selected = Gb.conf_devices[form_devices_list_index] - self.conf_device_selected_idx = form_devices_list_index + idx = -1 + for conf_device in Gb.conf_devices: + idx += 1 + if conf_device[CONF_IC3_DEVICENAME] == user_input[CONF_IC3_DEVICENAME]: + self.conf_device = conf_device + self.conf_device_idx = idx break - user_input[CONF_DEVICES] = self.conf_device_selected[CONF_IC3_DEVICENAME] - - self.conf_device_selected_idx = form_devices_list_index - - - return True + return (idx >= 0) -#------------------------------------------------------------------------------------------- +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# DELETE DEVICE +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_delete_device(self, user_input=None, errors=None): ''' 1. Delete the device from the tracking devices list and adjust the device index 2. Delete all devices - 3. Clear the FamShr, FmF, Mobile App and track_from_zone fields from all devices + 3. Clear the iCloud, Mobile App and track_from_zone fields from all devices ''' self.step_id = 'delete_device' self.errors = errors or {} @@ -2772,11 +2831,11 @@ async def async_step_delete_device(self, user_input=None, errors=None): if action_item and action_item.startswith('delete_this_device'): action_item = 'delete_this_device' - log_debug_msg(f"{self.step_id} ({action_item}) > UserInput-{user_input}, Errors-{errors}") + self.log_step_info(user_input, action_item) if user_input is None or action_item is None: - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='delete_device', + data_schema=form_delete_device(self), errors=self.errors) # if user_input is not None or action_item is not None: @@ -2793,34 +2852,39 @@ async def async_step_delete_device(self, user_input=None, errors=None): self._clear_icloud_mobapp_selection_parms() if action_item != 'delete_device_cancel': - self.config_flow_updated_parms.update(['tracking', 'restart']) + list_add(self.config_parms_update_control, ['tracking', 'restart']) self.header_msg = 'action_completed' return await self.async_step_device_list() #------------------------------------------------------------------------------------------- - def _delete_this_device(self): + def _delete_this_device(self, conf_device=None): """ Delete the device_tracker entity and associated ic3 configuration """ - devicename = self.conf_device_selected[CONF_IC3_DEVICENAME] + if conf_device: + devicename = conf_device[CONF_IC3_DEVICENAME] + self.conf_device = conf_device + self.conf_device_idx = Gb.conf_devices_idx_by_devicename[devicename] + else: + devicename = self.conf_device[CONF_IC3_DEVICENAME] + event_msg = (f"Configuration Changed > DeleteDevice-{devicename}, " - f"{self.conf_device_selected[CONF_FNAME]}/" - f"{DEVICE_TYPE_FNAME[self.conf_device_selected[CONF_DEVICE_TYPE]]}") + f"{self.conf_device[CONF_FNAME]}/" + f"{DEVICE_TYPE_FNAME[self.conf_device[CONF_DEVICE_TYPE]]}") post_event(event_msg) self._remove_device_tracker_entity(devicename) - Gb.conf_devices.pop(self.conf_device_selected_idx) - self.form_devices_list_all.pop(self.conf_device_selected_idx) - devicename = self.form_devices_list_devicename.pop(self.conf_device_selected_idx) + self.dev_page_item[self.dev_page_no] = '' + Gb.conf_devices.pop(self.conf_device_idx) + self._update_config_file_tracking(update_config_flag=True) + + # The lists may have not been built if deleting a device when deleting an Apple acct + if self.device_items_by_devicename == {}: + return - config_file.write_storage_icloud3_configuration_file() + del self.device_items_by_devicename[devicename] - device_cnt = len(self.form_devices_list_devicename) - 1 - if self.conf_device_selected_idx > device_cnt: - self.conf_device_selected_idx = device_cnt - if self.conf_device_selected_idx < 5: - self.device_list_page_no = 0 #------------------------------------------------------------------------------------------- def _delete_all_devices(self): @@ -2834,31 +2898,28 @@ def _delete_all_devices(self): self._remove_device_tracker_entity(devicename) Gb.conf_devices = [] - self.form_devices_list_all = [] - self.device_list_page_no = 0 - self.conf_device_selected_idx = 0 + self.devicename = {} + self.conf_device_idx = 0 + self.dev_page_item['', '', '', '', ''] - config_file.write_storage_icloud3_configuration_file() + self._update_config_file_tracking(update_config_flag=True) #------------------------------------------------------------------------------------------- def _clear_icloud_mobapp_selection_parms(self): """ - Reset the FamShr, FmF, Mobile App, track_from_zone fields to their initiial values. + Reset the iCloud & Mobile App, track_from_zone fields to their initiial values. Keep the devicename, friendly name, picture and other fields """ for conf_device in Gb.conf_devices: conf_device.update(DEFAULT_DEVICE_REINITIALIZE_CONF) - config_file.write_storage_icloud3_configuration_file() + self._update_config_file_tracking(update_config_flag=True) #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -# -# TRACKED DEVICE MENU - DEVICE LIST, DEVICE UPDATE FORMS -# +# ADD DEVICE #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - async def async_step_add_device(self, user_input=None, errors=None): ''' Display the device form. Validate and update the device parameters @@ -2866,52 +2927,52 @@ async def async_step_add_device(self, user_input=None, errors=None): self.step_id = 'add_device' self.errors = errors or {} self.errors_user_input = {} - await self._build_update_device_selection_lists() + await self._build_update_device_selection_lists(self.conf_device[CONF_IC3_DEVICENAME]) if user_input is None: - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='add_device', + data_schema=form_add_device(self), errors=self.errors) user_input, action_item = self._action_text_to_item(user_input) user_input = self._strip_special_text_from_user_input(user_input, CONF_IC3_DEVICENAME) user_input = self._strip_special_text_from_user_input(user_input, CONF_FNAME) user_input = self._strip_special_text_from_user_input(user_input, CONF_MOBILE_APP_DEVICE) - user_input = self._option_text_to_parm(user_input, CONF_TRACKING_MODE, TRACKING_MODE_ITEMS_KEY_TEXT) + user_input = self._option_text_to_parm(user_input, CONF_TRACKING_MODE, TRACKING_MODE_OPTIONS) user_input = self._option_text_to_parm(user_input, CONF_DEVICE_TYPE, DEVICE_TYPE_FNAME) - log_debug_msg(f"{self.step_id} ({action_item}) > UserInput-{user_input}, Errors-{errors}") + self.log_step_info(user_input, action_item) if (action_item == 'cancel' or user_input[CONF_IC3_DEVICENAME].strip() == ''): return await self.async_step_device_list() - self.add_device_flag = True + self.add_device_flag = self.display_rarely_updated_parms = True self._validate_devicename(user_input) if not self.errors: - self.conf_device_selected.update(user_input) + self.conf_device.update(user_input) - if user_input[MOBAPP] is False: - self.conf_device_selected[CONF_INZONE_INTERVAL] = DEFAULT_GENERAL_CONF[CONF_INZONE_INTERVALS][NO_MOBAPP] - self.conf_device_selected[CONF_MOBILE_APP_DEVICE] = 'None' + if user_input['mobapp'] is False: + self.conf_device[CONF_INZONE_INTERVAL] = DEFAULT_GENERAL_CONF[CONF_INZONE_INTERVALS][NO_MOBAPP] else: device_type = user_input[CONF_DEVICE_TYPE] - self.conf_device_selected[CONF_INZONE_INTERVAL] = DEFAULT_GENERAL_CONF[CONF_INZONE_INTERVALS][device_type] - - self.conf_device_selected.pop(MOBAPP) + self.conf_device[CONF_INZONE_INTERVAL] = DEFAULT_GENERAL_CONF[CONF_INZONE_INTERVALS][device_type] - self.step_id = 'update_device' + self.conf_device.pop('mobapp') if self._any_errors(): self.errors['action_items'] = 'update_aborted' - self.conf_device_selected.update(user_input) + self.conf_device.update(user_input) - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='update_device', + data_schema=form_update_device(self), errors=self.errors, last_step=False) -#------------------------------------------------------------------------------------------- + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# UPDATE DEVICE +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> async def async_step_update_device(self, user_input=None, errors=None): ''' Display the device form. Validate and update the device parameters @@ -2919,93 +2980,159 @@ async def async_step_update_device(self, user_input=None, errors=None): self.step_id = 'update_device' self.errors = errors or {} self.errors_user_input = {} - await self._build_update_device_selection_lists() + + await self._build_update_device_selection_lists(self.conf_device[CONF_IC3_DEVICENAME]) + log_debug_msg(f"{self.step_id.upper()} ( > UserInput-{user_input}, Errors-{errors}") if user_input is None: - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), + return self.async_show_form(step_id='update_device', + data_schema=form_update_device(self), errors=self.errors) user_input, action_item = self._action_text_to_item(user_input) - user_input = self._option_text_to_parm(user_input, CONF_FAMSHR_DEVICENAME, self.famshr_list_text_by_fname) - # user_input = self._option_text_to_parm(user_input, CONF_FMF_EMAIL, self.fmf_list_text_by_email) + self.log_step_info(user_input, action_item) + + # Add rarely used parameters to user input from their current values since they + # If rarely used parms is not in user_input (it is True and not displayed) or is True + # display the fields, otherwise do not display them and fill in there values + if (RARELY_UPDATED_PARMS not in user_input + or isnot_empty(user_input[RARELY_UPDATED_PARMS])): + ui_rarely_updated_parms = True + else: + ui_rarely_updated_parms = False + user_input.pop(RARELY_UPDATED_PARMS, None) + + if self.display_rarely_updated_parms is False: + user_input[CONF_DEVICE_TYPE] = self.conf_device[CONF_DEVICE_TYPE] + user_input[CONF_INZONE_INTERVAL] = self.conf_device[CONF_INZONE_INTERVAL] + user_input[CONF_FIXED_INTERVAL] = self.conf_device[CONF_FIXED_INTERVAL] + user_input[CONF_LOG_ZONES] = self.conf_device[CONF_LOG_ZONES] + user_input[CONF_TRACK_FROM_ZONES] = self.conf_device[CONF_TRACK_FROM_ZONES] + user_input[CONF_TRACK_FROM_BASE_ZONE] = self.conf_device[CONF_TRACK_FROM_BASE_ZONE] + ui_rarely_used_parms_changed = (ui_rarely_updated_parms != self.display_rarely_updated_parms) + self.display_rarely_updated_parms = ui_rarely_updated_parms + + if 'add_device' not in self.conf_device_update_control: + self.dev_page_item[self.dev_page_no] = self.conf_device[CONF_IC3_DEVICENAME] + + if action_item == 'cancel_device_selection': + return await self.async_step_device_list() + elif action_item == 'cancel': + return await self.async_step_menu() + + # Get the dname_username key from the value description of FAMSHR_DEVICENAME field + _icloud_dname_username = [icloud_dname_username + for icloud_dname_username, v in self.icloud_list_text_by_fname.items() + if (v == user_input[CONF_FAMSHR_DEVICENAME])] + + _icloud_dname_username = _icloud_dname_username[0] if isnot_empty(_icloud_dname_username) else 'None' + + # Reset if non-devicename entry selected (one that starts with a '.') + if _icloud_dname_username.startswith('.'): + self.errors[CONF_FAMSHR_DEVICENAME] = 'unknown_icloud' + user_input[CONF_FAMSHR_DEVICENAME] = ( f"{self.conf_device[CONF_FAMSHR_DEVICENAME]}" + f"{LINK}{self.conf_device[CONF_APPLE_ACCOUNT]}") + + self.log_step_info(user_input, action_item) + + if _icloud_dname_username == 'None': + user_input[CONF_APPLE_ACCOUNT] = '' + user_input[CONF_FAMSHR_DEVICENAME] = 'None' + elif instr( _icloud_dname_username, LINK): + icloud_dname_part, username_part = _icloud_dname_username.split(LINK) + user_input[CONF_APPLE_ACCOUNT] = username_part + user_input[CONF_FAMSHR_DEVICENAME] = icloud_dname_part + else: + user_input[CONF_APPLE_ACCOUNT] = self.conf_device[CONF_APPLE_ACCOUNT] + user_input[CONF_FAMSHR_DEVICENAME] = self.conf_device[CONF_FAMSHR_DEVICENAME] user_input[CONF_FMF_EMAIL] = 'None' + user_input = self._option_text_to_parm(user_input, CONF_MOBILE_APP_DEVICE, self.mobapp_list_text_by_entity_id) user_input = self._option_text_to_parm(user_input, CONF_PICTURE, self.picture_by_filename) user_input = self._option_text_to_parm(user_input, CONF_DEVICE_TYPE, DEVICE_TYPE_FNAME) user_input = self._option_text_to_parm(user_input, CONF_TRACK_FROM_BASE_ZONE, self.zone_name_key_text) - user_input = self._strip_special_text_from_user_input(user_input, CONF_IC3_DEVICENAME) - user_input = self._strip_special_text_from_user_input(user_input, CONF_FAMSHR_DEVICENAME) - # user_input = self._strip_special_text_from_user_input(user_input, CONF_FMF_EMAIL) - log_debug_msg(f"{self.step_id} ({action_item}) > UserInput-{user_input}, Errors-{errors}") + self.log_step_info(user_input, action_item) - if action_item == 'cancel': - return await self.async_step_device_list() - - user_input['old_devicename'] = self.conf_device_selected[CONF_IC3_DEVICENAME] + user_input['old_devicename'] = self.conf_device[CONF_IC3_DEVICENAME] user_input = self._validate_devicename(user_input) user_input = self._validate_update_device(user_input) change_flag = self._was_device_data_changed(user_input) + user_input.pop('old_devicename', None) - if not self.errors: - 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 - if 'add_device' in self.sensor_entity_attrs_changed: - Gb.conf_devices.append(self.conf_device_selected) - self.conf_device_selected_idx = len(Gb.conf_devices) - 1 - - # Add the new device to the device_list form and and set it's position index - self.form_devices_list_all.append(self._format_device_list_item(self.conf_device_selected)) - self.form_devices_list_devicename.append(ui_devicename) - - if self.device_list_page_no < int(self.conf_device_selected_idx/5): - self.device_list_page_no += 1 - self.device_list_page_selected_idx[self.device_list_page_no] = \ - self.conf_device_selected_idx - - event_msg = (f"Configuration Changed > AddDevice-{ui_devicename}, " - f"{self.conf_device_selected[CONF_FNAME]}/" - f"{DEVICE_TYPE_FNAME[self.conf_device_selected[CONF_DEVICE_TYPE]]}") - post_event(event_msg) - else: - event_msg = (f"Configuration Changed > ChangeDevice-{ui_devicename}, " - f"{self.conf_device_selected[CONF_FNAME]}/" - f"{DEVICE_TYPE_FNAME[self.conf_device_selected[CONF_DEVICE_TYPE]]}") - post_event(event_msg) - Gb.conf_devices[self.conf_device_selected_idx] = self.conf_device_selected + if self.errors: + self.errors['action_items'] = 'update_aborted' - config_file.write_storage_icloud3_configuration_file() + return self.async_show_form(step_id='update_device', + data_schema=form_update_device(self), + errors=self.errors, + last_step=True) - # Update the device_tracker & sensor entities now that the configuration has been updated - if 'add_device' in self.sensor_entity_attrs_changed: - if Gb.async_add_entities_device_tracker is None: - await Gb.hass.config_entries.async_forward_entry_setups(Gb.config_entry, ['device_tracker']) - self._create_device_tracker_and_sensor_entities(ui_devicename, self.conf_device_selected) + if change_flag is False: + if ui_rarely_used_parms_changed and self.display_rarely_updated_parms: + return await self.async_step_update_device() - else: - self._update_changed_sensor_entities() + return await self.async_step_device_list() - self.header_msg = 'conf_updated' - if only_non_tracked_field_updated: - self.config_flow_updated_parms.update(['devices']) - else: - self.config_flow_updated_parms.update(['tracking', 'restart']) + ui_devicename = user_input[CONF_IC3_DEVICENAME] - return await self.async_step_device_list() + only_non_tracked_field_updated = self._is_only_non_tracked_field_updated(user_input) + self.conf_device.update(user_input) + + # Update the configuration file + if 'add_device' in self.conf_device_update_control: + Gb.conf_devices.append(self.conf_device) + self.conf_device_idx = len(Gb.conf_devices) - 1 + self.dev_page_no = int(self.conf_device_idx / 5) + + # Add the new device to the device_list form and and set it's position index + self.device_items_by_devicename[ui_devicename] = self._format_device_list_item(self.conf_device) + + post_event( f"Configuration Changed > AddDevice-{ui_devicename}, " + f"{self.conf_device[CONF_FNAME]}/" + f"{DEVICE_TYPE_FNAME[self.conf_device[CONF_DEVICE_TYPE]]}") + else: + Gb.conf_devices[self.conf_device_idx] = self.conf_device + + post_event (f"Configuration Changed > ChangeDevice-{ui_devicename}, " + f"{self.conf_device[CONF_FNAME]}/" + f"{DEVICE_TYPE_FNAME[self.conf_device[CONF_DEVICE_TYPE]]}") + + self.dev_page_item[self.dev_page_no] = ui_devicename + self._update_config_file_tracking(update_config_flag=True) + + # Rebuild this list in case anything changed + Gb.devicenames_by_icloud_dname = {} + Gb.icloud_dnames_by_devicename = {} + for conf_device in Gb.conf_devices: + devicename = conf_device.get(CONF_IC3_DEVICENAME) + icloud_dname = conf_device.get(CONF_FAMSHR_DEVICENAME) + Gb.devicenames_by_icloud_dname[icloud_dname] = devicename + Gb.icloud_dnames_by_devicename[devicename] = icloud_dname + + await self._build_icloud_device_selection_list() + + self.header_msg = 'conf_updated' + if only_non_tracked_field_updated: + list_add(self.config_parms_update_control, 'devices') + else: + list_add(self.config_parms_update_control, ['tracking', 'restart']) + + # Update the device_tracker & sensor entities now that the configuration has been updated + if 'add_device' in self.conf_device_update_control: + if Gb.async_add_entities_device_tracker is None: + await Gb.hass.config_entries.async_forward_entry_setups(Gb.config_entry, ['device_tracker']) + self._create_device_tracker_and_sensor_entities(ui_devicename, self.conf_device) + + else: + self._update_changed_sensor_entities() + + if ui_rarely_used_parms_changed: + return await self.async_step_update_device() + + return await self.async_step_device_list() - if self._any_errors(): - self.errors['action_items'] = 'update_aborted' - return self.async_show_form(step_id=self.step_id, - data_schema=self.form_schema(self.step_id), - errors=self.errors, - last_step=True) #------------------------------------------------------------------------------------------- def _is_only_non_tracked_field_updated(self, user_input): @@ -3022,7 +3149,7 @@ def _is_only_non_tracked_field_updated(self, user_input): return False for pname, pvalue in user_input.items(): - if (Gb.conf_devices[self.conf_device_selected_idx][pname] != pvalue + if (Gb.conf_devices[self.conf_device_idx][pname] != pvalue and pname not in DEVICE_NON_TRACKING_FIELDS): return False except: @@ -3035,7 +3162,7 @@ def _validate_devicename(self, user_input): ''' Validate the add device parameters ''' - user_input = self._option_text_to_parm(user_input, CONF_TRACKING_MODE, TRACKING_MODE_ITEMS_KEY_TEXT) + user_input = self._option_text_to_parm(user_input, CONF_TRACKING_MODE, TRACKING_MODE_OPTIONS) ui_devicename = user_input[CONF_IC3_DEVICENAME] = slugify(user_input[CONF_IC3_DEVICENAME]).strip() ui_fname = user_input[CONF_FNAME] = user_input[CONF_FNAME].strip() @@ -3050,14 +3177,9 @@ def _validate_devicename(self, user_input): self.errors[CONF_FNAME] = 'required_field' return user_input - other_ic3_devicename_list = self.form_devices_list_devicename.copy() - if other_ic3_devicename_list: - current_ic3_devicename = Gb.conf_devices[self.conf_device_selected_idx][CONF_IC3_DEVICENAME] - if self.add_device_flag is False and current_ic3_devicename in other_ic3_devicename_list: - other_ic3_devicename_list.remove(current_ic3_devicename) - # Already used if the new ic3_devicename is in the devicename list - if ui_devicename in other_ic3_devicename_list: + if (ui_devicename in self.device_items_by_devicename + and ui_devicename != self.conf_device[CONF_IC3_DEVICENAME]): self.errors[CONF_IC3_DEVICENAME] = 'duplicate_ic3_devicename' self.errors_user_input[CONF_IC3_DEVICENAME] = ui_devicename self.errors_user_input[CONF_IC3_DEVICENAME] = f"{ui_devicename}{DATA_ENTRY_ALERT}Assigned to another iCloud3 device" @@ -3080,6 +3202,50 @@ def _validate_devicename(self, user_input): return user_input +#------------------------------------------------------------------------------------------- + def _validate_data_source_names(self, conf_device): + ''' + Check the devicenames in device's configuration for any errors in the iCloud and + Mobile App fields. + + Parameters: + - conf_device - the conf_device or user_input item for the device + + Return: + - self.errors[xxx] will be set if any errors are found + - True/False - if errors are found/not found + ''' + if self.add_device_flag: + return False + + _conf_apple_acct = conf_device[CONF_APPLE_ACCOUNT] + _conf_icloud_dname = conf_device[CONF_FAMSHR_DEVICENAME] + _conf_mobile_app_name = conf_device[CONF_MOBILE_APP_DEVICE] + icloud_dname_username = f"{_conf_icloud_dname}{LINK}{_conf_apple_acct}" + + if (_conf_apple_acct == '' + and _conf_icloud_dname != 'None'): + self.errors[CONF_FAMSHR_DEVICENAME] = 'unknown_apple_acct' + + elif (_conf_icloud_dname != 'None' + and icloud_dname_username not in self.icloud_list_text_by_fname + and instr(Gb.conf_tracking[CONF_DATA_SOURCE], ICLOUD)): + self.errors[CONF_FAMSHR_DEVICENAME] = 'unknown_icloud' + + elif (conf_device[CONF_TRACKING_MODE] != INACTIVE_DEVICE + and _conf_icloud_dname == 'None' + and _conf_mobile_app_name == 'None'): + self.errors[CONF_FAMSHR_DEVICENAME] = 'no_device_selected' + self.errors[CONF_MOBILE_APP_DEVICE] = 'no_device_selected' + + if (_conf_mobile_app_name != 'None' + and _conf_mobile_app_name not in self.mobapp_list_text_by_entity_id + and instr(Gb.conf_tracking[CONF_DATA_SOURCE], MOBAPP)): + self.errors[CONF_MOBILE_APP_DEVICE] = 'unknown_mobapp' + + return (CONF_FAMSHR_DEVICENAME in self.errors + or CONF_MOBILE_APP_DEVICE in self.errors) + #------------------------------------------------------------------------------------------- def _validate_update_device(self, user_input): """ Validate the device parameters @@ -3092,7 +3258,7 @@ def _validate_update_device(self, user_input): change_fname_flag: True if the fname was changed and the device_tracker entity needs to be updated change_tfz_flag: True if the track_fm_zones zone was changed and the sensors need to be updated """ - # self.errors = {} + ui_devicename = user_input[CONF_IC3_DEVICENAME] old_devicename = user_input.get('old_devicename', ui_devicename) ui_old_devicename = [ui_devicename, old_devicename] @@ -3108,34 +3274,19 @@ def _validate_update_device(self, user_input): if user_input[CONF_FAMSHR_DEVICENAME].strip() == '': user_input[CONF_FAMSHR_DEVICENAME] = 'None' - # if user_input[CONF_FMF_EMAIL].strip() == '': user_input[CONF_FMF_EMAIL] = 'None' - if user_input[CONF_MOBILE_APP_DEVICE].strip() == '': + if (user_input[CONF_MOBILE_APP_DEVICE].strip() == '' + or user_input[CONF_MOBILE_APP_DEVICE] == 'scan_hdr'): user_input[CONF_MOBILE_APP_DEVICE] = 'None' - if (user_input[CONF_TRACKING_MODE] != INACTIVE_DEVICE - and user_input[CONF_FAMSHR_DEVICENAME] == 'None' - and user_input[CONF_FMF_EMAIL] == 'None' - and user_input[CONF_MOBILE_APP_DEVICE] == 'None'): - self.errors['base'] = 'required_field_device' - self.errors[CONF_FAMSHR_DEVICENAME] = 'no_device_selected' - self.errors[CONF_FMF_EMAIL] = 'no_device_selected' - self.errors[CONF_MOBILE_APP_DEVICE] = 'no_device_selected' - - if (user_input[CONF_FAMSHR_DEVICENAME] in self.devicename_by_famshr_fmf - and self.devicename_by_famshr_fmf[user_input[CONF_FAMSHR_DEVICENAME]] not in ui_old_devicename): - self.errors[CONF_FAMSHR_DEVICENAME] = 'already_assigned' - - # if (user_input[CONF_FMF_EMAIL] in self.devicename_by_famshr_fmf - # and self.devicename_by_famshr_fmf[user_input[CONF_FMF_EMAIL]] not in ui_old_devicename): - # self.errors[CONF_FMF_EMAIL] = 'already_assigned' + self._validate_data_source_names(user_input) if self.PyiCloud: - _FamShr = self.PyiCloud.FamilySharing - conf_famshr_fname = user_input[CONF_FAMSHR_DEVICENAME] - device_id = self.PyiCloud.device_id_by_famshr_fname.get(conf_famshr_fname, '') - raw_model, model, model_display_name = self.PyiCloud.device_model_info_by_fname.get(conf_famshr_fname, ['', '', '']) + _AppleDev = self.PyiCloud.DeviceSvc + conf_icloud_dname = user_input[CONF_FAMSHR_DEVICENAME] + device_id = self.PyiCloud.device_id_by_icloud_dname.get(conf_icloud_dname, '') + raw_model, model, model_display_name = self.PyiCloud.device_model_info_by_fname.get(conf_icloud_dname, ['', '', '']) user_input[CONF_FAMSHR_DEVICE_ID] = device_id user_input[CONF_RAW_MODEL] = raw_model user_input[CONF_MODEL] = model @@ -3143,7 +3294,7 @@ def _validate_update_device(self, user_input): # Build 'log_zones' list if ('none' in user_input[CONF_LOG_ZONES] - and 'none' not in self.conf_device_selected[CONF_LOG_ZONES]): + and 'none' not in self.conf_device[CONF_LOG_ZONES]): log_zones = [] else: log_zones = [zone for zone in self.zone_name_key_text.keys() @@ -3160,8 +3311,9 @@ def _validate_update_device(self, user_input): track_from_zones = [zone for zone in self.zone_name_key_text.keys() if (zone in user_input[CONF_TRACK_FROM_ZONES] and zone not in ['.', - self.conf_device_selected[CONF_TRACK_FROM_BASE_ZONE]])] - track_from_zones.append(self.conf_device_selected[CONF_TRACK_FROM_BASE_ZONE]) + self.conf_device[CONF_TRACK_FROM_BASE_ZONE]])] + # track_from_zones.append(self.conf_device[CONF_TRACK_FROM_BASE_ZONE]) + list_add(track_from_zones, user_input[CONF_TRACK_FROM_BASE_ZONE]) user_input[CONF_TRACK_FROM_ZONES] = track_from_zones if isbetween(user_input[CONF_FIXED_INTERVAL], 1, 2): @@ -3184,24 +3336,27 @@ def _was_device_data_changed(self, user_input): return False change_flag = False - self.sensor_entity_attrs_changed[CONF_IC3_DEVICENAME] = self.conf_device_selected[CONF_IC3_DEVICENAME] - self.sensor_entity_attrs_changed['new_ic3_devicename'] = user_input[CONF_IC3_DEVICENAME] - self.sensor_entity_attrs_changed[CONF_TRACKING_MODE] = self.conf_device_selected[CONF_TRACKING_MODE] - self.sensor_entity_attrs_changed['new_tracking_mode'] = user_input[CONF_TRACKING_MODE] + self.conf_device_update_control[CONF_IC3_DEVICENAME] = self.conf_device[CONF_IC3_DEVICENAME] + self.conf_device_update_control['new_ic3_devicename'] = user_input[CONF_IC3_DEVICENAME] + self.conf_device_update_control[CONF_TRACKING_MODE] = self.conf_device[CONF_TRACKING_MODE] + self.conf_device_update_control['new_tracking_mode'] = user_input[CONF_TRACKING_MODE] + + for pname, pvalue in self.conf_device.items(): + if pname in ['evlog_display_order', 'unique_id', 'fmf_device_id']: + continue - for pname, pvalue in self.conf_device_selected.items(): if pname not in user_input or user_input[pname] != pvalue: change_flag = True if pname == CONF_FNAME and user_input[CONF_FNAME] != pvalue: - self.sensor_entity_attrs_changed[CONF_FNAME] = user_input[CONF_FNAME] + self.conf_device_update_control[CONF_FNAME] = user_input[CONF_FNAME] if pname == CONF_TRACK_FROM_ZONES and user_input[CONF_TRACK_FROM_ZONES] != pvalue: new_tfz_zones_list, remove_tfz_zones_list = \ self._devices_form_identify_new_and_removed_tfz_zones(user_input) - self.sensor_entity_attrs_changed['new_tfz_zones'] = new_tfz_zones_list - self.sensor_entity_attrs_changed['remove_tfz_zones'] = remove_tfz_zones_list + self.conf_device_update_control['new_tfz_zones'] = new_tfz_zones_list + self.conf_device_update_control['remove_tfz_zones'] = remove_tfz_zones_list return change_flag @@ -3213,54 +3368,183 @@ def _update_changed_sensor_entities(self): # device_tracker and sensor entities are stored. If the devicename was also changed, the # device_tracker and sensor entity names will be changed later - devicename = self.sensor_entity_attrs_changed[CONF_IC3_DEVICENAME] - new_devicename = self.sensor_entity_attrs_changed['new_ic3_devicename'] - tracking_mode = self.sensor_entity_attrs_changed[CONF_TRACKING_MODE] - new_tracking_mode = self.sensor_entity_attrs_changed['new_tracking_mode'] + devicename = self.conf_device_update_control[CONF_IC3_DEVICENAME] + new_devicename = self.conf_device_update_control['new_ic3_devicename'] + tracking_mode = self.conf_device_update_control[CONF_TRACKING_MODE] + new_tracking_mode = self.conf_device_update_control['new_tracking_mode'] # Remove the new track_fm_zone sensors just unchecked - if 'remove_tfz_zones' in self.sensor_entity_attrs_changed: - remove_tfz_zones_list = self.sensor_entity_attrs_changed['remove_tfz_zones'] + if 'remove_tfz_zones' in self.conf_device_update_control: + remove_tfz_zones_list = self.conf_device_update_control['remove_tfz_zones'] self.remove_track_fm_zone_sensor_entity(devicename, remove_tfz_zones_list) # Create the new track_fm_zone sensors just checked - if 'new_tfz_zones' in self.sensor_entity_attrs_changed: - new_tfz_zones_list = self.sensor_entity_attrs_changed['new_tfz_zones'] + if 'new_tfz_zones' in self.conf_device_update_control: + new_tfz_zones_list = self.conf_device_update_control['new_tfz_zones'] self._create_track_fm_zone_sensor_entity(devicename, new_tfz_zones_list) - # fname was changed - change the fname of device_tracker and all sensors to the new fname - # Inactive devices were not created so they are not in Gb.DeviceTrackers_by_devicename - if (devicename == new_devicename - and CONF_FNAME in self.sensor_entity_attrs_changed - and devicename in Gb.DeviceTrackers_by_devicename): - DeviceTracker = Gb.DeviceTrackers_by_devicename[devicename] - DeviceTracker.update_entity_attribute(new_fname=self.conf_device_selected[CONF_FNAME]) + # fname was changed - change the fname of device_tracker and all sensors to the new fname + # Inactive devices were not created so they are not in Gb.DeviceTrackers_by_devicename + if (devicename == new_devicename + and CONF_FNAME in self.conf_device_update_control + and devicename in Gb.DeviceTrackers_by_devicename): + DeviceTracker = Gb.DeviceTrackers_by_devicename[devicename] + DeviceTracker.update_entity_attribute(new_fname=self.conf_device[CONF_FNAME]) + + try: + for sensor, Sensor in Gb.Sensors_by_devicename[devicename].items(): + Sensor.update_entity_attribute(new_fname=self.conf_device[CONF_FNAME]) + except: + pass + + # check to see if device has tfz sensors + try: + for sensor, Sensor in Gb.Sensors_by_devicename_from_zone[devicename].items(): + Sensor.update_entity_attribute(new_fname=self.conf_device[CONF_FNAME]) + except: + pass + + # devicename was changed - delete device_tracker and all sensors for devicename and add them for new_devicename + if devicename != new_devicename: + self._update_config_file_tracking() + self._create_device_tracker_and_sensor_entities(new_devicename, self.conf_device) + self._remove_device_tracker_entity(devicename) + + # If the device was 'inactive' it's entity may not exist since they are not created for + # inactive devices. If so, create it now if it is no longer 'inactive'. + elif (tracking_mode == INACTIVE_DEVICE + and new_tracking_mode != INACTIVE_DEVICE + and new_devicename not in Gb.DeviceTrackers_by_devicename): + self._create_device_tracker_and_sensor_entities(new_devicename, self.conf_device) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# CHANGE DEVICE ORDER +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + async def async_step_change_device_order(self, user_input=None, errors=None): + self.step_id = 'change_device_order' + user_input, action_item = self._action_text_to_item(user_input) + + if user_input is None: + self.log_step_info(user_input, action_item) + self.cdo_devicenames = [self._format_device_text_hdr(conf_device) + for conf_device in Gb.conf_devices] + self.cdo_new_order_idx = [x for x in range(0, len(Gb.conf_devices))] + self.actions_list_default = 'move_down' + return self.async_show_form(step_id='change_device_order', + data_schema=form_change_device_order(self), + errors=self.errors) + + if action_item == 'cancel': + return await self.async_step_device_list() + + if action_item == 'save': + new_conf_devices = [] + for idx in self.cdo_new_order_idx: + new_conf_devices.append(Gb.conf_devices[idx]) + + Gb.conf_devices = new_conf_devices + config_file.set_conf_devices_index_by_devicename() + self._update_config_file_tracking(update_config_flag=True) + self._build_devices_list() + list_add(self.config_parms_update_control, ['restart', 'profile']) + self.errors['base'] = 'conf_updated' + return await self.async_step_device_list() + + self.cdo_curr_idx = self.cdo_devicenames.index(user_input['device_desc']) + new_idx = self.cdo_curr_idx + + if action_item == 'move_up': + if new_idx > 0: + new_idx = new_idx - 1 + + elif action_item == 'move_down': + if new_idx < len(self.cdo_devicenames) - 1: + new_idx = new_idx + 1 + self.actions_list_default = action_item + + if new_idx != self.cdo_curr_idx: + self.cdo_devicenames[self.cdo_curr_idx], self.cdo_devicenames[new_idx] = \ + self.cdo_devicenames[new_idx], self.cdo_devicenames[self.cdo_curr_idx] + self.cdo_new_order_idx[self.cdo_curr_idx], self.cdo_new_order_idx[new_idx] = \ + self.cdo_new_order_idx[new_idx], self.cdo_new_order_idx[self.cdo_curr_idx] + + self.cdo_curr_idx = new_idx + + return self.async_show_form(step_id='change_device_order', + data_schema=form_change_device_order(self), + errors=self.errors) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# AWAY TIME ZONE +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + async def async_step_away_time_zone(self, user_input=None, errors=None): + self.step_id = 'away_time_zone' + user_input, action_item = self._action_text_to_item(user_input) + + self._build_away_time_zone_devices_list() + self._build_away_time_zone_hours_list() + + if self.common_form_handler(user_input, action_item, errors): + return await self.async_step_menu() + + if self._any_errors(): + self.errors['action_items'] = 'update_aborted' + + return self.async_show_form(step_id= 'away_time_zone', + data_schema=form_away_time_zone(self), + errors=self.errors) + +#------------------------------------------------------------------------------------------- + def _build_away_time_zone_hours_list(self): + if self.away_time_zone_hours_key_text != {}: + return + + ha_time = int(Gb.this_update_time[0:2]) + for hh in range(ha_time-12, ha_time+13): + away_hh = hh + 24 if hh < 0 else hh + + if away_hh == 0: ap_hh = 12; ap = 'a' + elif away_hh < 12: ap_hh = away_hh; ap = 'a' + elif away_hh == 12: ap_hh = 12; ap = 'p' + else: ap_hh = away_hh - 12; ap = 'p' + + if away_hh >= 24: + away_hh -= 24 + if ap_hh == 12: ap = 'a' + elif ap_hh >= 13: ap_hh -= 12; ap = 'a' + + if Gb.time_format_12_hour: + time_str = f"{ap_hh:}{Gb.this_update_time[2:]}{ap}" + else: + time_str = f"{away_hh:02}{Gb.this_update_time[2:]}" + + if away_hh == ha_time: + time_str = f"Home Time Zone" + elif hh < ha_time: + time_str += f" (-{abs(hh-ha_time):} hours)" + else: + time_str += f" (+{abs(ha_time-hh):} hours)" + self.away_time_zone_hours_key_text[hh-ha_time] = time_str + +#------------------------------------------------------------------------------------------- + def _build_away_time_zone_devices_list(self): - try: - for sensor, Sensor in Gb.Sensors_by_devicename[devicename].items(): - Sensor.update_entity_attribute(new_fname=self.conf_device_selected[CONF_FNAME]) - except: - pass + 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()) - # v3.0.0-beta3-Added check to see if device has tfz sensors - try: - for sensor, Sensor in Gb.Sensors_by_devicename_from_zone[devicename].items(): - Sensor.update_entity_attribute(new_fname=self.conf_device_selected[CONF_FNAME]) - except: - pass +#------------------------------------------------------------------------------------------- + def _build_log_level_devices_list(self): - # devicename was changed - delete device_tracker and all sensors for devicename and add them for new_devicename - if devicename != new_devicename: - config_file.write_storage_icloud3_configuration_file() - self._create_device_tracker_and_sensor_entities(new_devicename, self.conf_device_selected) - self._remove_device_tracker_entity(devicename) + self.log_level_devices_key_text = {'all': 'All Devices - Add RawData for all devices to the `icloud-0.log` file'} + self.log_level_devices_key_text.update(self._devices_selection_list()) - # If the device was 'inactive' it's entity may not exist since they are not created for - # inactive devices. If so, create it now if it is no longer 'inactive'. - elif (tracking_mode == 'inactive' - and new_tracking_mode != 'inactive' - and new_devicename not in Gb.DeviceTrackers_by_devicename): - self._create_device_tracker_and_sensor_entities(new_devicename, self.conf_device_selected) +#------------------------------------------------------------------------------------------- + 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} #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> @@ -3269,289 +3553,427 @@ def _update_changed_sensor_entities(self): # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - async def _build_update_device_selection_lists(self): - """ Setup the option lists used to select device parameters """ - - await self._build_picture_filename_list() - await self._build_mobapp_entity_list() - await self._build_zone_list() - - await self._build_famshr_devices_list() - # self._build_fmf_devices_list() - await self._build_devicename_by_famshr_fmf() - -#---------------------------------------------------------------------- - async def _build_famshr_devices_list(self): + def _build_apple_accounts_list(self): ''' - 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). + Build a list of the Apple Accounts that is used in the data source, + username/password and reauthentication screens to s select the + Apple Account or add a new one. - This creates the list of devices used on the update devices screen + Parameters: + include_icloud_dnames: + True - Add a list of the devices in the Apple Account and add a + new account option ''' - self.famshr_list_text_by_fname_base = NONE_DICT_KEY_TEXT.copy() - if self.PyiCloud is None: - return + self.apple_acct_items_by_username = {} + self.is_verification_code_needed = False - 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) + aa_idx = -1 + for apple_account in Gb.conf_apple_accounts: + aa_idx += 1 + username = apple_account[CONF_USERNAME] + devicenames_by_username, icloud_dnames_by_username = \ + self.get_conf_device_names_by_username(username) + devices_assigned_cnt = len(devicenames_by_username) - if _FamShr := self.PyiCloud.FamilySharing: - self._check_finish_v2v3conversion_for_famshr_fname() + if aa_idx == 0 and username == '': + break + elif username == '': + continue + else: + PyiCloud = Gb.PyiCloud_by_username.get(username) + if PyiCloud: + aa_text = f"{PyiCloud.account_owner.split('@')[0]}{RARROW}" + if PyiCloud.requires_2fa: + self.is_verification_code_needed = True + aa_text += f"{RED_ALERT} AUTHENTICATION NEEDED, " - sorted_famshr_info_by_famshr_fname = sort_dict_by_values(self.PyiCloud.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() + aa_text += (f"{devices_assigned_cnt} of " + f"{len(PyiCloud.icloud_dnames)} iCloud Devices Tracked") -#---------------------------------------------------------------------- - def _check_finish_v2v3conversion_for_famshr_fname(self): + else: + aa_text = f"{username}{RARROW}{RED_ALERT} Not logged into this Apple Account" + + self.apple_acct_items_by_username[username] = aa_text + +#------------------------------------------------------------------------------------------- + def _build_devices_list(self): ''' - This will be done if the v2 files were just converted to the v3 configuration. - Finish setting up the device by determining the actual FamShr devicename and the - raw_model, model, model_display_name and device_id fields. + Rebuild the device list for displaying on the devices list form. This is necessary + since the parameters displayed may have been changed. Update the default values for + each page for the device selected on each page. ''' + self.device_items_by_devicename = {} - if self.PyiCloud is None or self.PyiCloud.FamilySharing is None: - return - - _FamShr = self.PyiCloud.FamilySharing - famshr_devicenames = [conf_device[CONF_FAMSHR_DEVICENAME] - for conf_device in Gb.conf_devices - if conf_device[CONF_FAMSHR_DEVICENAME] == \ - conf_device[CONF_IC3_DEVICENAME]] + # Format all the device info to be listed on the form + for conf_device in Gb.conf_devices: + conf_device[CONF_IC3_DEVICENAME] = conf_device[CONF_IC3_DEVICENAME].replace(' ', '_') + self.device_items_by_devicename[conf_device[CONF_IC3_DEVICENAME]] = \ + self._format_device_list_item(conf_device) - if famshr_devicenames == []: + # No devices in config, reset to initial conditions + if self.device_items_by_devicename == {}: + self.conf_device_idx = 0 return - # Build a dictionary of the FamShr fnames to compare to the ic3_devicename {gary_iphone: Gary-iPhone} - famshr_fname_by_ic3_devicename = {slugify(fname).strip(): fname - for fname in self.PyiCloud.device_model_info_by_fname.keys()} - - # Cycle thru conf_devices and see if there are any ic3_devicename = famshr_fname entries. - # If so, they were just converted and the real famshr_devicename needs to be reset to the actual - # value from the PyiCloud RawData fields - update_conf_file_flag = False - for conf_device in Gb.conf_devices: - famshr_devicename = conf_device[CONF_FAMSHR_DEVICENAME] - ic3_devicename = conf_device[CONF_IC3_DEVICENAME] +#------------------------------------------------------------------------------------------- + def _format_device_list_item(self, conf_device): + ''' + Format the text that is displayed for the device on the device_list screen + ''' + device_text = (f"{conf_device[CONF_FNAME]}" + f" ({conf_device[CONF_IC3_DEVICENAME]}){RARROW}") - if (famshr_devicename != ic3_devicename - or famshr_devicename not in famshr_fname_by_ic3_devicename): - continue + if conf_device[CONF_TRACKING_MODE] == MONITOR_DEVICE: + device_text += "MONITOR, " + elif conf_device[CONF_TRACKING_MODE] == INACTIVE_DEVICE: + device_text += "✪ INACTIVE, " + + if conf_device[CONF_FAMSHR_DEVICENAME] != 'None': + icloud_dname = conf_device[CONF_FAMSHR_DEVICENAME] + apple_acct = conf_device[CONF_APPLE_ACCOUNT] + device_text += "iCloud > " + if PyiCloud := Gb.PyiCloud_by_username.get(apple_acct): + if icloud_dname in PyiCloud.device_id_by_icloud_dname: + device_text += f"{icloud_dname}{PyiCloud.account_owner_link}, " + else: + device_text += f"{RED_X}{icloud_dname}{PyiCloud.account_owner_link}" + device_text += " (DEVICE NOT IN APPLE ACCT), " + if PyiCloud.requires_2fa: + device_text += f" {YELLOW_ALERT}AUTH NEEDED, " + elif apple_acct in ['', 'None']: + device_text += f"{icloud_dname}{LINK}{RED_X}UNKNOWN{RLINK}, " + #device_text += " (NO APPLE ACCT), " + else: + device_text += f"{icloud_dname}{LINK}{RED_X}UNKNOWN-{apple_acct}{RLINK}, " + #device_text += " (UNKNOWN APPLE ACCT), " + + if conf_device[CONF_MOBILE_APP_DEVICE] != 'None': + mobapp_dname = conf_device[CONF_MOBILE_APP_DEVICE] + device_text += "MobApp > " + if mobapp_dname in Gb.device_info_by_mobapp_dname: + device_text += f"{Gb.device_info_by_mobapp_dname[mobapp_dname][0]}, " + else: + device_text += f"{RED_X}UNKNOWN-{mobapp_dname}, " - famshr_fname = famshr_fname_by_ic3_devicename[ic3_devicename] - conf_device[CONF_FAMSHR_DEVICENAME] = famshr_fname + if conf_device[CONF_TRACK_FROM_BASE_ZONE] != HOME: + tfhbz = conf_device[CONF_TRACK_FROM_BASE_ZONE] + device_text += f"PrimaryHomeZone > {zone_dname(tfhbz)}, " - raw_model, model, model_display_name = \ - self.PyiCloud.device_model_info_by_fname[famshr_fname] - device_id = Gb.device_id_by_famshr_fname[famshr_fname] + if conf_device[CONF_TRACK_FROM_ZONES] != [HOME]: + tfz_fnames = [zone_dname(z) for z in conf_device[CONF_TRACK_FROM_ZONES]] + device_text += f"TrackFromZones > {list_to_str(tfz_fnames)}, " - conf_device[CONF_FAMSHR_DEVICE_ID] = device_id - conf_device[CONF_MODEL] = model - conf_device[CONF_MODEL_DISPLAY_NAME] = model_display_name - conf_device[CONF_RAW_MODEL] = raw_model - update_conf_file_flag = True + device_text = device_text.replace(' , ', ' ') - if update_conf_file_flag: - config_file.write_storage_icloud3_configuration_file() + return device_text[:-2] #---------------------------------------------------------------------- - def _build_fmf_devices_list(self): + async def _build_update_device_selection_lists(self, selected_devicename=None): ''' - Cycle through fmf following, followers and contact details data and get - devices that can be tracked for the icloud device selection list + Setup the option lists used to select device parameters + + Parameter: + selected_device - The iC3 devicename being added or updated on the Update + Devices screen. This is used to highlight the selected device and + place it at the top of the finddev device list ''' - self.fmf_list_text_by_email_base = NONE_DICT_KEY_TEXT.copy() + try: + await self._build_icloud_device_selection_list(selected_devicename) + await self._build_mobapp_entity_selection_list(selected_devicename) + await self._build_picture_filename_selection_list() + await self._build_zone_selection_list() - if self.PyiCloud is None: - return + # _xtraceha(f"{self.icloud_list_text_by_fname=}") + # _xtraceha(f"{self.mobapp_list_text_by_entity_id=}") + # _xtraceha(f"{self.conf_device=}") + # _xtraceha(f"{self.zone_name_key_text=}") - # devices_desc = start_ic3.get_fmf_devices(self.PyiCloud) - # self.fmf_list_text_by_email_base.update(devices_desc[2]) - if _FmF := self.PyiCloud.FindMyFriends: - # if _FmF: - self.fmf_list_text_by_email_base.update(_FmF.device_info_by_fmf_email) - self.fmf_list_text_by_email = self.fmf_list_text_by_email_base.copy() + except Exception as err: + log_exception(err) #---------------------------------------------------------------------- - async def _build_devicename_by_famshr_fmf(self, current_devicename=None): - ''' - Cycle thru the configured devices and build a devicename by the - famshr fname and fmf email values. This is used to validate these - items are only assigned to one devicename. + async def _build_icloud_device_selection_list(self, selected_devicename=None): ''' - self.devicename_by_famshr_fmf = {} - for conf_device in Gb.conf_devices: - if conf_device[CONF_FAMSHR_DEVICENAME] != 'None': - self.devicename_by_famshr_fmf[conf_device[CONF_FAMSHR_DEVICENAME]] = \ - conf_device[CONF_IC3_DEVICENAME] - if conf_device[CONF_FMF_EMAIL] != 'None': - self.devicename_by_famshr_fmf[conf_device[CONF_FMF_EMAIL]] = \ - conf_device[CONF_IC3_DEVICENAME] - - 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_alert = '' - try: - if current_devicename != self.devicename_by_famshr_fmf[famshr_devicename]: - devicename_msg_alert = 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"{devicename_msg_alert}{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(): - # devicename_msg = '' - # try: - # if current_devicename != self.devicename_by_famshr_fmf[fmf_email]: - # devicename_msg = ( f"{RARROW}ASSIGNED TO-" - # f"{self.devicename_by_famshr_fmf[fmf_email]}") - # except: - # pass - # self.fmf_list_text_by_email[fmf_email] = f"{fmf_text}{devicename_msg}" + Create the iCloud object if it does not exist. This will create the icloud_info_by_icloud_dname + 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). -#---------------------------------------------------------------------- - async def _build_mobapp_entity_list(self): - ''' - Cycle through the /config/.storage/core.entity_registry file and return - the entities for platform ('mobile_app', etc) + This creates the list of devices used on the update devices screen ''' + self.icloud_list_text_by_fname_base = NONE_DICT_KEY_TEXT.copy() + self.icloud_list_text_by_fname2 = NONE_FAMSHR_DICT_KEY_TEXT.copy() + all_devices_available = {} + all_devices_used = {} + all_devices_this_device = {} + all_devices_unknown_device = {} + username_hdr_available = {} + selected_device_icloud_dname = 'None' if is_empty(Gb.conf_devices) else '' + + # Get the list of devices with unknown apple accts + for conf_device in Gb.conf_devices: + if conf_device[CONF_FAMSHR_DEVICENAME] == 'None': + continue - # Build dict of all HA device_tracker entity devicenames ({devicename: platform}) - mobapp_entities, mobapp_entity_data = entity_io.get_entity_registry_data(domain='device_tracker') - self.device_trkr_by_entity_id_all = {entity_io._base_entity_id(k): v['platform'] - for k, v in mobapp_entity_data.items()} - - # Build dict of Mobile App device_tracker entity devicenames ({devicename: entity_id > fname}) - mobapp_entities, mobapp_entity_data = \ - 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() + if Gb.username_valid_by_username.get(conf_device[CONF_APPLE_ACCOUNT], False) is False: + icloud_dname_username = f".{conf_device[CONF_IC3_DEVICENAME]}{LINK}UNKNOWN" + self.icloud_list_text_by_fname2[icloud_dname_username] = ( + f"{RED_X}{conf_device[CONF_FNAME]} ({conf_device[CONF_IC3_DEVICENAME]})" + f"{LINK}{conf_device[CONF_APPLE_ACCOUNT]}{RLINK}" + f"{RARROW}UNKNOWN APPLE ACCOUNT") - # 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()} + # Save the FamShr config parameter in case it is not found + if conf_device[CONF_IC3_DEVICENAME] == selected_devicename: + selected_device_icloud_dname = conf_device[CONF_FAMSHR_DEVICENAME] - # Get `Search` items - try: - 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()} - except: - search_mobapp_devices = {} + # Get the list of devices with valid apple accounts + aa_idx = 0 + for apple_account in Gb.conf_apple_accounts: + username = apple_account[CONF_USERNAME] + aa_idx += 1 - 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)) + if Gb.username_valid_by_username.get(username, False) is False: + continue - return + PyiCloud = Gb.PyiCloud_by_username.get(username) + if PyiCloud is None: + continue -#------------------------------------------------------------------------------------------- - @staticmethod - def _mobapp_fname(entity_attrs): - return entity_attrs.get('name') or entity_attrs.get('original_name') or 'Unknown' + if PyiCloud.DeviceSvc is None or PyiCloud.is_DeviceSvc_setup_complete is False: + _AppleDev = await Gb.hass.async_add_executor_job( + self.create_DeviceSvc_config_flow, + PyiCloud) + + if PyiCloud: + self._check_finish_v2v3conversion_for_icloud_dname() + + devices_available, devices_used, this_device = \ + self._get_icloud_devices_list_avail_used_this(aa_idx, + PyiCloud, PyiCloud.account_owner, + selected_devicename) + # Available devices + devices_cnt = len(devices_used) + len(devices_available) + len(this_device) + assigned_cnt = len(devices_used) + len(this_device) + aa_idx_dots = '.'*aa_idx + username_hdr_available = {f"{aa_idx_dots}hdr": + f"🍎 ~~~~ {PyiCloud.account_owner}, " + f"Apple Account #{aa_idx} of {len(Gb.conf_apple_accounts)} ~~~~ " + f"{assigned_cnt} of {devices_cnt} Tracked ~~~~"} + if devices_available == {}: + devices_available = {f"{aa_idx_dots}nodev": + f"⊗ All Apple account devices are assigned"} + + all_devices_available.update(username_hdr_available) + all_devices_available.update(devices_available) + all_devices_used.update(devices_used) + all_devices_this_device.update(this_device) + + if (is_empty(all_devices_this_device) + and selected_device_icloud_dname != 'None'): + this_device = {f".{conf_device[CONF_IC3_DEVICENAME]}{LINK}UNKNOWN": + f"{RED_X}{conf_device[CONF_FNAME]} ({conf_device[CONF_IC3_DEVICENAME]})" + f"{LINK}{conf_device[CONF_APPLE_ACCOUNT]}{RLINK}" + f"{RARROW}UNKNOWN APPLE ACCOUNT"} + + self.icloud_list_text_by_fname2.update(all_devices_this_device) + self.icloud_list_text_by_fname2.update(all_devices_available) + self.icloud_list_text_by_fname2.update({f".assigned": f"⛔ ~~~~ ASSIGNED TO ICLOUD3 DEVICES ~~~~"}) + self.icloud_list_text_by_fname2.update(sort_dict_by_values(all_devices_used)) + + self.icloud_list_text_by_fname = self.icloud_list_text_by_fname2.copy() -#------------------------------------------------------------------------------------------- - def _prepare_device_selection_list(self): +#---------------------------------------------------------------------- + def _get_icloud_devices_list_avail_used_this( self, aa_idx, PyiCloud, apple_acct_owner, + selected_devicename=None): ''' - Rebuild the device list for displaying on the devices list form. This is necessary - since the parameters displayed may have been changed. Update the default values for - each page for the device selected on each page. + Build the dictionary with the Apple Account devices + + Return: + [devices_available, devices_used, devices_this_device] ''' - self.form_devices_list_all = [] - self.form_devices_list_displayed = [] - self.form_devices_list_devicename = [] + this_device = {} + devices_available = {} + devices_used = {} + unknown_devices = {} + famshr_available = {} + owner_available = {} + aa_idx_msg = f"#{aa_idx} - " + aa_idx_dots = '.'*aa_idx + + devices_assigned = {} + selected_device_icloud_dname = '' + for _conf_device in Gb.conf_devices: + if _conf_device[CONF_APPLE_ACCOUNT] != PyiCloud.username: + continue - # Format all the device info to be listed on the form - for conf_device_data in Gb.conf_devices: - conf_device_data[CONF_IC3_DEVICENAME] = conf_device_data[CONF_IC3_DEVICENAME].replace(' ', '_') - self.form_devices_list_all.append(self._format_device_list_item(conf_device_data)) - self.form_devices_list_devicename.append(conf_device_data[CONF_IC3_DEVICENAME]) + if _conf_device[CONF_FAMSHR_DEVICENAME] != 'None': + devices_assigned[_conf_device[CONF_FAMSHR_DEVICENAME]] = _conf_device[CONF_IC3_DEVICENAME] + devices_assigned[_conf_device[CONF_IC3_DEVICENAME]] = _conf_device[CONF_FAMSHR_DEVICENAME] - # No devices in config, reset to initial conditions - if self.form_devices_list_all == []: - self.conf_device_selected_idx = 0 - self.device_list_page_no = 0 - self.device_list_page_selected_idx[0] = 0 - return + if _conf_device[CONF_FAMSHR_DEVICENAME] not in PyiCloud.device_id_by_icloud_dname: + icloud_dname_username = f"{_conf_device[CONF_FAMSHR_DEVICENAME]}{LINK}{PyiCloud.username}" + icloud_dname_owner = f"{_conf_device[CONF_FAMSHR_DEVICENAME]}{LINK}{PyiCloud.account_owner}{RLINK}" + unknown_devices[icloud_dname_username] = ( + f"{RED_X}{icloud_dname_owner} >" + f"{RARROW}UNKNOWN DEVICE") - # Build the device-list page items - device_from_pos = self.device_list_page_no * 5 - self.form_devices_list_displayed = self.form_devices_list_all[device_from_pos:device_from_pos+5] + try: + for icloud_dname, device_display_name in PyiCloud.device_model_name_by_icloud_dname.items(): + icloud_dname_username = f"{icloud_dname}{LINK}{PyiCloud.username}" + icloud_dname_owner = f"{icloud_dname}{LINK}{PyiCloud.account_owner}{RLINK}" + + # If not assigned to an ic3 device + if icloud_dname not in devices_assigned: + device_id = PyiCloud.device_id_by_icloud_dname[icloud_dname] + _RawData = PyiCloud.RawData_by_device_id[device_id] + if _RawData.family_share_device: + famshr_available[icloud_dname_username] = ( + f"{icloud_dname_owner} > " + f"{device_display_name}, " + f"Family Share Device" + f"{aa_idx_dots}") + else: + owner_available[icloud_dname_username] = ( + f"{icloud_dname_owner} > " + f"{device_display_name}" + f"{aa_idx_dots}") + continue - # Build list of devices on next page - device_from_pos = device_from_pos + 5 - if device_from_pos >= len(self.form_devices_list_devicename): - device_from_pos = 0 - self.next_page_devices_list = ", ".join(self.form_devices_list_devicename[device_from_pos:device_from_pos+5]) + # Is the icloud device name assigned to the current device being updated + devicename = devices_assigned[icloud_dname] + if devicename == selected_devicename: + this_device[icloud_dname_username] = ( + f"{icloud_dname_owner} > " + f"{device_display_name}") + continue - # Save the selected item info just updated to be used in reselecting the same item via the default value - self.device_list_page_selected_idx[self.device_list_page_no] = self.conf_device_selected_idx + # Assigned to another device + _assigned_to_fname = self._icloud_device_assigned_to(PyiCloud.username, icloud_dname) + if _assigned_to_fname: + devices_used[icloud_dname_username] = ( + f"{icloud_dname_owner}{RARROW}" + f"{_assigned_to_fname}, " + f"{device_display_name}") -#------------------------------------------------------------------------------------------- - def _format_device_list_item(self, conf_device_data): - """ Format the text that is displayed for the device on the device_list form """ + except Exception as err: + log_exception(err) + + devices_available.update(sort_dict_by_values(unknown_devices)) + devices_available.update(sort_dict_by_values(owner_available)) + devices_available.update(sort_dict_by_values(famshr_available)) + + return devices_available, devices_used, this_device - device_info = (f"{conf_device_data[CONF_IC3_DEVICENAME]}{RARROW}") +#---------------------------------------------------------------------- + def _icloud_device_assigned_to(self, username, icloud_dname): + _assigned_to_fname = [f"{conf_device[CONF_FNAME]} ({conf_device[CONF_IC3_DEVICENAME]})" + for conf_device in Gb.conf_devices + if (username == conf_device[CONF_APPLE_ACCOUNT] + and icloud_dname == conf_device[CONF_FAMSHR_DEVICENAME])] + + if _assigned_to_fname: + return _assigned_to_fname[0] + else: + return '' - if conf_device_data[CONF_TRACKING_MODE] == MONITOR_DEVICE: - device_info += "MONITOR, " - elif conf_device_data[CONF_TRACKING_MODE] == INACTIVE_DEVICE: - device_info += "INACTIVE, " +#---------------------------------------------------------------------- + async def _build_mobapp_entity_selection_list(self, selected_devicename=None): + ''' + Cycle through the /config/.storage/core.entity_registry file and return + the entities for platform ('mobile_app', etc) - device_info += f"{conf_device_data[CONF_FNAME]}" + Gb.devicenames_by_mobapp_dname={'gary_iphone_app': 'gary_iphone', 'Gary-iPhone-MobApp': 'gary_iphone'} + Gb.device_info_by_mobapp_dname={'gary_iphone_app': ['Gary-iPhone-MobApp', 'iPhone17,2', 'iPhone', 'iPhone 16 Pro Max'], ... + mobapp_devices={'gary_iphone_app': 'Gary-iPhone-MobApp (iPhone17,2); device_tracker.gary_iphone_app'} + ''' - if conf_device_data[CONF_FAMSHR_DEVICENAME] != 'None': - device_info += f", FamShr-({conf_device_data[CONF_FAMSHR_DEVICENAME]})" + devices_this_device = {} + devices_available = {} + devices_used = {} + scan_for_mobapp_devices = {} + self.mobapp_list_text_by_entity_id = MOBAPP_DEVICE_NONE_OPTIONS.copy() + + Gb.devicenames_by_mobapp_dname = {} + Gb.mobapp_dnames_by_devicename = {} + for _conf_device in Gb.conf_devices: + if _conf_device[CONF_MOBILE_APP_DEVICE] != 'None': + Gb.devicenames_by_mobapp_dname[_conf_device[CONF_MOBILE_APP_DEVICE]] =\ + _conf_device[CONF_IC3_DEVICENAME] + Gb.mobapp_dnames_by_devicename[_conf_device[CONF_IC3_DEVICENAME]] =\ + _conf_device[CONF_MOBILE_APP_DEVICE] + + # if is_empty(Gb.devicenames_by_mobapp_dname): + # return + + mobapp_devices ={mobapp_dname:( + f"{mobapp_info[0]} " + f"(device_tracker.{mobapp_dname}) > " + f"{mobapp_info[3]}") + for mobapp_dname, mobapp_info in Gb.device_info_by_mobapp_dname.items()} + + for mobapp_dname, mobapp_info in mobapp_devices.items(): + if mobapp_dname not in Gb.devicenames_by_mobapp_dname: + devices_available[mobapp_dname] = mobapp_info + continue - # if conf_device_data[CONF_FMF_EMAIL] != 'None': - # device_info += f", FmF-({conf_device_data[CONF_FMF_EMAIL]})" + if (selected_devicename + and mobapp_dname == self.conf_device[CONF_MOBILE_APP_DEVICE]): + devices_this_device[mobapp_dname] = mobapp_info + continue - if conf_device_data[CONF_MOBILE_APP_DEVICE] != 'None': - device_info += f", MobApp-({conf_device_data[CONF_MOBILE_APP_DEVICE]})" + else: + devicename = Gb.devicenames_by_mobapp_dname[mobapp_dname] + Device = Gb.Devices_by_devicename[devicename] + devices_used[mobapp_dname] = ( + f"{mobapp_info.split(';')[0]}{RARROW}" + f"ASSIGNED TO-{Device.fname_devicename}") - if conf_device_data[CONF_TRACK_FROM_ZONES] != [HOME]: - tfz_fnames = [zone_dname(z) - for z in conf_device_data[CONF_TRACK_FROM_ZONES]] - device_info += f", TrackFromZones-({', '.join(tfz_fnames)})" - if conf_device_data[CONF_TRACK_FROM_BASE_ZONE] != HOME: - z = conf_device_data[CONF_TRACK_FROM_BASE_ZONE] - device_info += f", PrimaryHomeZone-({zone_dname(z)})" + try: + scan_for_mobapp_devices = { + f"ScanFor: {_conf_device[CONF_IC3_DEVICENAME]}": ( + f"Starting with > " + f"{_conf_device[CONF_IC3_DEVICENAME]} " + f"({_conf_device[CONF_FNAME]})") + for _conf_device in Gb.conf_devices} + except: + scan_for_mobapp_devices = {} + + if (selected_devicename + and is_empty(devices_this_device) + and self.conf_device[CONF_MOBILE_APP_DEVICE] != 'None'): + devices_this_device = {'.unknown': + f"{RED_X}{self.conf_device[CONF_MOBILE_APP_DEVICE]}{RARROW}UNKNOWN MOBILE APP DEVICE"} + + self.mobapp_list_text_by_entity_id.update(devices_this_device) + self.mobapp_list_text_by_entity_id.update({'.available': f"✅ ~~~~ AVAILABLE MOBILE APP DEVICES ~~~~"}) + self.mobapp_list_text_by_entity_id.update(sort_dict_by_values(devices_available)) + self.mobapp_list_text_by_entity_id.update({'.assigned': f"⛔ ~~~~ ASSIGNED MOBILE APP DEVICES ~~~~"}) + self.mobapp_list_text_by_entity_id.update(sort_dict_by_values(devices_used)) + self.mobapp_list_text_by_entity_id.update({'.scanfor': f"🔄 ~~~~ SCAN FOR DEVICE TRACKER ENTITY ~~~~"}) + self.mobapp_list_text_by_entity_id.update(sort_dict_by_values(scan_for_mobapp_devices)) - return device_info + return +#------------------------------------------------------------------------------------------- + @staticmethod + def _mobapp_fname(entity_attrs): + return entity_attrs['name'] or entity_attrs['original_name'] #------------------------------------------------------------------------------------------- - async def _build_picture_filename_list(self): + async def _build_picture_filename_selection_list(self): try: if self.picture_by_filename != {}: return - directory_list, start_dir, file_filter = [False, 'www', ['png', 'jpg', 'jpeg']] + start_dir = 'www' + file_filter = ['png', 'jpg', 'jpeg'] image_filenames = await Gb.hass.async_add_executor_job( - get_file_list, + file_io.get_directory_filename_list, start_dir, file_filter) - # image_filenames = await Gb.hass.async_add_executor_job( - # self.get_file_or_directory_list, - # directory_list, - # start_dir, - # file_filter) sorted_image_filenames = [] over_25_warning_msgs = [] @@ -3577,7 +3999,6 @@ async def _build_picture_filename_list(self): www_dir_idx += 1 self.picture_by_filename[f"www_dirs{www_dir_idx}"] = over_25_warning_msg - #self.picture_by_filename.extend(sorted_image_filenames) self.picture_by_filename['www_dirs998'] = "Set filter on `Tracking and Other Parameters` screen" self.picture_by_filename['www_dirs999'] = f"{'-'*85}" self.picture_by_filename.update(self.picture_by_filename_base) @@ -3589,98 +4010,113 @@ async def _build_picture_filename_list(self): except Exception as err: log_exception(err) + #------------------------------------------------------------------------------------------- - def x_build_picture_filename_list(self): + async def _build_zone_selection_list(self): - try: - if self.picture_by_filename != {}: - return + if self.zone_name_key_text != {}: + return - dir_filters = ['/.', 'deleted', '/x-'] - image_filenames = [] - path_config_base = f"{Gb.ha_config_directory}/" - back_slash = '\\' - www_dir_25_items = {} + fname_zones = [] + for zone, Zone in Gb.HAZones_by_zone.items(): + if is_statzone(zone): + continue - for path, dirs, files in os.walk(f"{path_config_base}www"): - www_sub_directory = path.replace(path_config_base, '') - in_filter_cnt = len([filter for filter in dir_filters if instr(www_sub_directory, filter)]) - if in_filter_cnt > 0 or www_sub_directory.count('/') > 4 or www_sub_directory.count(back_slash): - continue + passive_msg = ' (Passive)' if Zone.passive else '' + fname_zones.append(f"{Zone.dname}{passive_msg}|{zone}") - # Filter unwanted directories - std dirs are www/icloud3, www/cummunity, www/images - if Gb.picture_www_dirs: - valid_dir = [dir for dir in Gb.picture_www_dirs if www_sub_directory.startswith(dir)] - if valid_dir == []: - continue + fname_zones.sort() - dir_image_filenames = [f"{www_sub_directory}/{file}" - for file in files - if file.rsplit('.', 1)[-1] in ['png', 'jpg', 'jpeg']] + self.zone_name_key_text = {'home': 'Home'} - image_filenames.extend(dir_image_filenames[:25]) - if len(dir_image_filenames) > 25: - www_dir_25_items[f"www_dirs-{www_sub_directory}"] = \ - (f" ⛔ {www_sub_directory} > The first 25 files out of " - f"{len(dir_image_filenames)} are listed") + for fname_zone in fname_zones: + fname, zone = fname_zone.split('|') + self.zone_name_key_text[zone] = fname - sorted_image_filenames = [] - for image_filename in image_filenames: - sorted_image_filenames.append(f"{image_filename.rsplit('/', 1)[1]}:{image_filename}") - sorted_image_filenames.sort() +#---------------------------------------------------------------------- + async def _build_trusted_devices_list(self): + ''' + Build a list of the trusted devices for the Apple account + ''' + try: + return {} + _log(f"BUILD BEF TRUSTED DEVICES") + trusted_devices = await self.hass.async_add_executor_job( + getattr, self.PyiCloud, "trusted_devices") + # trusted_devices = trusted_devices_json.json() + _log(f"BUILD AFT TRUSTED DEVICES") + _log(f"{trusted_devices=}") - self.picture_by_filename = {} - self.picture_by_filename['www_dirs'] = "Source Directories:" - if Gb.picture_www_dirs: - www_dir_idx = 0 - while www_dir_idx < len(Gb.picture_www_dirs): - self.picture_by_filename[f"www_dirs{www_dir_idx}"] = \ - f"{DOT}{list_to_str(Gb.picture_www_dirs[www_dir_idx:www_dir_idx+3])}" - www_dir_idx += 3 - else: - self.picture_by_filename["www_dirs0"] = f"{DOT}All `www/*` directories are searched" + trusted_devices_key_text = {} + for i, device in enumerate(trusted_devices): + trusted_devices_key_text[i] = self.PyiCloud.device.get( + "deviceName", f"SMS to {device.get('phoneNumber')}") - self.picture_by_filename.update(www_dir_25_items) - self.picture_by_filename['www_dirs998'] = "Set filter on `Tracking and Other Parameters` screen" - self.picture_by_filename['www_dirs999'] = f"{'-'*85}" - self.picture_by_filename.update(self.picture_by_filename_base) + _log(f"{trusted_devices_key_text=}") - for sorted_image_filename in sorted_image_filenames: - image_filename, image_filename_path = sorted_image_filename.split(':') - self.picture_by_filename[image_filename_path] = \ - f"{image_filename}{RARROW}{image_filename_path.replace(image_filename, '')}" + return trusted_devices_key_text except Exception as err: log_exception(err) -#------------------------------------------------------------------------------------------- - async def _build_zone_list(self): + return {} - if self.zone_name_key_text != {}: +#---------------------------------------------------------------------- + def _check_finish_v2v3conversion_for_icloud_dname(self): + ''' + This will be done if the v2 files were just converted to the v3 configuration. + Finish setting up the device by determining the actual iCloud devicename and the + raw_model, model, model_display_name and device_id fields. + ''' + + if (Gb.conf_profile[CONF_VERSION] == 1 + or self.PyiCloud is None + or self.PyiCloud.DeviceSvc is None): return + icloud_dnames = [conf_device[CONF_FAMSHR_DEVICENAME] + for conf_device in Gb.conf_devices + if (conf_device[CONF_FAMSHR_DEVICENAME] == \ + conf_device[CONF_IC3_DEVICENAME] + and conf_device[CONF_TRACKING_MODE] == INACTIVE_DEVICE)] - fname_zones = [] - for zone, Zone in Gb.HAZones_by_zone.items(): - if is_statzone(zone): + if icloud_dnames == []: + return + + # Build a dictionary of the iCloud fnames to compare to the ic3_devicename {gary_iphone: Gary-iPhone} + icloud_dname_by_ic3_devicename = {slugify(fname).strip(): fname + for fname in self.PyiCloud.device_model_info_by_fname.keys()} + + # Cycle thru conf_devices and see if there are any ic3_devicename = icloud_dname entries. + # If so, they were just converted and the real icloud_dname needs to be reset to the actual + # value from the PyiCloud RawData fields + update_conf_file_flag = False + for conf_device in Gb.conf_devices: + icloud_dname = conf_device[CONF_FAMSHR_DEVICENAME] + ic3_devicename = conf_device[CONF_IC3_DEVICENAME] + + if (icloud_dname != ic3_devicename + or ic3_devicename not in icloud_dname_by_ic3_devicename): continue - passive_msg = ' (Passive)' if Zone.passive else '' - fname_zones.append(f"{Zone.dname}{passive_msg}|{zone}") + icloud_dname = icloud_dname_by_ic3_devicename[ic3_devicename] - fname_zones.sort() + conf_device[CONF_APPLE_ACCOUNT] = self.username + conf_device[CONF_FAMSHR_DEVICENAME] = icloud_dname - self.zone_name_key_text = {'home': 'Home'} + raw_model, model, model_display_name = \ + self.PyiCloud.device_model_info_by_fname[icloud_dname] - for fname_zone in fname_zones: - fname, zone = fname_zone.split('|') - self.zone_name_key_text[zone] = fname + device_id = self.PyiCloud.device_id_by_icloud_dname[icloud_dname] - self.zone_name_key_text = ensure_six_item_dict(self.zone_name_key_text) - dummy_key = '' - for i in range(6 - len(self.zone_name_key_text)): - dummy_key += '.' - self.zone_name_key_text[dummy_key] = '.' + conf_device[CONF_FAMSHR_DEVICE_ID] = device_id + conf_device[CONF_MODEL] = model + conf_device[CONF_MODEL_DISPLAY_NAME] = model_display_name + conf_device[CONF_RAW_MODEL] = raw_model + update_conf_file_flag = True + + if update_conf_file_flag: + self._update_config_file_tracking() #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -3709,7 +4145,7 @@ def _create_device_tracker_and_sensor_entities(self, devicename, conf_device): associated with the device. """ - if conf_device[CONF_TRACKING_MODE] == 'inactive': + if conf_device[CONF_TRACKING_MODE] == INACTIVE_DEVICE: return NewDeviceTrackers = [] @@ -3728,7 +4164,7 @@ def _create_device_tracker_and_sensor_entities(self, devicename, conf_device): Gb.async_add_entities_device_tracker(NewDeviceTrackers, True) - sensors_list = self._get_all_sensors_list() + sensors_list = self._build_all_sensors_list() self._create_sensor_entity(devicename, conf_device, sensors_list) #------------------------------------------------------------------------------------------- @@ -3769,7 +4205,7 @@ def _devices_form_identify_new_and_removed_tfz_zones(self, user_input): new_tfz_zones_list = [] remove_tfz_zones_list = [] # base device sensors - old_tfz_zones_list = self.conf_device_selected[CONF_TRACK_FROM_ZONES] + old_tfz_zones_list = self.conf_device[CONF_TRACK_FROM_ZONES] ui_tfz_zones_list = user_input[CONF_TRACK_FROM_ZONES] # Cycle thru the devices tfz zones before the update to get a list of new @@ -3794,7 +4230,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).copy() + device_tfz_sensors = Gb.Sensors_by_devicename_from_zone.get(devicename) if device_tfz_sensors is None: return @@ -3803,7 +4239,7 @@ def remove_track_fm_zone_sensor_entity(self, devicename, remove_tfz_zones_list): # through the Device's sensor list and remove all track_from_zone sensors ending with # that zone. for zone in remove_tfz_zones_list: - for sensor, Sensor in device_tfz_sensors.items(): + for sensor, Sensor in device_tfz_sensors.copy().items(): if (sensor.endswith(f"_{zone}") and Sensor.entity_removed_flag is False): Sensor.remove_entity() @@ -3824,7 +4260,7 @@ def _create_track_fm_zone_sensor_entity(self, devicename, new_tfz_zones_list): for sensor in Gb.conf_sensors[CONF_TRACK_FROM_ZONES]: sensors_list.append(sensor) - NewZones = ic3_sensor.create_tracked_device_sensors(devicename, self.conf_device_selected, sensors_list) + NewZones = ic3_sensor.create_tracked_device_sensors(devicename, self.conf_device, sensors_list) if NewZones is not []: Gb.async_add_entities_sensor(NewZones, True) @@ -3843,7 +4279,7 @@ def _sensor_form_identify_new_and_removed_sensors(self, user_input): or user_input[sensor_group] == Gb.conf_sensors[sensor_group] or sensor_group == CONF_EXCLUDED_SENSORS): if user_input[CONF_EXCLUDED_SENSORS] != Gb.conf_sensors[CONF_EXCLUDED_SENSORS]: - self.config_flow_updated_parms.update(['restart_ha', 'restart']) + list_add(self.config_parms_update_control, ['restart_ha', 'restart']) continue # Cycle thru the sensors now in the user_input sensor_group @@ -3949,7 +4385,7 @@ def _create_sensor_entity(self, devicename, conf_device, new_sensors_list): Gb.async_add_entities_sensor(NewSensors, True) #------------------------------------------------------------------------------------------- - def _get_all_sensors_list(self): + def _build_all_sensors_list(self): """ Get a list of all sensors from the ic3 config file """ sensors_list = [] @@ -3978,7 +4414,7 @@ def update_area_id_personal_device(self, devicename): dr_entry = device_registry.async_update_device(ha_device_id, **kwargs) log_debug_msg( "Device Tracker entity changed: device_tracker.icloud3, " - "iCloud3, Personal Device") + "iCloud3, Personal Device") except: pass @@ -4055,8 +4491,8 @@ def _action_text_to_item(self, user_input): if action_text.startswith('NEXT PAGE ITEMS'): action_item = 'next_page_items' else: - action_text_len = 25 if len(action_text) > 25 else len(action_text) - action_item = [k for k, v in ACTION_LIST_ITEMS_KEY_TEXT.items() + action_text_len = len(action_text) + action_item = [k for k, v in ACTION_LIST_OPTIONS.items() if v.startswith(action_text[:action_text_len])][0] if 'action_items' in user_input: user_input.pop('action_items') @@ -4070,7 +4506,7 @@ def _action_text_to_item(self, user_input): return user_input, action_item #------------------------------------------------------------------------------------------- - def _parm_or_error_msg(self, pname, conf_group=CF_DATA_GENERAL, conf_dict_variable=None): + def _parm_or_error_msg(self, pname, conf_group=CF_GENERAL, conf_dict_variable=None): ''' Determine the value that should be displayed in the config_flow parameter entry screen based on whether it was entered incorrectly and has an error message. @@ -4089,7 +4525,7 @@ def _parm_or_error_msg(self, pname, conf_group=CF_DATA_GENERAL, conf_dict_variab # pname is in the 'Tracking' data fields # Example: [data][general][tracking][username] # Example: [data][general][tracking][devices] - elif conf_group == CF_DATA_TRACKING: + elif conf_group == CF_TRACKING: return self.errors_user_input.get(pname) or Gb.conf_tracking[pname] # pname is in a dictionary variable in the 'General Data' data fields grupo. It is a dictionary variable. @@ -4113,8 +4549,8 @@ def _parm_or_device(self, pname, suggested_value=''): ''' try: parm_displayed = self.errors_user_input.get(pname) \ - or self.user_input_multi_form.get(pname) \ - or self.conf_device_selected.get(pname) \ + or self.multi_form_user_input.get(pname) \ + or self.conf_device.get(pname) \ or suggested_value if pname == 'device_type': @@ -4155,14 +4591,14 @@ def _option_parm_to_text(self, pname, option_list_key_text, conf_device=False): elif pname in Gb.conf_tracking: pvalue_key = Gb.conf_tracking[pname] - elif pname in Gb.conf_general and pname in self.conf_device_selected: + elif pname in Gb.conf_general and pname in self.conf_device: if conf_device: - pvalue_key = self.conf_device_selected[pname] + pvalue_key = self.conf_device[pname] else: pvalue_key = Gb.conf_general[pname] - elif pname in self.conf_device_selected: - pvalue_key = self.conf_device_selected[pname] + elif pname in self.conf_device: + pvalue_key = self.conf_device[pname] else: pvalue_key = Gb.conf_general[pname] @@ -4175,12 +4611,14 @@ def _option_parm_to_text(self, pname, option_list_key_text, conf_device=False): return option_list_key_text.values()[0] - except Exception as err: + log_exception(err) # If the parameter value is already the key to the items dict, it is ok. if pvalue_key not in option_list_key_text: - if pname in [CONF_FAMSHR_DEVICENAME, CONF_FMF_EMAIL, CONF_MOBILE_APP_DEVICE]: - self.errors[pname] = 'unknown_devicename' + if pname == CONF_FAMSHR_DEVICENAME: + self.errors[pname] = 'unknown_icloud' + elif pname == CONF_MOBILE_APP_DEVICE: + self.errors[pname] = 'unknown_mobapp' else: self.errors[pname] = 'unknown_value' @@ -4200,6 +4638,8 @@ def _option_text_to_parm(self, user_input, pname, option_list_key_text): pvalue_text = '_' if user_input is None: return None + if pname not in user_input: + return user_input pvalue_text = user_input[pname] @@ -4345,11 +4785,11 @@ def _extract_name_device_type(self, devicename): return (fname, device_type) #-------------------------------------------------------------------- - def action_default_text(self, action_item, action_items_key_text=None): - if action_items_key_text: - return action_items_key_text.get(action_item, 'UNKNOWN ACTION > Unknown Action') + def action_default_text(self, action_item, action_OPTIONS=None): + if action_OPTIONS: + return action_OPTIONS.get(action_item, 'UNKNOWN ACTION > Unknown Action') else: - return ACTION_LIST_ITEMS_KEY_TEXT.get(action_item, 'UNKNOWN ACTION - Unknown Action') + return ACTION_LIST_OPTIONS.get(action_item, 'UNKNOWN ACTION - Unknown Action') #-------------------------------------------------------------------- def _discard_changes(self, user_input): @@ -4362,6 +4802,12 @@ def _discard_changes(self, user_input): else: return False +#-------------------------------------------------------------------- + def log_step_info(self, user_input, action_item=None): + + log_info_msg( f"{self.step_id.upper()} ({action_item}) > " + f"UserInput-{user_input}, Errors-{self.errors}") + #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # # FORM SCHEMA DEFINITIONS @@ -4377,849 +4823,12 @@ def form_schema(self, step_id, actions_list=None, actions_list_default=None): self.actions_list = actions_list or ACTION_LIST_ITEMS_BASE.copy() if step_id in ['menu', 'menu_0', 'menu_1']: - menu_title = MENU_PAGE_TITLE[self.menu_page_no] - menu_action_items = MENU_ACTION_ITEMS.copy() - - if self.menu_page_no == 0: - menu_key_text = MENU_KEY_TEXT_PAGE_0 - menu_action_items[1] = MENU_KEY_TEXT['next_page_1'] - - - if (self.username == '' or self.password == ''): - self.menu_item_selected[0] = MENU_KEY_TEXT['icloud_account'] - elif (self.username and self.password - and (self._device_cnt() == 0 or self._device_cnt() == self._inactive_device_cnt())): - self.menu_item_selected[0] = MENU_KEY_TEXT['device_list'] - else: - menu_key_text = MENU_KEY_TEXT_PAGE_1 - menu_action_items[1] = MENU_KEY_TEXT['next_page_0'] - - - return vol.Schema({ - vol.Required("menu_items", - default=self.menu_item_selected[self.menu_page_no]): - selector.SelectSelector(selector.SelectSelectorConfig( - options=menu_key_text, mode='list')), - vol.Required("action_items", - default=menu_action_items[0]): - selector.SelectSelector(selector.SelectSelectorConfig( - options=menu_action_items, mode='list')), - }) - - #------------------------------------------------------------------------ - elif step_id.startswith('confirm_action'): - actions_list_default = actions_list_default or self.actions_list[0] - - return vol.Schema({ - vol.Required('action_items', - default=actions_list_default): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - - #------------------------------------------------------------------------ - elif step_id == 'restart_icloud3': - self.actions_list = [] - restart_default='restart_ic3_now' - - if 'restart_ha' in self.config_flow_updated_parms: - restart_default='restart_ha' - self.actions_list.append(ACTION_LIST_ITEMS_KEY_TEXT['restart_ha']) - - self.actions_list.append(ACTION_LIST_ITEMS_KEY_TEXT['restart_ic3_now']) - self.actions_list.append(ACTION_LIST_ITEMS_KEY_TEXT['restart_ic3_later']) - - actions_list_default = self.action_default_text(restart_default) - if self._inactive_device_cnt() > 0: - inactive_devices = [conf_device[CONF_IC3_DEVICENAME] - for conf_device in Gb.conf_devices - if conf_device[CONF_TRACKING_MODE] == INACTIVE_DEVICE] - inactive_devices_list = ( - f"{ACTION_LIST_ITEMS_KEY_TEXT['review_inactive_devices']} " - f"({list_to_str(inactive_devices)})") - self.actions_list.append(inactive_devices_list) - if self._set_inactive_devices_header_msg() in ['all', 'most']: - actions_list_default = inactive_devices_list - - self.actions_list.append(ACTION_LIST_ITEMS_KEY_TEXT['cancel']) - - return vol.Schema({ - vol.Required('action_items', - default=actions_list_default): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - - #------------------------------------------------------------------------ - elif step_id == 'review_inactive_devices': - self.actions_list = REVIEW_INACTIVE_DEVICES.copy() - - self.inactive_devices_key_text = {conf_device[CONF_IC3_DEVICENAME]: self._format_device_info(conf_device) - for conf_device in Gb.conf_devices - if conf_device[CONF_TRACKING_MODE] == INACTIVE_DEVICE} - - return vol.Schema({ - vol.Required('inactive_devices', - default=[]): - cv.multi_select(self.inactive_devices_key_text), - - vol.Required('action_items', - default=self.action_default_text('inactive_keep_inactive')): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - - #------------------------------------------------------------------------ - elif step_id == 'icloud_account': - self.actions_list = ICLOUD_ACCOUNT_ACTIONS.copy() - self.actions_list.extend(ACTION_LIST_ITEMS_BASE) - self._set_action_list_item_username_password() - - data_source_icloud_list = [] - data_source_mobapp_list = [] - 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' - - url_suffix_china = (Gb.icloud_server_endpoint_suffix == 'cn') - - return vol.Schema({ - vol.Optional('data_source_icloud', - default=data_source_icloud_list): - cv.multi_select(DATA_SOURCE_ICLOUD_ITEMS_KEY_TEXT), - vol.Optional(CONF_USERNAME, - default=self.username): - selector.TextSelector(selector.TextSelectorConfig(type='password')), - vol.Optional(CONF_PASSWORD, - default=self.password): - selector.TextSelector(selector.TextSelectorConfig(type='password')), - vol.Optional('url_suffix_china', - default=url_suffix_china): - selector.BooleanSelector(), - vol.Optional('data_source_mobapp', - default=data_source_mobapp_list): - cv.multi_select(DATA_SOURCE_MOBAPP_ITEMS_KEY_TEXT), - - vol.Required('action_items', - default=self.action_default_text(default_action)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - - #------------------------------------------------------------------------ + return form_menu(self) + elif step_id == 'data_source': + return form_data_source(self) + elif step_id == 'update_apple_acct': + return form_update_apple_acct(self) elif step_id == 'reauth': - self.actions_list = REAUTH_ACTIONS.copy() - # self._set_action_list_item_username_password() - return vol.Schema({ - vol.Optional(CONF_VERIFICATION_CODE, default=' '): - selector.TextSelector(), - vol.Required('action_items', - default=self.action_default_text('send_verification_code')): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - - #------------------------------------------------------------------------ - elif step_id == 'device_list': - - action_default = 'add_device' if Gb.conf_devices == [] else 'update_device' - - idx = self.device_list_page_selected_idx[self.device_list_page_no] - if len(self.form_devices_list_all) > 0: - device_list_default = self.form_devices_list_all[idx] - - if Gb.conf_devices == []: - self.actions_list = DEVICE_LIST_ACTIONS_ADD.copy() - - elif len(self.form_devices_list_all) <= 5: - self.actions_list = DEVICE_LIST_ACTIONS.copy() - - else: - devices_text = f"iCloud3 Devices: {self.next_page_devices_list}" - next_page_text = ACTION_LIST_ITEMS_KEY_TEXT['next_page_items'] - next_page_text = next_page_text.replace('^info_field^', devices_text) - self.actions_list = [next_page_text] - self.actions_list.extend(DEVICE_LIST_ACTIONS) - - schema = {} - schema = vol.Schema({}) - if self.form_devices_list_displayed != []: - schema = schema.extend({ - vol.Required('devices', - default=device_list_default): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.form_devices_list_displayed)), - }) - schema = schema.extend({ - vol.Required('action_items', - default=self.action_default_text(action_default)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - return schema - - #------------------------------------------------------------------------ - elif step_id == 'add_device': - - return vol.Schema({ - vol.Required(CONF_IC3_DEVICENAME, - default=self._parm_or_device(CONF_IC3_DEVICENAME)): - selector.TextSelector(), - vol.Required(CONF_FNAME, - default=self._parm_or_device(CONF_FNAME)): - selector.TextSelector(), - vol.Required(CONF_DEVICE_TYPE, - default=self._parm_or_device(CONF_DEVICE_TYPE, suggested_value=IPHONE)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(DEVICE_TYPE_FNAME), mode='dropdown')), - vol.Required(CONF_TRACKING_MODE, - default=self._option_parm_to_text(CONF_TRACKING_MODE, TRACKING_MODE_ITEMS_KEY_TEXT)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(TRACKING_MODE_ITEMS_KEY_TEXT), mode='dropdown')), - vol.Required('mobapp', - default=True): - #cv.boolean, - selector.BooleanSelector(), - }) - - #------------------------------------------------------------------------ - elif step_id == 'update_device': - # self._build_picture_filename_list() - # self._build_devicename_by_famshr_fmf(self.conf_device_selected[CONF_IC3_DEVICENAME]) - error_key = '' - self.errors = self.errors or {} - - # If conf_famshr_devicename is not in available famshr values list, add it - famshr_devicename = self.conf_device_selected[CONF_FAMSHR_DEVICENAME] - famshr_list_text_by_fname = self.famshr_list_text_by_fname.copy() - if famshr_devicename not in self.famshr_list_text_by_fname: - error_key = '_famshr' - self.errors[CONF_FAMSHR_DEVICENAME] = 'unknown_famshr' - famshr_list_text_by_fname[famshr_devicename] = f"{famshr_devicename}{UNKNOWN_DEVICE_TEXT}" - - if self.PyiCloud: - try: - if self.PyiCloud.FamilySharing.is_service_not_available: - famshr_list_text_by_fname[famshr_devicename] = f"{famshr_devicename}{DATA_ENTRY_ALERT}{SERVICE_NOT_AVAILABLE}" - except: - famshr_list_text_by_fname[famshr_devicename] = f"{famshr_devicename}{DATA_ENTRY_ALERT}{SERVICE_NOT_STARTED_YET}" - elif 'base' not in self.errors: - self.errors['base'] = 'icloud_acct_not_available' - - # If conf_fmf_email is not in available fmf emails list, add it - # fmf_email = self.conf_device_selected[CONF_FMF_EMAIL] - # fmf_list_text_by_email = self.fmf_list_text_by_email.copy() - # if fmf_email not in self.fmf_list_text_by_email: - # error_key = f"{error_key}_fmf" - # self.errors[CONF_FMF_EMAIL] = 'unknown_fmf' - # fmf_list_text_by_email[fmf_email] = f"{fmf_email}{UNKNOWN_DEVICE_TEXT}" - - if self.PyiCloud: - pass - # try: - # if self.PyiCloud.FindMyFriends.is_service_not_available: - # fmf_list_text_by_email[fmf_email] = f"{fmf_email}{DATA_ENTRY_ALERT}{SERVICE_NOT_AVAILABLE}" - # except: - # fmf_list_text_by_email[fmf_email] = f"{fmf_email}{DATA_ENTRY_ALERT}{SERVICE_NOT_STARTED_YET}" - elif 'base' not in self.errors: - self.errors['base'] = 'icloud_acct_not_available' - - # If conf_mobapp_device is not in available mobapp devices list, add it - mobapp_device = self.conf_device_selected[CONF_MOBILE_APP_DEVICE] - mobapp_list_text_by_entity_id = self.mobapp_list_text_by_entity_id.copy() - if mobapp_device not in mobapp_list_text_by_entity_id: - error_key = f"{error_key}_mobapp" - self.errors[CONF_MOBILE_APP_DEVICE] = 'unknown_mobapp' - mobapp_list_text_by_entity_id[mobapp_device] = f"{mobapp_device}{UNKNOWN_DEVICE_TEXT}" - - picture_filename = self.conf_device_selected[CONF_PICTURE] - picture_by_filename = self.picture_by_filename.copy() - - if picture_filename not in picture_by_filename: - error_key = f"{error_key}_picture" - self.errors[CONF_PICTURE] = 'unknown_picture' - picture_by_filename[picture_filename] = f"{picture_filename}{UNKNOWN_DEVICE_TEXT}" - if error_key and 'base' not in self.errors: - self.errors['base'] = f'unknown{error_key}' - - if self.conf_device_selected[CONF_TRACKING_MODE] == INACTIVE_DEVICE: - self.errors[CONF_TRACKING_MODE] = 'inactive_device' - - log_zones_key_text = {'none': 'None'} - log_zones_key_text.update(self.zone_name_key_text) - log_zones_key_text.update(LOG_ZONES_KEY_TEXT) - - return vol.Schema({ - vol.Required(CONF_IC3_DEVICENAME, - default=self._parm_or_device(CONF_IC3_DEVICENAME)): - selector.TextSelector(), - vol.Required(CONF_FNAME, - default=self._parm_or_device(CONF_FNAME)): - selector.TextSelector(), - vol.Required(CONF_DEVICE_TYPE, - default=self._option_parm_to_text(CONF_DEVICE_TYPE, DEVICE_TYPE_FNAME)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(DEVICE_TYPE_FNAME), mode='dropdown')), - vol.Required(CONF_TRACKING_MODE, - default=self._option_parm_to_text(CONF_TRACKING_MODE, TRACKING_MODE_ITEMS_KEY_TEXT)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(TRACKING_MODE_ITEMS_KEY_TEXT), mode='dropdown')), - vol.Required(CONF_FAMSHR_DEVICENAME, - default=self._option_parm_to_text(CONF_FAMSHR_DEVICENAME, famshr_list_text_by_fname)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(famshr_list_text_by_fname), mode='dropdown')), - # vol.Required(CONF_FMF_EMAIL, - # default=self._option_parm_to_text(CONF_FMF_EMAIL, fmf_list_text_by_email)): - # selector.SelectSelector(selector.SelectSelectorConfig( - # options=dict_value_to_list(fmf_list_text_by_email), mode='dropdown')), - vol.Required(CONF_MOBILE_APP_DEVICE, - default=self._option_parm_to_text(CONF_MOBILE_APP_DEVICE, mobapp_list_text_by_entity_id)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(mobapp_list_text_by_entity_id), mode='dropdown')), - vol.Required(CONF_PICTURE, - default=self._option_parm_to_text(CONF_PICTURE, picture_by_filename)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(picture_by_filename), mode='dropdown')), - - vol.Optional(CONF_LOG_ZONES, - default=self._parm_or_device(CONF_LOG_ZONES)): - cv.multi_select(log_zones_key_text), - vol.Required(CONF_TRACK_FROM_ZONES, - default=self._parm_or_device(CONF_TRACK_FROM_ZONES)): - cv.multi_select(self.zone_name_key_text), - - vol.Required(CONF_INZONE_INTERVAL, - default=self.conf_device_selected[CONF_INZONE_INTERVAL]): - # default=self._parm_or_device(CONF_INZONE_INTERVAL)): - selector.NumberSelector(selector.NumberSelectorConfig( - min=5, max=480, step=5, unit_of_measurement='minutes')), - vol.Required(CONF_FIXED_INTERVAL, - default=self.conf_device_selected[CONF_FIXED_INTERVAL]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=0, max=480, step=5, unit_of_measurement='minutes')), - vol.Required(CONF_TRACK_FROM_BASE_ZONE, - default=self._option_parm_to_text(CONF_TRACK_FROM_BASE_ZONE, self.zone_name_key_text, conf_device=True)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(self.zone_name_key_text), mode='dropdown')), - - vol.Required('action_items', - default=self.action_default_text('save')): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - - #------------------------------------------------------------------------ - elif step_id == 'delete_device': - self.actions_list = DELETE_DEVICE_ACTIONS.copy() - device_info = ( f"{self.conf_device_selected[CONF_IC3_DEVICENAME]}, " - f"{self.conf_device_selected[CONF_FNAME]}") - - # The first item is 'Delete this device, add the selected device's info - self.actions_list[0] = f"{self.actions_list[0]}{device_info}" - - return vol.Schema({ - vol.Required('action_items', - default=self.action_default_text('delete_device_cancel')): - selector.SelectSelector( - selector.SelectSelectorConfig(options=self.actions_list, mode='list')), - }) - - #------------------------------------------------------------------------ - elif step_id == 'actions': - debug_items_key_text = ACTIONS_DEBUG_ITEMS.copy() - if Gb.log_debug_flag: - debug_items_key_text.pop('debug_start') - else: - debug_items_key_text.pop('debug_stop') - if Gb.log_rawdata_flag: - debug_items_key_text.pop('rawdata_start') - else: - debug_items_key_text.pop('rawdata_stop') - - return vol.Schema({ - vol.Optional('ic3_actions', default=[]): - cv.multi_select(ACTIONS_IC3_ITEMS), - # selector.SelectSelector(selector.SelectSelectorConfig( - # options=dict_value_to_list(ACTIONS_IC3_ITEMS), mode='list')), - vol.Optional('debug_actions', default=[]): - cv.multi_select(debug_items_key_text), - # selector.SelectSelector(selector.SelectSelectorConfig( - # options=dict_value_to_list(debug_items_key_text), mode='list')), - vol.Optional('other_actions', default=[]): - cv.multi_select(ACTIONS_OTHER_ITEMS), - # selector.SelectSelector(selector.SelectSelectorConfig( - # options=dict_value_to_list(ACTIONS_OTHER_ITEMS), mode='list')), - vol.Optional('action_items', default=[]): - cv.multi_select(ACTIONS_ACTION_ITEMS), - # selector.SelectSelector(selector.SelectSelectorConfig( - # options=dict_value_to_list(ACTIONS_ACTION_ITEMS), mode='list')), - }) - #------------------------------------------------------------------------ - elif step_id == 'format_settings': - self._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( - options=dict_value_to_list(LOG_LEVEL_ITEMS_KEY_TEXT), mode='dropdown')), - vol.Required(CONF_DISPLAY_ZONE_FORMAT, - default=self._option_parm_to_text(CONF_DISPLAY_ZONE_FORMAT, DISPLAY_ZONE_FORMAT_ITEMS_KEY_TEXT)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(DISPLAY_ZONE_FORMAT_ITEMS_KEY_TEXT), mode='dropdown')), - vol.Required(CONF_DEVICE_TRACKER_STATE_SOURCE, - default=self._option_parm_to_text(CONF_DEVICE_TRACKER_STATE_SOURCE, DEVICE_TRACKER_STATE_SOURCE_ITEMS_KEY_TEXT)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(DEVICE_TRACKER_STATE_SOURCE_ITEMS_KEY_TEXT), mode='dropdown')), - vol.Required(CONF_UNIT_OF_MEASUREMENT, - default=self._option_parm_to_text(CONF_UNIT_OF_MEASUREMENT, UNIT_OF_MEASUREMENT_ITEMS_KEY_TEXT)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list( UNIT_OF_MEASUREMENT_ITEMS_KEY_TEXT), mode='dropdown')), - vol.Required(CONF_TIME_FORMAT, - default=self._option_parm_to_text(CONF_TIME_FORMAT, TIME_FORMAT_ITEMS_KEY_TEXT)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(TIME_FORMAT_ITEMS_KEY_TEXT), mode='dropdown')), - vol.Required(CONF_DISPLAY_GPS_LAT_LONG, - default=Gb.conf_general[CONF_DISPLAY_GPS_LAT_LONG]): - # cv.boolean, - selector.BooleanSelector(), - - vol.Required('action_items', - default=self.action_default_text('save')): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - - #------------------------------------------------------------------------ - elif step_id == 'change_device_order': - self.actions_list = [ - ACTION_LIST_ITEMS_KEY_TEXT['move_up'], - ACTION_LIST_ITEMS_KEY_TEXT['move_down']] - self.actions_list.extend(ACTION_LIST_ITEMS_BASE) - - return vol.Schema({ - vol.Required('device_desc', - default=self.cdo_devicenames[self.cdo_curr_idx]): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.cdo_devicenames, mode='list')), - vol.Required('action_items', - default=self.action_default_text(self.actions_list_default)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - - #------------------------------------------------------------------------ - elif step_id == 'away_time_zone': - self.actions_list = [] - self.actions_list.extend(ACTION_LIST_ITEMS_BASE) - - return vol.Schema({ - vol.Required(CONF_AWAY_TIME_ZONE_1_DEVICES, - default=Gb.conf_general[CONF_AWAY_TIME_ZONE_1_DEVICES]): - cv.multi_select(self.away_time_zone_devices_key_text), - vol.Required(CONF_AWAY_TIME_ZONE_1_OFFSET, - default=self.away_time_zone_hours_key_text[Gb.conf_general[CONF_AWAY_TIME_ZONE_1_OFFSET]]): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(self.away_time_zone_hours_key_text), mode='dropdown')), - - vol.Required(CONF_AWAY_TIME_ZONE_2_DEVICES, - default=Gb.conf_general[CONF_AWAY_TIME_ZONE_2_DEVICES]): - cv.multi_select(self.away_time_zone_devices_key_text), - vol.Required(CONF_AWAY_TIME_ZONE_2_OFFSET, - default=self.away_time_zone_hours_key_text[Gb.conf_general[CONF_AWAY_TIME_ZONE_2_OFFSET]]): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(self.away_time_zone_hours_key_text), mode='dropdown')), - - vol.Required('action_items', - default=self.action_default_text('save')): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - - #------------------------------------------------------------------------ - elif step_id == 'display_text_as': - self.dta_selected_idx = self.dta_selected_idx_page[self.dta_page_no] - if self.dta_selected_idx <= 4: - dta_page_display_list = [v for k,v in self.dta_working_copy.items() - if k <= 4] - dta_next_page_display_list = [v.split('>')[0] for k,v in self.dta_working_copy.items() - if k >= 5] - else: - dta_page_display_list = [v for k,v in self.dta_working_copy.items() - if k >= 5] - dta_next_page_display_list = [v.split('>')[0] for k,v in self.dta_working_copy.items() - if k <= 4] - - dta_next_page_display_items = ", ".join(dta_next_page_display_list) - next_page_text = ACTION_LIST_ITEMS_KEY_TEXT['next_page_items'] - next_page_text = next_page_text.replace('^info_field^', dta_next_page_display_items) - self.actions_list = [next_page_text] - self.actions_list.extend([ACTION_LIST_ITEMS_KEY_TEXT['select_text_as']]) - self.actions_list.extend(ACTION_LIST_ITEMS_BASE) - - return vol.Schema({ - vol.Required(CONF_DISPLAY_TEXT_AS, - default=self.dta_working_copy[self.dta_selected_idx]): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dta_page_display_list)), - vol.Required('action_items', - default=self.action_default_text('select_text_as')): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - - #------------------------------------------------------------------------ - elif step_id == 'display_text_as_update': - self.actions_list = [ACTION_LIST_ITEMS_KEY_TEXT['clear_text_as']] - self.actions_list.extend(ACTION_LIST_ITEMS_BASE) - - if instr(self.dta_working_copy[self.dta_selected_idx], '>'): - text_from_to_parts = self.dta_working_copy[self.dta_selected_idx].split('>') - text_from = text_from_to_parts[0].strip() - text_to = text_from_to_parts[1].strip() - else: - text_from = '' - text_to = '' - - return vol.Schema({ - vol.Optional('text_from', - default=text_from): - selector.TextSelector(), - vol.Optional('text_to' , - default=text_to): - selector.TextSelector(), - vol.Required('action_items', - default=self.action_default_text('save')): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - - #------------------------------------------------------------------------ - elif step_id == 'tracking_parameters': - self.actions_list = ACTION_LIST_ITEMS_BASE.copy() - - self.picture_by_filename = {} - if PICTURE_WWW_STANDARD_DIRS in Gb.conf_profile[CONF_PICTURE_WWW_DIRS]: - Gb.conf_profile[CONF_PICTURE_WWW_DIRS] = [] - - return vol.Schema({ - vol.Required(CONF_DISTANCE_BETWEEN_DEVICES, - default=Gb.conf_general[CONF_DISTANCE_BETWEEN_DEVICES]): - selector.BooleanSelector(), - vol.Required(CONF_GPS_ACCURACY_THRESHOLD, - default=Gb.conf_general[CONF_GPS_ACCURACY_THRESHOLD]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=5, max=300, step=5, unit_of_measurement='m')), - vol.Required(CONF_OLD_LOCATION_THRESHOLD, - default=Gb.conf_general[CONF_OLD_LOCATION_THRESHOLD]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=1, max=60, step=1, unit_of_measurement='minutes')), - vol.Required(CONF_OLD_LOCATION_ADJUSTMENT, - default=Gb.conf_general[CONF_OLD_LOCATION_ADJUSTMENT]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=0, max=60, step=1, unit_of_measurement='minutes')), - vol.Required(CONF_MAX_INTERVAL, - default=Gb.conf_general[CONF_MAX_INTERVAL]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=15, max=480, step=5, unit_of_measurement='minutes')), - vol.Required(CONF_EXIT_ZONE_INTERVAL, - default=Gb.conf_general[CONF_EXIT_ZONE_INTERVAL]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=.5, max=10, step=.5, unit_of_measurement='minutes')), - vol.Required(CONF_MOBAPP_ALIVE_INTERVAL, - default=Gb.conf_general[CONF_MOBAPP_ALIVE_INTERVAL]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=15, max=240, step=5, unit_of_measurement='minutes')), - vol.Required(CONF_OFFLINE_INTERVAL, - default=Gb.conf_general[CONF_OFFLINE_INTERVAL]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=5, max=240, step=5, unit_of_measurement='minutes')), - vol.Required(CONF_TFZ_TRACKING_MAX_DISTANCE, - default=Gb.conf_general[CONF_TFZ_TRACKING_MAX_DISTANCE]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=1, max=100, unit_of_measurement='Km')), - vol.Optional(CONF_DISCARD_POOR_GPS_INZONE, - default=Gb.conf_general[CONF_DISCARD_POOR_GPS_INZONE]): - selector.BooleanSelector(), - vol.Required(CONF_TRAVEL_TIME_FACTOR, - default=self._option_parm_to_text(CONF_TRAVEL_TIME_FACTOR, TRAVEL_TIME_INTERVAL_MULTIPLIER_KEY_TEXT)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(TRAVEL_TIME_INTERVAL_MULTIPLIER_KEY_TEXT), mode='dropdown')), - vol.Required(CONF_PICTURE_WWW_DIRS, - default=Gb.conf_profile[CONF_PICTURE_WWW_DIRS] or self.www_directory_list): - cv.multi_select(self.www_directory_list), - vol.Required(CONF_EVLOG_CARD_DIRECTORY, - default=self._parm_or_error_msg(CONF_EVLOG_CARD_DIRECTORY, conf_group=CF_PROFILE)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(self.www_directory_list), mode='dropdown')), - vol.Optional(CONF_EVLOG_BTNCONFIG_URL, - default=f"{self._parm_or_error_msg(CONF_EVLOG_BTNCONFIG_URL, conf_group=CF_PROFILE)} "): - selector.TextSelector(), - - vol.Required('action_items', - default=self.action_default_text('save')): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - - #------------------------------------------------------------------------ - elif step_id == 'inzone_intervals': - return vol.Schema({ - vol.Optional(IPHONE, - default=Gb.conf_general[CONF_INZONE_INTERVALS][IPHONE]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=5, max=480, step=5, unit_of_measurement='minutes')), - vol.Optional(IPAD, - default=Gb.conf_general[CONF_INZONE_INTERVALS][IPAD]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=5, max=480, step=5, unit_of_measurement='minutes')), - vol.Optional(WATCH, - default=Gb.conf_general[CONF_INZONE_INTERVALS][WATCH]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=5, max=480, step=5, unit_of_measurement='minutes')), - vol.Optional(AIRPODS, - default=Gb.conf_general[CONF_INZONE_INTERVALS][AIRPODS]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=5, max=480, step=5, unit_of_measurement='minutes')), - vol.Optional(NO_MOBAPP, - default=Gb.conf_general[CONF_INZONE_INTERVALS][NO_MOBAPP]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=5, max=480, step=5, unit_of_measurement='minutes')), - vol.Optional(OTHER, - default=Gb.conf_general[CONF_INZONE_INTERVALS][OTHER]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=5, max=480, step=5, unit_of_measurement='minutes')), - - vol.Optional('action_items', - default=self.action_default_text('save')): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - - #------------------------------------------------------------------------ - elif step_id == 'waze_main': - self.actions_list = ACTION_LIST_ITEMS_BASE.copy() - - wuh_default = [WAZE_USED_HEADER] if Gb.conf_general[CONF_WAZE_USED] else [] - whuh_default = [WAZE_HISTORY_USED_HEADER] if Gb.conf_general[CONF_WAZE_HISTORY_DATABASE_USED] else [] - return vol.Schema({ - vol.Optional(CONF_WAZE_USED, - default=wuh_default): - cv.multi_select([WAZE_USED_HEADER]), - vol.Optional(CONF_WAZE_SERVER, - default=self._option_parm_to_text(CONF_WAZE_SERVER, WAZE_SERVER_ITEMS_KEY_TEXT)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(WAZE_SERVER_ITEMS_KEY_TEXT), mode='dropdown')), - vol.Optional(CONF_WAZE_MIN_DISTANCE, - default=Gb.conf_general[CONF_WAZE_MIN_DISTANCE]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=0, max=100, step=5, unit_of_measurement='km')), - vol.Optional(CONF_WAZE_MAX_DISTANCE, - default=Gb.conf_general[CONF_WAZE_MAX_DISTANCE]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=0, max=1000, step=5, unit_of_measurement='km')), - vol.Optional(CONF_WAZE_REALTIME, - default=Gb.conf_general[CONF_WAZE_REALTIME]): - selector.BooleanSelector(), - - vol.Required(CONF_WAZE_HISTORY_DATABASE_USED, - default=whuh_default): - cv.multi_select([WAZE_HISTORY_USED_HEADER]), - vol.Required(CONF_WAZE_HISTORY_MAX_DISTANCE, - default=Gb.conf_general[CONF_WAZE_HISTORY_MAX_DISTANCE]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=0, max=1000, step=5, unit_of_measurement='km')), - vol.Required(CONF_WAZE_HISTORY_TRACK_DIRECTION, - default=self._option_parm_to_text(CONF_WAZE_HISTORY_TRACK_DIRECTION, - WAZE_HISTORY_TRACK_DIRECTION_ITEMS_KEY_TEXT)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(WAZE_HISTORY_TRACK_DIRECTION_ITEMS_KEY_TEXT), mode='dropdown')), - - vol.Required('action_items', - default=self.action_default_text('save')): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - - #------------------------------------------------------------------------ - elif step_id == 'special_zones': - try: - pass_thru_zone_used = (Gb.conf_general[CONF_PASSTHRU_ZONE_TIME] > 0) - stat_zone_used = (Gb.conf_general[CONF_STAT_ZONE_STILL_TIME] > 0) - track_from_base_zone_used = Gb.conf_general[CONF_TRACK_FROM_BASE_ZONE_USED] - - ptzh_default = [PASSTHRU_ZONE_HEADER] if pass_thru_zone_used else [] - szh_default = [STAT_ZONE_HEADER] if stat_zone_used else [] - tfzh_default = [TRK_FROM_HOME_ZONE_HEADER] if track_from_base_zone_used else [] - - return vol.Schema({ - vol.Required('stat_zone_header', - default=szh_default): - cv.multi_select([STAT_ZONE_HEADER]), - vol.Required(CONF_STAT_ZONE_FNAME, - default=self._parm_or_error_msg(CONF_STAT_ZONE_FNAME)): - selector.TextSelector(), - vol.Required(CONF_STAT_ZONE_STILL_TIME, - default=Gb.conf_general[CONF_STAT_ZONE_STILL_TIME]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=0, max=60, unit_of_measurement='minutes')), - vol.Required(CONF_STAT_ZONE_INZONE_INTERVAL, - default=Gb.conf_general[CONF_STAT_ZONE_INZONE_INTERVAL]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=5, max=60, step=5, unit_of_measurement='minutes')), - - vol.Optional('passthru_zone_header', - default=ptzh_default): - cv.multi_select([PASSTHRU_ZONE_HEADER]), - vol.Required(CONF_PASSTHRU_ZONE_TIME, - default=Gb.conf_general[CONF_PASSTHRU_ZONE_TIME]): - selector.NumberSelector(selector.NumberSelectorConfig( - min=0, max=5, step=.5, unit_of_measurement='minutes')), - - vol.Optional(CONF_TRACK_FROM_BASE_ZONE_USED, - default=tfzh_default): - cv.multi_select([TRK_FROM_HOME_ZONE_HEADER]), - vol.Required(CONF_TRACK_FROM_BASE_ZONE, - default=self._option_parm_to_text(CONF_TRACK_FROM_BASE_ZONE, self.zone_name_key_text)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(self.zone_name_key_text), mode='dropdown')), - vol.Optional(CONF_TRACK_FROM_HOME_ZONE, - default=Gb.conf_general[CONF_TRACK_FROM_HOME_ZONE]): - # cv.boolean, - selector.BooleanSelector(), - - vol.Required('action_items', - default=self.action_default_text('save')): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - except Exception as err: - log_exception(err) - - #------------------------------------------------------------------------ - elif step_id == 'sensors': - self.actions_list = [ACTION_LIST_ITEMS_KEY_TEXT['exclude_sensors']] - self.actions_list.extend(ACTION_LIST_ITEMS_BASE) - - if HOME_DISTANCE not in Gb.conf_sensors[CONF_SENSORS_TRACKING_DISTANCE]: - Gb.conf_sensors[CONF_SENSORS_TRACKING_DISTANCE].append(HOME_DISTANCE) - - return vol.Schema({ - vol.Required(CONF_SENSORS_DEVICE, - default=Gb.conf_sensors[CONF_SENSORS_DEVICE]): - cv.multi_select(CONF_SENSORS_DEVICE_KEY_TEXT), - vol.Required(CONF_SENSORS_TRACKING_UPDATE, - default=Gb.conf_sensors[CONF_SENSORS_TRACKING_UPDATE]): - cv.multi_select(CONF_SENSORS_TRACKING_UPDATE_KEY_TEXT), - vol.Required(CONF_SENSORS_TRACKING_TIME, - default=Gb.conf_sensors[CONF_SENSORS_TRACKING_TIME]): - cv.multi_select(CONF_SENSORS_TRACKING_TIME_KEY_TEXT), - vol.Required(CONF_SENSORS_TRACKING_DISTANCE, - default=Gb.conf_sensors[CONF_SENSORS_TRACKING_DISTANCE]): - cv.multi_select(CONF_SENSORS_TRACKING_DISTANCE_KEY_TEXT), - vol.Required(CONF_SENSORS_ZONE, - default=Gb.conf_sensors[CONF_SENSORS_ZONE]): - cv.multi_select(CONF_SENSORS_ZONE_KEY_TEXT), - vol.Required(CONF_SENSORS_OTHER, - default=Gb.conf_sensors[CONF_SENSORS_OTHER]): - cv.multi_select(CONF_SENSORS_OTHER_KEY_TEXT), - # vol.Required(CONF_SENSORS_TRACK_FROM_ZONES, - # default=Gb.conf_sensors[CONF_SENSORS_TRACK_FROM_ZONES]): - # cv.multi_select(CONF_SENSORS_TRACK_FROM_ZONES_KEY_TEXT), - vol.Required(CONF_SENSORS_MONITORED_DEVICES, - default=Gb.conf_sensors[CONF_SENSORS_MONITORED_DEVICES]): - cv.multi_select(CONF_SENSORS_MONITORED_DEVICES_KEY_TEXT), - vol.Required(CONF_SENSORS_TRACKING_OTHER, - default=Gb.conf_sensors[CONF_SENSORS_TRACKING_OTHER]): - cv.multi_select(CONF_SENSORS_TRACKING_OTHER_KEY_TEXT), - vol.Optional(CONF_EXCLUDED_SENSORS, - default=Gb.conf_sensors[CONF_EXCLUDED_SENSORS]): - selector.SelectSelector(selector.SelectSelectorConfig( - options=Gb.conf_sensors[CONF_EXCLUDED_SENSORS], mode='list', multiple=True)), - - vol.Required('action_items', - default=self.action_default_text('save')): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - #------------------------------------------------------------------------ - elif step_id == 'exclude_sensors': - self.actions_list = [ACTION_LIST_ITEMS_KEY_TEXT['filter_sensors']] - self.actions_list.extend(ACTION_LIST_ITEMS_BASE) - - if self.sensors_list_filter == '?': - filtered_sensors_fname_list = [f"None Displayed - Enter a Filter or `all` \ - to display all sensors ({len(self.sensors_fname_list)} Sensors)"] - filtered_sensors_list_default = [] - else: - self.sensors_list_filter.replace('?', '') - if self.sensors_list_filter.lower() == 'all': - filtered_sensors_fname_list = [sensor_fname - for sensor_fname in self.sensors_fname_list - if sensor_fname not in self.excluded_sensors] - else: - filtered_sensors_fname_list = list(set([sensor_fname - for sensor_fname in self.sensors_fname_list - if ((instr(sensor_fname.lower(), self.sensors_list_filter) - and sensor_fname not in self.excluded_sensors))])) - - filtered_sensors_list_default = list(set([sensor_fname - for sensor_fname in filtered_sensors_fname_list - if sensor_fname in self.excluded_sensors])) - - filtered_sensors_fname_list.sort() - if filtered_sensors_fname_list == []: - filtered_sensors_fname_list = [f"No Sensors found containing \ - '{self.sensors_list_filter}'"] - - return vol.Schema({ - vol.Optional(CONF_EXCLUDED_SENSORS, - default=self.excluded_sensors): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.excluded_sensors, mode='list', multiple=True)), - vol.Optional('filter', - default=self.sensors_list_filter): - selector.TextSelector(), - vol.Optional('filtered_sensors', - default=filtered_sensors_list_default): - selector.SelectSelector(selector.SelectSelectorConfig( - options=filtered_sensors_fname_list, mode='list', multiple=True)), - - vol.Required('action_items', - default=self.action_default_text('filter_sensors')): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - - #------------------------------------------------------------------------ - elif step_id.startswith('restart_ha_ic3'): - restart_default = 'restart_ha' - self.actions_list = [] - self.actions_list.append(ACTION_LIST_ITEMS_KEY_TEXT['restart_ha']) - self.actions_list.append(ACTION_LIST_ITEMS_KEY_TEXT['reload_icloud3']) - self.actions_list.append(ACTION_LIST_ITEMS_KEY_TEXT['cancel']) - - actions_list_default = self.action_default_text(restart_default) - - return vol.Schema({ - vol.Required('action_items', - default=actions_list_default): - selector.SelectSelector(selector.SelectSelectorConfig( - options=self.actions_list, mode='list')), - }) - - #------------------------------------------------------------------------ - elif step_id == '': - pass - - return schema + return form_reauth(self) + else: + return {} diff --git a/custom_components/icloud3/config_flow_forms.py b/custom_components/icloud3/config_flow_forms.py new file mode 100644 index 0000000..bada6c1 --- /dev/null +++ b/custom_components/icloud3/config_flow_forms.py @@ -0,0 +1,1213 @@ + +from homeassistant.helpers import (selector, entity_registry as er, device_registry as dr, + area_registry as ar,) +import homeassistant.helpers.config_validation as cv +import voluptuous as vol +import pyotp +import time +from datetime import datetime + +from .global_variables import GlobalVariables as Gb +from .const import (RED_ALERT, LINK, RLINK, RARROW, + IPHONE, IPAD, WATCH, AIRPODS, ICLOUD, OTHER, + DEVICE_TYPE_FNAME, MOBAPP, NO_MOBAPP, + INACTIVE_DEVICE, HOME_DISTANCE, + PICTURE_WWW_STANDARD_DIRS, CONF_PICTURE_WWW_DIRS, + DEFAULT_DEVICE_CONF, + CONF_EVLOG_CARD_DIRECTORY, CONF_EVLOG_BTNCONFIG_URL, + CONF_APPLE_ACCOUNT, CONF_USERNAME, CONF_PASSWORD, CONF_LOCATE_ALL, CONF_TOTP_KEY, + CONF_DATA_SOURCE, CONF_VERIFICATION_CODE, + CONF_TRACK_FROM_ZONES, CONF_LOG_ZONES, + CONF_TRACK_FROM_BASE_ZONE_USED, CONF_TRACK_FROM_BASE_ZONE, CONF_TRACK_FROM_HOME_ZONE, + CONF_PICTURE, CONF_DEVICE_TYPE, CONF_INZONE_INTERVALS, + CONF_UNIT_OF_MEASUREMENT, CONF_TIME_FORMAT, + CONF_MAX_INTERVAL, CONF_OFFLINE_INTERVAL, CONF_EXIT_ZONE_INTERVAL, CONF_MOBAPP_ALIVE_INTERVAL, + CONF_GPS_ACCURACY_THRESHOLD, CONF_OLD_LOCATION_THRESHOLD, CONF_OLD_LOCATION_ADJUSTMENT, + CONF_TRAVEL_TIME_FACTOR, CONF_TFZ_TRACKING_MAX_DISTANCE, + 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_DISCARD_POOR_GPS_INZONE, + CONF_DISTANCE_BETWEEN_DEVICES, + CONF_WAZE_USED, CONF_WAZE_SERVER, CONF_WAZE_MAX_DISTANCE, CONF_WAZE_MIN_DISTANCE, + CONF_WAZE_REALTIME, CONF_WAZE_HISTORY_DATABASE_USED, CONF_WAZE_HISTORY_MAX_DISTANCE, + CONF_WAZE_HISTORY_TRACK_DIRECTION, + CONF_STAT_ZONE_FNAME, CONF_STAT_ZONE_STILL_TIME, CONF_STAT_ZONE_INZONE_INTERVAL, + CONF_DISPLAY_TEXT_AS, + CONF_IC3_DEVICENAME, CONF_FNAME, CONF_FAMSHR_DEVICENAME, CONF_MOBILE_APP_DEVICE, + CONF_TRACKING_MODE, CONF_INZONE_INTERVAL, CONF_FIXED_INTERVAL, + CONF_AWAY_TIME_ZONE_1_OFFSET, CONF_AWAY_TIME_ZONE_1_DEVICES, + CONF_AWAY_TIME_ZONE_2_OFFSET, CONF_AWAY_TIME_ZONE_2_DEVICES, + CONF_SENSORS_MONITORED_DEVICES, + CONF_SENSORS_DEVICE, + CONF_SENSORS_TRACKING_UPDATE, CONF_SENSORS_TRACKING_TIME, CONF_SENSORS_TRACKING_DISTANCE, + CONF_SENSORS_TRACKING_OTHER, CONF_SENSORS_ZONE, + CONF_SENSORS_OTHER, CONF_EXCLUDED_SENSORS, + CF_PROFILE, + ) +from .const_config_flow import * +from .support import config_file +from .helpers.common import (instr, isbetween, list_to_str, list_add, is_empty, zone_dname, ) +from .helpers.messaging import (log_exception, log_debug_msg, log_info_msg, + _log, _evlog, + post_event, post_monitor_msg, ) +from .helpers.time_util import (format_timer, ) +from .support import mobapp_interface +from .support import config_file + +#----------------------------------------------------------------------------------------- +def dict_value_to_list(key_value_dict): + """ Make a drop down list from a list """ + + if type(key_value_dict) is dict: + value_list = [v for v in key_value_dict.values() if v.startswith('.') is False] + else: + value_list = list(key_value_dict) + + return value_list + +#----------------------------------------------------------------------------------------- +def six_item_list(list_item): + if len(list_item) >= 6: return list_item + + for i in range(6 - len(list_item)): + list_item.append('.') + + return list_item + +#----------------------------------------------------------------------------------------- +def six_item_dict(dict_item): + if len(dict_item) >= 6: return dict_item + + dummy_key = '' + for i in range(6 - len(dict_item)): + dummy_key += '.' + dict_item[dummy_key] = '.' + + return dict_item + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# MENU +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_menu(self): + menu_title = MENU_PAGE_TITLE[self.menu_page_no] + menu_action_items = MENU_ACTION_ITEMS.copy() + + if self.menu_page_no == 0: + menu_key_text = MENU_KEY_TEXT_PAGE_0 + menu_action_items[1] = MENU_KEY_TEXT['next_page_1'] + + if (self.username == '' or self.password == ''): + self.menu_item_selected[0] = MENU_KEY_TEXT['data_source'] + elif (self.username and self.password + and (self._device_cnt() == 0 or self._device_cnt() == self._inactive_device_cnt())): + self.menu_item_selected[0] = MENU_KEY_TEXT['device_list'] + else: + menu_key_text = MENU_KEY_TEXT_PAGE_1 + menu_action_items[1] = MENU_KEY_TEXT['next_page_0'] + + return vol.Schema({ + vol.Required("menu_items", + default=self.menu_item_selected[self.menu_page_no]): + selector.SelectSelector(selector.SelectSelectorConfig( + options=menu_key_text, mode='list')), + vol.Required("action_items", + default=menu_action_items[0]): + selector.SelectSelector(selector.SelectSelectorConfig( + options=menu_action_items, mode='list')), + }) + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# CONFIRM ACTION +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_confirm_action(self, + extra_action_items=None, + actions_list_default=None, + confirm_action_form_hdr=None): + + actions_list = [] + if extra_action_items is None: extra_action_items = [] + for extra_action_item in extra_action_items: + actions_list.append(ACTION_LIST_OPTIONS[extra_action_item]) + actions_list.extend(CONFIRM_ACTIONS) + + actions_list_default = actions_list_default or 'confirm_return' + confirm_action_form_hdr = confirm_action_form_hdr or 'Do you want to perform the selected action' + + return vol.Schema({ + vol.Required('confirm_action_form_hdr', + default=confirm_action_form_hdr): + selector.SelectSelector(selector.SelectSelectorConfig( + options=[confirm_action_form_hdr], mode='list')), + vol.Required('action_items', + default=self.action_default_text(actions_list_default)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=actions_list, mode='list')), + }) + +#------------------------------------------------------------------------ +def form_restart_icloud3(self): + self.actions_list = [] + restart_default='restart_ic3_now' + + if 'restart_ha' in self.config_parms_update_control: + restart_default='restart_ha' + self.actions_list.append(ACTION_LIST_OPTIONS['restart_ha']) + + self.actions_list.append(ACTION_LIST_OPTIONS['restart_ic3_now']) + self.actions_list.append(ACTION_LIST_OPTIONS['restart_ic3_later']) + + actions_list_default = self.action_default_text(restart_default) + if self._inactive_device_cnt() > 0: + inactive_devices = [conf_device[CONF_IC3_DEVICENAME] + for conf_device in Gb.conf_devices + if conf_device[CONF_TRACKING_MODE] == INACTIVE_DEVICE] + inactive_devices_list = ( + f"{ACTION_LIST_OPTIONS['review_inactive_devices']} " + f"({list_to_str(inactive_devices)})") + self.actions_list.append(inactive_devices_list) + if self._set_inactive_devices_header_msg() in ['all', 'most']: + actions_list_default = inactive_devices_list + + self.actions_list.append(ACTION_LIST_OPTIONS['cancel']) + + return vol.Schema({ + vol.Required('action_items', + default=actions_list_default): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# REVIEW INACTIVE DEVICES +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_review_inactive_devices(self): + self.actions_list = REVIEW_INACTIVE_DEVICES.copy() + + self.inactive_devices_key_text = {conf_device[CONF_IC3_DEVICENAME]: self._format_device_info(conf_device) + for conf_device in Gb.conf_devices + if conf_device[CONF_TRACKING_MODE] == INACTIVE_DEVICE} + + return vol.Schema({ + vol.Required('inactive_devices', + default=[]): + cv.multi_select(six_item_dict(self.inactive_devices_key_text)), + + vol.Required('action_items', + default=self.action_default_text('inactive_keep_inactive')): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# DATA SOURCE +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_data_source(self): + self._build_apple_accounts_list() + + self.actions_list = [] + self.actions_list.extend(ICLOUD_ACCOUNT_ACTIONS) + self.actions_list.extend(ACTION_LIST_ITEMS_BASE) + if self.username != '' and self.password != '' and instr(self.data_source, ICLOUD) is False: + self.errors['base'] = 'icloud_acct_data_source_warning' + + default_action = self.actions_list_default if self.actions_list_default else 'save' + self.actions_list_default = '' + + # Build list of all apple accts + self.apple_acct_items_list= [apple_acct_item + for apple_acct_username, apple_acct_item in self.apple_acct_items_by_username.items() + if apple_acct_username != 'apple_acct_hdr'] + + if len(self.apple_acct_items_list) <= 6: + self.apple_acct_items_displayed = self.apple_acct_items_list + else: + _build_apple_accts_displayed_over_5(self) + list_add(self.apple_acct_items_displayed, '➤ ADD A NEW APPLE ACCOUNT') + + default_key = self.aa_page_item[self.aa_page_no] + default_item = self.apple_acct_items_by_username.get(default_key) + if default_item not in self.apple_acct_items_displayed: + default_item = self.apple_acct_items_displayed[0] + + if is_empty(Gb.devicenames_x_mobapp_dnames): + mobapp_interface.get_mobile_app_integration_device_info() + if is_empty(Gb.devicenames_x_mobapp_dnames): + self.errors['data_source_mobapp'] = 'mobile_app_error' + + return vol.Schema({ + vol.Optional('data_source', + default=Gb.conf_tracking[CONF_DATA_SOURCE].replace(' ', '').split(',')): + cv.multi_select(DATA_SOURCE_OPTIONS), + vol.Optional('apple_accts', + default=default_item): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.apple_acct_items_displayed, mode='list')), + vol.Required('action_items', + default=self.action_default_text(default_action)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + +#................................................................................................ +def _build_apple_accts_displayed_over_5(self): + ''' + Build the display page and next page line for the Apple Accts list + when more than 5 apple accounts + ''' + # Build the list of apple accts to display on this page + list_from_idx = self.aa_page_no * 5 + self.apple_acct_items_displayed = self.apple_acct_items_list[list_from_idx:list_from_idx+5] + + # Build the list of apple accts to display on the next page + list_from_idx = list_from_idx + 5 + if list_from_idx >= len(self.apple_acct_items_list): + list_from_idx = 0 + + # Extract owners from the accts list (GaryCobb (username) -> 5 of 7 Tracked Devices)) + account_owners = [apple_acct_list_item.split(RARROW)[0] + for apple_acct_list_item in self.apple_acct_items_list] + account_owners_next_page = (f"➤ OTHER APPLE ACCOUNTS{RARROW}" + f"{', '.join(account_owners[list_from_idx:list_from_idx+5])}") + list_add(self.apple_acct_items_displayed, account_owners_next_page) + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# APPLE USERNAME PASSWORD +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_update_apple_acct(self): + self.actions_list = USERNAME_PASSWORD_ACTIONS.copy() + + errs_ui = self.errors_user_input + username = errs_ui.get(CONF_USERNAME) or self.conf_apple_acct[CONF_USERNAME] or f" {self.username}" + password = errs_ui.get(CONF_PASSWORD) or self.conf_apple_acct[CONF_PASSWORD] or f" {self.password}" + locate_all = errs_ui.get(CONF_LOCATE_ALL) or self.conf_apple_acct[CONF_LOCATE_ALL] + totp_key = errs_ui.get(CONF_TOTP_KEY) or self.conf_apple_acct[CONF_TOTP_KEY] or ' ' + + if password.strip() == '' or self.add_apple_acct_flag: + password_selector = selector.TextSelector() + else: + password_selector = selector.TextSelector(selector.TextSelectorConfig(type='password')) + + url_suffix_china = (Gb.icloud_server_endpoint_suffix == 'cn') + + if (self.add_apple_acct_flag is False + and username not in Gb.PyiCloud_by_username): + self.errors['base'] = 'icloud_acct_not_logged_into' + + if self.add_apple_acct_flag: + acct_info = '➤ ADD A NEW APPLE ACCOUNT' + else: + acct_info =(f"{self.apple_acct_items_by_username[username]}, " + f"{self.tracked_untracked_form_msg(username)}") + + schema = ({ + vol.Optional('account_selected', + default=acct_info): + selector.SelectSelector(selector.SelectSelectorConfig( + options=[acct_info], mode='list')), + vol.Optional(CONF_USERNAME, + default=username): + selector.TextSelector(), + vol.Optional(CONF_PASSWORD , + default=password): + password_selector, + vol.Optional(CONF_TOTP_KEY, + default=totp_key): + selector.TextSelector(), + vol.Optional('locate_all', + default=locate_all): + cv.boolean, + }) + + if self.aa_idx == 0: + schema.update({ + vol.Optional('url_suffix_china', + default=url_suffix_china): + cv.boolean, + }) + + schema.update({ + vol.Required('action_items', + default=self.action_default_text('log_into_apple_acct')): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + + return vol.Schema(schema) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# DELETE APPLE ACCOUNT +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_delete_apple_acct(self): + self.actions_list = DELETE_APPLE_ACCT_ACTIONS.copy() + + username = self.conf_apple_acct[CONF_USERNAME] + apple_acct_info = self.apple_acct_items_by_username[username] + if (instr(apple_acct_info, '0 of') + or instr(apple_acct_info, '1 of 1') + or instr(apple_acct_info, 'Tracked-()') + or instr(apple_acct_info, 'Not logged into')): + default_device_action = 'delete_devices' + else: + default_device_action = 'reassign_devices' + + acct_info = f"{apple_acct_info}, {self.tracked_untracked_form_msg(username)}" + + return vol.Schema({ + vol.Optional('account_selected', + default=acct_info): + selector.SelectSelector(selector.SelectSelectorConfig( + options=[acct_info], mode='list')), + vol.Required('device_action', + default=DELETE_APPLE_ACCT_DEVICE_ACTION_OPTIONS[default_device_action]): + selector.SelectSelector( + selector.SelectSelectorConfig( + options=dict_value_to_list(DELETE_APPLE_ACCT_DEVICE_ACTION_OPTIONS), + mode='list')), + vol.Required('action_items', + default=self.action_default_text('cancel_return')): + selector.SelectSelector( + selector.SelectSelectorConfig(options=self.actions_list, mode='list')), + }) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# REAUTH +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_reauth(self): + + try: + self._build_apple_accounts_list() + + # Get the 1st username if it is coming from the menu or the HA reauth request + # and has not been selected yet + if Gb.conf_apple_accounts: + if self.conf_apple_acct == '': + if instr(str(self.apple_acct_items_by_username), 'AUTHENTICATION'): + self.apple_acct_reauth_username = [username + for username, acct_info in self.apple_acct_items_by_username.items() + if instr(acct_info, 'AUTHENTICATION')][0] + self.conf_apple_acct, self.aa_idx = \ + config_file.conf_apple_acct(self.apple_acct_reauth_username) + else: + self.conf_apple_acct, self.aa_idx = config_file.conf_apple_acct(0) + + username = (self.apple_acct_reauth_username or self.conf_apple_acct[CONF_USERNAME]) + default_acct_selected = self.apple_acct_items_by_username.get(username) + else: + default_acct_selected = ' ' + + # try: + # if self.conf_apple_acct[CONF_TOTP_KEY]: + # _log(f"{self.conf_apple_acct[CONF_TOTP_KEY].replace('-', '')=}") + # otp = pyotp.TOTP(self.conf_apple_acct[CONF_TOTP_KEY].replace('-', '')) + # otp_code = otp.now() + # except Exception as err: + # log_exception(err) + # else: + otp_code = ' ' + + # See if any apple acounts are not logged into + # Check a str of the list instead of cycling thru it since we don't care which one + if instr(default_acct_selected, 'Not logged into'): + action_list_default = 'log_into_apple_acct' + self.errors['base'] = 'icloud_acct_not_logged_into' + self.actions_list = [ACTION_LIST_OPTIONS['log_into_apple_acct']] + else: + action_list_default = 'send_verification_code' + self.actions_list = [] + self.actions_list.extend(REAUTH_ACTIONS) + + return vol.Schema({ + vol.Optional('account_selected', + default=default_acct_selected): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(self.apple_acct_items_by_username), + mode='dropdown')), + vol.Optional(CONF_VERIFICATION_CODE, default=otp_code): + selector.TextSelector(), + vol.Optional('action_items', + default=self.action_default_text(action_list_default)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + except Exception as err: + log_exception(err) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# DEVICE LIST +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_device_list(self): + action_default = 'update_device' + self.actions_list = DEVICE_LIST_ACTIONS.copy() + + # Build list of all devices + self.device_items_list = [device_item for device_item in self.device_items_by_devicename.values()] + + if is_empty(self.device_items_list): + pass + elif len(self.device_items_list) <= 6: + self.device_items_displayed = self.device_items_list + else: + _build_device_items_displayed_over_5(self) + list_add(self.device_items_displayed, '➤ ADD A NEW DEVICE') + + default_key = self.dev_page_item[self.dev_page_no] + default_item = self.device_items_by_devicename.get(default_key) + if default_item not in self.device_items_displayed: + default_item = self.device_items_displayed[0] + + return vol.Schema({ + vol.Required('devices', + default=default_item): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.device_items_displayed, mode='list')), + vol.Required('action_items', + default=self.action_default_text(action_default)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + +#................................................................................................ +def _build_device_items_displayed_over_5(self): + ''' + Build the display page and next page line for the Apple Accts list + when more than 5 apple accounts + ''' + # Build the list of apple accts to display on this page + list_from_idx = self.dev_page_no * 5 + self.device_items_displayed = self.device_items_list[list_from_idx:list_from_idx+5] + + # Build the list of devices to display on the next page + list_from_idx = list_from_idx + 5 + if list_from_idx >= len(self.device_items_list): + list_from_idx = 0 + + # Extract fname (devicename) from the devices list (Gary (gary_iphone) → ...)) + device_fnames = [device_item.split(RARROW)[0] + for device_item in self.device_items_list] + device_fnames_next_page = (f"➤ OTHER DEVICES{RARROW}" + f"{', '.join(device_fnames[list_from_idx:list_from_idx+5])}") + list_add(self.device_items_displayed, device_fnames_next_page) + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# ADD DEVICE +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_add_device(self): + self.actions_list = ACTION_LIST_ITEMS_BASE.copy() + + return vol.Schema({ + vol.Required(CONF_IC3_DEVICENAME, + default=self._parm_or_device(CONF_IC3_DEVICENAME)): + selector.TextSelector(), + vol.Required(CONF_FNAME, + default=self._parm_or_device(CONF_FNAME)): + selector.TextSelector(), + vol.Required(CONF_DEVICE_TYPE, + default=self._parm_or_device(CONF_DEVICE_TYPE, suggested_value=IPHONE)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(DEVICE_TYPE_FNAME), mode='dropdown')), + vol.Required(CONF_TRACKING_MODE, + default=self._option_parm_to_text(CONF_TRACKING_MODE, TRACKING_MODE_OPTIONS)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(TRACKING_MODE_OPTIONS), mode='dropdown')), + vol.Required('mobapp', + default=True): + cv.boolean, + # selector.BooleanSelector(), + }) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# UPDATE DEVICE +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_update_device(self): + + error_key = '' + self.errors = self.errors or {} + self.actions_list = [] + self.actions_list.append(ACTION_LIST_OPTIONS['save']) + self.actions_list.append(ACTION_LIST_OPTIONS['cancel']) + self.actions_list.append(ACTION_LIST_OPTIONS['cancel_device_selection']) + + # Display Advanced Tracking Parameters + log_zones_fnames = [zone_dname(zone) for zone in self.conf_device[CONF_LOG_ZONES] if zone.startswith('Name') is False] + tfz_fnames = [zone_dname(zone) for zone in self.conf_device[CONF_TRACK_FROM_ZONES]] + RARELY_UPDATED_PARMS_HEADER = ( f"DeviceType ({self._option_parm_to_text(CONF_DEVICE_TYPE, DEVICE_TYPE_FNAME)}), " + f"inZoneInterval ({format_timer(self.conf_device[CONF_INZONE_INTERVAL]*60)}), " + f"FixedInterval ({format_timer(self.conf_device[CONF_FIXED_INTERVAL]*60)}), " + f"LogFromZones ({list_to_str(log_zones_fnames)}), " + f"Track-from-Zone ({list_to_str(tfz_fnames)}), " + f"PrimaryTrackFromZone ({zone_dname(self.conf_device[CONF_TRACK_FROM_BASE_ZONE])})") + atp_default = [RARELY_UPDATED_PARMS_HEADER] if self.display_rarely_updated_parms else [] + + icloud_dname_username =(f"{self.conf_device[CONF_FAMSHR_DEVICENAME]}" + f"{LINK}{self.conf_device[CONF_APPLE_ACCOUNT]}") + + # iCloud Devices list setup + if self.conf_device[CONF_FAMSHR_DEVICENAME] == 'None': + icloud_dname_username = 'None' + elif (self.conf_device[CONF_APPLE_ACCOUNT] == '' + and self.conf_device[CONF_FAMSHR_DEVICENAME] != 'None'): + icloud_dname_username = f"{self.conf_device[CONF_IC3_DEVICENAME]}{LINK}UNKNOWN" + + default_icloud_devicename = self.icloud_list_text_by_fname.get( + icloud_dname_username, + f"{self.conf_device[CONF_FAMSHR_DEVICENAME]}{LINK}UNKNOWN") + + unknown_key = f".{self.conf_device[CONF_IC3_DEVICENAME]}{LINK}UNKNOWN" + if unknown_key in self.icloud_list_text_by_fname: + default_icloud_devicename = self.icloud_list_text_by_fname[unknown_key] + # if '.unknown' in self.icloud_list_text_by_fname: + # default_icloud_devicename = self.icloud_list_text_by_fname['.unknown'] + + + # Check the icloud_dname and mobile app devicename for any errors + self._validate_data_source_names(self.conf_device) + + # Mobile App Devices list setup + mobapp_device = self.conf_device[CONF_MOBILE_APP_DEVICE] + mobapp_list_text_by_entity_id = self.mobapp_list_text_by_entity_id.copy() + if '.unknown' in mobapp_list_text_by_entity_id: + default_mobile_app_device = mobapp_list_text_by_entity_id['.unknown'] + else: + default_mobile_app_device = mobapp_list_text_by_entity_id[mobapp_device] + + # Picture list setup + picture_filename = self.conf_device[CONF_PICTURE] + picture_by_filename = self.picture_by_filename.copy() + if picture_filename not in picture_by_filename: + self.errors[CONF_PICTURE] = 'unknown_picture' + picture_by_filename[picture_filename] = (f"{RED_ALERT}{picture_filename}{RARROW}" + "FILE NOT FOUND") + + if self.errors != {}: + self.errors['base'] = 'unknown_value' + + if self.conf_device[CONF_TRACKING_MODE] == INACTIVE_DEVICE: + self.errors[CONF_TRACKING_MODE] = 'inactive_device' + + log_zones_key_text = {'none': 'None'} + log_zones_key_text.update(self.zone_name_key_text) + log_zones_key_text.update(LOG_ZONES_KEY_TEXT) + + schema = { + vol.Required(CONF_IC3_DEVICENAME, + default=self._parm_or_device(CONF_IC3_DEVICENAME)): + selector.TextSelector(), + vol.Required(CONF_FNAME, + default=self._parm_or_device(CONF_FNAME)): + selector.TextSelector(), + vol.Required(CONF_TRACKING_MODE, + default=self._option_parm_to_text(CONF_TRACKING_MODE, TRACKING_MODE_OPTIONS)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(TRACKING_MODE_OPTIONS), mode='dropdown')), + vol.Required(CONF_FAMSHR_DEVICENAME, + default=default_icloud_devicename): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(self.icloud_list_text_by_fname), mode='dropdown')), + vol.Required(CONF_MOBILE_APP_DEVICE, + default=default_mobile_app_device): + # default=self._option_parm_to_text(CONF_MOBILE_APP_DEVICE, mobapp_list_text_by_entity_id)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(mobapp_list_text_by_entity_id), mode='dropdown')), + vol.Required(CONF_PICTURE, + default=self._option_parm_to_text(CONF_PICTURE, picture_by_filename)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(picture_by_filename), mode='dropdown')), + } + + if self.display_rarely_updated_parms is False: + schema.update({ + vol.Optional(RARELY_UPDATED_PARMS, + default=atp_default): + cv.multi_select([RARELY_UPDATED_PARMS_HEADER]), + }) + + if self.display_rarely_updated_parms: + schema.update({ + vol.Required(CONF_DEVICE_TYPE, + default=self._option_parm_to_text(CONF_DEVICE_TYPE, DEVICE_TYPE_FNAME)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(DEVICE_TYPE_FNAME), mode='dropdown')), + vol.Required(CONF_INZONE_INTERVAL, + default=self.conf_device[CONF_INZONE_INTERVAL]): + # default=self._parm_or_device(CONF_INZONE_INTERVAL)): + selector.NumberSelector(selector.NumberSelectorConfig( + min=5, max=480, step=5, unit_of_measurement='minutes')), + vol.Required(CONF_FIXED_INTERVAL, + default=self.conf_device[CONF_FIXED_INTERVAL]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=0, max=480, step=5, unit_of_measurement='minutes')), + vol.Optional(CONF_LOG_ZONES, + default=self._parm_or_device(CONF_LOG_ZONES)): + cv.multi_select(six_item_dict(log_zones_key_text)), + vol.Required(CONF_TRACK_FROM_ZONES, + default=self._parm_or_device(CONF_TRACK_FROM_ZONES)): + cv.multi_select(six_item_dict(self.zone_name_key_text)), + #cv.multi_select(self.zone_name_key_text), + vol.Required(CONF_TRACK_FROM_BASE_ZONE, + default=self._option_parm_to_text(CONF_TRACK_FROM_BASE_ZONE, + self.zone_name_key_text, conf_device=True)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(self.zone_name_key_text), mode='dropdown')), + }) + + schema.update({ + vol.Required('action_items', + default=self.action_default_text('save')): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + + return vol.Schema(schema) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# DELETE DEVICE +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_delete_device(self): + self.actions_list = DELETE_DEVICE_ACTIONS.copy() + device_text = ( f"{self.conf_device[CONF_FNAME]} " + f"({self.conf_device[CONF_IC3_DEVICENAME]})") + device_selected = self.device_items_list[self.conf_device_idx] + + # The first item is 'Delete this device, add the selected device's info + return vol.Schema({ + vol.Required('device_selected', + default=device_selected): + selector.SelectSelector(selector.SelectSelectorConfig( + options=[device_selected], mode='list')), + vol.Required('action_items', + default=self.action_default_text('delete_device_cancel')): + selector.SelectSelector( + selector.SelectSelectorConfig(options=self.actions_list, mode='list')), + }) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# ACTIONS +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_actions(self): + debug_OPTIONS = ACTIONS_DEBUG_ITEMS.copy() + if Gb.log_debug_flag: + debug_OPTIONS.pop('debug_start') + else: + debug_OPTIONS.pop('debug_stop') + if Gb.log_rawdata_flag: + debug_OPTIONS.pop('rawdata_start') + else: + debug_OPTIONS.pop('rawdata_stop') + + return vol.Schema({ + vol.Optional('ic3_actions', default=[]): + cv.multi_select(ACTIONS_IC3_ITEMS), + # selector.SelectSelector(selector.SelectSelectorConfig( + # options=dict_value_to_list(ACTIONS_IC3_ITEMS), mode='list')), + vol.Optional('debug_actions', default=[]): + cv.multi_select(debug_OPTIONS), + # selector.SelectSelector(selector.SelectSelectorConfig( + # options=dict_value_to_list(debug_OPTIONS), mode='list')), + vol.Optional('other_actions', default=[]): + cv.multi_select(ACTIONS_OTHER_ITEMS), + # selector.SelectSelector(selector.SelectSelectorConfig( + # options=dict_value_to_list(ACTIONS_OTHER_ITEMS), mode='list')), + vol.Optional('action_items', default=[]): + cv.multi_select(ACTIONS_ACTION_ITEMS), + # selector.SelectSelector(selector.SelectSelectorConfig( + # options=dict_value_to_list(ACTIONS_ACTION_ITEMS), mode='list')), + }) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# FORMAT SETTINGS +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_format_settings(self): + self.actions_list = ACTION_LIST_ITEMS_BASE.copy() + 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(six_item_dict(self.log_level_devices_key_text)), + vol.Required(CONF_LOG_LEVEL, + default=self._option_parm_to_text(CONF_LOG_LEVEL, LOG_LEVEL_OPTIONS)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(LOG_LEVEL_OPTIONS), mode='dropdown')), + vol.Required(CONF_DISPLAY_ZONE_FORMAT, + default=self._option_parm_to_text(CONF_DISPLAY_ZONE_FORMAT, DISPLAY_ZONE_FORMAT_OPTIONS)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(DISPLAY_ZONE_FORMAT_OPTIONS), mode='dropdown')), + vol.Required(CONF_DEVICE_TRACKER_STATE_SOURCE, + default=self._option_parm_to_text(CONF_DEVICE_TRACKER_STATE_SOURCE, DEVICE_TRACKER_STATE_SOURCE_OPTIONS)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(DEVICE_TRACKER_STATE_SOURCE_OPTIONS), mode='dropdown')), + vol.Required(CONF_UNIT_OF_MEASUREMENT, + default=self._option_parm_to_text(CONF_UNIT_OF_MEASUREMENT, UNIT_OF_MEASUREMENT_OPTIONS)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list( UNIT_OF_MEASUREMENT_OPTIONS), mode='dropdown')), + vol.Required(CONF_TIME_FORMAT, + default=self._option_parm_to_text(CONF_TIME_FORMAT, TIME_FORMAT_OPTIONS)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(TIME_FORMAT_OPTIONS), mode='dropdown')), + vol.Required(CONF_DISPLAY_GPS_LAT_LONG, + default=Gb.conf_general[CONF_DISPLAY_GPS_LAT_LONG]): + # cv.boolean, + selector.BooleanSelector(), + + vol.Required('action_items', + default=self.action_default_text('save')): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# CHANGE DEVICE ORDER +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_change_device_order(self): + self.actions_list = [ + ACTION_LIST_OPTIONS['move_up'], + ACTION_LIST_OPTIONS['move_down']] + self.actions_list.extend(ACTION_LIST_ITEMS_BASE) + + return vol.Schema({ + vol.Required('device_desc', + default=self.cdo_devicenames[self.cdo_curr_idx]): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.cdo_devicenames, mode='list')), + vol.Required('action_items', + default=self.action_default_text(self.actions_list_default)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# AWAY TIME ZONE +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_away_time_zone(self): + self.actions_list = [] + self.actions_list.extend(ACTION_LIST_ITEMS_BASE) + + return vol.Schema({ + vol.Required(CONF_AWAY_TIME_ZONE_1_DEVICES, + default=Gb.conf_general[CONF_AWAY_TIME_ZONE_1_DEVICES]): + cv.multi_select(six_item_dict(self.away_time_zone_devices_key_text)), + vol.Required(CONF_AWAY_TIME_ZONE_1_OFFSET, + default=self.away_time_zone_hours_key_text[Gb.conf_general[CONF_AWAY_TIME_ZONE_1_OFFSET]]): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(self.away_time_zone_hours_key_text), mode='dropdown')), + + vol.Required(CONF_AWAY_TIME_ZONE_2_DEVICES, + default=Gb.conf_general[CONF_AWAY_TIME_ZONE_2_DEVICES]): + cv.multi_select(six_item_dict(self.away_time_zone_devices_key_text)), + vol.Required(CONF_AWAY_TIME_ZONE_2_OFFSET, + default=self.away_time_zone_hours_key_text[Gb.conf_general[CONF_AWAY_TIME_ZONE_2_OFFSET]]): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(self.away_time_zone_hours_key_text), mode='dropdown')), + + vol.Required('action_items', + default=self.action_default_text('save')): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# DISPLAY TEXT AS +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_display_text_as(self): + self.dta_selected_idx = self.dta_selected_idx_page[self.dta_page_no] + if self.dta_selected_idx <= 4: + dta_page_display_list = [v for k,v in self.dta_working_copy.items() + if k <= 4] + dta_next_page_display_list = [v.split('>')[0] for k,v in self.dta_working_copy.items() + if k >= 5] + else: + dta_page_display_list = [v for k,v in self.dta_working_copy.items() + if k >= 5] + dta_next_page_display_list = [v.split('>')[0] for k,v in self.dta_working_copy.items() + if k <= 4] + + dta_next_page_display_items = ", ".join(dta_next_page_display_list) + next_page_text = ACTION_LIST_OPTIONS['next_page_items'] + next_page_text = next_page_text.replace('^info_field^', dta_next_page_display_items) + + self.actions_list = [next_page_text] + self.actions_list.extend([ACTION_LIST_OPTIONS['select_text_as']]) + self.actions_list.extend(ACTION_LIST_ITEMS_BASE) + + return vol.Schema({ + vol.Required(CONF_DISPLAY_TEXT_AS, + default=self.dta_working_copy[self.dta_selected_idx]): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dta_page_display_list)), + vol.Required('action_items', + default=self.action_default_text('select_text_as')): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# DISPLAY TEXT AS UPDATE +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_display_text_as_update(self): + self.actions_list = [ACTION_LIST_OPTIONS['clear_text_as']] + self.actions_list.extend(ACTION_LIST_ITEMS_BASE) + + if instr(self.dta_working_copy[self.dta_selected_idx], '>'): + text_from_to_parts = self.dta_working_copy[self.dta_selected_idx].split('>') + text_from = text_from_to_parts[0].strip() + text_to = text_from_to_parts[1].strip() + else: + text_from = '' + text_to = '' + + return vol.Schema({ + vol.Optional('text_from', + default=text_from): + selector.TextSelector(), + vol.Optional('text_to' , + default=text_to): + selector.TextSelector(), + vol.Required('action_items', + default=self.action_default_text('save')): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# TRACKING PARAMETERS +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_tracking_parameters(self): + self.actions_list = ACTION_LIST_ITEMS_BASE.copy() + + self.picture_by_filename = {} + if PICTURE_WWW_STANDARD_DIRS in Gb.conf_profile[CONF_PICTURE_WWW_DIRS]: + Gb.conf_profile[CONF_PICTURE_WWW_DIRS] = [] + + return vol.Schema({ + vol.Required(CONF_DISTANCE_BETWEEN_DEVICES, + default=Gb.conf_general[CONF_DISTANCE_BETWEEN_DEVICES]): + selector.BooleanSelector(), + vol.Required(CONF_GPS_ACCURACY_THRESHOLD, + default=Gb.conf_general[CONF_GPS_ACCURACY_THRESHOLD]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=5, max=300, step=5, unit_of_measurement='m')), + vol.Required(CONF_OLD_LOCATION_THRESHOLD, + default=Gb.conf_general[CONF_OLD_LOCATION_THRESHOLD]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=1, max=60, step=1, unit_of_measurement='minutes')), + vol.Required(CONF_OLD_LOCATION_ADJUSTMENT, + default=Gb.conf_general[CONF_OLD_LOCATION_ADJUSTMENT]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=0, max=60, step=1, unit_of_measurement='minutes')), + vol.Required(CONF_MAX_INTERVAL, + default=Gb.conf_general[CONF_MAX_INTERVAL]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=15, max=480, step=5, unit_of_measurement='minutes')), + vol.Required(CONF_EXIT_ZONE_INTERVAL, + default=Gb.conf_general[CONF_EXIT_ZONE_INTERVAL]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=.5, max=10, step=.5, unit_of_measurement='minutes')), + vol.Required(CONF_MOBAPP_ALIVE_INTERVAL, + default=Gb.conf_general[CONF_MOBAPP_ALIVE_INTERVAL]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=15, max=240, step=5, unit_of_measurement='minutes')), + vol.Required(CONF_OFFLINE_INTERVAL, + default=Gb.conf_general[CONF_OFFLINE_INTERVAL]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=5, max=240, step=5, unit_of_measurement='minutes')), + vol.Required(CONF_TFZ_TRACKING_MAX_DISTANCE, + default=Gb.conf_general[CONF_TFZ_TRACKING_MAX_DISTANCE]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=1, max=100, unit_of_measurement='Km')), + vol.Optional(CONF_DISCARD_POOR_GPS_INZONE, + default=Gb.conf_general[CONF_DISCARD_POOR_GPS_INZONE]): + selector.BooleanSelector(), + vol.Required(CONF_TRAVEL_TIME_FACTOR, + default=self._option_parm_to_text(CONF_TRAVEL_TIME_FACTOR, TRAVEL_TIME_INTERVAL_MULTIPLIER_KEY_TEXT)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(TRAVEL_TIME_INTERVAL_MULTIPLIER_KEY_TEXT), mode='dropdown')), + vol.Required(CONF_PICTURE_WWW_DIRS, + default=Gb.conf_profile[CONF_PICTURE_WWW_DIRS] or self.www_directory_list): + cv.multi_select(six_item_list(self.www_directory_list)), + vol.Required(CONF_EVLOG_CARD_DIRECTORY, + default=self._parm_or_error_msg(CONF_EVLOG_CARD_DIRECTORY, conf_group=CF_PROFILE)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(self.www_directory_list), mode='dropdown')), + vol.Optional(CONF_EVLOG_BTNCONFIG_URL, + default=f"{self._parm_or_error_msg(CONF_EVLOG_BTNCONFIG_URL, conf_group=CF_PROFILE)} "): + selector.TextSelector(), + + vol.Required('action_items', + default=self.action_default_text('save')): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# INZONE INTERVALS +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_inzone_intervals(self): + self.actions_list = ACTION_LIST_ITEMS_BASE.copy() + return vol.Schema({ + vol.Optional(IPHONE, + default=Gb.conf_general[CONF_INZONE_INTERVALS][IPHONE]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=5, max=480, step=5, unit_of_measurement='minutes')), + vol.Optional(IPAD, + default=Gb.conf_general[CONF_INZONE_INTERVALS][IPAD]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=5, max=480, step=5, unit_of_measurement='minutes')), + vol.Optional(WATCH, + default=Gb.conf_general[CONF_INZONE_INTERVALS][WATCH]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=5, max=480, step=5, unit_of_measurement='minutes')), + vol.Optional(AIRPODS, + default=Gb.conf_general[CONF_INZONE_INTERVALS][AIRPODS]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=5, max=480, step=5, unit_of_measurement='minutes')), + vol.Optional(NO_MOBAPP, + default=Gb.conf_general[CONF_INZONE_INTERVALS][NO_MOBAPP]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=5, max=480, step=5, unit_of_measurement='minutes')), + vol.Optional(OTHER, + default=Gb.conf_general[CONF_INZONE_INTERVALS][OTHER]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=5, max=480, step=5, unit_of_measurement='minutes')), + + vol.Optional('action_items', + default=self.action_default_text('save')): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# WAZE MAIN +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_waze_main(self): + self.actions_list = ACTION_LIST_ITEMS_BASE.copy() + + wuh_default = [WAZE_USED_HEADER] if Gb.conf_general[CONF_WAZE_USED] else [] + whuh_default = [WAZE_HISTORY_USED_HEADER] if Gb.conf_general[CONF_WAZE_HISTORY_DATABASE_USED] else [] + return vol.Schema({ + vol.Optional(CONF_WAZE_USED, + default=wuh_default): + cv.multi_select([WAZE_USED_HEADER]), + vol.Optional(CONF_WAZE_SERVER, + default=self._option_parm_to_text(CONF_WAZE_SERVER, WAZE_SERVER_OPTIONS)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(WAZE_SERVER_OPTIONS), mode='dropdown')), + vol.Optional(CONF_WAZE_MIN_DISTANCE, + default=Gb.conf_general[CONF_WAZE_MIN_DISTANCE]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=0, max=100, step=5, unit_of_measurement='km')), + vol.Optional(CONF_WAZE_MAX_DISTANCE, + default=Gb.conf_general[CONF_WAZE_MAX_DISTANCE]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=0, max=1000, step=5, unit_of_measurement='km')), + vol.Optional(CONF_WAZE_REALTIME, + default=Gb.conf_general[CONF_WAZE_REALTIME]): + selector.BooleanSelector(), + + vol.Required(CONF_WAZE_HISTORY_DATABASE_USED, + default=whuh_default): + cv.multi_select([WAZE_HISTORY_USED_HEADER]), + vol.Required(CONF_WAZE_HISTORY_MAX_DISTANCE, + default=Gb.conf_general[CONF_WAZE_HISTORY_MAX_DISTANCE]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=0, max=1000, step=5, unit_of_measurement='km')), + vol.Required(CONF_WAZE_HISTORY_TRACK_DIRECTION, + default=self._option_parm_to_text(CONF_WAZE_HISTORY_TRACK_DIRECTION, + WAZE_HISTORY_TRACK_DIRECTION_OPTIONS)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(WAZE_HISTORY_TRACK_DIRECTION_OPTIONS), mode='dropdown')), + + vol.Required('action_items', + default=self.action_default_text('save')): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + +#------------------------------------------------------------------------ +def form_special_zones(self): + self.actions_list = ACTION_LIST_ITEMS_BASE.copy() + + try: + pass_thru_zone_used = (Gb.conf_general[CONF_PASSTHRU_ZONE_TIME] > 0) + stat_zone_used = (Gb.conf_general[CONF_STAT_ZONE_STILL_TIME] > 0) + track_from_base_zone_used = Gb.conf_general[CONF_TRACK_FROM_BASE_ZONE_USED] + + ptzh_default = [PASSTHRU_ZONE_HEADER] if pass_thru_zone_used else [] + szh_default = [STAT_ZONE_HEADER] if stat_zone_used else [] + tfzh_default = [TRK_FROM_HOME_ZONE_HEADER] if track_from_base_zone_used else [] + + return vol.Schema({ + vol.Required('stat_zone_header', + default=szh_default): + cv.multi_select([STAT_ZONE_HEADER]), + vol.Required(CONF_STAT_ZONE_FNAME, + default=self._parm_or_error_msg(CONF_STAT_ZONE_FNAME)): + selector.TextSelector(), + vol.Required(CONF_STAT_ZONE_STILL_TIME, + default=Gb.conf_general[CONF_STAT_ZONE_STILL_TIME]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=0, max=60, unit_of_measurement='minutes')), + vol.Required(CONF_STAT_ZONE_INZONE_INTERVAL, + default=Gb.conf_general[CONF_STAT_ZONE_INZONE_INTERVAL]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=5, max=60, step=5, unit_of_measurement='minutes')), + + vol.Optional('passthru_zone_header', + default=ptzh_default): + cv.multi_select([PASSTHRU_ZONE_HEADER]), + vol.Required(CONF_PASSTHRU_ZONE_TIME, + default=Gb.conf_general[CONF_PASSTHRU_ZONE_TIME]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=0, max=5, step=.5, unit_of_measurement='minutes')), + + vol.Optional(CONF_TRACK_FROM_BASE_ZONE_USED, + default=tfzh_default): + cv.multi_select([TRK_FROM_HOME_ZONE_HEADER]), + vol.Required(CONF_TRACK_FROM_BASE_ZONE, + default=self._option_parm_to_text(CONF_TRACK_FROM_BASE_ZONE, self.zone_name_key_text)): + selector.SelectSelector(selector.SelectSelectorConfig( + options=dict_value_to_list(self.zone_name_key_text), mode='dropdown')), + vol.Optional(CONF_TRACK_FROM_HOME_ZONE, + default=Gb.conf_general[CONF_TRACK_FROM_HOME_ZONE]): + # cv.boolean, + selector.BooleanSelector(), + + vol.Required('action_items', + default=self.action_default_text('save')): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + except Exception as err: + log_exception(err) + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# SENSORS +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_sensors(self): + self.actions_list = [ACTION_LIST_OPTIONS['exclude_sensors']] + self.actions_list.extend(ACTION_LIST_ITEMS_BASE) + + if HOME_DISTANCE not in Gb.conf_sensors[CONF_SENSORS_TRACKING_DISTANCE]: + Gb.conf_sensors[CONF_SENSORS_TRACKING_DISTANCE].append(HOME_DISTANCE) + + return vol.Schema({ + vol.Required(CONF_SENSORS_DEVICE, + default=Gb.conf_sensors[CONF_SENSORS_DEVICE]): + cv.multi_select(CONF_SENSORS_DEVICE_KEY_TEXT), + vol.Required(CONF_SENSORS_TRACKING_UPDATE, + default=Gb.conf_sensors[CONF_SENSORS_TRACKING_UPDATE]): + cv.multi_select(CONF_SENSORS_TRACKING_UPDATE_KEY_TEXT), + vol.Required(CONF_SENSORS_TRACKING_TIME, + default=Gb.conf_sensors[CONF_SENSORS_TRACKING_TIME]): + cv.multi_select(CONF_SENSORS_TRACKING_TIME_KEY_TEXT), + vol.Required(CONF_SENSORS_TRACKING_DISTANCE, + default=Gb.conf_sensors[CONF_SENSORS_TRACKING_DISTANCE]): + cv.multi_select(CONF_SENSORS_TRACKING_DISTANCE_KEY_TEXT), + vol.Required(CONF_SENSORS_ZONE, + default=Gb.conf_sensors[CONF_SENSORS_ZONE]): + cv.multi_select(CONF_SENSORS_ZONE_KEY_TEXT), + vol.Required(CONF_SENSORS_OTHER, + default=Gb.conf_sensors[CONF_SENSORS_OTHER]): + cv.multi_select(CONF_SENSORS_OTHER_KEY_TEXT), + # vol.Required(CONF_SENSORS_TRACK_FROM_ZONES, + # default=Gb.conf_sensors[CONF_SENSORS_TRACK_FROM_ZONES]): + # cv.multi_select(CONF_SENSORS_TRACK_FROM_ZONES_KEY_TEXT), + vol.Required(CONF_SENSORS_MONITORED_DEVICES, + default=Gb.conf_sensors[CONF_SENSORS_MONITORED_DEVICES]): + cv.multi_select(CONF_SENSORS_MONITORED_DEVICES_KEY_TEXT), + vol.Required(CONF_SENSORS_TRACKING_OTHER, + default=Gb.conf_sensors[CONF_SENSORS_TRACKING_OTHER]): + cv.multi_select(CONF_SENSORS_TRACKING_OTHER_KEY_TEXT), + vol.Optional(CONF_EXCLUDED_SENSORS, + default=Gb.conf_sensors[CONF_EXCLUDED_SENSORS]): + selector.SelectSelector(selector.SelectSelectorConfig( + options=Gb.conf_sensors[CONF_EXCLUDED_SENSORS], mode='list', multiple=True)), + + vol.Required('action_items', + default=self.action_default_text('save')): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# EXCLUDE SENSORS +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_exclude_sensors(self): + self.actions_list = DEVICE_LIST_ACTIONS_EXCLUDE_SENSORS.copy() + + if self.sensors_list_filter == '?': + filtered_sensors_fname_list = [f"None Displayed - Enter a Filter or `all` \ + to display all sensors ({len(self.sensors_fname_list)} Sensors)"] + filtered_sensors_list_default = [] + else: + self.sensors_list_filter.replace('?', '') + if self.sensors_list_filter.lower() == 'all': + filtered_sensors_fname_list = [sensor_fname + for sensor_fname in self.sensors_fname_list + if sensor_fname not in self.excluded_sensors] + else: + filtered_sensors_fname_list = list(set([sensor_fname + for sensor_fname in self.sensors_fname_list + if ((instr(sensor_fname.lower(), self.sensors_list_filter) + and sensor_fname not in self.excluded_sensors))])) + + filtered_sensors_list_default = list(set([sensor_fname + for sensor_fname in filtered_sensors_fname_list + if sensor_fname in self.excluded_sensors])) + + filtered_sensors_fname_list.sort() + if filtered_sensors_fname_list == []: + filtered_sensors_fname_list = [f"No Sensors found containing \ + '{self.sensors_list_filter}'"] + + return vol.Schema({ + vol.Optional(CONF_EXCLUDED_SENSORS, + default=self.excluded_sensors): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.excluded_sensors, mode='list', multiple=True)), + vol.Optional('filter', + default=self.sensors_list_filter): + selector.TextSelector(), + vol.Optional('filtered_sensors', + default=filtered_sensors_list_default): + selector.SelectSelector(selector.SelectSelectorConfig( + options=filtered_sensors_fname_list, mode='list', multiple=True)), + + vol.Required('action_items', + default=self.action_default_text('filter_sensors')): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# RESTART HA IC3 +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def form_restart_ha_ic3(self): + + self.actions_list = [] + self.actions_list.append(ACTION_LIST_OPTIONS['restart_ha']) + self.actions_list.append(ACTION_LIST_OPTIONS['restart_icloud3']) + self.actions_list.append(ACTION_LIST_OPTIONS['cancel']) + actions_list_default = self.action_default_text('restart_HA') + + return vol.Schema({ + vol.Required('action_items', + default=actions_list_default): + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.actions_list, mode='list')), + }) diff --git a/custom_components/icloud3/const.py b/custom_components/icloud3/const.py index 440c8ae..15127e3 100644 --- a/custom_components/icloud3/const.py +++ b/custom_components/icloud3/const.py @@ -10,16 +10,17 @@ # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -VERSION = '3.0.5.9' +VERSION = '3.1' VERSION_BETA = '' #----------------------------------------- -DOMAIN = 'icloud3' ICLOUD3 = 'iCloud3' +DOMAIN = ICLOUD3.lower() +ICLOUD3_VERSION_MSG = f"{ICLOUD3} v{VERSION}{VERSION_BETA}" +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 MODE_PLATFORM = -1 MODE_INTEGRATION = 1 DEBUG_TRACE_CONTROL_FLAG = False -STORAGE_KEY = DOMAIN -STORAGE_VERSION = 1 HA_ENTITY_REGISTRY_FILE_NAME = 'config/.storage/core.entity_registry' ENTITY_REGISTRY_FILE_KEY = 'core.entity_registry' @@ -27,14 +28,14 @@ STORAGE_DIR = ".storage" STORAGE_KEY_ENTITY_REGISTRY = 'core.entity_registry' -SENSOR_EVENT_LOG_NAME = 'icloud3_event_log' -EVLOG_CARD_WWW_DIRECTORY = 'www/icloud3' +SENSOR_EVENT_LOG_NAME = f'{DOMAIN}_event_log' +EVLOG_CARD_WWW_DIRECTORY = f'www/{DOMAIN}' EVLOG_CARD_WWW_JS_PROG = 'icloud3-event-log-card.js' -EVLOG_BTNCONFIG_DEFAULT_URL = '/config/integrations/integration/icloud3' -HA_CONFIG_IC3_URL = '/config/integrations/integration/icloud3' +EVLOG_BTNCONFIG_DEFAULT_URL = f'/config/integrations/integration/{DOMAIN}' +HA_CONFIG_IC3_URL = f'/config/integrations/integration/{DOMAIN}' WAZE_LOCATION_HISTORY_DATABASE = 'icloud3.waze_location_history.db' SENSOR_WAZEHIST_TRACK_NAME = 'icloud3_wazehist_track' -IC3LOG_FILENAME = 'icloud3-0.log' +IC3LOG_FILENAME = f'{DOMAIN}-0.log' PICTURE_WWW_STANDARD_DIRS = 'www/icloud3, www/community, www/images, www/custom_cards' DEVICE_TRACKER = 'device_tracker' @@ -98,13 +99,15 @@ IPHONE = 'iphone' IPAD_FNAME = 'iPad' IPAD = 'ipad' +IMAC_FNAME = 'iMac' +IMAC = 'imac' IPOD_FNAME = 'iPod' IPOD = 'ipod' WATCH_FNAME = 'Watch' WATCH = 'watch' AIRPODS_FNAME = 'AirPods' AIRPODS = 'airpods' -ICLOUD_FNAME = 'iCloud' +ICLOUD = 'iCloud' ICLOUD = 'icloud' OTHER_FNAME = 'Other' OTHER = 'other' @@ -114,23 +117,26 @@ APPLE_SPECIAL_ICLOUD_SERVER_COUNTRY_CODE = ['cn', 'CN'] DEVICE_TYPES = [ - IPHONE, IPAD, IPOD, WATCH, ICLOUD_FNAME, AIRPODS, - IPHONE_FNAME, IPAD_FNAME, IPOD_FNAME, WATCH_FNAME, ICLOUD_FNAME, AIRPODS_FNAME, + IPHONE, IPAD, WATCH, AIRPODS, IMAC, IPOD, ICLOUD, + IPHONE_FNAME, IPAD_FNAME, WATCH_FNAME, AIRPODS_FNAME, + IMAC_FNAME, IPOD_FNAME, ICLOUD, ] DEVICE_TYPE_FNAME = { IPHONE: IPHONE_FNAME, IPAD: IPAD_FNAME, WATCH: WATCH_FNAME, AIRPODS: AIRPODS_FNAME, + IMAC: IMAC_FNAME, IPOD: IPOD_FNAME, OTHER: OTHER_FNAME, } DEVICE_TYPE_ICONS = { IPHONE: "mdi:cellphone", IPAD: "mdi:tablet", - IPOD: "mdi:ipod", - AIRPODS: "mdi:earbuds-outline", WATCH: "mdi:watch-variant", + AIRPODS: "mdi:earbuds-outline", + IMAC : "mdi:laptop", + IPOD: "mdi:ipod", OTHER: 'mdi:laptop' } @@ -258,10 +264,13 @@ lite_circled_letters = "Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ" dark_circled_letters = "🅐 🅑 🅒 🅓 🅔 🅕 🅖 🅗 🅘 🅙 🅚 🅛 🅜 🅝 🅞 🅟 🅠 🅡 🅢 🅣 🅤 🅥 🅦 🅧 🅨 🅩 ✪" Symbols = ±▪•●▬⮾ ⊗ ⊘✓×ø¦ ▶◀ ►◄▲▼ ∙▪ »« oPhone=►▶→⟾➤➟➜➔➤🡆🡪🡺⟹🡆➔ᐅ◈🝱☒☢⛒⊘Ɵ⊗ⓧⓍ⛒🜔 -Important =✔️❗❌✨➰⚠️❓⚽🛑⛔⚡⭐⭕ⓘ• ⍰ ‶″“”‘’‶″ 🕓 - — –ᗒ ⁃ » ━▶ ━➤🡺 —> > > ❯↦ … ⋮ 🡪ᗕ ᗒ ᐳ ─🡢 ──ᗒ 🡢 ─ᐅ ↣ ➙ →《》◆◈◉● - ▐‖ ▹▻▷◁◅◃‖╠ᐅ🡆▶▐🡆▐▶‖➤▐➤➜➔❰❰❱❱ ⠤ ² - ⣇⠈⠉⠋⠛⠟⠿⡿⣿ https://www.fileformat.info/info/unicode/block/braille_patterns/utf8test.htm +Important =✔️❗❌✨➰⚠️❓⚽🛑⛔⚡⭐⭕ⓘ• ⍰ ‶″“”‘’‶″ 🕓 🔻🔺✔✅❎☑️☁️🍎🔻⏭️⏮️🍏🅰️⮽➕ +↺↻⟲⟳⭯⭮↺↻⥀⥁↶↷⮌⮍⮎⮏⤻⤸⤾⤿⤺⤼⤽⤹🗘⮔⤶⤷⃕⟳↻🔄🔁➡️🔃⬇️ + — –ᗒ⋮… ⁃ » ━▶ ━➤🡺 —> > ❯↦ … ⋮ 🡪ᗕᗒ ᐳ ─🡢 ⎯ ━ ──ᗒ 🡢 ─ᐅ ↣ ➙ →《》◆◈◉● ⟷•⟛⚯⧟⫗' '᚛᚜ 〉〈 ⦒⦑ ⟩⟨ ≻≺ ⸩⸨ + ▐‖ ▹▻◁─▷◅◃‖╠ᐅ🡆▶▐🡆▐▶‖➤▐➤➜➔❰❰❱❱ ⠤ … ² ⚯⟗⟐⥄⥵⧴⧕⫘⧉⯏≷≶≳≲≪≫⋘⋙ ∮∯ ❪❫❴❵❮❯❰❱ + ⣇⠈⠉⠋⠛⠟⠿⡿⣿ ⠗⠺ ⠿ ⸩⸨ + https://www.fileformat.info/info/unicode/block/braille_patterns/utf8test.htm + https://www.htmlsymbols.xyz/unit-symbols ''' NBSP = '⠈' #' ' NBSP2 = '⠉' #'  ' @@ -271,7 +280,12 @@ NBSP6 = '⠿' #'      ' CRLF = '⣇' #'
' NL = '\n' +#LINK = ' ⟛ ' +LLINK = '⸨' +RLINK = '⸩' +LINK = '-⸨' CLOCK_FACE = '🕓' +INFO = '🛈' CHECK_MARK = '✓ ' RED_X = '❌' YELLOW_ALERT = '⚠️' @@ -293,6 +307,7 @@ GT = '>' LTE = '≤' GTE = '≥' +DOTS = '…' PLUS_MINUS = '±' LDOT2 = f'•{NBSP2}' CRLF_DOT = f'{CRLF}{NBSP3}•{NBSP2}' @@ -304,14 +319,15 @@ CRLF_CHK = f'{CRLF}{NBSP3}✓{NBSP}' CRLF_STAR = f'{CRLF}{NBSP3}✪{NBSP}' CRLF_RED_X = f'{CRLF}❌' +CRLF_YELLOW_ALERT = f'{CRLF}⚠️{NBSP}' CRLF_CIRCLE_X = f'{CRLF}{NBSP2}ⓧ{NBSP}' CRLF_SP3_DOT = f'{CRLF}{NBSP3}•{NBSP}' CRLF_SP5_DOT = f'{CRLF}{NBSP5}•{NBSP}' CRLF_SP8_DOT = f'{CRLF}{NBSP4}{NBSP4}•{NBSP}' CRLF_SP8_HDOT = f'{CRLF}{NBSP4}{NBSP4}◦{NBSP}' -CRLF_SP3_HDOT = f'{CRLF}{NBSP3}◦{NBSP}' +CRLF_SP3_HDOT = f'{CRLF}{NBSP3}◦{NBSP2}' CRLF_SP3_STAR = f'{CRLF}{NBSP3}✪{NBSP}' -CRLF_TAB = f'{CRLF}{NBSP6}' +CRLF_TAB = f'{CRLF}{NBSP4}{NBSP4}{NBSP4}' CRLF_INDENT = f'{CRLF}{NBSP6}{NBSP6}' CRLF_DASH_75 = f'{CRLF}{"-"*75}' @@ -332,21 +348,12 @@ OPT_NONE = 0 #tracking_method config parameter being used -ICLOUD = 'icloud' #iCloud Location Services (FmF & FamShr) -ICLOUD_FNAME = 'iCloud' -FMF = 'fmf' #Find My Friends -FAMSHR = 'famshr' #Family Sharing +ICLOUD = 'iCloud' #iCloud Location Services +FAMSHR = 'iCloud' #Family Sharing IOSAPP = 'iosapp' -MOBAPP = 'mobapp' #HA Mobile App v1.5x or v2.x -MOBAPP_FNAME = 'MobApp' +MOBAPP = 'MobApp' #HA Mobile App v1.5x or v2.x NO_MOBAPP = 'no_mobapp' NO_IOSAPP = 'no_iosapp' -FMF_FNAME = 'FmF' -FAMSHR_FNAME = 'FamShr' -FAMSHR_FMF = 'famshr_fmf' -FAMSHR_FMF_FNAME = 'FamShr-FmF' -DATA_SOURCE_FNAME = {FMF: FMF_FNAME, FAMSHR: FAMSHR_FNAME, FAMSHR_FMF: FAMSHR_FMF_FNAME, - MOBAPP: MOBAPP_FNAME, ICLOUD: ICLOUD_FNAME} # Device tracking modes TRACK_DEVICE = 'track' @@ -390,12 +397,11 @@ STATIONARY_FNAME: STATIONARY_FNAME, } -TRK_METHOD_SHORT_NAME = { - FMF: FMF_FNAME, - FAMSHR: FAMSHR_FNAME, - MOBAPP: MOBAPP_FNAME, } +# TRK_METHOD_SHORT_NAME = { +# ICLOUD: ICLOUD, +# MOBAPP: MOBAPP, } -# Standardize the battery status text between the Mobile App and icloud famshr +# Standardize the battery status text between the Mobile App and icloud icloud BATTERY_STATUS_CODES = { 'full': 'not charging', 'charged': 'not charging', @@ -492,7 +498,7 @@ CONF_EXCLUDE_SENSORS = 'exclude_sensors' CONF_CONFIG_IC3_FILE_NAME = 'config_ic3_file_name' -# entity attributes (iCloud FmF & FamShr) +# entity attributes (iCloud FmF & iCloud) ICLOUD_TIMESTAMP = 'timeStamp' ICLOUD_HORIZONTAL_ACCURACY = 'horizontalAccuracy' ICLOUD_VERTICAL_ACCURACY = 'verticalAccuracy' @@ -538,7 +544,7 @@ BATTERY_SOURCE = 'battery_data_source' BATTERY_LEVEL = 'battery_level' BATTERY_UPDATE_TIME = 'battery_level_updated' -BATTERY_FAMSHR = 'famshr_battery_info' +BATTERY_ICLOUD = 'icloud_battery_info' BATTERY_MOBAPP = 'mobapp_battery_info' BATTERY_LATEST = 'battery_info' WAZE_METHOD = 'waze_method' @@ -588,9 +594,12 @@ '0': 'Unknown', } BATTERY_LEVEL_LOW = 20 -DEVICE_STATUS_ONLINE = ['Online', 'Pending', 'Unknown', 'unknown', ''] -DEVICE_STATUS_OFFLINE = ['Offline'] -DEVICE_STATUS_PENDING = ['Pending'] +DEVICE_STATUS_ONLINE = [200, 203, 204, 0] +DEVICE_STATUS_OFFLINE = 201 +DEVICE_STATUS_PENDING = 203 +# DEVICE_STATUS_ONLINE = ['Online', 'Pending', 'Unknown', 'unknown', ''] +# DEVICE_STATUS_OFFLINE = ['Offline'] +# DEVICE_STATUS_PENDING = ['Pending'] #<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> ICLOUD3_EVENT_LOG = 'icloud3_event_log' @@ -603,7 +612,6 @@ # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -# to store the cookie STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 @@ -622,6 +630,9 @@ # Account, Devices, Tracking Parameters CONF_USERNAME = 'username' CONF_PASSWORD = 'password' +CONF_TOTP_KEY = 'totp_key' +CONF_LOCATE_ALL = 'locate_all' +CONF_APPLE_ACCOUNTS = 'apple_accounts' CONF_DEVICES = 'devices' CONF_DATA_SOURCE = 'data_source' CONF_VERIFICATION_CODE = 'verification_code' @@ -692,21 +703,19 @@ # Devices Parameters CONF_IC3_DEVICENAME = 'ic3_devicename' CONF_FNAME = 'fname' +CONF_APPLE_ACCOUNT = 'apple_account' +CONF_APPLE_ACCT = 'apple_account' +CONF_ICLOUD_DEVICENAME = 'famshr_devicename' +CONF_ICLOUD_DEVICE_ID = 'famshr_device_id' CONF_FAMSHR_DEVICENAME = 'famshr_devicename' CONF_FAMSHR_DEVICE_ID = 'famshr_device_id' CONF_RAW_MODEL = 'raw_model' CONF_MODEL = 'model' CONF_MODEL_DISPLAY_NAME = 'model_display_name' -CONF_FAMSHR_DEVICENAME2 = 'famshr_devicename2' -CONF_FAMSHR_DEVICE_ID2 = 'famshr_device_id2' -CONF_RAW_MODEL2 = 'raw_model2' -CONF_MODEL2 = 'model2' -CONF_MODEL_DISPLAY_NAME2 = 'model_display_name2' CONF_FMF_EMAIL = 'fmf_email' CONF_FMF_DEVICE_ID = 'fmf_device_id' CONF_IOSAPP_DEVICE = 'iosapp_device' CONF_MOBILE_APP_DEVICE = 'mobile_app_device' -CONF_MOBILE_APP_DEVICE2 = 'mobapp_device2' CONF_PICTURE = 'picture' CONF_TRACKING_MODE = 'tracking_mode' CONF_TRACK_FROM_BASE_ZONE_USED = 'track_from_base_zone_used' # Primary Zone a device is tracking from, normally Home @@ -810,10 +819,11 @@ CF_PROFILE = 'profile' CF_DATA = 'data' -CF_DATA_TRACKING = 'tracking' +CF_TRACKING = 'tracking' CF_DATA_DEVICES = 'devices' -CF_DATA_GENERAL = 'general' -CF_DATA_SENSORS = 'sensors' +CF_DATA_APPLE_ACCOUNTS = 'apple_accounts' +CF_GENERAL = 'general' +CF_SENSORS = 'sensors' #-------------------------------------------------------- DEFAULT_PROFILE_CONF = { @@ -829,13 +839,21 @@ CONF_PICTURE_WWW_DIRS: [] } +DEFAULT_APPLE_ACCOUNTS_CONF = { + CONF_USERNAME: '', + CONF_PASSWORD: '', + CONF_TOTP_KEY: '', + CONF_LOCATE_ALL: True, +} + DEFAULT_TRACKING_CONF = { CONF_USERNAME: '', CONF_PASSWORD: '', + CONF_APPLE_ACCOUNTS: [DEFAULT_APPLE_ACCOUNTS_CONF], CONF_ENCODE_PASSWORD: True, CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX: '', CONF_SETUP_ICLOUD_SESSION_EARLY: True, - CONF_DATA_SOURCE: f'{FAMSHR},{MOBAPP}', + CONF_DATA_SOURCE: f'{ICLOUD},{MOBAPP}', CONF_DEVICES: [], } @@ -849,6 +867,7 @@ CONF_INZONE_INTERVAL: 120, CONF_FIXED_INTERVAL: 0, CONF_TRACKING_MODE: TRACK_DEVICE, + CONF_APPLE_ACCOUNT: '', CONF_FAMSHR_DEVICENAME: 'None', CONF_FAMSHR_DEVICE_ID: '', CONF_RAW_MODEL : '', @@ -909,6 +928,7 @@ IPHONE: 120, IPAD: 120, WATCH: 15, + IMAC: 120, AIRPODS: 15, NO_MOBAPP: 15, OTHER: 120, @@ -1004,17 +1024,17 @@ } DEFAULT_DATA_CONF = { - CF_DATA_TRACKING: DEFAULT_TRACKING_CONF, - CF_DATA_GENERAL: DEFAULT_GENERAL_CONF, - CF_DATA_SENSORS: DEFAULT_SENSORS_CONF, + CF_TRACKING: DEFAULT_TRACKING_CONF, + CF_GENERAL: DEFAULT_GENERAL_CONF, + CF_SENSORS: DEFAULT_SENSORS_CONF, } CF_DEFAULT_IC3_CONF_FILE = { CF_PROFILE: DEFAULT_PROFILE_CONF, CF_DATA: { - CF_DATA_TRACKING: DEFAULT_TRACKING_CONF, - CF_DATA_GENERAL: DEFAULT_GENERAL_CONF, - CF_DATA_SENSORS: DEFAULT_SENSORS_CONF, + CF_TRACKING: DEFAULT_TRACKING_CONF, + CF_GENERAL: DEFAULT_GENERAL_CONF, + CF_SENSORS: DEFAULT_SENSORS_CONF, } } @@ -1032,6 +1052,7 @@ CONF_OLD_LOCATION_ADJUSTMENT, IPHONE, IPAD, + IMAC, WATCH, AIRPODS, NO_MOBAPP, @@ -1044,7 +1065,7 @@ CONF_STAT_ZONE_BASE_LONGITUDE, ] -CONF_ALL_FAMSHR_DEVICES = "all_famshr_devices" +CONF_ALL_FAMSHR_DEVICES = "all_find_devices" DEFAULT_ALL_FAMSHR_DEVICES = True # .storage/icloud3.restore_state file used to resore the device_trackers @@ -1107,7 +1128,7 @@ ICLOUD_VERTICAL_ACCURACY: 0, 'positionType': 'Wifi', } -FMF_FAMSHR_LOCATION_FIELDS = [ +FAMSHR_LOCATION_FIELDS = [ ALTITUDE, LATITUDE, LONGITUDE, diff --git a/custom_components/icloud3/const_config_flow.py b/custom_components/icloud3/const_config_flow.py new file mode 100644 index 0000000..2fc0b8a --- /dev/null +++ b/custom_components/icloud3/const_config_flow.py @@ -0,0 +1,391 @@ + +from .global_variables import GlobalVariables as Gb +from .const import (NAME, BATTERY, WAZE_SERVERS_FNAME, ) + +#---------------------------------------------------------------------------------------- +MENU_PAGE_0_INITIAL_ITEM = 1 +MENU_PAGE_TITLE = [ + 'Menu > Configure Devices and Sensor Menu', + 'Menu > Configure Parameters Menu' + ] +MENU_KEY_TEXT = { + 'data_source': 'DATA SOURCES > APPLE ACCOUNT & MOBILE APP ᐳ Select Location Data Sources; Apple Account Username/Password', + 'device_list': 'ICLOUD3 DEVICES ᐳ Add, Change and Delete Tracked and Monitored Devices', + 'verification_code': 'ENTER/REQUEST AN APPLE ACCOUNT VERIFICATION CODE ᐳ Enter or Request the 6-digit Apple Account Verification Code', + 'away_time_zone': 'AWAY TIME ZONE ᐳ Select the displayed time zone for devices away from Home', + 'change_device_order': 'CHANGE DEVICE ORDER ᐳ Change the Event Log Device display and tracking update sequence', + 'sensors': 'SENSORS ᐳ Set Sensors created by iCloud3; Exclude Specific Sensors from being created', + 'actions': 'ACTION COMMANDS ᐳ Restart/Pause/Resume Polling; Debug Logging; Export Event Log; Waze Utilities', + + 'format_settings': 'FORMAT SETTINGS ᐳ Log Level; Zone Display Format; Device Tracker State; Unit of Measure; Time & Distance, Display GPS Coordinates', + 'display_text_as': 'DISPLAY TEXT AS ᐳ Event Log Text Replacement, etc', + 'waze': 'WAZE ROUTE DISTANCE, TIME & HISTORY ᐳ Route Server and Parameters; Waze History Database Parameters and Controls', + 'inzone_intervals': 'INZONE INTERVALS ᐳ inZone Interval assigned to new devices', + 'special_zones': 'SPECIAL ZONES ᐳ Enter Zone Delay Time; Stationary Zone; Primary Track-from-Home Zone Override', + 'tracking_parameters': 'TRACKING & OTHER PARAMETERS ᐳ Set Nearby Device Info, Accuracy Thresholds & Other Location Request Intervals; Picture Image Directories; Event Log Custom Card Directory', + + 'select': 'SELECT ᐳ Select the parameter update form', + 'next_page_0': f'{MENU_PAGE_TITLE[0].upper()} ᐳ iCloud Account & Mobile App; iCloud3 Devices; Enter & Request Verification Code; Change Device Order; Sensors; Action Commands', + 'next_page_1': f'{MENU_PAGE_TITLE[1].upper()} ᐳ Format Parameters; Display Text As; Waze Route Distance, Time & History; inZone Intervals; Special Zones; Other Parameters', + 'exit': f'EXIT AND RESTART/RELOAD ICLOUD3 (Current version is v{Gb.version})' +} + +MENU_KEY_TEXT_PAGE_0 = [ + MENU_KEY_TEXT['data_source'], + MENU_KEY_TEXT['device_list'], + MENU_KEY_TEXT['verification_code'], + MENU_KEY_TEXT['away_time_zone'], + MENU_KEY_TEXT['sensors'], + MENU_KEY_TEXT['actions'], + ] +MENU_PAGE_1_INITIAL_ITEM = 0 +MENU_KEY_TEXT_PAGE_1 = [ + MENU_KEY_TEXT['format_settings'], + MENU_KEY_TEXT['display_text_as'], + MENU_KEY_TEXT['waze'], + MENU_KEY_TEXT['special_zones'], + MENU_KEY_TEXT['tracking_parameters'], + MENU_KEY_TEXT['inzone_intervals'], + ] +MENU_ACTION_ITEMS = [ + MENU_KEY_TEXT['select'], + MENU_KEY_TEXT['next_page_1'], + MENU_KEY_TEXT['exit'] + ] + +ACTION_LIST_OPTIONS = { + 'next_page_items': 'NEXT PAGE ITEMS ᐳ ^info_field^', + 'next_page': 'NEXT PAGE ᐳ Save changes. Display the next page', + 'next_page_device': 'NEXT PAGE ᐳ Friendly Name, Track-from-Zones, Other Setup Fields', + 'next_page_waze': 'NEXT PAGE ᐳ Waze History Database parameters', + 'select_form': 'SELECT ᐳ Select the parameter update form', + + 'update_apple_acct': 'SELECT APPLE ACCOUNT ᐳ Update the Username/Password of the selected Apple Account, Add a new Apple Account, Remove the Apple Account', + 'log_into_apple_acct': 'SAVE CHANGES & LOG INTO APPLE ACCT ᐳ Log into the Apple Account, Save any configuration changes', + 'stop_using_apple_acct': 'STOP USING AN APPLE ACCOUNT ᐳ Stop using an Apple Account, Remove it from the Apple Accounts list and all devices using it', + 'verification_code': 'ENTER/REQUEST AN APPLE ACCOUNT VERIFICATION CODE ᐳ Enter (or Request) the 6-digit Apple Account Verification Code', + + 'delete_apple_acct': 'DELETE APPLE ACCOUNT ᐳ Delete the Apple Account. It will no longer be used as a data source', + + 'send_verification_code': 'SEND THE VERIFICATION CODE TO APPLE ᐳ Send the 6-digit Apple Account Verification Code back to Apple to approve access to Apple Account', + "request_verification_code":'REQUEST A NEW APPLE ACCOUNT VERIFICATION CODE ᐳ Reset Apple Account Interface and request a new Apple Account Verification Code', + 'cancel_verification_entry':'CANCEL ᐳ Cancel the Verification Code Entry and Close this screen', + + 'update_device': 'SELECT THE DEVICE ᐳ Update the selected device, Add a new device to be tracked by iCloud3, Display more Devices on the next page', + 'add_device': 'ADD DEVICE ᐳ Add a device to be tracked by iCloud3', + 'delete_device': 'DELETE DEVICE(S), OTHER DEVICE MAINTENANCE ᐳ Delete the device(s) from the tracked device list, clear the iCloud/Mobile App selection fields', + 'change_device_order': 'CHANGE DEVICE ORDER ᐳ Change the tracking order of the Devices and their display sequence on the Event Log', + + 'delete_this_device': 'DELETE THIS DEVICE ᐳ Delete this device', + 'delete_all_devices': 'DELETE ALL DEVICES ᐳ Delete all devices from the iCloud3 tracked devices list', + 'delete_icloud_mobapp_info':'CLEAR ICLOUDR/MOBAPP INFO ᐳ Reset the iCloud/Mobile App seletion fields on all devices', + 'delete_device_cancel': 'CANCEL ᐳ Return to the Device List screen', + + 'inactive_to_track': 'TRACK ALL OR SELECTED ᐳ Change the `Tracking Mode‘ of all of the devices (or the selected devices) from `Inactive‘ to `Tracked‘', + 'inactive_keep_inactive': 'DO NOT TRACK, KEEP INACTIVE ᐳ None of these devices should be `Tracked‘ and should remain `Inactive‘', + + 'restart_ha': 'RESTART HOME ASSISTANT ᐳ Restart HA, Restart iCloud3', + 'restart_icloud3': 'RESTART ICLOUD3 ᐳ Restart iCloud3 (Does not restart Home Assistant)', + 'restart_ic3_now': 'RESTART NOW ᐳ Restart iCloud3 now to load the updated configuration', + 'restart_ic3_later': 'RESTART LATER ᐳ The configuration changes have been saved. Load the updated configuration the next time iCloud3 is started', + 'review_inactive_devices': 'REVIEW INACTIVE DEVICES ᐳ Some Devices are `Inactive` and will not be located or tracked', + + 'select_text_as': 'SELECT ᐳ Update selected `Display Text As‘ field', + 'clear_text_as': 'CLEAR ᐳ Remove `Display Text As‘ entry', + + 'exclude_sensors': 'EXCLUDE SENSORS ᐳ Select specific Sensors that should not be created', + 'filter_sensors': 'FILTER SENSORS ᐳ Select Sensors that should be displayed', + + 'move_up': 'MOVE UP ᐳ Move the Device up in the list', + 'move_down': 'MOVE DOWN ᐳ Move the Device down in the list', + + 'save': 'SAVE ᐳ Update Configuration File, Return to the Menu screen', + 'save_stay': 'SAVE ᐳ Update Configuration File', + 'return': 'MENU ᐳ Return to the Menu screen', + + 'cancel_return': 'RETURN ᐳ Return to the previous screen. Cancel any unsaved changes', + 'cancel': 'MENU ᐳ Return to the Menu screen. Cancel any unsaved changes', + 'cancel_device_selection': 'BACK TO DEVICE SELECTION ᐳ Return to the Device Selection screen. Cancel any unsaved changes', + 'exit': 'EXIT ᐳ Exit the iCloud3 Configurator', + + 'confirm_return': 'NO, RETURN WITHOUT DOING ANYTHING ᐳ Cancel the request and return to the previous screen', + 'confirm_save': 'SAVE THE CONFIGURATION CHANGES ᐳ Save any changes, then return to the Main Menu', + 'confirm_action': 'YES, PERFORM THE REQUESTED ACTION ᐳ Complete the requested action and return to the previous screen', + + "divider1": "═══════════════════════════════════════", + "divider2": "═══════════════════════════════════════", + "divider3": "═══════════════════════════════════════" + } + +ACTION_LIST_ITEMS_KEY_BY_TEXT = {text: key for key, text in ACTION_LIST_OPTIONS.items()} + +ACTION_LIST_ITEMS_BASE = [ + ACTION_LIST_OPTIONS['save'], + ACTION_LIST_OPTIONS['cancel'] + ] + +NONE_DICT_KEY_TEXT = {'None': 'None'} +NONE_FAMSHR_DICT_KEY_TEXT = {'None': 'None - Not using the Apple Acct iCloud Location Service'} +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 = 1 # Index number of the Action list item containing the username/password +APPLE_ACCOUNT_USERNAME_ACTION_LIST_IDX = 0 # Index number of the Action list item containing the username/password +APPLE_ACCOUNTS_MULTI_HDR = {'apple_acct_hdr': '═════════ Additional Apple Accounts ═════════'} +ADD = UNSELECTED = -1 + +# Action List Items for all screens +ICLOUD_ACCOUNT_ACTIONS = [ + ACTION_LIST_OPTIONS['update_apple_acct']] +DELETE_APPLE_ACCT_ACTIONS = [ + ACTION_LIST_OPTIONS['delete_apple_acct'], + ACTION_LIST_OPTIONS['cancel_return']] +USERNAME_PASSWORD_ACTIONS = [ + ACTION_LIST_OPTIONS['log_into_apple_acct'], + ACTION_LIST_OPTIONS['stop_using_apple_acct'], + ACTION_LIST_OPTIONS['verification_code'], + ACTION_LIST_OPTIONS['cancel_return']] +REAUTH_CONFIG_FLOW_ACTIONS = [ + ACTION_LIST_OPTIONS['send_verification_code'], + ACTION_LIST_OPTIONS['request_verification_code'], + ACTION_LIST_OPTIONS['cancel_verification_entry']] +REAUTH_ACTIONS = [ + ACTION_LIST_OPTIONS['send_verification_code'], + ACTION_LIST_OPTIONS['request_verification_code'], + ACTION_LIST_OPTIONS['cancel_return']] +DEVICE_LIST_ACTIONS = [ + ACTION_LIST_OPTIONS['update_device'], + ACTION_LIST_OPTIONS['delete_device'], + ACTION_LIST_OPTIONS['change_device_order'], + ACTION_LIST_OPTIONS['return']] +DEVICE_LIST_ACTIONS_ADD = [ + ACTION_LIST_OPTIONS['add_device'], + ACTION_LIST_OPTIONS['return']] +DEVICE_LIST_ACTIONS_EXCLUDE_SENSORS = [ + ACTION_LIST_OPTIONS['filter_sensors'], + ACTION_LIST_OPTIONS['save_stay'], + ACTION_LIST_OPTIONS['cancel_return']] +DEVICE_LIST_ACTIONS_NO_ADD = [ + ACTION_LIST_OPTIONS['update_device'], + ACTION_LIST_OPTIONS['delete_device'], + ACTION_LIST_OPTIONS['change_device_order'], + ACTION_LIST_OPTIONS['return']] +DELETE_DEVICE_ACTIONS = [ + ACTION_LIST_OPTIONS['delete_this_device'], + ACTION_LIST_OPTIONS['delete_all_devices'], + ACTION_LIST_OPTIONS['delete_icloud_mobapp_info'], + ACTION_LIST_OPTIONS['delete_device_cancel']] +REVIEW_INACTIVE_DEVICES = [ + ACTION_LIST_OPTIONS['inactive_to_track'], + ACTION_LIST_OPTIONS['inactive_keep_inactive']] +RESTART_NOW_LATER_ACTIONS = [ + ACTION_LIST_OPTIONS['restart_ha'], + ACTION_LIST_OPTIONS['restart_icloud3'], + ACTION_LIST_OPTIONS['restart_ic3_now'], + ACTION_LIST_OPTIONS['restart_ic3_later'], + ACTION_LIST_OPTIONS['review_inactive_devices']] +CONFIRM_ACTIONS = [ + ACTION_LIST_OPTIONS['confirm_action'], + ACTION_LIST_OPTIONS['confirm_return']] + + +# Parameter List Selections Items +DATA_SOURCE_OPTIONS = { + 'iCloud': 'APPLE ACCOUNT - Location data is provided for devices in the Family Sharing List', + 'MobApp': 'HA MOBILE APP - Location data and zone enter/exit triggers from devices with the Mobile App' + } +DELETE_APPLE_ACCT_DEVICE_ACTION_OPTIONS = { + 'reassign_devices': 'REASSIGN DEVICES ᐳ Search for another Apple Account with this device device and reassign it to that Apple Account. Set it to Inactive if one is not found', + 'delete_devices': 'DELETE DEVICES ᐳ Delete all devices that are using this Apple Account', + 'set_devices_inactive': 'SET DEVICES TO INACTIVE ᐳ Set the devices using this Apple Account to Inactive. They will be assigned to another Apple Account later' + } +ICLOUD_SERVER_ENDPOINT_SUFFIX_OPTIONS = { + 'none': 'Use normal Apple iCloud Servers', + 'cn': 'China - Use Apple iCloud Servers located in China' + } +MOBAPP_DEVICE_NONE_OPTIONS = {'None': 'None - The Mobile App is not installed on this device'} +LOG_ZONES_KEY_TEXT = { + 'name-zone': ' → [year]-[zone].csv', + 'name-device': ' → [year]-[device].csv', + 'name-device-zone': ' → [year]-[device]-[zone].csv', + 'name-zone-device': ' → [year]-[zone]-[device].csv', + } +TRACKING_MODE_OPTIONS = { + 'track': 'Track - Request Location and track the device', + 'monitor': 'Monitor - Report location only when another tracked device is updated', + 'inactive': 'INACTIVE - Device is inactive and will not be tracked' + } +UNIT_OF_MEASUREMENT_OPTIONS = { + 'mi': 'Imperial (mi, ft)', + 'km': 'Metric (km, m)' + } +TIME_FORMAT_OPTIONS = { + '12-hour': '12-hour Time Format (9:05:30a, 4:40:15p)', + '24-hour': '24-hour Time Format (09:05:30, 16:40:15)' + } +TRAVEL_TIME_INTERVAL_MULTIPLIER_KEY_TEXT = { + .25: 'Shortest Interval Time - 1/4 TravelTime (¼ × 8 mins = Next Locate in 2m)', + .33: 'Shorter Interval Time - 1/3 TravelTime (⅓ × 8 mins = Next Locate in 2m40s)', + .50: 'Half Way (Default) - 1/2 TravelTime (½ × 8 mins = Next Locate in 4m)', + .66: 'Longer Interval Time - 2/3 TravelTime (⅔ × 8 mins = Next Locate in 5m20s', + .75: 'Longest Interval Time - 3/4 TravelTime (¾ × 8 mins = Next Locate in 6m)' + } +DISPLAY_ZONE_FORMAT_OPTIONS = {} +DISPLAY_ZONE_FORMAT_OPTIONS_BASE = { + 'fname': 'HA Zone Friendly Name (Home, Away, TheShores) →→→ PREFERRED', + 'zone': 'HA Zone entity_id (home, not_home, the_shores)', + 'name': 'iCloud3 reformated Zone entity_id (zone.the_shores → TheShores)', + 'title': 'iCloud3 reformated Zone entity_id (zone.the_shores → The Shores)' + } +DEVICE_TRACKER_STATE_SOURCE_OPTIONS = { + 'ic3_evlog': 'iCloud3 Zone - Use EvLog Zone Display Name (gps & accuracy) →→→ PREFERRED', + 'ic3_fname': 'iCloud3 Zone - Use Zone Friendly Name (gps & accuracy)', + 'ha_gps': 'HA Zone - Use gps coordinates to determine the zone (except Stationary Zones)' +} +LOG_LEVEL_OPTIONS = { + 'info': 'Info - Log General Information and Event Log messages', + 'debug': 'Debug - Info + Other Internal Tracking Monitors', + 'debug-ha': 'Debug (HALog) - Also add log records to the `home-assistant.log` file', + 'debug-auto-reset': 'Debug (AutoReset) - Debug logging that resets to Info at midnight', + 'rawdata': 'Rawdata - Debug + Raw Data (filtered) received from iCloud Location Servers', + 'rawdata-auto-reset': 'Rawdata (AutoReset) - RawData logging that resets to Info at midnight', + 'unfiltered': 'Rawdata (Unfiltered) - Raw Data (everything) received from iCloud Location Servers', + } +DISTANCE_METHOD_OPTIONS = { + 'waze': 'Waze - Waze Route Service provides travel time & distance information', + 'calc': 'Calc - Distance is calculated using a `straight line` formula' + } +WAZE_SERVER_OPTIONS = { + 'us': WAZE_SERVERS_FNAME['us'], + 'il': WAZE_SERVERS_FNAME['il'], + 'row': WAZE_SERVERS_FNAME['row'] + } +WAZE_HISTORY_TRACK_DIRECTION_OPTIONS = { + 'north_south': 'North-South - You generally travel in North-to-South direction', + 'east_west': 'East-West - You generally travel in East-West direction' + } + +CONF_SENSORS_MONITORED_DEVICES_KEY_TEXT = { + 'md_badge': '_badge ᐳ Badge sensor - A badge showing the Zone Name or distance from the Home zone. Attributes include location related information', + 'md_battery': '_battery, battery_status ᐳ Create Battery (65%) and Battery Status (Charging, Low, etc) sensors', + 'md_location_sensors': 'Location related sensors ᐳ Name, zone, distance, travel_time, etc. (_name, _zone, _zone_fname, _zone_name, _zone_datetime, _home_distance, _travel_time, _travel_time_min, _last_located, _last_update)', + } +CONF_SENSORS_DEVICE_KEY_TEXT = { + NAME: '_name ᐳ iCloud3 Device Name', + 'badge': '_badge ᐳ A badge showing the Zone Name or distance from the Home zone', + BATTERY: '_battery, _battery_status ᐳ Create Battery Level (65%) and Battery Status (Charging, Low, etc) sensors', + 'info': '_info ᐳ An information message containing status, alerts and errors related to device location updates, data accuracy, etc', + } +CONF_SENSORS_TRACKING_UPDATE_KEY_TEXT = { + 'interval': '_interval ᐳ Time between location requests', + 'last_update': '_last_update ᐳ Last time the location was updated', + 'next_update': '_next_update ᐳ Next time the location will be updated', + 'last_located': '_last_located ᐳ Last time the was located using iCloud or Mobile App location', + } +CONF_SENSORS_TRACKING_TIME_KEY_TEXT = { + 'travel_time': '_travel_time ᐳ Waze Travel time to Home or closest Track-from-Zone zone', + 'travel_time_min': '_travel_time_min ᐳ Waze Travel time to Home or closest Track-from-Zone zone in minutes', + 'travel_time_hhmm': '_travel_time_hhmm ᐳ Waze Travel time to a Zone in hours:minutes', + 'arrival_time': '_arrival_time ᐳ Home Zone arrival time based on Waze Travel time', + } +CONF_SENSORS_TRACKING_DISTANCE_KEY_TEXT = { + 'home_distance': '_home_distance ᐳ Distance to the Home zone', + 'zone_distance': '_zone_distance ᐳ Distance to the Home or closest Track-from-Zone zone', + 'dir_of_travel': '_dir_of_travel ᐳ Direction of Travel for the Home zone or closest Track-from-Zone zone (Towards, AwayFrom, inZone, etc)', + 'moved_distance': '_moved_distance ᐳ Distance moved from the last location', + } +CONF_SENSORS_TRACK_FROM_ZONES_KEY_TEXT = { + 'general_sensors': 'Include General Sensors (_zone_info)', + 'time_sensors': 'Include Travel Time Sensors (_travel_time, _travel_time_mins, _travel_time_hhmm, _arrival_time', + 'distance_sensors': 'Include Zone Distance Sensors (_zone_distance, _distance, _dir_of_travel)', + } +CONF_SENSORS_TRACK_FROM_ZONES_KEYS = ['general_sensors', 'time_sensors', 'distance_sensors'] +CONF_SENSORS_TRACKING_OTHER_KEY_TEXT = { + 'trigger': '_trigger ᐳ Last action that triggered a location update', + 'waze_distance': '_waze_distance ᐳ Waze distance from a TrackFrom zone', + 'calc_distance': '_calc_distance ᐳ Calculated straight line distance from a TrackFrom zone', + } +CONF_SENSORS_ZONE_KEY_TEXT = { + 'zone_fname': '_zone_fname ᐳ HA Zone entity Friendly Name (HA Config > Areas & Zones > Zones > Name)', + 'zone': '_zone ᐳ HA Zone entity_id (`the_shores`)', + 'zone_name': '_zone_name ᐳ Reformat the Zone entity_id, capitalize and remove `_`s (`the_shores` → `TheShores`)', + 'zone_datetime': '_zone_datetime ᐳ The time the Device entered the Zone', + 'last_zone': '_last_zone_[...] ᐳ Create the same sensors for the device`s last HA Zone', + } +CONF_SENSORS_OTHER_KEY_TEXT = { + 'gps_accuracy': '_gps_accuracy ᐳ GPS acuracy of the last location coordinates', + 'vertical_accuracy':'_vertical_accuracy ᐳ Vertical (Elevation) Accuracy', + 'altitude': '_altitude ᐳ Altitude/Elevation', + } + +ACTIONS_SCREEN_OPTIONS = { + "divider1": "═════════════ ICLOUD3 CONTROL ACTIONS ══════════════", + "restart": "RESTART ᐳ Restart iCloud3", + "pause": "PAUSE ᐳ Pause polling on all devices", + "resume": "RESUME ᐳ Resume Polling on all devices, Refresh all locations", + "divider2": "════════════════ DEBUG LOG ACTIONS ══════════════", + "debug_start": "START DEBUG LOGGING ᐳ Start or stop debug logging", + "debug_stop": "STOP DEBUG LOGGING ᐳ Start or stop debug logging", + "rawdata_start": "START RAWDATA LOGGING ᐳ Start or stop debug rawdata logging", + "rawdata_stop": "STOP RAWDATA LOGGING ᐳ Start or stop debug rawdata logging", + "commit": "COMMIT DEBUG LOG RECORDS ᐳ Verify all debug log file records are written", + "divider3": "════════════════ OTHER COMMANDS ═══════════════", + "evlog_export": "EXPORT EVENT LOG ᐳ Export Event Log data", + "wazehist_maint": "WAZE HIST DATABASE ᐳ Recalc time/distance data at midnight", + "wazehist_track": "WAZE HIST MAP TRACK ᐳ Load route locations for map display", + "divider4": "═══════════════════════════════════════════════", + "restart_ha": "RESTART HA, RESTART ICLOUD3 ᐳ Restart HA, Restart iCloud3", + "return": "MAIN MENU ᐳ Return to the Main Menu" + } +ACTIONS_SCREEN_ITEMS_TEXT = [text for text in ACTIONS_SCREEN_OPTIONS.values()] +ACTIONS_SCREEN_ITEMS_KEY_BY_TEXT = {text: key + for key, text in ACTIONS_SCREEN_OPTIONS.items() + if key.startswith('divider') is False} + +ACTIONS_IC3_ITEMS = { + "restart": "RESTART ᐳ Restart iCloud3", + "pause": "PAUSE ᐳ Pause polling on all devices", + "resume": "RESUME ᐳ Resume Polling on all devices, Refresh all locations", +} +ACTIONS_DEBUG_ITEMS = { + "debug_start": "START DEBUG LOGGING ᐳ Start or stop debug logging", + "debug_stop": "STOP DEBUG LOGGING ᐳ Start or stop debug logging", + "rawdata_start": "START RAWDATA LOGGING ᐳ Start or stop debug rawdata logging", + "rawdata_stop": "STOP RAWDATA LOGGING ᐳ Start or stop debug rawdata logging", + "commit": "COMMIT DEBUG LOG RECORDS ᐳ Verify all debug log file records are written", +} +ACTIONS_OTHER_ITEMS = { + "evlog_export": "EXPORT EVENT LOG ᐳ Export Event Log data", + "wazehist_maint": "WAZE HIST DATABASE ᐳ Recalc time/distance data at midnight", + "wazehist_track": "WAZE HIST MAP TRACK ᐳ Load route locations for map display", +} +ACTIONS_ACTION_ITEMS = { + "restart_ha": "RESTART HA, RELOAD ICLOUD3 ᐳ Restart HA or Reload iCloud3", + "return": "MAIN MENU ᐳ Return to the Main Menu" +} +RARELY_UPDATED_PARMS = 'rarely_updated_parms' +RARELY_UPDATED_PARMS_HEADER = ("➤ RARELY USED PARAMETERS - Display inZone & Fixed Interval, Track-from-Zone and Track-from-Home Zone Override parameters the parameters") +WAZE_USED_HEADER = ("The Waze Route Service provides the travel time and distance information from your " + "current location to the Home or another tracked from zone. This information is used to determine " + "when the next location request should be made") +WAZE_HISTORY_USED_HEADER = ("The Waze History Data base stores 'close to zone' travel time and distance information " + "for a GPS location (100m radius). It reduces the number of internet requests to the Waze Servers " + "after it has been in use for a while and speed up response time when in a poor cell area") +PASSTHRU_ZONE_HEADER = ("You may be driving through a non-tracked zone but not stopping at tne zone. The Mobile " + "App issues an Enter Zone trigger when the device enters the zone and changes the " + "device_tracker entity state to the Zone. iCloud3 does not process the Enter Zone " + "trigger until the delay time has passed. This prevents processing a Zone Enter " + "trig[er that is immediately followed by an Exit Zone trigger.") +STAT_ZONE_HEADER = ("A Stationary Zone is automatically created if the device remains in the same location " + "(store, friends house, doctor`s office, etc.) for an extended period of time") +TRK_FROM_HOME_ZONE_HEADER =("Normally, the Home zone is used as the primary track-from-zone for the tracking results " + "(travel time, distance, etc). However, a different zone can be used as the base location " + "if you are away from Home for an extended period or the device is normally at another " + "location (vacation house, second home, parent's house, etc.). This is a global setting " + "that overrides the Primary Track-from-Home Zone assigned to an individual Device on the Update " + "Devices screen.") +DATA_SOURCE_ICLOUD_HDR = ("APPLE ACCOUNT ᐳ Location data is provided by devices in the Family Sharing list") +DATA_SOURCE_MOBAPP_HDR = ("HA MOBILE APP ᐳ Location data and zone Enter/Exit triggers are provided by the Mobile App") + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> diff --git a/custom_components/icloud3/const_more_info.py b/custom_components/icloud3/const_more_info.py index ec19e19..5d65bd7 100644 --- a/custom_components/icloud3/const_more_info.py +++ b/custom_components/icloud3/const_more_info.py @@ -10,32 +10,32 @@ more_info_text = { 'mobapp_error_not_found_msg': ( f"{CRLF_DASH_75}" - f"{CRLF}1. If the name of the device was changed on the Mobile App " - f"Integration > Devices screen, the Mobile App Device Name parameter " - f"on the iCloud3 Configure Setting > Update Devices screen " + f"{CRLF}1. If the name of the device was changed on the “Mobile App " + f"Integration > Devices” screen, the Mobile App Device Name parameter " + f"on the “iCloud3 Configure Setting > Update Devices” screen " f"needs to be updated with the new device name." - f"{CRLF}2. Check the Mobile App Integration devices to see if it is listed." - f"{CRLF}3. Check the Mobile App Integration devices to see if it is enabled." + f"{CRLF}2. Check the “Mobile App Integration” devices to see if it is listed." + f"{CRLF}3. Check the “Mobile App Integration” devices to see if it is enabled." f"{CRLF}4. Check the MobApp on the device that was not found to make " f"sure it is operational and can communicate with HA. Refresh its location " - f"by pulling down On the screen." - f"{CRLF}5. Check the MobApp device_tracker entities on HA Settings > " - f"Developer Tools > States to verify are the devices using the Mobile App are " + f"by pulling down on the screen." + f"{CRLF}5. Check the MobApp device_tracker entities on “HA Settings > " + f"Developer Tools > States” to verify that the devices using the Mobile App are " f"listed, enabled and that the data is current."), 'mobapp_error_search_msg': ( f"{CRLF_DASH_75}" f"{CRLF}1. Check the MobApp Device Entity assigned to the iCloud3 device " - f"on the iCloud3 Configure Settings > Update Devices screen. Change the " - f"Mobile App device_tracker entity from `Scan for mobile_app device_tracker` " + f"on the “Configure Settings > Update Devices” screen. Change the " + f"Mobile App device_tracker entity from “Scan for mobile_app device_tracker” " f"to a specific device_tracker entity." - f"{CRLF}2. Check the mobile_app devices in HA Settings > Devices & " - f"Services > Devices and delete or rename the devices starting with the " + f"{CRLF}2. Check the mobile_app devices in “HA Settings > Devices & " + f"Services > Devices” screen and delete or rename the devices starting with the " f"iCloud3 devicename that should be selected."), 'mobapp_error_disabled_msg': ( f"{CRLF_DASH_75}" - f"{CRLF}1. Go to HA Devices & Services > Integrations > Mobile App" + f"{CRLF}1. Go to “HA Devices & Services > Integrations > Mobile App”" f"{CRLF}2. Select the disabled device. Then select the 3-dots in the upper " f"right corner." f"{CRLF}Select Enable device."), @@ -43,11 +43,11 @@ 'mobapp_error_multiple_devices_on_scan': ( f"{CRLF_DASH_75}" f"{CRLF}1. Check the MobApp Device Entity assigned to the iCloud3 device " - f"on the iCloud3 Configure Settings > Update Devices screen. Change the " - f"Mobile App device_tracker entity from `Scan for mobile_app device_tracker` " + f"on the “iCloud3 Configure Settings > Update Devices” screen. Change the " + f"Mobile App device_tracker entity from “Scan for mobile_app device_tracker” " f"to a specific device_tracker entity." - f"{CRLF}2. Check the mobile_app devices in HA Settings > Devices & " - f"Services > Devices and delete or rename the devices starting with the " + f"{CRLF}2. Check the Mobile App devices on the “HA Settings > Devices & " + f"Services > Devices” screen. Delete or rename the devices starting with the " f"iCloud3 devicename that should not be selected."), 'mobapp_error_mobile_app_msg': ( @@ -58,7 +58,7 @@ f"Review the HA Companion App docs to verify it is set up and can communicate with HA." f"{CRLF}3. Under each device is a line reading `1 Device and ## Entities`. " f"Select Entities and verify that the _battery_level and _last_update_trigger " - f"entities are listed and enabled. If disabled, select the Gear Icon and enable them. " + f"entities are listed and enabled. If disabled, select the “Gear” Icon and enable them. " f"If necessary. Review the HA Mobile App docs for more information." f"{CRLF}4. Do this for each mobile_app device with a problem." f"{CRLF}5. Restart iCloud3 after making any changes (Event Log > Action > Restart)."), @@ -67,49 +67,49 @@ f"{CRLF_DASH_75}" f"{CRLF}1. Verify that the Mobile App Integration has been added to HA. " f"If not, add it. You may have to restart HA." - f"{CRLF}2. Verify the Mobile App Integration operational status (HA Settings > Devices " - f"and Services > Integrations). Check to see if a `Failed to set up` error message is " + f"{CRLF}2. Verify the Mobile App Integration operational status (“HA Settings > Devices " + f"& Services > Integrations”). Check to see if a `Failed to set up` error message is " f"displayed in general or for a specific device. If so, that issue must be corrected " f"before the Mobile App can be used for this device." f"{CRLF_DASH_75}" - f"{CRLF}The FamShr tracking method will continue to be used for this device."), + f"{CRLF}The iCloud tracking method will continue to be used for this device."), 'mobapp_device_no_location': ( f"{CRLF_DASH_75}" - f"{CRLF}1. Verify the Mobile App Integration operational status (HA Settings > Devices " - f"and Services > Integrations). Check to see if an error message is displayed in general " + f"{CRLF}1. Verify the Mobile App Integration operational status (“HA Settings > Devices " + f"& Services > Integrations”). Check to see if an error message is displayed in general " f"or for a specific device. If so, that issue should be corrected." - f"{CRLF}2. Check the device_tracker entity state and attribute values (HA Developer Tools > " - f"States). Verify the device is on-line and location data is being updated by the Mobile App. " + f"{CRLF}2. Check the device_tracker entity state and attribute values (“HA Developer Tools > " + f"States”). Verify the device is on-line and location data is being updated by the Mobile App. " f"{CRLF}3. Check the Mobile App on the device." f"{CRLF}{NBSP3}1. Verify it is online and can connect to HA. " f"{CRLF}{NBSP3}2. Verify that the device can be located." f"{CRLF}{NBSP3}3. Verify that location services are enabled and the MobApp settings are correct." - f"{CRLF}{NBSP3}4. Go to the Mobile App > Location screen, scroll to the bottom and select " - f"Update Location. Then see if any errors are displayed in the Event Log."), + f"{CRLF}{NBSP3}4. Go to the “Mobile App > Location” screen, scroll to the bottom and select " + f"`Update Location`. Then see if any errors are displayed in the Event Log."), - 'famshr_device_not_available': ( + 'icloud_device_not_available': ( f"{CRLF_DASH_75}" f"{CRLF}1. Check the Family Share devices in the FindMy app on your phone. " f"See if any devices have been renamed, are missing or there is more than one " f"device with the same name." - f"{CRLF}2. Have you change phones? The FamShr devicename in Configure Settings " - f"may be the old phone, not the new phone. The new one may have a different name." - f"{CRLF}3. Check the FamShr devices in Stage 4 above. It lists all the FamShr devices that " - f"have been returned from your Apple iCloud account. Sometimes, Apple does not return " + f"{CRLF}2. Have you change phones? The iCloud devicename on the “Configure Settings > Update Device” " + f"screen may be the old phone, not the new phone. The new one may have a different name." + f"{CRLF}3. Check the iCloud devices in Stage 4 above. It lists all the iCloud devices that " + f"have been returned from your Apple Account. Sometimes, Apple does not return " f"all of the devices if there is a delay locating it or it is asleep. iCloud3 will " - f"request the list 2-times. Open the missing device so it is available, then restart " + f"request the list severan times. Open the missing device so it is available, then restart " f"iCloud3 to see if it is found." - f"{CRLF}4. Check the FamShr Device assigned to the iCloud3 device " - f"on the iCloud3 `Configure Settings > Update Devices` screen. Open the FamShr Devices " + f"{CRLF}4. Check the Apple Account iCloud Device assigned to the iCloud3 device " + f"on the “Configure Settings > Update Devices” screen. Open the iCloud Devices " f"list and review the devicenames available. Make sure the devices are correct and " f"there are no duplicates or additional/new devices with a different name."), - 'famshr_dup_devices': ( + 'icloud_dup_devices': ( f"{CRLF_DASH_75}" - f"{CRLF}1. Review the Family Share devices in the list above and verify the last " + f"{CRLF}1. Review the Apple Account iCloud Devices in the list above and verify the last " f"located device is the one iCloud3 should track or monitor." - f"{CRLF}2. Update the devices in your Family Sharing List and remove the old, unused " + f"{CRLF}2. In the FindMy App, update the devices in your Family List and remove the old, unused " f"devices or any devices you no longer have." f"{CRLF}{NBSP3}1. Open the FindMy App. Select `Devices`." f"{CRLF}{NBSP3}2. Select the device you want to remove. `Remove This Device` is displayed " @@ -118,27 +118,27 @@ f"logged in and connected." f"{CRLF}{NBSP3}3. Select `Remove This Device`, then `Yes`" f"{CRLF}{NBSP3}4. If your current device has a name with a number suffix, like Gary-iPad(2), " - f"you can clean up your device names by renaming it. On the device itself, go to Settings " - f"App > General > About > Name. Remove the suffix so it reads something like Gary-iPad." + f"you can clean up your device names by renaming it. On the device itself, go to “Settings " + f"App > General > About > Name”. Remove the suffix so it reads something like Gary-iPad." f"{CRLF}{NBSP3}5. Do this for all devices that need to be removed and renamed." f"{CRLF}{NBSP3}6. Restart HA. The new device names will be identified by iCloud3 and your " f"configuration will be updated with the new name." - f"{CRLF}{NBSP3}7. Go to the iCloud3 > Configure Settings > Update Devices screen for the " - f"devices you changed and verify the correct Family Sharing Device is selected from it's list." + f"{CRLF}{NBSP3}7. Go to the “Configure Settings > Update Devices” screen for the " + f"devices you changed and verify the correct Apple Account iCloud Device is selected from it's list." f"Correct any that are wrong." f"{CRLF}{NBSP3}8. Restart iCloud3 and verify that the devices are tracked correctly." ), - 'famshr_find_my_phone_alert_error': ( + 'icloud_dind_my_phone_alert_error': ( f"{CRLF_DASH_75}" - f"{CRLF}1. Review the Event Log Stages 3 & 4 for this device and make sure the FamShr " + f"{CRLF}1. Review the Event Log Stages 3 & 4 for this device and make sure the iCloud " f"device you selected is correct and has been verified." - f"{CRLF}2. Review the Configure Settings > Update Devices screen for this device and " + f"{CRLF}2. Review the “Configure Settings > Update Devices” screen for this device and " f"verify it has been assigned correctly, is not still assigned to an old device and " f"there are no error messages." f"{CRLF}3. Review the FindMy App devices screen and verify the device can be located." f"{CRLF}4. Review the Event Log for this device and make sure it is being tracked with the " - f"FamShr tracking method." + f"iCloud tracking method." ), 'refresh_browser': ( @@ -155,42 +155,48 @@ 'unverified_device': ( f"{CRLF_DASH_75}" f"{CRLF}This can be caused by:" - f"{CRLF}1. iCloud3 Device configuration error. Check the iCloud3 `Configure Settings > " - f"Update Devices` screen and verify the FamShr and Mobile App Device selections are correct." + f"{CRLF}1. iCloud3 Device configuration error. Check the “Configure Settings > " + f"Update Devices” screen and verify the iCloud and Mobile App Device selections are correct." f"{CRLF}2. No iCloud or Mobile App device have been selected. See #1 above." - f"{CRLF}3. This device is no longer in your iCloud Family Sharring device list. Review " - f"the devices in the FindMy app on your phone and on your iCloud account. Review the list of " - f"devices returned from your iCloud account when iCloud3 was starting up in the Event Log Stage 4." - f"{CRLF}4. iCloud or Mobile App are not being used to locate devices. Verify that your iCloud " - f"access is set up on the `Configure Settings > iCloud Account` screen. Also verify that FamShr " - f"devices have been assigned to iCloud3 devices `Configure Settings Update Devices` screen." + f"{CRLF}3. This device is no longer in your iCloud Family Sharing device list. Review " + f"the devices in the FindMy App on your phone and on your Apple Account. Review the list of " + f"devices returned from your Apple Account when iCloud3 was starting up in the Event Log Stage 4." + f"{CRLF}4. iCloud or Mobile App are not being used to locate devices. Verify that your Apple Account " + f"is a data source and is set up on the “Configure Settings > iCloud Account” screen. Also verify that " + f"Apple Account iCloud Devices have been assigned correctly to iCloud3 devices on the “Configure " + f"Settings Update Devices” screen." f"{CRLF}5. iCloud is down. The network is down. iCloud is not responding to location requests." - f"{CRLF}6. An internal code error occurred. Check HA Settings > System > Logs for errors." + f"{CRLF}6. An internal code error occurred. Check “HA Settings > System > Logs” for errors." f"{CRLF_DASH_75}" f"{CRLF}Restart iCloud3 (Event Log > Actions > Restart iCloud) and see if the problem reoccurs."), 'all_devices_inactive': ( - f"Devices can be tracked, monitored or inactive on the iCloud3 `Configure Settings > Update " - f"Devices` screen. In this case, all of the devices are set to an Inactive status. " + f"Devices can be tracked, monitored or inactive on the “Configure Settings > Update " + f"Devices” screen. In this case, all of the devices are set to an Inactive status. " f"{CRLF}1. Change the `Tracking Mode` from INACTIVE to Track or Monitor." - f"{CRLF}2. Verify the Family Share (FamShr) Device assigned to the iCloud3 device." - f"{CRLF}3. Verify the Mobile App Device assigned to the iCloud3 device." + f"{CRLF}2. Verify the Apple Account iCloud Device assigned to the iCloud3 device is correct." + f"{CRLF}3. Verify the Mobile App Device assigned to the iCloud3 device is correct." f"{CRLF}4. Review the other parameters for the device while you are on this screen." f"{CRLF}5. Do this for all of your devices." - f"{CRLF}6. Exit the iCloud3 `Configure Settings` screens and Restart iCloud3."), + f"{CRLF}6. Exit the “Configure Settings” screens and Restart iCloud3."), 'add_icloud3_integration': ( f"{CRLF}1. Select {SETTINGS_INTEGRATIONS_MSG}" f"{CRLF}2. Select `+Add Integration` to add the iCloud3 integration if it is not dislayed. Then search " f"for `iCloud3`, select it and complete the installation." - f"{CRLF}3. Select {INTEGRATIONS_IC3_CONFIG_MSG} to open iCloud3 Configure Settings screens." + f"{CRLF}3. Select {INTEGRATIONS_IC3_CONFIG_MSG} to open “Configure Settings” screens." f"{CRLF}4. Review and setup the `iCloud Account` and `Update Devices` configuration screens." f"{CRLF}5. Exit the configurator and `Restart iCloud3`."), 'configure_icloud3': ( - f"{CRLF}1. {SETTINGS_INTEGRATIONS_MSG} >" - f"{CRLF}2. {INTEGRATIONS_IC3_CONFIG_MSG}" - f"{CRLF}3. Then select Update Devices and review/update the devices in error."), + f"{CRLF}1. Click the “Gear” icon at the top right corner on the Event Log screen " + f"or select “Settings > Devices & Services > Integrations” from the HA sidebar >" + f"{CRLF}2. Select “iCloud3 > Configuration” to display the “Configure Settings” screen." + f"{CRLF}3. Select “Update Data Sources” to add your Apple account that will " + f"provide location information" + f"{CRLF}3. Select “Update Devices” to add devices that will be tracked." + f"{CRLF}3. Review the other configuration screens and update any parameters " + f"that need to be changed."), 'unverified_devices_caused_by': ( f"{CRLF}This can be caused by:" diff --git a/custom_components/icloud3/const_sensor.py b/custom_components/icloud3/const_sensor.py index a6b4217..2a3ca0b 100644 --- a/custom_components/icloud3/const_sensor.py +++ b/custom_components/icloud3/const_sensor.py @@ -11,7 +11,7 @@ LAST_ZONE, LAST_ZONE_DNAME, LAST_ZONE_NAME, LAST_ZONE_FNAME, LAST_ZONE_DATETIME, INTERVAL, LOCATION_SOURCE, BATTERY_SOURCE, BATTERY, BATTERY_STATUS, BATTERY_UPDATE_TIME, - BATTERY_FAMSHR, BATTERY_MOBAPP, BATTERY_LATEST, + BATTERY_ICLOUD, 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, @@ -166,7 +166,7 @@ 'battery', 'mdi:battery-outline', [BATTERY_STATUS, BATTERY_SOURCE, BATTERY_UPDATE_TIME, - BATTERY_FAMSHR, BATTERY_MOBAPP, + BATTERY_ICLOUD, BATTERY_MOBAPP, 'mobapp_sensor-battery_level', ], 0], BATTERY_STATUS: [ diff --git a/custom_components/icloud3/device.py b/custom_components/icloud3/device.py index c07647f..99bf8fc 100644 --- a/custom_components/icloud3/device.py +++ b/custom_components/icloud3/device.py @@ -8,7 +8,7 @@ NOTIFY, DISTANCE_TO_DEVICES, NEAR_DEVICE_DISTANCE, 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, + DOT, RED_X, RARROW, RARROW2, 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, @@ -18,8 +18,7 @@ TRACKING_NORMAL, TRACKING_PAUSED, TRACKING_RESUMED, 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, + ICLOUD, MOBAPP, TRACK_DEVICE, MONITOR_DEVICE, INACTIVE_DEVICE, TRACKING_MODE_FNAME, NAME, DEVICE_TYPE_FNAME, ICLOUD_HORIZONTAL_ACCURACY, ICLOUD_VERTICAL_ACCURACY, ICLOUD_BATTERY_STATUS, @@ -31,7 +30,7 @@ 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_LEVEL_LOW, - BATTERY_FAMSHR, BATTERY_MOBAPP, BATTERY_LATEST, + BATTERY_ICLOUD, 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, @@ -44,11 +43,11 @@ INFO, GPS_ACCURACY, GPS, VERT_ACCURACY, ALTITUDE, DEVICE_STATUS_CODES, DEVICE_STATUS_OFFLINE, DEVICE_STATUS_PENDING, CONF_TRACK_FROM_BASE_ZONE, CONF_TRACK_FROM_ZONES, CONF_LOG_ZONES, - FRIENDLY_NAME, PICTURE, ICON, BADGE, + CONF_FNAME, FRIENDLY_NAME, PICTURE, ICON, BADGE, CONF_PICTURE, CONF_STAT_ZONE_FNAME, CONF_DEVICE_TYPE, CONF_RAW_MODEL, CONF_MODEL, CONF_MODEL_DISPLAY_NAME, - CONF_FNAME, CONF_FAMSHR_DEVICENAME, - CONF_MOBILE_APP_DEVICE, CONF_FMF_EMAIL, + CONF_APPLE_ACCOUNTS, CONF_APPLE_ACCOUNT, CONF_FAMSHR_DEVICENAME, CONF_USERNAME, + CONF_MOBILE_APP_DEVICE, CONF_TRACKING_MODE, CONF_INZONE_INTERVAL, CONF_FIXED_INTERVAL, ) from .const_sensor import (SENSOR_LIST_ZONE_NAME, SENSOR_ICONS, ) @@ -64,7 +63,7 @@ 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, ) + post_internal_error, _evlog, _log, ) 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, @@ -84,12 +83,14 @@ class iCloud3_Device(TrackerEntity): def __init__(self, devicename, conf_device): + self.setup_time = time_now() self.conf_device = conf_device self.devicename = devicename self.ha_device_id = '' # ha device_registry device_id self.fname = devicename.title() self.StatZone = None # The StatZone this Device is in or None if not in a StatZone + self.PyiCloud = None # PyiCloud object for the Apple Account for this iCloud device self.FromZones_by_zone = {} # DeviceFmZones objects for the track_from_zones parameter for this Device self.FromZone_Home = None # DeviceFmZone object for the Home zone @@ -107,23 +108,26 @@ def __init__(self, devicename, conf_device): self.Sensors = Gb.Sensors_by_devicename.get(devicename, {}) self.Sensors_from_zone = Gb.Sensors_by_devicename_from_zone.get(devicename, {}) + self.initialize() self.initialize_on_initial_load() self.initialize_sensors() - self._link_device_entities_sensor_device_tracker() + self.configure_device(conf_device) self.initialize_track_from_zones() det_interval.determine_TrackFrom_zone(self) - + self._link_device_entities_sensor_device_tracker() def initialize(self): self.devicename_verified = False + self.verified_flag = False # Indicates this is a valid and trackable Device # Operational variables self.device_type = 'iPhone' self.raw_model = DEVICE_TYPE_FNAME.get(self.device_type, self.device_type) # iPhone15,2 self.model = DEVICE_TYPE_FNAME.get(self.device_type, self.device_type) # iPhone - self.model_display_name = DEVICE_TYPE_FNAME.get(self.device_type, self.device_type) # iPhone 14 Pro + #self.model_display_name = DEVICE_TYPE_FNAME.get(self.device_type, self.device_type) # iPhone 14 Pro + self.model_display_name = Gb.model_display_name_by_raw_model.get(self.raw_model, self.raw_model) # iPhone 14 Pro self.data_source = None self.tracking_status = TRACKING_NORMAL self.tracking_mode = TRACK_DEVICE #normal, monitor, inactive @@ -136,8 +140,6 @@ def initialize(self): self.last_zone = '' self.last_track_from_zone = '' self.log_zones_filenames = [] # Log Zone activity to a .csv file - self.zone_change_datetime = DATETIME_ZERO - self.zone_change_secs = 0 self.info_msg = '' # results of last format_info_msg self.went_3km = False self.near_device_distance = 0.0 # Distance to the NearDevice device @@ -145,7 +147,10 @@ def initialize(self): self.near_device_used = '' self.dist_apart_msg = '' # Distance to all other devices msg set in icloud3_main self.dist_apart_msg_by_devicename = {} # Distance to all other devices msg set in icloud3_main - self.last_near_devices_msg = '' # Nearby devices msg displayed every 15-minutes + self.dist_apart_msg2 = '' # Distance to all other devices msg set in icloud3_main + self.dist_apart_msg_by_devicename2= {} # Distance to all other devices msg set in icloud3_main + self.dist_to_devices_data = [] # Near devices msg displayed in the det_interval.post_neary_devices_msg + self.dist_to_devices_secs = 0 # Near devices msg displayed in the det_interval.post_neary_devices_msg self.last_update_loc_secs = 0 # Located secs from the device tracker entity update self.last_update_loc_time = DATETIME_ZERO # Located time from the device tracker entity update self.last_update_gps_accuracy = 0 @@ -175,12 +180,9 @@ def initialize(self): self.outside_no_exit_trigger_flag = False self.moved_since_last_update_km = 0 - # Fields used in FmF and FamShr initialization - self.verified_flag = False # Indicates this is a valid and trackable Device - self.device_id_famshr = None # " - self.device_id_fmf = None # iCloud device_id - self.paired_with_id = None # famshr device id for paired devices (iPhone Device <--> Watch) - self.PairedDevice = None # Device of the other Device paired to this one + # Fields used in iCloud initialization + self.icloud_device_id = '' # " + self.icloud_person_id = '' # icloud person id that's a key into the Gb.Devices_by_person_id # StatZone fields self.statzone_latitude = 0.0 @@ -190,27 +192,23 @@ def initialize(self): self.statzone_setup_secs = 0 # Time the statzone was set up # iCloud3 configration fields - self.conf_famshr_name = None - self.conf_famshr_devicename = None - self.conf_famshr_device_id = None - self.conf_fmf_email = None - self.conf_fmf_device_id = None - self.conf_mobapp_fname = None + self.conf_apple_acct_username= '' + self.conf_icloud_dname = '' # The iCloud famshr devicename parameter used to select the device's iCloud data + self.conf_icloud_devicename = '' # slugified version of the dname value + self.conf_icloud_device_id = '' + self.family_share_device = False # False=One of Apple acct owners devices, True=Family Share device + self.conf_mobapp_fname = '' # Device source self.primary_data_source = ICLOUD - self.is_data_source_FAMSHR = True - self.is_data_source_FMF = False self.is_data_source_ICLOUD = True - self.is_data_source_FAMSHR_FMF = True self.is_data_source_MOBAPP = True self.verified_ICLOUD = False - self.verified_FAMSHR = False - self.verified_FMF = False self.verified_MOBAPP = False # Device location & gps fields - self.old_loc_cnt = 0 + self.old_loc_cnt = 0 # Number of old locations received from Apple in a row + self.max_error_cycle_cnt = 0 # Number of times the old loc interval retry time has cycled self.old_loc_msg = '' self.old_loc_threshold_secs = 120 self.poor_gps_flag = False @@ -222,10 +220,11 @@ def initialize(self): self.pending_secs = 0 # Time the device went into a pending status (checked after authentication) self.dist_to_other_devices_secs = 0 self.dist_to_other_devices = {} # A dict of other devices distances + self.dist_to_other_devices2 = {} # A dict of other devices distances # {devicename: [distance_m, gps_accuracy_factor, location_old_flag]} - self.loc_time_updates_famshr = [HHMMSS_ZERO] # History of update times from one results to the next + self.loc_time_updates_icloud = [HHMMSS_ZERO] # History of update times from one results to the next self.loc_time_updates_mobapp = [HHMMSS_ZERO] # History of update times from one results to the next - self.loc_msg_famshr_mobapp_time = '' # Time string the locate msg was displayed to prevent dup msgs (icl_data_handlr) + self.loc_msg_icloud_mobapp_time = '' # Time string the locate msg was displayed to prevent dup msgs (icl_data_handlr) self.dev_data_useable_chk_secs = 0 # The device data is checked several times during an update self.dev_data_useable_chk_results = [] # If this check is the same as the last one, return the previous results @@ -243,17 +242,6 @@ def initialize(self): self.mobapp_request_sensor_update_secs = 0 # MobApp state variables - self.update_mobapp_data_monitor_msg= '' - self.mobapp_data_state = NOT_SET - self.mobapp_data_latitude = 0.0 - self.mobapp_data_longitude = 0.0 - self.mobapp_data_state_secs = 0 - self.mobapp_data_state_time = HHMMSS_ZERO - self.mobapp_data_trigger_secs = 0 - self.mobapp_data_trigger_time = HHMMSS_ZERO - self.mobapp_data_secs = 0 - self.mobapp_data_time = HHMMSS_ZERO - self.mobapp_data_trigger = NOT_SET self.mobapp_data_gps_accuracy = 0 self.mobapp_data_vertical_accuracy = 0 self.mobapp_data_altitude = 0.0 @@ -319,27 +307,43 @@ def initialize(self): self.debug_save_dict = {} def __repr__(self): - return (f"") + return (f"") #------------------------------------------------------------------------------ def initialize_on_initial_load(self): # Initialize these variables only when starting up # Do not initialize them on a restart - # If self.sensors exista, this device has been initialized during the initial + # If self.sensors exists, this device has been initialized during the initial # load or when iC3 is restarted and it is not a new device. try: - if self.sensor != {}: + if self.sensors != {}: return except: pass - # if Gb.initial_icloud3_loading_flag is False: - # return + self.mobapp = {DEVICE_TRACKER: '', TRIGGER: '', BATTERY_LEVEL: '', BATTERY_STATUS: '', NOTIFY: ''} - self.mobapp_data_battery_level = 0 - self.mobapp_data_battery_status = UNKNOWN + # MobApp state variables + self.update_mobapp_data_monitor_msg= '' + self.mobapp_data_state = NOT_SET + self.mobapp_data_latitude = 0.0 + self.mobapp_data_longitude = 0.0 + self.mobapp_data_state_secs = 0 + self.mobapp_data_state_time = HHMMSS_ZERO + self.mobapp_data_trigger_secs = 0 + self.mobapp_data_trigger_time = HHMMSS_ZERO + self.mobapp_data_secs = 0 + self.mobapp_data_time = HHMMSS_ZERO + self.mobapp_data_trigger = NOT_SET + self.mobapp_data_battery_level = 0 + self.mobapp_data_battery_status = UNKNOWN self.mobapp_data_battery_update_secs = 0 + self._restore_state_reset_mobapp_items() + + self.zone_change_datetime = DATETIME_ZERO + self.zone_change_secs = 0 + self._restore_state_reset_other_items() self.dev_data_battery_source = '' self.dev_data_battery_level = 0 @@ -350,9 +354,9 @@ def initialize_on_initial_load(self): 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 + # rc9 Added battery_info sensors to display last battery data for icloud # & mobapp sensor.battery attributes - self.battery_info = {FAMSHR_FNAME: '', MOBAPP_FNAME: ''} + self.battery_info = {ICLOUD: '', MOBAPP: ''} #------------------------------------------------------------------------------ def initialize_sensors(self): @@ -380,8 +384,8 @@ def initialize_sensors(self): self.sensors['mobapp_sensor-battery_level'] = '' self.sensors['mobapp_sensor-battery_status'] = '' - # rc9 Added battery_famshr & battery_mobapp to display battery_info data - self.sensors[BATTERY_FAMSHR] = '' + # rc9 Added battery_icloud & battery_mobapp to display battery_info data + self.sensors[BATTERY_ICLOUD] = '' self.sensors[BATTERY_MOBAPP] = '' self.sensors[BATTERY_LATEST] = '' @@ -393,7 +397,7 @@ def initialize_sensors(self): self.sensors[GPS_ACCURACY] = 0 self.sensors[ALTITUDE] = 0 self.sensors[VERT_ACCURACY] = 0 - self.sensors[LOCATION_SOURCE] = '' #icloud:fmf/famshr or mobapp + self.sensors[LOCATION_SOURCE] = '' #icloud: icloud or mobapp self.sensors[NEAR_DEVICE_USED] = '' self.sensors[TRIGGER] = '' self.sensors[LAST_LOCATED_DATETIME] = DATETIME_ZERO @@ -424,7 +428,7 @@ def initialize_sensors(self): self.sensors[TRAVEL_TIME] = 0 self.sensors[TRAVEL_TIME_MIN] = 0 self.sensors[TRAVEL_TIME_HHMM] = HHMM_ZERO - self.sensors[ARRIVAL_TIME] = HHMMSS_ZERO + self.sensors[ARRIVAL_TIME] = HHMM_ZERO self.sensors[ZONE_DISTANCE] = 0.0 self.sensors[ZONE_DISTANCE_M] = 0.0 self.sensors[ZONE_DISTANCE_M_EDGE] = 0.0 @@ -496,15 +500,6 @@ def configure_device(self, conf_device): self.sensors['dev_id'] = self.devicename self.evlog_fname_alert_char = '' # Character added to the fname in the EvLog (❗❌⚠️) - # mobapp device_tracker/sensor entity ids - self.mobapp = { - DEVICE_TRACKER: '', - TRIGGER: '', - BATTERY_LEVEL: '', - BATTERY_STATUS: '', - NOTIFY: '', - } - self._initialize_data_source_fields(conf_device) self.initialize_non_tracking_config_fields(conf_device) self._validate_zone_parameters() @@ -520,18 +515,18 @@ def configure_device(self, conf_device): if (Gb.is_track_from_base_zone_used and Gb.track_from_base_zone != HOME): self.track_from_base_zone = Gb.track_from_base_zone - self.track_from_zones = list_add(self.track_from_zones, self.track_from_base_zone) + list_add(self.track_from_zones, self.track_from_base_zone) if Gb.track_from_home_zone is False: - self.track_from_zones = list_del(self.track_from_zones, HOME) + list_del(self.track_from_zones, HOME) else: self.track_from_base_zone = conf_device[CONF_TRACK_FROM_BASE_ZONE] if self.track_from_base_zone == HOME: - self.track_from_zones = list_add(self.track_from_zones, HOME) + list_add(self.track_from_zones, HOME) # Put it at the end of the track-from list if self.track_from_base_zone != self.track_from_zones[-1]: - self.track_from_zones = list_del(self.track_from_zones, self.track_from_base_zone) - self.track_from_zones = list_add(self.track_from_zones, self.track_from_base_zone) + list_del(self.track_from_zones, self.track_from_base_zone) + list_add(self.track_from_zones, self.track_from_base_zone) except Exception as err: log_exception(err) @@ -567,8 +562,8 @@ def initialize_non_tracking_config_fields(self, conf_device): 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: + if conf_device.get(CONF_PICTURE, 'None') != 'None': + picture = conf_device.get(CONF_PICTURE, 'None').replace('www/', '/local/') self.sensors[PICTURE] = picture if instr(picture, '/') else (f"/local/{picture}") self.sensor_badge_attrs[PICTURE] = self.sensors[PICTURE] @@ -586,27 +581,24 @@ def initialize_non_tracking_config_fields(self, conf_device): #-------------------------------------------------------------------- def _initialize_data_source_fields(self, conf_device): - if Gb.conf_data_source_FAMSHR and conf_device.get(CONF_FAMSHR_DEVICENAME, 'None') != 'None': - self.conf_famshr_name = self._extract_devicename(conf_device[CONF_FAMSHR_DEVICENAME]) - self.conf_famshr_devicename = slugify(self.conf_famshr_name) + if Gb.conf_data_source_ICLOUD and conf_device.get(CONF_FAMSHR_DEVICENAME, 'None') != 'None': + self.conf_apple_acct_username = conf_device.get(CONF_APPLE_ACCOUNT, '') + self.conf_icloud_dname = conf_device.get(CONF_FAMSHR_DEVICENAME, 'None') + self.conf_icloud_devicename = slugify(self.conf_icloud_dname) - if Gb.conf_data_source_FMF and conf_device.get(CONF_FMF_EMAIL, 'None') != 'None': - self.conf_fmf_email = self._extract_devicename(conf_device[CONF_FMF_EMAIL]) + if self.conf_apple_acct_username: + username_devices = Gb.Devices_by_username.get(self.conf_apple_acct_username, []) + Gb.Devices_by_username[self.conf_apple_acct_username] = list_add(username_devices, self) if Gb.conf_data_source_MOBAPP and conf_device.get(CONF_MOBILE_APP_DEVICE, 'None') != 'None': self.mobapp[DEVICE_TRACKER] = conf_device[CONF_MOBILE_APP_DEVICE] - self.is_data_source_FAMSHR = Gb.conf_data_source_FAMSHR and self.conf_famshr_devicename is not None - self.is_data_source_FMF = Gb.conf_data_source_FMF and self.conf_fmf_email is not None - self.is_data_source_ICLOUD = Gb.primary_data_source_ICLOUD and (self.is_data_source_FAMSHR or self.is_data_source_FMF) - self.is_data_source_FAMSHR_FMF = self.is_data_source_ICLOUD + self.is_data_source_ICLOUD = Gb.conf_data_source_ICLOUD and self.conf_icloud_dname is not None self.is_data_source_MOBAPP = Gb.conf_data_source_MOBAPP and self.mobapp[DEVICE_TRACKER] != '' # Set primary data source - if self.conf_famshr_devicename: - self.primary_data_source = FAMSHR - elif self.conf_fmf_email: - self.primary_data_source = FMF + if self.conf_icloud_dname: + self.primary_data_source = ICLOUD elif self.mobapp[DEVICE_TRACKER]: self.primary_data_source = MOBAPP else: @@ -668,7 +660,7 @@ def initialize_track_from_zones(self): self.TrackFromBaseZone = FromZone self.last_track_from_zone = FromZone.from_zone - FromZone.zone_dist = FromZone.sensors[ZONE_DISTANCE] + FromZone.zone_dist_km = FromZone.sensors[ZONE_DISTANCE] # Set a list of tracked from zone names to make it easier to get them later self.from_zone_names = [k for k in self.FromZones_by_zone.keys()] @@ -813,30 +805,39 @@ def _restore_sensors_from_restore_state_file(self, zone=None, FromZone=None): #-------------------------------------------------------------------- @property def fname_devicename(self): - return (f"{self.fname}{INFO_SEPARATOR}{self.devicename}") + return (f"{self.fname} ({self.devicename})") + # return (f"{self.fname}{INFO_SEPARATOR}{self.devicename}") @property def devicename_fname(self): - return (f"{self.devicename}{INFO_SEPARATOR}{self.fname}") + return (f"{self.devicename} ({self.fname})") + # return (f"{self.devicename}{INFO_SEPARATOR}{self.fname}") + + @property + def devtype_fname(self): + return DEVICE_TYPE_FNAME.get(self.device_type, self.device_type) @property def fname_devtype(self): if instr(self.fname, DEVICE_TYPE_FNAME.get(self.device_type, self.device_type)): return self.fname - return (f"{self.fname}{INFO_SEPARATOR}" - f"{DEVICE_TYPE_FNAME.get(self.device_type, self.device_type)}") + return (f"{self.fname} " + f"({DEVICE_TYPE_FNAME.get(self.device_type, self.device_type)})") + # return (f"{self.fname}{INFO_SEPARATOR}" + # f"{DEVICE_TYPE_FNAME.get(self.device_type, self.device_type)}") @property - def device_id8_famshr(self): - if self.device_id_famshr: - return f"#{self.device_id_famshr[:8]}" - return 'None' + def conf_apple_acct_username_base(self): + try: + return self.conf_apple_acct_username.split('@')[0] + except: + return '' @property - def device_id8_fmf(self): - if self.device_id_fmf: - return f"#{self.device_id_fmf[:8]}" + def device_id8_icloud(self): + if self.icloud_device_id: + return f"#{self.icloud_device_id[:8]}" return 'None' @property @@ -844,28 +845,23 @@ def tracking_mode_fname(self, track_fname=False): if self.tracking_mode == TRACK_DEVICE and track_fname is False: return '' else: - return f"({TRACKING_MODE_FNAME[self.tracking_mode]})" + return f", {TRACKING_MODE_FNAME[self.tracking_mode]}" def is_statzone_name(self, zone_name): return zone_name in Gb.StatZones_by_zone def set_fname_alert(self, alert_char): - if instr(self.evlog_fname_alert_char, alert_char) is False: + if alert_char == '': + self.evlog_fname_alert_char = '' + elif instr(self.evlog_fname_alert_char, alert_char) is False: self.evlog_fname_alert_char += alert_char @property - def PyiCloud_RawData_famshr(self): - if Gb.PyiCloud is None: - return None - else: - return Gb.PyiCloud.RawData_by_device_id.get(self.device_id_famshr) + def PyiCloud_RawData_icloud(self): + if self.PyiCloud: + return self.PyiCloud.RawData_by_device_id.get(self.icloud_device_id) - @property - def PyiCloud_RawData_fmf(self): - if Gb.PyiCloud is None: - return None - else: - return Gb.PyiCloud.RawData_by_device_id.get(self.device_id_fmf) + return None def device_model(self): return f"{self.device_type}" @@ -985,7 +981,7 @@ def format_battery_time(self): #-------------------------------------------------------------------- @property def data_source_fname(self): - return DATA_SOURCE_FNAME.get(self.data_source, self.data_source) + return self.data_source # is_dev_data_source properties @property @@ -996,25 +992,13 @@ def is_dev_data_source_NOT_SET(self): def is_dev_data_source_SET(self): return self.dev_data_source != NOT_SET - @property - def is_dev_data_source_FMF(self): - return self.dev_data_source in [FMF, FMF_FNAME] - - @property - def is_dev_data_source_FAMSHR(self): - return self.dev_data_source in [FAMSHR, FAMSHR_FNAME] - - @property - def is_dev_data_source_FAMSHR_FMF(self): - return self.dev_data_source in [FAMSHR, FMF, FAMSHR_FNAME, FMF_FNAME] - @property def is_dev_data_source_ICLOUD(self): - return self.is_dev_data_source_FAMSHR_FMF + return self.is_dev_data_source_ICLOUD @property def is_dev_data_source_MOBAPP(self): - return self.dev_data_source in [MOBAPP, MOBAPP_FNAME] + return self.dev_data_source in [MOBAPP] @property def no_location_data(self): @@ -1033,6 +1017,10 @@ def is_monitored(self): def is_inactive(self): return self.tracking_mode == INACTIVE_DEVICE + @property + def isnot_inactive(self): + return (not self.is_inactive) + @property def is_online(self): return not self.is_offline @@ -1041,16 +1029,14 @@ def is_online(self): def is_offline(self): ''' Returns True/False if the device is offline based on the device_status - Return False if there is no GPS location so the old location will be processed + Return False if there is no GPS location so the old location will be processed, + it was located in the last 5-mins or it is not a 201/offline code ''' - if self.no_location_data: - return False - - if (self.dev_data_device_status not in DEVICE_STATUS_OFFLINE - or self.is_data_source_FMF): - return False - - return True + if (self.no_location_data + or (self.dev_data_device_status_code == DEVICE_STATUS_OFFLINE + and mins_since(self.loc_data_secs) > 5)): + return True + return False @property def is_pending(self): @@ -1060,7 +1046,7 @@ def is_pending(self): @property def is_using_mobapp_data(self): ''' Return True/False if using MobApp data ''' - return self.dev_data_source == MOBAPP_FNAME + return self.dev_data_source == MOBAPP @property def track_from_other_zone_flag(self): @@ -1082,11 +1068,10 @@ def is_approaching_tracked_zone(self): ''' if self.FromZone_TrackFrom: if (secs_to(self.next_update_secs) <= 15 - and secs_since(self.loc_data_secs > 15) + and secs_since(self.loc_data_secs) > 15 and self.FromZone_TrackFrom.is_going_towards and self.went_3km): return True - # and self.FromZone_TrackFrom.zone_dist < 1 return False @property @@ -1263,7 +1248,7 @@ def is_statzone_timer_set(self): @property def in_statzone_interval_secs(self): - if self.FromZone_Home.calc_dist < 180 or self.mobapp_monitor_flag is False: + if self.FromZone_Home.calc_dist_km < 180 or self.mobapp_monitor_flag is False: return self.statzone_inzone_interval_secs return Gb.max_interval_secs / 2 @@ -1338,7 +1323,7 @@ def resume_tracking(self, interval_secs=0): Gb.iCloud3.initialize_5_sec_loop_control_flags() - if Gb.primary_data_source_ICLOUD is False or self.is_data_source_ICLOUD is False: + if Gb.use_data_source_ICLOUD is False or self.is_data_source_ICLOUD is False: self.write_ha_sensor_state(NEXT_UPDATE, '___') return @@ -1351,7 +1336,7 @@ def resume_tracking(self, interval_secs=0): log_exception(err) #-------------------------------------------------------------------- - def reset_tracking_fields(self, interval_secs=0): + def reset_tracking_fields(self, interval_secs=0, max_error_cnt_reached=False): ''' Reset all tracking fields @@ -1368,6 +1353,8 @@ def reset_tracking_fields(self, interval_secs=0): self.next_update_time = self.FromZone_Home.next_update_time self.old_loc_cnt = 0 + if max_error_cnt_reached is False: + self.max_error_cycle_cnt = 0 self.old_loc_msg = '' self.poor_gps_flag = False self.outside_no_exit_trigger_flag = False @@ -1667,7 +1654,9 @@ def _update_restore_state_values(self): Gb.restore_state_devices[self.devicename] = {} Gb.restore_state_devices[self.devicename]['last_update'] = datetime_now() - Gb.restore_state_devices[self.devicename]['sensors'] = copy.deepcopy(self.sensors) + Gb.restore_state_devices[self.devicename]['sensors'] = copy.deepcopy(self.sensors) + Gb.restore_state_devices[self.devicename]['mobapp'] = self._restore_state_save_mobapp_items() + Gb.restore_state_devices[self.devicename]['other'] = self._restore_state_save_other_items() Gb.restore_state_devices[self.devicename]['from_zone'] = {} for from_zone, FromZone in self.FromZones_by_zone.items(): @@ -1676,6 +1665,80 @@ def _update_restore_state_values(self): restore_state.write_storage_icloud3_restore_state_file() #-------------------------------------------------------------------- + def _restore_state_save_mobapp_items(self): + ''' + Build dictionary of Mobile App items to be saved in restore_state file + ''' + mobapp_items = {} + mobapp_items['state'] = self.mobapp_data_state + mobapp_items['latitude'] = self.mobapp_data_latitude + mobapp_items['longitude'] = self.mobapp_data_longitude + mobapp_items['state_secs'] = self.mobapp_data_state_secs + mobapp_items['state_time'] = self.mobapp_data_state_time + mobapp_items['trigger_secs'] = self.mobapp_data_trigger_secs + mobapp_items['trigger_time'] = self.mobapp_data_trigger_time + mobapp_items['secs'] = self.mobapp_data_secs + mobapp_items['time'] = self.mobapp_data_time + mobapp_items['trigger'] = self.mobapp_data_trigger + mobapp_items['battery_level'] = self.mobapp_data_battery_level + mobapp_items['battery_status'] = self.mobapp_data_battery_status + mobapp_items['battery_update_secs'] = self.mobapp_data_battery_update_secs + return mobapp_items + + +#-------------------------------------------------------------------- + def _restore_state_reset_mobapp_items(self): + + try: + mobapp_items = Gb.restore_state_devices[self.devicename]['mobapp'] + + self.mobapp_data_state = mobapp_items['state'] + self.mobapp_data_latitude = mobapp_items['latitude'] + self.mobapp_data_longitude = mobapp_items['longitude'] + self.mobapp_data_state_secs = mobapp_items['state_secs'] + self.mobapp_data_state_time = mobapp_items['state_time'] + self.mobapp_data_trigger_secs = mobapp_items['trigger_secs'] + self.mobapp_data_trigger_time = mobapp_items['trigger_time'] + self.mobapp_data_secs = mobapp_items['secs'] + self.mobapp_data_time = mobapp_items['time'] + self.mobapp_data_trigger = mobapp_items['trigger'] + self.mobapp_data_battery_level = mobapp_items['battery_level'] + self.mobapp_data_battery_status = mobapp_items['battery_status'] + self.mobapp_data_battery_update_secs = mobapp_items['battery_update_secs'] + + except Exception as err: + #log_exception(err) + pass + +#-------------------------------------------------------------------- + def _restore_state_save_other_items(self): + ''' + Build dictionary of Otheritems to be saved in restore_state file + ''' + other_items = {} + other_items['zone_change_secs'] = self.zone_change_secs + other_items['zone_change_datetime'] = self.zone_change_datetime + + return other_items + +#-------------------------------------------------------------------- + def _restore_state_reset_other_items(self): + + try: + other_items = Gb.restore_state_devices[self.devicename]['other'] + + self.zone_change_secs = other_items['zone_change_secs'] + self.zone_change_datetime = other_items['zone_change_datetime'] + + except Exception as err: + #log_exception(err) + pass + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# UPDATE SENSORS FUNCTIONS +# +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> @property def badge_sensor_value(self): """ Determine the badge sensor state value """ @@ -1686,13 +1749,12 @@ def badge_sensor_value(self): sensor_value = PAUSED_CAPS # Display zone name if in a zone - # elif self.loc_data_zone != NOT_HOME and self.isnotin_statzone: elif self.isin_zone and self.isnotin_statzone: sensor_value = self.loc_data_zone_fname # Display the distance to Home elif self.FromZone_Home: - sensor_value = (self.FromZone_Home.zone_dist) + sensor_value = (km_to_um(self.FromZone_Home.zone_dist_km)) else: sensor_value = BLANK_SENSOR_FIELD @@ -1730,12 +1792,12 @@ def _set_battery_sensor_values(self): 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_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_ICLOUD] = self.battery_info[ICLOUD] + self.sensors[BATTERY_MOBAPP] = self.battery_info[MOBAPP] + if self.dev_data_battery_source == ICLOUD: + self.sensors[BATTERY_LATEST] = f"(iCloud) {self.sensors[BATTERY_ICLOUD]}" + self.sensors[BATTERY_ICLOUD] = f"(Latest) {self.sensors[BATTERY_ICLOUD]}" + elif self.dev_data_battery_source == MOBAPP: self.sensors[BATTERY_LATEST] = f"(MobApp) {self.sensors[BATTERY_MOBAPP]}" self.sensors[BATTERY_MOBAPP] = f"(Latest) {self.sensors[BATTERY_MOBAPP]}" @@ -1773,7 +1835,7 @@ def calculate_old_location_threshold(self): threshold_secs = interval_secs * .025 # 2.5% of interval_secs time if threshold_secs < 120: threshold_secs = 120 - elif self.FromZone_BeingUpdated.zone_dist > 5: + elif self.FromZone_BeingUpdated.zone_dist_km > 5: threshold_secs = 180 elif interval_secs < 90: @@ -1845,6 +1907,7 @@ def is_location_data_rejected(self): # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + # Used in sensor._format_devices_distance_extra_attrs to build device dust sttrs def other_device_distance(self, other_devicename): return self.dist_to_other_devices[other_devicename][0] @@ -1853,6 +1916,8 @@ def update_distance_to_other_devices(self): Cycle through all devices and update this device's and the other device's dist_to_other_device_info field + This is run when the self device's location is updated. + {devicename: [distance_m, gps_accuracy_factor, loc_time (newer), display_text]} ''' update_at_time = secs_to_hhmm(self.loc_data_secs) @@ -1868,19 +1933,29 @@ def update_distance_to_other_devices(self): loc_data_time = secs_to_hhmm(_Device.loc_data_secs) time_msg = f" ({loc_data_time})" display_text = f"{m_to_um(dist_apart_m)}{gps_msg}{time_msg}" - dist_apart_data = [dist_apart_m, min_gps_accuracy, _Device.loc_data_secs, display_text] - if (_devicename not in self.dist_to_other_devices - or self.devicename not in _Device.dist_to_other_devices - or _Device.dist_to_other_devices[self.devicename] != dist_apart_data - or self.dist_to_other_devices[_devicename] != dist_apart_data): + time_msg2 = f" (^SECS={_Device.loc_data_secs}^)" + display_text2 = f"{m_to_um(dist_apart_m)}{gps_msg}{time_msg2}" + dist_apart_data2 = [dist_apart_m, min_gps_accuracy, _Device.loc_data_secs, display_text2] + + # if (_devicename not in self.dist_to_other_devices + # or self.devicename not in _Device.dist_to_other_devices + # or _Device.dist_to_other_devices[self.devicename] != dist_apart_data + # or self.dist_to_other_devices[_devicename] != dist_apart_data): + try: self.dist_to_other_devices[_devicename] = dist_apart_data _Device.dist_to_other_devices[self.devicename] = dist_apart_data + self.dist_to_other_devices2[_devicename] = \ + _Device.dist_to_other_devices2[self.devicename] = dist_apart_data2 + Gb.dist_to_other_devices_update_sensor_list.add(self.devicename) Gb.dist_to_other_devices_update_sensor_list.add(_devicename) + except Exception as err: + log_exception(err) + #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # # UPDATE BATTERY INFORMATION @@ -1888,7 +1963,7 @@ def update_distance_to_other_devices(self): #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> def update_battery_data_from_mobapp(self): ''' - Update the battery info from the Mobile App if the Mobile App data is newer than the FamShr + Update the battery info from the Mobile App if the Mobile App data is newer than the iCloud battery info. Then update the sensors if it has changed. sensor.gary_iphone_app_battery_level entity_attrs={'unit_of_measurement': '%', 'device_class': @@ -1904,25 +1979,25 @@ def update_battery_data_from_mobapp(self): ''' if (self.mobapp_monitor_flag is False or Gb.conf_data_source_MOBAPP is False + or self.mobapp.get(BATTERY_LEVEL) is None or self.is_dev_data_source_NOT_SET or Gb.start_icloud3_inprocess_flag): - return + return False try: battery_level_attrs = entity_io.get_attributes(self.mobapp[BATTERY_LEVEL]) - if STATE not in battery_level_attrs: return False - + battery_level = int(battery_level_attrs[STATE]) battery_update_secs = \ - max(battery_level_attrs[LAST_UPDATED_SECS], battery_level_attrs[LAST_CHANGED_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 - 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 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}> {s2t(battery_update_secs)=} {s2t(self.mobapp_data_battery_update_secs)=} {format_age(battery_update_secs - self.mobapp_data_battery_update_secs)}", battery_level_attrs) if battery_level > 99: battery_status = 'Charged' @@ -1942,8 +2017,7 @@ def update_battery_data_from_mobapp(self): self.mobapp_data_battery_status = battery_status self._update_battery_data_and_sensors( - MOBAPP_FNAME, battery_update_secs, battery_level, battery_status) - + MOBAPP, battery_update_secs, battery_level, battery_status) # self.write_ha_sensors_state([BATTERY, BATTERY_STATUS]) return True @@ -2016,15 +2090,15 @@ def update_dev_loc_data_from_raw_data_MOBAPP(self, RawData=None): self.last_data_update_secs = time_now_secs() - self.dev_data_source = MOBAPP_FNAME + self.dev_data_source = MOBAPP self.dev_data_fname = self.fname self.dev_data_device_class = self.device_type self.dev_data_device_status = "Online" self.dev_data_device_status_code = 200 self._update_battery_data_and_sensors( - MOBAPP_FNAME, self.mobapp_data_battery_update_secs, - self.mobapp_data_battery_level, self.mobapp_data_battery_status) + MOBAPP, 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 @@ -2044,12 +2118,12 @@ def update_dev_loc_data_from_raw_data_MOBAPP(self, RawData=None): self.display_update_location_msg() #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - def update_dev_loc_data_from_raw_data_FAMSHR_FMF(self, RawData, requesting_device_flag=True): + def update_dev_loc_data_from_raw_data_FAMSHR(self, RawData, requesting_device_flag=True): ''' - Update the Device's location data with the RawData (FamShr or FmF) from the iCloud Account. + Update the Device's location data with the RawData (iCloud) from the iCloud Account. Parameters: - RawData - FamShr or FmF object to be used to update this Device + RawData - iCloud object to be used to update this Device requesting_device_flag - Multiple devices can be updated since all device info is returned from iCloud on a location request. True- This is the Device that requested the update and the Update Location @@ -2083,9 +2157,9 @@ def update_dev_loc_data_from_raw_data_FAMSHR_FMF(self, RawData, requesting_devic icloud_rawdata_battery_level = 0 icloud_rawdata_battery_status = UNKNOWN - if RawData.is_data_source_FAMSHR: + if RawData.is_data_source_ICLOUD: self._update_battery_data_and_sensors( - FAMSHR_FNAME, location_secs, + ICLOUD, location_secs, icloud_rawdata_battery_level, icloud_rawdata_battery_status) self.dev_data_device_status_code = RawData.device_data.get(ICLOUD_DEVICE_STATUS, 0) @@ -2141,11 +2215,14 @@ def update_sensor_values_from_data_fields(self): try: self._set_next_FromZone_to_update() + det_interval.set_dist_to_devices(post_event_msg=False) + det_interval.format_dist_to_devices_msg(self, time=True, age=False) # 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._set_battery_sensor_values() + # Device related sensors self.sensors[DEVICE_STATUS] = self.device_status self.sensors[LOW_POWER_MODE] = self.dev_data_low_power_mode @@ -2167,13 +2244,14 @@ def update_sensor_values_from_data_fields(self): self.sensors[LAST_LOCATED_TIME] = self.loc_data_time self.sensors[LAST_LOCATED] = self.loc_data_time self.sensors[LAST_LOCATED_SECS] = self.loc_data_secs - self.sensors[DISTANCE_TO_DEVICES] = self.dist_apart_msg.rstrip(', ') + # self.sensors[DISTANCE_TO_DEVICES] = self.dist_apart_msg.rstrip(', ') + self.sensors[DISTANCE_TO_DEVICES] = det_interval.format_dist_to_devices_msg(self, time=True, age=False) self.sensors[MOVED_DISTANCE] = self.loc_data_dist_moved_km self.sensors[MOVED_TIME_FROM] = self.loc_data_time_moved_from 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: + 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 @@ -2360,8 +2438,8 @@ def format_info_msg(self): elif self.zone_change_secs > 0: if self.isin_zone: - info_msg +=( f"@{zone_dname(self.loc_data_zone)}-" - f"{format_time_age(self.zone_change_secs)}, ") + info_msg +=( f"{zone_dname(self.loc_data_zone)}-" + f"{self.sensors[ARRIVAL_TIME]}, ") elif self.mobapp_zone_exit_zone != '': info_msg +=(f"Left-{zone_dname(self.mobapp_zone_exit_zone)}-" f"{format_time_age(self.mobapp_zone_exit_secs)}, ") @@ -2376,7 +2454,7 @@ def format_info_msg(self): if self.NearDeviceUsed: info_msg +=(f"UsedNearbyDevice-{self.NearDeviceUsed.fname}, " - f"({m_to_um_ft(self.near_device_distance, as_integer=True)}") + f"({m_to_um_ft(self.near_device_distance, as_integer=True)}, ") # if self.data_source != self.dev_data_source.lower(): #info_msg += f"LocationData-{self.dev_data_source}, " @@ -2386,19 +2464,15 @@ def format_info_msg(self): if (self.mobapp_monitor_flag and secs_since(self.mobapp_data_secs) > 3600): - info_msg += (f"MobApp LastUpdate-" - f"{format_age(self.mobapp_data_secs)}, ") + info_msg += (f"MobApp LastUpdate-{format_age(self.mobapp_data_secs)}, ") if self.mobapp_request_loc_last_secs > 0: - info_msg += f", MobApp LocRequest-{format_age(self.mobapp_request_loc_last_secs)}" + info_msg += f"MobApp LocRequest-{format_age(self.mobapp_request_loc_last_secs)}, " if self.is_gps_poor: - info_msg += (f"PoorGPS-±{self.loc_data_gps_accuracy}m " - f"#{self.old_loc_cnt}") - if (is_zone(self.loc_data_zone) - and Gb.discard_poor_gps_inzone_flag): - info_msg += "(Ignored)" - info_msg += ", " + info_msg += (f"PoorGPS-±{self.loc_data_gps_accuracy}m #{self.old_loc_cnt}, ") + if is_zone(self.loc_data_zone) and Gb.discard_poor_gps_inzone_flag: + info_msg = f"{info_msg[:-2]} (Ignored), " if self.old_loc_cnt > 3: info_msg += f"LocationOld-{format_age(self.loc_data_secs)} (#{self.old_loc_cnt}), " @@ -2414,7 +2488,6 @@ def format_info_msg(self): self.info_msg = info_msg if info_msg else \ f'iCloud3 v{Gb.version}, Running for {format_secs_since(Gb.started_secs)}' - return self.info_msg #------------------------------------------------------------------- diff --git a/custom_components/icloud3/device_fm_zone.py b/custom_components/icloud3/device_fm_zone.py index 3c6749b..b1af5e1 100644 --- a/custom_components/icloud3/device_fm_zone.py +++ b/custom_components/icloud3/device_fm_zone.py @@ -32,7 +32,7 @@ NEXT_UPDATE, NEXT_UPDATE_TIME, NEXT_UPDATE_DATETIME, ) from .helpers.dist_util import (gps_distance_km, km_to_um,) -from .helpers.messaging import (log_exception, post_internal_error, _trace, _traceha, ) +from .helpers.messaging import (log_exception, post_internal_error, _evlog, _log, ) import homeassistant.util.dt as dt_util import traceback @@ -76,13 +76,13 @@ def initialize(self): self.next_update_secs = 0 self.next_update_devicenames = '' self.waze_time = 0 - self.waze_dist = 0 - self.calc_dist = 0 - self.zone_dist = 0 + self.waze_dist_km = 0 + self.calc_dist_km = 0 + self.zone_dist_km = 0 self.zone_dist_m = 0 self.zone_center_dist = 0 self.waze_results = None - self.home_dist = gps_distance_km(Gb.HomeZone.gps, self.FromZone.gps) + self.home_dist_km = gps_distance_km(Gb.HomeZone.gps, self.FromZone.gps) self.max_dist_km = 0 self.sensor_prefix = (f"sensor.{self.devicename}_") \ @@ -139,7 +139,7 @@ def __repr__(self): #-------------------------------------------------------------------- @property def zone_distance_str(self): - return ('' if self.zone_dist == 0 else (f"{km_to_um(self.zone_dist)}")) + return ('' if self.zone_dist_km == 0 else (f"{km_to_um(self.zone_dist_km)}")) @property def distance_km(self): diff --git a/custom_components/icloud3/device_tracker.py b/custom_components/icloud3/device_tracker.py index 8f43422..6e33d6f 100644 --- a/custom_components/icloud3/device_tracker.py +++ b/custom_components/icloud3/device_tracker.py @@ -1,14 +1,14 @@ """Support for tracking for iCloud devices.""" from .global_variables import GlobalVariables as Gb -from .const import (DOMAIN, ICLOUD3, +from .const import (DOMAIN, ICLOUD3, ICLOUD3_VERSION_MSG, DISTANCE_TO_DEVICES, NOT_SET, HOME, DEVICE_TYPE_ICONS, BLANK_SENSOR_FIELD, DEVICE_TRACKER_STATE, INACTIVE_DEVICE, NAME, FNAME, PICTURE, ALERT, - LATITUDE, LONGITUDE, GPS, LOCATION_SOURCE, TRIGGER, + DEVICE_TRACKER, LATITUDE, LONGITUDE, GPS, LOCATION_SOURCE, TRIGGER, ZONE, ZONE_DATETIME, LAST_ZONE, FROM_ZONE, ZONE_FNAME, BATTERY, BATTERY_LEVEL, MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, @@ -31,7 +31,7 @@ from .helpers.messaging import (post_event, log_info_msg, log_debug_msg, log_error_msg, log_exception, log_exception_HA, log_info_msg_HA, - _trace, _traceha, ) + _evlog, _log, ) from .helpers.time_util import (adjust_time_hour_values, secs_to_datetime) from .support import start_ic3 from .support import config_file @@ -65,7 +65,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e try: if Gb.conf_file_data == {}: start_ic3.initialize_directory_filenames() - config_file.load_storage_icloud3_configuration_file() + # config_file.load_storage_icloud3_configuration_file() + await config_file.async_load_storage_icloud3_configuration_file() try: Gb.conf_devicenames = [conf_device[CONF_IC3_DEVICENAME] @@ -97,12 +98,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e # Set the total count of the device_trackers that will be created if Gb.device_trackers_cnt == 0: Gb.device_trackers_cnt = len(NewDeviceTrackers) - log_info_msg(f'Device Tracker Entities: {Gb.device_trackers_cnt}') + post_event(f"Device Tracker Entities > Created-{Gb.device_trackers_cnt}") if NewDeviceTrackers != []: async_add_entities(NewDeviceTrackers, True) + _get_ha_device_ids_from_device_registry(hass) - log_info_msg_HA(f"iCloud3 Device Tracker entities: {Gb.device_trackers_cnt}") + log_info_msg_HA(f"{ICLOUD3} Device Tracker entities: {Gb.device_trackers_cnt}") Devices_no_area = [Device for Device in Gb.DeviceTrackers_by_devicename.values() \ if Device.ha_area_id in [None, 'unknown', '']] @@ -179,9 +181,9 @@ def _get_ha_device_id_from_device_entry(hass, device, device_entry): id='9045bf3f0363c28957353cf2c47163d0', orphaned_timestamp=None ''' try: - if device_entry.name in [DOMAIN, ICLOUD3, 'iCloud3 Integration']: - Gb.ha_device_id_by_devicename[ICLOUD3] = device_entry.id - Gb.ha_area_id_by_devicename[ICLOUD3] = device_entry.area_id + if device_entry.name in [DOMAIN, DOMAIN, f'{ICLOUD3} Integration']: + Gb.ha_device_id_by_devicename[DOMAIN] = device_entry.id + Gb.ha_area_id_by_devicename[DOMAIN] = device_entry.area_id return except: pass @@ -213,6 +215,9 @@ def __init__(self, devicename, conf_device, data=None): self.devicename = devicename self.Device = None # Filled in after Device object has been created in start_ic3 self.entity_id = f"device_tracker.{devicename}" + # If the DOMAIN is not 'icloud3' ('iCloud3_dev'), add it to the device_tracker + # entity name to make it unique + if DOMAIN != 'icloud3': self.entity_id += f"_{DOMAIN}" self.ha_device_id = Gb.ha_device_id_by_devicename.get(self.devicename) self.ha_area_id = Gb.ha_area_id_by_devicename.get(self.devicename) @@ -297,20 +302,13 @@ def location_accuracy(self): @property def latitude(self): """Return latitude value of the device.""" - # return self.Device.sensors[LATITUDE] return self._get_sensor_value(LATITUDE, number=True) @property def longitude(self): """Return longitude value of the device.""" - # return self.Device.sensors[LONGITUDE] return self._get_sensor_value(LONGITUDE, number=True) - # @property - # def gps(self): - # """Return gps value of the device.""" - # return (self.latitude, self.longitude) - @property def battery_level(self): """Return the battery level of the device.""" @@ -357,18 +355,33 @@ def _get_extra_attributes(self): self.extra_attrs_away_time_zone_offset = \ f"HomeZone {plus_minus}{self.Device.away_time_zone_offset} hours" + icloud3 = ICLOUD3.lower() extra_attrs = {} - 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"{'-'*5} DEVICE CONFIGURATION {'-'*20}"] = '' - extra_attrs['integration'] = ICLOUD3 - extra_attrs[NAME] = self._get_sensor_value(NAME) - extra_attrs[PICTURE] = self._get_sensor_value(PICTURE) - extra_attrs['picture_file'] = self._get_sensor_value(PICTURE) + extra_attrs['integration'] = icloud3 + + if self.Device: + if self.Device.PyiCloud: + extra_attrs['apple_account'] = self.Device.PyiCloud.account_owner_username + else: + extra_attrs['apple_account'] = 'None' + if self.Device.mobapp[DEVICE_TRACKER]: + extra_attrs['mobile_app'] = self.Device.mobapp[DEVICE_TRACKER] + else: + extra_attrs['mobile_app'] = 'None' + + extra_attrs[NAME] = self._get_sensor_value(NAME) + picture = self._get_sensor_value(PICTURE) + if instr(picture,'None'): + extra_attrs['picture_file'] = 'None' + else: + extra_attrs[PICTURE] = picture + extra_attrs['picture_file'] = self._get_sensor_value(PICTURE) extra_attrs['track_from_zones'] = self.extra_attrs_track_from_zones extra_attrs['primary_home_zone'] = self.extra_attrs_primary_home_zone extra_attrs['away_time_zone_offset'] = self.extra_attrs_away_time_zone_offset @@ -387,15 +400,15 @@ def _get_extra_attributes(self): extra_attrs[WAZE_DISTANCE] = self._get_sensor_value(WAZE_DISTANCE) extra_attrs[DISTANCE_TO_DEVICES] = self._get_sensor_value(DISTANCE_TO_DEVICES) extra_attrs[ZONE_DATETIME] = self._get_sensor_value(ZONE_DATETIME) - extra_attrs[LAST_UPDATE] = self._get_sensor_value(LAST_UPDATE_DATETIME) + #extra_attrs[LAST_UPDATE] = self._get_sensor_value(LAST_UPDATE_DATETIME) extra_attrs[NEXT_UPDATE] = self._get_sensor_value(NEXT_UPDATE_DATETIME) extra_attrs['last_timestamp']= f"{self._get_sensor_value(LAST_LOCATED_SECS)}" extra_attrs[f"{'-'*5} ICLOUD3 CONFIGURATION {'-'*19}"] = '' extra_attrs['icloud3_devices'] = ', '.join(Gb.Devices_by_devicename.keys()) - extra_attrs['icloud3_version'] = f"v{Gb.version}" + extra_attrs[f'{icloud3}_version'] = f"v{Gb.version}" extra_attrs['event_log_version'] = f"v{Gb.version_evlog}" - extra_attrs['icloud3_directory'] = Gb.icloud3_directory + extra_attrs[f'{icloud3}_directory'] = Gb.icloud3_directory return extra_attrs @@ -548,7 +561,7 @@ def after_removal_cleanup(self): and called by HA after processing the async_remove request """ - log_info_msg(f"Registered device_tracker.icloud3 entity removed: {self.entity_id}") + log_info_msg(f"Unregistered device_tracker.icloud3 entity removed: {self.entity_id}") self._remove_from_registries() self.entity_removed_flag = True diff --git a/custom_components/icloud3/event_log_card/icloud3-event-log-card.js b/custom_components/icloud3/event_log_card/icloud3-event-log-card.js index 0d9545c..9a791a3 100644 --- a/custom_components/icloud3/event_log_card/icloud3-event-log-card.js +++ b/custom_components/icloud3/event_log_card/icloud3-event-log-card.js @@ -22,7 +22,7 @@ class iCloud3EventLogCard extends HTMLElement { } //--------------------------------------------------------------------------- setConfig(config) { - const version = "3.0.17" + const version = "3.1" const cardTitle = "iCloud3 v3 - Event Log" const root = this.shadowRoot @@ -170,14 +170,14 @@ class iCloud3EventLogCard extends HTMLElement { btnAction.appendChild(btnActionGenResume) var btnActionGenLoc = document.createElement("option") - var btnActionGenLocTxt = document.createTextNode("Locate All Devices with iCloud FamShr") + var btnActionGenLocTxt = document.createTextNode("Locate All Devices using iCloud") btnActionGenLoc.setAttribute("value", "locate") btnActionGenLoc.classList.add("btnActionOption") btnActionGenLoc.appendChild(btnActionGenLocTxt) btnAction.appendChild(btnActionGenLoc) var btnActionGenMobRqst = document.createElement("option") - var btnActionGenMobRqstTxt = document.createTextNode("Send Locate Request to Mobile App") + var btnActionGenMobRqstTxt = document.createTextNode("Send Locate Requests to Mobile App") btnActionGenMobRqst.setAttribute("value", "location") btnActionGenMobRqst.classList.add("btnActionOption") btnActionGenMobRqst.appendChild(btnActionGenMobRqstTxt) @@ -191,21 +191,21 @@ class iCloud3EventLogCard extends HTMLElement { btnAction.appendChild(btnActionDevGrp) var btnActionDevPause = document.createElement("option") - var btnActionDevPauseTxt = document.createTextNode("Pause Tracking This Device") + var btnActionDevPauseTxt = document.createTextNode("Pause Tracking this Device") btnActionDevPause.setAttribute("value", "dev-pause") btnActionDevPause.classList.add("btnActionOption") btnActionDevPause.appendChild(btnActionDevPauseTxt) btnAction.appendChild(btnActionDevPause) var btnActionDevResume = document.createElement("option") - var btnActionDevResumeTxt = document.createTextNode("Resume Tracking This Device") + var btnActionDevResumeTxt = document.createTextNode("Resume Tracking this Device") btnActionDevResume.setAttribute("value", "dev-resume") btnActionDevResume.classList.add("btnActionOption") btnActionDevResume.appendChild(btnActionDevResumeTxt) btnAction.appendChild(btnActionDevResume) var btnActionDevLoc = document.createElement("option") - var btnActionDevLocTxt = document.createTextNode("Locate This Device with iCloud FamShr") + var btnActionDevLocTxt = document.createTextNode("Locate this Device using iCloud") btnActionDevLoc.setAttribute("value", "dev-locate") btnActionDevLoc.classList.add("btnActionOption") btnActionDevLoc.appendChild(btnActionDevLocTxt) @@ -219,14 +219,14 @@ class iCloud3EventLogCard extends HTMLElement { btnAction.appendChild(btnActionDevMobRqst) var btnActionDevFind = document.createElement("option") - var btnActionDevFindTxt = document.createTextNode("Send Find-My-iPhone Alert (FamShr)") + var btnActionDevFindTxt = document.createTextNode("Send Find-My-iPhone Alert to iCloud") btnActionDevFind.setAttribute("value", "dev-find-iphone-alert") btnActionDevFind.classList.add("btnActionOption") btnActionDevFind.appendChild(btnActionDevFindTxt) btnAction.appendChild(btnActionDevFind) // var btnActionDevLostLost = document.createElement("option") - // var btnActionDevLostLostTxt = document.createTextNode("Send Lost-Device Alert (FamShr)") + // var btnActionDevLostLostTxt = document.createTextNode("Send Lost-Device Alert to iCloud") // btnActionDevLostLost.setAttribute("value", "dev-lost-device-alert") // btnActionDevLostLost.classList.add("btnActionOption") // btnActionDevLostLost.appendChild(btnActionDevLostLostTxt) @@ -292,13 +292,13 @@ class iCloud3EventLogCard extends HTMLElement { btnActionOptOC7.appendChild(btnActionOptOC7Txt) btnAction.appendChild(btnActionOptOC7) - var btnActionOptOC6 = document.createElement("option") - var btnActionOptOC6Txt = document.createTextNode("Request Apple ID Verification Code") - btnActionOptOC6.setAttribute("value", "reset_session") - btnActionOptOC6.setAttribute("id", "optResetPyicloud") - btnActionOptOC6.classList.add("btnActionOption") - btnActionOptOC6.appendChild(btnActionOptOC6Txt) - btnAction.appendChild(btnActionOptOC6) + // var btnActionOptOC6 = document.createElement("option") + // var btnActionOptOC6Txt = document.createTextNode("Request Apple ID Verification Code") + // btnActionOptOC6.setAttribute("value", "reset_session") + // btnActionOptOC6.setAttribute("id", "optResetPyicloud") + // btnActionOptOC6.classList.add("btnActionOption") + // btnActionOptOC6.appendChild(btnActionOptOC6Txt) + // btnAction.appendChild(btnActionOptOC6) var btnActionOptVer = document.createElement("optGroup") btnActionOptVer.setAttribute("label", "Version Information") @@ -511,7 +511,7 @@ class iCloud3EventLogCard extends HTMLElement { background-color: rgba(var(--rgb-primary-color), 0.85); border-top: 1px solid rgba(108, 204, 249, .5); border-bottom: 1px solid rgba(108, 204, 249, .5); - font-weight: 450; + font-weight: 500; } .updateRecdHdrTime {color: black; background-color: rgba(var(--rgb-primary-color), 0.85); @@ -521,7 +521,7 @@ class iCloud3EventLogCard extends HTMLElement { .updateEdgeBar {border-left: 2px solid var(--dark-primary-color);} .highlightBar {color: white; background-color: green; - font-weight: 450; + font-weight: 500; border-top: 1px solid darkseagreen; border-bottom: 1px solid darkseagreen; } @@ -529,7 +529,7 @@ class iCloud3EventLogCard extends HTMLElement { .iC3StartingHdr {color: white; background-color: chocolate; - font-weight: 450; + font-weight: 500; border-top: 1px solid chocolate; border-bottom: 1px solid chocolate; } @@ -540,7 +540,7 @@ class iCloud3EventLogCard extends HTMLElement { } .stageRecdHdr {color: white; background-color: peru; - font-weight: 450; + font-weight: 500; border-top: 1px solid peru; border-bottom: 1px solid peru; } @@ -792,7 +792,7 @@ class iCloud3EventLogCard extends HTMLElement { /*visibility: visible;*/ font-family: Roboto,sans-serif; font-size: 14px; - font-weight: bolder; + font-weight: 500; color: var(--primary-text-color); /*background-color: transparent;*/ background-color: rgba(var(--rgb-primary-text-color), 0.05); @@ -842,7 +842,7 @@ class iCloud3EventLogCard extends HTMLElement { #btnAction:hover {border: 1px solid var(--primary-color);} .btnAction { background: darkred; - font-weight: bolder; + font-weight: 500; height: 24px; width: 80px; border-radius: 3px; @@ -891,9 +891,9 @@ class iCloud3EventLogCard extends HTMLElement { .title {font-size: 18px;} .btnBaseFormat {margin: 0px 2px 4px 0px; padding: 1px 3px;) .btnAction {width: 45px; height: 22px;} - .updateRecd {font-weight: 450;} + .updateRecd {font-weight: 500;} - .ic3StartupMsg {font-weight: 450;} + .ic3StartupMsg {font-weight: 500;} .tblEvlogBody tr:nth-child(even) {background-color: #EEF2F5;} ::-webkit-scrollbar {width: 1px;} ::-webkit-scrollbar-thumb {background: rgba(var(--rgb-accent-color), 0.7);} @@ -903,9 +903,9 @@ class iCloud3EventLogCard extends HTMLElement { @media only screen and (min-device-width : 768px) and (max-device-width : 1024px) { - .updateRecd {font-weight: 450;} + .updateRecd {font-weight: 500;} .updateEdgeBar {border-left-width: 2px;} - .ic3StartupMsg {font-weight: 450;} + .ic3StartupMsg {font-weight: 500;} .tblEvlogBody tr:nth-child(even) {background-color: #EEF2F5;} ::-webkit-scrollbar {width: 1px;} ::-webkit-scrollbar-thumb {background: rgba(var(--rgb-accent-color), 0.7);} @@ -1477,6 +1477,7 @@ class iCloud3EventLogCard extends HTMLElement { cancelEdgeBarFlag = true classHeaderBar = ' highlightBar' classEdgeBar = ' highlightEdgeBar' + classTime = ' highlightBar' // ^g^ = iCloud3 Stage # Header } else if (tText.startsWith("^g^")) { @@ -1583,6 +1584,11 @@ class iCloud3EventLogCard extends HTMLElement { classTime += classRecdType + classHeaderBar classTime += ' colTimeTextRow' + if (classHeaderBar.indexOf("highlightBar") > 0) { + classTime = classTime.replace("normalText", "") + classTime = classTime.replace("colTimeTextRow", "") + } + if (classTime.indexOf("Hdr") >= 0) { classText += ' noLeftEdge' classTime = classTime.replace("colTimeTextRow", "") @@ -2065,13 +2071,12 @@ class iCloud3EventLogCard extends HTMLElement { } else if (buttonId == "btnAction") { var versionMsg = "" - // if (iC3Version == null) {iC3Version = '?.?' } - // versionMsg += "iCloud3 v" + iC3Version +", " - + if (iC3Version == null) {iC3Version = '?.?' } if (EvLogLatestVersion == null) {EvLogLatestVersion = '?.?'} - versionMsg += "EventLog Latest v" + EvLogLatestVersion + versionMsg += "iCloud3 v" + iC3Version +", " + versionMsg += "EvLog v" + aboutVersion.innerText if (EvLogLatestVersion != aboutVersion.innerText) { - versionMsg += ", Running v" + aboutVersion.innerText + versionMsg += " (Avail v" + EvLogLatestVersion + ')' } this._displayInfoText(versionMsg) 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 deleted file mode 100644 index ecd60f7..0000000 Binary files a/custom_components/icloud3/event_log_card/icloud3-event-log-card.js.gz and /dev/null differ diff --git a/custom_components/icloud3/global_variables.py b/custom_components/icloud3/global_variables.py index 1b5284b..b9ebeae 100644 --- a/custom_components/icloud3/global_variables.py +++ b/custom_components/icloud3/global_variables.py @@ -24,8 +24,7 @@ #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> from .const import (DEVICENAME_MOBAPP, VERSION, VERSION_BETA, - NOT_SET, HOME_FNAME, HOME, STORAGE_DIR, WAZE_USED, - FAMSHR, FMF, FAMSHR_FMF, ICLOUD, MOBAPP, FNAME, HIGH_INTEGER, + NOT_SET, HOME_FNAME, HOME, STORAGE_DIR, WAZE_USED, DEFAULT_GENERAL_CONF, CONF_UNIT_OF_MEASUREMENT, CONF_DISPLAY_ZONE_FORMAT, CONF_DEVICE_TRACKER_STATE_SOURCE, @@ -98,8 +97,11 @@ class GlobalVariables(object): iC3EntityPlatform = None # iCloud3 Entity Platform (homeassistant.helpers.entity_component) PyiCloud = None # iCloud Account service - PyiCloudInit = None # iCloud Account service when started from __init__ via executive job - PyiCloudConfigFlow = None # iCloud Account service when started from config_flow + PyiCloudLoggingInto = None # PyiCloud being set up that can be used if the login fails + PyiCloud_needing_reauth_via_ha = {} # Reauth needed sent to ha for notify msg display + PyiCloudValidateAppleAcct = None # A session that can be used to verify the username/password + PyiCloudSession_by_username = {} # Session object for a username, set in Session so exists on an error + username_pyicloud_503_connection_error = [] # Session object for a username, set in Session so exists on an error Waze = None WazeHist = None @@ -108,14 +110,15 @@ class GlobalVariables(object): operating_mode = 0 # Platform (Legacy using configuration.yaml) or Integration ha_config_platform_stmt = False # a platform: icloud3 stmt is in the configurationyaml file that needs to be removed v2v3_config_migrated = False # Th v2 configuration parameters were migrated to v3 - add_entities = None + add_entities = None + ha_started = False # Set to True in start_ic3.ha_startup_completed from listener in __init__ # iCloud3 Directory & File Names ha_config_directory = '' # '/config', '/home/homeassistant/.homeassistant' ha_storage_directory = '' # 'config/.storage' directory ha_storage_icloud3 = '' # 'config/.storage/icloud3' - icloud3_config_filename = '' # 'config/.storage/icloud3.configuration' - iC3 Configuration File - icloud3_restore_state_filename = '' # 'config/.storage/icloud3.restore_state' + icloud3_config_filename = '' # 'config/.storage/icloud3/configuration' - iC3 Configuration File + icloud3_restore_state_filename = '' # 'config/.storage/icloud3/restore_state' config_ic3_yaml_filename = '' # 'config/config_ic3.yaml' (v2 config file name) icloud3_directory = '' ha_config_www_directory = '' @@ -130,7 +133,7 @@ class GlobalVariables(object): password = '' icloud_server_endpoint_suffix = '' encode_password_flag = True - all_famshr_devices = True + all_find_devices = True entity_registry_file = '' devices = '' @@ -141,25 +144,62 @@ class GlobalVariables(object): Devices_by_devicename_tracked = {} # All monitored Devices by devicename Devices_by_icloud_device_id = {} # Devices by the icloud device_id receive from Apple Devices_by_ha_device_id = {} # Device by the device_id in the entity/device registry - Devices_by_mobapp_devicename = {} # All verified Devices by the conf_mobapp_devicename - PairedDevices_by_paired_with_id = {} # Paired Devices by the paired_with_id (famshr prsID) id=[Dev1, Dev2] - - # FamShr Device information - These is used verify the device, display on the EvLog and in the Config Flow + Devices_by_mobapp_dname = {} # All verified Devices by the conf_mobapp_dname + + # PyiCloud objects by various categories + conf_usernames = set() # List of Apple Acct usernames in config + Devices_by_username = {} # A list of Devices for each Apple Acct username ('gary@email.com': [gary_iphone, lillian_iphone]) + owner_device_ids_by_username = {} # List of owner info for Devices in Apple Acct + owner_Devices_by_username = {} # List of Devices in the owner Apple Acct (excludes those in the iCloud list) + username_valid_by_username = {} # The username/password validation status + PyiCloud_by_devicename = {} # PyiCloud object for each ic3 devicename + PyiCloud_by_username = {} # PyiCloud object for each Apple acct username + PyiCloud_password_by_username = {} # Password for each Apple acct username + PyiCloud_logging_in_usernames = [] # A list of usernames that are currently logging in. Used to prevent another login + usernames_setup_error_retry_list = [] # A list of usernames that failed to set up in Stage 4 and need to be retried + devicenames_setup_error_retry_list= [] # A list of devices that failed to set up in Stage 4 and need to be retried + log_file_filter = [] # email extensions filter from apple accounts to remove in the icloud3-0.log file (messaging.py) + + # iCloud Device information - These is used verify the device, display on the EvLog and in the Config Flow # device selection list on the iCloud3 Devices screen devices_not_set_up = [] - device_id_by_famshr_fname = {} # Example: {'Gary-iPhone': 'n6ofM9CX4j...'} - famshr_fname_by_device_id = {} # Example: {'n6ofM9CX4j...': 'Gary-iPhone14'} - device_info_by_famshr_fname = {} # Example: {'Gary-iPhone': 'Gary-iPhone (iPhone 14 Pro (iPhone15,2)'} + device_id_by_icloud_dname = {} # Example: {'Gary-iPhone': 'n6ofM9CX4j...'} + icloud_dname_by_device_id = {} # Example: {'n6ofM9CX4j...': 'Gary-iPhone14'} + device_info_by_icloud_dname = {} # Example: {'Gary-iPhone': 'Gary-iPhone (iPhone 14 Pro (iPhone15,2)'} device_model_info_by_fname = {} # {'Gary-iPhone': [raw_model,model,model_display_name]} - dup_famshr_fname_cnt = {} # Used to create a suffix for duplicate devicenames + dup_icloud_dname_cnt = {} # Used to create a suffix for duplicate devicenames devices_without_location_data = [] + devicenames_by_icloud_dname = {} # All ic3_devicenames by conf_find_devices + icloud_dnames_by_devicename = {} # All ic3_devicenames by conf_find_devices + devicenames_by_mobapp_dname = {} # All ic3_devicenames by conf_mobapp_dname + mobapp_dnames_by_devicename = {} # All ic3_devicenames by conf_mobapp_dname + + mobapp_fnames_by_mobapp_id = {} # All mobapp_fnames by mobapp_deviceid from HA hass.data MobApp entry + mobapp_ids_by_mobapp_fname = {} # All mobapp_fnames by mobapp_deviceid from HA hass.data MobApp entry + mobapp_fnames_disabled = [] + mobile_app_device_fnames = [] # fname = name_by_user or name in mobile_app device entry + model_display_name_by_raw_model = {} + + # From HA Entity Reg file - mobapp_interface.get_entity_registry_mobile_app_devices + mobapp_id_by_mobapp_dname = {} + mobapp_dname_by_mobapp_id = {} + + device_info_by_mobapp_dname = {} # [mobapp_fname, raw_model, model, model_display_name] + # ['Gary-iPhome (MobApp)','iPhone15,2', 'iPhone', 'iPhone 14 Pro'] + last_updt_trig_by_mobapp_dname = {} + mobile_app_notify_devicename = [] + battery_level_sensors_by_mobapp_dname = {} + battery_state_sensors_by_mobapp_dname = {} + devicenames_x_famshr_devices = {} # All ic3_devicenames by conf_famshr_devices (both ways) - devicenames_x_mobapp_devicenames = {} # All ic3_devicenames by conf_mobapp_devicename (both ways) + devicenames_x_mobapp_dnames = {} # All ic3_devicenames by conf_mobapp_dname (both ways) - mobapp_fnames_x_mobapp_id = {} # All mobapp_fnames by mobapp_deviceid from HA hass.data MobApp entry (both ways) - mobapp_fnames_disabled = [] - mobile_app_device_fnames = [] # fname = name_by_user or name in mobile_app device entry + # Mobile App Integration info from hass_data['mobile_app'], Updated in start_ic3.check_mobile_app_integration + MobileApp_data = {} # data dict from hass.data['mobile_app'] + MobileApp_device_fnames = [] # fname = name_by_user or name in mobile_app device entry + MobileApp_fnames_x_mobapp_id = {} # All mobapp_fnames by mobapp_deviceid (both ways) + MobileApp_fnames_disabled = [] Zones = [] # Zones object list Zones_by_zone = {} # Zone object by zone name for HA Zones and iC3 Pseudo Zones @@ -199,9 +239,9 @@ class GlobalVariables(object): 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 + startup_lists = {} # Log variable and dictionsry field/values to icloud3-0.log file - get_FAMSHR_devices_retry_cnt = 0 # Retry count to connect to iCloud and retrieve FamShr devices + get_ICLOUD_devices_retry_cnt = 0 # Retry count to connect to iCloud and retrieve iCloud devices reinitialize_icloud_devices_flag= False # Set when no devices are tracked and iC3 needs to automatically restart reinitialize_icloud_devices_cnt = 0 @@ -265,12 +305,14 @@ class GlobalVariables(object): conf_data = {} conf_tracking = {} conf_devices = [] + conf_apple_accounts = [] conf_general = {} conf_sensors = {} conf_devicenames = [] - conf_famshr_devicenames = [] + conf_icloud_dnames = [] + conf_startup_errors_by_devicename = {} # device config apple acct & mobapp devicename errors conf_devices_idx_by_devicename = {} # Index of each device names preposition in the conf_devices parameter - conf_famshr_device_cnt = 0 # Number of devices with FamShr tracking set up + conf_icloud_device_cnt = 0 # Number of devices with iCloud tracking set up conf_fmf_device_cnt = 0 # Number of devices with FmF tracking set up conf_mobapp_device_cnt = 0 # Number of devices with Mobile App tracking set up @@ -281,14 +323,16 @@ class GlobalVariables(object): area_id_personal_device = None # restore_state file - restore_state_file_data = {} - restore_state_profile = {} - restore_state_devices = {} + restore_state_commit_time = 0 # Set a callback timmer to commit te changes to the restore_state file + restore_state_commit_cnt = 0 # Set a callback timmer to commit te changes to the restore_state file + restore_state_file_data = {} + restore_state_profile = {} + restore_state_devices = {} # This stores the type of configuration parameter change done in the config_flow module # It indicates the type of change and if a restart is required to load device or sensor changes. # Items in the set are 'tracking', 'devices', 'profile', 'sensors', 'general', 'restart' - config_flow_updated_parms = {''} + config_parms_update_control = [] distance_method_waze_flag = True icloud_force_update_flag = False @@ -312,7 +356,6 @@ class GlobalVariables(object): display_gps_lat_long_flag = DEFAULT_GENERAL_CONF[CONF_DISPLAY_GPS_LAT_LONG] center_in_zone_flag = DEFAULT_GENERAL_CONF[CONF_CENTER_IN_ZONE] display_zone_format = DEFAULT_GENERAL_CONF[CONF_DISPLAY_ZONE_FORMAT] - display_gps_lat_long_flag = DEFAULT_GENERAL_CONF[CONF_DISPLAY_GPS_LAT_LONG] # device_tracker_state_format = DEFAULT_GENERAL_CONF[CONF_DEVICE_TRACKER_STATE_FORMAT] # if device_tracker_state_format == 'display_as': device_tracker_state_format = display_zone_format @@ -351,42 +394,31 @@ class GlobalVariables(object): # Used to reset Gb.is_data_source after pyicloud/icloud account successful reset # Specifed in configuration file (set in config_flow icloud credentials screen) - conf_data_source_FAMSHR = True - conf_data_source_FMF = False - conf_data_source_MOBAPP = True - conf_data_source_ICLOUD = conf_data_source_FAMSHR or conf_data_source_FMF + conf_data_source_ICLOUD = False + conf_data_source_MOBAPP = False + conf_data_source_ICLOUD = False # A trackable device uses this data source (set in start_ic3.set trackable_devices) - used_data_source_FAMSHR = False - used_data_source_FMF = False + used_data_source_ICLOUD = False used_data_source_MOBAPP = False - mobapp_monitor_any_devices_false_flag = False + device_not_monitoring_mobapp = True # At least one device does not monitor the mobapp + device_mobapp_verify_retry_cnt = 0 + device_mobapp_verify_retry_needed = False # A device that monitors the mobapp has not been verfied as + # being set up in tme mobapp integration (mobapp_data_handler) # Primary data source being used that can be turned off if errors - primary_data_source_ICLOUD = conf_data_source_ICLOUD - primary_data_source = ICLOUD if primary_data_source_ICLOUD else MOBAPP + use_data_source_ICLOUD = False + use_data_source_MOBAPP = False + primary_data_source = None # iCloud account authorization variables - icloud_acct_error_cnt = 0 - authenticated_time = 0 - authentication_error_cnt = 0 - authentication_error_retry_secs = HIGH_INTEGER - pyicloud_refresh_time = {} # Last time Pyicloud was refreshed for the trk method - pyicloud_refresh_time[FMF] = 0 - pyicloud_refresh_time[FAMSHR] = 0 - - # Pyicloud counts, times and common variables force_icloud_update_flag = False - pyicloud_auth_started_secs = 0 - pyicloud_authentication_cnt = 0 - pyicloud_location_update_cnt = 0 - pyicloud_calls_time = 0.0 trusted_device = None verification_code = None - icloud_cookies_dir = '' - icloud_cookies_file = '' - fmf_device_verified_cnt = 0 - famshr_device_verified_cnt = 0 + icloud_cookie_directory = '' + icloud_session_directory = '' + icloud_cookie_file = '' + icloud_device_verified_cnt = 0 mobapp_device_verified_cnt = 0 authentication_alert_displayed_flag = False diff --git a/custom_components/icloud3/helpers/common.py b/custom_components/icloud3/helpers/common.py index a30c8b1..90406c1 100644 --- a/custom_components/icloud3/helpers/common.py +++ b/custom_components/icloud3/helpers/common.py @@ -1,12 +1,16 @@ from ..global_variables import GlobalVariables as Gb -from ..const import (NOT_HOME, STATIONARY, CIRCLE_LETTERS_DARK, UNKNOWN, CRLF_DOT, CRLF, ) +from ..const import (NOT_HOME, STATIONARY, + CIRCLE_LETTERS_DARK, CIRCLE_LETTERS_LITE, + UNKNOWN, CRLF_DOT, CRLF, + CONF_USERNAME, CONF_PASSWORD, ) from collections import OrderedDict -from homeassistant.util import json as json_util -from homeassistant.helpers import json as json_helpers -import os -import json +# from homeassistant.util import json as json_util +# from homeassistant.helpers import json as json_helpers +# import os +# import json +import base64 import logging _LOGGER = logging.getLogger(__name__) #_LOGGER = logging.getLogger(f"icloud3") @@ -37,7 +41,7 @@ def list_to_str(list_value, separator=None): list_valt - list to be converted separator - Strig valut that separates each item (default = ', ') ''' - if list_value == []: return '' + if list_value == [] or list_value is None: return '' separator_str = separator if separator else ', ' if None in list_value or '' in list_value: list_value = [lv for lv in list_value if lv is not None and lv != ''] @@ -52,6 +56,13 @@ def list_to_str(list_value, separator=None): def list_add(list_value, add_value): if add_value is None: return list_value + + if type(add_value) is list: + for add_item in add_value: + if add_item not in list_value: + list_value.append(add_item) + return list_value + if add_value not in list_value: list_value.append(add_value) return list_value @@ -98,6 +109,17 @@ def sort_dict_by_values(dict_value): return sorted_dict_value +#----------------------------------------------------------------------------------------- +def dict_value_to_list(key_value_dict): + """ Make a list from a dictionary's values """ + + if type(key_value_dict) is dict: + value_list = [v for v in key_value_dict.values()] + else: + value_list = list(key_value_dict) + + return value_list + #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -107,17 +129,23 @@ def sort_dict_by_values(dict_value): def instr(string, substring): ''' - Fine a substring or a list of substrings strings in a string + Find a substring or a list of substrings strings in a string ''' + if type(substring) is str: + try: + return substring in string + except: + return False + if string is None or substring is None: return False - if type(substring) is str: - substring = [substring] + # Is a list of substrings in the string + if type(substring) is list: + for substring_item in substring: + if substring_item in string: + return True - for substring_str in substring: - if str(string).find(substring_str) >= 0: - return True return False #-------------------------------------------------------------------- @@ -155,6 +183,13 @@ def inlist(string, list_items): return False +#-------------------------------------------------------------------- +def is_empty(list_dict_str): + return not list_dict_str + +def isnot_empty(list_dict_str): + return not is_empty(list_dict_str) + #-------------------------------------------------------------------- def round_to_zero(number): if isnumber(number) is False: @@ -200,7 +235,8 @@ def ordereddict_to_dict(odict_item): #-------------------------------------------------------------------- def circle_letter(field): first_letter = field[:1].lower() - return CIRCLE_LETTERS_DARK.get(first_letter, '✪') + # return CIRCLE_LETTERS_DARK.get(first_letter, '✪') + return CIRCLE_LETTERS_LITE.get(first_letter, '✪') #-------------------------------------------------------------------- def obscure_field(field): @@ -276,213 +312,26 @@ def format_list(arg_list): return (f"{CRLF_DOT}{formatted_list}") +#-------------------------------------------------------------------- +def strip_lead_comma(text): + ''' + Strip a leading special character from a text string + ''' + if text[:1] in [',', '+']: + return text[1:].strip() + else: + return text.strip() + #-------------------------------------------------------------------- def format_cnt(desc, n): return f", {desc}(#{n})" if n > 1 else '' + #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # -# PYTHON OS. FILE I/O AND OTHER UTILITIES +# PASSWORD ENCODE/DECODE FUNCTIONS # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -async def async_load_json_file(filename): - if os.path.exists(filename) is False: - return {} - - try: - data = await Gb.hass.async_add_executor_job( - json_util.load_json, - filename) - return data - - except Exception as err: - #_LOGGER.exception(err) - pass - - return {} - -#-------------------------------------------------------------------- -def load_json_file(filename): - if os.path.exists(filename) is False: - return {} - - try: - if Gb.initial_icloud3_loading_flag: - data = json_util.load_json(filename) - else: - data = Gb.hass.async_add_executor_job( - json_util.load_json, - filename) - return data - - except RuntimeError as err: - if str(err) == 'no running event loop': - data = json_util.load_json(filename) - return data - - except Exception as err: - _LOGGER.exception(err) - pass - - return {} - -#-------------------------------------------------------------------- -async def async_save_json_file(filename, data): - - try: - await Gb.hass.async_add_executor_job( - json_helpers.save_json, - filename, - data) - return True - - except Exception as err: - _LOGGER.exception(err) - pass - - return False - -#-------------------------------------------------------------------- -def save_json_file(filename, data): - - try: - # The HA event loop has not been set up yet during initialization - if Gb.initial_icloud3_loading_flag: - json_helpers.save_json(filename, data) - else: - - Gb.hass.async_add_executor_job( - json_helpers.save_json, - filename, - data) - return True - - except RuntimeError as err: - if err == 'no running event loop': - json_helpers.save_json(filename, data) - return True - - except Exception as err: - _LOGGER.exception(err) - pass - - return False - -#-------------------------------------------------------------------- -def delete_file(file_desc, directory, filename, backup_extn=None, delete_old_sv_file=False): - ''' - Delete a file. - Parameters: - directory - directory containing the file to be deleted - filename - file to be deleted - backup_extn - rename the filename to this extension before deleting - delete_old_sv_file - Some files were previously renamed to .sv before deleting - They should be deleted if they exist. - ''' - try: - file_msg = "" - directory_filename = (f"{directory}/{filename}") - - if backup_extn: - filename_bu = f"{filename.split('.')[0]}.{backup_extn}" - directory_filename_bu = (f"{directory}/{filename_bu}") - - if os.path.isfile(directory_filename_bu): - os.remove(directory_filename_bu) - file_msg += (f"{CRLF_DOT}Deleted backup file (...{filename_bu})") - - os.rename(directory_filename, directory_filename_bu) - file_msg += (f"{CRLF_DOT}Rename current file to ...{filename}.{backup_extn})") - - if os.path.isfile(directory_filename): - os.remove(directory_filename) - file_msg += (f"{CRLF_DOT}Deleted file (...{filename})") - - if delete_old_sv_file: - filename = f"{filename.split('.')[0]}.sv" - directory_filename = f"{directory_filename.split('.')[0]}.sv" - if os.path.isfile(directory_filename): - os.remove(directory_filename) - file_msg += (f"{CRLF_DOT}Deleted file (...{filename})") - - if file_msg != "": - if instr(directory, 'config'): - directory = f"config{directory.split('config')[1]}" - file_msg = f"{file_desc} file > ({directory}) {file_msg}" - # Gb.EvLog.post_event(event_msg) - - return file_msg - - except Exception as err: - Gb.HALogger.exception(err) - return "Delete error" - -#-------------------------------------------------------------------- -def get_file_list(start_dir=None, file_extn_filter=[]): - return get_file_or_directory_list( directory_list=False, - start_dir=start_dir, - file_extn_filter=file_extn_filter) - -def get_directory_list(start_dir=None): - return get_file_or_directory_list( directory_list=True, - start_dir=start_dir, - file_extn_filter=[]) - -#-------------------------------------------------------------------- -def get_file_or_directory_list(directory_list=False, start_dir=None, file_extn_filter=[]): - ''' - Return a list of directories or files in a given path - - Parameters: - - directory_list = True (List of directories), False (List of files) - - start_dir = Top level directory to start searching from ('www') - -file_extn_filter = List of files witn extensions to include (['png' 'jpg'], []) - - Can call from executor function: - directory_list, start_dir, file_filter = [False, 'www', ['png', 'jpg', 'jpeg']] - image_filenames = await Gb.hass.async_add_executor_job( - self.get_file_or_directory_list, - directory_list, - start_dir, - file_filter) - ''' - - directory_filter = ['/.', 'deleted', '/x-'] - filename_or_directory_list = [] - path_config_base = f"{Gb.ha_config_directory}/" - back_slash = '\\' - if start_dir is None: start_dir = '' - - for path, dirs, files in os.walk(f"{path_config_base}{start_dir}"): - www_sub_directory = path.replace(path_config_base, '') - in_filter_cnt = len([filter for filter in directory_filter if instr(www_sub_directory, filter)]) - if in_filter_cnt > 0 or www_sub_directory.count('/') > 4 or www_sub_directory.count(back_slash): - continue - - if directory_list: - filename_or_directory_list.append(www_sub_directory) - continue - - # Filter unwanted directories - std dirs are www/icloud3, www/cummunity, www/images - if Gb.picture_www_dirs: - valid_dir = [dir for dir in Gb.picture_www_dirs if www_sub_directory.startswith(dir)] - if valid_dir == []: - continue - - dir_filenames = [f"{www_sub_directory}/{file}" - for file in files - if (file_extn_filter - and file.rsplit('.', 1)[-1] in file_extn_filter)] - - filename_or_directory_list.extend(dir_filenames[:25]) - if len(dir_filenames) > 25: - filename_or_directory_list.append( - f"⛔ {www_sub_directory} > The first 25 files out of " - f"{len(dir_filenames)} are listed") - - return filename_or_directory_list - -#-------------------------------------------------------------------- def encode_password(password): ''' Determine if the password is encoded. @@ -544,7 +393,7 @@ def decode_password(password): return base64_decode(password) except Exception as err: - #log_exception(err) + _LOGGER.exception(err) password = password.replace('«', '').replace('»', '') return password diff --git a/custom_components/icloud3/helpers/dist_util.py b/custom_components/icloud3/helpers/dist_util.py index 1cbd27e..7b444a0 100644 --- a/custom_components/icloud3/helpers/dist_util.py +++ b/custom_components/icloud3/helpers/dist_util.py @@ -2,10 +2,11 @@ from homeassistant.util.location import distance +from ..const import (NEAR_DEVICE_DISTANCE, LT, ) from ..global_variables import GlobalVariables as Gb from .common import (round_to_zero, isnumber, ) -from .messaging import (_trace, _traceha, ) - +from .messaging import (_evlog, _log, ) +from .time_util import (format_secs_since, ) #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -28,8 +29,7 @@ def gps_distance_m(from_gps, to_gps): return 0.0 dist_m = distance(from_lat, from_long, to_lat, to_long) - #dist_m = round_to_zero(dist_m) - #dist_m = 0 if dist_m < .002 else dist_m + return round(dist_m, 8) #-------------------------------------------------------------------- @@ -39,7 +39,6 @@ def km_to_mi(dist_km): #-------------------------------------- def mi_to_km(dist_mi): return round(float(dist_mi) / Gb.um_km_mi_factor, 8) - #return round(float(dist_mi) / Gb.um_km_mi_factor, 2) #-------------------------------------- def m_to_ft(dist_m): @@ -101,9 +100,9 @@ def format_dist_m(dist_m): def format_dist_km(dist_km): if dist_km < 0: - dist_km = abs(dist_km) + dist_km = abs(dist_km) - if dist_km >= 100: return f"{dist_km:.0f}km" + if dist_km >= 100: return f"{dist_km:.0f}km".replace('.0', '') 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 @@ -115,9 +114,9 @@ def format_dist_km(dist_km): def format_dist_mi(dist_mi): if dist_mi < 0: - dist_mi = abs(dist_mi) + dist_mi = abs(dist_mi) - if dist_mi >= 100: return f"{dist_mi:.0f}mi" + if dist_mi >= 100: return f"{dist_mi:.0f}mi".replace('.0', '') 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" diff --git a/custom_components/icloud3/helpers/entity_io.py b/custom_components/icloud3/helpers/entity_io.py index 9fa83bc..efd93e3 100644 --- a/custom_components/icloud3/helpers/entity_io.py +++ b/custom_components/icloud3/helpers/entity_io.py @@ -9,7 +9,7 @@ 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, ) + _evlog, _log, ) from .time_util import (secs_to_time) from homeassistant.helpers import entity_registry, device_registry @@ -56,7 +56,7 @@ def get_state(entity_id): # When starting iCloud3, the device_tracker for the mobapp might # not have been set up yet. Catch the entity_id error here. except Exception as err: - log_exception(err) + #log_exception(err) state = NOT_SET return state @@ -355,7 +355,7 @@ def trace_device_attributes(Device, description, fct_name, attrs): return attrs_in_attrs = {} - if description.find("iCloud") >= 0 or description.find("FamShr") >= 0: + if description.find("iCloud") >= 0: attrs_base_elements = TRACE_ICLOUD_ATTRS_BASE if LOCATION in attrs: attrs_in_attrs = attrs[LOCATION] @@ -383,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"FamShr iCloud Rawdata - <{Device.devicename}> {description}", attrs) + log_rawdata(f"iCloud Rawdata - <{Device.devicename}> {description}", attrs) except Exception as err: pass diff --git a/custom_components/icloud3/helpers/file_io.py b/custom_components/icloud3/helpers/file_io.py index c7f59dd..6693ea1 100644 --- a/custom_components/icloud3/helpers/file_io.py +++ b/custom_components/icloud3/helpers/file_io.py @@ -2,14 +2,14 @@ from ..global_variables import GlobalVariables as Gb from ..const import (CRLF_DOT, ) from .common import (instr, ) -from .messaging import (log_exception, _trace, _traceha, ) +from .messaging import (log_exception, _evlog, _log, ) from collections import OrderedDict from homeassistant.util import json as json_util from homeassistant.helpers import json as json_helpers import os +import shutil import asyncio -import aiofiles.ospath import logging _LOGGER = logging.getLogger(__name__) #_LOGGER = logging.getLogger(f"icloud3") @@ -20,7 +20,7 @@ # JSON FILE UTILITIES # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -async def async_load_json_file(filename): +async def async_read_json_file(filename): if file_exists(filename) is False: return {} @@ -38,7 +38,7 @@ async def async_load_json_file(filename): return {} #-------------------------------------------------------------------- -def load_json_file(filename): +def read_json_file(filename): if file_exists(filename) is False: return {} @@ -81,24 +81,11 @@ async def async_save_json_file(filename, data): #-------------------------------------------------------------------- def save_json_file(filename, data): - try: # The HA event loop has not been set up yet during initialization - if Gb.initial_icloud3_loading_flag: - json_helpers.save_json(filename, data) - else: - - Gb.hass.async_add_executor_job( - json_helpers.save_json, - filename, - data) + json_helpers.save_json(filename, data) return True - except RuntimeError as err: - if err == 'no running event loop': - json_helpers.save_json(filename, data) - return True - except Exception as err: _LOGGER.exception(err) pass @@ -114,107 +101,128 @@ def save_json_file(filename, data): def file_exists(filename): return _os(os.path.isfile, filename) - # return _os(aiofiles.ospath.isfile, filename) -def remove_file(filename): - return _os(os.remove, filename) - # return _os(aiofiles.ospath.remove, filename) -def directory_exists(dir_name): - return _os(os.path.exists, dir_name) - # return _os(aiofiles.ospath.exists, dir_name) -def make_directory(dir_name): - if directory_exists(dir_name): - return True - return _os(os.makedirs, dir_name) - # return _os(aiofiles.ospath.makedirs, dir_name) - -def extract_filename(directory_filename): - return _os(os.path.basename, directory_filename) - # return _os(aiofiles.ospath.basename, directory_filename) - -#-------------------------------------------------------------------- -def _os(os_module, filename, on_error=None): +def delete_file(filename): try: - # if Gb.ha_started: - if True is False: - results = asyncio.run_coroutine_threadsafe( - async_os(os_module, filename), Gb.hass.loop).result() - else: - results = os_module(filename) + return _os(os.remove, filename) + except FileNotFoundError: + pass + except Exception as err: + log_exception(err) - return results +def copy_file(from_dir_filename, to_directory): + shutil.copy(from_dir_filename, to_directory) - except RuntimeError as err: - if err == 'no running event loop': - return os_module(filename) +def move_files(from_dir_filename, to_directory): + shutil.move(from_dir_filename, to_directory) +def file_size(filename): + try: + return _os(os.path.getsize, filename) + except FileNotFoundError: + pass except Exception as err: log_exception(err) - pass + return 0 - return on_error or False -#................................................................... -async def async_os(os_module, filename): - return await Gb.hass.async_add_executor_job(os_module, filename) +def extract_filename(directory_filename): + return _os(os.path.basename, directory_filename) + + +def directory_exists(dir_name): + return _os(os.path.exists, dir_name) + +def make_directory(dir_name): + if directory_exists(dir_name) is False: + _os(os.makedirs, dir_name) #-------------------------------------------------------------------- def rename_file(from_filename, to_filename): + if file_exists(from_filename) is False: + return + if file_exists(to_filename): + delete_file(to_filename) + + os.rename(from_filename, to_filename) + +#-------------------------------------------------------------------- +def _os(os_module, filename): + results = os_module(filename) + return results + +#-------------------------------------------------------------------- +def is_event_loop_running(): try: - if file_exists(from_filename) is False: - return False - if file_exists(to_filename): - remove_file(to_filename) + asyncio.get_running_loop() + return True + except RuntimeError: + return False + except Exception as err: + log_exception(err) + return False - if Gb.initial_icloud3_loading_flag: - os.rename(from_filename, to_filename) - return True - else: - asyncio.run_coroutine_threadsafe( - async_rename_file(from_filename, to_filename), Gb.hass.loop) +def is_event_loop_running2(): + if asyncio.get_event_loop_policy()._local._loop: return True + return False - except RuntimeError as err: - if err == 'no running event loop': - os.rename(from_filename, to_filename) - return True +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# PYTHON ASYNC OS. FILE I/O AND OTHER UTILITIES +# +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +async def async_file_exists(filename): + return await Gb.hass.async_add_executor_job(os.path.isfile, filename) + +async def async_delete_file(filename): + try: + return await Gb.hass.async_add_executor_job(os.remove, filename) + except FileNotFoundError: + pass except Exception as err: log_exception(err) - pass - return False -#................................................................... +async def async_copy_file(from_dir_filename, to_directory): + return await Gb.hass.async_add_executor_job(shutil.copy, from_dir_filename, to_directory) + +async def async_file_size(filename): + return await Gb.hass.async_add_executor_job(os.path.getsize, filename) + +async def async_extract_filename(directory_filename): + return await Gb.hass.async_add_executor_job(os.path.basename, directory_filename) + async def async_rename_file(from_filename, to_filename): + if await async_file_exists(from_filename) is False: + return + if await async_file_exists(to_filename): + await async_delete_file(to_filename) return await Gb.hass.async_add_executor_job(os.rename, from_filename, to_filename) -#-------------------------------------------------------------------- -# def x_extract_filename(directory_filename): -# try: -# if file_exists(directory_filename) is False: -# return '???.???' -# elif Gb.initial_icloud3_loading_flag: -# filename = os.path.basename(directory_filename) -# else: -# filename = asyncio.run_coroutine_threadsafe( -# async_extract_filename(directory_filename), Gb.hass.loop).result() -# return filename - -# except RuntimeError as err: -# if err == 'no running event loop': -# filename = os.path.basename(directory_filename) -# return filename - -# except Exception as err: -# log_exception(err) -# pass - -# return '???.???' -# #................................................................... -# async def async_extract_filename(directory_filename): -# return await Gb.hass.async_add_executor_job(os.path.basename, directory_filename) + +async def async_directory_exists(dir_name): + return await Gb.hass.async_add_executor_job(os.path.exists, dir_name) + +async def async_make_directory(dir_name): + _directory_exists = await async_directory_exists(dir_name) + if _directory_exists is False: + exist_ok = True + await Gb.hass.async_add_executor_job(os.makedirs, dir_name, exist_ok) + +async def async_delete_directory(dir_name): + try: + return await Gb.hass.async_add_executor_job(os.rmdir, dir_name) + except FileNotFoundError: + pass + except Exception as err: + log_exception(err) + +#................................................................... +async def async_os(os_module, filename): + return await Gb.hass.async_add_executor_job(os_module, filename) #-------------------------------------------------------------------- -def delete_file(file_desc, directory, filename, backup_extn=None, delete_old_sv_file=False): +async def async_delete_file_with_msg(file_desc, directory, filename, backup_extn=None, delete_old_sv_file=False): ''' Delete a file. Parameters: @@ -233,21 +241,21 @@ def delete_file(file_desc, directory, filename, backup_extn=None, delete_old_sv_ directory_filename_bu = (f"{directory}/{filename_bu}") if file_exists(directory_filename_bu): - remove_file(directory_filename_bu) + delete_file(directory_filename_bu) file_msg += (f"{CRLF_DOT}Deleted backup file (...{filename_bu})") rename_file(directory_filename, directory_filename_bu) file_msg += (f"{CRLF_DOT}Rename current file to ...{filename}.{backup_extn})") if file_exists(directory_filename): - remove_file(directory_filename) + delete_file(directory_filename) file_msg += (f"{CRLF_DOT}Deleted file (...{filename})") if delete_old_sv_file: filename = f"{filename.split('.')[0]}.sv" directory_filename = f"{directory_filename.split('.')[0]}.sv" if file_exists(directory_filename): - remove_file(directory_filename) + delete_file(directory_filename) file_msg += (f"{CRLF_DOT}Deleted file (...{filename})") if file_msg != "": @@ -263,23 +271,35 @@ def delete_file(file_desc, directory, filename, backup_extn=None, delete_old_sv_ #-------------------------------------------------------------------- -def get_file_list(start_dir=None, file_extn_filter=[]): - return get_file_or_directory_list( directory_list=False, +def get_directory_filename_list(start_dir=None, file_extn_filter=[]): + return get_file_or_directory_list( list_type=0, start_dir=start_dir, file_extn_filter=file_extn_filter) def get_directory_list(start_dir=None): - return get_file_or_directory_list( directory_list=True, + return get_file_or_directory_list( list_type=1, start_dir=start_dir, file_extn_filter=[]) +def get_file_list(list_type=None, start_dir=None, file_extn_filter=[]): + return get_file_or_directory_list( list_type, + start_dir=start_dir, + file_extn_filter=file_extn_filter) + +def get_filename_list(start_dir=None, file_extn_filter=[]): + return get_file_or_directory_list( list_type=2, + start_dir=start_dir, + file_extn_filter=file_extn_filter) + #-------------------------------------------------------------------- -def get_file_or_directory_list(directory_list=False, start_dir=None, file_extn_filter=[]): +def get_file_or_directory_list(list_type=None, start_dir=None, file_extn_filter=[]): ''' Return a list of directories or files in a given path Parameters: - - directory_list = True (List of directories), False (List of files) + - list_type = 0 - Directory/filename list (Default) + - list_type = 1 - Directory/subdirectory list + - list_type = 2 - Filename list - start_dir = Top level directory to start searching from ('www') -file_extn_filter = List of files witn extensions to include (['png' 'jpg'], []) @@ -291,38 +311,49 @@ def get_file_or_directory_list(directory_list=False, start_dir=None, file_extn_ start_dir, file_filter) ''' - + if list_type is None: list_type = 0 + back_slash = '\\' directory_filter = ['/.', 'deleted', '/x-'] filename_or_directory_list = [] path_config_base = f"{Gb.ha_config_directory}/" - back_slash = '\\' + start_dir = start_dir.replace(path_config_base, '') if start_dir is None: start_dir = '' - for path, dirs, files in os.walk(f"{path_config_base}{start_dir}"): - www_sub_directory = path.replace(path_config_base, '') - in_filter_cnt = len([filter for filter in directory_filter if instr(www_sub_directory, filter)]) - if in_filter_cnt > 0 or www_sub_directory.count('/') > 4 or www_sub_directory.count(back_slash): - continue - - if directory_list: - filename_or_directory_list.append(www_sub_directory) - continue - - # Filter unwanted directories - std dirs are www/icloud3, www/cummunity, www/images - if Gb.picture_www_dirs: - valid_dir = [dir for dir in Gb.picture_www_dirs if www_sub_directory.startswith(dir)] - if valid_dir == []: + try: + for path, dirs, files in os.walk(f"{path_config_base}{start_dir}"): + sub_directory = path.replace(path_config_base, '') + in_filter_cnt = len([filter for filter in directory_filter if instr(sub_directory, filter)]) + if in_filter_cnt > 0 or sub_directory.count('/') > 4 or sub_directory.count(back_slash): continue - dir_filenames = [f"{www_sub_directory}/{file}" - for file in files - if (file_extn_filter - and file.rsplit('.', 1)[-1] in file_extn_filter)] + # list_type=1 - Directory/subdirectory list + if list_type == 1: + filename_or_directory_list.append(sub_directory) + continue - filename_or_directory_list.extend(dir_filenames[:25]) - if len(dir_filenames) > 25: - filename_or_directory_list.append( - f"⛔ {www_sub_directory} > The first 25 files out of " - f"{len(dir_filenames)} are listed") + # Filter unwanted directories - std dirs are www/icloud3, www/community, www/images + if start_dir.endswith('/event_log_card/'): + pass + elif Gb.picture_www_dirs: + valid_dir = [dir for dir in Gb.picture_www_dirs if sub_directory.startswith(dir)] + if valid_dir == []: + continue + + # list_type=2 - filename only, does not contain directory name + dir_name = '' if list_type == 2 else f"{sub_directory}/" + dir_name = dir_name.replace('//', '/') + dir_filenames = [f"{dir_name}{file}" + for file in files + if (file_extn_filter + and file.rsplit('.', 1)[-1] in file_extn_filter)] + + filename_or_directory_list.extend(dir_filenames[:25]) + if len(dir_filenames) > 25: + filename_or_directory_list.append( + f"⛔ {sub_directory} > The first 25 files out of " + f"{len(dir_filenames)} are listed") + + return filename_or_directory_list - return filename_or_directory_list \ No newline at end of file + except Exception as err: + Gb.HALogger.exception(err) \ No newline at end of file diff --git a/custom_components/icloud3/helpers/format.py b/custom_components/icloud3/helpers/format.py index 4fc678c..35d43d6 100644 --- a/custom_components/icloud3/helpers/format.py +++ b/custom_components/icloud3/helpers/format.py @@ -5,7 +5,7 @@ from ..const import (UNKNOWN, HIGH_INTEGER, CRLF_DOT, ) from .common import (round_to_zero, ) from .messaging import (log_debug_msg, log_exception, log_debug_msg, log_error_msg, log_rawdata, - _trace, _traceha, ) + _evlog, _log, ) import homeassistant.util.dt as dt_util #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> diff --git a/custom_components/icloud3/helpers/messaging.py b/custom_components/icloud3/helpers/messaging.py index 9478b35..63bba89 100644 --- a/custom_components/icloud3/helpers/messaging.py +++ b/custom_components/icloud3/helpers/messaging.py @@ -1,15 +1,16 @@ from ..global_variables import GlobalVariables as Gb -from ..const import (DOT, ICLOUD3_ERROR_MSG, EVLOG_DEBUG, EVLOG_ERROR, EVLOG_INIT_HDR, EVLOG_MONITOR, +from ..const import (VERSION, VERSION_BETA, ICLOUD3, ICLOUD3_VERSION, DOMAIN, ICLOUD3_VERSION_MSG, + 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, EVLOG_TRACE, - CRLF, CRLF_DOT, NBSP, NBSP2, NBSP3, NBSP4, NBSP5, NBSP6, CRLF_INDENT, + CRLF, CRLF_DOT, NBSP, NBSP2, NBSP3, NBSP4, NBSP5, NBSP6, CRLF_INDENT, LINK, 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, + ICLOUD, MOBAPP, CONF_IC3_DEVICENAME, CONF_FNAME, CONF_LOG_LEVEL, CONF_PASSWORD, CONF_USERNAME, CONF_DEVICES, LATITUDE, LONGITUDE, LOCATION_SOURCE, TRACKING_METHOD, @@ -22,11 +23,10 @@ AUTHENTICATED, LAST_UPDATE_TIME, LAST_UPDATE_DATETIME, NEXT_UPDATE_TIME, LAST_LOCATED_DATETIME, LAST_LOCATED_TIME, INFO, GPS_ACCURACY, GPS, POLL_COUNT, VERT_ACCURACY, ALTITUDE, - ICLOUD3_VERSION, BADGE, ) from ..const_more_info import more_info_text -from .common import (obscure_field, instr, ) +from .common import (obscure_field, instr, is_empty, isnot_empty, ) import homeassistant.util.dt as dt_util from homeassistant.components import persistent_notification @@ -37,8 +37,9 @@ import traceback import logging - -FILTER_DATA_DICTS = ['items', 'userInfo', 'dsid', 'dsInfo', 'webservices', 'locations','location', ] +DO_NOT_SHRINK = ['url', 'accountName', ] +FILTER_DATA_DICTS = ['items', 'userInfo', 'dsid', 'dsInfo', 'webservices', 'locations','location', + 'params', 'headers', 'kwargs', ] FILTER_DATA_LISTS = ['devices', 'content', 'followers', 'following', 'contactDetails',] FILTER_FIELDS = [ ICLOUD3_VERSION, AUTHENTICATED, @@ -66,7 +67,11 @@ 'invitationSentToEmail', 'invitationAcceptedByEmail', 'invitationFromHandles', 'invitationFromEmail', 'invitationAcceptedHandles', 'items', 'userInfo', 'prsId', 'dsid', 'dsInfo', 'webservices', 'locations', - 'devices', 'content', 'followers', 'following', 'contactDetails', ] + 'devices', 'content', 'followers', 'following', 'contactDetails', + 'dsWebAuthToken', 'accountCountryCode', 'extended_login', 'trustToken', + 'data', 'json', 'headers', 'params', 'url', 'retry_cnt', 'retried', 'retry', '#', + 'code', 'ok', 'method', 'securityCode', + 'accountName', 'salt', 'a', 'b', 'c', 'm1', 'm2', 'protocols', 'iteration', 'Authorization', ] SP_str = ' '*50 @@ -215,6 +220,9 @@ def post_evlog_greenbar_msg(Device, evlog_greenbar_msg='+'): evlog_greenbar_msg: Message to display - ''' + if evlog_greenbar_msg == '': + return Gb.EvLog.clear_evlog_greenbar_msg() + # See if the message is really in the Device parameter if evlog_greenbar_msg == '+': evlog_greenbar_msg = Device @@ -228,16 +236,14 @@ def post_evlog_greenbar_msg(Device, evlog_greenbar_msg='+'): if Gb.EvLog.greenbar_alert_msg == evlog_greenbar_msg: return - if evlog_greenbar_msg == '': - Gb.EvLog.clear_evlog_greenbar_msg() else: Gb.EvLog.greenbar_alert_msg = evlog_greenbar_msg - Gb.EvLog.display_user_message('') + Gb.EvLog.display_user_message(Gb.EvLog.user_message) #------------------------------------------------------------------------------------------- def clear_evlog_greenbar_msg(): Gb.EvLog.clear_evlog_greenbar_msg() - Gb.EvLog.display_user_message('') + Gb.EvLog.display_user_message(Gb.EvLog.user_message) #-------------------------------------------------------------------- def post_startup_alert(alert_msg): @@ -263,7 +269,7 @@ def format_filename(path): if path.startswith('/config') or len(path) < 50: return path else: - return (f"{Gb.ha_config_directory}...{CRLF_INDENT}" + return (f"{Gb.ha_config_directory}……{CRLF_INDENT}" f"{path.replace(Gb.ha_config_directory, '')}") #-------------------------------------------------------------------- def more_info(key): @@ -296,7 +302,7 @@ def open_ic3log_file(new_log_file=False): filemode = 'w' if new_log_file else 'a' if Gb.iC3Logger is None or new_log_file: - Gb.iC3Logger = logging.getLogger('icloud3') + Gb.iC3Logger = logging.getLogger(DOMAIN) formatter = logging.Formatter('%(asctime)s %(message)s', datefmt='%m-%d %H:%M:%S') fileHandler = logging.FileHandler(ic3logger_file, mode=filemode, encoding='utf-8') fileHandler.setFormatter(formatter) @@ -370,7 +376,7 @@ def archive_ic3log_file(): log_file_1 = Gb.hass.config.path(IC3LOG_FILENAME).replace('-0.', '-1.') log_file_2 = Gb.hass.config.path(IC3LOG_FILENAME).replace('-0.', '-2.') - post_event(f"iCloud3 Log File Archived") + post_event(f"{ICLOUD3} Log File Archived") if os.path.isfile(log_file_2): os.remove(log_file_2) if os.path.isfile(log_file_1): os.rename(log_file_1, log_file_2) @@ -382,19 +388,19 @@ def archive_ic3log_file(): open_ic3log_file(new_log_file=True) except Exception as err: - post_event(f"iCloud3 Log File Archive encountered an error > {err}") + post_event(f"{ICLOUD3} Log File Archive encountered an error > {err}") #------------------------------------------------------------------------------ def write_config_file_to_ic3log(): conf_tracking_recd = Gb.conf_tracking.copy() - conf_tracking_recd[CONF_USERNAME] = obscure_field(conf_tracking_recd[CONF_USERNAME]) + # conf_tracking_recd[CONF_USERNAME] = obscure_field(conf_tracking_recd[CONF_USERNAME]) conf_tracking_recd[CONF_PASSWORD] = obscure_field(conf_tracking_recd[CONF_PASSWORD]) 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) - log_msg = ( f"iCloud3 v{Gb.version}, " + log_msg = ( f"{ICLOUD3_VERSION_MSG}, " f"{dt_util.now().strftime('%A')}, " f"{dt_util.now().strftime(DATETIME_FORMAT)[:19]}") log_msg = ( f" \n" @@ -409,6 +415,8 @@ def write_config_file_to_ic3log(): f"{indent}{Gb.conf_profile}") log_info_msg(f"Tracking:\n" f"{indent}{conf_tracking_recd}") + log_info_msg(f"Apple Accounts:\n" + f"{indent}{Gb.conf_apple_accounts}") log_info_msg(f"General Configuration:\n" f"{indent}{Gb.conf_general}\n" f"{indent}{Gb.ha_location_info}") @@ -446,7 +454,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) #-------------------------------------------------------------------- @@ -472,7 +480,11 @@ def log_error_msg(module_name, log_msg='+'): #-------------------------------------------------------------------- def log_exception(err): - write_ic3log_recd(traceback.format_exc()) + try: + write_ic3log_recd(f"{ICLOUD3_VERSION_MSG}\n{traceback.format_exc()}") + except: + write_ic3log_recd(err) + Gb.HALogger.exception(err) #-------------------------------------------------------------------- @@ -489,7 +501,7 @@ def log_debug_msg(devicename_or_Device, log_msg='+', msg_prefix=None): 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) #-------------------------------------------------------------------- @@ -499,10 +511,10 @@ def log_start_finish_update_banner(start_finish, devicename, Display a banner in the log file at the start and finish of a device update cycle ''' - - Device = Gb.Devices_by_devicename[devicename] - text = (f"{devicename}, {method}, " - f"CurrZone-{Device.sensor_zone}, {update_reason} ") + # The devicename may be the 'appleacct~devicename' + # if instr(devicename, '~'): devicename = devicename.split('~')[1] + # Device = Gb.Devices_by_devicename[devicename] + text = (f"{devicename}, {method}, {update_reason} ") log_msg = format_header_box(text, indent=43, start_finish=start_finish) log_info_msg(log_msg) @@ -523,13 +535,17 @@ def format_msg_line(log_msg, area=None): f"{RED_ALERT} " if instr(log_msg, EVLOG_ERROR) else \ area if area else \ Gb.trace_prefix - source = f"{_called_from()}{program_area}" + source = f"{_called_from()}{program_area}" log_msg = format_startup_header_box(log_msg) msg_prefix= ' ' if log_msg.startswith('⡇') else \ - ' ❗' if source.startswith('[pyicloud') else \ + '\n🔺 ' if instr(log_msg, 'REQUEST') else \ + '\n🔻 ' if instr(log_msg, 'RESPONSE') else \ + '\n❗' if instr(log_msg, 'ICLOUD DATA') else \ ' ⡇ ' if Gb.trace_group else \ - ' ' + ' ' + # if instr(msg_prefix, '🔺'): log_msg += " 🔺" + # if instr(msg_prefix, '🔻'): log_msg += " 🔻" log_msg = filter_special_chars(log_msg) log_msg = f"{source}{msg_prefix}{log_msg}" @@ -539,7 +555,7 @@ def format_msg_line(log_msg, area=None): return log_msg #-------------------------------------------------------------------- -def filter_special_chars(recd, evlog_export=False): +def filter_special_chars(log_msg, evlog_export=False): ''' Filter out EVLOG_XXX control fields ''' @@ -547,39 +563,43 @@ def filter_special_chars(recd, evlog_export=False): 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, '') - recd = recd.replace(NBSP, ' ') - recd = recd.replace(NBSP2, ' ') - recd = recd.replace(NBSP3, ' ') - recd = recd.replace(NBSP4, ' ') - recd = recd.replace(NBSP5, ' ') - recd = recd.replace(NBSP6, ' ') - recd = recd.strip() - recd = recd.replace(CRLF, f"\n{indent}") - recd = recd.replace('◦', f" ◦") - recd = recd.replace('* >', '') - recd = recd.replace('<', '<') - - if recd.find('^') == -1: return recd.strip() - - recd = recd.replace(EVLOG_TIME_RECD , '') - recd = recd.replace(EVLOG_UPDATE_HDR, '') - recd = recd.replace(EVLOG_UPDATE_START, '') - recd = recd.replace(EVLOG_UPDATE_END , '') - recd = recd.replace(EVLOG_ERROR, '') - recd = recd.replace(EVLOG_ALERT, '') - recd = recd.replace(EVLOG_WARNING, '') - recd = recd.replace(EVLOG_INIT_HDR, '') - recd = recd.replace(EVLOG_HIGHLIGHT, '') - recd = recd.replace(EVLOG_IC3_STARTING, '') - recd = recd.replace(EVLOG_IC3_STAGE_HDR, '') - - recd = recd.replace('^1^', '').replace('^2^', '').replace('^3^', '') - recd = recd.replace('^4^', '').replace('^5^', '') - - return recd.strip() + if log_msg.startswith('^'): log_msg = log_msg[3:] + + log_msg = log_msg.replace(EVLOG_MONITOR, '') + log_msg = log_msg.replace(NBSP, ' ') + log_msg = log_msg.replace(NBSP2, ' ') + log_msg = log_msg.replace(NBSP3, ' ') + log_msg = log_msg.replace(NBSP4, ' ') + log_msg = log_msg.replace(NBSP5, ' ') + log_msg = log_msg.replace(NBSP6, ' ') + log_msg = log_msg.strip() + log_msg = log_msg.replace(CRLF, f"\n{indent}") + log_msg = log_msg.replace('◦', f" ◦") + log_msg = log_msg.replace('* >', '') + log_msg = log_msg.replace('<', '<') + + for log_file_filter in Gb.log_file_filter: + log_msg = log_msg.replace(log_file_filter, '…………') + + if log_msg.find('^') == -1: return log_msg.strip() + + log_msg = log_msg.replace(EVLOG_TIME_RECD , '') + log_msg = log_msg.replace(EVLOG_UPDATE_HDR, '') + log_msg = log_msg.replace(EVLOG_UPDATE_START, '') + log_msg = log_msg.replace(EVLOG_UPDATE_END , '') + log_msg = log_msg.replace(EVLOG_ERROR, '') + log_msg = log_msg.replace(EVLOG_ALERT, '') + log_msg = log_msg.replace(EVLOG_WARNING, '') + log_msg = log_msg.replace(EVLOG_INIT_HDR, '') + log_msg = log_msg.replace(EVLOG_HIGHLIGHT, '') + log_msg = log_msg.replace(EVLOG_IC3_STARTING, '') + log_msg = log_msg.replace(EVLOG_IC3_STAGE_HDR, '') + + log_msg = log_msg.replace('^1^', '').replace('^2^', '').replace('^3^', '') + log_msg = log_msg.replace('^4^', '').replace('^5^', '') + + + return log_msg.strip() #-------------------------------------------------------------------- @@ -599,11 +619,11 @@ def format_startup_header_box(log_msg): return log_msg #-------------------------------------------------------------------- -def format_header_box(recd, indent=None, start_finish=None, evlog_export=False): +def format_header_box(log_msg, indent=None, start_finish=None, evlog_export=False): ''' Format a box around this item ''' - start_pos = recd.find('^') + start_pos = log_msg.find('^') if start_pos == -1: start_pos = 0 # Default indent for icloud3-0.log file is 43 @@ -618,7 +638,7 @@ def format_header_box(recd, indent=None, start_finish=None, evlog_export=False): Gb.trace_group = False return (f"⡇{top_char}\n" - f"{SP(indent)}⡇{SP(4)}{recd[start_pos:].upper()}\n" + f"{SP(indent)}⡇{SP(4)}{log_msg[start_pos:].upper()}\n" f"{SP(indent)}⡇{bot_char}") #------------------------------------------------------------------------------------------- @@ -646,12 +666,12 @@ def _resolve_module_name_log_msg(module_name, log_msg): # RAWDATA LOGGING ROUTINES # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -def log_rawdata_unfiltered(title, rawdata): +def log_rawdata_unfiltered(title, rawdata, data_source=None, filter_id=None): try: rawdata_copy = rawdata['raw'].copy() if 'raw' in rawdata else rawdata.copy() except: - log_info_msg(f"{'_'*8} {title.upper()} {'_'*8}\n{rawdata}") + log_info_msg(f"__ {title.upper()}\n{rawdata}") return devices_data = {} @@ -660,21 +680,17 @@ def log_rawdata_unfiltered(title, rawdata): for device_data in rawdata_copy['content']: devices_data[device_data['name']] = device_data - rawdata_copy['content'] = 'DeviceData...' + rawdata_copy['content'] = 'DeviceData……' - log_info_msg(f"{'_'*8} {title.upper()} {'_'*8}\n{rawdata_copy}") + log_info_msg(f"__ {title.upper()}\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" + log_msg = ( f"iCloud PyiCloud Data (unfiltered -- " f"{device_data}") log_info_msg(log_msg) #-------------------------------------------------------------------- -def log_rawdata(title, rawdata, log_rawdata_flag=False): +def log_rawdata(title, rawdata, log_rawdata_flag=False, data_source=None, filter_id=None): ''' Add raw data records to the HA log file for debugging purposes. @@ -688,143 +704,148 @@ def log_rawdata(title, rawdata, log_rawdata_flag=False): FILTER_DATA_LISTS. ''' - if Gb.log_rawdata_flag is False or rawdata is None: - return + if rawdata is None: + return False + elif Gb.log_rawdata_flag is False and log_rawdata_flag is False: + return False 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) + and (instr(title, ICLOUD) + or instr(title, MOBAPP) 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 + if instr(title,'iCloud Data'): + 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 = {} + log_msg = '' try: if type(rawdata) is not dict: - log_info_msg(f"{'_'*8} {title.upper()} {'_'*8}\n{rawdata}") + log_info_msg(f"__ {title.upper()}\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() + rawdata_items = {k: _shrink_value(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} + + rawdata_data['filter'] = {k: _shrink_value(k, v) + for k, v in rawdata['filter'].items() + if k in FILTER_FIELDS or Gb.log_rawdata_flag_unfiltered} except: - rawdata_items = {k: v for k, v in rawdata.items() + rawdata_items = {k: _shrink_value(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'] = {k: _shrink_value(k, v) + for k, v in rawdata.items() + if (k in FILTER_FIELDS or Gb.log_rawdata_flag_unfiltered)} rawdata_data['filter']['items'] = rawdata_items if rawdata_data['filter']: for data_dict in FILTER_DATA_DICTS: - filter_results = _filter_data_dict(rawdata_data['filter'], data_dict) + filter_results = filter_data_dict(rawdata_data['filter'], data_dict) if filter_results: - filtered_dicts[f"▶{data_dict.upper()}◀ ({data_dict})"] = filter_results + log_msg += f"\n❗ {data_dict}={filter_results}" for data_list in FILTER_DATA_LISTS: if data_list in rawdata_data['filter']: filter_results = _filter_data_list(rawdata_data['filter'][data_list]) if filter_results: - filtered_lists[f"▶{data_list.upper()}◀ ({data_list})"] = filter_results + log_msg += f"\n❗ {data_list}={filter_results}" - filtered_data.update(filtered_dicts) - filtered_data.update(filtered_lists) - try: - log_msg = None - if filtered_data: - log_msg = f"{filtered_data}" - else: - if 'id' in rawdata_data and len(rawdata_data['id']) > 10: - rawdata_data['id'] = f"{rawdata_data['id'][:10]}..." - elif 'id' in rawdata_data['filter'] and len(rawdata_data['filter']['id']) > 10: - rawdata_data['filter']['id'] = f"{rawdata_data['filter']['id'][:10]}..." - if rawdata_data: - log_msg = f"{rawdata_data}" - else: - log_msg = f"{rawdata[:15]}" + if 'data' in rawdata_data['filter']: + try: + rawdata_data_items = {k: _shrink_value(k, v) + for k, v in rawdata_data['filter']['data'].items() + if k in FILTER_FIELDS and type(v) not in [dict, list]} + if rawdata_data_items: + log_msg += f"\n❗ data.items={rawdata_data_items}" + except: + pass + + for data_dict in FILTER_DATA_DICTS: + filter_results = filter_data_dict(rawdata_data['filter']['data'], data_dict) + if filter_results: + log_msg += f"\n❗ data.{data_dict}={filter_results}" - except Exception as err: - log_exception(err) - pass - if log_msg != {}: - log_info_msg(f"{'_'*8} {title.upper()} {'_'*8}\n{log_msg}") + if log_msg: + log_info_msg(f"{title.upper()}{log_msg}") return #-------------------------------------------------------------------- -def _filter_data_dict(rawdata_data, data_dict_items): +def filter_data_dict(rawdata_data, data_dict): try: - if data_dict_items == 'webservices': - return rawdata_data.get('webservices') + if data_dict == 'webservices' and 'webservices' in rawdata_data: + webservices = { 'findme': rawdata_data['webservices']['findme'], + 'contacts': rawdata_data['webservices']['contacts']} + return webservices + # return rawdata_data.get('webservices') - filter_results = {k: v for k, v in rawdata_data[data_dict_items].items() - if k in FILTER_FIELDS} - if 'id' in filter_results and len(filter_results['id']) > 10: - filter_results['id'] = f"{filter_results['id'][:10]}..." + filter_results = {k: _shrink_value(k, v) + for k, v in rawdata_data[data_dict].items() + if (k in FILTER_FIELDS or Gb.log_rawdata_flag_unfiltered)} return filter_results except Exception as err: # log_exception(err) - return {} + return '' #-------------------------------------------------------------------- def _filter_data_list(rawdata_data_list): try: - filtered_list = [] + filtered_list = '' for list_item in rawdata_data_list: - filter_results = {k: v for k, v in list_item.items() - if k in FILTER_FIELDS} - if id := filter_results.get('id'): - if id in Gb.Devices_by_icloud_device_id: - filtered_list.append(f"◉◉ <{filter_results['name']}> ◉◉") - continue - if 'id' in filter_results: - if len(filter_results['id']) > 10: - filter_results['id'] = f"{filter_results['id'][:10]}..." + filter_results = {k: _shrink_value(k, v) + for k, v in list_item.items() + if (k in FILTER_FIELDS or Gb.log_rawdata_flag_unfiltered)} if 'location' in filter_results and filter_results['location']: - filter_results['location'] = {k: v for k, v in filter_results['location'].items() + if Gb.log_rawdata_flag_unfiltered: + filter_results['location'] = {k: v for k, v in filter_results['location'].items()} + else: + filter_results['location'] = {k: v for k, v in filter_results['location'].items() if k in FILTER_FIELDS} filter_results['location'].pop('address', None) if filter_results: - filtered_list.append(f"◉◉ <{filter_results['name']}> ⭑⭑ {filter_results} ◉◉") + filtered_list += f"\n❗ {filter_results['name']}={filter_results}" return filtered_list except: - return [] + return '' +#-------------------------------------------------------------------- +def _shrink_value(k, v): + if (k in DO_NOT_SHRINK + or Gb.log_rawdata_flag_unfiltered): + return v + + if type(v) is str: + if v.startswith('http'): + return v + + if len(v) > 20: + return f"{v[:6]}……{v[-6:]}" + else: + return v + else: + return v #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -869,9 +890,9 @@ def post_internal_error(err_text, traceback_format_exec_obj='+'): Example traceback_format_exec_obj(): [ 'Traceback (most recent call last):' - ' File "/config/custom_components/icloud3/support/start_ic3.py", line 1268, in setup_tracked_devices_for_famshr' + ' File "/config/custom_components/icloud3/support/start_ic3.py", line 1268, in setup_tracked_devices_for_icloud' " a = 1 + 'a'" - ' ~~^~~~~' + ' ~~^……' "TypeError: unsupported operand type(s) for +: 'int' and 'str'" '' ] @@ -906,7 +927,7 @@ def post_internal_error(err_text, traceback_format_exec_obj='+'): try: - err_msg = (f"{CRLF_DOT}File... > {err_file_line_module})" + err_msg = (f"{CRLF_DOT}File…… > {err_file_line_module})" f"{CRLF_DOT}Code > {err_code}" f"{CRLF_DOT}Error. > {err_error_msg}") @@ -926,11 +947,11 @@ def post_internal_error(err_text, traceback_format_exec_obj='+'): # DEBUG TRACE ROUTINES # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -def dummy_trace(): - _trace(None, None) +def dummy_evlog(): + _evlog(None, None) #-------------------------------------------------------------------- -def _trace(devicename_or_Device, items='+'): +def _evlog(devicename_or_Device, items='+'): ''' Display a message or variable in the Event Log ''' @@ -947,7 +968,7 @@ def _trace(devicename_or_Device, items='+'): write_ic3log_recd(f"{called_from}⛔.⛔ . . . {devicename} > {items}") #-------------------------------------------------------------------- -def _traceha(items, v1='+++', v2='', v3='', v4='', v5=''): +def _log(items, v1='+++', v2='', v3='', v4='', v5=''): ''' Display a message or variable in the HA log file ''' @@ -986,7 +1007,7 @@ def _called_from(trace=False): return ' ' caller_path = caller.filename.replace('.py','') - caller_filename = f"{caller_path.split('/')[-1]}........" + caller_filename = f"{caller_path.split('/')[-1]}………….." caller_lineno = caller.lineno return f"[{caller_filename[:12]}:{caller_lineno:04}] " diff --git a/custom_components/icloud3/helpers/time_util.py b/custom_components/icloud3/helpers/time_util.py index 3ba5140..1263783 100644 --- a/custom_components/icloud3/helpers/time_util.py +++ b/custom_components/icloud3/helpers/time_util.py @@ -1,8 +1,9 @@ from ..global_variables import GlobalVariables as Gb -from ..const import ( HIGH_INTEGER, HHMMSS_ZERO, DATETIME_ZERO, DATETIME_FORMAT, WAZE_USED, ) +from ..const import (HIGH_INTEGER, HHMMSS_ZERO, HHMM_ZERO, DATETIME_ZERO, + DATETIME_FORMAT, WAZE_USED, ) -from .messaging import (_trace, _traceha, post_event, internal_error_msg, ) +from .messaging import (_evlog, _log, post_event, log_exception, internal_error_msg, ) from .common import instr import homeassistant.util.dt as dt_util @@ -24,9 +25,43 @@ def time_now(): return str(datetime.fromtimestamp(int(time.time())))[11:19] #-------------------------------------------------------------------- -def datetime_now(): +def utcnow(): + ''' + Return the utcnow datetime item + + now=datetime.datetime(2024, 8, 29, 19, 44, 55, 444380, tzinfo=datetime.timezone.utc) + dt_util.utcnow()=datetime.datetime(2024, 8, 29, 19, 44, 55, 446351, tzinfo=datetime.timezone.utc) + utcnow = 2024-08-29 19:44:55.442437+00:00 + ''' + return dt_util.utcnow() + +#-------------------------------------------------------------------- +def datetime_plus(datetime, secs=None, mins=None, hrs=None, days=None): + ''' + Determine the current datetime + specified interval + + datetime: datetime.datetime(2024, 8, 29, 19, 44, 55, 444380, tzinfo=datetime.timezone.utc) + ''' + secs = secs or 0 + mins = mins or 0 + hrs = hrs or 0 + days = days or 0 + return datetime + timedelta(days=days, seconds=secs, minutes=mins, hours=hrs) + +#-------------------------------------------------------------------- +def datetime_now(datetime_struct=False): ''' now --> epoch/unix yyy-mm-dd 10:23:45 ''' - return str(datetime.fromtimestamp(int(time.time()))) + if datetime_struct: + return datetime.fromtimestamp(int(time.time())) + else: + return str(datetime.fromtimestamp(int(time.time()))) + +#-------------------------------------------------------------------- +def smh_time(time): + smh_time_str = time.replace(' sec', 's').replace(' secs', 's') + smh_time_str = smh_time_str.replace(' min', 'm').replace(' mins', 'm') + smh_time_str = smh_time_str.replace(' hr', 'h').replace(' hrss', 'h') + return smh_time_str #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -85,10 +120,12 @@ def secs_to_hhmm(secs_utc): if isnot_valid(secs_utc): return '00:00' if Gb.time_format_24_hour: - return time_local(secs_utc)[:-3] + return time_local(secs_utc+30)[:-3] - hhmmss = time_to_12hrtime(time_local(secs_utc)) - return hhmmss[:-4] + hhmmss[-1:] + hhmmss = time_to_12hrtime(time_local(secs_utc+30)) + hhmm = hhmmss[:-4] + hhmmss[-1:] + + return hhmm except: return '00:00' @@ -338,6 +375,16 @@ def time_str_to_secs(time_str=None) -> int: return secs +#-------------------------------------------------------------------- +def datetime_to_secs(date_time, date_time_format=None): + + if date_time_format is None: + date_time_format = "%Y-%m-%d %H:%M:%S" + + dt_struct = datetime.strptime(date_time, date_time_format) + secs = datetime.timestamp(dt_struct) + + return secs #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # # Time conversion and formatting functions @@ -497,64 +544,40 @@ def adjust_time_hour_value(hhmmss, hh_adjustment): new time value in the same format as the Input hhmmss time ''' - if hh_adjustment == 0 or hhmmss == HHMMSS_ZERO: + if hh_adjustment == 0 or hhmmss == HHMMSS_ZERO or hhmmss == HHMM_ZERO: return hhmmss hhmm_colon = hhmmss.find(':') if hhmm_colon == -1: return hhmmss - hhmmss24 = time_to_24hrtime(hhmmss) + try: + hhmm_flag = len(hhmmss) < 7 + if hhmm_flag: + _hhmmss = f"{hhmmss}:00" if Gb.time_format_24_hour else f"{hhmmss[:5]}:00{_ap(hhmmss)}" + else: + _hhmmss = hhmmss + except Exception as err: + log_exception(err) + + hhmmss24 = time_to_24hrtime(_hhmmss) hh = int(hhmmss24[0:2]) + hh_adjustment if hh <= 0: hh += 24 elif hh >= 24: hh -=24 + if hh > 12 and Gb.time_format_12_hour: + hh -= 12 hhmmss24 = f"{hh:0>2}{hhmmss24[2:8]}" + adj_hhmmss = time_to_12hrtime(hhmmss24) - 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) + try: + if hhmm_flag: + adj_hhmmss = f"{adj_hhmmss[:-4]}{_ap(adj_hhmmss)}" + except Exception as err: + log_exception(err) - return hhmmss + return adj_hhmmss #-------------------------------------------------------------------- def timestamp_to_time_utcsecs(utc_timestamp) -> int: @@ -569,3 +592,11 @@ def timestamp_to_time_utcsecs(utc_timestamp) -> int: hhmmss = hhmmss[1:] return hhmmss + +#-------------------------------------------------------------------- +def _has_ap(hhmmss): + ''' See if the time ends in a or p (12-hour time) ''' + return hhmmss[-1].lower() in ['a', 'p'] + +def _ap(hhmmss): + return hhmmss[-1].lower() if _has_ap(hhmmss) else '' diff --git a/custom_components/icloud3/icloud3_main.py b/custom_components/icloud3/icloud3_main.py index 725fb0a..d4f0384 100644 --- a/custom_components/icloud3/icloud3_main.py +++ b/custom_components/icloud3/icloud3_main.py @@ -15,38 +15,28 @@ Thanks to all """ - -import os import time import traceback from re import match -import voluptuous as vol -from homeassistant.util import slugify -import homeassistant.util.yaml.loader as yaml_loader -import homeassistant.util.dt as dt_util -from homeassistant.util.location import distance -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_utc_time_change -from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant import config_entries +import homeassistant.util.dt as dt_util +from homeassistant.helpers.event import track_utc_time_change # ================================================================= from .global_variables import GlobalVariables as Gb from .const import (VERSION, VERSION_BETA, - HOME, NOT_HOME, NOT_SET, HIGH_INTEGER, RARROW, LT, NBSP3, CLOCK_FACE, + HOME, NOT_HOME, NOT_SET, HIGH_INTEGER, RARROW, LT, NBSP3, CLOCK_FACE, LINK, CRLF, DOT, LDOT2, CRLF_DOT, CRLF_LDOT, CRLF_HDOT, CRLF_X, NL, NL_DOT, - TOWARDS, EVLOG_IC3_STAGE_HDR, - ICLOUD, ICLOUD_FNAME, TRACKING_NORMAL, FNAME, - CONF_USERNAME, CONF_PASSWORD, + EVLOG_IC3_STAGE_HDR, + ICLOUD, TRACKING_NORMAL, FNAME, + CONF_USERNAME, CONF_PASSWORD, CONF_TOTP_KEY, 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, - MOBAPP_UPDATE, ICLOUD_UPDATE, ARRIVAL_TIME, - EVLOG_UPDATE_START, EVLOG_UPDATE_END, EVLOG_ALERT, EVLOG_NOTICE, - FMF, FAMSHR, MOBAPP, MOBAPP_FNAME, + MOBAPP_UPDATE, ICLOUD_UPDATE, ARRIVAL_TIME, TOWARDS, AWAY_FROM, + EVLOG_UPDATE_START, EVLOG_UPDATE_END, EVLOG_ERROR, EVLOG_ALERT, EVLOG_NOTICE, + ICLOUD, MOBAPP, ENTER_ZONE, EXIT_ZONE, INTERVAL, NEXT_UPDATE, CONF_LOG_LEVEL, STATZONE_RADIUS_1M, ) @@ -62,17 +52,20 @@ 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, isbetween, ) +from .helpers.file_io import (is_event_loop_running, is_event_loop_running2,) +from .helpers.common import (instr, is_empty, isnot_empty, is_zone, is_statzone, isnot_statzone, + list_to_str, isbetween, ) +from .helpers.file_io import (file_exists, directory_exists, make_directory, extract_filename, ) 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, log_info_msg, log_exception, log_start_finish_update_banner, log_debug_msg, archive_ic3log_file, - _trace, _traceha, ) + _evlog, _log, ) from .helpers.time_util import (time_now, time_now_secs, secs_to, secs_since, mins_since, secs_to_time, secs_to_hhmm, secs_to_datetime, calculate_time_zone_offset, - format_timer, format_age, format_time_age, ) + format_timer, format_age, format_time_age, format_secs_since, ) from .helpers.dist_util import (km_to_um, m_to_um_ft, ) @@ -89,8 +82,7 @@ def __init__(self): Gb.polling_5_sec_loop_running = False self.pyicloud_refresh_time = {} # Last time Pyicloud was refreshed for the trk method - self.pyicloud_refresh_time[FMF] = 0 - self.pyicloud_refresh_time[FAMSHR] = 0 + self.pyicloud_refresh_time[ICLOUD] = 0 self.attributes_initialized_flag = False self.e_seconds_local_offset_secs = 0 @@ -151,6 +143,13 @@ def start_icloud3(self): Gb.start_icloud3_inprocess_flag = True Gb.restart_icloud3_request_flag = False Gb.all_tracking_paused_flag = False + # _evlog(f"fil exists {Gb.icloud3_config_filename} {file_exists(Gb.icloud3_config_filename)}") + # _evlog(f"dir exists {Gb.ha_storage_icloud3} {directory_exists(Gb.ha_storage_icloud3)}") + # _evlog(f"dir exists {Gb.ha_storage_directory} {directory_exists(Gb.ha_storage_directory)}") + # _evlog(f"fil exists {f'{Gb.icloud3_config_filename}.test2'} {file_exists(f'{Gb.icloud3_config_filename}.test2')}") + # _evlog(f"dir exists {f'{Gb.ha_storage_icloud3}/test2'} {directory_exists(f'{Gb.ha_storage_icloud3}/test2')}") + # _evlog(f"make dir {f'make-{Gb.ha_storage_icloud3}/test2'} {make_directory(f'{Gb.ha_storage_icloud3}/test2')}") + # _evlog(f"ext filename {Gb.icloud3_config_filename} {extract_filename(Gb.icloud3_config_filename)}") start_ic3_control.stage_1_setup_variables() start_ic3_control.stage_2_prepare_configuration() @@ -158,7 +157,9 @@ def start_icloud3(self): start_ic3_control.stage_3_setup_configured_devices() stage_4_success = start_ic3_control.stage_4_setup_data_sources() if stage_4_success is False or Gb.reinitialize_icloud_devices_flag: - start_ic3_control.stage_4_setup_data_sources(retry=True) + stage_4_success = start_ic3_control.stage_4_setup_data_sources_retry() + if stage_4_success is False or Gb.reinitialize_icloud_devices_flag: + stage_4_success = start_ic3_control.stage_4_setup_data_sources_retry(final_retry=True) start_ic3_control.stage_5_configure_tracked_devices() start_ic3_control.stage_6_initialization_complete() @@ -202,8 +203,8 @@ def _polling_loop_5_sec_device(self, ha_timer_secs): if Gb.start_icloud3_inprocess_flag: return - if Gb.config_flow_updated_parms != {''}: - start_ic3.process_config_flow_parameter_updates() + if Gb.config_parms_update_control != {''}: + start_ic3.handle_config_parms_update() # Restart iCloud via service call from EvLog or config_flow if Gb.restart_icloud3_request_flag: @@ -234,9 +235,13 @@ def _polling_loop_5_sec_device(self, ha_timer_secs): if Gb.evlog_action_request == '': pass - elif Gb.evlog_action_request == CMD_RESET_PYICLOUD_SESSION: - pyicloud_ic3_interface.pyicloud_reset_session() - Gb.evlog_action_request = '' + # elif Gb.evlog_action_request == CMD_RESET_PYICLOUD_SESSION: + # post_event(f"{EVLOG_ERROR}The `Action > Request Apple Verification Code` " + # f"is no longer available. This must be done using the " + # f"`Configuration > Enter/Request An Apple Account Verification " + # f"Code` screen") + # pyicloud_ic3_interface.pyicloud_reset_session() + # Gb.evlog_action_request = '' try: #<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>> @@ -247,14 +252,14 @@ def _polling_loop_5_sec_device(self, ha_timer_secs): # Start of uncommented out code to test of moving device into a statzone while home # if Gb.this_update_time.endswith('5:00'): # if Gb.Devices[0].StatZone is None: - # _trace(f"{Gb.Devices[0].fname} creating") + # _evlog(f"{Gb.Devices[0].fname} creating") # statzone.move_device_into_statzone(Gb.Devices[0]) - # _trace(f"{Gb.Devices[0].StatZone.zone} created") + # _evlog(f"{Gb.Devices[0].StatZone.zone} created") # if Gb.this_update_time.endswith('0:00'): # if Gb.Devices[0].StatZone: - # _trace(f"{Gb.Devices[0].StatZone.zone} removing") + # _evlog(f"{Gb.Devices[0].StatZone.zone} removing") # statzone.remove_statzone(Gb.Devices[0].StatZone, Gb.Devices[0]) - # _trace(f"{Gb.Devices[0].StatZone.zone} removed") + # _evlog(f"{Gb.Devices[0].StatZone.zone} removed") # End of uncommented out code to test of moving device into a statzone while home if Gb.all_tracking_paused_flag: @@ -320,7 +325,7 @@ def _polling_loop_5_sec_device(self, ha_timer_secs): 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() + #self._display_clear_authentication_needed_msg() self.initial_locate_complete_flag = True Gb.trace_prefix = 'WRAPUP' @@ -451,7 +456,7 @@ def _main_5sec_loop_update_tracked_devices_mobapp(self, Device): mobapp_data_handler.reset_statzone_on_enter_exit_trigger(Device) self._validate_new_mobapp_data(Device) - self.process_updated_location_data(Device, MOBAPP_FNAME) + self.process_updated_location_data(Device, MOBAPP) # Send a location request to device if needed mobapp_data_handler.check_if_mobapp_is_alive(Device) @@ -462,8 +467,11 @@ def _main_5sec_loop_update_tracked_devices_icloud(self, Device): Update the device based on iCloud data ''' - if Gb.PyiCloud is None: + if (Device.PyiCloud is None): + # or Device.devicename in Gb.username_pyicloud_503_connection_error): return + # if Gb.PyiCloud is None: + # return Gb.trace_prefix = 'ICLOUD' devicename = Device.devicename @@ -485,9 +493,15 @@ def _main_5sec_loop_update_tracked_devices_icloud(self, Device): else: return - # Update device info. Get data from FmF or FamShr + # Update device info. Get data from iCloud icloud_data_handler.request_icloud_data_update(Device) + # Test forcing Away --> Home with home_zone HA config override + # if time_now_secs() > Gb.started_secs + 120: + # Device.loc_data_latitude = Gb.HomeZone.latitude + # Device.loc_data_longitude = Gb.HomeZone.longitude + # _evlog(Device, "Home location Override") + # Do not redisplay update reason if in error retries. It has already been displayed. if icloud_data_handler.update_device_with_latest_raw_data(Device) is False: Device.icloud_acct_error_flag = True @@ -520,7 +534,7 @@ def _main_5sec_loop_update_tracked_devices_icloud(self, Device): self._validate_new_icloud_data(Device) self._post_before_update_monitor_msg(Device) - self.process_updated_location_data(Device, ICLOUD_FNAME) + self.process_updated_location_data(Device, ICLOUD) # Refresh the EvLog if this is an initial locate if self.initial_locate_complete_flag == False: @@ -571,10 +585,15 @@ def _main_5sec_loop_icloud_prefetch_control(self): Update the iCloud location data if it the next_update_time will be reached in the next 10-seconds ''' - if Gb.PyiCloud is None: + if (Gb.use_data_source_ICLOUD is False + or Gb.all_tracking_paused_flag): return + # if Gb.PyiCloud is None: + # return + + if Device := det_interval.device_will_update_in_15secs(): + if Device.PyiCloud is None: return - if Device := self._get_icloud_data_prefetch_device(): Gb.trace_prefix = 'GETLOC' log_start_finish_update_banner('start', Device.devicename, 'icloud prefetch', '') post_monitor_msg(Device.devicename, "iCloud Location Requested (prefetch)") @@ -596,65 +615,66 @@ def _main_5sec_loop_special_time_control(self): time_now_ss = Gb.this_update_time[-2:] time_now_mm = Gb.this_update_time[3:5] if time_now_ss == '00' else '' - # Every hour + # Every hour: + # - Check for iCloud3 update in HACS data + # - Clean up lingering Stat Zones if time_now_mmss == '00:00': self._timer_tasks_every_hour() - # At midnight + # At midnight: + # - Reset auto-reset log levels + # - Reset daily counts + # - Compress the WazeHist database + # - Cycle the iCloud3 log files if Gb.this_update_time == '00:00:00': self._timer_tasks_midnight() - # At 1am + # At 1am: + # - Check for daylight savings time change elif Gb.this_update_time == '01:00:00': calculate_time_zone_offset() if (Gb.this_update_secs >= Gb.EvLog.clear_secs): Gb.EvLog.update_event_log_display(show_one_screen=True) - # Every minute + # Every 30-seconds: + # - See if a device needs a 2fa request via a otp token + if time_now_ss in ['00', '30']: + self._check_apple_acct_2fa_totp_key_request() + pass + + # Every minute: + # - Update the device's info msg + # - Check to see if a MobApp location refresh has been requested if time_now_ss == '00': - for Device in Gb.Devices_by_devicename.values(): - Device.display_info_msg(Device.format_info_msg, new_base_msg=True) - if (Device.mobapp_monitor_flag - and Device.mobapp_request_loc_first_secs == 0 - and Device.mobapp_data_state != Device.loc_data_zone - and Device.mobapp_data_state_secs < (Gb.this_update_secs - 120)): - mobapp_interface.request_location(Device) + self._check_mobappp_location_request() - # Every 15-minutes + # Every 15-minutes: + # - Refresh a device's distance to the other devices if time_now_mm in ['00', '15', '30', '45']: if Gb.log_debug_flag: for devicename, Device in Gb.Devices_by_devicename_tracked.items(): Device.log_data_fields() + pyicloud_ic3_interface.retry_apple_acct_login() + det_interval.set_dist_to_devices(post_event_msg=True) - 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 " - f"({LT}{m_to_um_ft(NEAR_DEVICE_DISTANCE, as_integer=True)}) > " - f"@{device_time}, " - f"{Device.dist_apart_msg.replace(f' ({device_time})', '')}") - if event_msg != Device.last_near_devices_msg: - Device.last_near_devices_msg = event_msg - post_event(devicename, event_msg) + # Every 5-minutes + if time_now_mmss[1:] in ['0:00', '5:00']: + # There are some Devices that monitor the mobapp that are not in the HA MobApp Integrations + # devices list. Rebuild the list and try again. The MobApp Integrations may not have been + # set up on the first try when iCloud3 started. + if Gb.device_mobapp_verify_retry_needed: + mobapp_data_handler.unverified_devices_mobapp_handler() + # Every 10-minutes - if time_now_mm in ['00', '10', '20', '30', '40', '50']: - pass + # if time_now_mm in ['00', '10', '20', '30', '40', '50']: + # pass # Every 1/2-hour - if time_now_mm in ['00', '30']: - pass - - if Gb.PyiCloud is not None and Gb.this_update_secs >= Gb.authentication_error_retry_secs: - post_event(f"Retry Authentication > " - f"Timer={secs_to_time(Gb.authentication_error_retry_secs)}") - pyicloud_ic3_interface.authenticate_icloud_account(Gb.PyiCloud) - - service_handler.issue_ha_notification() + # if time_now_mm in ['00', '30']: + # pass #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -702,14 +722,14 @@ def _validate_new_mobapp_data(self, Device): return try: - #log_start_finish_update_banner('start', devicename, MOBAPP_FNAME, update_reason) + #log_start_finish_update_banner('start', devicename, MOBAPP, update_reason) Device.update_sensors_flag = True # Request the mobapp location if mobapp location is old and the next update # time is reached and less than 1km from the zone if (Device.is_mobapp_data_old and Device.is_next_update_time_reached - and Device.FromZone_NextToUpdate.zone_dist < 1 + and Device.FromZone_NextToUpdate.zone_dist_km < 1 and Device.FromZone_NextToUpdate.dir_of_travel == TOWARDS and Device.isnotin_zone): @@ -749,9 +769,7 @@ def _validate_new_icloud_data(self, Device): their update time has not been reached. """ - update_reason = Device.icloud_update_reason - devicename = Device.devicename - zone = Device.loc_data_zone + zone = Device.loc_data_zone if Gb.any_device_was_updated_reason == '': Gb.any_device_was_updated_reason = f'{Device.icloud_update_reason}, {Device.fname_devtype}' @@ -769,6 +787,7 @@ def _validate_new_icloud_data(self, Device): latitude = Device.loc_data_latitude longitude = Device.loc_data_longitude + # See if the GPS accuracy is poor, the locate is old, there is no location data # available or the device is offline self._set_old_location_status(Device) @@ -851,17 +870,18 @@ def _validate_new_icloud_data(self, Device): # leaves a zone. If the location is valid but before the request # time, it will be left in the zone until the Next Update Time. Treat the # location as old to force an update to check the zone status again. Clear - # the check zone exit time when the update is done. - if Device.loc_data_secs < Device.check_zone_exit_secs: - # det_interval.update_all_device_fm_zone_sensors_interval(Device, 5) - # event_msg =(f"Locate > Update Device at " - # f"{Device.FromZone_Home.next_update_time} " - # f"({Device.FromZone_Home.interval_str})") - # post_event(Device, event_msg) + # the check zone exit time when the update is done. Stop checking when + # the old loc cnt > 8 or the location is > 2-hr ago. It probably was never located. + + if (Device.loc_data_secs < Device.check_zone_exit_secs + and mins_since(Device.loc_data_secs) < 120 + and Device.old_loc_cnt <= 8): Device.old_loc_cnt += 1 Device.old_loc_msg = 'Located before Zone Exit Check' Device.update_sensors_error_msg = Device.old_loc_msg Device.update_sensors_flag = False + Device.check_zone_exit_secs = 0 + elif (Device.check_zone_exit_secs > 0 and Device.loc_data_secs >= Device.check_zone_exit_secs): Device.check_zone_exit_secs = 0 @@ -892,24 +912,29 @@ def _validate_new_icloud_data(self, Device): def process_updated_location_data(self, Device, data_source): try: devicename = Gb.devicename = Device.devicename + acct_name = '' + + if data_source == ICLOUD: + update_reason = f"{Device.icloud_update_reason}" + if Device.PyiCloud: + acct_name = Device.PyiCloud.account_owner_link - if data_source == ICLOUD_FNAME: - update_reason = Device.icloud_update_reason - elif data_source == MOBAPP_FNAME: + elif data_source == MOBAPP: update_reason = Device.mobapp_data_change_reason else: update_reason = Device.trigger # Makw sure the Device mobapp_state is set to the statzone if the device is in a statzone # and the Device mobapp state value is not_nome. The Device state value can be out of sync - # if the mobapp was updated but no trigger or state change was detected when a FamShr + # if the mobapp was updated but no trigger or state change was detected when a iCloud # update is processed since the Device's location gps did not actually change mobapp_data_handler.sync_mobapp_data_state_statzone(Device) # Location is good or just setup the StatZone. Determine next update time and update interval, # next_update_time values and sensors with the good data if Device.update_sensors_flag: - log_start_finish_update_banner('start', devicename, data_source, update_reason) + log_start_finish_update_banner('start', f"{devicename}{acct_name}", + data_source, update_reason) self._get_tracking_results_and_update_sensors(Device, data_source) else: @@ -985,16 +1010,17 @@ def _get_tracking_results_and_update_sensors(self, Device, update_requested_by): ''' devicename = Device.devicename update_reason = Device.mobapp_data_change_reason \ - if update_requested_by == MOBAPP_FNAME \ + if update_requested_by == MOBAPP \ else Device.icloud_update_reason - if Gb.PyiCloud: + if Device.PyiCloud: + # if Gb.PyiCloud: icloud_data_handler.update_device_with_latest_raw_data(Device) else: Device.update_dev_loc_data_from_raw_data_MOBAPP() if Device.is_tracked and Device.is_location_data_rejected(): - if Device.is_dev_data_source_FAMSHR_FMF: + if Device.is_dev_data_source_iCloud: det_interval.determine_interval_after_error(Device, counter=OLD_LOCATION_CNT) elif Device.is_monitored and Device.is_offline: @@ -1011,24 +1037,54 @@ def _get_tracking_results_and_update_sensors(self, Device, update_requested_by): if self._determine_interval_and_next_update(Device): Device.update_sensor_values_from_data_fields() - event_msg = EVLOG_UPDATE_END update_requested_by = 'Tracking' if Device.is_tracked else 'Monitor' from_zone = Device.FromZone_TrackFrom.from_zone - event_msg += f"{Device.dev_data_source} Results > " - if Device.FromZone_TrackFrom.dir_of_travel == TOWARDS: - event_msg+=(f"Arrive: {Device.FromZone_TrackFrom.from_zone_dname[:8]} at " - f"{Device.sensors[ARRIVAL_TIME]}") - else: - event_msg+=(f"Next Update: {Device.FromZone_TrackFrom.next_update_time}") - - post_event(devicename, event_msg) + post_event(devicename, ( + f"{EVLOG_UPDATE_END}{Device.dev_data_source} Results > " + f"{self._results_special_msg(Device)}")) - log_start_finish_update_banner('finish', devicename, Device.dev_data_source, '') + if Device.dev_data_source == ICLOUD and Device.PyiCloud: + acct_name = Device.PyiCloud.account_owner_link + else: + acct_name = '' + log_start_finish_update_banner('finish', f"{devicename}{acct_name}", + Device.dev_data_source, + f"CurrZone-{Device.sensor_zone}") self._post_after_update_monitor_msg(Device) +#............................................................................... + def _results_special_msg(self, Device): + if Device.is_offline: + return 'Offline' + + if is_empty(Device.sensors[ARRIVAL_TIME]): + # return (f"Update in {format_timer(Device.FromZone_TrackFrom.interval_secs)} " + return (f"Update in {Device.interval_str} " + f"at {Device.FromZone_TrackFrom.next_update_time}") + + if (Device.isin_zone + and Device.loc_data_zone == Device.FromZone_TrackFrom.from_zone): + return (f"Arrived {Device.FromZone_TrackFrom.from_zone_dname[:8]} " + f"at {Device.sensors[ARRIVAL_TIME].replace('@', '')}, " + f"Update {secs_to_hhmm(Device.FromZone_TrackFrom.next_update_secs)}") + + if Device.FromZone_TrackFrom.dir_of_travel != AWAY_FROM: + if Device.FromZone_TrackFrom.waze_time > 0: + arrival_secs = Device.FromZone_TrackFrom.waze_time * 60 + time_now_secs() + arrival_time = (f"in {format_timer(secs_to(arrival_secs))} " + f"at {secs_to_hhmm(arrival_secs)}") + else: + arrival_time = (f"~{Device.sensors[ARRIVAL_TIME]}, " + f"Update {format_timer(secs_to(Device.FromZone_TrackFrom.next_update_secs))}") + return (f"Arrive {Device.FromZone_TrackFrom.from_zone_dname[:8]} " + f"{arrival_time}") -#---------------------------------------------------------------------------- + + return (f"Arrive {Device.FromZone_TrackFrom.from_zone_dname[:8]} " + f"around {Device.sensors[ARRIVAL_TIME]}") + +#------------------------------------------------------------------------------- def _determine_interval_and_next_update(self, Device): ''' Determine the update interval, Update the sensors and device_tracker entity: @@ -1094,50 +1150,6 @@ def _determine_interval_and_next_update(self, Device): return True -#-------------------------------------------------------------------- - def _get_icloud_data_prefetch_device(self): - ''' - Get the time (secs) until the next update for any device. This is used to determine - when icloud data should be prefetched before it is needed. - - Return: - Device that will be updated in 5-secs - ''' - # At least 10-secs between prefetch refreshes - if (secs_since(Gb.pyicloud_refresh_time[FAMSHR]) < 10 - and secs_since(Gb.pyicloud_refresh_time[FMF]) < 10): - return None - - prefetch_before_update_secs = 5 - for Device in Gb.Devices_by_devicename_tracked.values(): - if (Device.is_data_source_ICLOUD is False - or Device.is_tracking_paused): - continue - if Device.icloud_initial_locate_done is False: - return Device - - secs_to_next_update = secs_to(Device.next_update_secs) - - if Device.inzone_interval_secs < -15 or Device.inzone_interval_secs > 15: - continue - - # If going towards a TrackFmZone and the next update is in 15-secs or less and distance < 1km - # and current location is older than 15-secs, prefetch data now - # Changed to is_approaching_tracked_zone and added error_cnt check (rc9) - if (Device.is_approaching_tracked_zone - and Device.old_loc_cnt <= 4): - Device.old_loc_threshold_secs = 15 - return Device - - if Device.is_location_gps_good: - continue - - # Updating the device in the next 10-secs - Device.display_info_msg(f"Requesting iCloud Location, Next Update in {format_timer(secs_to_next_update)} secs") - return Device - - return None - #-------------------------------------------------------------------- def _display_icloud_acct_error_msg(self, Device): ''' @@ -1173,89 +1185,112 @@ def _display_device_alert_evlog_greenbar_msg(self): Tracked device screen displayed - Show all alert messages Monitored device screen displayed - Show only that devices alerts ''' + if (Gb.EvLog.evlog_attrs['fname'] == 'Startup Events' + or Gb.EvLog.greenbar_alert_msg.startswith('Start up log') + or Gb.WazeHist.wazehist_recalculate_time_dist_running_flag): + return + + # post_evlog_greenbar_msg('Test Alert Message Display') + # return + general_alert_msg = startup_alert_attr = '' tracked_alert_attr = monitored_alert_attr = '' - if Gb.startup_alerts != []: - general_alert_msg = f"{LDOT2}Errors Starting iCloud3" + if Gb.version_hacs: + general_alert_msg += f"{LDOT2}iCloud3 {Gb.version_hacs} is available on HACS, you are running v{Gb.version}" + + if (Gb.startup_alerts + and is_empty(Gb.username_pyicloud_503_connection_error) + and is_empty(Gb.usernames_setup_error_retry_list)): + Gb.startup_alerts = [] + + if isnot_empty(Gb.username_pyicloud_503_connection_error): + general_alert_msg += (f"{LDOT2}Apple Acct Waiting to Complete Login > " + f"{list_to_str(Gb.username_pyicloud_503_connection_error)}") + + if isnot_empty(Gb.startup_alerts): startup_alert_attr = Gb.startup_alerts_str - Gb.primary_data_source_ICLOUD - if Gb.PyiCloud is None: - if Gb.conf_tracking[CONF_USERNAME] or Gb.conf_tracking[CONF_PASSWORD]: - general_alert_msg += f"{CRLF}{LDOT2}iCloud acct not logged into, Invalid Username/Password" - elif Gb.primary_data_source_ICLOUD is False: - general_alert_msg += f"{CRLF}{LDOT2}iCloud acct not logged into, Possible Connection Error" + if general_alert_msg: general_alert_msg += CRLF + general_alert_msg += f"{LDOT2}Alerts starting iCloud3{RARROW}Review Event Log for more info" + + for devicename, error_msg in Gb.conf_startup_errors_by_devicename.items(): + if general_alert_msg: general_alert_msg += CRLF + general_alert_msg += f"{LDOT2}{devicename} > {error_msg}" - elif Gb.version_hacs: - general_alert_msg = f"iCloud3 {Gb.version_hacs} is available on HACS, you are running v{Gb.version}" + for username, PyiCloud in Gb.PyiCloud_by_username.items(): + if PyiCloud and PyiCloud.requires_2fa: + if general_alert_msg: general_alert_msg += CRLF + general_alert_msg += ( f"{LDOT2}Apple Acct > {PyiCloud.account_owner_short}, " + f"Authentication Needed") if (Gb.icloud_acct_error_cnt > 5 and instr(general_alert_msg, 'errors accessing') is False): - general_alert_msg += "Internet or Apple may be down, errors accessing iCloud acct" + if general_alert_msg: general_alert_msg += CRLF + general_alert_msg += f"{LDOT2}Internet or Apple may be down, errors accessing Apple Acct" + + apple_acct_errors = [username.split('@')[0] for username in Gb.username_valid_by_username + if Gb.username_valid_by_username[username] is False] + if isnot_empty(apple_acct_errors): + if general_alert_msg: general_alert_msg += CRLF + general_alert_msg += f"{LDOT2}Apple Acct Login Errors-{list_to_str(apple_acct_errors)}" + + verified_msg = '' + poor_location_msg = '' + offline_msg = '' + paused_msg = '' for Device in Gb.Devices: device_alert_msg = '' if (Device.verified_flag is False or (Device.is_data_source_ICLOUD is False and Device.is_data_source_MOBAPP is False)): - device_alert_msg = "Not Verified, No Data Source, " - if Device.no_location_data: - device_alert_msg += "No GPS Data, " - if Device.is_offline: - device_alert_msg += "Offline, " - - if device_alert_msg: - device_alert_msg = device_alert_msg[:-2] + verified_msg += f"{Device.fname}, " elif Device.is_tracking_paused: - device_alert_msg = "Tracking Paused" - elif mins_since(Device.loc_data_secs) > 300: - device_alert_msg = f"Location Very Old (>{format_age(Device.loc_data_secs)})" - elif isbetween(Device.dev_data_battery_level, 1, 19): - device_alert_msg = f"Low Battery (< 20%)" - - show_on_displayed_evlog_screen_flag = (general_alert_msg != '') - + paused_msg += f"{Device.fname}, " + elif Device.is_tracked: + if Device.is_offline: + offline_msg += f"{Device.fname}, " + elif Device.no_location_data: + poor_location_msg += f"{Device.fname}, " + elif mins_since(Device.loc_data_secs) > 300: + poor_location_msg += f"{Device.fname} ({format_secs_since(Device.loc_data_secs)} ago), " + if isbetween(Device.dev_data_battery_level, 1, 19): + device_alert_msg = f"{Device.fname} > Low Battery ({Device.dev_data_battery_level}%)" + + crlf = CRLF_LDOT if general_alert_msg else LDOT2 if device_alert_msg: - fname = f"{Device.fname} > {device_alert_msg}" - crlf = CRLF_LDOT if general_alert_msg else LDOT2 - general_alert_msg += f"{crlf}{fname}" - if Device.is_tracked: - nldot = NL_DOT if tracked_alert_attr else DOT - tracked_alert_attr += f"{nldot}{fname}" - else: - nldot = NL_DOT if monitored_alert_attr else DOT - monitored_alert_attr += f"{nldot}{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 + general_alert_msg += f"{crlf}{device_alert_msg}" if device_alert_msg != Device.alert: Device.alert = Device.sensors[ALERT] = device_alert_msg Device.write_ha_device_tracker_state() + if paused_msg: + general_alert_msg += f"{CRLF_LDOT}Paused-{paused_msg[:-2]}" + if verified_msg: + general_alert_msg += f"{CRLF_LDOT}Setup Errors-{verified_msg[:-2]}" + if poor_location_msg: + general_alert_msg += f"{CRLF_LDOT}Poor Location-{poor_location_msg[:-2]}" + if offline_msg: + general_alert_msg += f"{CRLF_LDOT}Offline-{offline_msg[:-2]}" + Gb.EvLog.evlog_attrs['alert_startup'] = Gb.EvLog.alert_attr_filter(startup_alert_attr) Gb.EvLog.evlog_attrs['alert_tracked'] = Gb.EvLog.alert_attr_filter(tracked_alert_attr) Gb.EvLog.evlog_attrs['alert_monitored'] = Gb.EvLog.alert_attr_filter(monitored_alert_attr) - if (general_alert_msg != Gb.EvLog.greenbar_alert_msg - and show_on_displayed_evlog_screen_flag): + + if general_alert_msg != Gb.EvLog.greenbar_alert_msg: post_evlog_greenbar_msg(general_alert_msg) elif general_alert_msg == '' and Gb.EvLog.greenbar_alert_msg: clear_evlog_greenbar_msg() #---------------------------------------------------------------------------- - def _display_clear_authentication_needed_msg(self): + def _check_apple_acct_authentication_needed(self): - if Gb.PyiCloud is None: - pass - - elif (Gb.PyiCloud.requires_2fa - 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.greenbar_alert_msg == 'iCloud Account authentication is needed'): - clear_evlog_greenbar_msg() + msg = "" + for username, PyiCloud in Gb.PyiCloud_by_username.items(): + if PyiCloud.requires_2fa: + msg += (f"Apple Acct > {PyiCloud.account_owner}, " + f"Auth Code Needed") #-------------------------------------------------------------------- def _format_fname_devtype(self, Device): @@ -1313,6 +1348,36 @@ def _timer_tasks_every_hour(self): Gb.StatZones_to_delete = [StatZone for StatZone in Gb.StatZones if StatZone.radius_m == STATZONE_RADIUS_1M] +#-------------------------------------------------------------------- + def _check_apple_acct_2fa_totp_key_request(self): + + # Get all Apple accts needing a 2fa-auth code that have otp tokens + conf_apple_accts = [conf_apple_acct + for conf_apple_acct in Gb.conf_apple_accounts + if (conf_apple_acct[CONF_USERNAME] in Gb.PyiCloud_by_username + and Gb.PyiCloud_by_username[conf_apple_acct[CONF_USERNAME]].requires_2fa + and conf_apple_acct[CONF_TOTP_KEY])] + + if is_empty(conf_apple_accts): + return + + pyicloud_ic3_interface.send_totp_key(conf_apple_accts) + +#-------------------------------------------------------------------- + def _check_mobappp_location_request(self): + ''' + Update the devices info msg + Check to see if the MobApp's location needs to be refreshed + ''' + for Device in Gb.Devices_by_devicename.values(): + Device.display_info_msg(Device.format_info_msg, new_base_msg=True) + + if (Device.mobapp_monitor_flag + and Device.mobapp_request_loc_first_secs == 0 + and Device.mobapp_data_state != Device.loc_data_zone + and Device.mobapp_data_state_secs < (Gb.this_update_secs - 120)): + mobapp_interface.request_location(Device) + #-------------------------------------------------------------------- def _timer_tasks_midnight(self): @@ -1321,9 +1386,9 @@ def _timer_tasks_midnight(self): start_ic3.set_log_level('info') start_ic3.update_conf_file_log_level('info') - Gb.pyicloud_authentication_cnt = 0 - Gb.pyicloud_location_update_cnt = 0 - Gb.pyicloud_calls_time = 0.0 + for username, PyiCloud in Gb.PyiCloud_by_username.items(): + PyiCloud.authentication_cnt = 0 + PyiCloud.location_update_cnt = 0 if Gb.WazeHist: Gb.WazeHist.wazehist_delete_invalid_records() @@ -1369,7 +1434,7 @@ def _set_old_location_status(self, Device): if Device.old_loc_cnt == 1: return - cnt_msg = f"#{Device.old_loc_cnt}" + cnt_msg = f"#{Device.old_loc_cnt}.{Device.max_error_cycle_cnt}" # No GPS data takes presidence over offline if Device.no_location_data: Device.old_loc_msg = f"Location > No GPS Data {cnt_msg}" @@ -1398,7 +1463,7 @@ def _display_secs_to_next_update_info_msg(self, Device): without the age to make sure it goes away. The age may be for a non-Home zone but displat it in the Home zone sensor. ''' - if (Gb.primary_data_source_ICLOUD is False + if (Gb.use_data_source_ICLOUD is False or Device.is_data_source_ICLOUD is False or Device.is_tracking_paused): return diff --git a/custom_components/icloud3/manifest-dev.json b/custom_components/icloud3/manifest-dev.json new file mode 100644 index 0000000..00888bb --- /dev/null +++ b/custom_components/icloud3/manifest-dev.json @@ -0,0 +1,14 @@ +{ + "domain": "icloud3", + "name": "iCloud3 v3", + "after_dependencies": ["recorder", "ios"], + "codeowners": ["@gcobb321"], + "config_flow": true, + "dependencies": [], + "documentation": "https://gcobb321.github.io/icloud3_v3_docs/#/", + "iot_class": "cloud_polling", + "issue_tracker": "https://github.com/gcobb321/icloud3_v3/issues", + "loggers": ["icloud3"], + "requirements": ["srp"], + "version": "3.1" +} diff --git a/custom_components/icloud3/manifest.json b/custom_components/icloud3/manifest.json index 42184da..7df85cc 100644 --- a/custom_components/icloud3/manifest.json +++ b/custom_components/icloud3/manifest.json @@ -7,8 +7,8 @@ "dependencies": [], "documentation": "https://gcobb321.github.io/icloud3_v3_docs/#/", "iot_class": "cloud_polling", - "issue_tracker": "https://github.com/gcobb321/icloud3_v3/issues", + "issue_tracker": "https://github.com/gcobb321/icloud3/issues", "loggers": ["icloud3"], - "requirements": [], - "version": "3.0.5.8" + "requirements": ["srp"], + "version": "3.1" } diff --git a/custom_components/icloud3/sensor.py b/custom_components/icloud3/sensor.py index 3345aa3..5e79f92 100644 --- a/custom_components/icloud3/sensor.py +++ b/custom_components/icloud3/sensor.py @@ -41,7 +41,7 @@ from .helpers.common import (instr, round_to_zero, isnumber, ) from .helpers.messaging import (post_event, log_info_msg, log_debug_msg, log_error_msg, log_exception, log_info_msg_HA, log_exception_HA, - _trace, _traceha, ) + _evlog, _log, ) from .helpers.time_util import (time_to_12hrtime, time_remove_am_pm, format_timer, format_mins_timer, time_now_secs, datetime_now, secs_to_datetime, secs_to_datetime, @@ -76,10 +76,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e try: if Gb.conf_file_data == {}: start_ic3.initialize_directory_filenames() - start_ic3.load_storage_icloud3_configuration_file() + # start_ic3.load_storage_icloud3_configuration_file() + await config_file.async_load_storage_icloud3_configuration_file() + NewSensors = [] Gb.EvLogSensor = Sensor_EventLog(SENSOR_EVENT_LOG_NAME) + # Gb.EvLogSensor = Sensor_EventLog_ExcludeFromRecorder(SENSOR_EVENT_LOG_NAME) if Gb.EvLogSensor: NewSensors.append(Gb.EvLogSensor) else: @@ -479,8 +482,6 @@ def _get_extra_attributes(self, sensor): ''' extra_attrs = OrderedDict() extra_attrs['integration'] = ICLOUD3 - # extra_attrs['sensor_name'] = sensor - # extra_attrs['type'] = self._get_sensor_definition(sensor, SENSOR_TYPE).split(',')[0] update_time = secs_to_datetime(time_now_secs()) if self.Device and self.Device.away_time_zone_offset != 0: @@ -1131,11 +1132,14 @@ def _format_devices_distance_extra_attrs(self): # ''' # try: # if isnumber(number) is False: + # _evlog(f"zz {self.sensor} {number=} {um=}") # return number # um = um if um else Gb.um # precision = 5 if um in ['km', 'mi'] else 2 if um in ['m', 'ft'] else 4 + # _evlog(f"aa {self.sensor} {number=} {um=} {precision=}") # number = round(float(number), precision) + # _evlog(f"bb {self.sensor} {number=} {um=} {precision=}") # except Exception as err: # pass diff --git a/custom_components/icloud3/services.yaml b/custom_components/icloud3/services.yaml index 30384cd..8871252 100644 --- a/custom_components/icloud3/services.yaml +++ b/custom_components/icloud3/services.yaml @@ -16,7 +16,7 @@ action: - "Restart iCloud3" - "Pause Tracking" - "Resume Tracking" - - "Locate Device(s) using iCloud FamShr" + - "Locate Device(s) using iCloud" - "Send Locate Request to Mobile App" device_name: name: "Device Name" diff --git a/custom_components/icloud3/strings.json b/custom_components/icloud3/strings.json index 3c5564b..a0b639b 100644 --- a/custom_components/icloud3/strings.json +++ b/custom_components/icloud3/strings.json @@ -5,20 +5,26 @@ "config_update_complete": "iCloud3 configuration updated successfully", "already_configured": "iCloud3 is already installed and can not be installed again. Select CONFIGURE in the iCloud3 Integration entry to configure iCloud3.\n\nIf you are deleting and then reinstalling, restart HA first and then reinstall iCloud3", "disabled": "iCloud3 is DISABLED and can not be installed again. Enable iCloud3, then select CONFIGURE in the iCloud3 Integration entry to configure iCloud3.\n\nIf you are deleting and then reinstalling, restart HA first and then reinstall iCloud3", - "login_error": "An error occurred logging into the iCloud account. Verify the username and password.", + "login_error": "An error occurred logging into the Apple Account. Verify the username and password.", "reauth_successful": "The reauthentication has been successfully completed", "verification_code_accepted": "The Apple ID Verification Code was accepted. Reauthentication is complete", "verification_code_cancelled": "The Apple ID Verification was cancelled", "update_cancelled": "Update Cancelled", - "icloud3_init_error": "A problem was encountered initializing the iCloud3 Configure Settings screens. iCloud3 has probably encountered an error during initialization and was not started. Check the 'home-assistant.log' file for any errors related to iCloud3" + "icloud3_init_error": "A problem was encountered initializing the iCloud3 Configure Settings screens. iCloud3 has probably encountered an error during initialization and was not started. Check the 'home-assistant.log' file for any errors related to iCloud3", + "reauth_apple_acct_unknown": "Apple account requesting verification code is unknown", + "reauth_apple_acct_unused": "Apple account requesting verification code is not used" }, "error": { "invalid_path": "The path provided is not valid. Should be in the format `user/repo-name`", "verification_code_send_error": "Failed to send the Apple ID Verification Code", "verification_code_requested2": "The Apple ID Verification Code was requested", + "verification_code_accepted": "The Apple ID Verification Code was accepted. Reauthentication is complete", "verification_code_invalid": "The Verification Code is not correct. Reenter or request a new code", + "verification_code_needed": "The Apple Account Verification Code is needed", + "verification_code_cancelled": "The Apple ID Verification was cancelled", + "icloud_no_devices": "No devices were found in the iCloud `Family Sharing` list", - "icloud_other_error": "An unknown error was encountered authenticating the iCloud account. Try again later" + "icloud_other_error": "An unknown error was encountered authenticating the Apple Account. Try again later" }, "step": { "user": { @@ -33,8 +39,9 @@ "title": "Apple ID Verification Code (HA Notifications)", "description": "Enter the 6-digit verification code you just received from Apple", "data": { - "verification_code": "ICLOUD ACCOUNT VERIFICATION CODE", - "action_items": "═════════════════════════════════════════════════════" + "apple_account": "APPLE ACCOUNT TO BE AUTHENTICATED", + "verification_code": "APPLE ACCOUNT VERIFICATION CODE", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" } }, "restart_ha": { @@ -52,33 +59,38 @@ "already_configured": "iCloud3 is already installed and can not be installed again. Select CONFIGURE in the iCloud3 Integration entry to configure iCloud3", "disabled": "iCloud3 is DISABLED and can not be installed again. Enable iCloud, then select CONFIGURE in the iCloud3 Integration entry to configure iCloud3", "ha_restarting": "Home Assistant is Restarting\n\nTHE ICLOUD3 SCREEN MUST BE REFRESHED AFTER RESTARTING", - "ic3_reloading": "Reloading iCloud3\n\nTHE ICLOUD3 SCREEN MUST BE REFRESHED AFTER RESTARTING", + "ic3_restarting": "Restarting iCloud3", "reauth_successful": "The reauthentication has been successfully completed" }, "error": { "update_aborted": "Update aborted, an error was detected in one of the data fields", - "conf_updated": "iCloud3 Configuration Parameters were updated successfully", + "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": "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 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", + "icloud_acct_logging_into": "Logging into Apple Account", + "icloud_acct_logged_into": "✅ Logged into the Apple Account", + "icloud_acct_already_logged_into": "Already Logged into the Apple Account", + "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_503": "🍎 Apple is delaying displaying a new Verification code to prevent Suspicious Activity, probably due to too many requests. It should be displayed in about 20-30 minutes. Restart HA if it is not displayed", + "icloud_acct_login_error_srp_401": "❌ Python SRP Library Credentials Error. The Python module that creates the Secure Remote Password hash key has calculated an incorrect value for a valid Username/Password. Try changing the Password to see if the Apple Acct can be logged into. ", + "icloud_acct_username_password_error": "❌ Entry Error, Invalid Username or Password", + "icloud_acct_dup_username_error": "Error: Username is being used by another Data Source entry", + "icloud_acct_username_inuse_error": "Error: This Username is being used by another Data Source entry. This one cannot be changed to it until the other one is removed. Select the other one and STOP USING it first.", + "icloud_acct_not_available": "❌ Login Failed, Apple Account is not Available", + "icloud_acct_not_logged_into": "Not logged into the Apple Account", + "icloud_acct_updated_not_logged_into": "Apple Acct info was saved, Login Error, Will Complete Login Later", + "icloud_acct_data_source_warning": "Apple Acct is not selected as a data source, username/password are setup", + "icloud_acct_not_set_up": "Apple Account Username or Password needs to be entered", + "icloud_acct_no_data_source": "❌ No Data Source has been selected (Apple iCloud Account or Mobile App)", + "ic3_icloud_same_name": "iCloud3 dev_trkr.entity_id and name on the device (Settings > General > About) can not be exactly the same (letters & case)", "mobile_app_error": "Error, The Mobile App Integration is not installed. The Mobile App will not be used as a data source; location data and zone enter/exit triggers will not be monitored", - "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", - "verification_code_needed": "The Apple ID Verification Code is needed", - "verification_code_accepted": "The Apple ID Verification Code was accepted", - "verification_code_invalid": "The Verification Code was not correct. Reenter or request a new code", - "verification_code_send_error": "Failed to send the Apple ID Verification Code", + "verification_code_requested": "The Apple Account Verification Code was requested, BROWSER REFRESH MAY BE NEEDED", + "verification_code_requested2": "The Apple Account Verification Code was requested", + "verification_code_needed": "The Apple Account Verification Code is needed", + "verification_code_accepted": "✅ The Apple Account Verification Code was accepted", + "verification_code_invalid": "❌ The Verification Code was not correct. Reenter or request a new code", + "verification_code_send_error": "❌ Failed to send the Apple ID Verification Code", "inactive_device": "Device is INACTIVE. Change to `Track` to locate and track this device", "inactive_all_devices": "✪✪ ALL DEVICES ARE INACTIVE. NOTHING WILL BE TRACKED ✪✪", @@ -91,32 +103,25 @@ "away_time_zone_dup_devices_2": "One of these devices is also selected in the Otner Device List", "review_filledin_fields": "Review the 'Filled in' fields", - "not_numeric": "The value entered is not numeric", + "not_numeric": "❌ The value entered is not numeric", "waze_server_error_us": "The correct server for your location is: United States, Canada", "waze_server_error_il": "The correct server for your location is: Israel", "waze_server_error_row": "The correct server for your location is: Rest of the World", - "required_field": "This parameter must be specified", + "required_field": "❌ This parameter must be specified", "required_field_device": "A device providing location data must be selected from the Family Share, Find-my-Friends, or Mobile App devices lists", - "no_device_selected": "A device providing location data must be selected", + "no_device_selected": "❌ A device providing location data must be selected", "no_add_entities_device_tracker_fct": "The HA component for adding devices is not available. HA MUST BE RESTARTED", - "unknown_devicename": "The device that was previously selected can no longer be identified. It`s name may have been changed or it may have been deleted. Reselect the device to be tracked or monitored", - "unknown_value": "The value of this parameter in the iCloud3 configuration file is unknown or invalid. It must be selected again", - "unknown_famshr": "The FamShr device was not returned from iCloud when iCloud3 started. Check FindMy devices list & Family Share list. See Event Log Startup Stage 4 for more info and a list of the devices returned from the iCloud account", - "unknown_fmf": "The FmF device was not returned from iCloud when iCloud3 started. Check FindMy app devices Sharing location info. See Event Log Startup Stage 4 for more info and a list of the devices returned from the iCloud account", - "unknown_mobapp": "The mobile_app device_tracker entity was not found during HA Device Registry scan. Check Mobile App device list in HA Settings > Devices & Services > Devices. See Event Log Startup Stage 4 for more info and a list of mobile_app devices found in the HA Device Registry", - "unknown_picture": "The Picture filename was not found in the HA config/www directory. Reselect the filename or check to see if it has been deleted", + "unknown_value": "One of the selection parameters needs to be reviewed", + "unknown_devicename": "The configured device was not found in any of the Apple Accts or the Mobile App device list", + "unknown_icloud": "The configured device was not found in any of the Apple Accounts", + "unknown_mobapp": "The configured device was not found in the Mobile App devices list", + "unknown_picture": "The configured picture file was not found in `config/www/...` directories", - "unknown_famshr_fmf": "Check the FamShr and FmF parameter values (Not found or Invalid)", - "unknown_famshr_fmf_mobapp": "Check the FamShr, FmF and Mobile App parameter values (Not found or Invalid)", - "unknown_famshr_fmf_mobapp_picture": "Check the FamShr, FmF, Mobile App and Picture parameter values (Not found or Invalid)", - "unknown_famshr_mobapp": "Check the FamShr and Mobile App parameter values (Not found or Invalid)", - "unknown_famshr_mobapp_picture": "Check the FamShr, Mobile App and Picture parameter values (Not found or Invalid)", - "unknown_famshr_picture": "Check the FamShr and Picture parameter values (Not found or Invalid)", - "unknown_fmf_mobapp": "Check the FmF and Mobile App parameter values (Not found or Invalid)", - "unknown_fmf_mobapp_picture": "Check the FmF, Mobile App and Picture parameter values (Not found or Invalid)", - "unknown_fmf_picture": "Check the FmFand Picture parameter values (Not found or Invalid)", - "unknown_mobapp_picture": "Check the Mobile App and Picture parameter values (Not found or Invalid)", + "unknown_apple_acct": "Apple Account is not selected for the assigned iCloud device", + "unknown_icloud_mobapp": "Check the iCloud and Mobile App parameter values (Not found or Invalid)", + "unknown_icloud_mobapp_picture": "Check the iCloud, Mobile App and Picture parameter values (Not found or Invalid)", + "unknown_icloud_picture": "Check the iCloud and Picture parameter values (Not found or Invalid)", "tfz_selection_invalid": "The value must be a zone that is being tracked from", "time_factor_invalid_range": "The 'Travel Time Multiplier' must be between .1 and .9", @@ -128,7 +133,7 @@ "not_found_file": "The file was not found", "duplicate_ic3_devicename": "This name is already used by another iCloud3 device", "already_assigned": "This selection is already assigned to another device", - "mobapp_search_error": "WARNING: Search Failure - No Mobile App device that starts with the iCloud3 or FamShr devicename was found. Select 'None' or the device to be used.", + "mobapp_search_error": "WARNING: Search Failure - No Mobile App device that starts with the iCloud3 or Apple Acct devicename was found. Select 'None' or the device to be used.", "duplicate_other_devicename": "This name is already used in another integration or platform", "action_completed": "Requested Action has been completed", "action_cancelled": "Requested Action has been cancelled", @@ -140,91 +145,122 @@ "menu": { "title": "iCloud3 Configure Settings", "data": { - "menu_items": "", - "action_items": "═════════════════════════════════════════════════════" + "menu_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" } }, "menu_0": { "title": "Configure Devices and Sensors Menu", "data": { - "menu_items": "", - "action_items": "═════════════════════════════════════════════════════" + "xmenu_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯", + "menu_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" } }, "menu_1": { "title": "Configure Parameters Menu", "data": { - "menu_items": "", - "action_items": "═════════════════════════════════════════════════════" + "menu_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" } }, "restart_icloud3": { "title": "Confirm Restarting iCloud3", "description": "Note: Changes to tracked devices require restarting iCloud3", "data": { - "action_items": "═════════════════════════════════════════════════════" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" } }, "restart_ha_ic3": { "title": "Restart Home Assistant or iCloud3", "description": "Restart Home Assistant or reload and reinitialize the iCloud3 Integration\n\nTHE ICLOUD3 DASHBOARD SCREEN MUST BE REFRESHED AFTER RESTARTING", "data": { - "action_items": "═════════════════════════════════════════════════════" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" } }, "restart_ha_ic3_load_error": { "title": "iCloud3 Load Error", "description": "iCloud3 did not load and initialize when HA started. Reload iCloud3 again or restart HA", "data": { - "action_items": "═════════════════════════════════════════════════════" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" } }, "confirm_action": { "title": "Confirm Selected Action", "data": { - "action_items": "═════════════════════════════════════════════════════" + "confirm_action_form_hdr": "REQUESTED ACTION", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, - "icloud_account": { - "title": "iCloud Account & Mobile App Data Sources", - "description": "The data sources provide location and other information iCloud3 uses to track the iDevice.\n\nThey are:\n• ICLOUD ACCOUNT - Apple iCloud Web Services provides location and other\n  information for the devices in the Family Sharing list.\n• MOBILE APP - The HA Companion app installed on the iPhone and iPad\n  provides zone enter/exit triggers and location information. The Mobile App\n  Integration needs to be installed to use this data service.\n\nThe devices in the Family Sharing List and Mobile App are assigned to every device iCloud3 trackes on the Update Devices screen.", + "data_source": { + "title": "Data Source - Apple Account, Mobile App", + "description": "The data sources provide location and other information iCloud3 uses to track the iDevice.", "data": { - "data_source_icloud": "═════════════════════════════════════════════════════ ICLOUD ACCOUNT - Location data is provided by the Apple iCloud account", - "username": "APPLE ID (USERNAME) - The email address used to sign in to the iCloud Account", - "password": "PASSWORD - The iCloud Account Password", + "data_source": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ DATA SOURCES", + "data_source_icloud": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ DATA SOURCES", + "data_source_mobapp": "", + "apple_accts": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ APPLE ICLOUD ACCOUNTS", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" + }, + "data_description": { + } + }, + "update_apple_acct": { + "title": "Update Apple Account Username/Password", + "data": { + "account_selected": "ACCOUNT SELECTED", + "username": "USERNAME - Email address/username used to sign in to the Apple Account", + "password": "PASSWORD - Account Password", + "totp_key": "TOTP KEY - Time Based One-Time Password Key used to generate the 6-digit authentication code", + "locate_all": "ALWAYS LOCATE ALL DEVICES - Locate all the devices in the Apple Account, including those in the Family list. Unchecked will only locate the Family list devices when they are being updated (default)", "url_suffix_china": "CHINA USERS - Use Apple iCloud Web Servers located in China (.cn URL suffix)", - "data_source_mobapp": "═════════════════════════════════════════════════════ MOBILE APP - Location data is provided by the HA Mobile App", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { + "locate_all": "Devices fall into two categories, your devices and everything else. Locating all devices will get their location with fewer internet calls that might take slightly longer while Apple locates them, especially if you have a lot of devices. Not doing this results in more internet calls that are faster." + } + }, + "delete_apple_acct": { + "title": "Remove an Apple Account", + "data": { + "account_selected": "ACCOUNT SELECTED", + "device_action": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ HOW SHOULD DEVICES ASSIGNED TO THIS APPLE ACCT BE HANDLED", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ CONFIRM DELETING THE APPLE ACCOUNT" } }, "reauth": { "title": "Apple ID Verification Code", "description": "Enter the 6-digit verification code you just received from Apple", "data": { - "verification_code": "ICLOUD ACCOUNT VERIFICATION CODE", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "account_selected": "APPLE ACCOUNT TO BE AUTHENTICATED", + "verification_code": "APPLE ACCOUNT VERIFICATION CODE", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" + } + }, + "trusted_device": { + "title": "Apple Account Trusted Devices", + "description": "Select your Trusted Device", + "data": { + "trusted_device": "Trusted Device" } }, "device_list": { "title": "iCloud3 Devices", "description": "Up to 10 devices can be tracked or monitored by iCloud3. They are listed here.\n\nThis screen is used to select a device that needs to be updated, add a new device and delete a device that should not be tracked any longer.", "data": { - "xdevices": "═════════════════════════════════════════════════════ ICLOUD3 DEVICES", - "devices": "═════════════════════════════════════════════════════", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "devices": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, "add_device": { "title": "Add iCloud3 Device", "data": { - "ic3_devicename": "ICLOUD3 DEVICE_TRACKER ENTITY ID - The HA device_tracker entity assigned to this device", - "fname": "FRIENDLY NAME - Displayed in HA device_tracker and sensor names and on the Event Log", + "ic3_devicename": "ICLOUD3 ENTITY ID - The HA device_tracker entity forto this device", + "fname": "FRIENDLY NAME - Displayed in HA entities and on the Event Log", "device_type": "DEVICE TYPE - iPhone, iPad, Watch, etc.", - "tracking_mode": "TRACKING MODE - How location requests should be done (Full tracking, Monitor, Inactive)", - "mobapp": "MOBILE APP INSTALLED - The HA Mobile App is installed on this device ", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "tracking_mode": "TRACKING MODE - Location request method (Tracked, Monitored, Inactive)", + "mobapp": "MOBILE APP INSTALLED - HA Mobile App is installed on this device", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { } @@ -235,18 +271,18 @@ "data": { "ic3_devicename": "ICLOUD3 DEVICE_TRACKER ENTITY ID - The HA device_tracker entity assigned to this device", "fname": "FRIENDLY NAME - Displayed in HA device_tracker and sensor names and on the Event Log", - "device_type": "DEVICE TYPE - iPhone, iPad, Watch, etc.", + "device_type": "DEVICE TYPE - iPhone, iPad, Watch, etc.E", "tracking_mode": "TRACKING MODE - How location requests should be done (Full tracking, Monitor, Inactive)", - "famshr_devicename": "FAMILY SHARING LIST DEVICE - Use location data from this iCloud Acct Family Sharing member", - "fmf_email": "FIND-MY-FRIENDS DEVICE - Use location data from this FindMy device sharing their info with you", - "mobile_app_device": "MOBILE APP DEVICE_TRACKER ENTITY - Use this HA device location data & zone enter/exit triggers", - "log_zones": "LOG ACTIVITY FOR ZONES - Enter/exit zone info (date, time, distance) is saved to a spreadsheet .csv file", - "track_from_zones": "TRACK-FROM-ZONES - Track travel time & distance from Home and other zones", - "track_from_base_zone": "PRIMARY TRACK-FROM-HOME ZONE OVERRIDE - Use this zone instead of the Home for tracking results", - "picture": "PICTURE - Photo image of the person normally using this device (44x44 pixels is a good size)", + "famshr_devicename": "APPLE ACCOUNT iCLOUD DEVICE - Apple iCloud device providing location data", + "mobile_app_device": "MOBILE APP DEVICE_TRACKER ENTITY - Mobile App device providing location data & zone triggers", + "picture": "PICTURE - Image of the person normally using this device (44x44 pixels is a good size)", "inzone_interval": "INZONE INTERVAL", + "rarely_updated_parms": "RARELY UPDATED PARAMETERS - Select and Submit to update these items", + "log_zones": "ZONE LOG ACTIVITY - Enter/exit zone info (date, time, distance) is saved to a spreadsheet .csv file", + "track_from_zones": "TRACK-FROM-ZONES - Track travel time & distance from Home and other zones", + "track_from_base_zone": "TRACK-FROM-HOME ZONE OVERRIDE - Use this zone instead of the Home for tracking results", "fixed_interval": "FIXED INTERVAL", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { "inzone_interval": "Time between location requests when in a zone", @@ -257,23 +293,24 @@ "delete_device": { "title": "Delete iCloud3 Device", "data": { - "action_items": "═════════════════════════════════════════════════════ DELETE OPTIONS" + "device_selected": "SELECTED DEVICE", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ DELETE OPTIONS" } }, "review_inactive_devices": { "title": "Review Untracked (Inactive) Devices", "description": "The 'Tracking Mode' of devices are set to 'Inactive' and will not be located or tracked.", "data": { - "inactive_devices": "═════════════════════════════════════════════════════ INACTIVE ICLOUD3 DEVICES", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "inactive_devices": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ INACTIVE ICLOUD3 DEVICES", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, "change_device_order": { "title": "Event Log Device Display Sequence", "description": "The devices are displayed in the Event Log heading area and in various Event Log messages in the sequence below.\n\nThis screen lets you change the order of the devices.", "data": { - "device_desc": "═════════════════════════════════════════════════════ ICLOUD3 DEVICES", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "device_desc": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ICLOUD3 DEVICES", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, "away_time_zone": { @@ -284,7 +321,7 @@ "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" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { "time_zone_1_offset": "PRIMARY DEVICES & LOCATION TIME - Devices and the current location time when away and in another time zone", @@ -295,17 +332,18 @@ "title": "Format Settings", "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", + "log_level": "LOG LEVEL - The type of messages that are written to the iCloud3 Log file (icloud3-0.log}", + "log_level_devices": "RAWDATA LOG DEVICE FILTER - Write iCloud RawData to the log file for only these devices", "display_zone_format": "EVENT LOG ZONE DISPLAY NAME - How the Zone name is displayed in sensors and the Event Log", "device_tracker_state_source": "DEVICE TRACKER STATE VALUE - How the device's device_tracker entity state value is determined", "time_format": "TIME FORMAT - How time fields are displayed in sensors and in the Event Log", - "unit_of_measurement": "UNIT OF MEASUREMENT - How distance fieldsare displayed in sensors and in the Event Log", + "unit_of_measurement": "UNIT OF MEASUREMENT - How distance fields are displayed in sensors and in the Event Log", "display_gps_lat_long2": "DISPLAY GPS COORDINATES - Display the GPS (Latitude, Longitude/±Accuracy) or only the GPS (/±Accuracy) in the Event Log", "display_gps_lat_long": "DISPLAY GPS COORDINATES - Display GPS-(22.32771, -76.33073/±35m) instead of GPS-/±35m in the Event Log", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { + "log_level": "iCloud3 can log configuration parameters, startup activity and errors, tracking activity, error messages and the Apple Account requests for iCloud Device location RawData information (request and response). Log levels specify the type of records that should be written to the iCloud3 log file (`icloud3-0.log`) from basic (Info) to more detailed (Debug) to extremely detailed (RawData).", "device_tracker_state_source": "HA uses the device's gps coordinates to determine the zone. The gps accuracy is not considered so the zone may be exited when the gps wanders out of the zone. iCloud3 does consider the gps accuracy and will not exit the zone when this occurs. iCloud3 will display the Zone's Friendly Name or zone name displayed on the Event Log." } }, @@ -313,8 +351,8 @@ "title": "Event Log 'Display Text As'", "description": "There may be some text fields, such as email addresses or phone numbers, that are displayed on the Event Log screen that are private and should not be displayed. For example, you can replace 'geekstergary@apple.com' with 'gary@email.com'.\n\nThis screenis used to specify the original text and the text that should be displayed.", "data": { - "display_text_as": "═════════════════════════════════════════════════════ TEXT REPLACEMENT FIELDS - [Actual text > Displayed text]", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "display_text_as": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ TEXT REPLACEMENT FIELDS - [Actual text > Displayed text]", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, "display_text_as_update": { @@ -322,7 +360,7 @@ "data": { "text_from": "ORIGINAL TEXT - Text to be replaced (example: gary_real_email@gmail.com)", "text_to": "DISPLAYED TEXT- Text to be displayed (display: gary@email.com)", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { } @@ -330,10 +368,10 @@ "actions": { "title": "iCloud3 Action Commands", "data": { - "ic3_actions": "═════════════════════════════════════════════════════ ICLOUD3 GENERAL CONTROL ACTIONS", - "debug_actions": "═════════════════════════════════════════════════════ DEBUG LOG ACTIONS", - "other_actions": "═════════════════════════════════════════════════════ OTHER ACTIONS", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "ic3_actions": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ICLOUD3 GENERAL CONTROL ACTIONS", + "debug_actions": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ DEBUG LOG ACTIONS", + "other_actions": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ OTHER ACTIONS", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, "inzone_intervals": { @@ -347,7 +385,7 @@ "no_mobapp": "MOBILE APP IS NOT INSTALLED", "other": "OTHER DEVICE TYPE", "distance_between_devices": "Determine the distance between devices. Use a near by device's tracking results", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { "no_mobapp": "Default interval if the Mobile App is not used for location monitoring and zone enter/exit triggers", @@ -357,15 +395,15 @@ "waze_main": { "title": "Waze - Route Service Travel Time/Distance", "data": { - "waze_used": "═════════════════════════════════════════════════════ WAZE ROUTE SERVICE", + "waze_used": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ WAZE ROUTE SERVICE", "waze_region": "ROUTE SERVER LOCATION - Location of the Waze Route Server for your area", "waze_realtime": "USE REAL TIME DATA - Waze should consider traffic delays when determining travel time", "waze_min_distance": "WAZE MINIMUM DISTANCE", "waze_max_distance": "WAZE MAXIMUM DISTANCE", - "waze_history_database_used": "═════════════════════════════════════════════════════ WAZE HISTORY DATABASE", + "waze_history_database_used": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ WAZE HISTORY DATABASE", "waze_history_track_direction": "GENERAL TRAVEL DIRECTION - Used to display 'Map Trace Lines' between saved locations", "waze_history_max_distance": "HISTORY MAX DISTANCE", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { "waze_min_distance": "Use the Waze Route Service when the zone distance is greater than this value", @@ -377,16 +415,16 @@ "title": "Special Zones", "description": "This screen is used to configure:\n  • Stationary Zones - Created when the device is in the same location for a short\n    period of time\n  • Enter Zone Delay Time - Delay processing a Zone Enter Trigger\n  • A temporary “home” zone at another location", "data": { - "stat_zone_header": "═════════════════════════════════════════════════════ STATIONARY ZONE", + "stat_zone_header": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ STATIONARY ZONE", "stat_zone_fname": "FRIENDLY NAME BASE - Name to display when in a Stationary Zone (StatZone)", "stat_zone_still_time": "NO MOVEMENT TIME", "stat_zone_inzone_interval": "INZONE INTERVAL", - "passthru_zone_header": "═════════════════════════════════════════════════════ ENTER ZONE DELAY", + "passthru_zone_header": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ENTER ZONE DELAY", "passthru_zone_time": "ENTER ZONE DELAY TIME", - "track_from_base_zone_used": "═════════════════════════════════════════════════════ PRIMARY TRACK-FROM-HOME ZONE OVERRIDE", + "track_from_base_zone_used": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ PRIMARY TRACK-FROM-HOME ZONE OVERRIDE", "track_from_base_zone": "TRACK FROM ZONE - Use this zone instead of Home for tracking results for all devices. Global setting", "track_from_home_zone": "TRACK FROM HOME ZONE - Keep tracking from the Home zone when the Primary Track From Zone is not Home", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { "passthru_zone_time": "Delay processing an Enter Zone Trigger that you may be driving through and not actually entering", @@ -399,17 +437,17 @@ "title": "Sensors", "description": "Many sensors are used to display tracking results and other information for a device.\n\n This screen is used to select the sensors that should be created.", "data": { - "monitored_devices": "═════════════════════════════════════════════════════ MONITORED DEVICE SENSORS - Select the type of sensors to create for a Monitored Device", - "device": "═════════════════════════════════════════════════════ DEVICE SENSORS - Device status and information", - "tracking_update": "═════════════════════════════════════════════════════ LOCATION UPDATE SENSORS - Device location update times", - "tracking_time": "═════════════════════════════════════════════════════ TIME SENSORS - Device tracking timers", - "tracking_distance": "═════════════════════════════════════════════════════ DISTANCE SENSORS - Device tracking distances", - "track_from_zones": "═════════════════════════════════════════════════════ TRACK FROM MULTIPLE ZONE SENSORS - Used when tracking from more than one zone (not needed for tracking only from the Home zone)", - "tracking_other": "═════════════════════════════════════════════════════ OTHER TRACKING SENSORS - Not normally used but available", - "zone": "═════════════════════════════════════════════════════ ZONE SENSORS - Device zone status and information", - "other": "═════════════════════════════════════════════════════ OTHER SENSORS - Sensors not in the above areas", - "excluded_sensors": "═════════════════════════════════════════════════════ EXCLUDED SENSORS - Sensors that will not be created when iCloud3 starts", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "monitored_devices": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ MONITORED DEVICE SENSORS - Select the type of sensors to create for a Monitored Device", + "device": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ DEVICE SENSORS - Device status and information", + "tracking_update": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ LOCATION UPDATE SENSORS - Device location update times", + "tracking_time": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ TIME SENSORS - Device tracking timers", + "tracking_distance": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ DISTANCE SENSORS - Device tracking distances", + "track_from_zones": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ TRACK FROM MULTIPLE ZONE SENSORS - Used when tracking from more than one zone (not needed for tracking only from the Home zone)", + "tracking_other": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ OTHER TRACKING SENSORS - Not normally used but available", + "zone": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ZONE SENSORS - Device zone status and information", + "other": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ OTHER SENSORS - Sensors not in the above areas", + "excluded_sensors": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ EXCLUDED SENSORS - Sensors that will not be created when iCloud3 starts", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, "exclude_sensors": { @@ -419,7 +457,7 @@ "excluded_sensors": "EXCLUDED SENSORS - Sensors that will not be created when iCloud3 starts", "filter": "FILTER DISPLAYED SENSORS - Select the Sensors that should be displayed", "filtered_sensors": "ICLOUD3 SENSORS - A list of Sensors that are created when iCloud3 starts", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, "tracking_parameters": { @@ -441,7 +479,7 @@ "picture_www_dirs": "WWW DIRECTORIES WITH PICTURE IMAGES - Filter for `Update Devices > Pictures` file locations", "event_log_card_directory": "EVENT LOG CARD LOVELACE RESOURCES DIRECTORY - Event Log custom card .js file directory", "event_log_btnconfig_url": "EVENT LOG CONFIGURE BUTTON (GEAR) URL > Special URL that display's the HA Configure Settings screen", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { "old_location_threshold": "Locations older than this value will be discarded", diff --git a/custom_components/icloud3/support/config_file.py b/custom_components/icloud3/support/config_file.py index 42c8940..dd5c09c 100644 --- a/custom_components/icloud3/support/config_file.py +++ b/custom_components/icloud3/support/config_file.py @@ -2,8 +2,8 @@ from ..global_variables import GlobalVariables as Gb from ..const import ( ICLOUD3, APPLE_SPECIAL_ICLOUD_SERVER_COUNTRY_CODE, - RARROW, HHMMSS_ZERO, DATETIME_ZERO, NONE_FNAME, INACTIVE_DEVICE, - ICLOUD, FAMSHR, FMF, NO_MOBAPP, NO_IOSAPP, HOME, + RARROW, RARROW2, HHMMSS_ZERO, DATETIME_ZERO, NONE_FNAME, INACTIVE_DEVICE, + ICLOUD, MOBAPP, NO_MOBAPP, NO_IOSAPP, HOME, CONF_PARAMETER_TIME_STR, CONF_INZONE_INTERVALS, CONF_FIXED_INTERVAL, CONF_EXIT_ZONE_INTERVAL, @@ -12,11 +12,12 @@ CONF_EVLOG_CARD_DIRECTORY, CONF_EVLOG_CARD_PROGRAM, CONF_TRAVEL_TIME_FACTOR, CONF_EVLOG_VERSION, CONF_EVLOG_VERSION_RUNNING, CONF_EVLOG_BTNCONFIG_URL, CONF_UPDATE_DATE, CONF_VERSION_INSTALL_DATE, - CONF_PASSWORD, CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX, + CONF_USERNAME, CONF_PASSWORD, CONF_LOCATE_ALL, CONF_TOTP_KEY, + 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_LOG_LEVEL_DEVICES, CONF_DATA_SOURCE, CONF_DISPLAY_GPS_LAT_LONG, CONF_LOG_ZONES, - CONF_FAMSHR_DEVICENAME, CONF_FMF_EMAIL, + CONF_APPLE_ACCOUNT, CONF_FAMSHR_DEVICENAME, CONF_MOBILE_APP_DEVICE, CONF_IOSAPP_DEVICE, CONF_TRACKING_MODE, CONF_PICTURE, CONF_INZONE_INTERVAL, CONF_TRACK_FROM_ZONES, @@ -34,13 +35,15 @@ CONF_EXCLUDED_SENSORS, CONF_OLD_LOCATION_ADJUSTMENT, CONF_DISTANCE_BETWEEN_DEVICES, CONF_PICTURE_WWW_DIRS, PICTURE_WWW_STANDARD_DIRS, RANGE_DEVICE_CONF, RANGE_GENERAL_CONF, MIN, MAX, STEP, RANGE_UM, + CF_PROFILE, CF_DATA, CF_TRACKING, CF_GENERAL, CF_SENSORS, + CONF_DEVICES, CONF_APPLE_ACCOUNTS, ) from ..support import start_ic3 from ..support import waze -from ..helpers.common import (instr, ordereddict_to_dict, isbetween, - load_json_file, async_load_json_file, save_json_file, ) -from ..helpers.messaging import (log_exception, _trace, _traceha, log_info_msg, ) +from ..helpers import file_io +from ..helpers.common import (instr, ordereddict_to_dict, isbetween, list_add, ) +from ..helpers.messaging import (log_exception, _evlog, _log, log_info_msg, ) from ..helpers.time_util import (datetime_now, ) import os @@ -52,53 +55,35 @@ _LOGGER = logging.getLogger(f"icloud3") -#------------------------------------------------------------------------------------------- +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# configuration file I/O +# +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +async def async_load_storage_icloud3_configuration_file(): + return await Gb.hass.async_add_executor_job(load_storage_icloud3_configuration_file) + def load_storage_icloud3_configuration_file(): - if os.path.exists(Gb.ha_storage_icloud3) is False: - os.makedirs(Gb.ha_storage_icloud3) + # Make the .storage/icloud3 directory if it does not exist + file_io.make_directory(Gb.ha_storage_icloud3) - if os.path.exists(Gb.icloud3_config_filename) is False: + if file_io.file_exists(Gb.icloud3_config_filename) is False: _LOGGER.info(f"Creating Configuration File-{Gb.icloud3_config_filename}") Gb.conf_file_data = CF_DEFAULT_IC3_CONF_FILE.copy() - build_initial_config_file_structure() + _build_initial_config_file_structure() write_storage_icloud3_configuration_file() success = read_storage_icloud3_configuration_file() if success: write_storage_icloud3_configuration_file('_backup') - else: - datetime = datetime_now().replace('-', '.').replace(':', '.').replace(' ', '-') - json_errors_filename = f"{Gb.icloud3_config_filename}_errors_{datetime}" - log_msg = ( f"iCloud3 Error > Configuration file failed to load, JSON Errors were encountered. " - f"Configuration file with errors was saved to `{json_errors_filename}`. " - f"Will restore from `configuration_backup` file") - _LOGGER.warning(log_msg) - os.rename(Gb.icloud3_config_filename, json_errors_filename) - success = read_storage_icloud3_configuration_file('_backup') - - if success: - log_msg = ("Restore from backup configuration file was successful") - _LOGGER.warning(log_msg) + _restore_config_file_from_backup() + _add_parms_and_check_config_file() - write_storage_icloud3_configuration_file() - - else: - _LOGGER.error(f"iCloud3{RARROW}Restore from backup configuration file failed") - _LOGGER.error(f"iCloud3{RARROW}Recreating configuration file with default parameters-" - f"{Gb.icloud3_config_filename}") - build_initial_config_file_structure() - Gb.conf_file_data = CF_DEFAULT_IC3_CONF_FILE.copy() - write_storage_icloud3_configuration_file() - read_storage_icloud3_configuration_file() - - config_file_check_new_ic3_version() - config_file_check_range_values() - config_file_check_devices() - count_device_tracking_methods_configured() + _count_device_tracking_methods_configured() if CONF_LOG_LEVEL in Gb.conf_general: start_ic3.set_log_level(Gb.conf_general[CONF_LOG_LEVEL]) @@ -121,99 +106,588 @@ def read_storage_icloud3_configuration_file(filename_suffix=''): try: filename = f"{Gb.icloud3_config_filename}{filename_suffix}" - Gb.conf_file_data = load_json_file(filename) + Gb.conf_file_data = file_io.read_json_file(filename) if Gb.conf_file_data == {}: return False - Gb.conf_profile = Gb.conf_file_data['profile'] - Gb.conf_data = Gb.conf_file_data['data'] + Gb.conf_profile = Gb.conf_file_data[CF_PROFILE] + Gb.conf_data = Gb.conf_file_data[CF_DATA] - Gb.conf_tracking = Gb.conf_data['tracking'] - Gb.conf_devices = Gb.conf_data['tracking']['devices'] - Gb.conf_general = Gb.conf_data['general'] - Gb.conf_sensors = Gb.conf_data['sensors'] + Gb.conf_tracking = Gb.conf_data[CF_TRACKING] + Gb.conf_apple_accounts = Gb.conf_tracking.get(CONF_APPLE_ACCOUNTS, []) + Gb.conf_devices = Gb.conf_tracking.get(CONF_DEVICES, []) + Gb.conf_general = Gb.conf_data[CF_GENERAL] + Gb.conf_sensors = Gb.conf_data[CF_SENSORS] Gb.log_level = Gb.conf_general[CONF_LOG_LEVEL] - Gb.conf_tracking[CONF_PASSWORD] = decode_password(Gb.conf_tracking[CONF_PASSWORD]) + _add_parms_and_check_config_file() + + return True + + except Exception as err: + _LOGGER.exception(err) + + return False + +#-------------------------------------------------------------------- +def write_storage_icloud3_configuration_file(filename_suffix=None): + ''' + Update the config/.storage/.icloud3.configuration file + + Parameters: + filename_suffix: A suffix added to the filename that allows saving multiple copies o + the configuration file + ''' + _reconstruct_conf_file() + + try: + if filename_suffix is None: filename_suffix = '' + filename = f"{Gb.icloud3_config_filename}{filename_suffix}" + + success = file_io.save_json_file(filename, Gb.conf_file_data) + + except Exception as err: + _LOGGER.exception(err) + return False + + # Update conf_devices devicename index dictionary + if len(Gb.conf_devices) != len(Gb.conf_devices_idx_by_devicename): set_conf_devices_index_by_devicename() - if instr(Gb.conf_tracking[CONF_DATA_SOURCE], FMF): - Gb.conf_tracking[CONF_DATA_SOURCE].pop(FMF) - try: - config_file_add_new_parameters() + return success + +#-------------------------------------------------------------------- +async def async_write_storage_icloud3_configuration_file(filename_suffix=None): + ''' + Update the config/.storage/.icloud3.configuration file + + Parameters: + filename_suffix: A suffix added to the filename that allows saving multiple + copies of the configuration file + ''' + _reconstruct_conf_file() - except Exception as err: - log_exception(err) - _LOGGER.error( "iCloud3 > An error occured verifying the iCloud3 " + try: + if filename_suffix is None: filename_suffix = '' + filename = f"{Gb.icloud3_config_filename}{filename_suffix}" + + success = await file_io.async_save_json_file(filename, Gb.conf_file_data) + + except Exception as err: + _LOGGER.exception(err) + return False + + # Update conf_devices devicename index dictionary + if len(Gb.conf_devices) != len(Gb.conf_devices_idx_by_devicename): + set_conf_devices_index_by_devicename() + + return success + +#-------------------------------------------------------------------- +def _reconstruct_conf_file(): + ''' + Move the Gb.conf_xxx variables back to the file's actual Gb.conf_file_data/ + Gb.conf_xxx[xxx] dictionary items. + + The Gb.conf_tracking[CONF_PASSWORD] field contains the real password + while iCloud3 is running. This makes it easier logging into PyiCloud + and in config_flow. Save it, then put the encoded password in the file + update the file and then restore the real password + ''' + Gb.conf_profile[CONF_UPDATE_DATE] = datetime_now() + + Gb.conf_tracking[CONF_PASSWORD] = \ + encode_password(Gb.conf_tracking[CONF_PASSWORD]) + + Gb.conf_tracking[CONF_APPLE_ACCOUNTS] = Gb.conf_apple_accounts + Gb.conf_tracking[CONF_DEVICES] = Gb.conf_devices + Gb.conf_data[CF_TRACKING] = Gb.conf_tracking + Gb.conf_data[CF_GENERAL] = Gb.conf_general + Gb.conf_data[CF_SENSORS] = Gb.conf_sensors + + Gb.conf_file_data[CF_PROFILE] = Gb.conf_profile + Gb.conf_file_data[CF_DATA] = Gb.conf_data + +#-------------------------------------------------------------------- +def _restore_config_file_from_backup(): + + datetime = datetime_now().replace('-', '.').replace(':', '.').replace(' ', '-') + json_errors_filename = f"{Gb.icloud3_config_filename}_errors_{datetime}" + log_msg = ( f"iCloud3 Error > Configuration file failed to load, JSON Errors were encountered. " + f"Configuration file with errors was saved to `{json_errors_filename}`. " + f"Will restore from `configuration_backup` file") + _LOGGER.warning(log_msg) + file_io.rename_file(Gb.icloud3_config_filename, json_errors_filename) + success = read_storage_icloud3_configuration_file('_backup') + + if success: + log_msg = ("Restore from backup configuration file was successful") + _LOGGER.warning(log_msg) + + write_storage_icloud3_configuration_file() + + else: + _LOGGER.error(f"iCloud3{RARROW}Restore from backup configuration file failed") + _LOGGER.error(f"iCloud3{RARROW}Recreating configuration file with default parameters-" + f"{Gb.icloud3_config_filename}") + _build_initial_config_file_structure() + Gb.conf_file_data = CF_DEFAULT_IC3_CONF_FILE.copy() + write_storage_icloud3_configuration_file() + read_storage_icloud3_configuration_file() + +#-------------------------------------------------------------------- +def _add_parms_and_check_config_file(): + + try: + #Add new parameters, check parameter settings + update_config_file_flag = False + update_config_file_flag = _config_file_check_new_ic3_version() or update_config_file_flag + update_config_file_flag = _update_tracking_parameters() or update_config_file_flag + update_config_file_flag = _update_device_parameters() or update_config_file_flag + update_config_file_flag = _update_general_parameters() or update_config_file_flag + update_config_file_flag = _config_file_check_range_values() or update_config_file_flag + update_config_file_flag = _config_file_check_device_settings() or update_config_file_flag + if update_config_file_flag: + write_storage_icloud3_configuration_file() + + decode_all_passwords() + build_log_file_filters() + set_conf_devices_index_by_devicename() + + except Exception as err: + _LOGGER.exception(err) + _LOGGER.error( "iCloud3 > An error occured verifying the iCloud3 " "Configuration File. Will continue.") - return True +#-------------------------------------------------------------------- +def set_conf_devices_index_by_devicename(): + ''' + Update the device name index position in the conf_devices parameter. + This let you access a devices configuration without searching through + the devices list to get a specific device. + + idx = Gb.conf_devices_idx_by_devicename('gary_iphone') + conf_device = Gb.conf_devices.index(idx) + ''' + Gb.conf_devices_idx_by_devicename = {} + for index, conf_device in enumerate(Gb.conf_devices): + Gb.conf_devices_idx_by_devicename[conf_device[CONF_IC3_DEVICENAME]] = index + +def get_conf_device(devicename): + idx = Gb.conf_devices_idx_by_devicename.get(devicename, -1) + if idx == -1: + return None + + return Gb.conf_devices[idx] + +#------------------------------------------------------------------------------------------- +async def async_load_icloud3_ha_config_yaml(ha_config_yaml): + + Gb.ha_config_yaml_icloud3_platform = {} + if ha_config_yaml == '': + return + + ha_config_yaml_devtrkr_platforms = ordereddict_to_dict(ha_config_yaml)['device_tracker'] + + ic3_ha_config_yaml = {} + for ha_config_yaml_platform in ha_config_yaml_devtrkr_platforms: + if ha_config_yaml_platform['platform'] == 'icloud3': + ic3_ha_config_yaml = ha_config_yaml_platform.copy() + break + + Gb.ha_config_yaml_icloud3_platform = ordereddict_to_dict(ic3_ha_config_yaml) + +#-------------------------------------------------------------------- +def build_log_file_filters(): + + try: + for apple_acct in Gb.conf_apple_accounts: + if instr(apple_acct[CONF_USERNAME], '@'): + email_extn = f"{apple_acct[CONF_USERNAME].split('@')[1]}" + list_add(Gb.log_file_filter, email_extn) + + list_add(Gb.log_file_filter, apple_acct[CONF_PASSWORD]) + list_add(Gb.log_file_filter, Gb.PyiCloud_password_by_username[apple_acct[CONF_USERNAME]]) + + except Exception as err: + _LOGGER.exception(err) + +#-------------------------------------------------------------------- +def _build_initial_config_file_structure(): + ''' + Create the initial data structure of the ic3 config file + + |---profile + |---data + |---tracking + |---devices + |---general + |---parameters + |---sensors + + ''' + + Gb.conf_profile = DEFAULT_PROFILE_CONF.copy() + Gb.conf_tracking = DEFAULT_TRACKING_CONF.copy() + Gb.conf_apple_accounts = [] + Gb.conf_devices = [] + Gb.conf_general = DEFAULT_GENERAL_CONF.copy() + Gb.conf_sensors = DEFAULT_SENSORS_CONF.copy() + Gb.conf_file_data = CF_DEFAULT_IC3_CONF_FILE.copy() + + Gb.conf_data[CF_TRACKING] = Gb.conf_tracking + Gb.conf_data[CF_GENERAL] = Gb.conf_general + Gb.conf_data[CF_SENSORS] = Gb.conf_sensors + + Gb.conf_file_data[CF_PROFILE] = Gb.conf_profile + Gb.conf_file_data[CF_DATA] = Gb.conf_data + + # Verify general parameters and make any necessary corrections + try: + if Gb.country_code in APPLE_SPECIAL_ICLOUD_SERVER_COUNTRY_CODE: + Gb.conf_tracking[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] = Gb.country_code + + if Gb.config and Gb.config.units['name'] != 'Imperial': + Gb.conf_general[CONF_UNIT_OF_MEASUREMENT] = 'km' + Gb.conf_general[CONF_TIME_FORMAT] = '24-hour' + + elif Gb.ha_use_metric: + Gb.conf_general[CONF_UNIT_OF_MEASUREMENT] = 'km' + Gb.conf_general[CONF_TIME_FORMAT] = '24-hour' + + except: + pass + +#-------------------------------------------------------------------- +def conf_apple_acct(idx_or_username): + ''' + Extract and return the Apple Account configuration item by it's index or + by username + + Returns: + - conf_apple_acct = dictionary item + - conf_apple_acct_idx = index + ''' + try: + if type(idx_or_username) is int: + if isbetween(idx_or_username, 0, len(Gb.conf_apple_accounts)-1): + conf_apple_acct = Gb.conf_apple_accounts[idx_or_username].copy() + conf_apple_acct[CONF_PASSWORD] = decode_password(conf_apple_acct[CONF_PASSWORD]) + return (conf_apple_acct, idx_or_username) + + elif type(idx_or_username) is str: + conf_apple_acct = [apple_account for apple_account in Gb.conf_apple_accounts + if apple_account[CONF_USERNAME] == idx_or_username] + conf_apple_acct_username = [apple_account[CONF_USERNAME] for apple_account in Gb.conf_apple_accounts] + conf_apple_acct_idx = conf_apple_acct_username.index(idx_or_username) + + if conf_apple_acct != []: + conf_apple_acct = conf_apple_acct[0] + conf_apple_acct[CONF_PASSWORD] = decode_password(conf_apple_acct[CONF_PASSWORD]) + return (conf_apple_acct, conf_apple_acct_idx) except Exception as err: log_exception(err) + pass - return False + return ({}, -1) #-------------------------------------------------------------------- -def count_device_tracking_methods_configured(): +def apple_acct_username_password(idx): ''' - Count the number of devices that have been configured for the famshr, + Extract and return the Apple Account username & password + + Note: idx = -1 when adding a new username + ''' + if idx < 0 or idx > (len(Gb.conf_apple_accounts)-1): + return ('', '', '', True) + + try: + return (Gb.conf_apple_accounts[idx][CONF_USERNAME], + decode_password(Gb.conf_apple_accounts[idx][CONF_PASSWORD]), + Gb.conf_apple_accounts[idx][CONF_LOCATE_ALL]) + except: + return ('', '', '' , True) + +#-------------------------------------------------------------------- +def apple_acct_password_for_username(username): + ''' + Extract and return the Apple Account password for a username + ''' + if username is None: + return '' + + try: + return [apple_account[CONF_PASSWORD] + for apple_account in Gb.conf_apple_accounts + if apple_account[CONF_USERNAME] == username][0] + except: + return '' + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# Verify various configuration file parameters +# +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def _count_device_tracking_methods_configured(): + ''' + Count the number of devices that have been configured for the icloud, fmf and Mobile App tracking methods. This will be compared to the actual number of devices returned from iCloud during setup in PyiCloud. Sometmes, - iCloud does not return all devices in the FamShr list and a refresh/retry + iCloud does not return all devices in the iCloud list and a refresh/retry is needed. ''' try: - Gb.conf_famshr_device_cnt = 0 - Gb.conf_fmf_device_cnt = 0 + Gb.conf_icloud_device_cnt = 0 + # Gb.conf_fmf_device_cnt = 0 Gb.conf_mobapp_device_cnt = 0 - for conf_device in Gb.conf_devices: - if conf_device[CONF_TRACKING_MODE] == INACTIVE_DEVICE: - continue + for conf_device in Gb.conf_devices: + if conf_device[CONF_TRACKING_MODE] == INACTIVE_DEVICE: + continue + + if conf_device[CONF_FAMSHR_DEVICENAME].startswith(NONE_FNAME) is False: + Gb.conf_icloud_device_cnt += 1 + + # if conf_device[CONF_FMF_EMAIL].startswith(NONE_FNAME) is False: + # Gb.conf_fmf_device_cnt += 1 + + if conf_device[CONF_MOBILE_APP_DEVICE].startswith(NONE_FNAME) is False: + Gb.conf_mobapp_device_cnt += 1 + + except Exception as err: + _LOGGER.exception(err) + +#-------------------------------------------------------------------- +def _config_file_check_new_ic3_version(): + ''' + Check to see if this is a new iCloud3 version + ''' + new_icloud3_version_flag = False + if Gb.conf_profile[CONF_IC3_VERSION] != f"{VERSION}{VERSION_BETA}": + Gb.conf_profile[CONF_IC3_VERSION] = f"{VERSION}{VERSION_BETA}" + Gb.conf_profile[CONF_VERSION_INSTALL_DATE] = datetime_now() + new_icloud3_version_flag = True + + elif Gb.conf_profile[CONF_VERSION_INSTALL_DATE] == DATETIME_ZERO: + Gb.conf_profile[CONF_VERSION_INSTALL_DATE] = datetime_now() + new_icloud3_version_flag = True + + return new_icloud3_version_flag + +#-------------------------------------------------------------------- +def _config_file_check_range_values(): + ''' + Check the min and max value of the items that have a range in config_flow to make + sure the actual value in the config file is within the min-max range + ''' + try: + range_errors = {} + update_configuration_flag = False + + range_errors.update({pname: DEFAULT_GENERAL_CONF.get(pname, range[MIN]) + for pname, range in RANGE_GENERAL_CONF.items() + if Gb.conf_general[pname] < range[MIN]}) + range_errors.update({pname: DEFAULT_GENERAL_CONF.get(pname, range[MAX]) + for pname, range in RANGE_GENERAL_CONF.items() + if Gb.conf_general[pname] > range[MAX]}) + update_configuration_flag = (range_errors != {}) + + for pname, pvalue in range_errors.items(): + log_info_msg( f"iCloud3 Config Parameter out of range, resetting to valid value, " + f"Parameter-{pname}, From-{Gb.conf_general[pname]}, To-{pvalue}") + Gb.conf_general[pname] = pvalue + + trav_time_factor = Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] + if Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] in [.25, .33, .5, .66, .75]: + pass + elif Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] < .3: + Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] = .25 + elif Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] < .4: + Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] = .33 + elif Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] < .6: + Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] = .5 + elif Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] < .7: + Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] = .66 + else: + Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] = .75 + if trav_time_factor != Gb.conf_general[CONF_TRAVEL_TIME_FACTOR]: + update_configuration_flag = True + + if update_configuration_flag: + write_storage_icloud3_configuration_file() + + except Exception as err: + _LOGGER.exception(err) + +#-------------------------------------------------------------------- +def _config_file_check_device_settings(): + ''' + Cycle thru the conf_devices and verify that the settings are valid + ''' + update_configuration_flag = False + + for conf_device in Gb.conf_devices: + if conf_device[CONF_PICTURE] == '': + conf_device[CONF_PICTURE] = 'None' + update_configuration_flag = True + if conf_device[CONF_INZONE_INTERVAL] < 5: + conf_device[CONF_INZONE_INTERVAL] = 5 + update_configuration_flag = True + if conf_device[CONF_LOG_ZONES]== []: + conf_device[CONF_LOG_ZONES] = ['none'] + update_configuration_flag = True + 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], 1, 2): + conf_device[CONF_FIXED_INTERVAL] = 3.0 + update_configuration_flag = True + + if update_configuration_flag: + write_storage_icloud3_configuration_file() + +#-------------------------------------------------------------------- +def _convert_hhmmss_to_minutes(conf_group): + + time_fields = {pname: _hhmmss_to_minutes(pvalue) + for pname, pvalue in conf_group.items() + if (pname in CONF_PARAMETER_TIME_STR + and instr(str(pvalue), ':'))} + if time_fields != {}: + conf_group.update(time_fields) + return True + + return False + +def _hhmmss_to_minutes(hhmmss): + hhmmss_parts = hhmmss.split(':') + return int(hhmmss_parts[0])*60 + int(hhmmss_parts[1]) + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# Add parameters to the configuration file +# +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def _update_tracking_parameters(): + ''' + Fix conf_tracking errors or add any new fields + ''' + + update_config_file_flag = False + + # Add tracking.CONF_SETUP_ICLOUD_SESSION_EARLY + update_config_file_flag = (_add_config_file_parameter(Gb.conf_tracking, CONF_SETUP_ICLOUD_SESSION_EARLY, True) + or update_config_file_flag) + + # Change icloud to iCloud (3.1) + if instr(Gb.conf_tracking[CONF_DATA_SOURCE], 'famshr'): + Gb.conf_tracking[CONF_DATA_SOURCE] = \ + Gb.conf_tracking[CONF_DATA_SOURCE].replace('famshr', ICLOUD).replace(' ', '') + update_config_file_flag = True + + # Change mobapp to MobApp (3.1) + if instr(Gb.conf_tracking[CONF_DATA_SOURCE], 'mobapp'): + Gb.conf_tracking[CONF_DATA_SOURCE] = \ + Gb.conf_tracking[CONF_DATA_SOURCE].replace('mobapp', MOBAPP).replace(' ', '') + update_config_file_flag = True + + # v3.1 Add Apple accounts list + try: + if (CONF_APPLE_ACCOUNTS not in Gb.conf_tracking + or Gb.conf_tracking[CONF_APPLE_ACCOUNTS] == []): + update_config_file_flag = True + + Gb.conf_tracking = _insert_into_conf_dict_parameter( + Gb.conf_tracking, + CONF_APPLE_ACCOUNTS, '', + before=CONF_DEVICES) + + Gb.conf_apple_accounts = Gb.conf_tracking[CONF_APPLE_ACCOUNTS] = [ + { CONF_USERNAME: Gb.conf_tracking[CONF_USERNAME], + CONF_PASSWORD: encode_password(Gb.conf_tracking[CONF_PASSWORD]), + CONF_TOTP_KEY: '', + CONF_LOCATE_ALL: True, + }] + + except Exception as err: + _LOGGER.exception(err) + + return update_config_file_flag + +#-------------------------------------------------------------------- +def _update_device_parameters(): + ''' + Update all device configuration with new/changeditem + ''' + update_config_file_flag = False + cd_idx = -1 + conf_devices = Gb.conf_devices.copy() + for conf_device in conf_devices: + cd_idx += 1 + # b16 - Remove the stat zone friendly name + if CONF_STAT_ZONE_FNAME in conf_device: + conf_device.pop(CONF_STAT_ZONE_FNAME) + update_config_file_flag = True + + # rc8.2 - Add zones for logging enter/exit activity + if CONF_LOG_ZONES not in conf_device: + conf_device[CONF_LOG_ZONES] = ['none'] + update_config_file_flag = True + + # rc8.2 - Add zones for logging enter/exit activity + if CONF_FIXED_INTERVAL not in conf_device: + conf_device[CONF_FIXED_INTERVAL] = 0.0 + update_config_file_flag = True - if conf_device[CONF_FAMSHR_DEVICENAME].startswith(NONE_FNAME) is False: - Gb.conf_famshr_device_cnt += 1 + # rc8.2 - Cleanup a bug introduced in v3.0.rc8 + if 'action_items' in conf_device: + conf_device.pop('action_items') + update_config_file_flag = True - if conf_device[CONF_FMF_EMAIL].startswith(NONE_FNAME) is False: - Gb.conf_fmf_device_cnt += 1 + # v3.1 - Add Apple account parameter + if CONF_APPLE_ACCOUNT not in conf_device: + conf_device = _insert_into_conf_dict_parameter( + conf_device, + CONF_APPLE_ACCOUNT, [], + before=CONF_FAMSHR_DEVICENAME) - if conf_device[CONF_MOBILE_APP_DEVICE].startswith(NONE_FNAME) is False: - Gb.conf_mobapp_device_cnt += 1 + conf_device[CONF_APPLE_ACCOUNT] = Gb.conf_tracking[CONF_USERNAME] + # Gb.conf_devices[cd_idx] = conf_device + update_config_file_flag = True - except Exception as err: - log_exception(err) + # v3.1 - Change Search: to ScanFor: in Mobile App parameter + if conf_device[CONF_MOBILE_APP_DEVICE].startswith('Search:'): + conf_device[CONF_MOBILE_APP_DEVICE] = \ + conf_device[CONF_MOBILE_APP_DEVICE].replace('Search:', 'ScanFor:') + update_config_file_flag = True -#-------------------------------------------------------------------- -def config_file_check_new_ic3_version(): - ''' - Check to see if this is a new iCloud3 version - ''' - update_config_file_flag = False - if Gb.conf_profile[CONF_IC3_VERSION] != f"{VERSION}{VERSION_BETA}": - Gb.conf_profile[CONF_IC3_VERSION] = f"{VERSION}{VERSION_BETA}" - Gb.conf_profile[CONF_VERSION_INSTALL_DATE] = datetime_now() - update_config_file_flag = True + # v3.1 Remove unused parameters + if 'evlog_display_order' in conf_device: + conf_device.pop('evlog_display_order') + conf_device.pop('unique_id') + update_config_file_flag = True + if 'old_devicename' in conf_device: + conf_device.pop('old_devicename') + update_config_file_flag = True - elif Gb.conf_profile[CONF_VERSION_INSTALL_DATE] == DATETIME_ZERO: - Gb.conf_profile[CONF_VERSION_INSTALL_DATE] = datetime_now() - update_config_file_flag = True + if update_config_file_flag: + Gb.conf_devices[cd_idx] = conf_device - if update_config_file_flag: - write_storage_icloud3_configuration_file() + return update_config_file_flag -#-------------------------------------------------------------------- -def config_file_add_new_parameters(): +#------------------------------------------------------------------- +def _update_general_parameters(): ''' - Fix configuration file errors or add any new fields + Fix conf_general and conf_sensors errors or add any new fields ''' update_config_file_flag = False # Fix time format from migration (b1) - if instr(Gb.conf_data['general'][CONF_TIME_FORMAT], '-hour-hour'): - Gb.conf_data['general'][CONF_TIME_FORMAT].replace('-hour-hour', '-hour') + if instr(Gb.conf_data[CF_GENERAL][CONF_TIME_FORMAT], '-hour-hour'): + Gb.conf_data[CF_GENERAL][CONF_TIME_FORMAT].replace('-hour-hour', '-hour') update_config_file_flag = True if Gb.conf_profile[CONF_EVLOG_CARD_DIRECTORY].startswith('www/') is False: @@ -228,38 +702,11 @@ def config_file_add_new_parameters(): Gb.conf_sensors.pop('tracking_by_zones', None) update_config_file_flag = True - # Update parameters for each device - for conf_device in Gb.conf_devices: - # beta 16 - Remove the device's stat zone friendly name since stat zones are ho longet - # associated with a device - if CONF_STAT_ZONE_FNAME in conf_device: - conf_device.pop(CONF_STAT_ZONE_FNAME) - update_config_file_flag = True - - # Add zones for logging enter/exit activity rc8.2 - if CONF_LOG_ZONES not in conf_device: - conf_device[CONF_LOG_ZONES] = ['none'] - update_config_file_flag = True - - # Add zones for logging enter/exit activity rc8.2 - if CONF_FIXED_INTERVAL not in conf_device: - conf_device[CONF_FIXED_INTERVAL] = 0.0 - update_config_file_flag = True - - # Cleanup a bug introduced in v3.0.rc8 - if 'action_items' in conf_device: - conf_device.pop('action_items') - update_config_file_flag = True - # Add sensors.HOME_DISTANCE sensor to conf_sensors if HOME_DISTANCE not in Gb.conf_sensors[CONF_SENSORS_TRACKING_DISTANCE]: Gb.conf_sensors[CONF_SENSORS_TRACKING_DISTANCE].append(HOME_DISTANCE) update_config_file_flag = True - # Add tracking.CONF_SETUP_ICLOUD_SESSION_EARLY - update_config_file_flag = (_add_config_file_parameter(Gb.conf_tracking, CONF_SETUP_ICLOUD_SESSION_EARLY, True) - or update_config_file_flag) - # Add sensors.CONF_EXCLUDED_SENSORS update_config_file_flag = (_add_config_file_parameter(Gb.conf_sensors, CONF_EXCLUDED_SENSORS, ['None']) or update_config_file_flag) @@ -315,12 +762,6 @@ def config_file_add_new_parameters(): if 'device_tracker_state_format' in Gb.conf_general: Gb.conf_general.pop('device_tracker_state_format') - # Change icloud to famshr since fmf no longer works (b16d) - if instr(Gb.conf_tracking[CONF_DATA_SOURCE], ICLOUD): - Gb.conf_tracking[CONF_DATA_SOURCE] = \ - Gb.conf_tracking[CONF_DATA_SOURCE].replace(ICLOUD, FAMSHR) - update_config_file_flag = True - # Remove profile.CONF_HA_CONFIG_IC3_URL that is used by EvLog to open Configuration Wizard (b20) if 'ha_config_ic3_url' in Gb.conf_profile: Gb.conf_profile.pop('ha_config_ic3_url') @@ -382,99 +823,7 @@ def config_file_add_new_parameters(): del conf_device[CONF_IOSAPP_DEVICE] update_config_file_flag = True - if update_config_file_flag: - write_storage_icloud3_configuration_file() - - return True - -#-------------------------------------------------------------------- -def config_file_check_range_values(): - ''' - Check the min and max value of the items that have a range in config_flow to make - sure the actual value in the config file is within the min-max range - ''' - try: - range_errors = {} - update_configuration_flag = False - - range_errors.update({pname: DEFAULT_GENERAL_CONF.get(pname, range[MIN]) - for pname, range in RANGE_GENERAL_CONF.items() - if Gb.conf_general[pname] < range[MIN]}) - range_errors.update({pname: DEFAULT_GENERAL_CONF.get(pname, range[MAX]) - for pname, range in RANGE_GENERAL_CONF.items() - if Gb.conf_general[pname] > range[MAX]}) - update_configuration_flag = (range_errors != {}) - - for pname, pvalue in range_errors.items(): - log_info_msg( f"iCloud3 Config Parameter out of range, resetting to valid value, " - f"Parameter-{pname}, From-{Gb.conf_general[pname]}, To-{pvalue}") - Gb.conf_general[pname] = pvalue - - trav_time_factor = Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] - if Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] in [.25, .33, .5, .66, .75]: - pass - elif Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] < .3: - Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] = .25 - elif Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] < .4: - Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] = .33 - elif Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] < .6: - Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] = .5 - elif Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] < .7: - Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] = .66 - else: - Gb.conf_general[CONF_TRAVEL_TIME_FACTOR] = .75 - if trav_time_factor != Gb.conf_general[CONF_TRAVEL_TIME_FACTOR]: - update_configuration_flag = True - - if update_configuration_flag: - write_storage_icloud3_configuration_file() - - except Exception as err: - log_exception(err) - -#-------------------------------------------------------------------- -def config_file_check_devices(): - ''' - Cycle thru the conf_devices and verify that the settings are valid - ''' - update_configuration_flag = False - - for conf_device in Gb.conf_devices: - if conf_device[CONF_PICTURE] == '': - conf_device[CONF_PICTURE] = 'None' - update_configuration_flag = True - if conf_device[CONF_INZONE_INTERVAL] < 5: - conf_device[CONF_INZONE_INTERVAL] = 5 - update_configuration_flag = True - if conf_device[CONF_LOG_ZONES]== []: - conf_device[CONF_LOG_ZONES] = ['none'] - update_configuration_flag = True - 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], 1, 2): - conf_device[CONF_FIXED_INTERVAL] = 3.0 - update_configuration_flag = True - - if update_configuration_flag: - write_storage_icloud3_configuration_file() - -#-------------------------------------------------------------------- -def _convert_hhmmss_to_minutes(conf_group): - - time_fields = {pname: _hhmmss_to_minutes(pvalue) - for pname, pvalue in conf_group.items() - if (pname in CONF_PARAMETER_TIME_STR - and instr(str(pvalue), ':'))} - if time_fields != {}: - conf_group.update(time_fields) - return True - - return False - -def _hhmmss_to_minutes(hhmmss): - hhmmss_parts = hhmmss.split(':') - return int(hhmmss_parts[0])*60 + int(hhmmss_parts[1]) + return update_config_file_flag #-------------------------------------------------------------------- def _add_config_file_parameter(conf_section, conf_parameter, default_value): @@ -488,7 +837,9 @@ def _add_config_file_parameter(conf_section, conf_parameter, default_value): if conf_parameter not in conf_section: if conf_section is Gb.conf_tracking: - _insert_into_conf_tracking(conf_parameter, default_value) + Gb.conf_tracking = _insert_into_conf_dict_parameter( + Gb.conf_tracking, + conf_parameter, default_value) else: conf_section[conf_parameter] = default_value return True @@ -496,81 +847,65 @@ def _add_config_file_parameter(conf_section, conf_parameter, default_value): return False #-------------------------------------------------------------------- -def _insert_into_conf_tracking(new_item, initial_value): - ''' - Add item to conf_tracking before the devices element - ''' - pos = list(Gb.conf_tracking.keys()).index(CONF_DEVICES) - 1 - items = list(Gb.conf_tracking.items()) - items.insert(pos, (new_item, initial_value )) - Gb.conf_tracking = dict(items) - -#-------------------------------------------------------------------- -def write_storage_icloud3_configuration_file(filename_suffix=''): +def _insert_into_conf_dict_parameter(dict_parameter, + new_item=None, + initial_value=None, before=None, after=None): ''' - Update the config/.storage/.icloud3.configuration file - - Parameters: - filename_suffix: A suffix added to the filename that allows saving multiple copies o - the configuration file - - The Gb.conf_tracking[CONF_PASSWORD] field contains the real password - while iCloud3 is running. This makes it easier logging into PyiCloud - and in config_flow. Save it, then put the encoded password in the file - update the file and then restore the real password + Add items to the configuration file dictionary parameters + + Input: + dict_parameter - Dictionary item to be updated + new_item - Field to be added + initial_value - Initial value + before - Insert it before this argument + after - Insert it after this argument ''' - Gb.conf_tracking[CONF_PASSWORD] = encode_password(Gb.conf_tracking[CONF_PASSWORD]) - Gb.conf_profile[CONF_UPDATE_DATE] = datetime_now() - - Gb.conf_data['tracking']['devices'] = Gb.conf_devices - Gb.conf_data['tracking'] = Gb.conf_tracking - Gb.conf_data['general'] = Gb.conf_general - Gb.conf_data['sensors'] = Gb.conf_sensors + if isinstance(dict_parameter, dict) is False or not new_item: + return dict_parameter + if initial_value is None: initial_value = '' - Gb.conf_file_data['profile'] = Gb.conf_profile - Gb.conf_file_data['data'] = Gb.conf_data + if before is None and after is None: + dict_parameter[new_item] = initial_value + return dict_parameter try: - filename = f"{Gb.icloud3_config_filename}{filename_suffix}" - success = save_json_file(filename, Gb.conf_file_data) - - # with open(filename, 'w', encoding='utf8') as f: - # json.dump(Gb.conf_file_data, f, indent=2, ensure_ascii=False) + if before: + pos = list(dict_parameter.keys()).index(before) + elif after: + pos = list(dict_parameter.keys()).index(after) + 1 + items = list(dict_parameter.items()) + items.insert(pos, (new_item, initial_value)) + return dict(items) except Exception as err: - log_exception(err) - return False - - Gb.conf_tracking[CONF_PASSWORD] = decode_password(Gb.conf_tracking[CONF_PASSWORD]) - - # Update conf_devices devicename index dictionary - if len(Gb.conf_devices) != len(Gb.conf_devices_idx_by_devicename): - set_conf_devices_index_by_devicename() - - return success - + _LOGGER.exception(err) + dict_parameter[new_item] = initial_value + return dict_parameter + + return dict_parameter + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# Password encode/decode functions +# +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def encode_all_passwords(): + Gb.conf_tracking[CONF_PASSWORD] = encode_password(Gb.conf_tracking[CONF_PASSWORD]) + for apple_acct in Gb.conf_apple_accounts: + apple_acct[CONF_PASSWORD] = \ + encode_password(apple_acct[CONF_PASSWORD]) #-------------------------------------------------------------------- -def set_conf_devices_index_by_devicename(): - ''' - Update the device name index position in the conf_devices parameter. - This let you access a devices configuration without searching through - the devices list to get a specific device. - - idx = Gb.conf_devices_idx_by_devicename('gary_iphone') - conf_device = Gb.conf_devices.index(idx) - ''' - Gb.conf_devices_idx_by_devicename = {} - for index, conf_device in enumerate(Gb.conf_devices): - Gb.conf_devices_idx_by_devicename[conf_device[CONF_IC3_DEVICENAME]] = index +def decode_all_passwords(): -def get_conf_device(devicename): - idx = Gb.conf_devices_idx_by_devicename.get(devicename, -1) - if idx == -1: - return None + try: + for apple_acct in Gb.conf_apple_accounts: + Gb.PyiCloud_password_by_username[apple_acct[CONF_USERNAME]] = \ + decode_password(apple_acct[CONF_PASSWORD]) - return Gb.conf_devices[idx] + except Exception as err: + _LOGGER.exception(err) #-------------------------------------------------------------------- def encode_password(password): @@ -581,17 +916,20 @@ def encode_password(password): Decoded password ''' try: - if (password == '' or Gb.encode_password_flag is False): + if (password == '' + or Gb.encode_password_flag is False + or password.startswith('««') + or password.endswith('»»')): return password return f"««{base64_encode(password)}»»" except Exception as err: - log_exception(err) + _LOGGER.exception(err) password = password.replace('«', '').replace('»', '') return password -def base64_encode(string): +def base64_encode(password): """ Encode the string via base64 encoder """ @@ -599,16 +937,15 @@ def base64_encode(string): # return encoded.rstrip("=") try: - string_bytes = string.encode('ascii') - base64_bytes = base64.b64encode(string_bytes) + password_bytes = password.encode('ascii') + base64_bytes = base64.b64encode(password_bytes) return base64_bytes.decode('ascii') except Exception as err: - log_exception(err) + _LOGGER.exception(err) password = password.replace('«', '').replace('»', '') return password - #-------------------------------------------------------------------- def decode_password(password): ''' @@ -622,10 +959,8 @@ def decode_password(password): # and it should be encoded, save the configuration file which will encode it if (Gb.encode_password_flag and password != '' - and (password.startswith('««') is False - or password.endswith('»»') is False)): + and (password.startswith('««') is False or password.endswith('»»') is False)): password = password.replace('«', '').replace('»', '') - Gb.conf_tracking[CONF_PASSWORD] = password write_storage_icloud3_configuration_file() # Decode password if it is encoded and has the '««password»»' format @@ -634,7 +969,7 @@ def decode_password(password): return base64_decode(password) except Exception as err: - log_exception(err) + _LOGGER.exception(err) password = password.replace('«', '').replace('»', '') return password @@ -651,72 +986,6 @@ def base64_decode(string): string_bytes = base64.b64decode(base64_bytes) return string_bytes.decode('ascii') -#------------------------------------------------------------------------------------------- -def load_icloud3_ha_config_yaml(ha_config_yaml): - - - Gb.ha_config_yaml_icloud3_platform = {} - if ha_config_yaml == '': - return - - ha_config_yaml_devtrkr_platforms = ordereddict_to_dict(ha_config_yaml)['device_tracker'] - - ic3_ha_config_yaml = {} - for ha_config_yaml_platform in ha_config_yaml_devtrkr_platforms: - if ha_config_yaml_platform['platform'] == 'icloud3': - ic3_ha_config_yaml = ha_config_yaml_platform.copy() - break - - Gb.ha_config_yaml_icloud3_platform = ordereddict_to_dict(ic3_ha_config_yaml) - -#-------------------------------------------------------------------- -def build_initial_config_file_structure(): - ''' - Create the initial data structure of the ic3 config file - - |---profile - |---data - |---tracking - |---devices - |---general - |---parameters - |---sensors - - ''' - - Gb.conf_profile = DEFAULT_PROFILE_CONF.copy() - Gb.conf_tracking = DEFAULT_TRACKING_CONF.copy() - Gb.conf_devices = [] - Gb.conf_general = DEFAULT_GENERAL_CONF.copy() - Gb.conf_sensors = DEFAULT_SENSORS_CONF.copy() - Gb.conf_file_data = CF_DEFAULT_IC3_CONF_FILE.copy() - - Gb.conf_data['tracking'] = Gb.conf_tracking - Gb.conf_data['tracking']['devices'] = Gb.conf_devices - Gb.conf_data['general'] = Gb.conf_general - Gb.conf_data['sensors'] = Gb.conf_sensors - - Gb.conf_file_data['profile'] = Gb.conf_profile - Gb.conf_file_data['data'] = Gb.conf_data - - - # Verify general parameters and make any necessary corrections - try: - if Gb.country_code in APPLE_SPECIAL_ICLOUD_SERVER_COUNTRY_CODE: - Gb.conf_tracking[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX] = Gb.country_code - - if Gb.config and Gb.config.units['name'] != 'Imperial': - Gb.conf_general[CONF_UNIT_OF_MEASUREMENT] = 'km' - Gb.conf_general[CONF_TIME_FORMAT] = '24-hour' - - elif Gb.ha_use_metric: - Gb.conf_general[CONF_UNIT_OF_MEASUREMENT] = 'km' - Gb.conf_general[CONF_TIME_FORMAT] = '24-hour' - - except: - pass - - #-------------------------------------------------------------------- def count_lines_of_code(start_directory, total_file_lines=0, total_code_lines=0, begin_start=None): @@ -729,7 +998,7 @@ def count_lines_of_code(start_directory, total_file_lines=0, total_code_lines=0, for file_name in os.listdir(start_directory): file_name = os.path.join(start_directory, file_name) - if os.path.isfile(file_name): + if file_io.file_exists(file_name): if file_name.endswith('.py') or file_name.endswith('.js'): with open(file_name, 'r') as f: lines = f.readlines() diff --git a/custom_components/icloud3/support/determine_interval.py b/custom_components/icloud3/support/determine_interval.py index bdd4b28..30f0900 100644 --- a/custom_components/icloud3/support/determine_interval.py +++ b/custom_components/icloud3/support/determine_interval.py @@ -21,16 +21,15 @@ from ..global_variables import GlobalVariables as Gb from ..const import (HOME, NOT_HOME, AWAY, NOT_SET, NOT_HOME_ZONES, HIGH_INTEGER, CRLF, CHECK_MARK, CIRCLE_X, LTE, LT, PLUS_MINUS, RED_X, CIRCLE_STAR2, CRLF_DOT, - STATIONARY, STATIONARY_FNAME, STATZONE, WATCH, MOBAPP_FNAME, YELLOW_ALERT, + RARROW, + STATIONARY, STATIONARY_FNAME, STATZONE, WATCH, MOBAPP, YELLOW_ALERT, + ICLOUD, AWAY_FROM, FAR_AWAY, TOWARDS, PAUSED, INZONE, INZONE_STATZONE, INZONE_HOME, - ERROR, UNKNOWN, - VALID_DATA, + ERROR, UNKNOWN, VALID_DATA, NEAR_DEVICE_DISTANCE, WAZE, - NEAR_DEVICE_DISTANCE, WAZE_USED, WAZE_NOT_USED, WAZE_PAUSED, WAZE_OUT_OF_RANGE, WAZE_NO_DATA, EVLOG_TIME_RECD, EVLOG_ALERT, - RARROW, NEAR_DEVICE_USEABLE_SYM, - EXIT_ZONE, + NEAR_DEVICE_USEABLE_SYM, EXIT_ZONE, ZONE, ZONE_INFO, INTERVAL, DISTANCE, ZONE_DISTANCE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD, @@ -41,19 +40,17 @@ LAST_LOCATED, ) -# from ..support import mobapp_interface -# from ..support import stationary_zone as statzone 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, post_internal_error, post_monitor_msg, log_debug_msg, log_rawdata, - log_info_msg, log_info_msg_HA, log_exception, _trace, _traceha, ) -from ..helpers.time_util import (secs_to_time, format_timer, format_time_age, - secs_since, time_to_12hrtime, secs_to_datetime, secs_to, format_age, + log_info_msg, log_info_msg_HA, log_exception, _evlog, _log, ) +from ..helpers.time_util import (secs_to_time, format_timer, format_time_age, format_secs_since, + secs_since, mins_since, time_to_12hrtime, secs_to_datetime, secs_to, format_age, datetime_now, time_now, time_now_secs, secs_to_hhmm, secs_to_hhmm, ) from ..helpers.dist_util import (km_to_mi, km_to_um, format_dist_km, format_dist_m, - km_to_um, m_to_um_ft, ) + km_to_um, m_to_um, m_to_um_ft, ) import homeassistant.util.dt as dt_util @@ -86,7 +83,7 @@ # RETRY_INTERVAL_RANGE_1 = 30s*4=2m, 4*1.5m=6m=8m, 4*15m=1h=1h8m, 4*30m=2h=3h8m, 4*1h=4h=6.5h # RETRY_INTERVAL_RANGE_1 = {0:.25, 4:1, 8:5, 12:30, 16:60, 20:60} # RETRY_INTERVAL_RANGE_1 = 15s*5=1.25m, 5*1m=5m=6m, 5*5m=25m=30m, 5*30m=2.5h=3, 4*1h=4h=6h25h -RETRY_INTERVAL_RANGE_1 = {0:.25, 4:.5, 8:1, 12:5, 16:15, 20:30, 22:60} +RETRY_INTERVAL_RANGE_1 = {0:.25, 4:.5, 8:1, 12:5, 16:15, 20:30, 24:60} MOBAPP_REQUEST_LOC_CNT = 2.1 RETRY_INTERVAL_RANGE_2 = {0:.5, 4:2, 8:30, 12:60, 16:60} # RETRY_INTERVAL_RANGE_2 = {0:.5, 4:2, 8:30, 12:60, 14:120, 16:180, 18:240, 20:240} @@ -99,8 +96,6 @@ def determine_interval(Device, FromZone): The last location: Device.sensors[LATITUDE]/[LONGITUDE] ''' - devicename = Device.devicename - battery10_flag = (0 > Device.dev_data_battery_level >= 10) battery5_flag = (0 > Device.dev_data_battery_level >= 5) @@ -111,6 +106,7 @@ def determine_interval(Device, FromZone): isin_zone_home = (Device.loc_data_zone == HOME) wasin_zone_home = (Device.sensor_zone == HOME) + devicename = Device.devicename Device.FromZone_BeingUpdated = FromZone if FromZone.from_zone == Device.loc_data_zone: @@ -272,7 +268,7 @@ def determine_interval(Device, FromZone): interval_method = '3.NeedInfo' interval_secs = 150 - elif dist_from_zone_km < 2 and dir_of_travel == AWAY_FROM: + elif dist_from_zone_km < 2 and dir_of_travel == AWAY_FROM and waze_interval_secs > 0: interval_method = '3.<2km+AwayFm' interval_secs = Device.old_loc_threshold_secs #1.5 mi & going Away @@ -454,19 +450,19 @@ def determine_interval(Device, FromZone): if (Device.state_change_flag is False and Device.is_gps_poor and dist_moved_km < 1): - dist_from_zone_km = FromZone.zone_dist + dist_from_zone_km = FromZone.zone_dist_km dist_from_zone_m = FromZone.zone_dist_m - waze_dist_from_zone_km = FromZone.waze_dist - calc_dist_from_zone_km = FromZone.calc_dist + waze_dist_from_zone_km = FromZone.waze_dist_km + calc_dist_from_zone_km = FromZone.calc_dist_km waze_time_from_zone = FromZone.waze_time else: #save for next poll if poor gps - FromZone.zone_dist = dist_from_zone_km - FromZone.zone_dist_m = dist_from_zone_m - FromZone.waze_dist = waze_dist_from_zone_km - FromZone.waze_time = waze_time_from_zone - FromZone.calc_dist = calc_dist_from_zone_km + FromZone.zone_dist_km = dist_from_zone_km + FromZone.zone_dist_m = dist_from_zone_m + FromZone.waze_dist_km = waze_dist_from_zone_km + FromZone.waze_time = waze_time_from_zone + FromZone.calc_dist_km = calc_dist_from_zone_km waze_time_msg = format_timer(waze_time_from_zone * 60) @@ -496,17 +492,10 @@ def determine_interval(Device, FromZone): sensors[TRAVEL_TIME] = format_timer(waze_time_from_zone * 60) sensors[TRAVEL_TIME_MIN] = f"{waze_time_from_zone:.0f} min" sensors[TRAVEL_TIME_HHMM] = secs_to_hhmm(waze_time_from_zone * 60) - - if (Device.isin_zone - and Device.loc_data_zone == FromZone.from_zone - and is_statzone(Device.loc_data_zone) is False): - sensors[ARRIVAL_TIME] =f"@{secs_to_hhmm(Device.zone_change_secs)}" - else: - sensors[ARRIVAL_TIME] = secs_to_hhmm(waze_time_from_zone * 60 + time_now_secs()) - + sensors[ARRIVAL_TIME] = _sensor_arrival_time(Device, FromZone) sensors[DIR_OF_TRAVEL] = dir_of_travel - sensors[DISTANCE] = km_to_mi(dist_from_zone_km) sensors[MAX_DISTANCE] = km_to_mi(FromZone.max_dist_km) + sensors[DISTANCE] = km_to_mi(dist_from_zone_km) sensors[ZONE_DISTANCE] = km_to_mi(dist_from_zone_km) sensors[ZONE_DISTANCE_M] = dist_from_zone_m sensors[ZONE_DISTANCE_M_EDGE] = abs(dist_from_zone_m - FromZone.from_zone_radius_m) @@ -514,34 +503,61 @@ def determine_interval(Device, FromZone): sensors[WAZE_METHOD] = Gb.Waze.waze_status_fname sensors[CALC_DISTANCE] = km_to_mi(calc_dist_from_zone_km) sensors[MOVED_DISTANCE] = km_to_mi(dist_moved_km) - - if Device.isin_zone: - sensors[ZONE_INFO] = f"@{Device.loc_data_zone_fname}" - if Device.wasnotin_zone: - log_info_msg_HA(f"{Device.fname} Entered Zone: {zone_dname(Device.loc_data_zone)}") - else: - sensors[ZONE_INFO] = km_to_um(dist_from_zone_km) - if Device.wasin_zone: - log_info_msg_HA(f"{Device.fname} Exited Zone: {zone_dname(Device.sensors[ZONE])}") + sensors[ZONE_INFO] = _sensor_zone_info(Device, FromZone) #save for event log if type(waze_time_msg) != str: waze_time_msg = '' FromZone.last_travel_time = waze_time_msg FromZone.last_distance_km = dist_from_zone_km FromZone.last_distance_str = km_to_um(dist_from_zone_km) - Device.loc_time_updates_famshr = [Device.loc_data_time] - - if Device.is_location_gps_good: - Device.old_loc_cnt = 0 - FromZone.sensors.update(sensors) + Device.loc_time_updates_icloud = [Device.loc_data_time] + if Device.is_location_gps_good: Device.old_loc_cnt = 0 Device.display_info_msg(Device.format_info_msg, new_base_msg=True) + post_results_message_to_event_log(Device, FromZone) post_zone_time_dist_event_msg(Device, FromZone) return sensors +#...............................................................................abs +def _sensor_arrival_time(Device, FromZone): + + if (Device.isin_zone + and is_statzone(Device.loc_data_zone) is False + and Device.loc_data_zone == FromZone.from_zone): + days = secs_since(Device.zone_change_secs)/86400 + day_adj = f"-{days:.0f}d" if days >= 1 else '' + return f"@{secs_to_hhmm(Device.zone_change_secs)}{day_adj}" + + if Gb.waze_status != WAZE_USED: + return '' + + # Display the last arrival time since device is under min waze dist + if Gb.Waze.is_status_USED is False: + if FromZone.zone_dist_km < Gb.Waze.waze_min_distance: + return f"~{Device.sensors[ARRIVAL_TIME].replace('~', '')}" + + # Display arrival time + if FromZone.waze_time > 0: + return secs_to_hhmm(FromZone.waze_time * 60 + time_now_secs()) + + return '' + +#...............................................................................abs +def _sensor_zone_info(Device, FromZone): + if Device.isin_zone: + if Device.wasnotin_zone: + log_info_msg_HA(f"{Device.fname} Entered Zone: {zone_dname(Device.loc_data_zone)}") + return f"@{Device.loc_data_zone_fname}" + + if Device.wasin_zone: + log_info_msg_HA(f"{Device.fname} Exited Zone: {zone_dname(Device.sensors[ZONE])}") + return km_to_um(FromZone.zone_dist_km) + + return '' + #-------------------------------------------------------------------------------- def post_results_message_to_event_log(Device, FromZone): ''' @@ -554,8 +570,9 @@ def post_results_message_to_event_log(Device, FromZone): else: event_msg = f"Results: From-{FromZone.from_zone_dname} > " - if (Device.isin_zone and FromZone.from_zone != Device.loc_data_zone - or Device.isnotin_zone): + if (FromZone.sensors[ARRIVAL_TIME] + and (Device.isnotin_zone + or (Device.isin_zone and FromZone.from_zone != Device.loc_data_zone))): event_msg += f"Arrive-{FromZone.sensors[ARRIVAL_TIME]}, " event_msg += f"NextUpdate-{FromZone.next_update_time}, " @@ -579,12 +596,14 @@ def post_results_message_to_event_log(Device, FromZone): event_msg += f"Method-{FromZone.interval_method}, " if Gb.Waze.waze_status == WAZE_OUT_OF_RANGE: - event_msg += f"WazeMsg-{Gb.Waze.range_msg(FromZone.zone_dist)}, " + event_msg += f"WazeMsg-{Gb.Waze.range_msg(FromZone.zone_dist_km)}, " if (Device.mobapp_monitor_flag and secs_since(Device.mobapp_data_secs) > 3600): event_msg += f"MobAppLocated-{format_age(Device.mobapp_data_secs)}, " + # event_msg += f"AppleAcct-{Device.PyiCloud.account_name}, " + post_event(Device, event_msg[:-2]) log_msg = ( f"RESULTS: From-{FromZone.from_zone_dname} > " @@ -592,7 +611,8 @@ def post_results_message_to_event_log(Device, FromZone): f"iC3-{Device.loc_data_zone}, " f"Intrvl-{FromZone.interval_str}, " f"TravTime-{FromZone.last_travel_time}, " - f"Dist-{format_dist_km(FromZone.zone_dist)}, " + f"Dist-{format_dist_km(FromZone.zone_dist_km)}, " + f"Arrival-{Device.sensors[ARRIVAL_TIME]}, " f"MaxDist-{format_dist_km(FromZone.max_dist_km)}, " f"Moved-{format_dist_km(Device.statzone_dist_moved_km)}, " f"Calc/WazeDist={FromZone.sensors[CALC_DISTANCE]}/{FromZone.sensors[WAZE_DISTANCE]}, " @@ -632,11 +652,11 @@ def post_zone_time_dist_event_msg(Device, FromZone): distance = FromZone.zone_distance_str post_event(Device, - f"{EVLOG_TIME_RECD}{mobapp_state},{ic3_zone},{interval_str},{travel_time},{distance}") + f"{EVLOG_TIME_RECD}{mobapp_state},{ic3_zone},{interval_str},{travel_time},{distance}") #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # -# iCloud FmF or FamShr authentication returned an error or no location +# iCloud FmF or iCloud authentication returned an error or no location # data is available. Update counter and device attributes and set # retry intervals based on current retry count. # @@ -678,7 +698,7 @@ def determine_interval_monitored_device_offline(Device): #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # -# iCloud FmF or FamShr authentication returned an error or no location +# iCloud iCloud authentication returned an error or no location # data is available. Update counter and device attributes and set # retry intervals based on current retry count. # @@ -711,11 +731,13 @@ def determine_interval_after_error(Device, counter=OLD_LOCATION_CNT): if (secs_since(Device.loc_data_secs) > 7200 or Device.no_location_data or Device.is_offline): - Device.reset_tracking_fields(interval_secs=5) + Device.reset_tracking_fields(interval_secs=5, max_error_cnt_reached=True) + Device.max_error_cycle_cnt += 1 interval_secs, error_cnt, max_error_cnt = get_error_retry_interval(Device, counter) post_event(Device, f"Location > Old > Tracking Reinitialized, " + f"Retry Cycle #{Device.max_error_cycle_cnt}, " f"LastLocated-{format_time_age(Device.loc_data_secs)}, " f"RetryAt-{Device.FromZone_Home.next_update_time}") return @@ -723,14 +745,27 @@ def determine_interval_after_error(Device, counter=OLD_LOCATION_CNT): if (Device.is_offline and Device.offline_secs == 0): Device.offline_secs = Gb.this_update_secs + # Adjust the interval based on the number of times the error retry table + # has been cycled thru and started at the beginning again. Pause tracking + # if cycled more than 8 times or 4 times and last location is over 2-days ago + if Device.max_error_cycle_cnt > 8: + Device.pause_tracking + elif (Device.max_error_cycle_cnt > 4 + and mins_since(Device.last_loc_secs) > 2880): + Device.pause_tracking + elif Device.max_error_cycle_cnt > 2: + interval_secs = interval_secs * int(Device.max_error_cycle_cnt/2) + if interval_secs > Gb.max_interval_secs: + interval_secs = Gb.max_interval_secs + # Often, iCloud does not actually locate the device but just returns the last # location it has. A second call is needed after a 5-sec delay. This also # happens after a reauthentication. If so, do not display an error on the # first retry. last_interval_secs = Device.interval_secs interval_str = format_timer(interval_secs) - next_update_secs = Gb.this_update_secs + interval_secs - next_update_time = secs_to_time(next_update_secs) + next_update_secs = Gb.this_update_secs + interval_secs + next_update_time = secs_to_time(next_update_secs) Device.update_sensors_error_msg = Device.update_sensors_error_msg or Device.old_loc_msg update_all_device_fm_zone_sensors_interval(Device, interval_secs) @@ -863,7 +898,7 @@ def _update_next_update_fields_and_sensors(Device_or_FromZone, interval_secs): Device = Device_or_FromZone if Device_or_FromZone in Gb.Devices else Device_or_FromZone.Device data_source_ICLOUD = Device.is_data_source_ICLOUD else: - data_source_ICLOUD = Gb.primary_data_source_ICLOUD + data_source_ICLOUD = Gb.use_data_source_ICLOUD sensors[INTERVAL] = format_timer(interval_secs) sensors[NEXT_UPDATE_DATETIME] = secs_to_datetime(next_update_secs) @@ -910,8 +945,8 @@ def determine_TrackFrom_zone(Device): if FromZone.next_update_secs <= Device.FromZone_TrackFrom.next_update_secs: # If within tfz tracking dist, display this tfz results # Then see if another trackFmZone is closer to the device - if (FromZone.zone_dist <= Gb.tfz_tracking_max_distance - and FromZone.zone_dist < Device.FromZone_TrackFrom.zone_dist): + if (FromZone.zone_dist_km <= Gb.tfz_tracking_max_distance + and FromZone.zone_dist_km < Device.FromZone_TrackFrom.zone_dist_km): Device.FromZone_TrackFrom = FromZone # If this is the last zone exited and going away from it, use Home instead @@ -1156,6 +1191,68 @@ def _get_interval_for_error_retry_cnt(Device, counter=OLD_LOCATION_CNT, pause_co return interval_secs +# #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# MISCELLANEOUS SUPPORT FUNCTIONS +# +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def device_will_update_in_15secs(Device=None, update_in_secs=None, only_icloud_devices=False): + ''' + Get the time (secs) until the next update for any device. This is used to determine + when icloud data should be prefetched before it is needed. + + Parameter: + = update_in_secs - Secs to be used in update test (default=15) + = only_icloud_devices - True=Only check iCloud devices, False=All devices + Return: + = Device - The device that will be updated within the update_in_secs time + = None - No device will be updated in the update_in_secs time + ''' + + _Devices_not_to_check = [_Device + for _Device in Gb.Devices_by_devicename_tracked.values() + if (_Device is Device + or _Device.PyiCloud is None + or (only_icloud_devices and _Device.family_share_device is False) + or _Device.is_offline + or _Device.is_data_source_ICLOUD is False + or _Device.is_tracking_paused + or secs_since(_Device.loc_data_secs) > Gb.max_interval_secs + or secs_since(_Device.PyiCloud.last_refresh_secs) < 10)] + + _Devices_to_check = [_Device for _Device in Gb.Devices_by_devicename_tracked.values() + if _Device not in _Devices_not_to_check] + + update_in_secs = 15 if update_in_secs is None else update_in_secs + for _Device in _Devices_to_check: #Gb.Devices_by_devicename_tracked.values(): + + if _Device.icloud_initial_locate_done is False: + return _Device + + secs_to_next_update = secs_to(_Device.next_update_secs) + + if (secs_to_next_update < -15 + or secs_to_next_update > update_in_secs): + continue + + # Corrected inzone_interval to next_update_secs (v3.1) + # If going towards a TrackFmZone and the next update is in 15-secs or less and distance < 1km + # and current location is older than 15-secs, prefetch data now + # Changed to is_approaching_tracked_zone and added error_cnt check (rc9) + if (_Device.is_approaching_tracked_zone + and _Device.old_loc_cnt <= 4): + _Device.old_loc_threshold_secs = 15 + return _Device + + if _Device.is_location_gps_good or _Device.old_loc_cnt > 6: + continue + + # Updating the device in the next 10-secs + _Device.display_info_msg(f"Requesting iCloud Location, Next Update in {format_timer(secs_to_next_update)} secs") + return _Device + + return None + #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # # If a NearDevice is available, Make sure that it is not a circular reference by checking @@ -1203,10 +1300,10 @@ def copy_near_device_results(Device, FromZone): 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 + FromZone.zone_dist_km = NearFromZone.zone_dist_km + FromZone.waze_dist_km = NearFromZone.waze_dist_km FromZone.waze_time = NearFromZone.waze_time - FromZone.calc_dist = NearFromZone.calc_dist + FromZone.calc_dist_km = NearFromZone.calc_dist_km FromZone.interval_method = NearFromZone.interval_method FromZone.last_update_secs = NearFromZone.last_update_secs FromZone.last_update_time = NearFromZone.last_update_time @@ -1250,32 +1347,126 @@ def update_near_device_info(Device): Return: The closest device {devicename: [dist_m, gps_accuracy_factor, display_text]} + dist_to_other_devices is updated in Device.update_distance_to_other_devices ''' if (len(Gb.Devices) == 1 or len(Device.dist_to_other_devices) == 0 or Gb.distance_between_device_flag is False): return - closest_device_distance = HIGH_INTEGER - Device.dist_apart_msg = '' Device.NearDevice = None Device.near_device_distance = 0.0 Device.near_device_checked_secs = time_now_secs() device_time = secs_to_hhmm(Device.dist_to_other_devices_secs) - for devicename, dist_apart_data in Device.dist_to_other_devices.items(): + Device.dist_apart_msg = _build_dist_apart_msg(Device, Device.dist_to_other_devices) + Device.dist_apart_msg2 = _build_dist_apart_msg(Device ,Device.dist_to_other_devices2) + + monitor_msg = ( f"Nearby Devices " + f"({LT}{m_to_um_ft(NEAR_DEVICE_DISTANCE, as_integer=True)}) > " + f"@{device_time}, " + f"{Device.dist_apart_msg.replace(f' ({device_time})', '')}") + post_monitor_msg(Device.devicename, monitor_msg) + + # post_nearby_devices_msg() + + return + +#-------------------------------------------------------------------------------- +def set_dist_to_devices(post_event_msg=False): + + for devicename_from, Device_from in Gb.Devices_by_devicename.items(): + dist_to_devices_data = [] + + try: + for devicename_to, Device_to in Gb.Devices_by_devicename.items(): + if (devicename_from == devicename_to + or Device_from.loc_data_secs == 0 + or Device_to.loc_data_secs == 0): + continue + + dist_to_m = Device_from.distance_m(Device_to.loc_data_latitude, Device_to.loc_data_longitude) + if dist_to_m == 0: + continue + loc_time_secs = min(Device_from.loc_data_secs, Device_to.loc_data_secs) + + dist_to_device_data = [dist_to_m, Device_to, loc_time_secs] + dist_to_devices_data.append(dist_to_device_data) + + except Exception as err: + # log_exception(err) + pass + + try: + # dist_to_devices_data.sort() + Device_from.dist_to_devices_data = dist_to_devices_data + Device_from.dist_to_devices_secs = Device_from.loc_data_secs + + if post_event_msg and dist_to_devices_data != []: + event_msg =(f"DistTo Devices > " + f"{format_dist_to_devices_msg(Device_from)}") + + post_event(devicename_from, event_msg) + + except Exception as err: + # log_exception(err) + pass + +#............................................................................... +def format_dist_to_devices_msg(Device, max_dist_to_m=HIGH_INTEGER, time=False, age=True): + + dist_msg = '' + for dist_to_device_data in Device.dist_to_devices_data: + dist_to_m, Device_to, loc_time_secs = dist_to_device_data + if dist_to_m > max_dist_to_m: + continue + + dist_msg += f", {Device_to.fname}--{m_to_um(dist_to_m)}" + + if abs(Device.dist_to_devices_secs - loc_time_secs) >= 5: + if time and age: + time_msg = format_time_age(loc_time_secs) + elif time: + time_msg = secs_to_hhmm(loc_time_secs) + elif age: + time_msg = format_secs_since(loc_time_secs) + else: + time_msg = None + if time_msg: + dist_msg += f" ({time_msg})" + + return dist_msg[1:] + +#-------------------------------------------------------------------------------- +def _build_dist_apart_msg(Device, dist_to_other_devices): + + dist_apart_msg = '' + closest_device_distance = HIGH_INTEGER + + for devicename, dist_apart_data in dist_to_other_devices.items(): _Device = Gb.Devices_by_devicename[devicename] if _Device is Device: continue dist_apart_m, min_gps_accuracy, loc_data_secs, display_text = dist_apart_data - # if one device is a watch and the devices are paired and the watch is close to the - # device, the watch may use the devices location info. Don't use the watch as nearby + # if one device is a watch and the device have the same person_id (they are paired) + # and the watch is close to the device, the watch may use the devices + # location info. Don't use the watch as nearby # ⦾×⌘⛒♺⚯⚠︎⚮∞ - if ((Device.device_type == WATCH or _Device.device_type == WATCH) - and Device.paired_with_id == _Device.paired_with_id - and dist_apart_m < NEAR_DEVICE_DISTANCE): + # if ((Device.device_type == WATCH or _Device.device_type == WATCH) + # and Device.PyiCloud.username + # and _Device.PyiCloud.username): + # _log(f"{Device.device_type=} {_Device.device_type=}" + # f"{Device.icloud_device_id in Gb.owner_device_ids_by_username[Device.PyiCloud.username]} {_Device.icloud_device_id in Gb.owner_device_ids_by_username[_Device.PyiCloud.username]}") + + if (Device.device_type == WATCH or _Device.device_type == WATCH): + # and Device.PyiCloud.username + # and _Device.PyiCloud.username + # and Device.icloud_device_id in Gb.owner_device_ids_by_username[Device.PyiCloud.username] + # and _Device.icloud_device_id in Gb.owner_device_ids_by_username[_Device.PyiCloud.username] + # and dist_apart_m < NEAR_DEVICE_DISTANCE): useable_symbol = '×' + elif dist_apart_m > NEAR_DEVICE_DISTANCE: useable_symbol = '×' elif min_gps_accuracy > NEAR_DEVICE_DISTANCE: @@ -1291,28 +1482,23 @@ def update_near_device_info(Device): else: useable_symbol = NEAR_DEVICE_USEABLE_SYM - Device.dist_apart_msg += f"{useable_symbol}{_Device.fname_devtype}-{display_text}, " + #dist_apart_msg += f"{useable_symbol}{_Device.fname_devtype}-{display_text}, " + dist_apart_msg += f"{useable_symbol}{_Device.fname}-{display_text}, " # The nearby devices can not point to each other and other criteria - if (Device.is_tracked + if (dist_apart_m < closest_device_distance + and Device.is_tracked and _Device.is_tracked and useable_symbol == NEAR_DEVICE_USEABLE_SYM and _Device.FromZone_Home.interval_secs > 0 and _Device.old_loc_cnt == 0 and _Device.is_online): - if dist_apart_m < closest_device_distance: - closest_device_distance = dist_apart_m - Device.NearDevice = _Device - Device.near_device_distance = dist_apart_m + closest_device_distance = dist_apart_m + Device.NearDevice = _Device + Device.near_device_distance = dist_apart_m - monitor_msg = ( f"Nearby Devices " - f"({LT}{m_to_um_ft(NEAR_DEVICE_DISTANCE, as_integer=True)}) > " - f"@{device_time}, " - f"{Device.dist_apart_msg.replace(f' ({device_time})', '')}") - post_monitor_msg(Device.devicename, monitor_msg) - - return + return dist_apart_msg #-------------------------------------------------------------------------------- def _check_near_device_circular_loop(_Device, Device): diff --git a/custom_components/icloud3/support/event_log.py b/custom_components/icloud3/support/event_log.py index 621be30..494db45 100644 --- a/custom_components/icloud3/support/event_log.py +++ b/custom_components/icloud3/support/event_log.py @@ -13,7 +13,7 @@ from ..global_variables import GlobalVariables as Gb from ..const import (HOME, HOME_FNAME, TOWARDS, HHMMSS_ZERO, HIGH_INTEGER, NONE, MOBAPP, - RED_X, YELLOW_ALERT, + RED_X, YELLOW_ALERT, CIRCLE_LETTERS_DARK, CIRCLE_LETTERS_LITE, NL, NL_DOT, LDOT2, CRLF, CRLF_DOT, CRLF_CHK, RARROW, DOT, LT, GT, DASH_50, NBSP, NBSP2, NBSP3, NBSP4, NBSP5, NBSP6, CLOCK_FACE, @@ -27,7 +27,7 @@ ) 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, +from ..helpers.messaging import (SP, log_exception, log_info_msg, log_warning_msg, _log, _evlog, 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, ) @@ -59,6 +59,7 @@ MONITORED_DEVICE_EVENT_FILTERS = [ 'iCloud Acct Auth', + 'Apple Acct Auth', 'Nearby Devices', 'iOSApp Location', 'MobApp Location', @@ -89,11 +90,13 @@ def initialize(self): self.log_rawdata_flag = False self.last_refresh_secs = 0 self.last_refresh_devicename = '' + self.dist_to_devices_recd_found_flag = False # Display only the last DistTo Devices > stmt + self.apple_acct_auth_cnts_by_owner = {} # Display only the last 4 Apple Acct Auth statements # An alert message is displayed in a green bar at the top of the EvLog screen # post_evlog_greenbar_msg("msg") = Display the message # clear_evlog_greenbar_msg() = Clear the alert msg and remove the green bar - self.greenbar_alert_msg = '' # Message to display in green bar at the top of the Evlog + self.greenbar_alert_msg = '' # Message to display in green bar at the top of the Evlog self.user_message = '' # Display a message in the name0 button self.user_message_alert_flag = False # Do not clear the message if this is True @@ -170,6 +173,7 @@ def setup_event_log_trackable_device_info(self): self.fnames_by_devicename.update({devicename: self.format_evlog_device_fname(Device) for devicename, Device in Gb.Devices_by_devicename_tracked.items() if devicename != ''}) + self.fnames_by_devicename.update({devicename: self.format_evlog_device_fname(Device) for devicename, Device in Gb.Devices_by_devicename_monitored.items() if devicename != ''}) @@ -226,16 +230,6 @@ def format_evlog_device_fname(self, Device): tracked = '' if Device.is_tracked else f" {circle_letter('m')}" return f"{Device.evlog_fname_alert_char}{Device.fname}{tracked}" -# #------------------------------------------------------ -# def update_evlog_device_fname(self, Device, new_fname=None): -# if new_fname: -# self.evlog_attrs["fnames"][Device.devicename] = new_fname -# else: -# self.evlog_attrs["fnames"][Device.devicename] = self.format_evlog_device_fname(Device) -# self.evlog_attrs["run_mode"] = "UpdateFnames" -# self.update_evlog_sensor() -# self.display_user_message("") - #------------------------------------------------------ def post_event(self, devicename_or_Device, event_text='+'): ''' @@ -270,7 +264,7 @@ def post_event(self, devicename_or_Device, event_text='+'): Device = None # If monitored device and the event msg is a status msg for other devices, - # do not display it on a monitoed device screen + # do not display it on a monitored device screen if Device and Device.is_monitored: start_pos = 2 if event_text.startswith('^') else 0 for filter_text in MONITORED_DEVICE_EVENT_FILTERS: @@ -365,7 +359,9 @@ def post_event(self, devicename_or_Device, event_text='+'): self._add_recd_to_event_recds(event_recd) if (self.startup_event_save_recd_flag - or event_recd[ELR_TEXT].startswith(EVLOG_ALERT)): + or (event_text.startswith(EVLOG_ALERT) + and instr(event_text, 'Location > Old') is False) + or instr(event_text, 'Acct Auth')): self._save_startup_log_recd(Device, event_recd) except Exception as err: @@ -682,7 +678,7 @@ def _filtered_evlog_recds(self, devicename='', max_recds=HIGH_INTEGER, selected_ refresh_msg = ( f"{EVLOG_HIGHLIGHT}Tap `Refresh` or select a device " f"to display all of the events") - refresh_recd = ['',refresh_msg] + refresh_recd = ['🔄',refresh_msg] time_text_recds.insert(0, refresh_recd) if self.greenbar_alert_msg != '': @@ -710,6 +706,9 @@ def _extract_filtered_evlog_recds(self, devicename): or Gb.evlog_trk_monitors_flag)] return el_recds + elif Gb.EvLog.greenbar_alert_msg.startswith('Start up log'): + self.clear_evlog_greenbar_msg() + el_devicename_check = ['*', '**', 'nodevices', devicename] if Device := Gb.Devices_by_devicename.get(devicename): @@ -721,6 +720,9 @@ def _extract_filtered_evlog_recds(self, devicename): # Select devicename recds, keep time & test elements, drop devicename try: + self.dist_to_devices_recd_found_flag = False + self.apple_acct_auth_cnts_by_owner = {} + el_recds = [self._master_reformat_text(el_recd, Device) for el_recd in self.event_recds if self._master_filter_recd(el_recd, devicename)] @@ -752,12 +754,34 @@ def _master_filter_recd(self, el_recd, devicename): # Return all of the time/text items if 'Show Tracking Monitors' was selected in the EvLog if Gb.evlog_trk_monitors_flag: return True + # if instr(el_recd[ELR_TEXT], 'Acct Auth'): + # return True + + # Only display the last DistTo Devices entry + if elr_text.startswith('DistTo Devices'): + if self.dist_to_devices_recd_found_flag: + return False + self.dist_to_devices_recd_found_flag = True + + # Only display the last DistTo Devices entry + # Apple Acct Auth #12 > GaryCobb(xxxxxxxx), Token + try: + if elr_text.startswith('Apple Acct Auth'): + apple_acct_owner = elr_text.split(' ')[5] + cnt = self.apple_acct_auth_cnts_by_owner.get(apple_acct_owner, 0) + cnt += 1 + self.apple_acct_auth_cnts_by_owner[apple_acct_owner] = cnt + if cnt < 3: + return True + except Exception as err: + log_exception(err) + pass # Drop Tracking Monitor recds or iCloud Authentication recds if elr_text.startswith(EVLOG_MONITOR): return False - if (elr_text.startswith('iCloud Acct') + if (instr(elr_text, 'Acct Auth') and Device and (Device.is_monitored or Device.primary_data_source == MOBAPP)): return False diff --git a/custom_components/icloud3/support/hacs_ic3.py b/custom_components/icloud3/support/hacs_ic3.py index 745702a..b54e55e 100644 --- a/custom_components/icloud3/support/hacs_ic3.py +++ b/custom_components/icloud3/support/hacs_ic3.py @@ -8,15 +8,16 @@ LAST_ZONE, LAST_ZONE_DNAME, LAST_ZONE_FNAME, LAST_ZONE_NAME, DIR_OF_TRAVEL, ) -from ..helpers.common import (instr, async_load_json_file, load_json_file, ) +from ..helpers.common import (instr, ) from ..helpers.messaging import (log_info_msg, log_debug_msg, log_exception, post_evlog_greenbar_msg, - _trace, _traceha, ) + _evlog, _log, ) from ..helpers.time_util import (datetime_now, secs_to_datetime, ) +from ..helpers.file_io import (file_exists, async_read_json_file, ) -import os +# import os import json import logging -import asyncio +# import asyncio # _LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(f"icloud3") @@ -31,12 +32,14 @@ async def check_hacs_icloud3_update_available(): return hacs_repository_file = Gb.hass.config.path(STORAGE_DIR, 'hacs.repositories') - if os.path.exists(hacs_repository_file) is False: + if file_exists(hacs_repository_file) is False: return None try: hacs_ic3_items = await _async_get_hacs_ic3_data(hacs_repository_file) - Gb.version_hacs = '' + version_hacs_ic3_dev = None + version_hacs_ic3 = None + Gb.version_hacs = '' if 'icloud3_v3' in hacs_ic3_items: version_hacs_ic3_dev = hacs_ic3_items['icloud3_v3'].get('last_version') @@ -67,7 +70,7 @@ async def _async_get_hacs_ic3_data(hacs_repository_file): ''' try: - hacs_repository_file_data = await async_load_json_file(hacs_repository_file) + hacs_repository_file_data = await async_read_json_file(hacs_repository_file) if hacs_repository_file_data != {}: hacs_ic3_items = {hacs_item_data['full_name'].split('/')[1].replace(' ', '_'): hacs_item_data diff --git a/custom_components/icloud3/support/icloud_data_handler.py b/custom_components/icloud3/support/icloud_data_handler.py index e22f47e..d0c6ae6 100644 --- a/custom_components/icloud3/support/icloud_data_handler.py +++ b/custom_components/icloud3/support/icloud_data_handler.py @@ -3,23 +3,23 @@ from ..const import (HOME, NOT_SET, HHMMSS_ZERO, EVLOG_ALERT, CRLF, CRLF_DOT, RARROW, - FMF, FAMSHR, - FMF_FNAME, FAMSHR_FNAME, MOBAPP, + ICLOUD, MOBAPP, LATITUDE, LONGITUDE, LOCATION, ) from ..support import start_ic3 as start_ic3 from ..support import pyicloud_ic3_interface - -from ..helpers.common import (instr, is_statzone, list_to_str, ) +from ..support import determine_interval as det_interval +from ..helpers.common import (instr, is_statzone, list_to_str, list_add, list_del, ) from ..helpers.messaging import (post_event, post_error_msg, post_monitor_msg, log_debug_msg, log_exception, log_start_finish_update_banner, log_rawdata, - _trace, _traceha, ) + _evlog, _log, ) + from ..helpers.time_util import (time_now_secs, secs_to_time, format_timer, format_age, secs_since,) from .pyicloud_ic3 import (PyiCloudAPIResponseException, PyiCloud2FARequiredException, - ICLOUD_ERROR_CODES, ) + HTTP_RESPONSE_CODES, ) #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> @@ -37,7 +37,7 @@ def no_icloud_update_needed_tracking(Device): and Device.isnotin_statzone): Device.icloud_no_update_reason = 'inZone & Next Update Time not Reached' - elif Gb.primary_data_source_ICLOUD is False: + elif Gb.use_data_source_ICLOUD is False: Device.icloud_no_update_reason = 'Global Mobile App Data Source' elif Device.is_data_source_ICLOUD is False: @@ -112,7 +112,7 @@ def is_icloud_update_needed_general(Device): and Device.next_update_secs == 0 and (Device.sensor_zone == NOT_SET or Device.icloud_initial_locate_done is False)): - Device.icloud_update_reason = f"Initial FamShr Locate@{Gb.this_update_time}" + Device.icloud_update_reason = f"Initial iCloud Locate@{Gb.this_update_time}" elif Device.icloud_update_retry_flag: Device.icloud_update_reason = "Retrying Location Refresh" @@ -136,9 +136,10 @@ def request_icloud_data_update(Device): 'locationType': '', 'timeStamp': 1587306847548, 'locationFinished': True, 'verticalAccuracy': 0.0, 'longitude': -80.3905776599289} ''' - if (Gb.primary_data_source_ICLOUD is False + if (Gb.use_data_source_ICLOUD is False or Device.is_data_source_ICLOUD is False - or Gb.PyiCloud is None): + or Device.PyiCloud is None): + # or Gb.PyiCloud is None): return False devicename = Device.devicename @@ -199,54 +200,51 @@ def update_PyiCloud_RawData_data(Device, results_msg_flag=True): ''' try: - if (Gb.primary_data_source_ICLOUD is False + if (Gb.use_data_source_ICLOUD is False or Device.is_data_source_ICLOUD is False - or Gb.PyiCloud is None): + or Device.PyiCloud is None): return False - if Device.device_id_famshr is None and Device.device_id_fmf is None: + if Device.icloud_device_id is None: return False - if pyicloud_ic3_interface.is_authentication_2fa_code_needed(Gb.PyiCloud): - if pyicloud_ic3_interface.authenticate_icloud_account(Gb.PyiCloud, called_from='data_handler') is False: + if pyicloud_ic3_interface.is_authentication_2fa_code_needed(Device.PyiCloud): + if pyicloud_ic3_interface.authenticate_icloud_account(Device.PyiCloud, called_from='data_handler') is False: return False if is_PyiCloud_RawData_data_useable(Device, results_msg_flag=False): return update_device_with_latest_raw_data(Device) - famshr_ok, famshr_loc_time_ok, famshr_gps_ok, famshr_secs, \ - famshr_gps_accuracy, famshr_time = \ - _get_devdata_useable_status(Device, FAMSHR) - fmf_ok, fmf_loc_time_ok, fmf_gps_ok, fmf_secs, \ - fmf_gps_accuracy, fmf_time = \ - False, False, False, 0, 0, '' - #_get_devdata_useable_status(Device, FMF) + icloud_ok, icloud_loc_time_ok, icloud_gps_ok, icloud_secs, \ + icloud_gps_accuracy, icloud_time = \ + _get_devdata_useable_status(Device, ICLOUD) - if (famshr_ok and famshr_secs > fmf_secs) or fmf_ok: + if icloud_ok: return update_all_devices_wih_latest_raw_data(Device) - pyicloud_start_call_time = time_now_secs() - - # Refresh FamShr Data - if Device.is_data_source_FAMSHR: - if ((secs_since(Gb.pyicloud_refresh_time[FAMSHR]) >= 5 - and (famshr_secs != Device.loc_data_secs - or Device.next_update_secs > (famshr_secs + 5))) + # Refresh iCloud Data + if Device.is_data_source_ICLOUD: + if ((secs_since(Device.PyiCloud.last_refresh_secs) >= 5 + and (icloud_secs != Device.loc_data_secs + or Device.next_update_secs > (icloud_secs + 5))) or Device.icloud_initial_locate_done is False): - Gb.PyiCloud.FamilySharing.refresh_client(requested_by_devicename=Device.devicename) + device_id = None if Device.PyiCloud.locate_all_devices else Device.icloud_device_id - # Refresh FmF Data - if Device.is_data_source_FMF: - if ((secs_since(Gb.pyicloud_refresh_time[FMF]) >= 5 - and (fmf_secs != Device.loc_data_secs - or Device.next_update_secs > (fmf_secs + 5))) - or Device.icloud_initial_locate_done is False): + locate_all_devices, device_id = _locate_all_or_acct_owner(Device) + if Device.PyiCloud.DeviceSvc: + Device.PyiCloud.DeviceSvc.refresh_client( requested_by_devicename=Device.devicename, + locate_all_devices=locate_all_devices, + device_id=device_id) + if (Device.PyiCloud.response_code == 503 + and Device.devicename not in Gb.username_pyicloud_503_connection_error): + list_add(Gb.username_pyicloud_503_connection_error, Device.devicename) + post_event( f"{EVLOG_ERROR}Apple Acct > {Device.PyiCloud.account_owner}, " + f"Refresh Location Data Failed, Connection Error 503, Will " + f"try to reconnect in 15-min") - Gb.PyiCloud.FindMyFriends.refresh_client(requested_by_devicename=Device.devicename) - Gb.pyicloud_location_update_cnt += 1 - Gb.pyicloud_calls_time += secs_since(pyicloud_start_call_time) + Device.PyiCloud.location_update_cnt += 1 if update_all_devices_wih_latest_raw_data(Device) is False: return False @@ -261,7 +259,7 @@ def update_PyiCloud_RawData_data(Device, results_msg_flag=True): except (PyiCloud2FARequiredException, PyiCloudAPIResponseException) as err: try: - _err_msg = ICLOUD_ERROR_CODES.get(Gb.PyiCloud.Session.response_status_code, + _err_msg = HTTP_RESPONSE_CODES.get(Device.PyiCloud.Session.response_code, 'Unknown Error') Device.icloud_acct_error_flag = True @@ -271,7 +269,8 @@ def update_PyiCloud_RawData_data(Device, results_msg_flag=True): f"refreshing the iCloud Location. iCloud may be down or there is an " f"internet connection issue. iCloud3 will try again later. " f"{CRLF_DOT}{_err_msg}, " - f"Error-{Gb.PyiCloud.Session.response_status_code}") + f"Error-{Device.PyiCloud.Session.response_code}") + # f"Error-{Gb.PyiCloud.Session.response_code}") post_event(Device, error_msg) post_error_msg(error_msg) @@ -283,13 +282,49 @@ def update_PyiCloud_RawData_data(Device, results_msg_flag=True): return False +#---------------------------------------------------------------------------- +def _locate_all_or_acct_owner(Device): + ''' + Determine how the refresh_client should be done: + - Locate all devices (the owners devices and icloud devices) + - locate only the devie requesting the locate (an owners device) + + Returns: + - [locate_all_devices, device_id] + ''' + PyiCloud = Device.PyiCloud + device_id = None if PyiCloud.locate_all_devices else Device.icloud_device_id + + _Device = det_interval.device_will_update_in_15secs(Device=Device, only_icloud_devices=True) + + # # If another device will update and that device is an owners device, set it to None + # # since it will update when the owners device updates + # if _Device and _Device.family_share_device is False: + # _Device = None + + # Locate_all override is enabled or + # locate_all=False but Locating a iCloud device (not in the owners device_id list) or + # Locate_all=False and updating an owners device but iCloud will update soon + if (PyiCloud.locate_all_devices + or Device.family_share_device + or _Device): + locate_all_devices = True + refresh_device_id = None + + # Updating an owners device + else: + locate_all_devices = False + refresh_device_id = device_id + + return (locate_all_devices, refresh_device_id) + #---------------------------------------------------------------------------- def update_all_devices_wih_latest_raw_data(Device): update_device_with_latest_raw_data(Device, all_devices=True) def update_device_with_latest_raw_data(Device, all_devices=False): ''' - Update a Device's location data with the latest data from FamSshr, FmF or the MobApp + Update a Device's location data with the latest data from FamSshr or the MobApp if is is better or newer than the old data. Optionally, cycle thru all PyiCloud Devices and update the data for every device being tracked or monitored when new data is requested for a device since iCloud gives us data for all devices. @@ -298,7 +333,7 @@ def update_device_with_latest_raw_data(Device, all_devices=False): and the one selecetd. ''' try: - save_trace_prefix, Gb.trace_prefix = Gb.trace_prefix, "LOCATE" + save_evlog_prefix, Gb.trace_prefix = Gb.trace_prefix, "LOCATE" if all_devices: Update_Devices = Gb.Devices # log_start_finish_update_banner('start', Device.devicename, 'Update All Devices from RawData', '') @@ -306,8 +341,11 @@ def update_device_with_latest_raw_data(Device, all_devices=False): Update_Devices = [Device] for _Device in Update_Devices: - _RawData = get_famshr_fmf_PyiCloud_RawData_to_use(_Device) - if _RawData is None: continue + if _Device.verified_flag is False: + continue + _RawData = get_icloud_PyiCloud_RawData_to_use(_Device) + if _RawData is None: + continue # Make sure data is really a available try: @@ -315,20 +353,16 @@ 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"iCloud - {rawdata_msg}-{_Device.devicename}/{_Device.is_data_source_FAMSHR_FMF}", + log_rawdata(f"iCloud - {rawdata_msg}-{_Device.devicename}/{_Device.is_data_source_ICLOUD}", {'filter': _RawData.device_data}) continue # log_exception(err) requesting_device_flag = (_Device.devicename == Device.devicename) - famshr_ok, famshr_loc_time_ok, famshr_gps_ok, famshr_secs, \ - famshr_gps_accuracy, famshr_time = \ - _get_devdata_useable_status(Device, FAMSHR) - fmf_ok, fmf_loc_time_ok, fmf_gps_ok, fmf_secs, \ - fmf_gps_accuracy, fmf_time = \ - False, False, False, 0, 0, '' - #_get_devdata_useable_status(Device, FMF) + icloud_ok, icloud_loc_time_ok, icloud_gps_ok, icloud_secs, \ + icloud_gps_accuracy, icloud_time = \ + _get_devdata_useable_status(Device, ICLOUD) # Add info for the Device that requested the update _Device.icloud_acct_error_flag = False @@ -338,7 +372,7 @@ def update_device_with_latest_raw_data(Device, all_devices=False): # Beta 6-Added _RawData.gps_accuracy test or _RawData.gps_accuracy > Gb.gps_accuracy_threshold or (_Device.is_location_good - and _Device.is_data_source_FAMSHR_FMF + and _Device.is_data_source_ICLOUD and _Device.loc_data_time > _RawData.location_time and _Device.loc_data_gps_accuracy < _RawData.gps_accuracy)): @@ -369,7 +403,7 @@ def update_device_with_latest_raw_data(Device, all_devices=False): _RawData.device_data[LOCATION][LONGITUDE]) # Move data from PyiCloud_RawData - _Device.update_dev_loc_data_from_raw_data_FAMSHR_FMF(_RawData, + _Device.update_dev_loc_data_from_raw_data_FAMSHR(_RawData, requesting_device_flag=requesting_device_flag) elif _Device.mobapp_data_secs > 0: @@ -393,30 +427,26 @@ def update_device_with_latest_raw_data(Device, all_devices=False): # This fct may be run a second time to recheck loc times for different data # sources. Don't redisplay it it's nothing changed. - if _Device.loc_msg_famshr_mobapp_time == \ + if _Device.loc_msg_icloud_mobapp_time == \ (f"{_Device.dev_data_source}-" f"{_Device.loc_data_time_gps}-" f"{_Device.mobapp_data_time_gps}"): continue - _Device.loc_msg_famshr_mobapp_time = \ + _Device.loc_msg_icloud_mobapp_time = \ (f"{_Device.dev_data_source}-" f"{_Device.loc_data_time_gps}-" f"{_Device.mobapp_data_time_gps}") other_times = "" - if famshr_secs > 0 and Gb.used_data_source_FAMSHR and _Device.dev_data_source != 'FamShr': - other_times += f"FamShr-{famshr_time}" - - # if fmf_secs > 0 and Gb.used_data_source_FMF and _Device.dev_data_source != 'FmF': - # if other_times != "": other_times += ", " - # other_times += f"FmF-{fmf_time}" + if icloud_secs > 0 and Gb.used_data_source_ICLOUD and _Device.dev_data_source != 'iCloud': + other_times += f"iCloud-{icloud_time}" if _Device.mobapp_monitor_flag and _Device.dev_data_source != 'MobApp': if other_times != "": other_times += ", " other_times += f"MobApp-{_Device.mobapp_data_time_gps}" # Display location times for the selected device and all other data sources - # (famshr or mobapp). Display in Event log if the requesting device or a monitor + # (icloud or mobapp). Display in Event log if the requesting device or a monitor # msg if another device. Don't redisplay this msg if the data was just updated # within the last 5-secs but running through this routine again. if secs_since(_Device.loc_data_secs) <= _Device.old_loc_threshold_secs + 5: @@ -434,8 +464,9 @@ def update_device_with_latest_raw_data(Device, all_devices=False): else: post_monitor_msg(_Device, event_msg) - pyicloud_ic3_interface.display_authentication_msg(Gb.PyiCloud) - Gb.trace_prefix = save_trace_prefix + pyicloud_ic3_interface.display_authentication_msg(Device.PyiCloud) + # pyicloud_ic3_interface.display_authentication_msg(Gb.PyiCloud) + Gb.trace_prefix = save_evlog_prefix return True @@ -455,13 +486,9 @@ def is_PyiCloud_RawData_data_useable(Device, results_msg_flag=True): False - The data for Device is old ''' - famshr_ok, famshr_loc_time_ok, famshr_gps_ok, famshr_secs, \ - famshr_gps_accuracy, famshr_time = \ - _get_devdata_useable_status(Device, FAMSHR) - fmf_ok, fmf_loc_time_ok, fmf_gps_ok, fmf_secs, \ - fmf_gps_accuracy, fmf_time = \ - False, False, False, 0, 0, '' - #_get_devdata_useable_status(Device, FMF) + icloud_ok, icloud_loc_time_ok, icloud_gps_ok, icloud_secs, \ + icloud_gps_accuracy, icloud_time = \ + _get_devdata_useable_status(Device, ICLOUD) if Gb.icloud_force_update_flag or Device.icloud_force_update_flag: Gb.icloud_force_update_flag = False @@ -469,13 +496,13 @@ def is_PyiCloud_RawData_data_useable(Device, results_msg_flag=True): useable_msg = 'Update Required' return False - if famshr_ok or fmf_ok: + if icloud_ok: is_useable_flag = True useable_msg = 'Useable' - elif famshr_loc_time_ok is False or fmf_loc_time_ok is False: + elif icloud_loc_time_ok is False: is_useable_flag = False useable_msg = 'Data-Old' - elif famshr_gps_ok is False or fmf_gps_ok is False: + elif icloud_gps_ok is False: is_useable_flag = False useable_msg = 'Data-PoorGps' @@ -483,16 +510,10 @@ def is_PyiCloud_RawData_data_useable(Device, results_msg_flag=True): return is_useable_flag data_type = 'iCloud' - if famshr_secs >= fmf_secs: - data_type = FAMSHR_FNAME - elif fmf_secs > famshr_secs: - data_type = FMF_FNAME event_msg = f"{data_type} {useable_msg} > " - if famshr_secs > 0 and Gb.used_data_source_FAMSHR: - event_msg += f"FamShr-{famshr_time}, " - # if fmf_secs > 0 and Gb.used_data_source_FMF: - # event_msg += f"FmF-{fmf_time}, " + if icloud_secs > 0 and Gb.used_data_source_ICLOUD: + event_msg += f"iCloud-{icloud_time}, " if is_useable_flag is False: event_msg += "Requesting New Location" @@ -524,12 +545,9 @@ def _get_devdata_useable_status(Device, data_source): device_id = None RawData = None - if data_source == FAMSHR: - RawData = Device.PyiCloud_RawData_famshr - device_id = Device.device_id_famshr - elif data_source == FMF: - RawData = Device.PyiCloud_RawData_fmf - device_id = Device.device_id_fmf + if data_source == ICLOUD: + RawData = Device.PyiCloud_RawData_icloud + device_id = Device.icloud_device_id else: return False, False, False, 0, 0, '' @@ -573,60 +591,23 @@ def _get_devdata_useable_status(Device, data_source): return Device.dev_data_useable_chk_results #---------------------------------------------------------------------------- -def get_famshr_fmf_PyiCloud_RawData_to_use(_Device): +def get_icloud_PyiCloud_RawData_to_use(_Device): ''' Analyze tracking method and location times from the raw PyiCloud device data to get best data to use Return: - _RawData - The PyiCloud_RawData (_famshr or _fmf) data object + _RawData - The PyiCloud_RawData (_icloud) data object ''' try: - _RawData_famshr = Gb.PyiCloud.RawData_by_device_id.get(_Device.device_id_famshr) - _RawData_fmf = Gb.PyiCloud.RawData_by_device_id.get(_Device.device_id_fmf) + _RawData_icloud = _Device.PyiCloud.RawData_by_device_id.get(_Device.icloud_device_id) - if _RawData_famshr and _RawData_fmf is None: - _RawData = _RawData_famshr - - elif _RawData_fmf and _RawData_famshr is None: - _RawData = _RawData_fmf - - elif _RawData_famshr is None and _RawData_famshr is None: + if _RawData_icloud is None: _RawData = None _Device.data_source = MOBAPP return - # Is famshr raw data newer than fmf raw data - elif _RawData_famshr.location_secs >= _RawData_fmf.location_secs: - _RawData = _RawData_famshr - - # Is fmf raw data newer than famshr raw data - elif _RawData_fmf.location_secs >= _RawData_famshr.location_secs: - _RawData = _RawData_fmf - - elif _RawData_famshr and _Device.is_data_source_FAMSHR: - _RawData = _RawData_famshr - - elif _RawData_fmf and _Device.is_data_source_FMF: - _RawData = _RawData_fmf - - elif _RawData_famshr: - _RawData = _RawData_famshr - - elif _RawData_fmf: - _RawData = _RawData_fmf - - else: - _RawData = None - _Device.data_source = MOBAPP - - post_event( f"{EVLOG_ALERT}Data Exception > {_Device.devicename} > No iCloud FamShr " - f"or FmF Device Id was assigned to this device. This can be caused by " - f"No location data was returned from iCloud when iCloud3 was started." - f"{CRLF}Actions > Restart iCloud3. If the error continues, check the Event Log " - f"(iCloud3 Initialization Stage 2) and verify that the device is valid and a " - f"tracking method has been assigned. " - f"The device will be tracked by the Mobile App.") + _RawData = _RawData_icloud error_msg = '' if _RawData is None: @@ -651,5 +632,9 @@ def get_famshr_fmf_PyiCloud_RawData_to_use(_Device): return _RawData except Exception as err: - log_exception(err) + # log_exception(err) + post_error_msg("iCloud3 Error > Error extracting device info from Apple Acct data, " + f"Device-{_Device.fname_devicename}, " + f"AppleAcct-{_Device.conf_apple_acct_username}. " + f"iCloudName-{_Device.conf_icloud_dname}") return None diff --git a/custom_components/icloud3/support/mobapp_data_handler.py b/custom_components/icloud3/support/mobapp_data_handler.py index 067202d..5f49151 100644 --- a/custom_components/icloud3/support/mobapp_data_handler.py +++ b/custom_components/icloud3/support/mobapp_data_handler.py @@ -9,13 +9,14 @@ LATITUDE, LONGITUDE, TIMESTAMP_SECS, TIMESTAMP_TIME, TRIGGER, LAST_ZONE, ZONE, GPS_ACCURACY, VERT_ACCURACY, ALTITUDE, - CONF_IC3_DEVICENAME, + CONF_IC3_DEVICENAME, CONF_MOBILE_APP_DEVICE, ) -from ..helpers.common import (instr, is_statzone, is_zone, zone_dname, ) +from ..helpers.common import (instr, is_statzone, is_zone, zone_dname, + list_add, list_to_str, ) from ..helpers.messaging import (post_event, post_monitor_msg, more_info, log_debug_msg, log_exception, log_error_msg, log_rawdata, - _trace, _traceha, ) + _evlog, _log, ) from ..helpers.time_util import (secs_to_time, secs_since, format_time_age, format_age, ) from ..helpers.dist_util import (format_dist_km, format_dist_m, ) from ..helpers import entity_io @@ -49,7 +50,7 @@ def check_mobapp_state_trigger_change(Device): mobapp_data_state_not_set_flag = (Device.mobapp_data_state == NOT_SET) - # Get the state data + # Get the state data, If error cnt > 50, mobile app monitoring disabled device_trkr_attrs = get_mobapp_device_trkr_entity_attrs(Device) if device_trkr_attrs is None: return @@ -417,36 +418,12 @@ def get_mobapp_device_trkr_entity_attrs(Device): return None entity_id = Device.mobapp[DEVICE_TRACKER] - device_trkr_attrs = {} - device_trkr_attrs[DEVICE_TRACKER] = entity_io.get_state(entity_id) - - if device_trkr_attrs[DEVICE_TRACKER] == 'unavailable': - Device.mobapp_monitor_flag = False - Device.mobapp_device_unavailable_flag = True - Device.mobapp_data_invalid_error_cnt += 1 - post_event( f"{EVLOG_ALERT}The Mobile App has returned a `not available` status " - f"and will not be used for tracking or zone enter/exit events" - f"{CRLF_DOT}{Device.fname_devicename}{RARROW}{entity_id}" - f"{more_info('mobapp_device_unavailable')}") - log_error_msg(f"iCloud3 Error ({Device.fname_devtype}) > " - f"The Mobile App is not available and this device will not be monitored") - return None - device_trkr_attrs.update(entity_io.get_attributes(entity_id)) - - if LATITUDE not in device_trkr_attrs or device_trkr_attrs[LATITUDE] == 0: - Device.mobapp_data_invalid_error_cnt += 1 - if Device.mobapp_data_invalid_error_cnt == 4: - post_event( f"{EVLOG_ALERT}The Mobile App has not reported the gps " - f"location after 4 requests. It may be asleep, offline " - f"or not available and should be reviewed." - f"{CRLF_DOT}{Device.fname_devicename}{RARROW}{entity_id}" - f"{more_info('mobapp_device_no_location')}") - log_error_msg(f"iCloud3 Alert ({Device.fname_devtype}) > " - f"The Mobile App has not reported the gps location after 4 requests. " - f"It may be asleep, offline or not available.") - return None + device_trkr_attrs = get_and_verify_device_trkr_data(Device, entity_id) + if device_trkr_attrs is None: + return + Device.mobapp_data_invalid_error_cnt = 0 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}"]) @@ -466,6 +443,71 @@ def get_mobapp_device_trkr_entity_attrs(Device): log_exception(err) return None +# ----------------------------------------------------------------- +def get_and_verify_device_trkr_data(Device, entity_id): + ''' + 1. Get the state value of the device_tracker entity, then get the attributes + if it is available. + 2. Verify the location is available in the attributes. Increment an error counter if not. + 3. If the error counter > 50, disable the Device's mobile_app monitoring flag. + Display an error message on every 5th error. The counter is reset when good data + is received. + + Return: + device_trkr_attributes - State value and all attributes or None + ''' + # Get only the entity stateto see if it is available + try: + device_trkr_attrs = {} + device_trkr_attrs[DEVICE_TRACKER] = entity_io.get_state(entity_id) + + if device_trkr_attrs[DEVICE_TRACKER] == NOT_SET: + if Device.mobapp_data_invalid_error_cnt < 50: + Device.mobapp_data_invalid_error_cnt += 1 + if (Device.mobapp_data_invalid_error_cnt % 5) == 0: + post_event(Device, + f"{EVLOG_ALERT}MOBILE APP ERROR (#{Device.mobapp_data_invalid_error_cnt}) > " + F"Returned a `not available` status" + f"{CRLF_DOT}{Device.fname_devicename}{RARROW}{entity_id}") + # f"{more_info('mobapp_device_unavailable')}") + log_error_msg(f"iCloud3 Error ({Device.fname}) > Mobile App status is `unavailable`") + return None + + # More than 50 errors, shut down Mobile App monitoring for this device + Device.mobapp_monitor_flag = False + Device.mobapp_device_unavailable_flag = True + + post_event(Device, + f"{EVLOG_ALERT}The Mobile App has not reported the gps " + f"location after 4 requests. It may be asleep, offline " + f"or not available and should be reviewed." + f"{CRLF_DOT}{Device.fname_devicename}{RARROW}{entity_id}" + f"{more_info('mobapp_device_no_location')}") + log_error_msg(f"iCloud3 Alert ({Device.fname_devtype}) > " + f"The Mobile App has not reported the gps location after 4 requests. " + f"It may be asleep, offline or not available.") + return None + + # Entity is available, get the attributes + device_trkr_attrs.update(entity_io.get_attributes(entity_id)) + + if LATITUDE not in device_trkr_attrs or device_trkr_attrs[LATITUDE] == 0: + Device.mobapp_data_invalid_error_cnt += 1 + if (Device.mobapp_data_invalid_error_cnt % 5) == 0: + post_event( f"{EVLOG_ALERT}MOBILE APP ERROR (#{Device.mobapp_data_invalid_error_cnt}) > " + f"No gps location reported. It may be asleep, offline or not available" + f"{CRLF_DOT}{Device.fname_devicename}{RARROW}{entity_id}") + # f"{more_info('mobapp_device_no_location')}") + log_error_msg(f"iCloud3 Alert ({Device.fname}) > " + f"Mobile App gps location not reported") + return None + + except Exception as err: + log_exception(err) + return None + + return device_trkr_attrs + # ----------------------------------------------------------------- def update_mobapp_data_from_entity_attrs(Device, device_trkr_attrs): ''' @@ -495,7 +537,6 @@ def update_mobapp_data_from_entity_attrs(Device, device_trkr_attrs): Device.mobapp_data_trigger = device_trkr_attrs.get("trigger", NOT_SET) Device.mobapp_data_secs = mobapp_data_secs Device.mobapp_data_time = mobapp_data_time - Device.mobapp_data_invalid_error_cnt = 0 Device.mobapp_data_latitude = device_trkr_attrs.get(LATITUDE, 0) Device.mobapp_data_longitude = device_trkr_attrs.get(LONGITUDE, 0) Device.mobapp_data_gps_accuracy = gps_accuracy @@ -515,7 +556,6 @@ def update_mobapp_data_from_entity_attrs(Device, device_trkr_attrs): Device.update_mobapp_data_monitor_msg = monitor_msg post_monitor_msg(Device.devicename, monitor_msg) - #-------------------------------------------------------------------- def sync_mobapp_data_state_statzone(Device): ''' @@ -543,7 +583,6 @@ def sync_mobapp_data_state_statzone(Device): if (is_statzone(mobapp_data_state) and is_statzone(Device.loc_data_zone) and Device.isnotin_zone_mobapp_state): - #and Device.mobapp_data_state == NOT_HOME): Device.mobapp_data_state = mobapp_data_state Device.mobapp_data_state_secs = Gb.this_update_secs Device.mobapp_data_state_time = Gb.this_update_time @@ -551,3 +590,129 @@ def sync_mobapp_data_state_statzone(Device): return True return False + +#-------------------------------------------------------------------- +def build_mobapp_integration_device_tables(): + + ''' + {'d7f4264ab72046285ca92c0946f381e167a6ba13292eef17d4f60a4bf0bd654c': + DeviceEntry(area_id=None, config_entries={'ad5c8f66b14fda011107827b383d4757'}, + configuration_url=None, connections=set(), disabled_by=None, entry_type=None, + hw_version=None, id='93e3d6b65eb05072dcb590b46c02d920', + identifiers={('mobile_app', '1A9EAFA3-2448-4F37-B069-3B3A1324EFC5')}, + manufacturer='Apple', model='iPad8,9', name_by_user='Gary-iPad-app', + name='Gary-iPad', serial_number=None, suggested_area=None, sw_version='17.2', + via_device_id=None, is_new=False), + ''' + try: + if Gb.conf_data_source_MOBAPP is False: + return True + + if 'mobile_app' in Gb.hass.data: + Gb.MobileApp_data = Gb.hass.data['mobile_app'] + mobile_app_devices = Gb.MobileApp_data.get('devices', {}) + + Gb.mobile_app_device_fnames = [] + Gb.mobapp_fnames_by_mobapp_id = {} + Gb.mobapp_ids_by_mobapp_fname = {} + Gb.mobapp_fnames_disabled = [] + for device_id, device_entry in mobile_app_devices.items(): + if device_entry.disabled_by is None: + list_add(Gb.mobile_app_device_fnames, device_entry.name_by_user) + list_add(Gb.mobile_app_device_fnames, device_entry.name) + Gb.mobapp_fnames_by_mobapp_id[device_entry.id] = device_entry.name_by_user or device_entry.name + Gb.mobapp_ids_by_mobapp_fname[device_entry.name] = device_entry.id + Gb.mobapp_ids_by_mobapp_fname[device_entry.name_by_user] = device_entry.id + else: + list_add(Gb.mobapp_fnames_disabled, device_entry.id) + list_add(Gb.mobapp_fnames_disabled, device_entry.name) + list_add(Gb.mobapp_fnames_disabled, device_entry.name_by_user) + + if Gb.mobile_app_device_fnames: + post_event( f"Checking Mobile App Integration > Loaded, " + f"Devices-{list_to_str(Gb.mobile_app_device_fnames)}") + + Gb.startup_lists['Gb.mobile_app_device_fnames'] = Gb.mobile_app_device_fnames + Gb.startup_lists['Gb.mobapp_fnames_by_mobapp_id'] = Gb.mobapp_fnames_by_mobapp_id + Gb.startup_lists['Gb.mobapp_ids_by_mobapp_fname'] = Gb.mobapp_ids_by_mobapp_fname + Gb.startup_lists['Gb.mobapp_fnames_disabled'] = Gb.mobapp_fnames_disabled + + return True + + except Exception as err: + #log_exception(err) + return False + +#-------------------------------------------------------------------- +def verify_device_in_ha_mobile_app_config(): + ''' + Cycle through the iCloud3 device's configuration and see if a + mobapp device is in the HA Mobiole App Integrations list + + Update the Device's mobapp_monitor_flag to True if it is found. + Do not change it if it is not found. + + Return: + unverified_devices_list - list of devices that are not verified (Device.fname) + ''' + + try: + # Cycle thru conf_devices, see if Device's Mobile app entity is found + Gb.device_mobapp_verify_retry_needed = False + unverified_devices_list = [] + for conf_device in Gb.conf_devices: + Device = Gb.Devices_by_devicename[conf_device[CONF_IC3_DEVICENAME]] + if (Device.mobapp_monitor_flag + or conf_device[CONF_MOBILE_APP_DEVICE] == 'None'): + continue + + if Device.conf_mobapp_fname in Gb.mobile_app_device_fnames: + Device.mobapp_monitor_flag = True + else: + Gb.device_mobapp_verify_retry_needed = True + list_add(unverified_devices_list, Device.fname) + + except Exception as err: + #log_exception(err) + return [] + + return unverified_devices_list + +#-------------------------------------------------------------------- +def x_unverified_devices_mobapp_handler(): + ''' + + Retry Device Mobile App verification + ''' + Gb.device_mobapp_verify_retry_cnt += 1 + build_mobapp_integration_device_tables() + unverified_devices_list = verify_device_in_ha_mobile_app_config() + + if Gb.device_mobapp_verify_retry_needed is False: + post_event("All Devices monitoring the Mobile App Integration have been verified") + else: + event_msg =(f"{EVLOG_ALERT}MOBILE APP DEVICES NOT VERIFIED (#{Gb.device_mobapp_verify_retry_cnt}) > " + f"Some Devices are not verified as as available in the HA " + f"Mobile App Integration, " + f"Devices-{list_to_str(unverified_devices_list)}, ") + if Gb.device_mobapp_verify_retry_cnt < 5: + event_msg += "Will retry in 5-mins" + else: + Gb.device_mobapp_verify_retry_needed = False + event_msg += "Max retries have been done. Devices will not monitor the Mobile App devices" + post_event(event_msg) + +#-------------------------------------------------------------------- +def unverified_devices_mobapp_list(): + ''' + Returns a list of devices that are unverified that are monitoring the mobapp + ''' + + unverified_devices_list = [] + for conf_device in Gb.conf_devices: + Device = Gb.Devices_by_devicename[conf_device[CONF_IC3_DEVICENAME]] + if (Device.mobapp_monitor_flag is False + and conf_device[CONF_MOBILE_APP_DEVICE] != 'None'): + list_add(unverified_devices_list, Device.fname) + + return unverified_devices_list \ No newline at end of file diff --git a/custom_components/icloud3/support/mobapp_interface.py b/custom_components/icloud3/support/mobapp_interface.py index 2d96da6..3ac745f 100644 --- a/custom_components/icloud3/support/mobapp_interface.py +++ b/custom_components/icloud3/support/mobapp_interface.py @@ -1,11 +1,14 @@ from ..global_variables import GlobalVariables as Gb -from ..const import (NOTIFY, EVLOG_NOTICE, NEXT_UPDATE, - CRLF_DOT, CRLF, NBSP6,RED_X, YELLOW_ALERT, ) -from ..helpers.common import (instr, list_add, ) +from ..const import (NOTIFY, EVLOG_NOTICE, NEXT_UPDATE, DEVICE_TYPES, + CRLF_DOT, CRLF, NBSP6,RED_X, YELLOW_ALERT, RARROW, + CONF_IC3_DEVICENAME, CONF_MOBILE_APP_DEVICE) +from ..helpers.common import (instr, list_add, list_to_str, ) +from ..helpers import file_io from ..helpers.messaging import (post_event, post_error_msg, post_evlog_greenbar_msg, - log_info_msg, log_exception, log_rawdata, _trace, _traceha, ) + log_info_msg, log_exception, log_rawdata, log_debug_msg, + _evlog, _log, ) 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 @@ -31,92 +34,89 @@ # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> def get_entity_registry_mobile_app_devices(): - mobapp_id_by_mobapp_devicename = {} - mobapp_devicename_by_mobapp_id = {} - device_info_by_mobapp_devicename = {} - device_model_info_by_mobapp_devicename = {} # [raw_model, model, model_display_name] - # ['iPhone15,2', 'iPhone', 'iPhone 14 Pro'] - last_updt_trig_by_mobapp_devicename = {} - mobile_app_notify_devicename = [] - battery_level_sensors_by_mobapp_devicename = {} - battery_state_sensors_by_mobapp_devicename = {} + Gb.mobapp_id_by_mobapp_dname = {} + Gb.mobapp_dname_by_mobapp_id = {} + Gb.device_info_by_mobapp_dname = {} # [mobapp_fname, raw_model, model, model_display_name] + # ['Gary-iPhone (MobApp), iPhone15,2', 'iPhone', 'iPhone 14 Pro'] + Gb.last_updt_trig_by_mobapp_dname = {} + Gb.mobile_app_notify_devicename = [] + Gb.battery_level_sensors_by_mobapp_dname = {} + Gb.battery_state_sensors_by_mobapp_dname = {} device_registry = dr.async_get(Gb.hass) try: - with open(Gb.entity_registry_file, 'r') as f: - entity_reg_data = json.load(f) - mobile_app_entities = [x for x in entity_reg_data['data']['entities'] - if x['platform'] == 'mobile_app'] - dev_trkr_entities = [x for x in mobile_app_entities - if x['entity_id'].startswith('device_tracker')] - - for dev_trkr_entity in dev_trkr_entities: - if 'device_id' not in dev_trkr_entity: continue - - mobapp_devicename = dev_trkr_entity['entity_id'].replace('device_tracker.', '') - dup_cnt = 1 - while mobapp_devicename in mobapp_id_by_mobapp_devicename: - dup_cnt += 1 - mobapp_devicename = f"{mobapp_devicename} ({dup_cnt})" - if dup_cnt > 1: - alert_msg = (f"Duplicate Mobile App devices in Entity Registry for " - f"{dev_trkr_entity['entity_id']}") - post_evlog_greenbar_msg(alert_msg) - - log_title = (f"MobApp entity_registry entry - <{mobapp_devicename}>)") - log_rawdata(log_title, dev_trkr_entity, log_rawdata_flag=True) - - raw_model = 'Unknown' - device_id = dev_trkr_entity['device_id'] - try: - # 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_rawdata(log_title, str(device_reg_data), log_rawdata_flag=True) - - raw_model = device_reg_data.model - - except Exception as err: - log_exception(err) - pass - - mobapp_id_by_mobapp_devicename[mobapp_devicename] = dev_trkr_entity['device_id'] - mobapp_devicename_by_mobapp_id[dev_trkr_entity['device_id']] = mobapp_devicename - - mobapp_fname = dev_trkr_entity['name'] or dev_trkr_entity['original_name'] - device_info_by_mobapp_devicename[mobapp_devicename] = f"{mobapp_fname} ({raw_model})" - device_model_info_by_mobapp_devicename[mobapp_devicename] = [raw_model,'',''] # iPhone15,2;iPhone;iPhone 14 Pro + entity_reg_data = file_io.read_json_file(Gb.entity_registry_file) + mobile_app_entities = [x for x in entity_reg_data['data']['entities'] + if x['platform'] == 'mobile_app'] + dev_trkr_entities = [x for x in mobile_app_entities + if x['entity_id'].startswith('device_tracker')] + + for dev_trkr_entity in dev_trkr_entities: + if 'device_id' not in dev_trkr_entity: continue + + mobapp_dname = dev_trkr_entity['entity_id'].replace('device_tracker.', '') + dup_cnt = 1 + while mobapp_dname in Gb.mobapp_id_by_mobapp_dname: + dup_cnt += 1 + mobapp_dname = f"{mobapp_dname} ({dup_cnt})" + if dup_cnt > 1: + alert_msg = (f"Duplicate Mobile App devices in Entity Registry for " + f"{dev_trkr_entity['entity_id']}") + post_evlog_greenbar_msg(alert_msg) + + + raw_model = 'Unknown' + device_id = dev_trkr_entity['device_id'] + try: + # Get raw_model from HA device_registry + device_reg_data = device_registry.async_get(device_id) + + log_title = (f"MobApp device_registry entry - <{mobapp_dname}>)") + log_rawdata(log_title, str(device_reg_data), log_rawdata_flag=True) + + raw_model = device_reg_data.model + + except Exception as err: + log_exception(err) + pass + + model_display_name = Gb.model_display_name_by_raw_model.get(raw_model, raw_model) + Gb.mobapp_id_by_mobapp_dname[mobapp_dname] = dev_trkr_entity['device_id'] + Gb.mobapp_dname_by_mobapp_id[dev_trkr_entity['device_id']] = mobapp_dname + + _device_types = [_device_type for _device_type in DEVICE_TYPES + if (instr(raw_model, _device_type) + or instr(dev_trkr_entity['name'], _device_type) + or instr(dev_trkr_entity['original_name'], _device_type))] + device_type = _device_types[0] if _device_types else raw_model + + mobapp_fname = dev_trkr_entity['name'] or dev_trkr_entity['original_name'] + Gb.device_info_by_mobapp_dname[mobapp_dname] = \ + [mobapp_fname, raw_model, device_type, model_display_name] # Gary-iPhone, iPhone15,2; iPhone; iPhone 14 Pro + + log_title = (f"MobApp entity_registry entry - <{mobapp_dname}>)") + log_rawdata(log_title, dev_trkr_entity, log_rawdata_flag=True) last_updt_trigger_sensors = _extract_mobile_app_entities(mobile_app_entities, '_last_update_trigger') battery_level_sensors = _extract_mobile_app_entities(mobile_app_entities, '_battery_level') battery_state_sensors = _extract_mobile_app_entities(mobile_app_entities, '_battery_state') - last_updt_trig_by_mobapp_devicename = _extract_sensor_entities( - mobapp_devicename_by_mobapp_id, last_updt_trigger_sensors) - battery_level_sensors_by_mobapp_devicename = _extract_sensor_entities( - mobapp_devicename_by_mobapp_id, battery_level_sensors) - battery_state_sensors_by_mobapp_devicename = _extract_sensor_entities( - mobapp_devicename_by_mobapp_id, battery_state_sensors) + Gb.last_updt_trig_by_mobapp_dname = _extract_sensor_entities(last_updt_trigger_sensors) + Gb.battery_level_sensors_by_mobapp_dname = _extract_sensor_entities(battery_level_sensors) + Gb.battery_state_sensors_by_mobapp_dname = _extract_sensor_entities(battery_state_sensors) - Gb.debug_log['_.mobapp_id_by_mobapp_devicename'] = {k: v[:10] for k, v in mobapp_id_by_mobapp_devicename.items()} - Gb.debug_log['_.mobapp_devicename_by_mobapp_id'] = {k[:10]: v for k, v in mobapp_devicename_by_mobapp_id.items()} - Gb.debug_log['_.last_updt_trig_by_mobapp_devicename'] = last_updt_trig_by_mobapp_devicename - Gb.debug_log['_.battery_level_sensors_by_mobapp_devicename'] = battery_level_sensors_by_mobapp_devicename - Gb.debug_log['_.battery_state_sensors_by_mobapp_devicename'] = battery_state_sensors_by_mobapp_devicename + Gb.startup_lists['Gb.mobapp_id_by_mobapp_dname'] = {k: v for k, v in Gb.mobapp_id_by_mobapp_dname.items()} + Gb.startup_lists['Gb.mobapp_dname_by_mobapp_id'] = {k: v for k, v in Gb.mobapp_dname_by_mobapp_id.items()} + Gb.startup_lists['Gb.device_info_by_mobapp_dname'] = Gb.device_info_by_mobapp_dname + Gb.startup_lists['Gb.last_updt_trig_by_mobapp_dname'] = Gb.last_updt_trig_by_mobapp_dname + Gb.startup_lists['Gb.battery_level_sensors_by_mobapp_dname'] = Gb.battery_level_sensors_by_mobapp_dname + Gb.startup_lists['Gb.battery_state_sensors_by_mobapp_dname'] = Gb.battery_state_sensors_by_mobapp_dname except Exception as err: log_exception(err) - return [mobapp_id_by_mobapp_devicename, - mobapp_devicename_by_mobapp_id, - device_info_by_mobapp_devicename, - device_model_info_by_mobapp_devicename, - last_updt_trig_by_mobapp_devicename, - mobile_app_notify_devicename, - battery_level_sensors_by_mobapp_devicename, - battery_state_sensors_by_mobapp_devicename] + return #----------------------------------------------------------------------------------------------------- def _extract_mobile_app_entities(mobile_app_entities, entity_name): @@ -130,33 +130,33 @@ def _extract_mobile_app_entities(mobile_app_entities, entity_name): if instr(x['unique_id'], entity_name)] #----------------------------------------------------------------------------------------------------- -def _extract_sensor_entities(mobapp_id_by_mobapp_devicename, sensor_entities): +def _extract_sensor_entities(sensor_entities): ''' - Match the mobile_app sensors entities with the devices they belong to. + Cycle through all of the sensor_entities (Ex. all last_updt_trigger_sensors) and + select the ones with device_ids that are Mobile App devices. + Example: {'gary_iphone_app': 'gary_iphone_app_last_update_trigger', 'gary_ipad_2': 'gary_ipad_last_update_trigger'}} Return - A dictionary of the sensor entity for the specific mobapp device - - Return - A list of the mobile_app entities ''' - return {mobapp_id_by_mobapp_devicename[sensor['device_id']]: _entity_name_disabled_by(sensor) - for sensor in sensor_entities - if sensor['device_id'] in mobapp_id_by_mobapp_devicename} + return {Gb.mobapp_dname_by_mobapp_id[sensor['device_id']]: _entity_name_disabled_by(sensor) + for sensor in sensor_entities + if sensor['device_id'] in Gb.mobapp_dname_by_mobapp_id} #----------------------------------------------------------------------------------------------------- def _entity_name(entity_id): return entity_id.replace('sensor.', '') def x_entity_name_disabled_by(sensor): - disabled_prefix = '' if sensor['disabled_by'] is None \ - else f"{RED_X}DISABLED SENSOR{CRLF}{NBSP6}{NBSP6}{NBSP6}" + disabled_prefix = '' if sensor['disabled_by'] is None \ + else f"{RED_X}DISABLED SENSOR{CRLF}{NBSP6}{NBSP6}{NBSP6}" return f"{disabled_prefix}{sensor['entity_id'].replace('sensor.', '')}" def _entity_name_disabled_by(sensor): if sensor['disabled_by']: - Gb.mobapp_fnames_disabled = list_add(Gb.mobapp_fnames_disabled, sensor['device_id']) + list_add(Gb.mobapp_fnames_disabled, sensor['device_id']) return sensor['entity_id'].replace('sensor.', '') @@ -167,26 +167,157 @@ def get_mobile_app_notify_devicenames(): send notifications to a device. ''' - mobile_app_notify_devicenames = [] + Gb.mobile_app_notify_devicenames = [] try: notify_targets = mobile_app_notify.push_registrations(Gb.hass) for notify_target in notify_targets: - mobile_app_notify_devicenames.append(f"mobile_app_{slugify(notify_target)}") + Gb.mobile_app_notify_devicenames.append(f"mobile_app_{slugify(notify_target)}") - return mobile_app_notify_devicenames + return Gb.mobile_app_notify_devicenames except Exception as err: log_info_msg("Mobile App Notify Service has not been set up yet. iCloud3 will retry later.") # log_exception(err) pass - return mobile_app_notify_devicenames + return Gb.mobile_app_notify_devicenames #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # -# Send a message to the mobapp +# GET MOBILE APP DEVICE FNAME # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + +def get_mobile_app_integration_device_info(ha_started_check=False): + ''' + Check to see if the Mobile App Integration is installed. + - If it is, set the Device.conf_mobapp_fname to the value in the Mobile App Integration + - If it is not, a temporary name will be assigned for display in Stage 4 and this will + be rechedked when ha has finished loading. + + {'d7f4264ab72046285ca92c0946f381e167a6ba13292eef17d4f60a4bf0bd654c': + DeviceEntry(area_id=None, config_entries={'ad5c8f66b14fda011107827b383d4757'}, + configuration_url=None, connections=set(), disabled_by=None, entry_type=None, + hw_version=None, id='93e3d6b65eb05072dcb590b46c02d920', + identifiers={('mobile_app', '1A9EAFA3-2448-4F37-B069-3B3A1324EFC5')}, + manufacturer='Apple', model='iPad8,9', name_by_user='Gary-iPad-app', + name='Gary-iPad', serial_number=None, suggested_area=None, sw_version='17.2', + via_device_id=None, is_new=False), + ''' + try: + return + #if Gb.conf_data_source_MOBAPP is False: + # return True + + if 'mobile_app' in Gb.hass.data: + Gb.MobileApp_data = Gb.hass.data['mobile_app'] + mobile_app_devices = Gb.MobileApp_data.get('devices', {}) + + Gb.MobileApp_device_fnames = [] + Gb.MobileApp_fnames_x_mobapp_id = {} + Gb.MobileApp_fnames_disabled = [] + for device_id, device_entry in mobile_app_devices.items(): + if device_entry.disabled_by is None: + list_add(Gb.MobileApp_device_fnames, device_entry.name_by_user) + list_add(Gb.MobileApp_device_fnames, device_entry.name) + Gb.MobileApp_fnames_x_mobapp_id[device_entry.id] = device_entry.name_by_user or device_entry.name + Gb.MobileApp_fnames_x_mobapp_id[device_entry.name] = device_entry.id + Gb.MobileApp_fnames_x_mobapp_id[device_entry.name_by_user] = device_entry.id + else: + list_add(Gb.MobileApp_fnames_disabled, device_entry.id) + list_add(Gb.MobileApp_fnames_disabled, device_entry.name) + list_add(Gb.MobileApp_fnames_disabled, device_entry.name_by_user) + + if Gb.MobileApp_device_fnames: + ha_started_check = True + log_debug_msg( f"Mobile App Integration Started-True" + f"Devices-{list_to_str(Gb.MobileApp_device_fnames)}") + + Gb.startup_lists['Gb.MobileApp_device_fnames'] = Gb.MobileApp_device_fnames + Gb.startup_lists['Gb.MobileApp_fnames_disabled'] = Gb.MobileApp_fnames_disabled + Gb.startup_lists['Gb.MobileApp_fnames_x_mobapp_id'] = \ + {k: v for k, v in Gb.MobileApp_fnames_x_mobapp_id.items()} + + if len(Gb.MobileApp_device_fnames) == Gb.conf_mobapp_device_cnt: + return True + + if len(Gb.MobileApp_device_fnames) == 0: + msg = f"Mobile App Integration Started-False > Temporary names assigned. " + if ha_started_check is False: + msg += f"Will recheck after HA is started" + post_event(msg) + return + + post_event(f"Mobile App Integration Started-True") + + # Cycle thru conf_devices and update the Device's mobapp_fname + mobapp_fname_update_msg = '' + + for conf_device in Gb.conf_devices: + conf_mobapp_dname = conf_device[CONF_MOBILE_APP_DEVICE] + if conf_mobapp_dname == 'None': + continue + + Device = Gb.Devices_by_devicename[conf_device[CONF_IC3_DEVICENAME]] + mobapp_id = Gb.mobapp_id_by_mobapp_dname.get(conf_mobapp_dname) + if mobapp_id: + if Device.conf_mobapp_fname is not None: + mobapp_fname_update_msg += (f"{CRLF_DOT}{Device.fname} > " + f"{Device.conf_mobapp_fname}{RARROW}" + f"{Gb.MobileApp_fnames_x_mobapp_id[mobapp_id]}") + Device.conf_mobapp_fname = Gb.MobileApp_fnames_x_mobapp_id[mobapp_id] + + if mobapp_fname_update_msg: + post_event(f"Mobile App Integration Name Assigned >{mobapp_fname_update_msg}") + + except Exception as err: + log_exception(err) + return False + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# DEVICE NOTIFY SERVICE HANDLERS +# +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + +def setup_notify_service_name_for_mobapp_devices(post_evlog_msg=False): + ''' + Get the MobApp device_tracker entities from the entity registry. Then cycle through the + Devices being tracked and match them up. Anything left over at the end is not matched and not monitored. + + Parameters: + post_evlog_msg - + Post an event msg indicating the notify device names were set up. This is done + when they are set up when this is run after HA has started + + ''' + mobile_app_notify_devicenames = get_mobile_app_notify_devicenames() + + setup_msg = '' + + # Cycle thru the ha notify names and match them up with a device. This function runs + # while iC3 is starting and again when ha has started. HA may run iC3 before + # 'notify.mobile_app' so running again when ha has started makes sure they are set up. + for mobile_app_notify_devicename in mobile_app_notify_devicenames: + mobapp_dname = mobile_app_notify_devicename.replace('mobile_app_', '') + for devicename, Device in Gb.Devices_by_devicename.items(): + if (Device.mobapp_monitor_flag is False + or Gb.conf_data_source_MOBAPP is False): + continue + + if instr(mobapp_dname, devicename) or instr(devicename, mobapp_dname): + if (Device.conf_mobapp_fname != 'None' + and Device.mobapp_monitor_flag + and Device.mobapp[NOTIFY] == ''): + Device.mobapp[NOTIFY] = mobile_app_notify_devicename + setup_msg+=(f"{CRLF_DOT}{Device.devicename_fname}{RARROW}" + f"{mobile_app_notify_devicename}") + break + + if setup_msg and post_evlog_msg: + post_event(f"Delayed MobApp Notifications Setup Completed > {setup_msg}") + +#-------------------------------------------------------------------- def send_message_to_device(Device, service_data): ''' Send a message to the device. An example message is: diff --git a/custom_components/icloud3/support/pyicloud_ic3.py b/custom_components/icloud3/support/pyicloud_ic3.py index ed89b4d..7617348 100644 --- a/custom_components/icloud3/support/pyicloud_ic3.py +++ b/custom_components/icloud3/support/pyicloud_ic3.py @@ -21,35 +21,40 @@ from ..global_variables import GlobalVariables as Gb from ..const import (AIRPODS_FNAME, NONE_FNAME, - EVLOG_NOTICE, EVLOG_ALERT, - HHMMSS_ZERO, RARROW, CRLF, CRLF_DOT, CRLF_STAR, CRLF_CHK, CRLF_HDOT, - FMF, FAMSHR, FMF_FNAME, FAMSHR_FNAME, NAME, ID, + EVLOG_NOTICE, EVLOG_ALERT, LINK, RLINK, LLINK, + HHMMSS_ZERO, RARROW, PDOT, CRLF, CRLF_DOT, CRLF_STAR, CRLF_CHK, CRLF_HDOT, + ICLOUD, NAME, ID, APPLE_SPECIAL_ICLOUD_SERVER_COUNTRY_CODE, ICLOUD_HORIZONTAL_ACCURACY, LOCATION, TIMESTAMP, LOCATION_TIME, DATA_SOURCE, ICLOUD_BATTERY_LEVEL, ICLOUD_BATTERY_STATUS, BATTERY_STATUS_CODES, BATTERY_LEVEL, BATTERY_STATUS, BATTERY_LEVEL_LOW, ICLOUD_DEVICE_STATUS, + CONF_USERNAME, CONF_APPLE_ACCOUNT, CONF_PASSWORD, CONF_MODEL_DISPLAY_NAME, CONF_RAW_MODEL, CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX, - CONF_IC3_DEVICENAME, CONF_FNAME, CONF_FAMSHR_DEVICENAME, CONF_FMF_EMAIL, - CONF_FAMSHR_DEVICE_ID, + CONF_IC3_DEVICENAME, CONF_FNAME, CONF_FAMSHR_DEVICENAME, + CONF_FAMSHR_DEVICE_ID, CONF_LOG_LEVEL_DEVICES, ) -from ..helpers.common import (instr, obscure_field, list_to_str, delete_file, encode_password, decode_password) -from ..helpers.time_util import (time_now_secs, secs_to_time, timestamp_to_time_utcsecs, +from ..helpers.common import (instr, is_empty, isnot_empty, list_add, encode_password, decode_password) +from ..helpers.file_io import (delete_file, read_json_file, save_json_file, ) +from ..helpers.time_util import (time_now, time_now_secs, secs_to_time, s2t, 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_rawdata_unfiltered) -#from .config_file import () + _evlog, _log, more_info, + log_info_msg, log_error_msg, log_debug_msg, log_warning_msg, + log_rawdata, log_exception, log_rawdata_unfiltered, filter_data_dict, ) +from ..support import pyicloud_srp as srp from uuid import uuid1 from requests import Session, adapters -from os import path, mkdir +from os import path from re import match +import hashlib +# import srp +import base64 import inspect import json -import traceback import http.cookiejar as cookielib import logging LOGGER = logging.getLogger(f"icloud3.pyicloud_ic3") @@ -67,22 +72,23 @@ APPLE_ID_VERIFICATION_CODE_INVALID_404 = 404 AUTHENTICATION_NEEDED_421_450_500 = [421, 450, 500] AUTHENTICATION_NEEDED_450 = 450 +CONNECTION_ERROR_503 = 503 -ICLOUD_ERROR_CODES = { +HTTP_RESPONSE_CODES = { 200: 'iCloud Server Responded', 204: 'Verification Code Accepted', 421: 'Verification Code May Be Needed', 450: 'Verification Code May Be Needed', 500: 'Verification Code May Be Needed', - 503: 'iCloud Server not Available', + 503: 'iCloud Server not Available (Connection Error)', 400: 'Invalid Verification Code', 403: 'Verification Code Requested', 404: 'iCloud http Error, Web Page not Found', 201: 'Device Offline', - -2: 'iCloud Server not Available', - 302: 'iCloud Server not Available', + -2: 'iCloud Server not Available (Connection Error)', + 302: 'iCloud Server not Available (Connection Error)', } -ICLOUD_ERROR_CODE_IDX = {str(code): code for code in ICLOUD_ERROR_CODES.keys()} +HTTP_RESPONSE_CODES_IDX = {str(code): code for code in HTTP_RESPONSE_CODES.keys()} ''' https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitWebServicesReference/ErrorCodes.html#//apple_ref/doc/uid/TP40015240-CH4-SW1 @@ -109,17 +115,25 @@ LOCK_SUCCESSFUL_2 = 2204 LOCK_FAIL_PASSCODE_NOT_SET_CONS_FAIL = 2403 LOCK_FAIL_NO_PASSCD_2 = 2406 -''' +app specific password notes +"appIdKey=ba2ec180e6ca6e6c6a542255453b24d6e6e5b2be0cc48bc1b0d8ad64cfe0228f&appleId=APPLE_ID&password=password2&protocolVersion=A1234&userLocale=en_US&format=plist" --header "application/x-www-form-urlencoded" "https://idmsa.apple.com/IDMSWebAuth/clientDAW.cgi" + +--data "appIdKey=ba2ec180e6ca6e6c6a542255453b24d6e6e5b2be0cc48bc1b0d8ad64cfe0228f&appleId=APPLE_ID&password=password2&protocolVersion=A1234&userLocale=en_US&format=plist" +--header "application/x-www-form-urlencoded" "https://idmsa.apple.com/IDMSWebAuth/clientDAW.cgi" +''' #-------------------------------------------------------------------- class PyiCloudPasswordFilter(logging.Filter): '''Password log hider.''' def __init__(self, password): super(PyiCloudPasswordFilter, self).__init__(password) - + self.filter_disabled_msg_displayed = False def filter(self, record): + # if self.filter_disabled_msg_displayed is False: + # self.filter_disabled_msg_displayed = True + # _log('PASSWORD FILTER DISABLED') # return True message = record.getMessage() if self.name in message: @@ -128,42 +142,62 @@ def filter(self, record): return True -#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + class PyiCloudSession(Session): '''iCloud session.''' - def __init__(self, Service): - self.Service = Service - self.response_status_code = 0 + def __init__(self, PyiCloud, validate_apple_acct=False): + self.setup_time = time_now() + self.PyiCloud = PyiCloud + self.username = PyiCloud.username + Gb.PyiCloudSession_by_username[self.username] = self + self.response_code = 0 self.response_ok = True + self.only_validate_apple_acct = validate_apple_acct super().__init__() # Increase the number of connections to prevent timeouts - # authenticting the iCloud Account + # authenticting the Apple Account adapter = adapters.HTTPAdapter(pool_connections=20, pool_maxsize=20) def request(self, method, url, **kwargs): # pylint: disable=arguments-differ - # callee.function and callee.lineno provice calling function and the line number callee = inspect.stack()[2] module = inspect.getmodule(callee[0]) request_logger = logging.getLogger(module.__name__).getChild("http") - if self.Service.password_filter not in request_logger.filters: - request_logger.addFilter(self.Service.password_filter) - - - has_retried = kwargs.get("retried", False) - kwargs.pop("retried", False) - retry_cnt = kwargs.get("retry_cnt", 0) - kwargs.pop("retry_cnt", 0) + try: + if (self.only_validate_apple_acct is False + and self.PyiCloud.password_filter not in request_logger.filters): + request_logger.addFilter(self.PyiCloud.password_filter) + + # if data is a str, unconvert it from json format, + # it will be reconverted to json later + if 'data' in kwargs and type(kwargs['data']) is str: + kwargs['data'] = json.loads(kwargs['data']) + retry_cnt = kwargs.get("retry_cnt", 0) + + log_rawdata_flag = (url.endswith('refreshClient') is False) + if Gb.log_rawdata_flag or log_rawdata_flag: + try: + log_hdr = ( f"{self.PyiCloud.username_base}, {method}, Request, " + f"{callee.function}/{callee.lineno}") + log_data = {'url': url[8:], 'retry': kwargs.get("retry_cnt", 0)} + log_data.update(kwargs) + log_rawdata(log_hdr, log_data, log_rawdata_flag=log_rawdata_flag) + + except Exception as err: + log_exception(err) + + kwargs.pop("retried", False) + kwargs.pop("retry_cnt", 0) + + if 'data' in kwargs and type(kwargs['data']) is dict: + kwargs['data'] = json.dumps(kwargs['data']) - if Gb.log_rawdata_flag: - log_msg = (f"{secs_to_time(time_now_secs())}, {method}, {url}, {self.prefilter_rawdata(kwargs)}") - log_rawdata(f"PyiCloud_ic3 iCloud Request, {self.Service.instance}, " - f"{callee.function}/{callee.lineno}", - {'raw': log_msg}) + except Exception as err: + log_exception(err) try: response = None @@ -172,9 +206,17 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ response = Session.request(self, method, url, **kwargs) #++++++++++++++++ REQUEST ICLOUD DATA +++++++++++++++ + + except ConnectionError as err: + self.PyiCloud.connection_error_retry_cnt += 1 + # log_exception(err) + self._raise_error(503, f"{HTTP_RESPONSE_CODES[503]}") + if response is None: + return + except Exception as err: - log_exception(err) - self._raise_error(-2, "Failed to establish a new connection") + # log_exception(err) + self._raise_error(-2, f"Other Error Setting up iCloud Server Connection ({err})") if response is None: return @@ -183,83 +225,89 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ try: data = response.json() - except: - data = None - self.response_status_code = response.status_code + except Exception as err: + # log_exception(err) + data = {} + + self.response_code = response.status_code + self.PyiCloud.response_code = response.status_code self.response_ok = response.ok - if Gb.log_rawdata_flag: - log_msg = ( f"ResponseCode-{response.status_code} ") + log_rawdata_flag = (url.endswith('refreshClient') is False) or response.status_code != 200 + if Gb.log_rawdata_flag or log_rawdata_flag: + log_hdr = ( f"{self.PyiCloud.username_base}, {method}, Response, " + f"{callee.function}/{callee.lineno} ") + log_data = {'code': response.status_code, 'ok': response.ok, 'data': data} - if (retry_cnt == 3 or response.status_code != 200 or response.ok is False): - log_msg += (f", ResponseOK-{response.ok}, Headers-{response.headers}") + if retry_cnt >= 2 or Gb.log_rawdata_flag_unfiltered: + log_data['headers'] = response.headers + logged = log_rawdata(log_hdr, log_data, log_rawdata_flag=log_rawdata_flag) - try: - if Gb.log_rawdata_flag_unfiltered: - log_rawdata_unfiltered(f"PyiCloud_ic3 iCloud Response-Header (Unfiltered), \ - {self.Service.instance}, {callee.function}/{callee.lineno} ", - {'raw': log_msg}) - log_rawdata_unfiltered(f"PyiCloud_ic3 iCloud Response-Data (Unfiltered), \ - {self.Service.instance}, {callee.function}/{callee.lineno}", - {'raw': data}) - - elif data and ('userInfo' in data is False or 'webservices' in data): - log_rawdata(f"PyiCloud_ic3 iCloud Response-Data, \ - {self.Service.instance}, {callee.function}/{callee.lineno} ", - {'filter': self.prefilter_rawdata(data)}) - except Exception as err: - # log_exception(err) - pass + # Validating the username/password, code=409 is valid, code=401 is invalid + if (response.status_code in [401, 409] + and instr(url, 'setup/authenticate/')): + return response.status_code - for header in HEADER_DATA: - if response.headers.get(header): - session_arg = HEADER_DATA[header] - self.Service.session_data.update({session_arg: response.headers.get(header)}) + for header_key, session_arg in HEADER_DATA.items(): + response_header_value = response.headers.get(header_key) + if response_header_value: + self.PyiCloud.session_data.update({session_arg: response_header_value}) - with open(self.Service.session_directory_filename, "w") as outfile: - json.dump(self.Service.session_data, outfile) + self.PyiCloud.session_data_token.update(self.PyiCloud.session_data) + save_json_file(self.PyiCloud.session_dir_filename, self.PyiCloud.session_data) + # cookie variable reference - self.cookies._cookies['.apple.com']['/']['acn01'].expires self.cookies.save(ignore_discard=True, ignore_expires=True) - if (response.ok is False - and (content_type not in json_mimetypes - or response.status_code in AUTHENTICATION_NEEDED_421_450_500)): - + if data and "webservices" in data: try: - # Handle re-authentication for Find My iPhone - fmip_url = self.Service._get_webservice_url("findme") - if retry_cnt == 0 and response.status_code in AUTHENTICATION_NEEDED_421_450_500 and fmip_url in url: - log_debug_msg(f"Re-authenticating iCloud Account ({response.status_code})") + self.PyiCloud.findme_url_root = data["webservices"]['findme']["url"] + self.PyiCloud._update_token_pw_file('findme_url', self.PyiCloud.findme_url_root) + except: + pass - try: - # If 450, authentication requires a sign in to the account - service = None if response.status_code == 450 else 'find' - self.Service.authenticate(True, service) + try: + if (response.ok is False + and (content_type not in json_mimetypes + or response.status_code in AUTHENTICATION_NEEDED_421_450_500)): - except PyiCloudAPIResponseException: - log_debug_msg("Re-authentication failed") + # Handle re-authentication for Find My iPhone + if (response.status_code in AUTHENTICATION_NEEDED_421_450_500 + and self.PyiCloud.findme_url_root + and url.startswith(self.PyiCloud.findme_url_root)): + + log_debug_msg( f"{self.PyiCloud.username_base}, " + f"Authenticating Apple Account ({response.status_code})") kwargs["retried"] = True retry_cnt += 1 kwargs['retry_cnt'] = retry_cnt - return self.request(method, url, **kwargs) - except Exception: - pass + try: + # If 421/450/503, retry sign in request + if retry_cnt <= 2: + self.PyiCloud.authenticate(refresh_session=True) - if retry_cnt == 0 and response.status_code in AUTHENTICATION_NEEDED_421_450_500: - self._log_debug_msg("AUTHENTICTION NEEDED, Status Code", response.status_code) + except PyiCloudAPIResponseException: + log_debug_msg(f"{self.username_base}, Authentication failed") + return self.request(method, url, **kwargs) - kwargs["retried"] = True - retry_cnt += 1 - kwargs['retry_cnt'] = retry_cnt + #if retry_cnt == 0 and response.status_code in AUTHENTICATION_NEEDED_421_450_500: + if (retry_cnt <= 2 + and response.status_code in [AUTHENTICATION_NEEDED_421_450_500]): + # CONNECTION_ERROR_503]): + self._log_debug_msg(f"{self.username_base}, " + f"AUTHENTICTION NEEDED, Code-{response.status_code}, " + f"RetryCnt-{retry_cnt}") - return self.request(method, url, **kwargs) + return self.request(method, url, **kwargs) - error_code, error_reason = self._resolve_error_code_reason(data) + error_code, error_reason = self._resolve_error_code_reason(data) - self._raise_error(response.status_code, error_reason) + self._raise_error(response.status_code, error_reason) + except Exception as err: + log_exception(err) if content_type not in json_mimetypes: return response @@ -275,6 +323,7 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ error_code, error_reason = self._resolve_error_code_reason(data) + if error_reason: self._raise_error(error_code, error_reason) @@ -321,11 +370,11 @@ def _raise_error(code, reason): api_error = PyiCloudServiceNotActivatedException(reason, code) elif code in AUTHENTICATION_NEEDED_421_450_500: #[204, 421, 450, 500]: - log_info_msg(f"iCloud Account Verification Code may be needed ({code})") + log_info_msg(f"Apple Account Verification Code may be needed ({code})") return elif reason == 'Missing X-APPLE-WEBAUTH-TOKEN cookie': - log_info_msg(f"iCloud Account Verification Code may be needed, No WebAuth Token") + log_info_msg(f"Apple Account Verification Code may be needed, No WebAuth Token") return # api_error = PyiCloud2FARequiredException() @@ -343,8 +392,8 @@ def _raise_error(code, reason): elif code == 404: reason = f"iCloud Web Page not Found ({code})" - elif code == -2: - reason = f"Could not connect to iCloud Location Servers ({code})" + elif code == 503: + reason = f"{HTTP_RESPONSE_CODES[503]}" log_info_msg(reason) return @@ -368,41 +417,125 @@ def _log_debug_msg(title, display_data): log_debug_msg(f"{title} -- None") #------------------------------------------------------------------ - @staticmethod - def prefilter_rawdata(kwargs_json): + def _shrink_items(self, prefiltered_dict): ''' Obscure account name and password in rawdata ''' - if kwargs_json is None: - return None + + if (prefiltered_dict is None + or type(prefiltered_dict) is not dict + or 'data' not in prefiltered_dict + or type(prefiltered_dict['data']) is not dict): + return prefiltered_dict try: - # if 'data' not in kwargs_json: - # return kwargs_json + filtered_dict = prefiltered_dict.copy() + prefiltered_data = prefiltered_dict['data'] + filtered_data = prefiltered_data.copy() + + if 'trustTokens' in prefiltered_data: + filtered_data['trustTokens'] = self._shrink(prefiltered_data['trustTokens']) + if 'trustToken' in prefiltered_data: + filtered_data['trustToken'] = self._shrink(prefiltered_data['trustToken']) + if 'dsWebAuthToken' in prefiltered_data: + filtered_data['dsWebAuthToken'] = self._shrink(prefiltered_data['dsWebAuthToken']) + if 'a' in prefiltered_data: + filtered_data['a'] = self._shrink(prefiltered_data['a']) - kwargs_dict = json.loads(kwargs_json['data']) - if 'password' in kwargs_dict: kwargs_dict['password'] = obscure_field(kwargs_dict['password']) - if 'accountName' in kwargs_dict: kwargs_dict['accountName'] = obscure_field(kwargs_dict['accountName']) - if 'trustTokens' in kwargs_dict: kwargs_dict['trustTokens'] = '... ...' - if 'trustToken' in kwargs_dict: kwargs_dict['trustToken'] = '... ...' - if 'dsWebAuthToken' in kwargs_dict: kwargs_dict['dsWebAuthToken'] = '... ...' - kwargs_json = json.dumps(kwargs_dict) + # filtered_dict = prefiltered_dict.copy() + filtered_dict['data'] = filtered_data + + return filtered_dict except Exception as err: - #log_exception(err) + log_exception(err) pass - return kwargs_json + return prefiltered_dict + + @property + def username_base(self): + return self.PyiCloud.username_base + +#------------------------------------------------------------------ + @staticmethod + def _shrink(value): + return f"{value[:6]}………{value[-6:]}" #------------------------------------------------------------------ - async def _async_session_request(self, method, url, **kwargs): - return await Gb.hass.async_add_executor_job( - Session.request, - self, - method, - url, - **kwargs) + # async def _session_request(self, method, url, **kwargs): + # return await Gb.hass.async_add_executor_job( + # Session.request, + # self, + # method, + # url, + # **kwargs) + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# CHECK APPLE ACCOUNT USERNAME/PASSWORD +# +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + +class PyiCloudValidateAppleAcct(): + ''' + Use the Apple Acct Validation url to validate the username/password + ''' + + def __init__(self): + self.validate_apple_acct = True + self.username = None + self.password = None + self.connection_error_retry_cnt = 0 + + self.PyiCloudSession = PyiCloudSession(self, validate_apple_acct=True) + +#---------------------------------------------------------------------------- + def validate_username_password(self, username, password): + ''' + Check if the username and password are still valid + The response code indicates the validity status (checked in the Session module) + code=409 is valid + code=401 is invalid + ''' + + + self.username = username + self.password = password + + self.session_data = '' #Dummy statement for PyiCloudSession + self.password_filter = '' #Dummy statement for PyiCloudSession + self.instance = '' #Dummy statement for PyiCloudSession + + self.username_base = f"{self.username}@".split('@')[0] + log_debug_msg(f"{self.username_base}, Checking Username/Password validity") + + username_password = f"{username}:{password}" + upw = username_password.encode('ascii') + username_password_b64 = base64.b64encode(upw) + username_password_b64 = username_password_b64.decode('ascii') + + headers = { + 'Content-Type': 'application/json', + 'User-Agent': 'Apple-iCloud/9.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/605.1.15 (KHTML, like Gecko)', + 'Authorization': f"Basic {username_password_b64}"} + url = f"https://setup.icloud.com/setup/authenticate/{self.username}" + data = None + + try: + response_code = self.PyiCloudSession.post(url, data=data, headers=headers) + + result_valid = True if response_code == 409 else False + log_info_msg( f"{self.username_base}, Validate Username/Password Results, " + f"Valid-{result_valid}") + + return result_valid + + except Exception as err: + log_exception(err) + log_debug_msg(f"Validate Username/Password ({self.username_base})Error ({err})") + return False #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> @@ -427,105 +560,91 @@ class PyiCloudService(): pyicloud.iphone.location() ''' - def __init__( self, apple_id, password=None, - cookie_directory=None, session_directory=None, + def __init__( self, username, password=None, + locate_all_devices=None, + cookie_directory=None, + session_directory=None, endpoint_suffix=None, - verify=True, client_id=None, with_family=True, - instance='notset', - verify_password=False, - request_verification_code=False): + verify_login=False, + config_flow_login=False): - if not apple_id: - msg = "Apple iCloud account username is not specified/432" + if is_empty(username): + msg = "Apple Account username is not specified/558" Gb.authenticate_method = 'Invalid username/password' raise PyiCloudFailedLoginException(msg) - if not password: - msg = "Apple iCloud account password is not specified/435" + if is_empty(password): + msg = "Apple Account password is not specified/562" Gb.authenticate_method = 'Invalid username/password' raise PyiCloudFailedLoginException(msg) - self.user = {"accountName": apple_id, "password": password} - self.apple_id = apple_id - self.username = apple_id + self.setup_time = time_now() + self.user = {"accountName": username, "password": password} + self.apple_id = username + self.username = username + self.username_base = username.split('@')[0] + self.username_base6 = self.username_base if Gb.log_debug_flag else f"{username[:6]}…" + + username_password = f"{username}:{password}" + upw = username_password.encode('ascii') + username_password_b64 = base64.b64encode(upw) + self.username_password_b64 = username_password_b64.decode('ascii') + self.password = password - self.is_authenticated = False # ICloud access has been authenticated via password or token self.requires_2sa = self._check_2sa_needed + + self.locate_all_devices = False if locate_all_devices is False else True + self.is_authenticated = False # ICloud access has been authenticated via password or token + # self.requires_2sa = self._check_2sa_needed self.requires_2fa = False # This is set during the authentication function + self.response_code_pwsrp_err = 0 + self.response_code = 0 + self.token_pw_data = {} self.token_password = password self.account_locked = False # set from the locked data item when authenticating with a token self.account_name = '' - self.verify_password = verify_password + self.verify_login = verify_login + self.verification_code = None + self.authentication_alert_displayed_flag = False self.update_requested_by = '' self.endpoint_suffix = endpoint_suffix if endpoint_suffix else Gb.icloud_server_endpoint_suffix - self.instance = instance # Module that created this PyiCloud object (initial, startup, config) - - self.HOME_ENDPOINT = f"https://www.icloud.com" - self.SETUP_ENDPOINT = f"https://setup.icloud.com/setup/ws/1" - self.AUTH_ENDPOINT = f"https://idmsa.apple.com/appleauth/auth" + self.config_flow_login = config_flow_login # Indicates this PyiCloud object is beinging created from config_flow + + self.cookie_directory = cookie_directory or Gb.icloud_cookie_directory + self.session_directory = session_directory or Gb.icloud_session_directory + self.cookie_filename = "".join([c for c in self.username if match(r"\w", c)]) + self.session_data = {} + self.session_data_token = {} + self.dsid = '' + self.trust_token = '' + self.session_token = '' + self.session_id = '' + self.connection_error_retry_cnt = 0 + + self.findme_url_root = None # iCloud url initialized from the accountLogin response data + self.HOME_ENDPOINT = "https://www.icloud.com" + self.SETUP_ENDPOINT = "https://setup.icloud.com/setup/ws/1" + self.AUTH_ENDPOINT = "https://idmsa.apple.com/appleauth/auth" + #self.AUTH_PASSWORD_ENDPOINT = "https://setup.icloud.com/setup/authenticate" - # if Gb.icloud_server_endpoint_suffix in APPLE_SPECIAL_ICLOUD_SERVER_COUNTRY_CODE: if self.endpoint_suffix in APPLE_SPECIAL_ICLOUD_SERVER_COUNTRY_CODE: self._setup_url_endpoint_suffix() - try: - if 'Complete' in self.init_step_complete: - return self - except: - self._initialize_variables() - - if 'Setup' in self.init_step_needed: - self._set_step_inprocess('Setup') - self._setup_password_filter(password) - self._setup_cookie_files(cookie_directory) - self._setup_PyiCloudSession(session_directory) - self._set_step_completed('Setup') - - if instance == 'config': - Gb.PyiCloudConfigFlow = self - elif instance == 'initial': - Gb.PyiCloudInit = self - else: - Gb.PyiCloudInit = self - Gb.PyiCloud = self - - if 'Cancel' in self.init_step_complete: - return - - if 'Authenticate' in self.init_step_needed: - post_monitor_msg(f"AUTHENTICATING iCloud Account Access, {self.account_name} " - f"({obscure_field(apple_id)}), {instance}") - self._set_step_inprocess('Authenticate') - self.authenticate() - self._set_step_completed('Authenticate') - - # config_flow is requesting a new verification code. Do not have to load FamShr & FmF data - if request_verification_code: - return - if 'Cancel' in self.init_step_complete: - return - - if 'FamShr' in self.init_step_needed: - self._set_step_inprocess('FamShr') - - self.create_FamilySharing_object() - self._set_step_completed('FamShr') - - # if 'FmF' in self.init_step_needed: - # self._set_step_inprocess('FmF') - # self.create_FindMyFriends_object() - # self._set_step_completed('FmF') + self.PyiCloudSession = None + self.DeviceSvc = None # PyiCloud_ic3 object for Apple Device Service used to refresh the device's location - if self.init_step_needed == []: - self._set_step_completed('Complete') + self._initialize_variables() + self._setup_password_filter(password) + self._setup_PyiCloudSession() -#---------------------------------------------------------------------------- - def _set_step_inprocess(self, step): - self.init_step_needed.remove(step) - self.init_step_inprocess = step + Gb.PyiCloudLoggingInto = self # Identifies a partial login that failed + Gb.PyiCloud_by_username[username] = self + self.authenticate() + # if self.DeviceSvc is None: + # self.create_DeviceSvc_object() + self.create_DeviceSvc_object() + if self.DeviceSvc: + self.DeviceSvc.refresh_client() - def _set_step_completed(self, step): - self.init_step_inprocess = '' - if step not in self.init_step_needed: - self.init_step_complete.append(step) + return #---------------------------------------------------------------------------- def _initialize_variables(self): @@ -533,41 +652,81 @@ def _initialize_variables(self): Initialize the PyiCloud variables ''' - log_info_msg(f"Initialize PyiCloud Service, establish iCloud Location Services connection") + log_info_msg(f"{self.username_base}, Initialize PyiCloud Service, Set up iCloud Location Services connection") self.data = {} self.client_id = f"auth-{str(uuid1()).lower()}" - self.params = { "clientBuildNumber": "2021Project52", - "clientMasteringNumber": "2021B29", - "ckjsBuildVersion": "17DProjectDev77", - "clientId": self.client_id[5:], } + self.params = { "clientBuildNumber": "2021Project52", + "clientMasteringNumber": "2021B29", + "ckjsBuildVersion": "17DProjectDev77", + "clientId": self.client_id[5:], } - self.with_family = True self.new_2fa_code_already_requested_flag = False + self.last_refresh_secs = time_now_secs() + self.authentication_cnt = 0 + self.last_authenticated_secs = 0 + self.location_update_cnt = 0 # PyiCloud tracking method and raw data control objects - self.FamilySharing = None # PyiCloud_ic3 object for FamilySharig used to refresh the device's location - self.FindMyFriends = None # PyiCloud_ic3 object for FindMyFriends used to refresh the device's location - self.RawData_by_device_id = {} # Device data for tracked devices, updated in Pyicloud famshr.refresh_client - self.RawData_by_device_id_famshr = {} - self.RawData_by_device_id_fmf = {} + self.RawData_by_device_id = {} # Device data for tracked devices, updated in Pyicloud icloud.refresh_client + self.RawData_items = [] # List of all RawData objects used to find non-tracked device data - # FamShr Device information - These is used verify the device, display on the EvLog and in the Config Flow + # iCloud Device information - These is used verify the device, display on the EvLog and in the Config Flow # device selection list on the iCloud3 Devices screen - self.device_id_by_famshr_fname = {} # Example: {'Gary-iPhone': 'n6ofM9CX4j...'} - self.famshr_fname_by_device_id = {} # Example: {'n6ofM9CX4j...': 'Gary-iPhone14'} - self.device_info_by_famshr_fname = {} # Example: {'Gary-iPhone': 'Gary-iPhone (iPhone 14 Pro; iPhone15,2)'} + self.device_id_by_icloud_dname = {} # Example: {'Gary-iPhone': 'n6ofM9CX4j...'} + self.icloud_dname_by_device_id = {} # Example: {'n6ofM9CX4j...': 'Gary-iPhone14'} + self.device_info_by_icloud_dname = {} # Example: {'Gary-iPhone': 'Gary-iPhone (iPhone 14 Pro; iPhone15,2)'} + self.device_model_name_by_icloud_dname= {} # Example: {'Gary-iPhone': 'iPhone 14 Pro'} self.device_model_info_by_fname = {} # {'Gary-iPhone': [raw_model, model, model_display_name]} - self.dup_famshr_fname_cnt = {} # Used to create a suffix for duplicate devicenames + self.dup_icloud_dname_cnt = {} # Used to create a suffix for duplicate devicenames # {'Gary-iPhone': ['iPhone15,2', 'iPhone', 'iPhone 14 Pro']} - self.init_step_needed = ['Setup', 'Authenticate', 'FamShr'] - # self.init_step_needed = ['Setup', 'Authenticate', 'FamShr', 'FmF'] - self.init_step_complete = [] - self.init_step_inprocess = '' +#--------------------------------------------------------------------------- + @property + def is_DeviceSvc_setup_complete(self): + return (self.findme_url_root is not None) + +#--------------------------------------------------------------------------- + @property + def account_owner_username(self): + if self.account_name: + return f"{self.account_name} ({self.username_base})" + + return f"{self.username_base6}" + + @property + def account_owner(self): + name = self.account_name or self.username_base6 + return f"{name}" + + @property + def account_owner_short(self): + if len(self.account_owner) <= 30: + return self.account_owner + else: + return f"{self.account_owner[:30]}…" + + @property + def account_owner_link(self): + name = self.account_name or self.username_base6 + return f"{LINK}{name}{RLINK}" + +#---------------------------------------------------------------------------- + @property + def primary_apple_account(self): + ''' + The primary Apple account is the first username in the iCloud3 + configuration file. It will not have the username as the iCloud + parameter (Gary-iPhone). A secondary Apple account will have it + specified (lillian@email:Gare-iPlone) + + Return: + True - This is the primary Apple Account' PyiCloud object + ''' + return Gb.conf_apple_accounts[0][CONF_USERNAME] == self.username #---------------------------------------------------------------------------- - def authenticate(self, refresh_session=False, service=None): + def authenticate(self, refresh_session=False): ''' Handles authentication, and persists cookies so that subsequent logins will not cause additional e-mails from Apple. @@ -575,7 +734,7 @@ def authenticate(self, refresh_session=False, service=None): login_successful = False self.authenticate_method = "" - this_fct_error_flag = True + self.response_code_pwsrp_err = 0 # Do not reset requires_2fa flag on a reauthenticate session # It may have been set on first authentication @@ -586,112 +745,141 @@ def authenticate(self, refresh_session=False, service=None): # Validate token - Consider authenticated if token is valid (POST=validate) if (refresh_session is False - and self.session_data.get("session_token") + and self.session_data.get('session_token') and 'dsid' in self.params): - log_info_msg("Checking session token validity") + log_info_msg(f"{self.username_base}, Checking session token validity") if self._validate_token(): login_successful = True self.authenticate_method += ", Token" - # Authenticate with Service - if login_successful is False and service != None: - app = self.data["apps"][service] - - if "canLaunchWithOneFactor" in app and app["canLaunchWithOneFactor"] == True: - log_debug_msg( f"AUTHENTICATING iCloud Account Access, " - f"{self.account_name} " - f"({obscure_field(self.user['accountName'])}), " - f"Service-{service}") - - if self._authenticate_with_password_service(service): - login_successful = True - self.authenticate_method += (f", Password") - - # Authenticate - Sign into icloud account (POST=/signin) + # Authenticate - Sign into Apple Account (POST=/signin) if login_successful is False: - info_msg = (f"Authenticating account {self.account_name} " - f"({obscure_field(self.user['accountName'])}) with token") - #if self.endpoint_suffix != '': + info_msg = (f"{self.username_base}, Authenticating with Token") + if self.endpoint_suffix: info_msg += f", iCloudServerCountrySuffix-'{self.endpoint_suffix}' " log_info_msg(info_msg) - if self.verify_password is False: - # Verify that the Token is still valid, if it is we are done - if self._authenticate_with_token(): - self.authenticate_method += ", Token" - login_successful = True + # Verify that the Token is still valid, if it is we are done + if self._authenticate_with_token(): + self.authenticate_method += ", Token" + login_successful = True + + if login_successful is False: + log_info_msg(f"{self.username_base}, Authenticating with Password SRP") + login_successful = self._authenticate_with_password_srp() + self.response_code_pwsrp_err = self.response_code + if login_successful: + self.authenticate_method += ", Password" - if login_successful is False or self.verify_password: - self._authenticate_with_password() - self.authenticate_method += ", Password" + if login_successful is False: if self._authenticate_with_token(): login_successful = True - self.authenticate_method += "+Token" + self.authenticate_method += ", Token" if login_successful is False: + err_msg = f"{self.username_base}, Authentication Failed, " if self.response_code == 302: - info_msg( f"iCloud Authentication Failed > " - f"iCloud Server Connection Error, Error={self.response_code}") + err_msg += f"iCloud Server Connection Error, " + elif self.response_code == 401: + username_password_valid = \ + Gb.PyiCloudValidateAppleAcct.validate_username_password( + self.username, self.password) + if username_password_valid: + err_msg += ("Python SRP Library Credentials Error. The Python " + "module that creates the Secure Remote Password hash key " + "has calculated an incorrect value for a valid " + "Username/Password. Try changing the Password to see " + "if the Apple Acct can be logged into. ") + else: + err_msg += "Authentication error, Invalid Username or Password, " + elif self.response_code == 503 or self.response_code_pwsrp_err == 503: + err_msg += ("Connection Error, Secure Password Validation Data " + "was not returned from Apple. ") + self.response_code = 503 + list_add(Gb.username_pyicloud_503_connection_error, self.username) + + elif self.response_code == 421: + err_msg += "Account will be Reauthenticated and login will continue, " else: - info_msg = (f"iCloud Authentication Failed > " - f"Username or Password is not valid, " - f"Error-{self.response_code}") - log_info_msg(info_msg) - raise PyiCloudFailedLoginException(info_msg) + err_msg += "An unknown error occurred, " - self.requires_2fa = self.requires_2fa or self._check_2fa_needed + if self.response_code not in [200, 409]: + err_msg += f"ErrorCode-{self.response_code}" - self._update_token_password_file() + log_info_msg(err_msg) + raise PyiCloudFailedLoginException(err_msg) + self.requires_2fa = self.requires_2fa or self._check_2fa_needed self.authenticate_method = self.authenticate_method[2:] + + self._update_token_pw_file(CONF_PASSWORD, encode_password(self.token_password)) + + log_info_msg( f"{self.username_base}, " + f"Authentication Successful, {self.username_base}" + f"Method-{self.authenticate_method}") + # self.authenticate_method = f"{self.account_owner_short}, {self.authenticate_method}" self.is_authenticated = self.is_authenticated or login_successful - log_info_msg(f"Authentication completed successfully, method-{self.authenticate_method}") #---------------------------------------------------------------------------- def _authenticate_with_token(self): '''Authenticate using session token. Return True if successful.''' this_fct_error_flag = True - data = {"accountCountryCode": self.session_data.get("account_country"), - "dsWebAuthToken": self.session_data.get("session_token"), - "extended_login": True, - "trustToken": self.session_data.get("trust_token", ""), - } + + if "account_country" in self.session_data_token: + data = {"accountCountryCode": self.session_data_token.get("account_country"), + "dsWebAuthToken": self.session_data_token.get("session_token"), + "extended_login": True, + "trustToken": self.session_data_token.get("trust_token", ""), + "appName": "iCloud3"} + + else: + log_debug_msg( f"{self.username_base}, " + f"Authenticate with Token > Failed, Invalid Session Data") + return False try: - req = self.Session.post(f"{self.SETUP_ENDPOINT}/accountLogin" - f"?clientBuildNumber=2021Project52&clientMasteringNumber=2021B29" - f"&clientId={self.client_id[5:]}", - data=json.dumps(data)) + url = f"{self.SETUP_ENDPOINT}/accountLogin" + + response = self.PyiCloudSession.post(url, params=self.params, data=data) - self.data = req.json() + self.data = response.json() if 'dsInfo' in self.data: + if 'dsid' in self.data['dsInfo']: + self.params['dsid'] = self.dsid = str(self.data['dsInfo']['dsid']) + self._update_token_pw_file('dsid', self.params) + if 'fullName' in self.data['dsInfo']: - self.account_name = self.data['dsInfo']['fullName'] + self.account_name = self.data['dsInfo']['fullName'].replace(' ', '') self.account_locked = self.data['dsInfo']['locked'] - if 'webservices' not in self.data: - if (self.data.get('success', False) is False - or self.data.get('error', 1) == 1): - return False + if 'webservices' in self.data: + try: + if self.is_DeviceSvc_setup_complete is False: + self.findme_url_root = data['webservices']['findme']['url'] + self._update_token_pw_file('findme_url', self.findme_url_root) + except: + pass - self._webservices = self.data["webservices"] - self._update_dsid(self.data) + elif (self.data.get('success', False) is False + or self.data.get('error', 1) == 1): + return False - log_debug_msg( f"Authenticate.authenticate_with_token > Successful") + log_debug_msg( f"{self.username_base}, Authenticate with Token > Successful") return True except PyiCloudAPIResponseException as err: - log_debug_msg( f"PyiCloudAPIResponseException.authenticate_with_token > " - f"Token is not valid, " - f"error-{err}, 2fa Needed-{self.requires_2fa}") + log_debug_msg( f"{self.username_base}, " + f"Authenticate with Token > Token is not valid, " + f"Error-{err}, 2fa Needed-{self.requires_2fa}") return False except Exception as err: + log_exception(err) if this_fct_error_flag is False: log_exception(err) return @@ -705,156 +893,202 @@ def _authenticate_with_password(self): ''' Sign into Apple account with password - Return - True - No errors, + Return: + True - Successful login + False - Invalid Password or other error ''' - this_fct_error_flag = True - data = dict(self.user) - data["rememberMe"] = True - data["trustTokens"] = [] - if self.session_data.get("trust_token"): - data["trustTokens"] = [self.session_data.get("trust_token")] - headers = self._get_auth_headers() - if self.session_data.get("scnt"): headers["scnt"] = self.session_data.get("scnt") - if self.session_data.get("session_id"): headers["X-Apple-ID-Session-Id"] = self.session_data.get("session_id") + url = f"{self.AUTH_ENDPOINT}/signin" + params = {"isRememberMeEnabled": "true"} + data = {"accountName": self.username, + "password": self.password, + "rememberMe": True, + "trustTokens": []} + if self.session_data.get("trust_token"): + data["trustTokens"] = [self.session_data_token.get("trust_token")] + try: - response = self.Session.post( - f"{self.AUTH_ENDPOINT}/signin", - params={"isRememberMeEnabled": "true"}, - data=json.dumps(data), - headers=headers,) - data = response.json - log_debug_msg( f"Authenticate.authenticate_with_password > Successful") + response = self.PyiCloudSession.post(url, params=params, data=data, headers=headers,) + + data = response.json() + + log_debug_msg( f"{self.username_base}, Authenticate with password > Successful") return True except PyiCloudAPIResponseException as err: - log_debug_msg( f"PyiCloudAPIResponseException.authenticate_with_password > " - f"Password is not valid, " + log_debug_msg( f"{self.username_base}, " + f"Authenticate with password > Failed, Password is not valid, " f"Error-{err}, 2fa Needed-{self.requires_2fa}") raise PyiCloudFailedLoginException() except Exception as err: - log_debug_msg( f"PyiCloudAPIResponseException.authenticate_with_password > " + log_debug_msg( f"{self.username_base}, " + f"Authenticate with password > Failed, " f"Other Error, {err}") - # log_exception(err) + log_exception(err) return False return False #---------------------------------------------------------------------------- - def _authenticate_with_password_service(self, service): - '''Authenticate to a specific service using credentials.''' - this_fct_error_flag = True - data = {"appName": service, - "apple_id": self.user["accountName"], - "password": self.user["password"], - "accountCountryCode": self.session_data.get("account_country"), - "dsWebAuthToken": self.session_data.get("session_token"), - "extended_login": True, - "trustToken": self.session_data.get("trust_token", ""), - } + def _authenticate_with_password_srp(self): + ''' + Sign into Apple account with password via Secure Remote Password verification - try: - log_debug_msg(f"Authenticating Service with Password, Service-{service}") + Return: + True - Successful login + False - Invalid Password or other error + ''' + #return self._authenticate_with_password() - self.Session.post(f"{self.SETUP_ENDPOINT}/accountLogin" - f"?clientBuildNumber=2021Project52&clientMasteringNumber=2021B29" - f"&clientId={self.client_id[5:]}", - data=json.dumps(data)) + class SrpPassword(): + def __init__(self, password: str): + self.password = password - log_debug_msg( f"Authenticate.authenticate_with_password_service > Successful") - return self._validate_token() + def set_encrypt_info(self, salt: bytes, iterations: int, key_length: int): + self.salt = salt + self.iterations = iterations + self.key_length = key_length - # return True + def encode(self): + password_hash = hashlib.sha256(self.password.encode('utf-8')).digest() + return hashlib.pbkdf2_hmac('sha256', password_hash, self.salt, self.iterations, self.key_length) - except PyiCloudAPIResponseException as err: - log_debug_msg( f"PyiCloudAPIResponseException.authenticate_with_password_service > " - f"Password is not valid, " - f"error-{err}, 2fa Needed-{self.requires_2fa}") - return False + srp_password = SrpPassword(self.password) + srp.rfc5054_enable() + srp.no_username_in_x() + usr = srp.User(self.username, srp_password, hash_alg=srp.SHA256, ng_type=srp.NG_2048) - log_exception(err) - msg = "Authenticate Request Failed744" - raise PyiCloudFailedLoginException(msg, err) + srp_username, A = usr.start_authentication() + + url = f"{self.AUTH_ENDPOINT}/signin/init" + data = {'accountName': srp_username, + 'a': base64.b64encode(A).decode(), + 'protocols': ['s2k', 's2k_fo']} + headers = self._get_auth_headers() + if self.session_data.get("scnt"): + headers["scnt"] = self.session_data.get("scnt") + if self.session_id: + headers["X-Apple-ID-Session-Id"] = self.session_id + + try: + log_info_msg(f"{self.username_base}, Authenticating with Password SRP, Send Credentials") + response = self.PyiCloudSession.post(url, data=data, headers=headers) + # response.raise_for_status() + + except PyiCloudAPIResponseException as error: + msg = "SRP Authentication Failed to start" + raise PyiCloudFailedLoginException(msg, error) from error + + try: + data = response.json() except Exception as err: - log_debug_msg( f"PyiCloudAPIResponseException.authenticate_with_password_service > " - f"Other Error, {err}") + data = {} # log_exception(err) + + if 'salt' not in data: return False - return False + salt = base64.b64decode(data['salt']) + b = base64.b64decode(data['b']) + c = data['c'] + iterations = data['iteration'] + key_length = 32 + srp_password.set_encrypt_info(salt, iterations, key_length) + + m1 = usr.process_challenge( salt, b ) + m2 = usr.H_AMK + + url = f"{self.AUTH_ENDPOINT}/signin/complete" + data = { + "accountName": srp_username, + "c": c, + "m1": base64.b64encode(m1).decode(), + "m2": base64.b64encode(m2).decode(), + "rememberMe": True, + "trustTokens": [self.session_data.get("trust_token", "")] + } + if 'trust_token' in self.session_data: + self.trust_token = self.session_data['trust_token'] + + params = {"isRememberMeEnabled": "true"} + + try: + log_info_msg(f"{self.username_base}, Authenticating with Password SRP, Verify Credentials") + response = self.PyiCloudSession.post(url, params=params, data=data, headers=headers, ) + + except PyiCloudAPIResponseException as error: + self.response_code = 401 #Authentication Error, invalid username/password + + return response.status_code in [200, 409] + #---------------------------------------------------------------------------- def _validate_token(self): '''Checks if the current access token is still valid.''' - log_debug_msg("Checking session token validity") + log_debug_msg(f"{self.username_base}, Checking session token validity") + + url = f"{self.SETUP_ENDPOINT}/validate" + data = "null" try: - response = self.Session.post("%s/validate" % self.SETUP_ENDPOINT, data="null") + response = self.PyiCloudSession.post(url, data=data) + self.data = response.json self.requires_2fa = self.requires_2fa or self._check_2fa_needed - log_debug_msg(f"Session token is still valid, 2fa Needed-{self.requires_2fa}") + log_debug_msg( f"{self.username_base}, " + f"Session token is still valid, 2fa Needed-{self.requires_2fa}") return True except PyiCloudAPIResponseException as err: - log_debug_msg( f"PyiCloudAPIResponseException.validate_token > " + log_debug_msg( f"{self.username_base}, " f"Token is not valid, " f"Error-{err}, 2fa Needed-{self.requires_2fa}") - return False except Exception as err: - log_debug_msg( f"PyiCloudAPIResponseException.validate_token > " - f"Other Error, {err}") - # log_exception(err) - return False + log_debug_msg( f"{self.username_base}, " + f"Error encountered validating token > " + f"Error, {err}") + log_exception(err) return False -#---------------------------------------------------------------------------- - def _update_dsid(self, data): - try: - # check self.data returned and contains dsid - if 'dsInfo' in data: - if 'dsid' in data['dsInfo']: - self.params["dsid"]= str(data["dsInfo"]["dsid"]) - else: - # if no dsid given delete it from self.params - until returned. - # Otherwise is passing default incorrect dsid - if 'dsid' in self.params: - self.params.pop("dsid") - - except: - log_debug_msg(u"Error setting dsid field.") - # if error, self.data None/empty delete - if 'dsid' in self.params: - self.params.pop("dsid") - - return - #---------------------------------------------------------------------------- def _get_auth_headers(self, overrides=None): - headers = { "Accept": "*/*", - "Content-Type": "application/json", - "X-Apple-OAuth-Client-Id": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", - "X-Apple-OAuth-Client-Type": "firstPartyAuth", - "X-Apple-OAuth-Redirect-URI": "https://www.icloud.com", - "X-Apple-OAuth-Require-Grant-Code": "true", - "X-Apple-OAuth-Response-Mode": "web_message", - "X-Apple-OAuth-Response-Type": "code", - "X-Apple-OAuth-State": self.client_id, - "X-Apple-Widget-Key": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", - } - + # headers = { "Accept": "*/*", + # "Content-Type": "application/json", + # "X-Apple-OAuth-Client-Id": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", + # "X-Apple-OAuth-Client-Type": "firstPartyAuth", + # "X-Apple-OAuth-Redirect-URI": "https://www.icloud.com", + # "X-Apple-OAuth-Require-Grant-Code": "true", + # "X-Apple-OAuth-Response-Mode": "web_message", + # "X-Apple-OAuth-Response-Type": "code", + # "X-Apple-OAuth-State": self.client_id, + # "X-Apple-Widget-Key": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", + # } + headers = { + "Accept": "application/json, text/javascript", + "Content-Type": "application/json", + 'referer':'https://www.apple.com/', + "X-Apple-OAuth-Client-Id": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", + "X-Apple-OAuth-Client-Type": "firstPartyAuth", + "X-Apple-OAuth-Redirect-URI": "https://www.icloud.com", + "X-Apple-OAuth-Require-Grant-Code": "true", + "X-Apple-OAuth-Response-Mode": "web_message", + "X-Apple-OAuth-Response-Type": "code", + "X-Apple-OAuth-State": self.client_id, + "X-Apple-Widget-Key": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", + } if overrides: headers.update(overrides) return headers @@ -862,72 +1096,84 @@ def _get_auth_headers(self, overrides=None): #---------------------------------------------------------------------------- def _setup_password_filter(self, password): ''' - Set up the password_filter, cookies and session files + Set up the password_filter ''' + # if self.only_validate_apple_acct is False: + # return self.password_filter = PyiCloudPasswordFilter(password) LOGGER.addFilter(self.password_filter) Gb.iC3Logger.addFilter(self.password_filter) #---------------------------------------------------------------------------- - def _setup_cookie_files(self, cookie_directory): + def _setup_PyiCloudSession(self): ''' Set up the password_filter, cookies and session files ''' - self.cookie_directory = path.expanduser(path.normpath(cookie_directory)) + # If password was changed, delete the session file to generate a new 6-digit + # verification code from Apple when the new session is created + self._read_token_pw_file() + self._update_token_pw_file(CONF_USERNAME, self.username) + if self.password != self.token_password: + delete_file(self.session_dir_filename) - if not path.exists(self.cookie_directory): - mkdir(self.cookie_directory) + try: + self.session_data = {} -#---------------------------------------------------------------------------- - def _setup_PyiCloudSession(self, session_directory): - ''' - Set up the password_filter, cookies and session files - ''' + self.session_data = read_json_file(self.session_dir_filename) - self.cookie_directory = path.expanduser(path.normpath(self.cookie_directory)) - self.session_directory = session_directory + if self.session_data != {}: + self.session_data_token.update(self.session_data) - if not path.exists(self.cookie_directory): - mkdir(self.cookie_directory) + # If this username is being opened again with another password, a new PyiCloud + # object is being created to verify the username/password are correct. Get some + # token and url values from the original PyiCloud instance in case an asap specific + # password is being used for this instance. + if (self.is_DeviceSvc_setup_complete is False + and self.username in Gb.PyiCloud_by_username): + _PyiCloud = Gb.PyiCloud_by_username[self.username] + if _PyiCloud.findme_url_root: + self.findme_url_root = _PyiCloud.findme_url_root + self._update_token_pw_file('findme_url', self.findme_url_root) - if not path.exists(self.session_directory): - mkdir(self.session_directory) + self.session_data_token = _PyiCloud.session_data_token.copy() + self.session_data_token.update(self.session_data) - self._read_token_password_file() + self.set_token_password_value('session_token', self.session_data_token) + if 'session_token' in self.session_data_token: + self.session_token = self.session_data_token.get() - if self.password != self.token_password: - delete_msg = delete_file('session', self.session_directory, self.cookie_filename) - post_event(delete_msg) + self._update_token_pw_file('session_token', self.session_data_token) + self._update_token_pw_file('trust_token', self.session_data_token) - try: - self.session_data = {} - with open(self.session_directory_filename) as session_f: - self.session_data = json.load(session_f) except: - log_info_msg("Session file does not exist") + log_info_msg( f"{self.username_base}, , " + f"Session file does not exist ({self.session_dir_filename})") if self.session_data.get("client_id"): self.client_id = self.session_data.get("client_id") else: self.session_data.update({"client_id": self.client_id}) + self.session_data_token.update({"client_id": self.client_id}) - self.Session = PyiCloudSession(self) + self.PyiCloudSession = PyiCloudSession(self) self._setup_url_endpoint_suffix() - self.Session.verify = True - self.Session.headers.update({"Origin": self.HOME_ENDPOINT, "Referer": self.HOME_ENDPOINT,}) + self.PyiCloudSession.verify = True + self.PyiCloudSession.headers.update({"Origin": self.HOME_ENDPOINT, "Referer": self.HOME_ENDPOINT,}) - self.Session.cookies = cookielib.LWPCookieJar(filename=self.cookie_directory_filename) - if path.exists(self.cookie_directory_filename): + self.PyiCloudSession.cookies = cookielib.LWPCookieJar(filename=self.cookie_dir_filename) + if path.exists(self.cookie_dir_filename): try: - self.Session.cookies.load(ignore_discard=True, ignore_expires=True) - log_debug_msg(f"Read Cookies from {self.cookie_directory_filename}") + self.PyiCloudSession.cookies.load(ignore_discard=True, ignore_expires=True) + log_debug_msg( f"{self.username_base}, " + f"Load Cookies File ({self.cookie_dir_filename})") except: - log_warning_msg(f"Failed to read cookie file {self.cookie_directory_filename}") + log_warning_msg(f"{self.username_base}, " + f"Load Cookies File Failed ({self.cookie_dir_filename})") #---------------------------------------------------------------------------- def _setup_url_endpoint_suffix(self): @@ -940,7 +1186,7 @@ def _setup_url_endpoint_suffix(self): if self.endpoint_suffix in APPLE_SPECIAL_ICLOUD_SERVER_COUNTRY_CODE: self.endpoint_suffix = f".{self.endpoint_suffix}" - post_event(f"iCloud Web Server URL Country Suffix > {self.endpoint_suffix}") + post_event(f"Apple Account URL Country Suffix > {self.endpoint_suffix}") else: self.endpoint_suffix = '' @@ -960,53 +1206,71 @@ def _setup_url_endpoint_suffix(self): without checking the password and a password change will not be handled until the token wxpires. ''' - def _read_token_password_file(self): + def _read_token_pw_file(self): try: self.token_password = '' - with open(self.tokenpw_directory_filename) as tokenpw_f: - token_pw = json.load(tokenpw_f) - self.token_password = decode_password(token_pw['tokenpw']) + self.token_pw_data = read_json_file(self.tokenpw_dir_filename) + + if self.username not in self.token_pw_data: + self.token_pw_data[CONF_USERNAME] = self.username + + self.token_password = decode_password(self.token_pw_data[CONF_PASSWORD]) except: self.token_password = self.password - def _update_token_password_file(self): +#................................................... + def _write_token_pw_file(self): self.token_password = self.password + try: - with open(self.tokenpw_directory_filename, 'w', encoding='utf8') as f: - token_pw = {'tokenpw': encode_password(self.token_password)} - json.dump(token_pw, f, indent=4, ensure_ascii=False) + save_json_file(self.tokenpw_dir_filename, self.token_pw_data) except Exception as err: - log_exception(err) - log_warning_msg(f"Failed to update tokenpw file {self.tokenpw_directory_filename}") + # log_exception(err) + log_warning_msg(f"Apple Acct > {self.account_owner}, " + f"Failed to update tokenpw file {self.tokenpw_dir_filename}") #---------------------------------------------------------------------------- - @property - def cookie_filename(self): - '''Get name for cookiejar file''' - return "".join([c for c in self.user.get("accountName") if match(r"\w", c)]) + def _update_token_pw_file(self, item_key, source_data): + + try: + new_value = source_data if type(source_data) is str else \ + source_data[item_key] if type(source_data) is dict else \ + None + + if isnot_empty(new_value): + if self.token_pw_data.get(item_key) != new_value: + self.token_pw_data[item_key] = new_value + self._write_token_pw_file() + + except Exception as err: + # log_exception(err) + pass + +#---------------------------------------------------------------------------- @property - def cookie_directory_filename(self): - '''Get path for cookiejar file.''' + def cookie_dir_filename(self): + '''Get path for cookie file''' return path.join(self.cookie_directory, - "".join([c for c in self.user.get("accountName") if match(r"\w", c)]),) + f"{self.cookie_filename}") @property - def session_directory_filename(self): - '''Get path for session data file.''' - return path.join(self.session_directory, - "".join([c for c in self.user.get("accountName") if match(r"\w", c)]),) + def session_dir_filename(self): + '''Get path for session data file''' + return path.join(self.cookie_directory, + f"{self.cookie_filename}.session") @property - def tokenpw_directory_filename(self): + def tokenpw_dir_filename(self): ''' Token Password - This file stores the username's password associated with the session token and is used to determine if the password has changed and the session needs to be reset ''' - return f"{self.session_directory_filename}.tpw" + return path.join(self.cookie_directory, + f"{self.cookie_filename}.tpw") @property def authentication_method(self): @@ -1026,7 +1290,6 @@ def _check_2sa_needed(self): '''Returns True if two-step authentication is required.''' try: needs_2sa_flag = (self.data.get("dsInfo", {}).get("hsaVersion", 0) >= 1 - # and (self.data.get("hsaChallengeRequired", False) and (self.is_challenge_required or self.is_trusted_browser is False)) return needs_2sa_flag @@ -1050,10 +1313,18 @@ def _check_2fa_needed(self): return False if needs_2fa_flag: - log_debug_msg(f"NEEDS-2FA, ChallengeRequired-{self.is_challenge_required}, " + log_debug_msg( f"{self.username_base}, " + f"NEEDS-2FA, " + f"ChallengeRequired-{self.is_challenge_required}, " f"TrustedBrowser-{self.is_trusted_browser}") return needs_2fa_flag + # @property + # def requires_2sa(self): + # """Returns True if two-step authentication is required.""" + # return (self.data.get("dsInfo", {}).get("hsaVersion", 0) >= 1 + # and (is_challenge_required or self.is_trusted_session is False)) + @property def is_challenge_required(self): '''Returns True if the challenge code is needed.''' @@ -1066,50 +1337,115 @@ def is_trusted_browser(self): @property def trusted_devices(self): + return '''Returns devices trusted for two-step authentication.''' - url = f"{self.SETUP_ENDPOINT}/listDevices" - request = self.Session.get(url, params=self.params) - return request.json().get("devices") + try: + # _log(f"BUILD IN PYICLOUD TRUSTED DEVICES {self.data}") + # url = f"{self.SETUP_ENDPOINT}/listDevices" + # return await Gb.hass.async_add_executor_job( + # self.PyiCloudSession.get, + # (f"{self.SETUP_ENDPOINT}/listDevices" + # f"?clientBuildNumber=2021Project52" + # f"&clientMasteringNumber=2021B29" + # f"&clientId={self.client_id[5:]}")) + # # data=json.dumps(self.data)) + + # self.data = req.json() + # _log(f"{self.data=}") + # _log(f"{self.data.get('devices')=}") + # return self.data.get('devices') + + # _session_request(self, method, url, **kwargs + # request = self.PyiCloudSession.get(url, params=self.params) + # return request.json().get("devices") + # url = f"{self.SETUP_ENDPOINT}/listDevices" + # request = self.PyiCloudSession.get(url, params=self.params) + # return request.json().get("devices") + # headers = self._get_auth_headers() + + # _log(f"BUILD IN PYICLOUD TRUSTED DEVICES {self.params.keys()}") + + data = dict(self.user) + data["rememberMe"] = True + data["trustTokens"] = [] + if self.session_data.get("trust_token"): + data["trustTokens"] = [self.session_data.get("trust_token")] + + headers = self._get_auth_headers({"Accept": "application/json"}) + if self.session_data.get("scnt"): + headers["scnt"] = self.session_data.get("scnt") + if self.session_data.get("session_id"): + headers["X-Apple-ID-Session-Id"] = self.session_data.get("session_id") + + # _log(f"BUILD IN PYICLOUD TRUSTED DEVICES {headers.keys()}") + + # request = await Gb.hass.async_add_executor_job( + # self.PyiCloudSession.get, + # f"{self.SETUP_ENDPOINT}/listDevices" + # f"?clientBuildNumber=2021Project52" + # f"&clientMasteringNumber=2021B29" + # f"&clientId={self.client_id[5:]}", + # headers) + # return request.json().get("devices") + request = self.PyiCloudSession.get( + f"{self.SETUP_ENDPOINT}/listDevices", + params=self.params, + data=data, + headers=headers) + return request.json().get('devices') + + except Exception as err: + log_exception(err) + + return {}#request.json().get('devices') def new_log_in_needed(self, username): - return username != self.apple_id + return username != self.username + + # @property + # def response_code(self): + # return self.PyiCloudSession.response_code @property - def response_code(self): - return self.Session.response_status_code + def icloud_dnames(self): + icloud_dnames = [icloud_dname + for icloud_dname in self.device_id_by_icloud_dname.keys()] + icloud_dnames.sort() + + return icloud_dnames #---------------------------------------------------------------------------- def send_verification_code(self, device): '''Requests that a verification code is sent to the given device.''' - data = json.dumps(device) - response = self.Session.post("%s/sendVerificationCode" % self.SETUP_ENDPOINT, - params=self.params, - data=data,) + + url = f"{self.SETUP_ENDPOINT}/sendVerificationCode" + data = device + + response = self.PyiCloudSession.post(url, params=self.params, data=data) return response.json().get("success", False) #---------------------------------------------------------------------------- def validate_2fa_code(self, code): '''Verifies a verification code received via Apple's 2FA system (HSA2).''' - data = {"securityCode": {"code": code}} headers = self._get_auth_headers({"Accept": "application/json"}) - if self.session_data.get("scnt"): headers["scnt"] = self.session_data.get("scnt") - if self.session_data.get("session_id"): headers["X-Apple-ID-Session-Id"] = self.session_data.get("session_id") + url = f"{self.AUTH_ENDPOINT}/verify/trusteddevice/securitycode" + data = {"securityCode": {"code": code}} + try: - response = self.Session.post(f"{self.AUTH_ENDPOINT}/verify/trusteddevice/securitycode", - data=json.dumps(data), - headers=headers,) + response = self.PyiCloudSession.post(url, data=data, headers=headers,) except PyiCloudAPIResponseException as error: # Wrong verification code if error.code == -21669: - log_error_msg("Incorrect verification code") + log_error_msg( f"Apple Acct > {self.account_owner}, " + f"Incorrect verification code") return False raise @@ -1122,33 +1458,28 @@ def validate_2fa_code(self, code): except ValueError: data = {} - code = int(data.get('service_errors', [{}])[0].get('code', 0)) - if code == -21669: - log_error_msg("Incorrect verification code") - return False - log_debug_msg("Verification Code accepted") - self.trust_session() self.requires_2fa = self.requires_2fa or self._check_2fa_needed - # Return true if 2fa code was successful + valid_msg = 'Rejected' if self.requires_2fa else 'Accepted' + log_debug_msg(f"{self.username_base}, Verification Code {valid_msg}") + # Return true if 2fa code was successful return not self.requires_2fa #---------------------------------------------------------------------------- def trust_session(self): '''Request session trust to avoid user log in going forward.''' - headers = self._get_auth_headers() + headers = self._get_auth_headers() if self.session_data.get("scnt"): headers["scnt"] = self.session_data.get("scnt") - if self.session_data.get("session_id"): headers["X-Apple-ID-Session-Id"] = self.session_data.get("session_id") try: - self.Session.get(f"{self.AUTH_ENDPOINT}/2sv/trust", headers=headers,) + self.PyiCloudSession.get(f"{self.AUTH_ENDPOINT}/2sv/trust", headers=headers,) if self._authenticate_with_token(): self.authenticate_method += "+Token" @@ -1165,44 +1496,42 @@ def trust_session(self): def _get_webservice_url(self, ws_key): '''Get webservice URL, raise an exception if not exists.''' try: - if self._webservices.get(ws_key) is None: + if self.webservices.get(ws_key) is None: return None - return self._webservices[ws_key]["url"] + return self.webservices[ws_key]["url"] except: return None #---------------------------------------------------------------------------- - @property - def famshr_devices(self): - ''' - Initializes the FindMyiPhone class, refresh the iCloud device data for all - devices and create the PyiCloud_RawData object containing the data for all locatible devices. - ''' - self.create_FamilySharing_object() + # @property + # def find_devices(self): + # ''' + # Initializes the DeviceSvc class, refresh the iCloud device data for all + # devices and create the PyiCloud_RawData object containing the data for all locatible devices. + # ''' + # self.create_DeviceSvc_object() #---------------------------------------------------------------------------- - def create_FamilySharing_object(self, config_flow_login=False): + def create_DeviceSvc_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 + config_flow_create indicates another Apple acct is being logged into and a new iCloud object + should be created instead of using the existing iCloud object created when iC3 started ''' try: - if self.FamilySharing is not None: - return - if config_flow_login is False and Gb.PyiCloud and Gb.PyiCloud.FamilySharing is not None: - self.PyiCloud = Gb.PyiCloud + if self.DeviceSvc: return - self.FamilySharing = PyiCloud_FamilySharing(self, - self._get_webservice_url("findme"), - self.Session, - self.params, - self.with_family) - return self.FamilySharing + self.DeviceSvc = PyiCloud_DeviceSvc(self, + self.PyiCloudSession, + self.params) + + log_debug_msg(f"{self.username_base}, Create iCloud object {self.username_base})") + + return self.DeviceSvc except Exception as err: log_exception(err) @@ -1211,31 +1540,12 @@ def create_FamilySharing_object(self, config_flow_login=False): #---------------------------------------------------------------------------- @property - def refresh_famshr_data(self): + def refresh_icloud_data(self): ''' Refresh the iCloud device data for all devices and update the PyiCloud_RawData object for all locatible devices that are being tracked by iCloud3. ''' - self.FamilySharing.refresh_client() - -#---------------------------------------------------------------------------- - - def create_FindMyFriends_object(self): - ''' - Initializes the Find My Friends object, refresh the iCloud device data for all - devices and create the PyiCloud_RawData object containing the data for all locatible devices. - ''' - try: - self.FindMyFriends = PyiCloud_FindMyFriends(self, - self._get_webservice_url("findme"), - self.Session, - self.params) - - # self._get_webservice_url("contacts"), - # self._get_webservice_url("cksharews"), - - except Exception as err: - log_exception(err) + self.DeviceSvc.refresh_client() #---------------------------------------------------------------------------- def play_sound(self, device_id, subject="Find My iPhone Alert"): @@ -1244,23 +1554,28 @@ def play_sound(self, device_id, subject="Find My iPhone Alert"): It's possible to pass a custom message by changing the `subject`. ''' - data = self.FamilySharing.play_sound(device_id, subject) + data = self.DeviceSvc.play_sound(device_id, subject) return data #---------------------------------------------------------------------------- def __repr__(self): try: - return (f"") + return (f"") except: - return (f"") + return (f"") #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # -# Find my iPhone service +# Find iCloud Devices service (originally find my iphone) # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -class PyiCloud_FamilySharing(): +REFRESH_ENDPOINT = "/fmipservice/client/web/refreshClient" +PLAYSOUND_ENDPOINT = "/fmipservice/client/web/playSound" +MESSAGE_ENDPOINT = "/fmipservice/client/web/sendMessage" +LOSTDEVICE_ENDPOINT = "/fmipservice/client/web/lostDevice" + +class PyiCloud_DeviceSvc(): ''' The 'Find my iPhone' iCloud service @@ -1269,43 +1584,26 @@ class PyiCloud_FamilySharing(): ''' def __init__(self, PyiCloud, - service_root, - Session, + PyiCloudSession, params, - with_family=False, task="RefreshData", + task="RefreshData", device_id=None, subject=None, message=None, sounds=False, number="", newpasscode=""): + self.setup_time = time_now() + self.PyiCloudSession = PyiCloudSession + self.PyiCloud = PyiCloud + self.username_base = self.PyiCloud.username_base - self.Session = Session - self.PyiCloud = PyiCloud - self.instance = PyiCloud.instance self.params = params - self.with_family = with_family self.task = task self.device_id = device_id self.devices_data = {} Gb.devices_without_location_data = [] - try: - self.is_service_available = True - self.is_service_not_available = False - self._set_service_available(service_root is not None) - except Exception as err: - log_exception(err) - - if self.is_service_not_available: - post_event( 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.instance})") - return - - fmip_endpoint = f"{service_root}/fmipservice/client/web" - self._fmip_refresh_url = f"{fmip_endpoint}/refreshClient" - self._fmip_sound_url = f"{fmip_endpoint}/playSound" - self._fmip_message_url = f"{fmip_endpoint}/sendMessage" - self._fmip_lost_url = f"{fmip_endpoint}/lostDevice" + self.timestamp_field = 'timeStamp' + self.data_source = ICLOUD fmiDict = {"clientContext": { "appName": "Home Assistant", "appVersion": "0.118", @@ -1329,116 +1627,133 @@ def __init__(self, PyiCloud, try: # This will generate an error if the table has not been defined from (init or start_ic3) - # Init may be in the process of setting up the table and FamShr but then start_ic3/Stage 4 + # Init may be in the process of setting up the table and iCloud but then start_ic3/Stage 4 # thinks it is not done and resets everything. - if self.PyiCloud.device_id_by_famshr_fname != {}: + if self.PyiCloud.device_id_by_icloud_dname != {}: return except: pass - self.refresh_client(device_id) + self.refresh_client(locate_all_devices=True) - if self.is_service_not_available: return - - Gb.devices_not_set_up = self._conf_famshr_devices_not_set_up() + Gb.devices_not_set_up = self._get_conf_icloud_devices_not_set_up() if Gb.devices_not_set_up == []: return - if self.PyiCloud.instance == 'initial': - self.PyiCloud.init_step_needed.append('FamShr') - return - - self.refresh_client(device_id) - -#---------------------------------------------------------------------------- - def _set_service_available(self, available): - self.is_service_available = available - self.is_service_not_available = not available + self.refresh_client(locate_all_devices=True) #---------------------------------------------------------------------------- - def _conf_famshr_devices_not_set_up(self): + def _get_conf_icloud_devices_not_set_up(self): ''' Return a list of devices in the iCloud3 configuration file that are - not in the FamShr data returned from Apple. + not in the iCloud data returned from Apple. ''' return [conf_device[CONF_IC3_DEVICENAME] for conf_device in Gb.conf_devices if (conf_device[CONF_FAMSHR_DEVICENAME] != NONE_FNAME - and conf_device[CONF_FAMSHR_DEVICENAME] not in self.PyiCloud.device_id_by_famshr_fname)] - -#---------------------------------------------------------------------------- - @property - def timestamp_field(self): - return 'timeStamp' + and conf_device[CONF_FAMSHR_DEVICENAME] not in \ + self.PyiCloud.device_id_by_icloud_dname)] +#--------------------------------------------------------------------------- @property - def data_source(self): - return FAMSHR_FNAME + def is_DeviceSvc_setup_complete(self): + return self.PyiCloud.is_DeviceSvc_setup_complete +#--------------------------------------------------------------------------- @property def devices_cnt(self): # Simulate no devices returned for the first 4 tries # if Gb.get_FAMSHR_devices_retry_cnt < 4: - # return -1 + # return 0 + if 'content' in self.devices_data: return len(self.devices_data.get('content', {})) else: - return -1 + return 0 #---------------------------------------------------------------------------- - def refresh_client(self, requested_by_devicename=None, _device_id=None, - _with_family=None, refreshing_poor_loc_flag=False): + def refresh_client(self, requested_by_devicename=None, + locate_all_devices=None, device_id=None): ''' Refreshes the FindMyiPhoneService endpoint, This ensures that the location data is up-to-date. + + requested_by_devicename: + = 'reload_all_devices' - Reload all devices during startup instead of + only devices that have already been verified + = devicename - Device that is requesting new location data + device_id: + = refresh/locate this device + = None - Refresh/located all devices in Family Sharing list + locate_all_devices: + = True - Locate all devices in the Family Sharing list (overrides device selection) + = False - Locate only the devices belonging to this Apple acct ''' - if self.is_service_not_available: return + if self.is_DeviceSvc_setup_complete is False: + return - selected_device = _device_id if _device_id else "all" - fmly_param = _with_family if _with_family is not None else self.with_family + locate_all_devices = True if locate_all_devices is None else locate_all_devices - try: - devices_data = self.Session.post( - self._fmip_refresh_url, - params=self.params, - data=json.dumps({"clientContext": - { "fmly": fmly_param, - "shouldLocate": True, - "selectedDevice": selected_device, - "deviceListVersion": 1, }}),) + if requested_by_devicename: + _Device = Gb.Devices_by_devicename[requested_by_devicename] + last_update_loc_time = _Device.last_update_loc_time + else: + last_update_loc_time = '?' + + if locate_all_devices is False: + device_msg = f"OwnerDev-{len(Gb.owner_device_ids_by_username[self.PyiCloud.username])}" + else: + device_msg = f"AllDevices-{len(Gb.Devices_by_username.get(self.PyiCloud.username, []))}" + + log_debug_msg( f"Apple Acct > {self.PyiCloud.username_base}, " + f"RefreshRequestBy-{requested_by_devicename}, " + f"LocateAllDev-{locate_all_devices}, {device_msg}, LastLoc-{last_update_loc_time}") + url = f"{self.PyiCloud.findme_url_root}{REFRESH_ENDPOINT}" + data = {"clientContext":{ + "fmly": locate_all_devices, + "shouldLocate": True, + "selectedDevice": device_id, + + "deviceListVersion": 1, }, + "accountCountryCode": self.PyiCloud.session_data_token.get("account_country"), + "dsWebAuthToken": self.PyiCloud.session_data_token.get("session_token"), + "trustToken": self.PyiCloud.session_data_token.get("trust_token", ""), + "extended_login": True,} + + try: + devices_data = self.PyiCloudSession.post(url, params=self.params, data=data) self.devices_data = devices_data.json() except Exception as err: self.devices_data = {} - log_debug_msg("No data returned from FamShr refresh request") + log_debug_msg(f"{self.PyiCloud.username_base}, No data returned from iCloud refresh request") - if self.Session.response_status_code == 501: + if self.PyiCloudSession.response_code == 501: self._set_service_available(False) - post_event( 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.instance})") + post_event( f"{EVLOG_ALERT}iCLOUD ALERT > {self.PyiCloud.account_owner}, " + f"Family Sharing Data Source is not available. " + f"The web url providing location data returned a " + f"Service Not Available error") return None - Gb.pyicloud_refresh_time[FAMSHR] = time_now_secs() + self.PyiCloud.last_refresh_secs = time_now_secs() self.update_device_location_data(requested_by_devicename, self.devices_data.get("content", {})) #---------------------------------------------------------------------------- def update_device_location_data(self, requested_by_devicename=None, devices_data=None): ''' - devices_data is the iCloud response['content'] data for all devices in the FamShr list. + devices_data is the iCloud response['content'] data for all devices in the iCloud list. Cycle through them, determine if the data is good and update each devices with the new location info. ''' - # content contains the device data and the location data - - if (self.is_service_not_available - or devices_data is None): + if devices_data is None: return try: + self.PyiCloud.last_refresh_secs = time_now_secs() self.PyiCloud.update_requested_by = requested_by_devicename - monitor_msg = f"UPDATED FamShr Data > RequestedBy-{requested_by_devicename}" + monitor_msg = f"UPDATED iCloud Data > RequestedBy-{requested_by_devicename}" for device_data in devices_data: @@ -1454,7 +1769,8 @@ def update_device_location_data(self, requested_by_devicename=None, devices_data device_id = device_data[ID] rawdata_hdr_msg = '' - if (device_data_name in Gb.conf_famshr_devicenames + if (device_data_name in Gb.conf_icloud_dnames + and requested_by_devicename != 'reload_all_devices' and Gb.start_icloud3_inprocess_flag): pass @@ -1470,23 +1786,31 @@ def update_device_location_data(self, requested_by_devicename=None, devices_data monitor_msg += f"{CRLF_STAR}OFFLINE > " else: monitor_msg += f"{CRLF_STAR}NO LOCATION > " - monitor_msg += (f"{device_data_name}/{device_id[:8]}, " + monitor_msg += (f"{self.PyiCloud.account_owner}, " + f"{device_data_name}/" + f"{device_id[:8]}, " f"{device_data['modelDisplayName']} " f"({device_data['rawDeviceModel']})") - # log_rawdata(f"FamShr Device - Offline/No Location Data, {self.instance} " - # f"<{device_data_name}>", device_data) - + # Create RawData object if the device_id is not already set up if device_id not in self.PyiCloud.RawData_by_device_id: # if device_data_name == 'Gary-iPad': # self._create_test_data(device_id, device_data_name, device_data) # else: - monitor_msg +=\ - self._create_RawData_famshr_object(device_id, device_data_name, device_data) + device_msg = self._create_iCloud_RawData_object(device_id, device_data_name, device_data) + monitor_msg += device_msg + continue + + # The PyiCloudSession is not recreated on a restart if it already is valid but we need to + # initialize all devices, not just tracked ones on an iC3 restart. + elif Gb.start_icloud3_inprocess_flag: + device_msg = self._initialize_iCloud_RawData_object(device_id, device_data_name, device_data) + monitor_msg += device_msg continue # Non-tracked devices are not updated _RawData = self.PyiCloud.RawData_by_device_id[device_id] + _Device = _RawData.Device if _RawData.Device is None: continue @@ -1496,17 +1820,22 @@ def update_device_location_data(self, requested_by_devicename=None, devices_data if _RawData.location_secs == 0: continue - log_rawdata(f"FamShr Data, {rawdata_hdr_msg} {self.instance} - " - f"<{device_data_name}/{_Device.devicename}>", _RawData.device_data) + if ('all' in Gb.conf_general[CONF_LOG_LEVEL_DEVICES] + or _RawData.ic3_devicename in Gb.conf_general[CONF_LOG_LEVEL_DEVICES]): + log_hdr = ( f"{self.PyiCloud.username_base}{LINK}" + f"{device_data_name}/{_Device.devicename}{RLINK}, " + f"{rawdata_hdr_msg}, iCloud Data ") + log_rawdata(log_hdr, _RawData.device_data, + data_source='icloud', filter_id=self.PyiCloud.username) if _RawData.last_loc_time_gps == _RawData.loc_time_gps: last_loc_time_gps_msg = last_loc_time_msg = '' else: last_loc_time_gps_msg = f"{_RawData.last_loc_time_gps}{RARROW}" last_loc_time_msg = f"{_RawData.last_loc_time}{RARROW}" - _Device.loc_time_updates_famshr.append(_RawData.location_time) + _Device.loc_time_updates_icloud.append(_RawData.location_time) - event_msg =(f"Located > FamShr-" + event_msg =(f"Located > iCloud-" f"{last_loc_time_msg}" f"{_RawData.location_time}, ") @@ -1527,8 +1856,6 @@ def update_device_location_data(self, requested_by_devicename=None, devices_data elif _RawData.location_secs > 0: post_event(_Device.devicename, event_msg) - - except Exception as err: log_exception(err) @@ -1544,9 +1871,9 @@ def _create_test_data(self, device_id, device_data_name, device_data): device_data_test2['location'] = device_data['location'].copy() device_data['location']['timeStamp'] = 0 - # device_data[ID] = f"XX0_{device_id}" + monitor_msg +=\ - self._create_RawData_famshr_object(device_id, device_data_name, device_data) + self._create_iCloud_RawData_object(device_id, device_data_name, device_data) device_data_test1[NAME] = f"{device_data_name}(1)" device_data_test1[ID] = f"XX1_{device_id}" @@ -1554,51 +1881,89 @@ def _create_test_data(self, device_id, device_data_name, device_data): device_data_test1['rawDeviceModel'] = 'iPad8,91' monitor_msg +=\ - self._create_RawData_famshr_object(device_data_test1[ID], device_data_test1[NAME], device_data_test1) + self._create_iCloud_RawData_object(device_data_test1[ID], device_data_test1[NAME], device_data_test1) device_data_test2[NAME] = f"{device_data_name}(2)" device_data_test2[ID] = f"XX2_{device_id}" device_data_test2['rawDeviceModel'] = 'iPad8,92' monitor_msg +=\ - self._create_RawData_famshr_object(device_data_test2[ID], device_data_test2[NAME], device_data_test2) + self._create_iCloud_RawData_object(device_data_test2[ID], device_data_test2[NAME], device_data_test2) #---------------------------------------------------------------------------- - def _create_RawData_famshr_object(self, device_id, device_data_name, device_data): + def _create_iCloud_RawData_object(self, device_id, device_data_name, device_data): _RawData = PyiCloud_RawData(device_id, device_data, - self.Session, + self.PyiCloudSession, self.params, - 'FamShr', 'timeStamp', + 'iCloud', + 'timeStamp', self, - device_data_name, - sound_url=self._fmip_sound_url, - lost_url=self._fmip_lost_url, - message_url=self._fmip_message_url,) + device_data_name,) + self.set_icloud_rawdata_fields(device_id, _RawData) - self.set_famshr_rawdata_fields(device_id, _RawData) + log_debug_msg( f"{self.PyiCloud.username_base}, " + f"Create RawData_icloud object, " + f"{self.PyiCloud.account_owner}{LINK}{_RawData.fname}{RLINK}") - log_debug_msg(f"Create RawData_famshr object {device_data_name}") - log_rawdata(f"FamShr Data - <{_RawData.fname}>", _RawData.device_data) + # if ('all' in Gb.conf_general[CONF_LOG_LEVEL_DEVICES] + # or _RawData.ic3_devicename in Gb.conf_general[CONF_LOG_LEVEL_DEVICES]): + # Log all devices (no filter) on initialization + log_hdr = f"{self.PyiCloud.account_name}{LINK}{_RawData.fname}{RLINK}, iCloud Data" + log_rawdata(log_hdr, _RawData.device_data, + data_source='icloud', filter_id=self.PyiCloud.username) dup_msg = f" as {_RawData.fname}" if _RawData.fname_dup_suffix else '' - return (f"{CRLF_DOT}ADDED > {device_data_name}{dup_msg}, " - f"{_RawData.loc_time_gps}") + return (f"{CRLF_DOT}ADDED > {device_data_name}{dup_msg}, {_RawData.loc_time_gps}") + +#---------------------------------------------------------------------------- + def _initialize_iCloud_RawData_object(self, device_id, device_data_name, device_data): + + + _RawData = self.PyiCloud.RawData_by_device_id[device_id] + + dname = _RawData._remove_special_chars(_RawData.name) + self.PyiCloud.dup_icloud_dname_cnt[dname] = 0 + + _RawData.__init__( device_id, + device_data, + self.PyiCloudSession, + self.params, + 'iCloud', 'timeStamp', + self, + device_data_name,) + + self.set_icloud_rawdata_fields(device_id, _RawData) + + log_debug_msg( f"Initialize RawData_icloud object " + f"{self.PyiCloud.username_base}{LINK}<{_RawData.fname}, " + f"{device_data_name}") + + # if ('all' in Gb.conf_general[CONF_LOG_LEVEL_DEVICES] + # or _RawData.ic3_devicename in Gb.conf_general[CONF_LOG_LEVEL_DEVICES]): + # Log all devices (no filter) on initialization + log_hdr = f"{self.PyiCloud.account_name}{LINK}{_RawData.fname}{RLINK}, iCloud Data" + log_rawdata(log_hdr, _RawData.device_data, + data_source='icloud', filter_id=self.PyiCloud.username) + + return (f"{CRLF_DOT}INITIALIZED > {device_data_name}, {_RawData.loc_time_gps}") #---------------------------------------------------------------------- - def set_famshr_rawdata_fields(self, device_id, _RawData): + def set_icloud_rawdata_fields(self, device_id, _RawData): ''' - The FamShr dictionaries contain info about the devices that is set - up when the RawData object for the device is created. If the FamShr + The iCloud dictionaries contain info about the devices that is set + up when the RawData object for the device is created. If the iCloud object is recreated during error, the device's RawData object already - exists and is not recreated. The FamShr dictionaries need to be - set up again. ''' + exists and is not recreated. The iCloud dictionaries need to be + set up again. + ''' + list_add(self.PyiCloud.RawData_items, _RawData) self.PyiCloud.RawData_by_device_id[device_id] = _RawData - self.PyiCloud.RawData_by_device_id_famshr[device_id] = _RawData - self.PyiCloud.device_id_by_famshr_fname[_RawData.fname] = device_id - self.PyiCloud.famshr_fname_by_device_id[device_id] = _RawData.fname - self.PyiCloud.device_info_by_famshr_fname[_RawData.fname] = _RawData.famshr_device_info - self.PyiCloud.device_model_info_by_fname[_RawData.fname] = _RawData.famshr_device_model_info + self.PyiCloud.device_id_by_icloud_dname[_RawData.fname] = device_id + self.PyiCloud.icloud_dname_by_device_id[device_id] = _RawData.fname + self.PyiCloud.device_info_by_icloud_dname[_RawData.fname] = _RawData.icloud_device_info + self.PyiCloud.device_model_info_by_fname[_RawData.fname] = _RawData.icloud_device_model_info + self.PyiCloud.device_model_name_by_icloud_dname[_RawData.fname]= _RawData.icloud_device_display_name #---------------------------------------------------------------------- @staticmethod @@ -1615,13 +1980,16 @@ def play_sound(self, device_id, subject="Find My iPhone Alert"): Send a request to the device to play a sound. It's possible to pass a custom message by changing the `subject`. ''' - if self.is_service_not_available: return + if self.is_DeviceSvc_setup_complete is False: + post_event("iCloud Service is not available, try again later") + return - data = json.dumps({ "device": device_id, - "subject": subject, - "clientContext": {"fmly": True}, }) + url = f"{self.PyiCloud.findme_url_root}{PLAYSOUND_ENDPOINT}" + data = {"device": device_id, + "subject": subject, + "clientContext": {"fmly": True}, } - self.Session.post(self._fmip_sound_url, params=self.params, data=data) + self.PyiCloudSession.post(url, params=self.params, data=data) return #---------------------------------------------------------------------------- @@ -1631,19 +1999,23 @@ def display_message(self, device_id, subject="Find My iPhone Alert", Send a request to the device to display a message. It's possible to pass a custom message by changing the `subject`. ''' - if self.is_service_not_available: return + if self.is_DeviceSvc_setup_complete is False: + post_event("iCloud Service is not available, try again later") + return - data = json.dumps( {"device": device_id, - "subject": subject, - "sound": sounds, - "userText": True, - "text": message, }) + url = f"{self.PyiCloud.findme_url_root}{MESSAGE_ENDPOINT}" + data = {"device": device_id, + "subject": subject, + "sound": sounds, + "userText": True, + "text": message, } - self.Session.post(self._fmip_message_url, params=self.params, data=data) + self.PyiCloudSession.post(url, params=self.params, data=data) return #---------------------------------------------------------------------------- - def lost_device(self, device_id, number, message="This iPhone has been lost. Please call me.", + def lost_device(self, device_id, number, + message="This iPhone has been lost. Please call me.", newpasscode=""): ''' Send a request to the device to trigger 'lost mode'. @@ -1652,441 +2024,29 @@ def lost_device(self, device_id, number, message="This iPhone has been lost. Ple been passed, then the person holding the device can call the number without entering the passcode. ''' - if self.is_service_not_available: return - - data = json.dumps({ "text": message, - "userText": True, - "ownerNbr": number, - "lostModeEnabled": True, - "trackingEnabled": True, - "device": device_id, - "passcode": newpasscode, }) - - self.Session.post(self._fmip_lost_url, params=self.params, data=data) - return - -#---------------------------------------------------------------------------- - def __repr__(self): - try: - return (f"") - except: - return (f"") - - -#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -# -# Find my Friends service -# -#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -class PyiCloud_FindMyFriends(): - ''' - The 'Find My' (aka 'Find My Friends') iCloud service - - This connects to iCloud and returns friend's data including - latitude and longitude. - ''' - def contacts__init__(self, PyiCloud, - service_root, - Session, - params): - - self.Session = Session - self.PyiCloud = PyiCloud - self.params = params - self._service_root = service_root - self.refresh_always = False - self.response = {} - - self.is_service_available = True - self.is_service_not_available = False - self._set_service_available(service_root is not None) - - # FmF Device information - These is used verify the device, display on the EvLog and in the Config Flow - # device selection list on the iCloud3 Devices screen - self.device_id_by_fmf_email = {} - self.fmf_email_by_device_id = {} - self.device_info_by_fmf_email = {} - self.device_form_icloud_fmf_list = [] - Gb.devices_without_location_data = [] - - self._service_root = service_root - self._contacts_endpoint = "%s/co" % self._service_root - self._contacts_refresh_url = "%s/startup" % self._contacts_endpoint - self._contacts_next_url = "%s/contacts" % self._contacts_endpoint - self._contacts_changeset_url = "%s/changeset" % self._contacts_endpoint - - self.refresh_client() - - def contacts_refresh_client(self): - """ - Refreshes the ContactsService endpoint, ensuring that the - contacts data is up-to-date. - """ - params_contacts = dict(self.params) - params_contacts.update( - {"clientVersion": "2.1", "locale": "en_US", "order": "last,first",} - ) - req = self.Session.get(self._contacts_refresh_url, params=params_contacts) - self.response = req.json() - - params_next = dict(params_contacts) - params_next.update( - { - "prefToken": self.response["prefToken"], - "syncToken": self.response["syncToken"], - "limit": "0", - "offset": "0", - } - ) - req = self.Session.get(self._contacts_next_url, params=params_next) - self.response = req.json() - - def contacts_all(self): - """ - Retrieves all contacts. - """ - self.refresh_client() - return self.response.get("contacts") - - - def __init__(self, PyiCloud, - service_root, - Session, - params): - - self._set_service_available(service_root is not None) - - self.Session = Session - self.PyiCloud = PyiCloud - self.params = params - self._service_root = service_root - self.refresh_always = False - self.response = {} - - # FmF Device information - These is used verify the device, display on the EvLog and in the Config Flow - # device selection list on the iCloud3 Devices screen - self.device_id_by_fmf_email = {} - self.fmf_email_by_device_id = {} - self.device_info_by_fmf_email = {} - self.device_form_icloud_fmf_list = [] - Gb.devices_without_location_data = [] - - # 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 - - self.is_service_available = False - self.is_service_not_available = True - if self.is_service_not_available: - # post_event( 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.instance})") - return - - self._friend_endpoint = f"{self._service_root}/fmipservice/client/fmfWeb/initClient" - - self.refresh_client() - - if self.is_service_not_available: - post_event( 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.instance})") - return - - self._update_fmf_email_tables() - - devices_not_set_up = self._conf_fmf_devices_not_set_up() - if devices_not_set_up == '': - return - - post_event( f"{EVLOG_NOTICE}iCloud3 Notice > Some FmF devices were not " - f"initialized, data was not received from iCloud " - f"Location Svcs. Retrying..." - f"{devices_not_set_up}") - - if self.PyiCloud.instance == 'initial': - self.PyiCloud.init_step_needed.append('FmF') - return - - self.refresh_client() - self._update_fmf_email_tables() - - devices_not_set_up = self._conf_fmf_devices_not_set_up() - if devices_not_set_up == '': - post_event(f"{EVLOG_NOTICE}Find-my-Friends initialization retry successful") + if self.is_DeviceSvc_setup_complete is False: + post_event("iCloud Service is not available, try again later") return - post_event( f"{EVLOG_ALERT}iCLOUD ALERT > Find-my-Friends initialization retry failed " - f"{devices_not_set_up}") + url = f"{self.PyiCloud.findme_url_root}{LOSTDEVICE_ENDPOINT}" + data = {"text": message, + "userText": True, + "ownerNbr": number, + "lostModeEnabled": True, + "trackingEnabled": True, + "device": device_id, + "passcode": newpasscode, } -#---------------------------------------------------------------------------- - def _set_service_available(self, available): - self.is_service_available = available - self.is_service_not_available = not available - -#---------------------------------------------------------------------------- - def _conf_fmf_devices_not_set_up(self): - ''' - Return with a list of famf devices in the conf_devices that are not in _RawData - ''' - devices_not_set_up = [f"{conf_device[CONF_IC3_DEVICENAME]} ({conf_device[CONF_FMF_EMAIL]})" - for conf_device in Gb.conf_devices - if (conf_device[CONF_FMF_EMAIL] != NONE_FNAME - and conf_device[CONF_FMF_EMAIL] not in self.device_id_by_fmf_email)] - - if devices_not_set_up == []: - return "" - else: - return list_to_str(devices_not_set_up, CRLF_DOT) - - @property - def timestamp_field(self): - return 'timestamp' - - @property - def data_source(self): - return FMF_FNAME - -#---------------------------------------------------------------------------- - def refresh_client(self, requested_by_devicename=None, refreshing_poor_loc_flag=False): - ''' - Refreshes all data from 'Find My' endpoint, - ''' - if self.is_service_not_available: return - - params = dict(self.params) - - # This is a request payload we mock to fetch the data - mock_payload = json.dumps( - { - "clientContext": { - "appVersion": "1.0", - "contextApp": "com.icloud.web.fmf", - "mapkitAvailable": True, - "productType": "fmfWeb", - "tileServer": "Apple", - "userInactivityTimeInMS": 537, - "windowInFocus": False, - "windowVisible": True, - }, - "dataContext": None, - "serverContext": None, - } - ) - try: - response = self.Session.post(self._friend_endpoint, data=mock_payload, params=params) - except: - self.response = {} - log_debug_msg("No data returned on FmF refresh request") - - if self.Session.response_status_code == 501: - self._set_service_available(False) - return None - - try: - self.response = response.json() - except: - self.response = {} - log_debug_msg("No data returned on FmF refresh decode request") - - Gb.pyicloud_refresh_time[FMF] = time_now_secs() - self.PyiCloud.update_requested_by = requested_by_devicename - monitor_msg = (f"FmF iCloudData Update RequestedBy-{requested_by_devicename}") - - try: - for device_data in self.response.get('locations', {}): - device_id = device_data[ID] - if Device := Gb.Devices_by_icloud_device_id.get(device_id): - device_data_name = Device.devicename - else: - device_data_name = '' - - # Device was already set up or rejected - if device_id in Gb.devices_without_location_data: - continue - - # Update PyiCloud_RawData with data just received for tracked devices - if device_id not in self.PyiCloud.RawData_by_device_id: - monitor_msg += \ - self._create_RawData_fmf_object(device_id, device_data_name, device_data) - continue - - elif device_id not in Gb.Devices_by_icloud_device_id: - continue - - _RawData = self.PyiCloud.RawData_by_device_id[device_id] - - _RawData.save_new_device_data(device_data) - - requested_by_flag = '' - if requested_by_devicename == _RawData.devicename: - _RawData.last_requested_loc_time_gps = _RawData.loc_time_gps - requested_by_flag = ' *' - - last_loc_time_gps_msg = '' - if _RawData.last_loc_time_gps != _RawData.loc_time_gps: - last_loc_time_gps_msg = f"{_RawData.last_loc_time_gps}{RARROW}" - - log_rawdata(f"FmF Data - <{_RawData.devicename}>", _RawData.device_data) - - monitor_msg += (f"{CRLF_DOT}" - f"{_RawData.devicename}, " - f"{last_loc_time_gps_msg}" - f"{_RawData.loc_time_gps}" - f"{requested_by_flag}") - - post_monitor_msg(monitor_msg) - - return self.response - - except Exception as err: - log_exception(err) - return None - -#---------------------------------------------------------------------------- - def _create_RawData_fmf_object(self, device_id, device_data_name, device_data): - - _RawData = PyiCloud_RawData(device_id, - device_data, - self.Session, - self.params, - 'FmF', - 'timestamp', - self, - device_data_name, - sound_url=None, - lost_url=None, - message_url=None,) - try: - self.PyiCloud.RawData_by_device_id[device_id] = _RawData - self.PyiCloud.RawData_by_device_id_fmf[device_id] = _RawData - - log_rawdata(f"FmF Data - <{_RawData.devicename}>", _RawData.device_data) - - monitor_msg = (f"{CRLF_DOT}ADDED > {device_data_name}/{device_id[:8]}") - - if (LOCATION not in device_data - or device_data[LOCATION] == {} - or device_data[LOCATION] is None): - monitor_msg += " (No Location Data)" - else: - monitor_msg += f", {_RawData.loc_time_gps}" - - except Exception as err: - log_exception(err) - monitor_msg = '' - - return monitor_msg - -#---------------------------------------------------------------------------- - def _update_fmf_email_tables(self): - - fmf_friends_data = {'emails': self.contact_details, - 'invitationFromHandles': self.followers, - 'invitationAcceptedHandles': self.following} - - for fmf_email_field, Pyicloud_FmF_data in fmf_friends_data.items(): - if Pyicloud_FmF_data is None: - continue - - for friend in Pyicloud_FmF_data: - friend_emails = friend.get(fmf_email_field) - full_name = (f"{friend.get('firstName', '')} {friend.get('lastName', '')}") - full_name = full_name.strip() - device_id = friend.get('id') - - # extracted_fmf_devices.append((device_id, friend_emails)) - for friend_email in friend_emails: - self.device_id_by_fmf_email[friend_email] = device_id - self.fmf_email_by_device_id[device_id] = friend_email - friend_email_full_name = f"{friend_email} ({full_name})" if full_name else friend_email - if (friend_email not in self.device_info_by_fmf_email or full_name): - self.device_info_by_fmf_email[friend_email] = f"{friend_email_full_name}" - -#---------------------------------------------------------------------------- - def contact_id_for(self, identifier, default=None): - ''' - Returns the contact id of your friend with a given identifier - ''' - lookup_key = "phones" - if "@" in identifier: - lookup_key = "emails" - - def matcher(item): - '''Returns True iff the identifier matches''' - hit = item.get(lookup_key) - if not isinstance(hit, list): - return hit == identifier - return any([el for el in hit if el == identifier]) - - candidates = [ - item.get(ID, default) - for item in self.contact_details - if matcher(item)] - if not candidates: - return default - return candidates[0] - -#---------------------------------------------------------------------------- - def location_of(self, contact_id, default=None): - ''' - Returns the location of your friend with a given contact_id - ''' - candidates = [ - item.get("location", default) - for item in self.locations - if item.get(ID) == contact_id] - if not candidates: - return default - return candidates[0] + self.PyiCloudSession.post(url, params=self.params, data=data) + return #---------------------------------------------------------------------------- - @property - def data(self): - ''' - Convenience property to return data from the 'Find My' endpoint. - Call `refresh_client()` before property access for latest data. - ''' - if not self.response: - self.refresh_client() - return self.response - - @property - def locations(self): - '''Returns a list of your friends' locations''' - return self.response.get("locations", []) - - @property - def followers(self): - '''Returns a list of friends who follow you''' - return self.response.get("followers") - - @property - def following(self): - '''Returns a list of friends who you follow''' - return self.response.get("following") - - @property - def contact_details(self): - '''Returns a list of your friends contact details''' - return self.response.get("contactDetails") - - @property - def my_prefs(self): - '''Returns a list of your own preferences details''' - return self.response.get("myPrefs") - def __repr__(self): try: - return (f"") + return (f"") except: - return (f"") - - + return (f"") #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -2101,39 +2061,62 @@ def __repr__(self): class PyiCloud_RawData(): ''' - PyiCloud_Device stores all the device data for Family Sharing and Find-my-Friends - tracking methods. FamShr device_data contains the device info and the location - FmF contains the location + PyiCloud_RawData stores all the device data for each Apple Acct + + Parameters: + - device_id = iCloud device id of the device + - device_data = data received from Apple + - PyiCloudSession = PyiCloud instance that authenticates and gets the data + - params = ? + - data_source = 'iCloud' + - timestamp_field = name of the location timestamp field in device_data + - DeviceSvc = DeviceSvc object that created this RawData object + - device_data_name = name of the device in device_data (Gary-iPhone) ''' def __init__(self, device_id, device_data, - Session, + PyiCloudSession, params, data_source, timestamp_field, - FamShr_FmF, - device_data_name, - sound_url=None, lost_url=None, message_url=None, ): + DeviceSvc, + device_data_name,): + + self.setup_time = time_now() + self.PyiCloud = DeviceSvc.PyiCloud # PyiCloud object (Apple Acct) with the device data + self.PyiCloudSession = PyiCloudSession + + # __init__ is run several times during to initialize the RawData fields + # Initialize the identity fields on the initial create + if device_id not in self.PyiCloud.RawData_by_device_id: + self.device_id = device_id + self.params = params + self.data_source = data_source + self.timestamp_field = timestamp_field + self.DeviceSvc = DeviceSvc # iCloud object creating this RawData object - self.device_id = device_id - self.device_data = device_data - self.Session = Session - self.params = params - self.data_source = data_source - self.timestamp_field = timestamp_field - self.PyiCloud = FamShr_FmF.PyiCloud # PyiCloud object (iCloud Acct) with the device data - self.FamShr_FmF = FamShr_FmF # FamShr object or FmF object creating this RawData object - self.name = device_data_name - self.fname = self.device_data_fname_dup_check # Clean up fname and check for duplicates - self.fname_dup_suffix= '' # Suffix added to fname if duplicates - self.evlog_alert_char= '' + try: + # Only update the device name fields when the RawData object is created + # or when it was changed on the device and iCloud3 was restarted. Setting + # it up again if the RawData is just being reinstalled creates errors + # detecting duplicate names. + name_update_flag = (self.name != device_data_name) + except: + name_update_flag = True + + if name_update_flag: + self.name = device_data_name + self.fname_original = '' # Original dname after cleanup + self.fname_dup_suffix= '' # Suffix added to fname if duplicates + self.fname = self.device_data_fname_dup_check # Clean up fname and check for duplicates - # self.Device = Gb.Devices_by_icloud_device_id.get(device_id) - # self.ic3_devicename = self.Device.devicename if self.Device else '' - self.ic3_devicename = Gb.devicenames_x_famshr_devices.get(self.fname, '') + self.evlog_alert_char= '' + self.ic3_devicename = Gb.devicenames_by_icloud_dname.get(self.fname, '') self.Device = Gb.Devices_by_devicename.get(self.ic3_devicename) + self.device_data = device_data + self.update_secs = time_now_secs() self.location_secs = 0 self.location_time = HHMMSS_ZERO @@ -2145,53 +2128,89 @@ def __init__(self, device_id, self.battery_level = 0 - self.sound_url = sound_url - self.lost_url = lost_url - self.message_url = message_url - self.set_located_time_battery_info() self.device_data[DATA_SOURCE] = self.data_source self.device_data[CONF_IC3_DEVICENAME] = self.ic3_devicename - + self.raw_model = self.device_data.get('rawDeviceModel', self.device_class).replace('_', '') + Gb.model_display_name_by_raw_model[self.raw_model] = self.icloud_device_display_name #---------------------------------------------------------------------- @property def device_id8(self): return self.device_id[:8] +#---------------------------------------------------------------------- + @property + def fname_device_id(self): + return f"{self.fname} ({self.device_id8})" + #---------------------------------------------------------------------- @property def devicename(self): if Device := Gb.Devices_by_icloud_device_id.get(self.device_id): return Device.devicename - elif self.is_data_source_FAMSHR: + elif self.is_data_source_ICLOUD: return self.fname else: return self.device_id[:8] +#---------------------------------------------------------------------- + @property + def family_share_device(self): + return self.device_data['fmlyShare'] + #---------------------------------------------------------------------- @property def device_data_fname_dup_check(self): ''' - Determine if the FamShr device being set up is the same name as one that has already - been set up. Is so, add (#0) to the end of the fname and set the fname suffix value. - There may be some devices with a (#) suffix. iCloud3 adds a (#0). + Determine if the iCloud device being set up is the same name as one that has already + been set up. Is so, add periods('.') to the end of the fname to make it unique. + Also set the fname suffix value. ''' # Remove non-breakable space and right quote mark - fname = self._remove_special_chars(self.name) - - if self.is_data_source_FAMSHR is False: - return fname - - _FamShr = self.FamShr_FmF + dname = self.fname_original = self._remove_special_chars(self.name) + + # This is a tracked and configured device if the device_id is already used + conf_devicename = self._find_conf_device_devicename(CONF_FAMSHR_DEVICE_ID, self.device_id) + if conf_devicename: + return dname + + # It is ok if dname has not been seen + if dname not in self.PyiCloud.device_id_by_icloud_dname: + return dname + + # This is not a tracked and configured device if the dname is not used + # but maybe a dupe because the dname is found with a different device_id + conf_devicename = self._find_conf_device_devicename(CONF_FAMSHR_DEVICENAME, dname) + if conf_devicename == '': + found_before = (dname in self.PyiCloud.device_id_by_icloud_dname) + if found_before is False: + return dname + + # Dupe dname, it has not been seen and dname has been used + # Add a period to dname to make it unique + _dname = f"{dname}." + while _dname in self.PyiCloud.device_id_by_icloud_dname: + _dname += '.' + self.fname_dup_suffix = _dname.replace(dname, '') + + return _dname + +#...................................................................... + def _find_conf_device_devicename(self, field, field_value): + ''' + Cycle through the conf_devices and return the ic3_devicename that matches + the requested field/field_value + ''' + conf_devicename = [conf_device[CONF_IC3_DEVICENAME] + for conf_device in Gb.conf_devices + if (conf_device[CONF_APPLE_ACCOUNT] == self.PyiCloud.username + and conf_device[field] == field_value)] - if fname not in self.PyiCloud.dup_famshr_fname_cnt: - self.PyiCloud.dup_famshr_fname_cnt[fname] = 1 + if conf_devicename: + return conf_devicename[0] else: - self.PyiCloud.dup_famshr_fname_cnt[fname] += 1 - self.fname_dup_suffix = f"({self.PyiCloud.dup_famshr_fname_cnt[fname]})" - return f"{fname}{self.fname_dup_suffix}" - return fname + return '' #---------------------------------------------------------------------- @staticmethod @@ -2203,7 +2222,7 @@ def _remove_special_chars(name): #---------------------------------------------------------------------- @property - def famshr_device_info(self): + def icloud_device_info(self): return f"{self.fname} ({self.device_identifier})" #---------------------------------------------------------------------- @@ -2214,52 +2233,51 @@ def device_identifier(self): - iPhone 14,2; iPhone15,2) - Gary-iPhone ''' - if self.is_data_source_FAMSHR: + if self.is_data_source_ICLOUD: display_name = self.device_data['deviceDisplayName'].split(' (')[0] display_name = display_name.replace('Series ', '') if self.device_data.get('rawDeviceModel').startswith(AIRPODS_FNAME): device_class = AIRPODS_FNAME else: device_class = self.device_data.get('deviceClass', '') - raw_model = self.device_data.get('rawDeviceModel', device_class).replace('_', '') - - return (f"{display_name}; {raw_model}").replace("’", "'") - elif self.is_data_source_FMF: - full_name = (f"{self.device_data.get('firstName', '')} {self.device_data.get('lastName', '')}").strip() - return full_name.replace("’", "'") + # return (f"{display_name}; {raw_model}").replace("’", "'") + return (f"{self.icloud_device_display_name}; {self.raw_model}").replace("’", "'") else: return self.name.replace("’", "'") #---------------------------------------------------------------------- - # @property - # def device_identifier(self): - # return (f"{self.response.get('firstName', '')} " - # f"{self.response.get('lastName', '')}").strip() + @property + def device_class(self): + if self.device_data.get('rawDeviceModel').startswith(AIRPODS_FNAME): + return AIRPODS_FNAME + else: + return self.device_data.get('deviceClass', '') #---------------------------------------------------------------------- @property - def famshr_device_display_name(self): - display_name = self.device_data['deviceDisplayName'].split(' (')[0] + def icloud_device_display_name(self): + display_name = self.device_data['deviceDisplayName'] + display_name = display_name.replace('generation', 'gen') display_name = display_name.replace('Series ', '') + display_name = display_name.replace('(', '').replace(')', '') + idx = display_name.find('-inch') + if idx > 0: + display_name = display_name[:idx-3] + display_name[idx+5:] return display_name #---------------------------------------------------------------------- @property - def famshr_device_model_info(self): + def icloud_device_model_info(self): return [self.device_data['rawDeviceModel'].replace("_", ""), # iPhone15,2 self.device_data['modelDisplayName'], # iPhone - self.famshr_device_display_name] # iPhone 14 Pro + self.icloud_device_display_name] # iPhone 14 Pro #---------------------------------------------------------------------- @property - def is_data_source_FMF(self): - return (self.data_source in [FMF, FMF_FNAME]) - - @property - def is_data_source_FAMSHR(self): - return (self.data_source in [FAMSHR, FAMSHR_FNAME]) + def is_data_source_ICLOUD(self): + return (self.data_source in [ICLOUD]) @property def loc_time_gps(self): @@ -2312,7 +2330,7 @@ def status(self, additional_fields=[]): Returns status information for device. This returns only a subset of possible properties. ''' - self.FamShr_FmF.refresh_client(self.device_id, with_family=True) + self.DeviceSvc.refresh_client(self.device_id) fields = ["batteryLevel", "deviceDisplayName", "deviceStatus", "name"] fields += additional_fields @@ -2356,7 +2374,7 @@ def set_located_time_battery_info(self): # if self.name=='Gary-iPhone': # raise PyiCloudAPIResponseException('test error', 404) # self.device_data[LOCATION][TIMESTAMP] = int(self.device_data[LOCATION][self.timestamp_field] / 1000) - 600 - # _trace('gary_iphone', f"Reduce loc time to {secs_to_time(self.device_data[LOCATION][TIMESTAMP])}") + # _evlog('gary_iphone', f"Reduce loc time to {secs_to_time(self.device_data[LOCATION][TIMESTAMP])}") except TypeError: @@ -2403,7 +2421,7 @@ def location(self): def __repr__(self): try: - return f"" @@ -2451,6 +2469,13 @@ class PyiCloud2FARequiredException(PyiCloudException): '''iCloud 2SA required exception.''' pass +#---------------------------------------------------------------------------- +class PyiCloud2SARequiredException(PyiCloudException): + """iCloud 2SA required exception.""" + def __init__(self, apple_id): + message = f"Two-step authentication required for account: {apple_id}" + super().__init__(message) + #---------------------------------------------------------------------------- class PyiCloudNoStoredPasswordAvailableException(PyiCloudException): '''iCloud no stored password exception.''' diff --git a/custom_components/icloud3/support/pyicloud_ic3_interface.py b/custom_components/icloud3/support/pyicloud_ic3_interface.py index 6aa08d0..5fcabf7 100644 --- a/custom_components/icloud3/support/pyicloud_ic3_interface.py +++ b/custom_components/icloud3/support/pyicloud_ic3_interface.py @@ -1,25 +1,31 @@ from ..global_variables import GlobalVariables as Gb from ..const import (HIGH_INTEGER, - EVLOG_ALERT, EVLOG_NOTICE, + EVLOG_ALERT, EVLOG_NOTICE, EVLOG_ERROR, CRLF, CRLF_DOT, DASH_20, - ICLOUD, FAMSHR, + ICLOUD, SETTINGS_INTEGRATIONS_MSG, INTEGRATIONS_IC3_CONFIG_MSG, - CONF_USERNAME + CONF_USERNAME, CONF_PASSWORD, CONF_TOTP_KEY, CONF_LOCATE_ALL, + CONF_TRACKING_MODE, INACTIVE_DEVICE, ) from ..support import start_ic3 as start_ic3 -from ..support.pyicloud_ic3 import (PyiCloudService, PyiCloudFailedLoginException, PyiCloudNoDevicesException, +from ..support import start_ic3_control +from ..support.pyicloud_ic3 import (PyiCloudValidateAppleAcct, PyiCloudService, + PyiCloudNoDevicesException, PyiCloudFailedLoginException, PyiCloudAPIResponseException, PyiCloud2FARequiredException,) - -from ..helpers.common import (instr, list_to_str, list_add, list_del, 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.common import (instr, list_to_str, list_add, list_del, is_empty, isnot_empty, ) +from ..helpers import file_io +from ..support import config_file +from ..helpers.messaging import (post_event, post_error_msg, post_monitor_msg, + post_evlog_greenbar_msg, post_startup_alert, log_debug_msg, + log_info_msg, log_exception, log_error_msg, log_warning_msg, + internal_error_msg2, _evlog, _log, ) from ..helpers.time_util import (time_now_secs, secs_to_time, format_age, format_time_age, ) -import os import time +import pyotp import traceback from re import match from homeassistant.util import slugify @@ -30,224 +36,255 @@ # PYICLOUD-IC3 INTERFACE FUNCTIONS # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -def create_PyiCloudService_executor_job(): +def create_all_PyiCloudServices(): ''' This is the entry point for the hass.async_add_executor_job statement from __init__ ''' - create_PyiCloudService(Gb.PyiCloudInit, instance='initial') + if Gb.PyiCloudValidateAppleAcct is None: + Gb.PyiCloudValidateAppleAcct = PyiCloudValidateAppleAcct() + post_event('Log into Apple Accounts') -#-------------------------------------------------------------------- -def create_PyiCloudService(PyiCloud, instance='unknown'): - #See if pyicloud_ic3 is available + for conf_apple_acct in Gb.conf_apple_accounts: + username = conf_apple_acct[CONF_USERNAME] + password = Gb.PyiCloud_password_by_username[username] + locate_all = conf_apple_acct[CONF_LOCATE_ALL] - Gb.pyicloud_authentication_cnt = 0 - Gb.pyicloud_location_update_cnt = 0 - Gb.pyicloud_calls_time = 0.0 + if is_empty(username) or is_empty(password): + continue - if Gb.username == '' or Gb.password == '': - return + post_evlog_greenbar_msg(f"Apple Acct > Setting up {username.split('@')[0]}") - if authenticate_icloud_account(PyiCloud, instance=instance, initial_setup=True): - if ((Gb.PyiCloud and Gb.PyiCloud.is_authenticated) - or (Gb.PyiCloudInit and Gb.PyiCloudInit.is_authenticated)): - event_msg =(f"iCloud Location Service interface > Verified ({instance})") - post_event(event_msg) + # Validate username/password so we know all future login attempts will be with valid apple accts + if username not in Gb.username_valid_by_username: + username_password_valid = \ + Gb.PyiCloudValidateAppleAcct.validate_username_password(username, password) - if Gb.PyiCloud: - log_debug_msg(f"PyiCloud Instance Verified > {Gb.PyiCloud.instance}") - if Gb.PyiCloudInit: - log_debug_msg(f"PyiCloudInit Instance Verified > {Gb.PyiCloudInit.instance}") - else: - event_msg =(f"iCloud Location Service interface > Not Verified ({instance})") - post_event(event_msg) + Gb.username_valid_by_username[username] = username_password_valid + + if Gb.username_valid_by_username[username]: + log_into_apple_account(username, password, locate_all) + else: + event_msg =(f"Apple Acct > " + f"{username.split('@')[0]}, Invalid Username or Password") + post_event( f"{EVLOG_ALERT}{event_msg}") + post_startup_alert(event_msg) + + post_evlog_greenbar_msg('') + + Gb.startup_lists['Gb.PyiCloud_by_username'] = Gb.PyiCloud_by_username + Gb.startup_lists['Gb.username_valid_by_username'] = Gb.username_valid_by_username + Gb.startup_lists['Gb.username_pyicloud_503_connection_error'] = Gb.username_pyicloud_503_connection_error #-------------------------------------------------------------------- -def verify_pyicloud_setup_status(): +def retry_apple_acct_login(): ''' - 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. - 2. Create the FamShr object and get the FamShr devices. - 3. Create the FmF object and get the FamShr devices. - - This function is called from Stage 4 to determine the set up status. - 1. If the set up started in __init__ is comlete, return the PyiCloudInit object - 2. If FamShr has not been completed, rerequest setting up the FamShr object. - 3. If FmF has not been completed, rerequest setting up the FmF object. - 4. Return the PyiCloudInit object after #2 and #3 above. - 5. If the the authenticate step from the original set up request is not done, - start all over. The original request will eventually be comleted but it will - not be used. This will prevent HA from issuing Blocking call errors indicating - the PyiCloud session data requests must be run in the event loop. - + Retry to log into the Apple acct after receiving a 503 Connection Error. + This is done in the 15-min cycle of icoud3_main ''' - # iCloud was never started - if Gb.PyiCloudInit is None: - return False + for username in Gb.username_pyicloud_503_connection_error: + conf_apple_acct, apple_acct_id = config_file.conf_apple_acct(username) + password = conf_apple_acct[CONF_PASSWORD] + locate_all = conf_apple_acct[CONF_LOCATE_ALL] - # The verify can be requested during started or after a restart request before - # the restart has begun - if (Gb.restart_icloud3_request_flag - and Gb.start_icloud3_inprocess_flag): - Gb.PyiCloudInit.init_step_needed = ['FamShr'] - Gb.PyiCloudInit.init_step_complete = ['Setup', 'Authenticate'] + PyiCloud = log_into_apple_account(username, password, locate_all) - if 'Complete' in Gb.PyiCloudInit.init_step_complete: - Gb.PyiCloud = Gb.PyiCloudInit + if PyiCloud: + post_event(f"{EVLOG_ERROR}Apple Acct > {PyiCloud.account_owner}, Login Successful") + list_del(Gb.username_pyicloud_503_connection_error, username) - log_debug_msg(f"PyiCloud Instance Selected > {Gb.PyiCloud.instance}") - if _get_famshr_devices(Gb.PyiCloud): - return True + list_add(Gb.usernames_setup_error_retry_list, username) + start_ic3_control.stage_4_setup_data_sources_retry() - if Gb.get_FAMSHR_devices_retry_cnt == 0: - post_event(f"iCloud Location Svcs > Setup Complete") - return else: - if Gb.PyiCloud.FamilySharing: - Gb.PyiCloud.FamilySharing.refresh_client() - post_event( f"iCloud Location Svcs > Refreshing FamShr Data " - f"(#{Gb.get_FAMSHR_devices_retry_cnt})") - return True - else: - Gb.PyiCloudInit.init_step_needed = ['FamShr'] - - if 'Authenticate' not in Gb.PyiCloudInit.init_step_complete: - Gb.PyiCloudInit._set_step_completed('Cancel') - create_PyiCloudService(Gb.PyiCloud, instance='startup') - - Gb.PyiCloud = Gb.PyiCloud or Gb.PyiCloudInit - if (Gb.PyiCloud is None - or 'Authenticate' not in Gb.PyiCloud.init_step_complete): - create_PyiCloudService(Gb.PyiCloud, instance='startup') - Gb.PyiCloud = Gb.PyiCloud or Gb.PyiCloudInit - if Gb.PyiCloud is None: - return False + post_event(f"Apple Acct > {username}, Failed to Login, will try again later") - log_debug_msg(f"PyiCloud Instance Created > {Gb.PyiCloud.instance}") + post_evlog_greenbar_msg('') - # FamShare object exists, check/refresh the devices list - Gb.PyiCloud = Gb.PyiCloud or Gb.PyiCloudInit - if 'FamShr' in Gb.PyiCloud.init_step_complete: - if _get_famshr_devices(Gb.PyiCloud): - return True +#-------------------------------------------------------------------- +def verify_all_apple_accounts(): + ''' + Cycle through the apple accounts and validate that each one is valid + ''' + if Gb.PyiCloudValidateAppleAcct is None: + Gb.PyiCloudValidateAppleAcct = PyiCloudValidateAppleAcct() - # Create FamShare object and then check/refresh the devices list - Gb.PyiCloud.create_FamilySharing_object() - Gb.PyiCloud.init_step_needed == [] - Gb.PyiCloud._set_step_completed('FamShr') - Gb.PyiCloud._set_step_completed('Complete') - if _get_famshr_devices(Gb.PyiCloud): - return True + post_event('Verify Apple Account Username/Password') + cnt = -1 + for conf_apple_acct in Gb.conf_apple_accounts: + cnt += 1 + username = conf_apple_acct[CONF_USERNAME] + password = Gb.PyiCloud_password_by_username[username] - return False + if is_empty(username): + Gb.username_valid_by_username[f"AA-NOTSPECIFIED-#{cnt}"] + continue + if is_empty(password): + Gb.username_valid_by_username[f"AA-NOPASSWORD-#-{cnt}"] + continue -#-------------------------------------------------------------------- -def _get_famshr_devices(PyiCloud): - if PyiCloud is None: - return False - if PyiCloud.FamilySharing.devices_cnt >= 0: - return True + # Validate username/password so we know all future login attempts will be with valid apple accts + valid_apple_acct = Gb.PyiCloudValidateAppleAcct.validate_username_password(username, password) - Gb.get_FAMSHR_devices_retry_cnt = 0 - while Gb.get_FAMSHR_devices_retry_cnt < 8: - Gb.get_FAMSHR_devices_retry_cnt += 1 - if Gb.get_FAMSHR_devices_retry_cnt > 1: - post_event( f"Family Sharing List Refresh " - f"(#{Gb.get_FAMSHR_devices_retry_cnt} of 8)") + Gb.username_valid_by_username[username] = valid_apple_acct + if valid_apple_acct is False: + post_event(f"Apple Acct > {username}, Username/Password Invalid") - PyiCloud.FamilySharing.refresh_client() - if PyiCloud.FamilySharing.devices_cnt >= 0: - return True + Gb.startup_lists['Gb.username_valid_by_username'] = Gb.username_valid_by_username - return (PyiCloud.FamilySharing.devices_cnt >= 0) #-------------------------------------------------------------------- -def authenticate_icloud_account(PyiCloud, instance='unknown', initial_setup=False): +def log_into_apple_account(username, password, locate_all=True): ''' - Authenticate the iCloud Account via pyicloud + Log in and Authenticate the Apple Account via pyicloud - Arguments: - PyiCloud - Gb.PyiCloud or Gb.PyiCloudInit object depending on instance module - instance - Called from module (init or start_ic3) - - If successful - Gb.PyiCloud or Gb.PyiCloudInit = PyiCloudService object - If not - Gb.PyiCloud or Gb.PyiCloudInit = None + If successful - PyiCloud = PyiCloudService object + If not - PyiCloud = None ''' # If not using the iCloud location svcs, nothing to do - if (Gb.primary_data_source_ICLOUD is False - or Gb.username == '' - or Gb.password == ''): + if (Gb.use_data_source_ICLOUD is False + or username == '' + or password == ''): return this_fct_error_flag = True + login_err = 0 + post_evlog_greenbar_msg(f"Apple Acct > Setting up {username.split('@')[0]}") try: - 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') - - elif PyiCloud: - PyiCloud.__init__(Gb.username, Gb.password, - cookie_directory=Gb.icloud_cookies_dir, - session_directory=(f"{Gb.icloud_cookies_dir}/session"), - instance=instance) - log_debug_msg(f"PyiCloud Instance Initialized > {PyiCloud.instance}") - + PyiCloud = Gb.PyiCloud_by_username.get(username) + + pyicloud_msg = (f"{PyiCloud=}") + if PyiCloud: + pyicloud_msg += f"{PyiCloud.is_DeviceSvc_setup_complete=} {PyiCloud.DeviceSvc=}" + if PyiCloud.DeviceSvc: + pyicloud_msg += f"{PyiCloud.RawData_by_device_id.values()=}" + username_base = username.split('@')[0] + debug_msg_hdr =f"APPLE ACCT SETUP > {username_base}, Step-" + log_debug_msg(f"{debug_msg_hdr}0, Login Started, {pyicloud_msg}") + + # # Refresh existing PyiCloud/iCloud + # if PyiCloud and PyiCloud.connection_error_retry_cnt > 5: + # return + + if (PyiCloud + and PyiCloud.is_DeviceSvc_setup_complete + and PyiCloud.DeviceSvc): + PyiCloud.dup_icloud_dname_cnt = {} + + PyiCloud.DeviceSvc.refresh_client() + + pyicloud_msg = f"{PyiCloud=} {PyiCloud.is_DeviceSvc_setup_complete=} {PyiCloud.DeviceSvc=}" + if PyiCloud.DeviceSvc: + pyicloud_msg += f"{PyiCloud.RawData_by_device_id.values()=}" + log_debug_msg(f"{debug_msg_hdr}1, Request iCloud Refresh, {pyicloud_msg}") + post_event(f"Apple Acct > {PyiCloud.account_owner}, Device Data Refreshed") + + # Setup iCloud + elif (PyiCloud + and PyiCloud.is_DeviceSvc_setup_complete): + + PyiCloud.create_DeviceSvc_object() + Gb.PyiCloud_by_username[username] = PyiCloud + + pyicloud_msg = f"{PyiCloud=} {PyiCloud.is_DeviceSvc_setup_complete=} {PyiCloud.DeviceSvc=}" + if PyiCloud.DeviceSvc: + pyicloud_msg += f"{PyiCloud.RawData_by_device_id.values()=}" + log_debug_msg(f"{debug_msg_hdr}2, Create DeviceSvc, {pyicloud_msg}") + post_event(f"Apple Acct > {PyiCloud.account_owner}, iCloud Created & Refreshed") + + return PyiCloud + + # Setup PyiCloud and iCloud else: - log_info_msg(f"Connecting to and Authenticating iCloud Location Service Interface ({instance})") - PyiCloud = PyiCloudService(Gb.username, Gb.password, - cookie_directory=Gb.icloud_cookies_dir, - session_directory=(f"{Gb.icloud_cookies_dir}/session"), - instance=instance) + PyiCloud = PyiCloudService( username, password, + locate_all_devices=locate_all, + cookie_directory=Gb.icloud_cookie_directory, + session_directory=Gb.icloud_session_directory) + + # Stage 4 checks to see if PyiCloud exists and it has RawData device info. These values exists + # if the __init__ login was completed. However, if it was not completed and they do not exist, + # Stage 4 will do another login and set these values when it finishes, which is before the + # __init__ is complete. Do not set them again when __init__ login finially completes. + + pyicloud_msg = (f"{PyiCloud.account_owner_username}, " + f"Complete={PyiCloud.is_DeviceSvc_setup_complete}, ") + if PyiCloud.DeviceSvc: + rawdata_items = [_RawData.fname_device_id for _RawData in PyiCloud.RawData_by_device_id.values()] + pyicloud_msg += f"RawDataItems-({list_to_str(rawdata_items)})" + log_debug_msg(f"{debug_msg_hdr}3, Setup PyiCloud, {pyicloud_msg}") + + post_event(f"Apple Acct > {PyiCloud.account_owner}, Login Successful") + + verify_icloud_device_info_received(PyiCloud) + is_authentication_2fa_code_needed(PyiCloud, initial_setup=True) + + display_authentication_msg(PyiCloud) - #PyiCloud.instance = instance #f"{instance}-{str(id(PyiCloud))[-5:]}" - log_debug_msg(f"PyiCloud Instance Created > {PyiCloud.instance}") + return PyiCloud + except PyiCloud2FARequiredException as err: + login_err = str(err) is_authentication_2fa_code_needed(PyiCloud, initial_setup=True) - display_authentication_msg(PyiCloud) + return PyiCloud except PyiCloudAPIResponseException as err: - event_msg =(f"{EVLOG_ALERT}iCloud3 Error > An error occurred communicating with " - f"iCloud Account servers. This can be caused by:" - f"{CRLF_DOT}Your network or wifi is down, or" - f"{CRLF_DOT}Apple iCloud servers are down" - f"{CRLF}Error-{err}") - post_startup_alert('Error occurred logging into the iCloud Account') + login_err = str(err) + pass except PyiCloudFailedLoginException as err: - event_msg =(f"{EVLOG_ALERT}iCloud3 Error > An error occurred logging into the iCloud Account. " - f"{err})") - # f"Authentication Process, Error-({Gb.PyiCloud.authenticate_method[2:]})") - post_error_msg(event_msg) - post_startup_alert('iCloud Account Login Error') - - Gb.PyiCloud = Gb.PyiCloudInit = PyiCloud = None - Gb.username = Gb.password = '' - return False - - except PyiCloud2FARequiredException as err: - is_authentication_2fa_code_needed(PyiCloud, initial_setup=True) - return False + PyiCloud = Gb.PyiCloudLoggingInto + login_err = str(err) + login_err + ", Will retry logging into the Apple Account later" + # Gb.username = Gb.username_base = Gb.password = '' except Exception as err: - if this_fct_error_flag is False: - log_exception(err) - return - - event_msg =(f"{EVLOG_ALERT}iCloud3 Error > An error occurred logging into the iCloud Account. " - f"Error-{err}") - post_error_msg(event_msg) + PyiCloud = Gb.PyiCloudLoggingInto + login_err = str(err) log_exception(err) - return False - return True + Gb.PyiCloud_by_username[username] = PyiCloud + if Gb.PyiCloud and Gb.PyiCloud.response_code_pwsrp_err == 503: + list_add(Gb.username_pyicloud_503_connection_error, username) + + else: + list_add(Gb.usernames_setup_error_retry_list, username) + + # list_del(Gb.PyiCloud_by_username, username) + post_error_msg( f"{EVLOG_ALERT}{login_err}") + post_startup_alert(f"Apple Acct > {username_base}, Login Failed") + + return PyiCloud #-------------------------------------------------------------------- -def reset_authentication_time(PyiCloud, authentication_took_secs): - display_authentication_msg(PyiCloud) +def verify_icloud_device_info_received(PyiCloud): + if (PyiCloud is None + or PyiCloud.DeviceSvc is None): + return False + if PyiCloud.DeviceSvc.devices_cnt >= 0: + return True + + Gb.get_ICLOUD_devices_retry_cnt = 0 + + while Gb.get_ICLOUD_devices_retry_cnt < 8: + Gb.get_ICLOUD_devices_retry_cnt += 1 + if Gb.get_ICLOUD_devices_retry_cnt > 1: + post_event( f"Apple Acct > {PyiCloud.account_owner}, " + f"Family Sharing List Refresh " + f"(#{Gb.get_ICLOUD_devices_retry_cnt} of 8)") + + PyiCloud.DeviceSvc.refresh_client(locate_all_devices=True) + + if PyiCloud.DeviceSvc.devices_cnt >= 0: + return True + + post_event( f"{EVLOG_ERROR}Apple Account > {PyiCloud.account_owner}, " + f"Family Sharing List Refresh failed") + + return (PyiCloud.DeviceSvc.devices_cnt >= 0) + +#-------------------------------------------------------------------- def display_authentication_msg(PyiCloud): ''' If an authentication was done, update the count & time and display @@ -257,22 +294,13 @@ def display_authentication_msg(PyiCloud): if authentication_method == '': return - last_authenticated_time = Gb.authenticated_time + last_authenticated_secs = PyiCloud.last_authenticated_secs + PyiCloud.last_authenticated_secs = time_now_secs() + PyiCloud.authentication_cnt += 1 - Gb.authenticated_time = time_now_secs() - Gb.pyicloud_authentication_cnt += 1 - - event_msg =(f"iCloud Acct Auth " - f"#{Gb.pyicloud_authentication_cnt} > {authentication_method}, " - f"Last-{secs_to_time(last_authenticated_time)}") - if instr(authentication_method, 'Password') is False: - event_msg += f" ({format_age(last_authenticated_time)})" - - - if instr(authentication_method, 'Password'): - post_event(event_msg) - else: - post_monitor_msg(event_msg) + event_msg =(f"Apple Acct > {PyiCloud.account_owner}, " + f"Auth #{PyiCloud.authentication_cnt} > {authentication_method}") + post_monitor_msg(event_msg) #-------------------------------------------------------------------- def is_authentication_2fa_code_needed(PyiCloud, initial_setup=False): @@ -292,9 +320,38 @@ def is_authentication_2fa_code_needed(PyiCloud, initial_setup=False): return False if new_2fa_authentication_code_requested(PyiCloud, initial_setup): + alert_msg =(f"Apple Acct > {PyiCloud.account_owner}, Authentication Needed") + post_startup_alert(alert_msg) + log_warning_msg(alert_msg) + if PyiCloud.new_2fa_code_already_requested_flag is False: + post_event(f"{EVLOG_ALERT}{alert_msg}") + # Tell HA to generate reauth needed notification that will be handled + # handled in config_flow Gb.hass.add_job(Gb.config_entry.async_start_reauth, Gb.hass) + PyiCloud.new_2fa_code_already_requested_flag = True + post_event( f"Apple Acct > {PyiCloud.account_owner}, " + f"Authentication Request Submitted to HA") + Gb.PyiCloud_needing_reauth_via_ha = { + CONF_USERNAME: PyiCloud.username, + CONF_PASSWORD: PyiCloud.password, + 'account_owner': PyiCloud.account_owner} + +#-------------------------------------------------------------------- +def send_totp_key(conf_apple_accts): + ''' + The 30-sec check in icloud3_main identified apple accts that need to be verified + using the otp token. Generate the token and send to the Apple acct + ''' + return + + for conf_apple_acct in conf_apple_accts: + PyCloud = Gb.PyiCloud_by_username[conf_apple_acct[CONF_USERNAME]] + OTP = pyotp.TOTP(conf_apple_acct[CONF_TOTP_KEY].replace('-', '')) + otp_code = OTP.now() + #_evlog(f"{conf_apple_acct[CONF_USERNAME]} {otp_code}") + pass #-------------------------------------------------------------------- def check_all_devices_online_status(): @@ -366,100 +423,3 @@ def new_2fa_authentication_code_requested(PyiCloud, initial_setup=False): except Exception as err: internal_error_msg2('Apple ID Verification', traceback.format_exc) return True - -#-------------------------------------------------------------------- -def pyicloud_reset_session(PyiCloud=None): - ''' - Reset the current session and authenticate to restart pyicloud_ic3 - and enter a new verification code - ''' - if PyiCloud is None: - PyiCloud = Gb.PyiCloud - requested_by = ' (Apple)' - else: - requested_by = ' (User)' - - if Gb.PyiCloud is None: - return - - resetting_primary_pyicloud_flag = (PyiCloud == Gb.PyiCloud) - try: - if Gb.authentication_alert_displayed_flag is False: - Gb.authentication_alert_displayed_flag = True - - if requested_by == ' (Apple)': - post_event( f"{EVLOG_NOTICE}Select `HA Notifications Bell`, then Select `Integration Requires " - f"Reconfiguration > Check it out`, then `iCloud3 > Reconfigure`." - f"{CRLF}Note: If the code is not accepted or has expired, request a " - f"new code and try again") - post_event(f"{EVLOG_NOTICE}iCLOUD ALERT > Apple ID Verification is needed {requested_by}") - post_event(f"{EVLOG_NOTICE}Resetting iCloud Session Files") - - delete_pyicloud_cookies_session_files() - - post_event(f"{EVLOG_NOTICE}Initializing iCloud Interface") - PyiCloud.__init__( Gb.username, Gb.password, - cookie_directory=Gb.icloud_cookies_dir, - session_directory=(f"{Gb.icloud_cookies_dir}/session"), - with_family=True, - instance='reset') - - # Initialize PyiCloud object to force a new one that will trigger the 2fa process - PyiCloud = None - Gb.verification_code = None - - authenticate_icloud_account(PyiCloud, initial_setup=True) - - post_event(f"{EVLOG_NOTICE}Waiting for 6-digit Verification Code Entry") - - Gb.EvLog.update_event_log_display(Gb.EvLog.devicename) - - except Exception as err: - log_exception(err) - -#-------------------------------------------------------------------- -def delete_pyicloud_cookies_session_files(cookie_filename=None): - ''' - Delete the cookies and session files as part of the reset_session and request_verification_code - This is called from config_flow/setp_reauth and pyicloud_reset_session - - ''' - cookie_directory = Gb.PyiCloud.cookie_directory - cookie_filename = cookie_filename or Gb.PyiCloud.cookie_filename - session_directory = f"{cookie_directory}/session" - - delete_msg = f"Deleting iCloud Cookie & Session Files > ({cookie_directory})" - delete_msg += f"{CRLF_DOT}Cookies ({cookie_filename})" - delete_file('iCloud Acct cookies', cookie_directory, cookie_filename, delete_old_sv_file=True) - delete_msg += f"{CRLF_DOT}Token Password ({cookie_filename}.tpw)" - delete_file('iCloud Acct tokenpw', session_directory, f"{cookie_filename}.tpw") - delete_msg += f"{CRLF_DOT}Session (/session/{cookie_filename})" - delete_file('iCloud Acct session', session_directory, cookie_filename, delete_old_sv_file=True) - post_monitor_msg(delete_msg) - -#-------------------------------------------------------------------- -def create_PyiCloudService_secondary(username, password, - endpoint_suffix, instance, - verify_password, request_verification_code=False): - ''' - Create the PyiCloudService object without going through the error checking and - authentication test routines. This is used by config_flow to open a second - PyiCloud session - ''' - PyiCloud = PyiCloudService( username, password, - cookie_directory=Gb.icloud_cookies_dir, - session_directory=(f"{Gb.icloud_cookies_dir}/session"), - endpoint_suffix=endpoint_suffix, - instance=instance, - verify_password=verify_password, - request_verification_code=request_verification_code) - - #PyiCloud.instance = instance #f"{instance}-{str(id(PyiCloud))[-5:]}" - - log_debug_msg(f"PyiCloud Instance Created > {PyiCloud.instance}") - 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/pyicloud_srp.py b/custom_components/icloud3/support/pyicloud_srp.py new file mode 100644 index 0000000..0c9d9e6 --- /dev/null +++ b/custom_components/icloud3/support/pyicloud_srp.py @@ -0,0 +1,450 @@ +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# PYTHON SRP MODULE +# +# v1.0.22 - 11/1/2024 https://github.com/cocagne/pysrp +# +# +# N A large safe prime (N = 2q+1, where q is prime) +# All arithmetic is done modulo N. +# g A generator modulo N +# k Multiplier parameter (k = H(N, g) in SRP-6a, k = 3 for legacy SRP-6) +# s User's salt +# I Username +# p Cleartext Password +# H() One-way hash function +# ^ (Modular) Exponentiation +# u Random scrambling parameter +# a,b Secret ephemeral values +# A,B Public ephemeral values +# x Private key (derived from p and s) +# v Password verifier +# +# #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + +import hashlib +import os +import binascii +import six + + +_rfc5054_compat = False +_no_username_in_x = False + +def rfc5054_enable(enable=True): + global _rfc5054_compat + _rfc5054_compat = enable + +def no_username_in_x(enable=True): + global _no_username_in_x + _no_username_in_x = enable + + +SHA1 = 0 +SHA224 = 1 +SHA256 = 2 +SHA384 = 3 +SHA512 = 4 + +NG_1024 = 0 +NG_2048 = 1 +NG_4096 = 2 +NG_8192 = 3 +NG_CUSTOM = 4 + +_hash_map = { SHA1 : hashlib.sha1, + SHA224 : hashlib.sha224, + SHA256 : hashlib.sha256, + SHA384 : hashlib.sha384, + SHA512 : hashlib.sha512 } + + +_ng_const = ( +# 1024-bit +('''\ +EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C9C256576D674DF7496\ +EA81D3383B4813D692C6E0E0D5D8E250B98BE48E495C1D6089DAD15DC7D7B46154D6B6CE8E\ +F4AD69B15D4982559B297BCF1885C529F566660E57EC68EDBC3C05726CC02FD4CBF4976EAA\ +9AFD5138FE8376435B9FC61D2FC0EB06E3''', +"2"), +# 2048 +('''\ +AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4\ +A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF60\ +95179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF\ +747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B907\ +8717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB37861\ +60279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DB\ +FBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73''', +"2"), +# 4096 +('''\ +FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08\ +8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B\ +302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9\ +A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6\ +49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8\ +FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D\ +670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C\ +180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718\ +3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D\ +04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D\ +B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226\ +1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C\ +BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC\ +E0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B26\ +99C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB\ +04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2\ +233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127\ +D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199\ +FFFFFFFFFFFFFFFF''', +"5"), +# 8192 +('''\ +FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08\ +8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B\ +302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9\ +A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6\ +49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8\ +FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D\ +670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C\ +180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718\ +3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D\ +04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D\ +B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226\ +1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C\ +BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC\ +E0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B26\ +99C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB\ +04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2\ +233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127\ +D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934028492\ +36C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406\ +AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918\ +DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B33205151\ +2BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03\ +F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97F\ +BEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AA\ +CC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58B\ +B7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632\ +387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E\ +6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA\ +3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C\ +5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD9\ +22222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC886\ +2F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A6\ +6D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC5\ +0846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268\ +359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6\ +FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E71\ +60C980DD98EDD3DFFFFFFFFFFFFFFFFF''', +'0x13') +) + +def get_ng( ng_type, n_hex, g_hex ): + if ng_type < NG_CUSTOM: + n_hex, g_hex = _ng_const[ ng_type ] + return int(n_hex,16), int(g_hex,16) + + +def bytes_to_long(s): + n = 0 + for b in six.iterbytes(s): + n = (n << 8) | b + return n + + +def long_to_bytes(n): + l = list() + x = 0 + off = 0 + while x != n: + b = (n >> off) & 0xFF + l.append( chr(b) ) + x = x | (b << off) + off += 8 + l.reverse() + return six.b(''.join(l)) + + +def get_random( nbytes ): + return bytes_to_long( os.urandom( nbytes ) ) + + +def get_random_of_length( nbytes ): + offset = (nbytes*8) - 1 + return get_random( nbytes ) | (1 << offset) + + +def old_H( hash_class, s1, s2 = '', s3=''): + if isinstance(s1, six.integer_types): + s1 = long_to_bytes(s1) + if s2 and isinstance(s2, six.integer_types): + s2 = long_to_bytes(s2) + if s3 and isinstance(s3, six.integer_types): + s3 = long_to_bytes(s3) + s = s1 + s2 + s3 + return long(hash_class(s).hexdigest(), 16) + + +def H( hash_class, *args, **kwargs ): + width = kwargs.get('width', None) + + h = hash_class() + + for s in args: + if s is not None: + data = long_to_bytes(s) if isinstance(s, six.integer_types) else s + if width is not None and _rfc5054_compat: + h.update( bytes(width - len(data))) + h.update( data ) + + return h.digest() + + + +#N = 0xAC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73; +#g = 2; +#k = H(N,g) + +def HNxorg( hash_class, N, g ): + bin_N = long_to_bytes(N) + bin_g = long_to_bytes(g) + + padding = len(bin_N) - len(bin_g) if _rfc5054_compat else 0 + + hN = hash_class( bin_N ).digest() + hg = hash_class( b''.join( [b'\0'*padding, bin_g] ) ).digest() + + return six.b( ''.join( chr( six.indexbytes(hN, i) ^ six.indexbytes(hg, i) ) for i in range(0,len(hN)) ) ) + + + +def gen_x( hash_class, salt, username, password ): + username = username.encode() if hasattr(username, 'encode') else username + password = password.encode() if hasattr(password, 'encode') else password + if _no_username_in_x: + username = six.b('') + return bytes_to_long( H(hash_class, salt, H( hash_class, username + six.b(':') + password ) )) + + + + +def create_salted_verification_key( username, password, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None, salt_len=4, k_hex=None ): + if ng_type == NG_CUSTOM and (n_hex is None or g_hex is None): + raise ValueError("Both n_hex and g_hex are required when ng_type = NG_CUSTOM") + hash_class = _hash_map[ hash_alg ] + N,g = get_ng( ng_type, n_hex, g_hex ) + _s = long_to_bytes( get_random( salt_len ) ) + _v = long_to_bytes( pow(g, gen_x( hash_class, _s, username, password ), N) ) + + return _s, _v + + + +def calculate_M( hash_class, N, g, I, s, A, B, K ): + I = I.encode() if hasattr(I, 'encode') else I + h = hash_class() + h.update( HNxorg( hash_class, N, g ) ) + h.update( hash_class(I).digest() ) + if isinstance(s, six.integer_types): + s = long_to_bytes(s) + h.update( s ) + h.update( long_to_bytes(A) ) + h.update( long_to_bytes(B) ) + h.update( K ) + return h.digest() + + +def calculate_H_AMK( hash_class, A, M, K ): + h = hash_class() + h.update( long_to_bytes(A) ) + h.update( M ) + h.update( K ) + return h.digest() + + + + +class Verifier: + + def __init__(self, username, bytes_s, bytes_v, bytes_A=None, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None, bytes_b=None, k_hex=None): + if ng_type == NG_CUSTOM and (n_hex is None or g_hex is None): + raise ValueError("Both n_hex and g_hex are required when ng_type = NG_CUSTOM") + if bytes_b and len(bytes_b) != 256: + raise ValueError("256 bytes required for bytes_b") + self.s = bytes_s + self.v = bytes_to_long(bytes_v) + self.I = username + self.K = None + self._authenticated = False + + self.safety_failed = False + + N,g = get_ng( ng_type, n_hex, g_hex ) + hash_class = _hash_map[ hash_alg ] + if k_hex is None: + k = bytes_to_long( H( hash_class, N, g, width=len(long_to_bytes(N)) )) + else: + k = int(k_hex, 16) + + self.hash_class = hash_class + self.N = N + self.g = g + self.k = k + + if bytes_A: + self._set_A(bytes_A) + + if not self.safety_failed: + if bytes_b: + self.b = bytes_to_long(bytes_b) + else: + self.b = get_random_of_length( 256 ) + self.B = (k*self.v + pow(g, self.b, N)) % N + + + def authenticated(self): + return self._authenticated + + + def get_username(self): + return self.I + + + def get_ephemeral_secret(self): + return long_to_bytes(self.b) + + + def get_session_key(self): + return self.K if self._authenticated else None + + # returns (bytes_s, bytes_B) on success, (None,None) if SRP-6a safety check fails + def get_challenge(self): + if self.safety_failed: + return None,None + else: + return (self.s, long_to_bytes(self.B)) + + # returns H_AMK on success, None on failure + def verify_session(self, user_M, bytes_A=None): + if bytes_A: + self._set_A(bytes_A) + if not hasattr(self, 'A'): + raise ValueError("bytes_A must be provided through Verifier constructor or verify_session parameter.") + if not self.safety_failed: + self._derive_H_AMK() + if user_M == self.M: + self._authenticated = True + return self.H_AMK + + + def _set_A(self, bytes_A): + self.A = bytes_to_long(bytes_A) + # SRP-6a safety check + self.safety_failed = self.A % self.N == 0 + + + def _derive_H_AMK(self): + self.u = bytes_to_long(H(self.hash_class, self.A, self.B, width=len(long_to_bytes(self.N)))) + self.S = pow(self.A*pow(self.v, self.u, self.N ), self.b, self.N) + self.K = self.hash_class( long_to_bytes(self.S) ).digest() + self.M = calculate_M( self.hash_class, self.N, self.g, self.I, self.s, self.A, self.B, self.K ) + self.H_AMK = calculate_H_AMK( self.hash_class, self.A, self.M, self.K ) + + + + +class User: + def __init__(self, username, password, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None, bytes_a=None, bytes_A=None, k_hex=None): + if ng_type == NG_CUSTOM and (n_hex is None or g_hex is None): + raise ValueError("Both n_hex and g_hex are required when ng_type = NG_CUSTOM") + if bytes_a and len(bytes_a) != 256: + raise ValueError("256 bytes required for bytes_a") + N,g = get_ng( ng_type, n_hex, g_hex ) + hash_class = _hash_map[ hash_alg ] + if k_hex is None: + k = bytes_to_long(H( hash_class, N, g, width=len(long_to_bytes(N)) )) + else: + k = int(k_hex, 16) + + self.I = username + self.p = password + if bytes_a: + self.a = bytes_to_long(bytes_a) + else: + self.a = get_random_of_length( 256 ) + if bytes_A: + self.A = bytes_to_long(bytes_A) + else: + self.A = pow(g, self.a, N) + self.v = None + self.M = None + self.K = None + self.H_AMK = None + self._authenticated = False + + self.hash_class = hash_class + self.N = N + self.g = g + self.k = k + + + def authenticated(self): + return self._authenticated + + + def get_username(self): + return self.I + + + def get_ephemeral_secret(self): + return long_to_bytes(self.a) + + + def get_session_key(self): + return self.K if self._authenticated else None + + + def start_authentication(self): + return (self.I, long_to_bytes(self.A)) + + + # Returns M or None if SRP-6a safety check is violated + def process_challenge(self, bytes_s, bytes_B): + + self.s = bytes_s + self.B = bytes_to_long( bytes_B ) + + N = self.N + g = self.g + k = self.k + + hash_class = self.hash_class + + # SRP-6a safety check + if (self.B % N) == 0: + return None + + self.u = bytes_to_long(H( hash_class, self.A, self.B, width=len(long_to_bytes(N)) )) + + # SRP-6a safety check + if self.u == 0: + return None + + self.x = gen_x( hash_class, self.s, self.I, self.p ) + + self.v = pow(g, self.x, N) + + self.S = pow((self.B - k*self.v), (self.a + self.u*self.x), N) + + self.K = hash_class( long_to_bytes(self.S) ).digest() + self.M = calculate_M( hash_class, N, g, self.I, self.s, self.A, self.B, self.K ) + self.H_AMK = calculate_H_AMK(hash_class, self.A, self.M, self.K) + + return self.M + + + def verify_session(self, host_HAMK): + if self.H_AMK == host_HAMK: + self._authenticated = True \ No newline at end of file diff --git a/custom_components/icloud3/support/restore_state.py b/custom_components/icloud3/support/restore_state.py index 87d9b95..99e56be 100644 --- a/custom_components/icloud3/support/restore_state.py +++ b/custom_components/icloud3/support/restore_state.py @@ -8,13 +8,19 @@ LAST_ZONE, LAST_ZONE_DNAME, LAST_ZONE_FNAME, LAST_ZONE_NAME, DIR_OF_TRAVEL, ) -from ..helpers.common import (instr, load_json_file, save_json_file, ) -from ..helpers.messaging import (log_info_msg, log_debug_msg, log_exception, _trace, _traceha, ) -from ..helpers.time_util import (datetime_now, ) +from ..helpers.common import (instr, ) +from ..helpers.file_io import (file_exists, read_json_file, save_json_file, async_save_json_file, ) +from ..helpers.messaging import (log_info_msg, log_debug_msg, log_exception, _evlog, _log, ) +from ..helpers.time_util import (datetime_now, time_now_secs, utcnow, s2t, datetime_plus, ) -import os import json import logging + +from homeassistant.core import (callback, ) +from homeassistant.helpers.event import (async_track_point_in_time, ) +import homeassistant.util.dt as dt_util +from datetime import datetime, timedelta, timezone + # _LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(f"icloud3") @@ -26,7 +32,7 @@ def load_storage_icloud3_restore_state_file(): try: - if os.path.exists(Gb.icloud3_restore_state_filename) is False: + if file_exists(Gb.icloud3_restore_state_filename) is False: build_initial_restore_state_file_structure() write_storage_icloud3_restore_state_file() @@ -84,17 +90,18 @@ def read_storage_icloud3_restore_state_file(): ''' try: - Gb.restore_state_file_data = load_json_file(Gb.icloud3_restore_state_filename) + Gb.restore_state_file_data = read_json_file(Gb.icloud3_restore_state_filename) if Gb.restore_state_file_data == {}: return False - # with open(Gb.icloud3_restore_state_filename, 'r') as f: - # Gb.restore_state_file_data = json.load(f) - Gb.restore_state_profile = Gb.restore_state_file_data['profile'] Gb.restore_state_devices = Gb.restore_state_file_data['devices'] + # The sensors here are used in sensor.py to set the device's last sensor state values when + # the sensors are being set up. They are not related to the Device.sensors{} and the + # Device.sensors{} are not loaded here. The Device.sensors{} are loaded in the + # Device._restore_sensors_from_restore_state_file() function when the Device object is created. for devicename, devicename_data in Gb.restore_state_devices.items(): sensors = devicename_data['sensors'] sensors[DISTANCE_TO_OTHER_DEVICES] = {} @@ -109,8 +116,6 @@ def read_storage_icloud3_restore_state_file(): return True - # except json.decoder.JSONDecodeError: - # pass except Exception as err: log_exception(err) return False @@ -120,24 +125,44 @@ def read_storage_icloud3_restore_state_file(): #-------------------------------------------------------------------- def write_storage_icloud3_restore_state_file(): ''' - Update the config/.storage/.icloud3.restore_state file + Update the config/.storage/.icloud3.restore_state file when the sensors for + a device have changed. Since the multiple sensors are updated on one tracking + update, the update to the restore file is done on a 10-sec delay to catch + other sensors that have been changed. + + The changes are committed based on the 10-sec timer event being fired in a + callback function. ''' Gb.restore_state_profile['last_update'] = datetime_now() Gb.restore_state_file_data['profile'] = Gb.restore_state_profile Gb.restore_state_file_data['devices'] = Gb.restore_state_devices + Gb.restore_state_commit_cnt += 1 + if Gb.restore_state_commit_time == 0: + Gb.restore_state_commit_time = time_now_secs() + 10 + Gb.restore_state_commit_cnt = 0 + + async_track_point_in_time(Gb.hass, + _async_commit_storage_icloud3_restore_state_file_changes, + datetime_plus(utcnow(), secs=10)) + +#-------------------------------------------------------------------- +@callback +async def _async_commit_storage_icloud3_restore_state_file_changes(callback_datetime_struct): try: - # with open(Gb.icloud3_restore_state_filename, 'w', encoding='utf8') as f: - # json.dump(Gb.restore_state_file_data, f, indent=4) - success = save_json_file(Gb.icloud3_restore_state_filename, Gb.restore_state_file_data) + Gb.restore_state_profile['last_commit'] = datetime_now() + Gb.restore_state_profile['recds_changed'] = Gb.restore_state_commit_cnt + + success = await async_save_json_file(Gb.icloud3_restore_state_filename, Gb.restore_state_file_data) + + Gb.restore_state_commit_time = 0 + Gb.restore_state_commit_cnt = 0 return success except Exception as err: log_exception(err) - return False - #-------------------------------------------------------------------- def _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 dcd99ae..ae2e1e9 100644 --- a/custom_components/icloud3/support/service_handler.py +++ b/custom_components/icloud3/support/service_handler.py @@ -27,7 +27,7 @@ post_evlog_greenbar_msg, clear_evlog_greenbar_msg, more_info, log_info_msg, log_debug_msg, log_exception, - _trace, _traceha, ) + _evlog, _log, ) from ..helpers.time_util import (secs_to_time, time_str_to_secs, datetime_now, secs_since, time_now_secs, time_now, ) @@ -85,7 +85,7 @@ 'Restart iCloud3': 'restart', 'Pause Tracking': 'pause', 'Resume Tracking': 'resume', - 'Locate Device(s) using iCloud FamShr': 'locate', + 'Locate Device(s) using iCloud iCloud': 'locate', 'Send Locate Request to iOS App': 'locate iosapp', 'Send Locate Request to Mobile App': 'locate mobapp', 'Send Locate Request to Mobile App': 'locate mobileapp' @@ -159,9 +159,9 @@ def process_lost_device_alert_service_request(call): result_msg = ( f"Required field missing, device_name-{devicename}, " f"number-{number}, message-{message}") - elif (Device.PyiCloud_RawData_famshr - and Device.PyiCloud_RawData_famshr.device_data - and Device.PyiCloud_RawData_famshr.device_data.get(ICLOUD_LOST_MODE_CAPABLE, False)): + elif (Device.PyiCloud_RawData_icloud + and Device.PyiCloud_RawData_icloud.device_data + and Device.PyiCloud_RawData_icloud.device_data.get(ICLOUD_LOST_MODE_CAPABLE, False)): lost_device_alert_service_handler(devicename, number, message) @@ -257,7 +257,7 @@ def update_service_handler(action_entry=None, action_fname=None, devicename=None - pause-resume - same as above but toggles between pause and resume - reset - reset everything and rescans all of the devices - location - request location update from mobile app - - locate x mins - locate in x minutes from FamShr or FmF + - locate x mins - locate in x minutes from iCloud - locate iosapp - request location update from ios app - locate mobapp - request location update from mobile app - locate mobile - request location update from mobile app @@ -381,7 +381,11 @@ def _handle_global_action(global_action, action_option): elif global_action == CMD_RESET_PYICLOUD_SESSION: # This will be handled in the 5-second ic3 loop - Gb.evlog_action_request = CMD_RESET_PYICLOUD_SESSION + # Gb.evlog_action_request = CMD_RESET_PYICLOUD_SESSION + post_event(f"{EVLOG_ERROR}The `Action > Request Apple Verification Code` " + f"is no longer available. This must be done using the " + f"`Configuration > Enter/Request An Apple Account Verification " + f"Code` screen") return elif global_action == CMD_LOG_LEVEL: @@ -389,14 +393,15 @@ def _handle_global_action(global_action, action_option): return elif global_action == CMD_WAZEHIST_MAINTENANCE: - event_msg = "Waze History > Recalculate Route Time/Distance " + event_msg = f"{EVLOG_ALERT}Waze History > Recalculate Route Time/Dist. " if Gb.wazehist_recalculate_time_dist_flag: event_msg += "Starting Immediately" post_event(event_msg) Gb.WazeHist.wazehist_recalculate_time_dist_all_zones() else: Gb.wazehist_recalculate_time_dist_flag = True - event_msg += "Scheduled to run tonight at Midnight" + event_msg+=(f"Scheduled to run tonight at Midnight. " + "SELECT AGAIN TO RUN IMMEDIATELY") post_event(event_msg) elif global_action == CMD_WAZEHIST_TRACK: @@ -478,15 +483,15 @@ def _handle_action_device_locate(Device, action_option): else: 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) + if (Gb.use_data_source_ICLOUD is False + or (Device.icloud_device_id is None) or Device.is_data_source_ICLOUD is False): 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") - return + # elif Device.is_offline: + # post_event(Device, "The device is offline, iCloud Location Tracking is not available") + # return try: interval_secs = time_str_to_secs(action_option) @@ -534,24 +539,26 @@ def issue_ha_notification(): #-------------------------------------------------------------------- def find_iphone_alert_service_handler(devicename): """ - Call the lost iPhone function if using th e FamShr tracking method. + Call the lost iPhone function if using th e iCloud tracking method. Otherwise, send a notification to the Mobile App """ Device = Gb.Devices_by_devicename[devicename] - if Device.is_data_source_FAMSHR: - device_id = Device.device_id_famshr - if device_id and Gb.PyiCloud and Gb.PyiCloud.FamilySharing: - Gb.PyiCloud.FamilySharing.play_sound(device_id, subject="Find My iPhone Alert") + if Device.is_data_source_ICLOUD: + device_id = Device.icloud_device_id + if device_id and Device.PyiCloud and Device.PyiCloud.DeviceSvc: + Device.PyiCloud.DeviceSvc.play_sound(device_id, subject="Find My iPhone Alert") + # if device_id and Gb.PyiCloud and Gb.PyiCloud.DeviceSvc: + # Gb.PyiCloud.DeviceSvc.play_sound(device_id, subject="Find My iPhone Alert") post_event(devicename, "iCloud Find My iPhone Alert sent") return - if Device.conf_famshr_device_id and Device.verified is False: - alert_msg =(f"{EVLOG_ALERT}ALERT CAN NOT BE SENT - The FamShr device has been specified " + if Device.conf_icloud_device_id and Device.verified is False: + alert_msg =(f"{EVLOG_ALERT}ALERT CAN NOT BE SENT - The iCloud device has been specified " f"but it was not verified during startup and is not available." - f"{more_info('famshr_find_my_phone_alert_error')}") + f"{more_info('icloud_dind_my_phone_alert_error')}") else: - alert_msg =("The iCloud FamShr Device was not specified or is not available. " + alert_msg =("The iCloud iCloud Device was not specified or is not available. " "The alert will be sent using the Mobile App") post_event(devicename, alert_msg) @@ -571,17 +578,19 @@ def find_iphone_alert_service_handler(devicename): #-------------------------------------------------------------------- def lost_device_alert_service_handler(devicename, number, message=None): """ - Call the lost iPhone function if using the FamShr tracking method. + Call the lost iPhone function if using the iCloud tracking method. Otherwise, send a notification to the Mobile App """ if message is None: message = 'This Phone has been lost. Please call this number to report it found.' Device = Gb.Devices_by_devicename[devicename] - if Device.is_data_source_FAMSHR: - device_id = Device.device_id_famshr - if device_id and Gb.PyiCloud and Gb.PyiCloud.FamilySharing: - Gb.PyiCloud.FamilySharing.lost_device(device_id, number=number, message=message) + if Device.is_data_source_ICLOUD: + device_id = Device.icloud_device_id + if device_id and Device.PyiCloud and Device.PyiCloud.DeviceSvc: + Device.PyiCloud.DeviceSvc.lost_device(device_id, number=number, message=message) + # if device_id and Gb.PyiCloud and Gb.PyiCloud.DeviceSvc: + # Gb.PyiCloud.DeviceSvc.lost_device(device_id, number=number, message=message) post_event(devicename, "iCloud Lost Device Alert sent") return diff --git a/custom_components/icloud3/support/start_ic3.py b/custom_components/icloud3/support/start_ic3.py index c262ba7..91c257d 100644 --- a/custom_components/icloud3/support/start_ic3.py +++ b/custom_components/icloud3/support/start_ic3.py @@ -1,23 +1,23 @@ from ..global_variables import GlobalVariables as Gb -from ..const import (ICLOUD3, +from ..const import (VERSION, VERSION_BETA, ICLOUD3, ICLOUD3_VERSION, DOMAIN, ICLOUD3_VERSION_MSG, STORAGE_DIR, STORAGE_KEY_ENTITY_REGISTRY, DEVICE_TRACKER, DEVICE_TRACKER_DOT, NOTIFY, HOME, ERROR, NONE_FNAME, STATE_TO_ZONE_BASE, CMD_RESET_PYICLOUD_SESSION, EVLOG_ALERT, EVLOG_IC3_STARTING, EVLOG_NOTICE, EVLOG_IC3_STAGE_HDR, + CIRCLE_LETTERS_DARK, EVENT_RECDS_MAX_CNT_BASE, EVENT_RECDS_MAX_CNT_ZONE, - CRLF, CRLF_DOT, CRLF_CHK, CRLF_SP3_DOT, CRLF_SP5_DOT, CRLF_HDOT, - CRLF_SP3_STAR, CRLF_INDENT, CRLF_X, CRLF_TAB, DOT, CRLF_SP8_HDOT, CRLF_SP8_DOT, - CRLF_RED_X, RED_X, CRLF_STAR, YELLOW_ALERT, UNKNOWN, - RARROW, NBSP4, NBSP6, CIRCLE_STAR, INFO_SEPARATOR, DASH_20, CHECK_MARK, - ICLOUD, FMF, FAMSHR, APPLE_SPECIAL_ICLOUD_SERVER_COUNTRY_CODE, + CRLF, CRLF_DOT, CRLF_CHK, CRLF_SP3_DOT, HDOT, CRLF_SP5_DOT, CRLF_HDOT, LINK, LLINK, RLINK, + CRLF_SP3_HDOT, CRLF_INDENT, CRLF_X, CRLF_TAB, DOT, CRLF_SP8_HDOT, CRLF_SP8_DOT, + CRLF_RED_X, RED_X, CRLF_STAR, CRLF_YELLOW_ALERT, YELLOW_ALERT, UNKNOWN, + RARROW, NBSP2, NBSP4, NBSP6, CIRCLE_STAR, INFO_SEPARATOR, DASH_20, CHECK_MARK, + ICLOUD, FAMSHR, APPLE_SPECIAL_ICLOUD_SERVER_COUNTRY_CODE, DEVICE_TYPE_FNAME, IPHONE, IPAD, IPOD, WATCH, AIRPODS, MOBAPP, NO_MOBAPP, ICLOUD_DEVICE_STATUS, TIMESTAMP, - INACTIVE_DEVICE, DATA_SOURCE_FNAME, - NAME, FNAME, TITLE, RADIUS, NON_ZONE_ITEM_LIST, FRIENDLY_NAME, + INACTIVE_DEVICE, NAME, FNAME, TITLE, RADIUS, NON_ZONE_ITEM_LIST, FRIENDLY_NAME, LOCATION, LATITUDE, RADIUS, TRIGGER, ZONE, ID, @@ -26,7 +26,7 @@ CONF_VERSION, CONF_VERSION_INSTALL_DATE, CONF_EVLOG_CARD_DIRECTORY, CONF_EVLOG_CARD_PROGRAM, CONF_EVLOG_BTNCONFIG_URL, PICTURE_WWW_STANDARD_DIRS, CONF_PICTURE_WWW_DIRS, - CONF_USERNAME, CONF_PASSWORD, + CONF_APPLE_ACCOUNT, CONF_USERNAME, CONF_PASSWORD, CONF_DATA_SOURCE, CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX, CONF_DEVICE_TYPE, CONF_RAW_MODEL, CONF_MODEL, CONF_MODEL_DISPLAY_NAME, CONF_INZONE_INTERVALS, CONF_TRACK_FROM_ZONES, @@ -48,8 +48,8 @@ CONF_STAT_ZONE_BASE_LATITUDE, CONF_STAT_ZONE_BASE_LONGITUDE, CONF_DISPLAY_TEXT_AS, CONF_IC3_DEVICENAME, CONF_FNAME, CONF_FAMSHR_DEVICENAME, - CONF_FAMSHR_DEVICE_ID, CONF_FAMSHR_DEVICE_ID2, CONF_FMF_DEVICE_ID, - CONF_MOBILE_APP_DEVICE, CONF_FMF_EMAIL, + CONF_FAMSHR_DEVICE_ID, + CONF_MOBILE_APP_DEVICE, CONF_TRACKING_MODE, CONF_DEVICE_TYPE, CONF_INZONE_INTERVAL, CONF_AWAY_TIME_ZONE_1_OFFSET, CONF_AWAY_TIME_ZONE_1_DEVICES, CONF_AWAY_TIME_ZONE_2_OFFSET, CONF_AWAY_TIME_ZONE_2_DEVICES, @@ -58,42 +58,38 @@ ) from ..device import iCloud3_Device from ..zone import iCloud3_Zone -from ..support import config_file +from ..helpers.file_io import (directory_exists, make_directory, copy_file, + get_filename_list, get_directory_filename_list,) from ..helpers import entity_io +from ..support import config_file from ..support import mobapp_interface from ..support import mobapp_data_handler from ..support import pyicloud_ic3_interface from ..support import service_handler from ..support import zone_handler from ..support import stationary_zone as statzone +from ..support.pyicloud_ic3_interface import log_into_apple_account from .waze import Waze from .waze_history import WazeRouteHistory as WazeHist -from ..helpers.common import (instr, format_gps, circle_letter, zone_dname, +from ..helpers.common import (instr, is_empty, isnot_empty, circle_letter, zone_dname, strip_lead_comma, is_statzone, isnot_statzone, list_to_str, list_add, list_del, ) from ..helpers.messaging import (broadcast_info_msg, - post_event, post_error_msg, post_monitor_msg, post_startup_alert, + post_event, post_evlog_greenbar_msg, + post_error_msg, post_monitor_msg, post_startup_alert, post_internal_error, log_info_msg, log_debug_msg, log_error_msg, log_warning_msg, log_rawdata, log_exception, format_filename, internal_error_msg2, - _trace, _traceha, more_info, ) + _evlog, _log, more_info, format_header_box,) from ..helpers.dist_util import (format_dist_km, m_to_um, ) -from ..helpers.time_util import (time_now_secs, format_timer, format_time_age, format_age, ) +from ..helpers.time_util import (time_now_secs, mins_since, format_timer, format_time_age, format_age, ) -import os -import json -import shutil -import traceback -from datetime import timedelta, date, datetime from collections import OrderedDict from homeassistant.helpers import event -from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback -from homeassistant.util import slugify -from re import match import logging # _LOGGER = logging.getLogger(__name__) -_LOGGER = logging.getLogger('icloud3') +_LOGGER = logging.getLogger(DOMAIN) #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -104,24 +100,22 @@ def initialize_directory_filenames(): """ Set up common directories and file names """ + # icloud3_config_dir = f"{DOMAIN}.config" + icloud3_config_dir = f"{DOMAIN}" Gb.ha_config_directory = Gb.hass.config.path() Gb.ha_storage_directory = Gb.hass.config.path(STORAGE_DIR) - Gb.ha_storage_icloud3 = Gb.hass.config.path(STORAGE_DIR, 'icloud3') - Gb.icloud3_config_filename = Gb.hass.config.path(STORAGE_DIR, 'icloud3', 'configuration') - Gb.icloud3_restore_state_filename = Gb.hass.config.path(STORAGE_DIR, 'icloud3', 'restore_state') - Gb.wazehist_database_filename = Gb.hass.config.path(STORAGE_DIR, 'icloud3', 'waze_location_history.db') - Gb.icloud3_directory = Gb.hass.config.path('custom_components', 'icloud3') + Gb.ha_storage_icloud3 = Gb.hass.config.path(STORAGE_DIR, icloud3_config_dir) + Gb.icloud3_config_filename = Gb.hass.config.path(STORAGE_DIR, icloud3_config_dir, 'configuration') + Gb.icloud3_restore_state_filename = Gb.hass.config.path(STORAGE_DIR, icloud3_config_dir, 'restore_state') + Gb.wazehist_database_filename = Gb.hass.config.path(STORAGE_DIR, icloud3_config_dir, 'waze_location_history.db') + Gb.icloud3_directory = Gb.hass.config.path('custom_components', DOMAIN) Gb.entity_registry_file = Gb.hass.config.path(STORAGE_DIR, STORAGE_KEY_ENTITY_REGISTRY) + Gb.icloud_cookie_directory = Gb.hass.config.path(STORAGE_DIR, 'icloud3.apple_acct') + Gb.icloud_session_directory = Gb.hass.config.path(STORAGE_DIR, 'icloud3.apple_acct') # Note: The Event Log directory & filename are initialized in config_file.py # after the configuration file has been read - #Set up pyicloud cookies directory & file names - Gb.icloud_cookies_dir = Gb.hass.config.path(STORAGE_DIR, 'icloud') - Gb.icloud_cookies_file = "".join([c for c in Gb.username if match(r"\w", c)]) - if not os.path.exists(Gb.icloud_cookies_dir): - os.makedirs(Gb.icloud_cookies_dir) - #------------------------------------------------------------------------------ # # ICLOUD3 CONFIGURATION PARAMETERS WERE UPDATED VIA CONFIG_FLOW @@ -130,53 +124,50 @@ def initialize_directory_filenames(): # devices based on the type of changes. # #------------------------------------------------------------------------------ -def process_config_flow_parameter_updates(): +def handle_config_parms_update(): - if Gb.config_flow_updated_parms == {''}: + if is_empty(Gb.config_parms_update_control): 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 = {''} + config_parms_update_control = Gb.config_parms_update_control.copy() + Gb.config_parms_update_control = [] post_event( f"Configuration Loading > " - f"Type-{list_to_str(config_flow_updated_parms).title()}") + f"Type-{list_to_str(config_parms_update_control).title()}") - if 'restart' in config_flow_updated_parms: - initialize_icloud_data_source() + if 'restart' in config_parms_update_control: + initialize_data_source_variables() Gb.restart_icloud3_request_flag = True return post_event(f"{EVLOG_IC3_STAGE_HDR}") - if 'general' in config_flow_updated_parms: + if 'general' in config_parms_update_control: set_global_variables_from_conf_parameters() - if 'zone_formats' in config_flow_updated_parms: + if 'special_zone' in Gb.config_parms_update_control: set_zone_display_as() - if 'evlog' in Gb.config_flow_updated_parms: + if 'evlog' in config_parms_update_control: 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: + if 'reauth' in config_parms_update_control: Gb.evlog_action_request = CMD_RESET_PYICLOUD_SESSION - if 'waze' in config_flow_updated_parms: + if 'waze' in config_parms_update_control: set_waze_conf_parameters() - if 'tracking' in config_flow_updated_parms: + if 'tracking' in config_parms_update_control: post_event("Tracking parameters updated") - initialize_icloud_data_source() + initialize_data_source_variables() - elif 'devices' in config_flow_updated_parms: + elif 'devices' in config_parms_update_control: post_event("Device parameters updated") - initialize_icloud_data_source() + initialize_data_source_variables() update_devices_non_tracked_fields() Gb.EvLog.setup_event_log_trackable_device_info() @@ -188,7 +179,6 @@ def process_config_flow_parameter_updates(): Gb.EvLog.display_user_message('') post_event(f"{EVLOG_IC3_STAGE_HDR}Configuration Changes Applied") - Gb.config_flow_updated_parms = {''} #------------------------------------------------------------------------------ # @@ -221,6 +211,7 @@ async def update_lovelace_resource_event_log_js_entry(new_evlog_dir=None): evlog_url = ( f"{Gb.conf_profile[CONF_EVLOG_CARD_DIRECTORY]}/" f"{Gb.conf_profile[CONF_EVLOG_CARD_PROGRAM]}") evlog_url = evlog_url.replace('www', '/local') + evlog_url = evlog_url.replace('/local//local', '/local') evlog_resource_id = None update_lovelace_resources = True @@ -228,6 +219,7 @@ async def update_lovelace_resource_event_log_js_entry(new_evlog_dir=None): # EvLog is in resources, nothing to do if item['url'] == evlog_url: update_lovelace_resources = False + # EvLog is in resources but in another directory if (item['url'].endswith(Gb.conf_profile[CONF_EVLOG_CARD_PROGRAM]) and item['url'] != evlog_url): @@ -341,11 +333,9 @@ def initialize_global_variables(): # Tracking method control vaiables # Used to reset Gb.primary_data_source after pyicloud/icloud account successful reset # Will be changed to MOBAPP if pyicloud errors - Gb.data_source_FAMSHR = False - Gb.data_source_FMF = False + Gb.data_source_ICLOUD = False Gb.data_source_MOBAPP = False - Gb.used_data_source_FMF = False - Gb.used_data_source_FAMSHR = False + Gb.used_data_source_ICLOUD = False Gb.used_data_source_MOBAPP = False Gb.any_data_source_MOBAPP_none = False @@ -373,10 +363,10 @@ def set_global_variables_from_conf_parameters(evlog_msg=True): ''' try: - config_event_msg = "Configure iCloud3 Operations >" - config_event_msg += f"{CRLF_DOT}Load configuration parameters" + config_evlog_msg = "Configure iCloud3 Operations >" + config_evlog_msg += f"{CRLF_DOT}Load configuration parameters" - initialize_icloud_data_source() + initialize_data_source_variables() Gb.www_evlog_js_directory = Gb.conf_profile[CONF_EVLOG_CARD_DIRECTORY] Gb.www_evlog_js_filename = Gb.conf_profile[CONF_EVLOG_CARD_PROGRAM] @@ -412,8 +402,8 @@ def set_global_variables_from_conf_parameters(evlog_msg=True): # Setup the Stationary Zone location and times # The stat_zone_base_lat/long will be adjusted after the Home zone is set up Gb.statzone_fname = Gb.conf_general[CONF_STAT_ZONE_FNAME].strip() - Gb.statzone_base_latitude = Gb.conf_general[CONF_STAT_ZONE_BASE_LATITUDE] - Gb.statzone_base_longitude = Gb.conf_general[CONF_STAT_ZONE_BASE_LONGITUDE] + # Gb.statzone_base_latitude = Gb.conf_general[CONF_STAT_ZONE_BASE_LATITUDE] + # Gb.statzone_base_longitude = Gb.conf_general[CONF_STAT_ZONE_BASE_LONGITUDE] Gb.statzone_still_time_secs = Gb.conf_general[CONF_STAT_ZONE_STILL_TIME] * 60 Gb.statzone_inzone_interval_secs = Gb.conf_general[CONF_STAT_ZONE_INZONE_INTERVAL] * 60 Gb.is_statzone_used = (14400 > Gb.statzone_still_time_secs > 0) @@ -443,31 +433,31 @@ def set_global_variables_from_conf_parameters(evlog_msg=True): if instr(display_text_as, '>'): from_to_text = display_text_as.split('>') Gb.EvLog.display_text_as[from_to_text[0].strip()] = from_to_text[1].strip() - config_event_msg += f"{CRLF_DOT}Set Display Text As Fields ({len(Gb.EvLog.display_text_as)} used)" + config_evlog_msg += f"{CRLF_DOT}Set Display Text As Fields ({len(Gb.EvLog.display_text_as)} used)" set_waze_conf_parameters() # Set other fields and flags based on configuration parameters set_primary_data_source(Gb.primary_data_source) - config_event_msg += ( f"{CRLF_DOT}Set Default Tracking Method " - f"({DATA_SOURCE_FNAME.get(Gb.primary_data_source, Gb.primary_data_source)})") + config_evlog_msg += ( f"{CRLF_DOT}Set Default Tracking Method " + f"({Gb.primary_data_source})") set_log_level(Gb.log_level) - config_event_msg += f"{CRLF_DOT}Initialize Debug Control ({Gb.log_level})" + config_evlog_msg += f"{CRLF_DOT}Initialize Debug Control ({Gb.log_level})" set_um_formats() - config_event_msg += f"{CRLF_DOT}Set Unit of Measure Formats ({Gb.um})" + config_evlog_msg += f"{CRLF_DOT}Set Unit of Measure Formats ({Gb.um})" event_recds_max_cnt = set_event_recds_max_cnt() - config_event_msg += f"{CRLF_DOT}Set Event Log Record Limits ({event_recds_max_cnt} Events)" + config_evlog_msg += f"{CRLF_DOT}Set Event Log Record Limits ({event_recds_max_cnt} Events)" - config_event_msg += f"{CRLF_DOT}Device Tracker State Value Source " + config_evlog_msg += f"{CRLF_DOT}Device Tracker State Value Source " etds = Gb.device_tracker_state_source - config_event_msg += f"{CRLF_INDENT}({DEVICE_TRACKER_STATE_SOURCE_DESC.get(etds, etds)})" + config_evlog_msg += f"{CRLF_INDENT}({DEVICE_TRACKER_STATE_SOURCE_DESC.get(etds, etds)})" if evlog_msg: - post_event(config_event_msg) + post_event(config_evlog_msg) except Exception as err: log_exception(err) @@ -485,8 +475,9 @@ def ha_startup_completed(dummy_parameter): # HA may have not set up the notify service before iC3 starts. If so, the mobile_app # Notify entity was not setup either. Do it now. - check_mobile_app_integration(ha_started_check=True) - setup_notify_service_name_for_mobapp_devices(post_event_msg=True) + Gb.ha_started = True + mobapp_interface.get_mobile_app_integration_device_info(ha_started_check=True) + mobapp_interface.setup_notify_service_name_for_mobapp_devices(post_evlog_msg=True) def ha_stopping(dummy_parameter): post_event("HA Shutting Down") @@ -500,33 +491,42 @@ def ha_restart(dummp_parameter): # This is used during the startup routines and in other routines when errors occur. # #------------------------------------------------------------------------------ -def initialize_icloud_data_source(): +def initialize_data_source_variables(): ''' Set up icloud username/password and devices from the configuration parameters ''' - Gb.username = Gb.conf_tracking[CONF_USERNAME].lower() + # username, password, locate_all = \ + # config_file.apple_acct_username_password(0) + conf_apple_acct, _idx = config_file.conf_apple_acct(0) + conf_username = conf_apple_acct[CONF_USERNAME] + conf_password = conf_apple_acct[CONF_PASSWORD] + + Gb.username = conf_username Gb.username_base = Gb.username.split('@')[0] - Gb.password = Gb.conf_tracking[CONF_PASSWORD] + Gb.password = conf_password Gb.encode_password_flag = Gb.conf_tracking[CONF_ENCODE_PASSWORD] Gb.icloud_server_endpoint_suffix= \ icloud_server_endpoint_suffix(Gb.conf_tracking[CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX]) + Gb.PyiCloud_logging_in_usernames = [] + + Gb.conf_data_source_ICLOUD = instr(Gb.conf_tracking[CONF_DATA_SOURCE], ICLOUD) \ + or instr(Gb.conf_tracking[CONF_DATA_SOURCE], 'icloud') + Gb.conf_data_source_ICLOUD = Gb.conf_data_source_ICLOUD + Gb.conf_data_source_MOBAPP = instr(Gb.conf_tracking[CONF_DATA_SOURCE], MOBAPP) - Gb.conf_data_source_FAMSHR = instr(Gb.conf_tracking[CONF_DATA_SOURCE], FAMSHR) - Gb.conf_data_source_FMF = instr(Gb.conf_tracking[CONF_DATA_SOURCE], FMF) - Gb.conf_data_source_MOBAPP = instr(Gb.conf_tracking[CONF_DATA_SOURCE], MOBAPP) - Gb.conf_data_source_ICLOUD = Gb.conf_data_source_FAMSHR or Gb.conf_data_source_FMF - Gb.primary_data_source_ICLOUD = Gb.conf_data_source_ICLOUD - Gb.primary_data_source = ICLOUD if Gb.primary_data_source_ICLOUD else MOBAPP - Gb.devices = Gb.conf_devices - Gb.icloud_force_update_flag = False + Gb.use_data_source_ICLOUD = Gb.conf_data_source_ICLOUD and Gb.username and Gb.password + Gb.use_data_source_MOBAPP = Gb.conf_data_source_MOBAPP + Gb.primary_data_source = ICLOUD if Gb.use_data_source_ICLOUD else MOBAPP - Gb.get_FAMSHR_devices_retry_cnt = 0 + Gb.devices = Gb.conf_devices + Gb.icloud_force_update_flag = False + Gb.get_ICLOUD_devices_retry_cnt = 0 #------------------------------------------------------------------------------ def icloud_server_endpoint_suffix(endpoint_suffix): ''' Determine the suffix to be used based on the country_code and the value of the - configuration file field. + configuration field. ''' if endpoint_suffix != '': return endpoint_suffix.replace('.', '') @@ -540,10 +540,11 @@ def icloud_server_endpoint_suffix(endpoint_suffix): def set_primary_data_source(data_source): ''' Set up tracking method. These fields will be reset based on the device_id's available - for the Device once the famshr and fmf tracking methods are set up. + for the Device once the icloud tracking method is set up. ''' + if (Gb.conf_profile[CONF_VERSION] > 0 - and Gb.primary_data_source_ICLOUD + and Gb.use_data_source_ICLOUD and (Gb.username == '' or Gb.password == '')): alert_msg =(f"{EVLOG_ALERT}ICLOUD USERNAME/PASSWORD ERROR > The username or password has not " f"been set up, iCloud Location Services will not be used. ") @@ -560,99 +561,8 @@ def set_primary_data_source(data_source): post_error_msg(error_msg) post_event(alert_msg) - if data_source in [FAMSHR, FMF, ICLOUD]: - Gb.primary_data_source_ICLOUD = Gb.conf_data_source_FAMSHR or Gb.conf_data_source_FMF - -#------------------------------------------------------------------------------ -def check_mobile_app_integration(ha_started_check=None): - ''' - Check to see if the Mobile App Integration is installed. If not, the mobapp - Tracking method is not available. Also, set the Gb.mobile_app variables - - Return: - True - It is installed or not needed - False - It is not installed - - {'d7f4264ab72046285ca92c0946f381e167a6ba13292eef17d4f60a4bf0bd654c': - DeviceEntry(area_id=None, config_entries={'ad5c8f66b14fda011107827b383d4757'}, - configuration_url=None, connections=set(), disabled_by=None, entry_type=None, - hw_version=None, id='93e3d6b65eb05072dcb590b46c02d920', - identifiers={('mobile_app', '1A9EAFA3-2448-4F37-B069-3B3A1324EFC5')}, - manufacturer='Apple', model='iPad8,9', name_by_user='Gary-iPad-app', - name='Gary-iPad', serial_number=None, suggested_area=None, sw_version='17.2', - via_device_id=None, is_new=False), - ''' - try: - if Gb.conf_data_source_MOBAPP is False: - return True - - if 'mobile_app' in Gb.hass.data: - Gb.MobileApp_data = Gb.hass.data['mobile_app'] - mobile_app_devices = Gb.MobileApp_data.get('devices', {}) - - Gb.mobile_app_device_fnames = [] - Gb.mobapp_fnames_x_mobapp_id = {} - Gb.mobapp_fnames_disabled = [] - for device_id, device_entry in mobile_app_devices.items(): - if device_entry.disabled_by is None: - Gb.mobile_app_device_fnames = list_add(Gb.mobile_app_device_fnames, device_entry.name_by_user) - Gb.mobile_app_device_fnames = list_add(Gb.mobile_app_device_fnames, device_entry.name) - Gb.mobapp_fnames_x_mobapp_id[device_entry.id] = device_entry.name_by_user or device_entry.name - Gb.mobapp_fnames_x_mobapp_id[device_entry.name] = device_entry.id - Gb.mobapp_fnames_x_mobapp_id[device_entry.name_by_user] = device_entry.id - else: - Gb.mobapp_fnames_disabled = list_add(Gb.mobapp_fnames_disabled, device_entry.id) - Gb.mobapp_fnames_disabled = list_add(Gb.mobapp_fnames_disabled, device_entry.name) - Gb.mobapp_fnames_disabled = list_add(Gb.mobapp_fnames_disabled, device_entry.name_by_user) - - if Gb.mobile_app_device_fnames: - ha_started_check = True - post_event( f"Checking Mobile App Integration > Loaded, " - f"Devices-{list_to_str(Gb.mobile_app_device_fnames)}") - - Gb.debug_log['Gb.mobile_app_device_fnames'] = Gb.mobile_app_device_fnames - Gb.debug_log['Gb.mobapp_fnames_x_mobapp_id'] = Gb.mobapp_fnames_x_mobapp_id - Gb.debug_log['Gb.mobapp_fnames_disabled'] = Gb.mobapp_fnames_disabled - - if len(Gb.mobile_app_device_fnames) == Gb.conf_mobapp_device_cnt: - return True - - # If the check is being done when HA startup is finished and Mobile App is still - # not loaded, it is not available and the Mobile App data source is not available. - # Display an error message since there are devices that use the Mobile App. - if ha_started_check is None: - post_event( f"Checking Mobile App Integration > Not Loaded. " - f"Will check again when HA is started") - return - - # Mobile App Integration not loaded - if len(Gb.mobile_app_device_fnames) == 0: - Gb.conf_data_source_MOBAPP = False - - # Cycle thru conf_devices since the Gb.Device - mobile_app_error_msg = '' - for conf_device in Gb.conf_devices: - if conf_device[CONF_MOBILE_APP_DEVICE] == 'None': - continue - Device = Gb.Devices_by_devicename[conf_device[CONF_IC3_DEVICENAME]] - if Device.conf_mobapp_fname in Gb.mobile_app_device_fnames: - Device.mobapp_monitor_flag = True - else: - Device.mobapp_monitor_flag = False - mobile_app_error_msg +=(f"{CRLF_DOT}{conf_device[CONF_MOBILE_APP_DEVICE]}" - f"{RARROW}Assigned to {Device.fname_devicename}") - - if mobile_app_error_msg: - # post_event( f"{EVLOG_ALERT}MOBILE APP INTEGRATION ERROR > Mobile App devices have been " - # f"configured but the Mobile App Integration has not been installed or an " - # f"Mobile App device is not available. " - # f"The Mobile App will not be used as a data source for that device." - # f"{mobile_app_error_msg}") - return False - - except Exception as err: - log_exception(err) - return False + if data_source in [ICLOUD]: + Gb.use_data_source_ICLOUD = Gb.conf_data_source_ICLOUD #------------------------------------------------------------------------------ # @@ -850,18 +760,17 @@ def check_ic3_event_log_file_version(): www_evlog_js_filename = Gb.hass.config.path(Gb.conf_profile[CONF_EVLOG_CARD_DIRECTORY], Gb.conf_profile[CONF_EVLOG_CARD_PROGRAM]) - # The _l is a html command and will stop the msg from displaying - ic3_evlog_js_directory_msg = "icloud3/event_log_card".replace('_log', '___log') - ic3_evlog_js_filename_msg = ic3_evlog_js_filename.replace('_log', '___log') - www_evlog_js_directory_msg = www_evlog_js_directory.replace('_log', '___log') - www_evlog_js_filename_msg = www_evlog_js_filename.replace('_log', '___log') + ic3_evlog_js_directory_msg = f"{DOMAIN}/event_log_card" + ic3_evlog_js_filename_msg = ic3_evlog_js_filename + www_evlog_js_directory_msg = www_evlog_js_directory + www_evlog_js_filename_msg = www_evlog_js_filename ic3_version, ic3_beta_version, ic3_version_text = _read_event_log_card_js_file(ic3_evlog_js_filename) www_version, www_beta_version, www_version_text = _read_event_log_card_js_file(www_evlog_js_filename) if ic3_version_text == 'Not Installed': Gb.version_evlog = f' Not Found: {ic3_evlog_js_filename_msg}' - post_event( f"iCloud3 Event Log > " + event_msg =(f"{EVLOG_ALERT}iCloud3 Event Log > " f"{CRLF_DOT}Current Version Installed-v{www_version_text}" f"{CRLF_DOT}WARNING: SOURCE FILE NOT FOUND" f"{CRLF_DOT}...{ic3_evlog_js_filename_msg}") @@ -876,10 +785,8 @@ def check_ic3_event_log_file_version(): # Make sure the /config/www and config/CONF_EVLOG_CARD_DIRECTORY exists. Create them if needed if www_version == 0: config_www_directory = Gb.hass.config.path('www') - if os.path.exists(config_www_directory) is False: - os.mkdir(config_www_directory) - if os.path.exists(www_evlog_js_directory) is False: - os.mkdir(www_evlog_js_directory) + make_directory(config_www_directory) + make_directory(www_evlog_js_directory) current_version_installed_flag = True if ic3_version > www_version: @@ -891,9 +798,11 @@ def check_ic3_event_log_file_version(): current_version_installed_flag = False if current_version_installed_flag: - post_event( f"iCloud3 Event Log > " + event_msg =(f"iCloud3 Event Log > " f"{CRLF_DOT}Current Version Installed-v{www_version_text}" f"{CRLF_DOT}File-{format_filename(www_evlog_js_filename_msg)}") + # The _l is a html command and will stop the msg from displaying + post_event(event_msg.replace('_', '_ ')) if Gb.evlog_version != www_version_text: Gb.evlog_version = Gb.conf_profile['event_log_version'] = www_version_text @@ -902,14 +811,14 @@ def check_ic3_event_log_file_version(): return try: - _copy_image_files_to_www_directory(www_evlog_js_directory) - shutil.copy(ic3_evlog_js_filename, www_evlog_js_filename) + _copy_image_files_to_www_directory(ic3_evlog_js_directory, www_evlog_js_directory) + copy_file(ic3_evlog_js_filename, www_evlog_js_filename) Gb.evlog_version = Gb.conf_profile['event_log_version'] = www_version_text config_file.write_storage_icloud3_configuration_file() post_startup_alert('Event Log was updated. Browser refresh needed') - post_event( f"{EVLOG_ALERT}" + event_msg =(f"{EVLOG_ALERT}" f"BROWSER REFRESH NEEDED > iCloud3 Event Log was updated to v{ic3_version_text}" f"{more_info('refresh_browser')}" f"{CRLF}{'-'*75}" @@ -917,6 +826,8 @@ def check_ic3_event_log_file_version(): f"{CRLF_DOT}New Version - v{ic3_version_text}" f"{CRLF_DOT}Copied From - {format_filename(ic3_evlog_js_directory_msg)}/" f"{CRLF_DOT}Copied To.... - {format_filename(www_evlog_js_directory_msg)}/") + # The _l is a html command and will stop the msg from displaying + post_event(event_msg.replace('_', '_ ')) Gb.info_notification = (f"Event Log Card updated to v{ic3_version_text}. " "See Event Log for more info.") @@ -950,7 +861,7 @@ def _read_event_log_card_js_file(evlog_filename): 0, 0, "Not Installed" if the 'icloud3-event-log-card.js' file was not found ''' try: - if os.path.exists(evlog_filename) == False: + if directory_exists(evlog_filename) == False: return (0, 0, 'Not Installed') #Cycle thru the file looking for the line with the 'const version' @@ -995,18 +906,19 @@ def _read_event_log_card_js_file(evlog_filename): return (version, version_beta, version_number_beta) #------------------------------------------------------------------------------ -def _copy_image_files_to_www_directory(www_evlog_directory): +def _copy_image_files_to_www_directory(ic3_evlog_js_directory, www_evlog_directory): ''' Copy any image files from the icloud3/event_log directory to the /www/[event_log_directory] ''' try: image_extensions = ['png', 'jpg', 'jpeg'] - image_filenames = [f'{Gb.icloud3_directory}/event_log_card/{x}' - for x in os.listdir(f"{Gb.icloud3_directory}/event_log_card/") - if instr(x, '.') and x.rsplit('.', 1)[1] in image_extensions] + image_dir_filenames = get_filename_list( + start_dir=f"{Gb.icloud3_directory}/event_log_card/", + file_extn_filter=['png', 'jpg', 'jpeg']) - for image_filename in image_filenames: - shutil.copy(image_filename, www_evlog_directory) + for image_filename in image_dir_filenames: + copy_file( f"{ic3_evlog_js_directory}/{image_filename}", + f"{www_evlog_directory}/{image_filename}") except Exception as err: log_exception(err) @@ -1097,8 +1009,8 @@ def create_Zones_object(): if isnot_statzone(zone) or Zone.is_ha_zone is False: continue - Gb.Zones = list_add(Gb.Zones, Zone) - Gb.HAZones = list_add(Gb.HAZones, Zone) + list_add(Gb.Zones, Zone) + list_add(Gb.HAZones, Zone) Gb.Zones_by_zone[zone] = Zone Gb.HAZones_by_zone[zone] = Zone Gb.zones_dname[zone] = Zone.dname @@ -1115,22 +1027,22 @@ def create_Zones_object(): post_event( f"Primary 'Home' Zone > {zone_dname(Gb.track_from_base_zone)} " f"{circle_letter(Gb.track_from_base_zone)}") - event_msg = "Special Zone Setup >" + evlog_msg = "Special Zone Setup >" if Gb.is_passthru_zone_used: - event_msg += f"{CRLF_DOT}Enter Zone Delay > DelayTime-{format_timer(Gb.passthru_zone_interval_secs)}" + evlog_msg += f"{CRLF_DOT}Enter Zone Delay > DelayTime-{format_timer(Gb.passthru_zone_interval_secs)}" else: - event_msg += f"{CRLF_DOT}ENTER ZONE DELAY IS NOT USED" + evlog_msg += f"{CRLF_DOT}ENTER ZONE DELAY IS NOT USED" dist = Gb.HomeZone.distance_km(Gb.statzone_base_latitude, Gb.statzone_base_longitude) if Gb.is_statzone_used: - event_msg += ( f"{CRLF_DOT}Stationary Zone > " - f"Radius-{Gb.HomeZone.radius_m}m, " - f"DistMoveLimit-{format_dist_km(Gb.statzone_dist_move_limit_km)}, " - f"MinDistFromAnotherZone-{format_dist_km(Gb.statzone_min_dist_from_zone_km)}") + r_ft = f"/{m_to_um(Gb.HomeZone.radius_m)}" if Gb.um_MI else "" + evlog_msg += ( f"{CRLF_DOT}Stationary Zone > {Gb.statzone_fname} (r{Gb.HomeZone.radius_m}m{r_ft}), " + f"{CRLF_HDOT}DistMoveLimit-{format_dist_km(Gb.statzone_dist_move_limit_km)}, " + f"DistFromAnotherZone-{format_dist_km(Gb.statzone_min_dist_from_zone_km)}") else: - event_msg += f"{CRLF_DOT}STATIONARY ZONES ARE NOT USED" - post_event(event_msg) + evlog_msg += f"{CRLF_DOT}STATIONARY ZONES ARE NOT USED" + post_event(evlog_msg) # Cycle thru the Device's conf and get all zones that are tracked from for all devices Gb.TrackedZones_by_zone = {} @@ -1139,7 +1051,7 @@ def create_Zones_object(): if from_zone in Gb.HAZones_by_zone: Gb.TrackedZones_by_zone[from_zone] = Gb.Zones_by_zone[from_zone] - Gb.debug_log['Gb.Zones'] = Gb.Zones + Gb.startup_lists['Gb.Zones'] = Gb.Zones #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -1201,12 +1113,11 @@ def create_Waze_object(): # fields based on the extracted devicename: # device_type # friendly_name -# fmf email address # sensor.picture name # device tracking flags # tracked_devices list # These fields may be overridden by the routines associated with the -# operating mode (fmf, icloud, mobapp) +# operating mode (icloud, mobapp) # #------------------------------------------------------------------------------ def create_Devices_object(): @@ -1215,68 +1126,100 @@ def create_Devices_object(): device_tracker_entities, device_tracker_entity_data = \ entity_io.get_entity_registry_data(platform='icloud3', domain=DEVICE_TRACKER) - old_Devices_by_devicename = Gb.Devices_by_devicename.copy() + old_Devices_by_devicename = Gb.Devices_by_devicename.copy() Gb.Devices = [] Gb.Devices_by_devicename = {} Gb.conf_devicenames = [] - Gb.conf_famshr_devicenames = [] - Gb.devicenames_x_famshr_devices = {} + Gb.conf_icloud_dnames = [] + Gb.devicenames_by_icloud_dname = {} + Gb.icloud_dnames_by_devicename = {} + Gb.conf_startup_errors_by_devicename = {} + + if len(Gb.conf_apple_accounts) != len(Gb.username_valid_by_username): + pyicloud_ic3_interface.verify_all_apple_accounts() for conf_device in Gb.conf_devices: - devicename = conf_device[CONF_IC3_DEVICENAME] + devicename = conf_device[CONF_IC3_DEVICENAME] + icloud_dname = conf_device[CONF_FAMSHR_DEVICENAME] + apple_acct = conf_device[CONF_APPLE_ACCOUNT] + if devicename == '': - post_startup_alert(f"HA device_tracker entity id not configured for {conf_device[CONF_FAMSHR_DEVICENAME]}") + post_startup_alert(f"HA device_tracker entity id not configured for {icloud_dname}") post_event( f"{EVLOG_ALERT}CONFIGURATION ALERT > The device_tracker entity id (devicename) " - f"has not been configured for {conf_device[CONF_FAMSHR_DEVICENAME]}/" - f"{conf_device[CONF_DEVICE_TYPE]}") + f"has not been configured for {icloud_dname}/" + f"{DEVICE_TYPE_FNAME.get(conf_device[CONF_DEVICE_TYPE], conf_device[CONF_DEVICE_TYPE])}") continue - Gb.conf_famshr_devicenames.append(conf_device[CONF_FAMSHR_DEVICENAME]) + Gb.conf_icloud_dnames.append(icloud_dname) broadcast_info_msg(f"Set up Device > {devicename}") if conf_device[CONF_TRACKING_MODE] == INACTIVE_DEVICE: - post_event( f"{CIRCLE_STAR}{devicename} > {conf_device[CONF_FNAME]}/{conf_device[CONF_DEVICE_TYPE]}, INACTIVE, " - f"{CRLF_SP3_DOT}FamShr Device-{conf_device[CONF_FAMSHR_DEVICENAME]}" - # f"{CRLF_SP3_DOT}FmF Device-{conf_device[CONF_FMF_EMAIL]}" - f"{CRLF_SP3_DOT}MobApp Entity-{conf_device[CONF_MOBILE_APP_DEVICE]}") + post_event( f"{CIRCLE_STAR} {conf_device[CONF_FNAME]} ({devicename}) > " + f"{DEVICE_TYPE_FNAME.get(conf_device[CONF_DEVICE_TYPE], conf_device[CONF_DEVICE_TYPE])}, INACTIVE, " + f"{CRLF_DOT}iCloud Device-{icloud_dname}" + f"{CRLF_DOT}MobApp Entity-{conf_device[CONF_MOBILE_APP_DEVICE]}") continue # This list is based on the configuration, it will be rebuilt when the devices are verified - # after the FamShr data has been read + # after the iCloud data has been read. It is also rebuild in config_flow when a device is updated devicename = conf_device.get(CONF_IC3_DEVICENAME) - famshr_name = conf_device.get(CONF_FAMSHR_DEVICENAME) - Gb.devicenames_x_famshr_devices[devicename] = famshr_name - Gb.devicenames_x_famshr_devices[famshr_name] = devicename + Gb.devicenames_by_icloud_dname[icloud_dname] = devicename + Gb.icloud_dnames_by_devicename[devicename] = icloud_dname # Reinitialize or add device, preserve the Sensor object if reinitializing if devicename in old_Devices_by_devicename: Device = old_Devices_by_devicename[devicename] Device.__init__(devicename, conf_device) - post_monitor_msg(f"INITIALIZED Device > {Device.fname} ({devicename})") + post_monitor_msg(f"INITIALIZED Device > {Device.fname_devicename}") + else: Device = iCloud3_Device(devicename, conf_device) - post_monitor_msg(f"ADDED Device > {Device.fname} ({devicename})") + + post_monitor_msg(f"ADDED Device > {Device.fname_devicename}") Gb.Devices.append(Device) Gb.conf_devicenames.append(devicename) Gb.Devices_by_devicename[devicename] = Device - famshr_dev_msg = Device.conf_famshr_name if Device.conf_famshr_name else 'None' - fmf_dev_msg = Device.conf_fmf_email if Device.conf_fmf_email else 'None' + if Device.conf_apple_acct_username in ['', 'None']: + apple_acct_msg = '✪ NONE' + + # elif Device.conf_apple_acct_username not in Gb.PyiCloud_by_username: + elif Device.conf_apple_acct_username not in Gb.username_valid_by_username: + apple_acct_msg =f"{RED_X}{Device.conf_apple_acct_username}, UNKNOWN APPLE ACCT" + # f"{CRLF_INDENT} {NBSP6} {HDOT}" + # f") + + elif Gb.username_valid_by_username.get(Device.conf_apple_acct_username, False) is False: + apple_acct_msg = f"{RED_X}{Device.conf_apple_acct_username}, INVALID USERNAME/PW" + # elif Device.conf_apple_acct_username in Gb.PyiCloud_by_username: + # PyiCloud = Gb.PyiCloud_by_username[Device.conf_apple_acct_username] + # apple_acct_msg = PyiCloud.account_owner + else: + apple_acct_msg = Device.conf_apple_acct_username + + apple_acct_msg = f"{CRLF_SP5_DOT}Apple Account: {apple_acct_msg}" + icloud_dev_msg = Device.conf_icloud_dname if Device.conf_icloud_dname else '✪ NONE' mobapp_dev_msg = Device.mobapp[DEVICE_TRACKER] \ - if Device.mobapp[DEVICE_TRACKER] else 'Not Monitored' - monitored_msg = '(Monitored)' if Device.is_monitored else '(Tracked)' + if Device.mobapp[DEVICE_TRACKER] else '✪ NONE' + if ((apple_acct_msg == '✪ NONE' or icloud_dev_msg == '✪ NONE') + and mobapp_dev_msg == '✪ NONE'): + monitored_msg = f"{RED_X}NO DATA SOURCE" + elif Device.is_monitored: + monitored_msg = 'Monitored' + else: + monitored_msg = 'Tracked' - event_msg = ( f"{CHECK_MARK}{devicename} > {Device.fname_devtype} {monitored_msg}" - f"{CRLF_SP5_DOT}FamShr Device: {famshr_dev_msg}" - # f"{CRLF_SP5_DOT}FmF Device: {fmf_dev_msg}" + evlog_msg = ( f"{CHECK_MARK}{Device.fname_devicename} > {Device.devtype_fname}, {monitored_msg}" + f"{apple_acct_msg}" + f"{CRLF_SP5_DOT}iCloud Device: {icloud_dev_msg}" f"{CRLF_SP5_DOT}MobApp Entity: {mobapp_dev_msg}") if Device.track_from_base_zone != HOME: - event_msg += f"{CRLF_SP5_DOT}Primary 'Home' Zone: {zone_dname(Device.track_from_base_zone)}" + evlog_msg += f"{CRLF_SP5_DOT}Primary 'Home' Zone: {zone_dname(Device.track_from_base_zone)}" if Device.track_from_zones != [HOME]: - event_msg += f"{CRLF_SP5_DOT}Track from Zones: {', '.join(Device.track_from_zones)}" - post_event(event_msg) + evlog_msg += f"{CRLF_SP5_DOT}Track from Zones: {list_to_str(Device.track_from_zones)}" + post_event(evlog_msg) try: # Added the try/except to not generate an error if the device was not in the registry @@ -1289,16 +1232,17 @@ def create_Devices_object(): except: pass - _verify_away_time_zone_devicenames() - except Exception as err: log_exception(err) - Gb.debug_log['Gb.Devices'] = Gb.Devices - Gb.debug_log['Gb.DevDevices_by_devicename'] = Gb.Devices_by_devicename - Gb.debug_log['Gb.conf_devicenames'] = Gb.conf_devicenames - Gb.debug_log['Gb.conf_famshr_devicenames'] = Gb.conf_famshr_devicenames - Gb.debug_log['Gb.devicenames_x_famshr_devices'] = Gb.devicenames_x_famshr_devices + _verify_away_time_zone_devicenames() + + Gb.startup_lists['Gb.Devices'] = Gb.Devices + Gb.startup_lists['Gb.DevDevices_by_devicename'] = Gb.Devices_by_devicename + Gb.startup_lists['Gb.conf_devicenames'] = Gb.conf_devicenames + Gb.startup_lists['Gb.conf_icloud_dnames'] = Gb.conf_icloud_dnames + Gb.startup_lists['Gb.devicenames_by_icloud_dname'] = Gb.devicenames_by_icloud_dname + Gb.startup_lists['Gb.icloud_dname_by_devicenames'] = Gb.icloud_dnames_by_devicename return @@ -1341,32 +1285,44 @@ def _verify_away_time_zone_devicenames(): # ICLOUD3 STARTUP MODULES -- STAGE 4 # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -def setup_data_source_ICLOUD(): - - if Gb.PyiCloud is None and Gb.PyiCloudInit is None: - pyicloud_ic3_interface.create_PyiCloudService(Gb.PyiCloudInit, instance='startup') - post_event("Setting up iCloud Data Source") - - if pyicloud_ic3_interface.verify_pyicloud_setup_status() is False: - if Gb.PyiCloud is None or Gb.PyiCloudInit is None: - if pyicloud_ic3_interface.verify_pyicloud_setup_status() is False: - return False +def setup_data_source_ICLOUD(retry=False): + ''' + Cycle through all the apple accounts and match their devices to the iCloud3 Device. + Then set the device source for each Device. + Now that all of the devices are set up (natched and unmatched), cycle back througn + the apple accounts and display the info for all accounts and all devices. This is done + so an earlier account that has the same device that is tracked by a later account has + the account info to display. + ''' - if (Gb.PyiCloud is None - or Gb.PyiCloud.FamilySharing is None - or Gb.PyiCloud.FamilySharing.devices_cnt == -1): - return False + apple_acct_not_found_msg = '' + for username, PyiCloud in Gb.PyiCloud_by_username.items(): + if PyiCloud and Gb.username_valid_by_username.get(username): + setup_tracked_devices_for_icloud(PyiCloud) + set_device_data_source(PyiCloud) + else: + apple_acct_not_found_msg = ( + f"{EVLOG_ALERT}INVALID APPLE ACCOUNT ({username}) > " + f"An Apple Acct login failed. Assigned devices are not tracked:") + apple_acct_not_found_msg += list_to_str( + [Device.fname for Device in Gb.Devices + if Device.conf_apple_acct_username == username]) + post_event(apple_acct_not_found_msg) + + _set_any_Device_alerts() + + for username, PyiCloud in Gb.PyiCloud_by_username.items(): + if retry and username not in Gb.usernames_setup_error_retry_list: + continue - setup_tracked_devices_for_famshr() - # setup_tracked_devices_for_fmf() - set_device_data_source_famshr_fmf() - # tune_device_data_source_famshr_fmf() + if PyiCloud: + _post_evlog_apple_acct_tracked_devices_info(PyiCloud) #-------------------------------------------------------------------- -def setup_tracked_devices_for_famshr(PyiCloud=None): +def setup_tracked_devices_for_icloud(PyiCloud): ''' The Family Share device data is available from PyiCloud when logging into the iCloud - account. This routine will get all the available FamShr devices from this data. + account. This routine will get all the available iCloud devices from this data. The raw data devices are then cycled through and matched with the conf tracked devices. Their status is displayed on the Event Log. The device is also marked as verified. @@ -1376,82 +1332,83 @@ def setup_tracked_devices_for_famshr(PyiCloud=None): ''' broadcast_info_msg(f"Stage 3 > Set up Family Share Devices") - if PyiCloud is None: - PyiCloud = Gb.PyiCloud if PyiCloud is None: return - PyiCloud = Gb.PyiCloud - _FamShr = PyiCloud.FamilySharing - - if Gb.conf_data_source_FAMSHR is False: - post_event("Family Sharing Devices > Not used as a data source") + if Gb.conf_data_source_ICLOUD is False: + post_event("Apple Account > Not used as a data source") return - if Gb.conf_famshr_device_cnt == 0: + if Gb.conf_icloud_device_cnt == 0: return - post_event(f"iCloud Location Service interface > Selected ({Gb.PyiCloud.instance})") + Gb.apple_accounts_flag = (len(Gb.conf_apple_accounts) > 1) - _check_renamed_famshr_devices(_FamShr) - _check_conf_famshr_devices_not_set_up(_FamShr) - _check_duplicate_device_names(PyiCloud, _FamShr) - _display_devices_verification_status(PyiCloud, _FamShr) + _check_renamed_find_devices(PyiCloud) + _match_PyiCloud_devices_to_Device(PyiCloud) + _check_duplicate_device_names(PyiCloud) + _check_for_missing_find_devices(PyiCloud) - Gb.debug_log['Gb.PyiCloud.device_id_by_famshr_fname'] = {k: v[:10] for k, v in Gb.PyiCloud.device_id_by_famshr_fname.items()} - Gb.debug_log['Gb.PyiCloud.famshr_fname_by_device_id'] = {k[:10]: v for k, v in Gb.PyiCloud.famshr_fname_by_device_id.items()} - Gb.debug_log['Gb.Devices_by_icloud_device_id'] = {k[:10]: v for k, v in Gb.Devices_by_icloud_device_id.items()} + owner = PyiCloud.username_base + Gb.startup_lists[f"{owner}.PyiCloud.device_id_by_icloud_dname"] = \ + {k: v[:10] for k, v in PyiCloud.device_id_by_icloud_dname.items()} + Gb.startup_lists[f"{owner}.PyiCloud.icloud_dname_by_device_id"] = \ + {k[:10]: v for k, v in PyiCloud.icloud_dname_by_device_id.items()} + Gb.startup_lists[f"Gb.Devices_by_icloud_device_id"] = \ + {k[:10]: v for k, v in Gb.Devices_by_icloud_device_id.items()} #---------------------------------------------------------------------------- -def _check_renamed_famshr_devices(_FamShr): +def _check_renamed_find_devices(PyiCloud): ''' - Return with a list of famshr devices in the conf_devices that are not in + Return with a list of icloud devices in the conf_devices that are not in _RawData ''' renamed_devices = \ [( f"{conf_device[CONF_IC3_DEVICENAME]} > " f"Renamed: {conf_device[CONF_FAMSHR_DEVICENAME]} " - f"{RARROW}{Gb.PyiCloud.famshr_fname_by_device_id[conf_device[CONF_FAMSHR_DEVICE_ID]]}") + f"{RARROW}{PyiCloud.icloud_dname_by_device_id[conf_device[CONF_FAMSHR_DEVICE_ID]]}") for conf_device in Gb.conf_devices - if (conf_device[CONF_FAMSHR_DEVICE_ID] in Gb.PyiCloud.famshr_fname_by_device_id) + if (conf_device[CONF_FAMSHR_DEVICE_ID] in PyiCloud.icloud_dname_by_device_id) + and conf_device[CONF_APPLE_ACCOUNT] == PyiCloud.username and conf_device[CONF_FAMSHR_DEVICENAME] != \ - Gb.PyiCloud.famshr_fname_by_device_id[conf_device[CONF_FAMSHR_DEVICE_ID]]] + PyiCloud.icloud_dname_by_device_id[conf_device[CONF_FAMSHR_DEVICE_ID]]] + if renamed_devices == []: return renamed_devices_str = list_to_str(renamed_devices, CRLF_DOT) - post_event( f"{EVLOG_ALERT}FAMSHR DEVICE NAME CHANGED > The FamShr device name returned " + post_event( f"{EVLOG_ALERT}ICLOUD DEVICE NAME CHANGED > The iCloud device name returned " f"from your Apple iCloud account Family Sharing List has a new name. " f"The iCloud3 configuration file will be updated." f"{renamed_devices_str}") try: - # Update the iCloud3 configuration file with the new FamShr devicename + # Update the iCloud3 configuration file with the new iCloud devicename renamed_devices_by_devicename = {} for renamed_device in renamed_devices: # gary_ipad > Renamed: Gary-iPad → Gary's iPad - icloud3_famshr_devicename = renamed_device.split(RARROW) - new_famshr_devicename = icloud3_famshr_devicename[1] - icloud3_devicename = icloud3_famshr_devicename[0].split(' ')[0] - renamed_devices_by_devicename[icloud3_devicename] = new_famshr_devicename + icloud3_icloud_devicename = renamed_device.split(RARROW) + new_icloud_devicename = icloud3_icloud_devicename[1] + icloud3_devicename = icloud3_icloud_devicename[0].split(' ')[0] + renamed_devices_by_devicename[icloud3_devicename] = new_icloud_devicename - # Set new famshr name in config and internal table, remove the old one - for devicename, new_famshr_devicename in renamed_devices_by_devicename.items(): + # Set new icloud name in config and internal table, remove the old one + for devicename, new_icloud_devicename in renamed_devices_by_devicename.items(): conf_device = config_file.get_conf_device(devicename) - old_famshr_devicename = conf_device[CONF_FAMSHR_DEVICENAME] - if old_famshr_devicename in Gb.conf_famshr_devicenames: - Gb.conf_famshr_devicenames.remove(old_famshr_devicename) - Gb.conf_famshr_devicenames.append(new_famshr_devicename) + old_icloud_devicename = conf_device[CONF_FAMSHR_DEVICENAME] + if old_icloud_devicename in Gb.conf_icloud_dnames: + Gb.conf_icloud_dnames.remove(old_icloud_devicename) + Gb.conf_icloud_dnames.append(new_icloud_devicename) conf_device[CONF_FAMSHR_DEVICENAME] = \ - Gb.PyiCloud.device_id_by_famshr_fname[conf_device[CONF_FAMSHR_DEVICENAME]] = \ - new_famshr_devicename + PyiCloud.device_id_by_icloud_dname[conf_device[CONF_FAMSHR_DEVICENAME]] = \ + new_icloud_devicename - Gb.PyiCloud.device_id_by_famshr_fname[new_famshr_devicename] = conf_device[CONF_FAMSHR_DEVICE_ID] - Gb.PyiCloud.device_id_by_famshr_fname.pop(old_famshr_devicename, None) + PyiCloud.device_id_by_icloud_dname[new_icloud_devicename] = conf_device[CONF_FAMSHR_DEVICE_ID] + PyiCloud.device_id_by_icloud_dname.pop(old_icloud_devicename, None) config_file.write_storage_icloud3_configuration_file() @@ -1459,160 +1416,305 @@ def _check_renamed_famshr_devices(_FamShr): log_exception(err) pass + +#-------------------------------------------------------------------- +def _match_PyiCloud_devices_to_Device(PyiCloud, retry_match_devices=None): + ''' + Cycle through the PyiCloud raw data and match that item with the + conf_devices iCloud entry. Then set the data fields to tie it to + the Device object. + ''' + evlog_msg = '' + _AppleDev = PyiCloud.DeviceSvc + if _AppleDev is None: + return + + setup_devices = retry_match_devices or PyiCloud.device_id_by_icloud_dname + owner_device_ids = [] + for pyicloud_dname, device_id in setup_devices.items(): + _RawData = PyiCloud.RawData_by_device_id.get(device_id, None) + + broadcast_info_msg(f"Set up iCloud Device > {pyicloud_dname}") + + conf_device = _find_icloud_conf_device(PyiCloud, pyicloud_dname, device_id) + + # iCloud device was not found in configuration, not being used by this PyiCloud username + if conf_device == {}: + continue + + # Update the conf_devices for this device if any of the icloud fields have changed + _update_config_with_icloud_device_fields(conf_device, _RawData) + + devicename = conf_device[CONF_IC3_DEVICENAME] + + if (devicename not in Gb.Devices_by_devicename + or conf_device[CONF_TRACKING_MODE] == INACTIVE_DEVICE): + continue + + Device = Gb.Devices_by_devicename[devicename] + if conf_device[CONF_APPLE_ACCOUNT] == '' and PyiCloud.primary_apple_account: + pass + elif conf_device[CONF_APPLE_ACCOUNT] != PyiCloud.username: + continue + + icloud_dname = conf_device[CONF_FAMSHR_DEVICENAME] + Gb.devicenames_by_icloud_dname[icloud_dname] = devicename + Gb.icloud_dnames_by_devicename[devicename] = icloud_dname + + _RawData.Device = Device + _RawData.ic3_devicename = devicename + Device.PyiCloud = PyiCloud + Device.icloud_device_id = device_id + Device.icloud_person_id = _RawData.device_data['prsId'] + Device.family_share_device = _RawData.device_data['fmlyShare'] + if Device.family_share_device is False: + owner_device_ids = Gb.owner_device_ids_by_username.get(PyiCloud.username, []) + list_add(owner_device_ids, device_id) + Gb.owner_device_ids_by_username[PyiCloud.username] = owner_device_ids + + Gb.Devices_by_icloud_device_id[device_id] = Device + Gb.PyiCloud_by_devicename[devicename] = PyiCloud + + # Set verify status to a valid device_id exists instead of always True + # This will pick up devices in the configuration file that no longer exist + Device.verified_ICLOUD = device_id in PyiCloud.RawData_by_device_id + Gb.icloud_device_verified_cnt += 1 + + # Gb.owner_device_ids_by_username[PyiCloud.username] = owner_device_ids + #---------------------------------------------------------------------------- -def _check_conf_famshr_devices_not_set_up(_FamShr): - devices_not_set_up = [ (f"{conf_device[CONF_IC3_DEVICENAME]} > " - f"Unknown: {conf_device[CONF_FAMSHR_DEVICENAME]}") - for conf_device in Gb.conf_devices - if (conf_device[CONF_FAMSHR_DEVICENAME] != NONE_FNAME - and conf_device[CONF_FAMSHR_DEVICENAME] not in Gb.PyiCloud.device_id_by_famshr_fname)] +def _check_for_missing_find_devices(PyiCloud): + ''' + Get all the devices that have the iCloud device configured that are not + in the PyiCloud devices iCloud list. This indicates we have a device that + is tracked via iCloud but that device is not in the Apple Account. + ''' + devices_conf_this_apple_acct = [devicename + for devicename, Device in Gb.Devices_by_devicename.items() + if Device.conf_apple_acct_username == PyiCloud.username] + devices_tracked_apple_acct = [devicename + for devicename, Device in Gb.Devices_by_devicename.items() + if Device.PyiCloud == PyiCloud] + devices_not_tracked_apple_acct = [devicename + for devicename, Device in Gb.Devices_by_devicename.items() + if (Device.conf_apple_acct_username == PyiCloud.username + and Device.PyiCloud is None)] + + Gb.startup_lists[f'{PyiCloud.account_owner}.devices_conf_this_apple_acct'] = \ + devices_conf_this_apple_acct + Gb.startup_lists[f'{PyiCloud.account_owner}.devices_tracked_apple_acct'] = \ + devices_tracked_apple_acct + Gb.startup_lists[f'{PyiCloud.account_owner}.devices_not_tracked_apple_acct'] = \ + devices_not_tracked_apple_acct + + if devices_not_tracked_apple_acct == []: + return + + retry_cnt = 0 if PyiCloud.DeviceSvc else 10 + while retry_cnt < 5: + retry_cnt += 1 + PyiCloud.DeviceSvc.refresh_client() + + # See in the untracked devices are now available + retry_match_devices = {} + for devicename in devices_not_tracked_apple_acct: + icloud_dname = Gb.icloud_dnames_by_devicename[devicename] + if icloud_dname in PyiCloud.device_id_by_icloud_dname: + retry_match_devices[icloud_dname] = PyiCloud.device_id_by_icloud_dname[icloud_dname] + + if isnot_empty(retry_match_devices): + _match_PyiCloud_devices_to_Device(PyiCloud, retry_match_devices=retry_match_devices) - if devices_not_set_up == []: - return [] + # Cycle thru the Devices. If there is no Device.PyiCloud, it was not returned from iCloud + devices_not_tracked_apple_acct = [devicename + for devicename, Device in Gb.Devices_by_devicename.items() + if (Device.conf_apple_acct_username == PyiCloud.username + and Device.PyiCloud is None)] - Gb.debug_log['_.devices_not_set_up'] = devices_not_set_up + if devices_not_tracked_apple_acct == []: + return + + # Some devices were not set up. Notify and prepare for a Stage 4 retry + list_add(Gb.usernames_setup_error_retry_list, PyiCloud.username) + + evlog_msg =(f"Apple Acct > {PyiCloud.account_owner_username}, " + f"Data for a Device not returned from Apple") + for devicename in devices_not_tracked_apple_acct: + Device = Gb.Devices_by_devicename[devicename] + list_add(Gb.usernames_setup_error_retry_list, PyiCloud.username) - for devices_msg in devices_not_set_up: - devicename = devices_msg.split(' >')[0] Device = Gb.Devices_by_devicename[devicename] - Device.set_fname_alert(YELLOW_ALERT) + evlog_msg+=(f"{CRLF_DOT}{Device.fname_devicename} > " + f"Device-{Device.conf_icloud_dname}") + post_event(evlog_msg) + return - devices_not_set_up_str = list_to_str(devices_not_set_up, CRLF_X) - post_startup_alert( f"FamShr Config Error > Device not found" - f"{devices_not_set_up_str.replace(CRLF_X, CRLF_DOT)}") +#-------------------------------------------------------------------- +def _set_any_Device_alerts(): + ''' + Set Yellow Alerts for devices + ''' + device_not_found_msg = '' + apple_acct_not_found_msg = '' - post_event( 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." - f"{devices_not_set_up_str}" - f"{more_info('famshr_device_not_available')}") + for Device in Gb.Devices: + # Device's Apple acct error + if Device.conf_apple_acct_username not in Gb.PyiCloud_by_username: + Device.set_fname_alert(YELLOW_ALERT) + apple_acct_not_found_msg += ( + f"{CRLF_DOT}Apple Acct-{Device.conf_apple_acct_username_base} > {Device.fname_devicename}") - log_error_msg(f"iCloud3 Error > Some FamShr Devices were not Initialized > " - f"{devices_not_set_up_str.replace(CRLF, ', ')}. " - f"See iCloud3 Event Log > Startup Stage 4 for more info.") + # Device's Apple acct ok but device not in Apple acct + elif Device.PyiCloud is None and Device.conf_icloud_dname: + Device.set_fname_alert(YELLOW_ALERT) + device_not_found_msg+=( + f"{CRLF_DOT}{Device.fname_devicename} > Apple Device-{Device.conf_icloud_dname}") + post_startup_alert(f"{Device.fname} > Apple Device-{Device.conf_icloud_dname}, Device Not Found") + + if device_not_found_msg: + post_event( f"{EVLOG_ALERT}APPLE ACCOUNT DEVICE NOT FOUND > " + f"A device was not found in it's Apple Acct and will not be tracked:" + f"{device_not_found_msg}") + if apple_acct_not_found_msg: + post_event( f"{EVLOG_ALERT}APPLE ACCOUNT NOT LOGGED INTO > " + f"A device's Apple Acct was not logged into and will not be tracked:" + f"{apple_acct_not_found_msg}") #-------------------------------------------------------------------- -def _display_devices_verification_status(PyiCloud, _FamShr): +def _post_evlog_apple_acct_tracked_devices_info(PyiCloud): + ''' + Cycle through the PyiCloud raw data and display the Event Log message for + the device indicating if it is: + - tracked (show the devicename that raw data item is tied to) + - tracked by another username ( show the other username) + - not tracked + ''' try: # Cycle thru the devices from those found in the iCloud data. We are not cycling # through the PyiCloud_RawData so we get devices without location info - event_msg =(f"iCloud Acct Family Sharing Devices > " - f"{Gb.conf_famshr_device_cnt} of " - f"{_FamShr.devices_cnt} used by iCloud3") - # f"{len(Gb.PyiCloud.device_id_by_famshr_fname)} used by iCloud3") - - Gb.famshr_device_verified_cnt = 0 - Gb.devicenames_x_famshr_devices = {} - sorted_device_id_by_famshr_fname = OrderedDict(sorted(Gb.PyiCloud.device_id_by_famshr_fname.items())) - for famshr_fname, device_id in sorted_device_id_by_famshr_fname.items(): - _RawData = PyiCloud.RawData_by_device_id_famshr.get(device_id, None) + _AppleDev = PyiCloud.DeviceSvc + devices_cnt = _AppleDev.devices_cnt if _AppleDev else 0 + devices_assigned_cnt = 0 + owner_icloud_dnames = list_to_str( + [_RawData.fname + for device_id, _RawData in PyiCloud.RawData_by_device_id.items() + if _RawData.family_share_device is False]) + famshr_dnames_msg = list_to_str( + [_RawData.fname + for device_id, _RawData in PyiCloud.RawData_by_device_id.items() + if _RawData.family_share_device]) + + devices_assigned_msg = devices_not_assigned_msg = reauth_needed_msg = '' + + if PyiCloud.requires_2fa: + reauth_needed_msg = f"{CRLF}{NBSP2}{YELLOW_ALERT} Authentication Needed" + + sorted_device_id_by_icloud_dname = OrderedDict(sorted(PyiCloud.device_id_by_icloud_dname.items())) + + for pyicloud_icloud_dname, device_id in sorted_device_id_by_icloud_dname.items(): + exception_msg = '' + _RawData = PyiCloud.RawData_by_device_id.get(device_id) + if _RawData is None: + continue - broadcast_info_msg(f"Set up FamShr Device > {famshr_fname}") + # If the Device is None, this PyiCloud is not tracked or assigned to another username + # pyicloud_icloud_dname ending with '.' indicates this is a duplicate device + devicename = Gb.devicenames_by_icloud_dname.get(pyicloud_icloud_dname) + Device = None + if devicename: + Device = Gb.Devices_by_devicename.get(devicename) - conf_device = _verify_conf_device(famshr_fname, device_id, _FamShr) + # Device belongs to another Apple account + if Device is None or Device.conf_apple_acct_username != PyiCloud.username: + if pyicloud_icloud_dname.endswith('.'): + msg_symb = f"{CRLF}{NBSP2}{YELLOW_ALERT}" + exception_msg = ', DUPLICATE DEVICE' + else: + msg_symb = CRLF_X - devicename = conf_device.get(CONF_IC3_DEVICENAME) - famshr_name = conf_device.get(CONF_FAMSHR_DEVICENAME) - Gb.devicenames_x_famshr_devices[devicename] = famshr_name - Gb.devicenames_x_famshr_devices[famshr_name] = devicename + device_msg = ( f"{msg_symb}" + f"{pyicloud_icloud_dname}{RARROW}") - _RawData.ic3_devicename = devicename - _RawData.Device = Gb.Devices_by_devicename.get(devicename) + # If the Device is already assigned to a PyiCloud iCloud device from another username, + # get that username's account owner and display it + _PyiCloud = Gb.PyiCloud_by_devicename.get(devicename) + if _PyiCloud: + devices_not_assigned_msg +=(f"{device_msg}" + f"Tracked By Apple Acct-{_PyiCloud.account_owner_short}") + else: + devices_not_assigned_msg +=(f"{device_msg}" + f"Not Assigned{exception_msg}") + continue + devicename = Device.devicename exception_msg = '' - if devicename is None: - exception_msg = 'Not Assigned to an iCloud3 Device' - elif conf_device.get(CONF_TRACKING_MODE, False) == INACTIVE_DEVICE: - exception_msg += 'INACTIVE, ' + if Device.tracking_mode == INACTIVE_DEVICE: + exception_msg += ', INACTIVE ' - elif _RawData is None: - Device = Gb.Devices_by_devicename.get(devicename) - if Device: - Gb.reinitialize_icloud_devices_flag = (Gb.conf_famshr_device_cnt > 0) - exception_msg += 'Not Assigned to an iCloud3 Device, ' - - famshr_fname = famshr_fname.replace('*', '') + pyicloud_icloud_dname = pyicloud_icloud_dname.replace('*', '') if exception_msg: - event_msg += ( f"{CRLF_X}" - f"{famshr_fname} ({_RawData.device_identifier}) >" - f"{CRLF_SP8_HDOT}{exception_msg}") + devices_assigned_msg += ( f"{msg_symb}" + f"{pyicloud_icloud_dname}{RARROW}" + f"{Device.fname}" + f"{exception_msg} " + f"({_RawData.icloud_device_display_name}") + continue - # If no location info in pyiCloud data but tracked device is matched, refresh the + # If no location info in pyiCloud data but t see racked device is matched, refresh the # data and see if it is locatable now. If so, all is OK. If not, set to verified but # display no location exception msg in EvLog exception_msg = '' - if _RawData.is_offline: - exception_msg = f", OFFLINE" - - if _RawData.is_location_data_available is False: - PyiCloud.FamilySharing.refresh_client() - if _RawData.is_location_data_available is False: - if Device: - Gb.reinitialize_icloud_devices_flag = (Gb.conf_famshr_device_cnt > 0) - exception_msg = f", NO LOCATION DATA" if _RawData and Gb.log_rawdata_flag: - log_title = ( f"FamShr PyiCloud Data (device_data -- " - f"{devicename}/{famshr_fname} " + log_title = ( f"iCloud Data {PyiCloud.account_name}{LINK}" + f"{devicename}/{pyicloud_icloud_dname} " f"({_RawData.device_identifier})") - log_rawdata(log_title, {'filter': _RawData.device_data}) - - # if devicename not in Gb.Devices_by_devicename: - Device = Gb.Devices_by_devicename.get(devicename) - if Device is None: - if exception_msg == '': exception_msg = ', Unknown Device or Other Device setup error' - event_msg += ( f"{CRLF_X}{famshr_fname} ({_RawData.device_identifier}) >" - f"{CRLF_SP8_HDOT}{devicename}" - f"{exception_msg}") - continue - - Device.device_id_famshr = device_id + # log_rawdata(log_title, {'filter': _RawData.device_data}) - # rc9 Set verify status to a valid device_id exists instead of always True - # This will pick up devices in the configuration file that no longer exist - #Device.verified_flag = device_id in PyiCloud.RawData_by_device_id - Device.verified_FAMSHR = device_id in PyiCloud.RawData_by_device_id - - # link paired devices (iPhone <--> Watch) - Device.paired_with_id = _RawData.device_data['prsId'] - if Device.paired_with_id is not None: - if Device.paired_with_id in Gb.PairedDevices_by_paired_with_id: - Gb.PairedDevices_by_paired_with_id[Device.paired_with_id].append(Device) - else: - Gb.PairedDevices_by_paired_with_id[Device.paired_with_id] = [Device] + devices_assigned_cnt += 1 + msg_symb = f"{CRLF}{NBSP2}{YELLOW_ALERT}" if pyicloud_icloud_dname.endswith('.') else CRLF_CHK + device_msg=(f"{msg_symb}" + f"{pyicloud_icloud_dname}{RARROW}" + f"{Device.fname}/{devicename} ") + if len(device_msg) > 40: device_msg += CRLF_TAB + device_msg += f"({_RawData.icloud_device_display_name})" + device_msg += exception_msg - Gb.Devices_by_icloud_device_id[device_id] = Device + devices_assigned_msg += device_msg - Gb.famshr_device_verified_cnt += 1 + evlog_msg =(f"Apple Acct > {PyiCloud.account_owner_username}, " + f"{devices_assigned_cnt} of {devices_cnt} tracked" + f"{reauth_needed_msg}" + f"{devices_assigned_msg}" + f"{devices_not_assigned_msg}") - event_msg += ( f"{CRLF_CHK}" - f"{famshr_fname} ({_RawData.device_identifier}) >" - f"{CRLF_SP8_HDOT}{devicename}, {Device.fname} " - f"{Device.tracking_mode_fname}" - f"{exception_msg}") + if owner_icloud_dnames: + evlog_msg += f"{CRLF_DOT} myDevices-{owner_icloud_dnames}" + if famshr_dnames_msg: + evlog_msg += f"{CRLF_DOT} FamShr List Devices-{famshr_dnames_msg}" - post_event(event_msg) + post_event(evlog_msg) return except Exception as err: log_exception(err) - event_msg =(f"iCloud3 Error from iCloud Loc Svcs > " + evlog_msg =(f"iCloud3 Error from iCloud Loc Svcs > " "Error Authenticating account or no data was returned from " "iCloud Location Services. iCloud access may be down or the " "Username/Password may be invalid.") - post_error_msg(event_msg) + post_error_msg(evlog_msg) return #-------------------------------------------------------------------- -def _verify_conf_device(famshr_fname, device_id, _FamShr): +def _find_icloud_conf_device(PyiCloud, pyicloud_dname, device_id): ''' - Get the this famshr device's configuration item. Then see if the raw_model, model + Get the icloud3 device's configuration item. Then see if the raw_model, model or model_display_name has changed. If so, update it in the configuration file. Return: @@ -1621,35 +1723,42 @@ def _verify_conf_device(famshr_fname, device_id, _FamShr): # Cycle through the config tracked devices and find the matching device. update_conf_file_flag = False try: - conf_device = [conf_device for conf_device in Gb.conf_devices - if famshr_fname == conf_device[CONF_FAMSHR_DEVICENAME]][0] - except: + conf_device = [conf_device + for conf_device in Gb.conf_devices + if (pyicloud_dname == conf_device[CONF_FAMSHR_DEVICENAME] + and conf_device[CONF_APPLE_ACCOUNT] == PyiCloud.username)] + + except Exception as err: + log_exception(err) return {} + if conf_device == []: + return {} + + conf_device = conf_device[0] + + # Get the model info from PyiCloud data and update it if necessary raw_model, model, model_display_name = \ - Gb.PyiCloud.device_model_info_by_fname[famshr_fname] + PyiCloud.device_model_info_by_fname[pyicloud_dname] - if (conf_device[CONF_RAW_MODEL] != raw_model + if (conf_device[CONF_FAMSHR_DEVICE_ID] != device_id + or conf_device[CONF_RAW_MODEL] != raw_model or conf_device[CONF_MODEL] != model or conf_device[CONF_MODEL_DISPLAY_NAME] != model_display_name): + conf_device[CONF_FAMSHR_DEVICE_ID] = device_id conf_device[CONF_RAW_MODEL] = raw_model conf_device[CONF_MODEL] = model conf_device[CONF_MODEL_DISPLAY_NAME] = model_display_name - update_conf_file_flag = True - if conf_device[CONF_FAMSHR_DEVICE_ID] != device_id: - conf_device[CONF_FAMSHR_DEVICE_ID] = device_id - - if update_conf_file_flag: config_file.write_storage_icloud3_configuration_file() return conf_device #-------------------------------------------------------------------- -def _check_duplicate_device_names(PyiCloud, _FamShr): +def _check_duplicate_device_names(PyiCloud): ''' - See if this fname is already used by another device in the FamShr list. If so, + See if this fname is already used by another device in the iCloud list. If so, consider it a duplicate device that would refer to the same iC3 devicename. Add a suffix to the fname to be able to keep track of it. Leave the actual devicename the original value. @@ -1663,115 +1772,85 @@ def _check_duplicate_device_names(PyiCloud, _FamShr): ''' if 'stage4_dup_check_done' in Gb.startup_stage_status_controls: return - # rc9 Reworked duplicate device message and will now display all dup devices - # Make a list of all device fnames without the (##) suffix that was added on - try: - famshr_fnames_base = [_fname_base(famshr_fname) - for famshr_fname in Gb.PyiCloud.device_id_by_famshr_fname.keys()] - - # Count each one, then drop the ones with a count = 1 to keep the duplicates - famshr_fnames_count = {famshr_fname:famshr_fnames_base.count(famshr_fname) - for famshr_fname in famshr_fnames_base} - - famshr_fnames_dupes = [famshr_fname_base - for famshr_fname_base, famshr_fname_count in famshr_fnames_count.items() - if famshr_fname_count > 1] - except Exception as err: - # log_exception(err) - return - - Gb.debug_log['_.famshr_fnames_base'] = famshr_fnames_base - Gb.debug_log['_.famshr_fnames_count'] = famshr_fnames_count - Gb.debug_log['_.famshr_fnames_dupes'] = famshr_fnames_dupes - - if famshr_fnames_dupes == []: - return - # Cycle thru the duplicates, create an evlog msg, select the one located most recent + # Cycle thru the duplicates, create an evlog msg try: - dup_devices_msg = "" - dups_found_msg = "" - famshr_fname_last_located_by_base_fname = {} - for famshr_fname_base in famshr_fnames_dupes: - _RawData_by_famshr_fname = {_RawData.fname:_RawData - for device_id, _RawData in PyiCloud.RawData_by_device_id_famshr.items() - if (_RawData.fname == famshr_fname_base - or _RawData.fname.startswith(f"{famshr_fname_base}("))} - - dup_devices_msg += f"{CRLF_DOT}{famshr_fname_base}" - located_last_secs = 0 - for famshr_fname, _RawData in _RawData_by_famshr_fname.items(): - if _RawData.location_secs > located_last_secs: - located_last_secs = _RawData.location_secs - famshr_fname_last_located_by_base_fname[famshr_fname_base] = famshr_fname - - dup_devices_msg += (f"{CRLF_HDOT}{famshr_fname} > " - f"Located-{format_age(_RawData.location_secs)} " - f"({_RawData.device_data['rawDeviceModel']})") - - # devicename = Gb.devicenames_x_famshr_devices.get(famshr_fname, '') - # _Device = Gb.Devices_by_devicename.get(devicename) - - if _RawData.Device: - _RawData.Device.set_fname_alert(YELLOW_ALERT) - post_startup_alert(f"Duplicate FamShr device found - {famshr_fname}") - - if famshr_fname_base in famshr_fname_last_located_by_base_fname: - dup_devices_msg += (f"{CRLF_HDOT}Last Located > {famshr_fname_last_located_by_base_fname[famshr_fname_base]}") - except: - if dup_devices_msg: - dup_devices_msg += (f"{CRLF_HDOT}Last Located > Could not be determined") + _dup_RawData_by_icloud_dname = {RawData.fname: RawData + for RawData in PyiCloud.RawData_items + if RawData.Device and RawData.fname.endswith('.')} + if is_empty(_dup_RawData_by_icloud_dname): + return - if dup_devices_msg == "": return + Gb.startup_lists['_._dup_RawData_by_icloud_dnames'] = _dup_RawData_by_icloud_dname.keys() - dups_found_msg =(f"{EVLOG_ALERT}DUPLICATE FAMSHR DEVICES > There are several devices in the " - f"iCloud Account Family Sharing list with the same or similar names. " - f"The unused devices should be reviewed and removed. The Devices are:" - f"{dup_devices_msg}") + dup_devices_msg = "" + dup_devices_list = "" - # Cycle thru dup names, get conf_devices entry for the one matching the famshr_fname_base and update it - # famshr_fname_last_located_by_base_fname = {'Gary-iPad': 'Gary_iPad(2)'} - update_conf_file_flag = False - devices_updated_msg = f"{EVLOG_ALERT}UPDATED CONFIGURATION > Duplicate FamShr device updated to last located device > " - for famshr_fname_base, famshr_fname_last_located in famshr_fname_last_located_by_base_fname.items(): - conf_device = [_conf_device for _conf_device in Gb.conf_devices - if (_conf_device[CONF_FAMSHR_DEVICENAME].startswith(famshr_fname_base) - and _conf_device[CONF_FAMSHR_DEVICENAME] != famshr_fname_last_located)] + for icloud_dname, RawData in _dup_RawData_by_icloud_dname.items(): + try: + # Get the RawData item for the original tracked device (without the dots) to + # be able to display that device's info + _RawData_original = [_RawData + for _RawData in PyiCloud.RawData_items + if _RawData.fname == RawData.fname_original][0] + #if isnot_empty(_RawData_original): + # _RawData_original = _RawData_original[0] + #else: + # _RawData_original = RawData + + + # Get the tracked device that was duplicated + _Device = _RawData_original.Device + _Device.set_fname_alert(YELLOW_ALERT) + + dup_devices_list += f"{_Device.fname_devicename} > Apple Device-{RawData.fname_original}, " + dup_devices_msg += (f"{CRLF_DOT}{_Device.fname_devicename} > " + f"Apple Device-{RawData.fname_original}, " + f"{CRLF_HDOT}{RawData.icloud_device_display_name}, Other Device with same name" + f"{CRLF_HDOT}{_RawData_original.icloud_device_display_name}, Assigned Device being tracked") + + except Exception as err: + log_exception(err) + + post_event( f"{EVLOG_ALERT}DUPLICATE APPLE ACCOUNT DEVICES > " + f"Two Apple Account Devices have the same name that is assigned to an " + f"iCloud3 tracked device. Review the Configure > Update Devices screen and " + f"verify the correct Apple Device is assigned." + f"{dup_devices_msg}" + f"{CRLF}Rename one of the devices (Settings App > General > About) to remove this notification") + alert_msg =(f"{_Device.fname} > Two Apple Acct devices with same name " + f"({RawData.fname_original}), " + f"{RawData.icloud_device_display_name} & " + f"{_Device.model_display_name}") + post_startup_alert(alert_msg) + log_warning_msg(f"iCloud3 Alert > {alert_msg}") + + Gb.startup_stage_status_controls.append('stage4_dup_check_done') + except Exception as err: + log_exception(err) - try: - if conf_device != []: - conf_device = conf_device[0] - devices_updated_msg += (f"{CRLF_DOT}{conf_device[CONF_IC3_DEVICENAME]} > " - f"{conf_device[CONF_FAMSHR_DEVICENAME]}{RARROW}" - f"{famshr_fname_last_located}") - - conf_device[CONF_FAMSHR_DEVICENAME] = famshr_fname_last_located - conf_device[CONF_FAMSHR_DEVICE_ID] = Gb.PyiCloud.device_id_by_famshr_fname[famshr_fname_last_located] - update_conf_file_flag = True - Device = Gb.Devices_by_devicename[conf_device[CONF_IC3_DEVICENAME]] - Device.set_fname_alert(YELLOW_ALERT) +#-------------------------------------------------------------------- +def _update_config_with_icloud_device_fields(conf_device, _RawData): - except Exception as err: - post_event( f"Error resolving similar device names, " - f"{famshr_fname_base}/{famshr_fname_last_located}, " - f"{err}") + # Get the model info from PyiCloud data and update it if necessary + raw_model, model, model_display_name = \ + _RawData.PyiCloud.device_model_info_by_fname[_RawData.fname] - # Print dups msg with info and update config file - if update_conf_file_flag: - dups_found_msg += more_info('famshr_dup_devices') - post_event(dups_found_msg) + if (conf_device[CONF_FAMSHR_DEVICE_ID] != _RawData.device_id + or conf_device[CONF_RAW_MODEL] != raw_model + or conf_device[CONF_MODEL] != model + or conf_device[CONF_MODEL_DISPLAY_NAME] != model_display_name): + conf_device[CONF_FAMSHR_DEVICE_ID] = _RawData.device_id + conf_device[CONF_RAW_MODEL] = raw_model + conf_device[CONF_MODEL] = model + conf_device[CONF_MODEL_DISPLAY_NAME] = model_display_name config_file.write_storage_icloud3_configuration_file() - post_event(devices_updated_msg) - post_startup_alert(devices_updated_msg.replace(EVLOG_ALERT, '').replace(CRLF_DOT, CRLF_HDOT)) - log_warning_msg(f"iCloud3 > {devices_updated_msg}") - - # Config is OK. Just print dups msg as an alert - elif dups_found_msg: - post_event(dups_found_msg) - Gb.startup_stage_status_controls.append('stage4_dup_check_done') + post_event( f"{EVLOG_NOTICE}Device Config Updated > {conf_device[CONF_FNAME]} " + f"({conf_device[CONF_IC3_DEVICENAME]}), " + f"Apple Acct Info Updated-{model_display_name}") #-------------------------------------------------------------------- def _fname_base(fname): @@ -1787,210 +1866,27 @@ def _fname_base(fname): return fname -#-------------------------------------------------------------------- -def setup_tracked_devices_for_fmf(PyiCloud=None): - ''' - The Find-my-Friends device data is available from PyiCloud when logging into the iCloud - account. This routine will get all the available FmF devices from the email contacts/following/ - followed data. The devices are then cycled through to be matched with the tracked devices - and their status is displayed on the Event Log. The device is also marked as verified. - - Arguments: - PyiCloud: Object containing data to be processed. This might be from Gb.PyiCloud (normal/default) - or the one created in config_flow if the username was changed. - ''' - return - - broadcast_info_msg(f"Stage 3 > Set up Find-my-Friends Devices") - - # devices_desc = get_fmf_devices(PyiCloud) - # device_id_by_fmf_email = devices_desc[0] - # fmf_email_by_device_id = devices_desc[1] - # device_info_by_fmf_email = devices_desc[2] - - if PyiCloud is None: - PyiCloud = Gb.PyiCloud - if PyiCloud is None: - return - - PyiCloud = Gb.PyiCloud - PyiCloud.create_FindMyFriends_object() - _FmF = PyiCloud.FindMyFriends - # _Fmf._set_service_available(True) - Gb.conf_data_source_FMF = True - _FmF.refresh_client() - - event_msg = "Find-My-Friends Devices > " - # if Gb.conf_data_source_FMF is False: - # event_msg += "Not used as a data source" - # post_event(event_msg) - # return - # event_msg += f"{Gb.conf_fmf_device_cnt} of {len(Gb.conf_devices)} iCloud3 Devices Configured" - # post_event(event_msg) - # if Gb.conf_fmf_device_cnt == 0: - # return - - # elif _FmF is None or _FmF.device_id_by_fmf_email == {}: - # event_msg += "NO DEVICES FOUND" - # post_event(f"{EVLOG_ALERT}{event_msg}") - # return - - try: - Gb.fmf_device_verified_cnt = 0 - - # Cycle through all the FmF devices in the iCloud account - exception_event_msg = '' - device_fname_by_device_id = {} - sorted_device_id_by_fmf_email = OrderedDict(sorted(_FmF.device_id_by_fmf_email.items())) - for fmf_email, device_id in sorted_device_id_by_fmf_email.items(): - broadcast_info_msg(f"Set up FmF Device > {fmf_email}") - devicename = '' - device_fname = '' - _RawData = None - exception_msg = '' - - # Cycle througn the tracked devices and find the matching device - # Verify the device_id in the configuration with the found - # device and display a configuration error msg later if something - # doesn't match - for device in Gb.conf_devices: - conf_fmf_email = device[CONF_FMF_EMAIL].split(" >")[0].strip() - - if conf_fmf_email == fmf_email: - devicename = device[CONF_IC3_DEVICENAME] - device_fname = device[CONF_FNAME] - device_type = device[CONF_DEVICE_TYPE] - _RawData = PyiCloud.RawData_by_device_id_fmf[device_id] - - if device[CONF_FMF_DEVICE_ID] != device_id: - device[CONF_FMF_DEVICE_ID] = device_id - break - - crlf_mark = CRLF_DOT - if _RawData is None: - exception_msg = 'Not Tracked' - - elif device[CONF_TRACKING_MODE] == INACTIVE_DEVICE: - exception_msg = 'INACTIVE' - crlf_mark = CRLF_X - - if exception_msg: - exception_event_msg += (f"{crlf_mark}{fmf_email}{RARROW}{exception_msg}") - continue - - # If no location info in pyiCloud data but tracked device is matched, refresh the - # data and see if it is locatable now. If so, all is OK. If not, set to verified but - # display no location exception msg in EvLog - exception_msg = '' - if _RawData.is_location_data_available is False: - exception_msg = f", NO LOCATION DATA" - - PyiCloud.FindMyFriends.refresh_client() - - if _RawData and Gb.log_rawdata_flag: - log_title = (f"FmF PyiCloud Data (device_data -- {devicename}/{fmf_email})") - log_rawdata(log_title, {'data': _RawData.device_data}) - - device_type = '' - # The tracked or monitored device has been matched with available devices, mark it as verified. - if devicename in Gb.Devices_by_devicename: - Device = Gb.Devices_by_devicename[devicename] - device_fname_by_device_id[device_id] = Device.fname - device_type = Device.device_type - #Device.verified_flag = True - Device.verified_FMF = True - Device.device_id_fmf = device_id - Gb.Devices_by_icloud_device_id[device_id] = Device - Gb.fmf_device_verified_cnt += 1 - - event_msg += ( f"{CRLF_CHK}" - f"{fmf_email}{RARROW}{devicename}, " - f"{DEVICE_TYPE_FNAME.get(device_type, device_type)}" - f"{exception_msg}") - else: - event_msg += ( f"{CRLF_X}" - f"{fmf_email}{RARROW}{devicename}, " - f"{DEVICE_TYPE_FNAME.get(device_type, device_type)}" - f"{exception_msg}") - - # Replace known device_ids whith the actual name - for device_id, device_fname in device_fname_by_device_id.items(): - exception_event_msg = exception_event_msg.replace( f"({device_id})", \ - f"({device_fname_by_device_id[device_id]})") - - # Remove any unknown device_ids - for device_id, fmf_email in _FmF.fmf_email_by_device_id.items(): - exception_event_msg = exception_event_msg.replace(f"({device_id})", "") - - event_msg += exception_event_msg - post_event(event_msg) - - return - - except Exception as err: - log_exception(err) - - event_msg =(f"iCloud3 Error from iCloud Loc Svcs > " - "Error Authenticating account or no data was returned from " - "iCloud Location Services. iCloud access may be down or the " - "Username/Password may be invalid.") - post_error_msg(event_msg) - - return #---------------------------------------------------------------------- -def get_famshr_devices_pyicloud(PyiCloud): +def get_find_devices_pyicloud(PyiCloud): ''' The device information tables are built when the devices are added the when - the FamilySharing object and RawData objects are created when logging into + the DeviceSvc object and RawData objects are created when logging into the iCloud account. ''' if PyiCloud is None: PyiCloud = Gb.PyiCloud - _FamShr = PyiCloud.FamilySharing - return [Gb.PyiCloud.device_id_by_famshr_fname, - Gb.PyiCloud.device_id_by_famshr_fname, - Gb.PyiCloud.device_info_by_famshr_fname, - Gb.PyiCloud.device_model_info_by_fname] - -#---------------------------------------------------------------------- -# def get_fmf_devices_pyicloud(PyiCloud): -# ''' -# The device information tables are built when the devices are added the when -# the FamilySharing object and RawData objects are created when logging into -# the iCloud account. -# ''' - -# if PyiCloud is None: PyiCloud = Gb.PyiCloud - -# _FmF = PyiCloud.FindMyFriends -# return (_FmF.device_id_by_fmf_email, -# _FmF.fmf_email_by_device_id, -# _FmF.device_info_by_fmf_email) - - -#-------------------------------------------------------------------- -# def set_device_data_source_mobapp(): -# ''' -# The Global tracking method is mobapp so set all Device's tracking method -# to mobapp -# ''' -# if Gb.conf_data_source_MOBAPP is False: -# returnYou - -# for Device in Gb.Devices: -# Device.data_source = 'mobapp' + _AppleDev = PyiCloud.DeviceSvc + return [PyiCloud.device_id_by_icloud_dname, + PyiCloud.device_id_by_icloud_dname, + PyiCloud.device_info_by_icloud_dname, + PyiCloud.device_model_info_by_fname] #-------------------------------------------------------------------- -def set_device_data_source_famshr_fmf(PyiCloud=None): +def set_device_data_source(PyiCloud): ''' - The goal is to get either all fmf or all famshr to minimize the number of - calls to iCloud Web Services by pyicloud_ic3. Look at the fmf and famshr - devices to see if: - 1. If all devices are fmf or all devices are famshr: - Do not make any changes - 2. If set to fmf but it also has a famshr id, change to famshr. - 2. If set to fmf and no famshr id, leave as fmf. + Cycle through the devices and change the iCloud data source to MobApp + if there is a problem with the iCloud setup and the MobApp is available ''' broadcast_info_msg(f"Stage 3 > Set up device data source") @@ -1998,40 +1894,27 @@ def set_device_data_source_famshr_fmf(PyiCloud=None): if Gb.Devices_by_devicename == {}: return - if PyiCloud is None: PyiCloud = Gb.PyiCloud - Gb.Devices_by_icloud_device_id = {} - devicename_not_tracked = {} for devicename, Device in Gb.Devices_by_devicename.items(): data_source = None broadcast_info_msg(f"Determine Device Tracking Method >{devicename}") - if Device.device_id_famshr: - device_id = Device.device_id_famshr + if Device.icloud_device_id: + device_id = Device.icloud_device_id if device_id in PyiCloud.RawData_by_device_id: - data_source = FAMSHR + data_source = ICLOUD Gb.Devices_by_icloud_device_id[device_id] = Device _RawData = PyiCloud.RawData_by_device_id[device_id] - # if Device.device_id_fmf: - # device_id = Device.device_id_fmf - # if device_id in PyiCloud.RawData_by_device_id: - # if data_source is None: - # data_source = FMF - # Gb.Devices_by_icloud_device_id[device_id] = Device - # _RawData = PyiCloud.RawData_by_device_id[device_id] - if (Device.mobapp_monitor_flag and data_source is None): data_source = MOBAPP if data_source != MOBAPP: info_msg = (f"Set PyiCloud Device Id > {Device.devicename}, " f"DataSource-{data_source}, " - f"{CRLF}FamShr-({Device.device_id8_famshr})") - # f"FmF-({Device.device_id8_fmf})") + f"{CRLF}iCloud-({Device.device_id8_icloud})") post_monitor_msg(info_msg) - #Device.data_source = data_source Device.primary_data_source = data_source info_msg = (f"PyiCloud Devices > ") @@ -2042,69 +1925,6 @@ def set_device_data_source_famshr_fmf(PyiCloud=None): except Exception as err: log_exception(err) -#-------------------------------------------------------------------- -def tune_device_data_source_famshr_fmf(): - ''' - The goal is to get either all fmf or all famshr to minimize the number of - calls to iCloud Web Services by pyicloud_ic3. Look at the fmf and famshr - devices to see if: - 1. If all devices are fmf or all devices are famshr: - Do not make any changes - 2. If set to fmf but it also has a famshr id, change to famshr. - 2. If set to fmf and no famshr id, leave as fmf. - ''' - - return - - broadcast_info_msg(f"Stage 3 > Tune Tracking Method") - - try: - # Global data_source specified, nothing to do - if Gb.primary_data_source_ICLOUD is False: - return - elif Gb.Devices_by_devicename == {}: - return - - cnt_famshr = 0 # famshr is specified as the data_source for the device in config - cnt_fmf = 0 # fmf is specified as the data_source for the device in config - cnt_famshr_to_fmf = 0 - cnt_fmf_to_famshr = 0 - - for devicename, Device in Gb.Devices_by_devicename.items(): - broadcast_info_msg(f"Tune Device Tracking Method > {devicename}") - - if Device.is_data_source_FAMSHR: - cnt_famshr += 1 - elif Device.is_data_source_FMF: - cnt_fmf += 1 - - # Only count those with no data_source config parm - Devices_famshr_to_fmf = [] - Devices_fmf_to_famshr = [] - if Device.data_source_config == '': - if Device.is_data_source_FAMSHR and Device.device_id_fmf: - Devices_fmf_to_famshr.append(Device) - cnt_famshr_to_fmf += 1 - elif Device.is_data_source_FMF and Device.device_id_famshr: - Devices_famshr_to_fmf.append(Device) - cnt_fmf_to_famshr += 1 - - if cnt_famshr == 0 or cnt_fmf == 0: - pass - elif cnt_famshr_to_fmf == 0 or cnt_fmf_to_famshr == 0: - pass - elif cnt_famshr >= cnt_fmf: - for Device in Devices_fmf_to_famshr: - Device.primary_data_source = FAMSHR - Gb.Devices_by_icloud_device_id.pop(Device.device_id_fmf) - Gb.Devices_by_icloud_device_id[Device.device_id_famshr] = Device - else: - for Device in Devices_famshr_to_fmf: - Device.primary_data_source = FMF - Gb.Devices_by_icloud_device_id.pop(Device.device_id_famshr) - Gb.Devices_by_icloud_device_id[Device.device_id_fmf] = Device - except: - pass #-------------------------------------------------------------------- def setup_tracked_devices_for_mobapp(): @@ -2112,115 +1932,108 @@ def setup_tracked_devices_for_mobapp(): Get the MobApp device_tracker entities from the entity registry. Then cycle through the Devices being tracked and match them up. Anything left over at the end is not matched and not monitored. ''' - check_mobile_app_integration() - - devices_desc = mobapp_interface.get_entity_registry_mobile_app_devices() - mobapp_id_by_mobapp_devicename = devices_desc[0] - mobapp_devicename_by_mobapp_id = devices_desc[1] - device_info_by_mobapp_devicename = devices_desc[2] - device_model_info_by_mobapp_devicename = devices_desc[3] - last_updt_trig_by_mobapp_devicename = devices_desc[4] - mobile_app_notify_devicenames = devices_desc[5] - battery_level_sensors_by_mobapp_devicename = devices_desc[6] - battery_state_sensors_by_mobapp_devicename = devices_desc[7] mobapp_error_mobile_app_msg = '' mobapp_error_search_msg = '' mobapp_error_disabled_msg = '' mobapp_error_not_found_msg = '' Gb.mobapp_device_verified_cnt = 0 + devices_monitored_cnt = 0 + devices_monitored_msg = devices_not_monitored_msg = '' - unmatched_mobapp_devices = mobapp_id_by_mobapp_devicename.copy() + unmatched_mobapp_devices = Gb.mobapp_id_by_mobapp_dname.copy() verified_mobapp_fnames = [] tracked_msg = f"Mobile App Devices > {Gb.conf_mobapp_device_cnt} of {len(Gb.conf_devices)} used by iCloud3" - Gb.devicenames_x_mobapp_devicenames = {} + Gb.devicenames_x_mobapp_dnames = {} for devicename, Device in Gb.Devices_by_devicename.items(): broadcast_info_msg(f"Set up Mobile App Devices > {devicename}") - conf_mobapp_device = Device.mobapp[DEVICE_TRACKER].replace(DEVICE_TRACKER_DOT, '') - Gb.devicenames_x_mobapp_devicenames[devicename] = None + conf_mobapp_dname = Device.mobapp[DEVICE_TRACKER].replace(DEVICE_TRACKER_DOT, '') + Gb.devicenames_x_mobapp_dnames[devicename] = None # Set mobapp devicename to icloud devicename if nothing is specified. Set to not monitored # if no icloud famshr name - if conf_mobapp_device in ['', 'None']: + if conf_mobapp_dname in ['', 'None']: Device.mobapp[DEVICE_TRACKER] = '' continue # Check if the specified mobapp device tracker is valid and in the entity registry - if conf_mobapp_device.startswith('Search: ') is False: - if conf_mobapp_device in mobapp_id_by_mobapp_devicename: - mobapp_devicename = conf_mobapp_device - Gb.devicenames_x_mobapp_devicenames[devicename] = mobapp_devicename - Gb.devicenames_x_mobapp_devicenames[mobapp_devicename] = devicename + if conf_mobapp_dname.startswith('Search: ') is False: + if conf_mobapp_dname in Gb.mobapp_id_by_mobapp_dname: + mobapp_dname = conf_mobapp_dname + Gb.devicenames_x_mobapp_dnames[devicename] = mobapp_dname + Gb.devicenames_x_mobapp_dnames[mobapp_dname] = devicename else: Device.set_fname_alert(YELLOW_ALERT) - mobapp_error_not_found_msg += ( f"{CRLF_X}{conf_mobapp_device} > " + mobapp_error_not_found_msg += ( f"{CRLF_X}{conf_mobapp_dname} > " f"Assigned to {Device.fname_devicename}") else: - mobapp_devicename = _search_for_mobapp_device( devicename, Device, - mobapp_id_by_mobapp_devicename, - conf_mobapp_device) - if mobapp_devicename: - Gb.devicenames_x_mobapp_devicenames[devicename] = mobapp_devicename - Gb.devicenames_x_mobapp_devicenames[mobapp_devicename] = devicename + mobapp_dname = _scan_for_for_mobapp_device( devicename, Device, + Gb.mobapp_id_by_mobapp_dname, + conf_mobapp_dname) + if mobapp_dname: + Gb.devicenames_x_mobapp_dnames[devicename] = mobapp_dname + Gb.devicenames_x_mobapp_dnames[mobapp_dname] = devicename else: Device.set_fname_alert(YELLOW_ALERT) - mobapp_error_search_msg += (f"{CRLF_X}{conf_mobapp_device}_??? > " + mobapp_error_search_msg += (f"{CRLF_X}{conf_mobapp_dname}_??? > " f"Assigned to {Device.fname_devicename}") for devicename, Device in Gb.Devices_by_devicename.items(): - mobapp_devicename = Gb.devicenames_x_mobapp_devicenames[devicename] - if mobapp_devicename is None: + mobapp_dname = Gb.devicenames_x_mobapp_dnames[devicename] + if mobapp_dname is None: continue - mobapp_id = mobapp_id_by_mobapp_devicename[mobapp_devicename] + mobapp_id = Gb.mobapp_id_by_mobapp_dname[mobapp_dname] - try: - mobapp_fname = Gb.mobapp_fnames_x_mobapp_id[mobapp_id] - except Exception as err: - # log_exception(err) - mobapp_fname = f"{mobapp_devicename.replace('_', ' ').title()}(?)" + # Get fname from MobileApp integration if it is started, otherwise build one for now + # and reset it when the MobileApp is checked again after HA has started + if mobapp_id in Gb.mobapp_fnames_by_mobapp_id: + mobapp_fname = Gb.mobapp_fnames_by_mobapp_id[mobapp_id] + else: + mobapp_fname = f"{mobapp_dname.replace('_', ' ').title()}" - Gb.devicenames_x_mobapp_devicenames[mobapp_fname] = devicename + Device.conf_mobapp_fname = mobapp_fname + Gb.devicenames_x_mobapp_dnames[mobapp_fname] = devicename # device_tracker entity is disabled - if mobapp_id in Gb.mobapp_fnames_disabled: + if mobapp_id in Gb.MobileApp_fnames_disabled: Device.set_fname_alert(YELLOW_ALERT) Device.mobapp_device_unavailable_flag = True - mobapp_error_disabled_msg += ( f"{CRLF_DOT}{mobapp_devicename} > " + mobapp_error_disabled_msg += ( f"{CRLF_DOT}{mobapp_dname} > " f"Assigned to-{Device.fname_devicename}") continue # Build errors message, can still use the Mobile App for zone changes but sensors are not monitored - if (last_updt_trig_by_mobapp_devicename.get(mobapp_devicename, '') == '' - or instr(last_updt_trig_by_mobapp_devicename.get(mobapp_devicename, ''), 'DISABLED') - or battery_level_sensors_by_mobapp_devicename.get(mobapp_devicename, '') == '' - or instr(battery_level_sensors_by_mobapp_devicename.get(mobapp_devicename, ''), 'DISABLED')): + if (Gb.last_updt_trig_by_mobapp_dname.get(mobapp_dname, '') == '' + or instr(Gb.last_updt_trig_by_mobapp_dname.get(mobapp_dname, ''), 'DISABLED') + or Gb.battery_level_sensors_by_mobapp_dname.get(mobapp_dname, '') == '' + or instr(Gb.battery_level_sensors_by_mobapp_dname.get(mobapp_dname, ''), 'DISABLED')): Device.set_fname_alert(YELLOW_ALERT) - mobapp_error_mobile_app_msg += (f"{CRLF_DOT}{mobapp_devicename} > " + mobapp_error_mobile_app_msg += (f"{CRLF_DOT}{mobapp_dname} > " f"Assigned to {Device.fname_devicename}") verified_mobapp_fnames.append(mobapp_fname) - Device.conf_mobapp_fname = mobapp_fname - Device.mobapp_monitor_flag = True Gb.mobapp_device_verified_cnt += 1 Device.verified_MOBAPP = True - + Device.mobapp_monitor_flag = True # Set raw_model that will get picked up by device_tracker and set in the device registry if it is still # at it's default value. Normally, raw_model is set when setting up FamShr if that is available, FmF does not # set raw_model since it is only shared via an email addres or phone number. This will also be saved in the # iCloud3 configuration file. - raw_model, model, model_display_name = device_model_info_by_mobapp_devicename[mobapp_devicename] + mobapp_fname, raw_model, model, model_display_name = \ + Gb.device_info_by_mobapp_dname[mobapp_dname] + if ((raw_model and Device.raw_model != raw_model) or (model and Device.model != model) or (model_display_name and Device.model_display_name != model_display_name)): for conf_device in Gb.conf_devices: - if conf_device[CONF_MOBILE_APP_DEVICE] == mobapp_devicename: + if conf_device[CONF_MOBILE_APP_DEVICE] == mobapp_dname: if raw_model: Device.raw_model = conf_device[CONF_RAW_MODEL] = raw_model if model: @@ -2230,58 +2043,76 @@ def setup_tracked_devices_for_mobapp(): config_file.write_storage_icloud3_configuration_file() break - Gb.Devices_by_mobapp_devicename[mobapp_devicename] = Device - Device.mobapp[DEVICE_TRACKER] = f"device_tracker.{mobapp_devicename}" - Device.mobapp[TRIGGER] = f"sensor.{last_updt_trig_by_mobapp_devicename.get(mobapp_devicename, '')}" + # Setup mobapp entities with data or to be monitored + Gb.Devices_by_mobapp_dname[mobapp_dname] = Device + Device.mobapp[DEVICE_TRACKER] = f"device_tracker.{mobapp_dname}" + Device.mobapp[TRIGGER] = f"sensor.{Gb.last_updt_trig_by_mobapp_dname.get(mobapp_dname, '')}" Device.mobapp[BATTERY_LEVEL] = Device.sensors['mobapp_sensor-battery_level'] = \ - f"sensor.{battery_level_sensors_by_mobapp_devicename.get(mobapp_devicename, '')}" + f"sensor.{Gb.battery_level_sensors_by_mobapp_dname.get(mobapp_dname, '')}" Device.mobapp[BATTERY_STATUS] = Device.sensors['mobapp_sensor-battery_status'] = \ - f"sensor.{battery_state_sensors_by_mobapp_devicename.get(mobapp_devicename, '')}" + f"sensor.{Gb.battery_state_sensors_by_mobapp_dname.get(mobapp_dname, '')}" + + device_msg = ( f"{CRLF_CHK}{mobapp_fname}{RARROW}" + f"{Device.fname}/{devicename}") + if len(device_msg) > 40: device_msg += CRLF_TAB + device_msg += f" ({model_display_name})" - tracked_msg += (f"{CRLF_CHK}{mobapp_fname}, {mobapp_devicename} ({Device.raw_model}) >" - f"{CRLF_SP8_HDOT}{devicename}, {Device.fname} " - f"{Device.tracking_mode_fname}") + devices_monitored_cnt += 1 + devices_monitored_msg += device_msg # Remove the mobapp device from the list since we know it is tracked - if mobapp_devicename in unmatched_mobapp_devices: - unmatched_mobapp_devices.pop(mobapp_devicename) + if mobapp_dname in unmatched_mobapp_devices: + unmatched_mobapp_devices.pop(mobapp_dname) - setup_notify_service_name_for_mobapp_devices() + mobapp_interface.setup_notify_service_name_for_mobapp_devices() # Devices in the list were not matched with an iCloud3 device or are disabled - for mobapp_devicename, mobapp_id in unmatched_mobapp_devices.items(): - devicename = Gb.devicenames_x_mobapp_devicenames.get(mobapp_devicename, 'unknown') + # Setup not monitored message + for mobapp_dname, mobapp_id in unmatched_mobapp_devices.items(): + devicename = Gb.devicenames_x_mobapp_dnames.get(mobapp_dname, 'unknown') Device = Gb.Devices_by_devicename.get(devicename) + conf_mobapp_dname = Device.conf_device[CONF_MOBILE_APP_DEVICE] if Device else '' try: - mobapp_fname = Gb.mobapp_fnames_x_mobapp_id[mobapp_id] - mobapp_dev_info = device_info_by_mobapp_devicename[mobapp_devicename] - fname_dev_type = mobapp_dev_info.rsplit('(') - mobapp_dev_type = f"({fname_dev_type[1]}" + mobapp_fname = Gb.device_info_by_mobapp_dname[mobapp_dname][0] + mobapp_model = Gb.device_info_by_mobapp_dname[mobapp_dname][3] except Exception as err: - # log_exception(err) - mobapp_info = f"{mobapp_devicename.replace('_', ' ').title()}(?)" - mobapp_fname = mobapp_info - mobapp_dev_type = '' + log_exception(err) + mobapp_fname = f"{RED_X}{mobapp_dname} (UNKNOWN DEVICE)" + Gb.conf_startup_errors_by_devicename[mobapp_fname] = f"UNKNOWN DEVICE" + mobapp_model = '' - duplicate_msg = ' (DUPLICATE NAME)' if mobapp_fname in verified_mobapp_fnames else '' crlf_sym = CRLF_X - device_msg = "Not Monitored" if Device else "Not Assigned to an iCloud3 Device" + duplicate_msg = ' (DUPLICATE NAME)' if mobapp_fname in verified_mobapp_fnames else '' + status_msg= "Not Monitored" if Device else "Not Assigned" if mobapp_id in Gb.mobapp_fnames_disabled: if Device: - device_msg = "DISABLED IN MOBILE APP INTEGRATION" + status_msg = "DISABLED" crlf_sym = CRLF_RED_X else: - device_msg += ' (Disabled)' + status_msg += 'Disabled' - tracked_msg += (f"{crlf_sym}{mobapp_fname}, {mobapp_devicename} {mobapp_dev_type} >") + device_msg = (f"{crlf_sym}{mobapp_fname}{RARROW}") if Device: Device.set_fname_alert(YELLOW_ALERT) - tracked_msg += (f"{CRLF_SP8_HDOT}{Device.fname}, {Device.devicename}") - tracked_msg += f"{CRLF_SP8_HDOT}{device_msg}{duplicate_msg}" - post_event(tracked_msg) + device_msg += f"{Device.devicename}" + if device_msg or duplicate_msg: device_msg += ', ' + if len(device_msg) > 40: device_msg += CRLF_TAB + device_msg += f"{status_msg}{duplicate_msg} ({mobapp_model})" + + if crlf_sym == CRLF_DOT: + devices_monitored_cnt += 1 + devices_monitored_msg += device_msg + else: + devices_not_monitored_msg += device_msg + + evlog_msg =(f"Mobile App Devices > " + f"{devices_monitored_cnt} of {len(Gb.conf_devices)} monitored" + f"{devices_monitored_msg}" + f"{devices_not_monitored_msg}") + post_event(evlog_msg) _display_any_mobapp_errors( mobapp_error_mobile_app_msg, mobapp_error_search_msg, @@ -2291,9 +2122,9 @@ def setup_tracked_devices_for_mobapp(): return #-------------------------------------------------------------------- -def _search_for_mobapp_device(devicename, Device, mobapp_id_by_mobapp_devicename, conf_mobapp_device): +def _scan_for_for_mobapp_device(devicename, Device, conf_mobapp_dname): ''' - The conf_mobapp_devicename parameter starts with 'Search: '. Scan the list of mobapp + The conf_mobapp_dname parameter starts with 'ScanFor: '. Scan the list of mobapp devices and find the one that starts with the ic3_devicename value Return: @@ -2301,12 +2132,12 @@ def _search_for_mobapp_device(devicename, Device, mobapp_id_by_mobapp_devicename None - More than one entity or no entities were found ''' - conf_mobapp_device = conf_mobapp_device.replace('Search: ', '') + conf_mobapp_dname = conf_mobapp_dname.replace('ScanFor: ', '') - matched_mobapp_devices = [k for k, v in mobapp_id_by_mobapp_devicename.items() - if k.startswith(conf_mobapp_device) and v.startswith('DISABLED') is False] + matched_mobapp_devices = [k for k, v in Gb.mobapp_id_by_mobapp_dname.items() + if k.startswith(conf_mobapp_dname) and v.startswith('DISABLED') is False] - Gb.debug_log['_.matched_mobapp_devices'] = matched_mobapp_devices + Gb.startup_lists['_.matched_mobapp_devices'] = matched_mobapp_devices if len(matched_mobapp_devices) == 1: return matched_mobapp_devices[0] @@ -2315,7 +2146,7 @@ def _search_for_mobapp_device(devicename, Device, mobapp_id_by_mobapp_devicename return None elif len(matched_mobapp_devices) > 1: - mobapp_devicename = matched_mobapp_devices[-1] + mobapp_dname = matched_mobapp_devices[-1] post_startup_alert( f"Mobile App device_tracker entity scan found several devices: " f"{Device.fname_devicename}") @@ -2326,18 +2157,18 @@ def _search_for_mobapp_device(devicename, Device, mobapp_id_by_mobapp_devicename f"{CRLF}{'-'*75}" f"{CRLF}Count-{len(matched_mobapp_devices)}, " f"{CRLF}Entities-{', '.join(matched_mobapp_devices)}, " - f"{CRLF}Monitored-{mobapp_devicename}") + f"{CRLF}Monitored-{mobapp_dname}") log_error_msg(f"iCloud3 Error > Mobile App Config Error > Dev Trkr Entity not found " - f"during Search {conf_mobapp_device}_???. " + f"during scan_for {conf_mobapp_dname}_???. " f"See iCloud3 Event Log > Startup Stage 4 for more info.") - return mobapp_devicename + return mobapp_dname return None #-------------------------------------------------------------------- def _display_any_mobapp_errors( mobapp_error_mobile_app_msg, - mobapp_error_search_msg, + mobapp_error_scan_for_msg, mobapp_error_disabled_msg, mobapp_error_not_found_msg): @@ -2357,18 +2188,18 @@ def _display_any_mobapp_errors( mobapp_error_mobile_app_msg, f"battery_level or last_update_trigger sensor entities were not found." f"{mobapp_error_mobile_app_msg}") - if mobapp_error_search_msg: + if mobapp_error_scan_for_msg: post_startup_alert( f"Mobile App Config Error > Device Tracker Entity not found" - f"{mobapp_error_search_msg.replace(CRLF_X, CRLF_DOT)}") + f"{mobapp_error_scan_for_msg.replace(CRLF_X, CRLF_DOT)}") post_event( 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." - f"{mobapp_error_search_msg}" - f"{more_info('mobapp_error_search_msg')}") + f"{mobapp_error_scan_for_msg}" + f"{more_info('mobapp_error_scan_for_msg')}") 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"{mobapp_error_scan_for_msg}. " f"See iCloud3 Event Log > Startup Stage 4 for more info.") if mobapp_error_not_found_msg: @@ -2401,52 +2232,20 @@ def _display_any_mobapp_errors( mobapp_error_mobile_app_msg, f"See iCloud3 Event Log > Startup Stage 4 for more info.") #-------------------------------------------------------------------- -def setup_notify_service_name_for_mobapp_devices(post_event_msg=False): - ''' - Get the MobApp device_tracker entities from the entity registry. Then cycle through the - Devices being tracked and match them up. Anything left over at the end is not matched and not monitored. - Parameters: - post_event_msg - - Post an event msg indicating the notify device names were set up. This is done - when they are set up when this is run after HA has started - - ''' - mobile_app_notify_devicenames = mobapp_interface.get_mobile_app_notify_devicenames() - - setup_msg = '' - - # Cycle thru the ha notify names and match them up with a device. This function runs - # while iC3 is starting and again when ha has started. HA may run iC3 before - # 'notify.mobile_app' so running again when ha has started makes sure they are set up. - for mobile_app_notify_devicename in mobile_app_notify_devicenames: - mobapp_devicename = mobile_app_notify_devicename.replace('mobile_app_', '') - for devicename, Device in Gb.Devices_by_devicename.items(): - if instr(mobapp_devicename, devicename) or instr(devicename, mobapp_devicename): - if (Device.conf_mobapp_fname != 'None' - and Device.mobapp_monitor_flag - and Device.mobapp[NOTIFY] == ''): - Device.mobapp[NOTIFY] = mobile_app_notify_devicename - setup_msg += (f"{CRLF_DOT}{Device.devicename_fname}{RARROW}{mobile_app_notify_devicename}") - break - - if setup_msg and post_event_msg: - post_event(f"Delayed MobApp Notifications Setup Completed > {setup_msg}") - -#-------------------------------------------------------------------- def log_debug_stage_4_results(): # if Gb.log_debug_flag: # log_debug_msg(f"{Gb.Devices=}") # log_debug_msg(f"{Gb.Devices_by_devicename=}") # log_debug_msg(f"{Gb.conf_devicenames=}") - # log_debug_msg(f"{Gb.conf_famshr_devicenames=}") + # log_debug_msg(f"{Gb.conf_icloud_dnames=}") # self.devices_not_set_up = [] - # self.device_id_by_famshr_fname = {} # Example: {'Gary-iPhone': 'n6ofM9CX4j...'} - # self.famshr_fname_by_device_id = {} # Example: {'n6ofM9CX4j...': 'Gary-iPhone14'} - # self.device_info_by_famshr_fname = {} # Example: {'Gary-iPhone': 'Gary-iPhone (iPhone 14 Pro (iPhone15,2)'} + # self.device_id_by_icloud_dname = {} # Example: {'Gary-iPhone': 'n6ofM9CX4j...'} + # self.icloud_dname_by_device_id = {} # Example: {'n6ofM9CX4j...': 'Gary-iPhone14'} + # self.device_info_by_icloud_dname = {} # Example: {'Gary-iPhone': 'Gary-iPhone (iPhone 14 Pro (iPhone15,2)'} # self.device_model_info_by_fname = {} # {'Gary-iPhone': [raw_model,model,model_display_name]} - # self.dup_famshr_fname_cnt + # self.dup_icloud_dname_cnt return #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> @@ -2455,54 +2254,19 @@ def log_debug_stage_4_results(): # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -# def remove_unverified_untrackable_devices(PyiCloud=None): - -# if PyiCloud is None: PyiCloud = Gb.PyiCloud -# if PyiCloud is None: -# return -# if PyiCloud.FamilySharing is None and PyiCloud.FindMyFriends is None: -# return - -# _Devices_by_devicename = Gb.Devices_by_devicename.copy() -# device_removed_flag = False -# alert_msg =(f"{EVLOG_ALERT}UNTRACKABLE DEVICES ALERT > Devices are not being tracked:") -# for devicename, Device in _Devices_by_devicename.items(): -# Device.display_info_msg("Verifing Devices") - -# # Device not verified as valid FmF, FamShr or MobApp device. Remove from devices list -# if Device.data_source is None or Device.verified_flag is False: -# device_removed_flag = True -# alert_msg +=(f"{CRLF_DOT}{devicename} ({Device.fname_devtype})") - -# devicename = Device.devicename -# if Device.device_id_famshr: -# Gb.Devices_by_icloud_device_id.pop(Device.device_id_famshr) -# if Device.device_id_fmf: -# Gb.Devices_by_icloud_device_id.pop(Device.device_id_fmf) - -# Gb.Devices_by_devicename.pop(devicename) - -# if device_removed_flag: -# alert_msg +=f"{more_info('unverified_device')}" -# post_event(alert_msg) -# post_startup_alert('Some devices are not being tracked') - -#------------------------------------------------------------------------------ def set_devices_verified_status(): ''' Cycle thru the Devices and set verified status based on data sources ''' for devicename, Device in Gb.Devices_by_devicename.items(): - Device.verified_flag = (Device.verified_FAMSHR - or Device.verified_FMF + Device.verified_flag = (Device.verified_ICLOUD or Device.verified_MOBAPP) - # If the data source is FamShr and the device is not verified, set the + # If the data source is iCloud and the device is not verified, set the # data source to MobApp if (Device.verified_flag - and Device.is_data_source_FAMSHR_FMF - and Device.verified_FAMSHR is False - and Device.verified_FMF is False + and Device.is_data_source_ICLOUD + and Device.verified_ICLOUD is False and Device.verified_MOBAPP): Device.primary_data_source = MOBAPP @@ -2519,8 +2283,8 @@ def identify_tracked_monitored_devices(): else: Gb.Devices_by_devicename_monitored[devicename] = Device - 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 + Gb.startup_lists['Gb.Devices_by_devicename_tracked'] = Gb.Devices_by_devicename_tracked + Gb.startup_lists['Gb.Devices_by_devicename_monitored'] = Gb.Devices_by_devicename_monitored #------------------------------------------------------------------------------ def _refresh_all_devices_sensors(): @@ -2536,17 +2300,6 @@ def setup_trackable_devices(): Display a list of all the devices that are tracked and their tracking information ''' - # Cycle thru any paired devices and associate them with each other - # Gb.PairedDevices_by_paired_with_id={'NDM0NTU2NzE3': [, ]} - for PairedDevices in Gb.PairedDevices_by_paired_with_id.values(): - if len(PairedDevices) != 2 or PairedDevices[0] is PairedDevices[1]: - continue - try: - PairedDevices[0].PairedDevice = PairedDevices[1] - PairedDevices[1].PairedDevice = PairedDevices[0] - except: - pass - _display_all_devices_config_info() # Initialize distance_to_other_devices, add other devicenames to this Device's field @@ -2565,14 +2318,14 @@ def setup_trackable_devices(): 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.use_data_source_ICLOUD is False: if Gb.conf_data_source_ICLOUD: - post_event("iCloud Location Tracking is not available") + post_event("Data Source > Apple Account not available") else: - post_event("iCloud Location Tracking is not being used") + post_event("Data Source > Apple Account not used") if Gb.conf_data_source_MOBAPP is False: - post_event("Mobile App Location Tracking is not being used") + post_event("Data Source > HA Mobile App not used") #------------------------------------------------------------------------------ def _display_all_devices_config_info(): @@ -2581,79 +2334,117 @@ def _display_all_devices_config_info(): if Device.verified_flag: tracking_mode = '' else: - Gb.reinitialize_icloud_devices_flag = (Gb.conf_famshr_device_cnt > 0) + Gb.reinitialize_icloud_devices_flag = (Gb.conf_icloud_device_cnt > 0) tracking_mode = f"{RED_X} NOT " Device.evlog_fname_alert_char += RED_X tracking_mode += 'Monitored' if Device.is_monitored else 'Tracked' - event_msg =(f"{tracking_mode} Device > {devicename} ({Device.fname_devtype})") - - if Gb.primary_data_source_ICLOUD: - event_msg += f"{CRLF_DOT}iCloud Tracking Parameters:" - if Device.is_data_source_FAMSHR: - Gb.used_data_source_FAMSHR = True - event_msg += f"{CRLF_HDOT}FamShr Device: {Device.conf_famshr_name}" - - # if Device.is_data_source_FMF: - # Gb.used_data_source_FMF = True - # event_msg += f"{CRLF_HDOT}FmF Device: {Device.conf_fmf_email}" + if instr(tracking_mode, 'NOT'): + tracking_mode = tracking_mode.upper() + evlog_msg =(f"{Device.fname_devicename} > {Device.devtype_fname}, {tracking_mode}") + + # Device is not in configured Apple acct if it was not matched + conf_apple_acct = Device.conf_device[CONF_APPLE_ACCOUNT] + icloud_dname = Device.conf_icloud_dname + error_icloud_dname = '' + + # Device apple acct and Apple acct fname are configured + if Device.conf_apple_acct_username or Device.conf_icloud_dname: + evlog_msg += ( f"{CRLF_DOT}iCloud Tracking Parameters:" + f"{CRLF_HDOT}Apple Account: ") + apple_acct = Device.conf_apple_acct_username + + # Apple acct fname specified, Apple acct not specified + if apple_acct == '': + evlog_msg += f"{RED_X}None (NOT SPECIFIED)" + Gb.conf_startup_errors_by_devicename[devicename] = "Apple Acct not specified" + log_error_msg( f"iCloud3 Device Configuration Error > " + f"{Device.fname_devicename}, Apple Acct not specified") + + # Apple acct fname specified, Apple acct specified, fname is in known Apple acct + elif apple_acct in Gb.PyiCloud_by_username: + _PyiCloud = Gb.PyiCloud_by_username[apple_acct] + # fname is in this Apple acct + if icloud_dname in _PyiCloud.device_id_by_icloud_dname: + evlog_msg += _PyiCloud.account_owner_username + + # fname is not in this Apple acct + else: + evlog_msg += f"{Device.fname}" + error_icloud_dname = f"{RED_X}{icloud_dname}, Not Found in Apple Acct" + Gb.conf_startup_errors_by_devicename[devicename] = ( + f"{icloud_dname}, Not in Apple Acct-{conf_apple_acct}") + log_error_msg(f"iCloud3 Device Configuration Error > " + f"{Device.fname_devicename}, " + f"iCloudDevice-{icloud_dname}, Not in Apple Acct-{conf_apple_acct}") + + # Apple acct fname specified, Apple acct specified, Apple acct not known + else: + evlog_msg += f"{RED_X}{apple_acct} (UNKNOWN APPLE ACCT)" + Gb.conf_startup_errors_by_devicename[devicename] = f"{apple_acct}, Unknown Apple Acct" + log_error_msg( f"iCloud3 Device Configuration Error > " + f"{Device.fname_devicename}, AppleAcct-{apple_acct}, Unknown Apple Acct") - if Device.PairedDevice is not None: - event_msg += (f"{CRLF_HDOT}Paired Device: {Device.PairedDevice.fname_devicename}") + evlog_msg += f"{CRLF_HDOT}iCloud Device: " + evlog_msg += error_icloud_dname or Device.conf_icloud_dname + else: + evlog_msg += f"{CRLF_DOT}iCloud Device: Not used as a data source" # Set a flag indicating there is a tracked device that does not use the Mobile App if Device.mobapp_monitor_flag is False and Device.is_tracked: - Gb.mobapp_monitor_any_devices_false_flag = True + Gb.device_not_monitoring_mobapp = True if Device.mobapp[DEVICE_TRACKER] == '': - event_msg += f"{CRLF_DOT}Mobile App Tracking Parameters: Mobile App Not Used" + if Device.conf_mobapp_fname: + evlog_msg += f"{CRLF_YELLOW_ALERT}Mobile App Device > {Device.conf_mobapp_fname}, Not Found" + else: + evlog_msg += f"{CRLF_DOT}Mobile App Tracking Parameters: Mobile App Not Used" + else: - event_msg += CRLF_DOT if Device.mobapp_monitor_flag else CRLF_RED_X - event_msg += f"Mobile App Tracking Parameters:" + evlog_msg += CRLF_DOT if Device.mobapp_monitor_flag else CRLF_RED_X + evlog_msg += f"Mobile App Tracking Parameters:" trigger_entity = Device.mobapp[TRIGGER][7:] or 'UNKNOWN' bat_lev_entity = Device.mobapp[BATTERY_LEVEL][7:] or 'UNKNOWN' notify_entity = Device.mobapp[NOTIFY] or 'WAITING FOR NOTIFY SVC TO START"' - event_msg += ( f"{CRLF_HDOT}Device Tracker: {Device.mobapp[DEVICE_TRACKER]}" - f"{CRLF_HDOT}Action Trigger.: {trigger_entity}" - f"{CRLF_HDOT}Battery Sensor: {bat_lev_entity}" - f"{CRLF_HDOT}Notifications...: {notify_entity}") - - event_msg += f"{CRLF_DOT}Other Parameters:" - event_msg += f"{CRLF_HDOT}inZone Interval: {format_timer(Device.inzone_interval_secs)}" + evlog_msg += ( f"{CRLF_HDOT}MobApp Device: {Device.conf_mobapp_fname}" + f"{CRLF_HDOT}Device Tracker..: {Device.mobapp[DEVICE_TRACKER]}" + f"{CRLF_HDOT}Action Trigger...: {trigger_entity}" + f"{CRLF_HDOT}Battery Sensor..: {bat_lev_entity}" + f"{CRLF_HDOT}Notifications.....: {notify_entity}") + + evlog_msg += f"{CRLF_DOT}Other Parameters:" + evlog_msg += f"{CRLF_HDOT}inZone Interval: {format_timer(Device.inzone_interval_secs)}" if Device.fixed_interval_secs > 0: - event_msg += f"{CRLF_HDOT}Fixed Interval: {format_timer(Device.fixed_interval_secs)}" + evlog_msg += f"{CRLF_HDOT}Fixed Interval: {format_timer(Device.fixed_interval_secs)}" if 'none' not in Device.log_zones: log_zones_fname = [zone_dname(zone) for zone in Device.log_zones] log_zones = list_to_str(log_zones_fname) log_zones = f"{log_zones.replace(', Name-', f'{RARROW}(')}.csv)" - event_msg += f"{CRLF_HDOT}Log Zone Activity: {log_zones}" + evlog_msg += f"{CRLF_HDOT}Log Zone Activity: {log_zones}" if Device.track_from_base_zone != HOME: - event_msg += f"{CRLF_HDOT}Primary Track from Zone: {zone_dname(Device.track_from_base_zone)}" + evlog_msg += f"{CRLF_HDOT}Primary Track from Zone: {zone_dname(Device.track_from_base_zone)}" if Device.track_from_zones != [HOME]: tfz_fnames = [zone_dname(zone) for zone in Device.track_from_zones] - event_msg += (f"{CRLF_HDOT}Track from Zones: {list_to_str(tfz_fnames)}") + evlog_msg += (f"{CRLF_HDOT}Track from Zones: {list_to_str(tfz_fnames)}") if Device.away_time_zone_offset != 0: plus_minus = '+' if Device.away_time_zone_offset > 0 else '' - event_msg += (f"{CRLF_HDOT}Away Time Zone: HomeZone {plus_minus}{Device.away_time_zone_offset} hours") + evlog_msg += (f"{CRLF_HDOT}Away Time Zone: HomeZone {plus_minus}{Device.away_time_zone_offset} hours") try: - device_status = Device.PyiCloud_RawData_famshr.device_data[ICLOUD_DEVICE_STATUS] - timestamp = Device.PyiCloud_RawData_famshr.device_data[LOCATION][TIMESTAMP] - if device_status == '201': - event_msg += (f"{CRLF_RED_X}DEVICE IS OFFLINE > " + device_status = Device.PyiCloud_RawData_icloud.device_data[ICLOUD_DEVICE_STATUS] + timestamp = Device.PyiCloud_RawData_icloud.device_data[LOCATION][TIMESTAMP] + if device_status == '201' and mins_since(timestamp) > 5: + evlog_msg += (f"{CRLF_RED_X}DEVICE IS OFFLINE > " f"Since-{format_time_age(timestamp)}") Device.offline_secs = timestamp - # if Device.no_location_data: - # event_msg += f"{CRLF_RED_X}NO GPS DATA RETURNED FROM ICLOUD LOCATION SERVICE" - except Exception as err: # log_exception(err) pass - post_event(event_msg) + post_event(evlog_msg) #------------------------------------------------------------------------------ def display_inactive_devices(): @@ -2662,18 +2453,20 @@ def display_inactive_devices(): ''' inactive_devices =[(f"{conf_device[CONF_IC3_DEVICENAME]} (" - f"{conf_device[CONF_FNAME]}/{conf_device[CONF_DEVICE_TYPE]})") + f"{conf_device[CONF_FNAME]}/" + f"{DEVICE_TYPE_FNAME.get( + conf_device[CONF_DEVICE_TYPE], conf_device[CONF_DEVICE_TYPE])})") for conf_device in Gb.conf_devices if conf_device[CONF_TRACKING_MODE] == INACTIVE_DEVICE] - Gb.debug_log['_.inactive_devices'] = inactive_devices + Gb.startup_lists['_.inactive_devices'] = inactive_devices if inactive_devices == []: return - event_msg = f"Inactive/Untracked Devices > " - event_msg+= list_to_str(inactive_devices, separator=CRLF_DOT) - post_event(event_msg) + evlog_msg = f"Inactive/Untracked Devices > " + evlog_msg+= list_to_str(inactive_devices, separator=CRLF_DOT) + post_event(evlog_msg) if len(inactive_devices) == len(Gb.conf_devices): post_startup_alert('All devices are Inactive and nothing will be tracked') @@ -2701,23 +2494,36 @@ def display_object_lists(): post_monitor_msg(monitor_msg) #-------------------------------------------------------------------- -def write_debug_log(debug_log_title=None): +def dump_startup_lists_to_log(debug_log_title=None): ''' - Cycle thru the Gb.debug_log dictionary that contains internal lists and + Cycle thru the Gb.startup_lists dictionary that contains internal lists and dictionaries. Write all items to the icloud3-0.log file ''' - if Gb.log_debug_flag is False or Gb.debug_log == {}: return + if Gb.startup_lists == {}: return if debug_log_title: log_debug_msg(f"{format_header_box(debug_log_title)}") - for field, values in Gb.debug_log.items(): + _startup_lists = Gb.startup_lists.copy() + for field, values in _startup_lists.items(): log_debug_msg(f"{field}={values}") + # Check to see if any items were added by another task while writing the list to the + # log. That can generate a 'RuntimeError: dictionary changed size during iteration' + items_start = len(_startup_lists) + items_end = len(Gb.startup_lists) + if items_end > items_start: + cnt = -1 + for field, values in _startup_lists.items(): + cnt += 1 + if cnt >= items_start: + log_debug_msg(f"{field}={values}") + + if debug_log_title: log_debug_msg(f"{format_header_box(debug_log_title)}") - Gb.debug_log = {} + Gb.startup_lists = {} #------------------------------------------------------------------------------ def display_platform_operating_mode_msg(): @@ -2739,6 +2545,19 @@ def post_restart_icloud3_complete_msg(): for devicename, Device in Gb.Devices_by_devicename.items(): # Device.display_info_msg("Setup Complete, Locating Device") - post_event(f"{EVLOG_IC3_STARTING}Initializing iCloud3 v{Gb.version} > Complete") + post_event(f"{EVLOG_IC3_STARTING}Initializing {ICLOUD3_VERSION_MSG} > Complete") - Gb.EvLog.update_event_log_display("") \ No newline at end of file + Gb.EvLog.update_event_log_display("") + +#------------------------------------------------------------------------------ +def dump_gb_dictionaries_to_icloud_log(): + pass + + Gb.startup_lists['Gb.Devices_by_devicename_tracked'] = Gb.Devices_by_devicename_tracked + Gb.startup_lists['Gb.Devices_by_devicename_monitored'] = Gb.Devices_by_devicename_monitored + Gb.startup_lists['Gb.devicenames_by_mobapp_dname'] = Gb.devicenames_by_mobapp_dname + Gb.startup_lists['Gb.mobile_app_device_fnames'] = Gb.mobile_app_device_fnames + Gb.startup_lists['Gb.mobapp_fnames_by_mobapp_id'] = Gb.mobapp_fnames_by_mobapp_id + Gb.startup_lists['Gb.mobapp_ids_by_mobapp_fname'] = Gb.mobapp_ids_by_mobapp_fname + Gb.startup_lists['Gb.mobapp_fnames_disabled'] = Gb.mobapp_fnames_disabled + Gb.startup_lists['Gb.model_display_name_by_raw_model'] = Gb.model_display_name_by_raw_model \ No newline at end of file diff --git a/custom_components/icloud3/support/start_ic3_control.py b/custom_components/icloud3/support/start_ic3_control.py index 4f4f78d..95e1f2d 100644 --- a/custom_components/icloud3/support/start_ic3_control.py +++ b/custom_components/icloud3/support/start_ic3_control.py @@ -1,40 +1,38 @@ from ..global_variables import GlobalVariables as Gb -from ..const import (NOT_SET, IC3LOG_FILENAME, - CRLF, CRLF_DOT, CRLF_HDOT, CRLF_X, NL, NL_DOT, +from ..const import (VERSION, VERSION_BETA, ICLOUD3, ICLOUD3_VERSION, DOMAIN, ICLOUD3_VERSION_MSG, + NOT_SET, IC3LOG_FILENAME, + CRLF, CRLF_DOT, CRLF_HDOT, CRLF_X, NL, NL_DOT, LINK, EVLOG_ALERT, EVLOG_ERROR, 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, + CONF_VERSION, ICLOUD, ZONE_DISTANCE, + CONF_USERNAME, CONF_PASSWORD, CONF_LOCATE_ALL, + ICLOUD, MOBAPP, DISTANCE_TO_DEVICES, ) from ..support import hacs_ic3 from ..support import start_ic3 from ..support import config_file -#from ..support import pyicloud_ic3_interface -from ..support import icloud_data_handler +from ..support import mobapp_interface +from ..support import pyicloud_ic3_interface from ..support import determine_interval as det_interval -from ..helpers.common import (instr, obscure_field, list_to_str, ) +from ..helpers.common import (instr, is_empty, isnot_empty, list_to_str, list_add, list_del, ) from ..helpers.messaging import (broadcast_info_msg, post_event, post_error_msg, log_error_msg, post_startup_alert, post_monitor_msg, post_internal_error, log_start_finish_update_banner, log_debug_msg, log_warning_msg, log_info_msg, log_exception, log_rawdata, - _trace, _traceha, more_info, format_filename, + _evlog, _log, more_info, format_filename, write_config_file_to_ic3log, open_ic3log_file, ) from ..helpers.time_util import (time_now_secs, calculate_time_zone_offset, ) import homeassistant.util.dt as dt_util -import os #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> def stage_1_setup_variables(): - # new_log_file=False - # ic3logger_file = Gb.hass.config.path(IC3LOG_FILENAME) - # filemode = 'w' if (new_log_file or os.path.isfile(ic3logger_file) is False) else 'a' Gb.trace_prefix = 'STAGE1' stage_title = f'Stage 1 > Initial Preparations' @@ -43,16 +41,9 @@ 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 - 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.startup_alerts = [] Gb.EvLog.alert_message = '' @@ -60,26 +51,37 @@ def stage_1_setup_variables(): Gb.reinitialize_icloud_devices_flag = False # Set when no devices are tracked and iC3 needs to automatically restart Gb.reinitialize_icloud_devices_cnt = 0 + config_file.load_storage_icloud3_configuration_file() + write_config_file_to_ic3log() + start_ic3.initialize_global_variables() + start_ic3.set_global_variables_from_conf_parameters() + + # Run these setup items on a restart. Do not then when initially starting iC3 if Gb.initial_icloud3_loading_flag is False: Gb.EvLog.startup_event_recds = [] 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() + if (Gb.use_data_source_ICLOUD): + # Can not run this as an executor job to avoid 'no running event loop' error + # Gb.hass.async_add_executor_job( + # pyicloud_ic3_interface.create_all_PyiCloudServices) + pyicloud_ic3_interface.create_all_PyiCloudServices() start_ic3.define_tracking_control_fields() if Gb.ha_config_directory != '/config': - post_event(f"Base Config Directory > {Gb.ha_config_directory}") - post_event(f"iCloud3 Directory > {Gb.icloud3_directory}") + post_event( f"Base Config Directory > " + f"{CRLF_DOT}{Gb.ha_config_directory}") + post_event( f"iCloud3 Directory > " + f"{CRLF_DOT}{Gb.icloud3_directory}") if Gb.conf_profile[CONF_VERSION] == 0: - post_event(f"iCloud3 Configuration File > {format_filename(Gb.config_ic3_yaml_filename)}") + post_event( f"iCloud3 Configuration File > " + f"{CRLF_DOT}{format_filename(Gb.config_ic3_yaml_filename)}") else: - post_event(f"iCloud3 Configuration File > {format_filename(Gb.icloud3_config_filename)}") + post_event( f"iCloud3 Configuration File > " + f"{CRLF_DOT}{format_filename(Gb.icloud3_config_filename)}") start_ic3.display_platform_operating_mode_msg() Gb.hass.loop.create_task(start_ic3.update_lovelace_resource_event_log_js_entry()) @@ -97,6 +99,7 @@ def stage_1_setup_variables(): except Exception as err: log_exception(err) + #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> def stage_2_prepare_configuration(): @@ -108,11 +111,6 @@ def stage_2_prepare_configuration(): Gb.EvLog.display_user_message(stage_title) broadcast_info_msg(stage_title) - if Gb.initial_icloud3_loading_flag is False: - Gb.PyiCloud = None - - # start_ic3.initialize_global_variables() - # start_ic3.set_global_variables_from_conf_parameters() start_ic3.create_Zones_object() start_ic3.create_Waze_object() @@ -133,13 +131,14 @@ def stage_2_prepare_configuration(): elif Gb.conf_profile[CONF_VERSION] == 0: configuration_needed_msg = 'CONFIGURATION PARAMETERS WERE MIGRATED FROM v2 to v3 - ' \ 'THEY MUST BE REVIEWED BEFORE STARTING ICLOUD3' - elif Gb.conf_devices == []: - configuration_needed_msg = 'DEVICES MUST BE SET UP TO ENABLE TRACKING' + elif ((is_empty(Gb.conf_apple_accounts) and Gb.conf_data_source_ICLOUD) + or is_empty(Gb.conf_devices)): + configuration_needed_msg = 'ICLOUD3 CONFIGURATION NEEDS TO BE SET UP' if configuration_needed_msg: - post_startup_alert('iCloud3 Integration not set up') + post_startup_alert('iCloud3 Configuration not set up') event_msg =(f"{EVLOG_ALERT}CONFIGURATION ALERT > {configuration_needed_msg}{CRLF}" - f"{more_info('add_icloud3_integration')}") + f"{more_info('configure_icloud3')}") post_event(event_msg) Gb.EvLog.update_event_log_display("") @@ -147,13 +146,12 @@ def stage_2_prepare_configuration(): except Exception as err: log_exception(err) - #write_debug_log() #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> def stage_3_setup_configured_devices(): Gb.trace_prefix = 'STAGE3' - stage_title = f'Stage 3 > Prepare Configured Devices' + stage_title = f'Stage 3 > Device Configuration' log_info_msg(f"* > {EVLOG_IC3_STAGE_HDR}{stage_title}") try: @@ -162,19 +160,17 @@ def stage_3_setup_configured_devices(): # 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}, " + Gb.conf_startup_errors_by_devicename = {} + + if Gb.conf_data_source_ICLOUD: data_sources += f"{ICLOUD}, " + if Gb.conf_data_source_MOBAPP: data_sources += f"{MOBAPP}, " 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 - # and Gb.fmf_device_verified_cnt < len(Gb.Devices)): - # Gb.config_track_devices_change_flag = True - elif (Gb.conf_data_source_FAMSHR - and Gb.famshr_device_verified_cnt < len(Gb.Devices)): + elif (Gb.conf_data_source_ICLOUD + and Gb.icloud_device_verified_cnt < len(Gb.Devices)): Gb.config_track_devices_change_flag = True elif Gb.log_debug_flag: Gb.config_track_devices_change_flag = True @@ -184,65 +180,183 @@ def stage_3_setup_configured_devices(): except Exception as err: log_exception(err) - #write_debug_log() - post_event(f"{EVLOG_IC3_STAGE_HDR}{stage_title}") Gb.EvLog.update_event_log_display("") + #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -def stage_4_setup_data_sources(retry=False): +def stage_4_setup_data_sources(): Gb.trace_prefix = 'STAGE4' - stage_title = f"Stage 4 > Setup iCloud & MobApp Data Source" + stage_title = f"Stage 4 > Data Source Device Assignment" log_info_msg(f"* > {EVLOG_IC3_STAGE_HDR}{stage_title}") - - # Missing username/password, PyiCloud can not be started - if Gb.primary_data_source_ICLOUD: - if Gb.username == '' or Gb.password == '': - Gb.conf_data_source_FAMSHR = False - Gb.conf_data_source_FMF = False - Gb.primary_data_source_ICLOUD = False - post_startup_alert('iCloud username/password invalid or not set up') - post_event( f"{EVLOG_ALERT}CONFIGURATION ALERT > The iCloud username or password has not been " - f"set up or is incorrect. iCloud will not be used for location tracking") - - return_code = True Gb.EvLog.display_user_message(stage_title) broadcast_info_msg(stage_title) + # Remove any device related errors so a retry will not show previous alerts + _startup_alerts = [_startup_alert for _startup_alert in Gb.startup_alerts + if instr(_startup_alert.lower(), 'device') is False] + Gb.startup_alerts = _startup_alerts + + for Device in Gb.Devices: + Device.set_fname_alert('') + + post_event(f"Data Source > Apple Account used-{Gb.use_data_source_ICLOUD}") + post_event(f"Data Source > HA Mobile App used-{Gb.use_data_source_MOBAPP}") + try: - if Gb.primary_data_source_ICLOUD: - account_name = Gb.PyiCloud.account_name if Gb.PyiCloud else '' - post_event(f"iCloud Account > Logging Into-{account_name} " - f"({obscure_field(Gb.username)})") + if Gb.use_data_source_ICLOUD: + _log_into_apple_accounts() + start_ic3.setup_data_source_ICLOUD() - if Gb.PyiCloud is None: - post_event('iCloud Location Service > Not used as a data source') - elif Gb.PyiCloud.account_locked: - post_error_msg( f"{EVLOG_ERROR}iCloud Account is Locked. Log onto www.icloud.com " - f"and unlock your account to reauthorize location services. ") - post_startup_alert('iCloud Account is Locked') + for PyiCloud in Gb.PyiCloud_by_username.values(): + if PyiCloud.account_locked: + post_error_msg( f"{EVLOG_ERROR}Apple Account {PyiCloud.account_owner} " + f"is Locked. Log onto www.icloud.com and unlock " + f"your account to reauthorize location services.") + post_startup_alert(f"Apple Account {PyiCloud.account_owner} is Locked") + + mobapp_interface.get_entity_registry_mobile_app_devices() + mobapp_interface.get_mobile_app_integration_device_info() if Gb.conf_data_source_MOBAPP: start_ic3.setup_tracked_devices_for_mobapp() - else: - post_event('Mobile App > Not used as a data source') start_ic3.set_devices_verified_status() - return_code = _are_all_devices_verified(retry=retry) + return_code = _are_all_devices_verified() + + if isnot_empty(Gb.username_pyicloud_503_connection_error): + post_event( f"{EVLOG_ERROR}Apple Acct > {list_to_str(Gb.username_pyicloud_503_connection_error)}, " + f"Failed to log into the Apple Account (Connection Error 503), will " + f"retry every 15-minutes") except Exception as err: log_exception(err) return_code = False - # write_debug_log() - post_event(f"{EVLOG_IC3_STAGE_HDR} {stage_title}") Gb.EvLog.update_event_log_display("") return return_code +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def stage_4_setup_data_sources_retry(final_retry=False): + ''' + Apple accounts with login errors or containing tracked devices that are not found + in Stage 4 are added to Gb.usernames_setup_error_retry_list. iCloud3_main checks to + see if there are any devices that are in this list when starting up. Their setup + will be retried here if necessary. This is a shortened version of the full Stage 4 + where all devices for all data sources are set up. + ''' + + Gb.trace_prefix = 'STAGE4' + stage_title = f"Stage 4 > Data Source Device Assignment (Retry)" + log_info_msg(f"* > {EVLOG_IC3_STAGE_HDR}{stage_title}") + Gb.EvLog.display_user_message(stage_title) + broadcast_info_msg(stage_title) + + post_event(f"{EVLOG_ALERT}Apple Acct setup will be retried to resolve Missing Devices:" + f"{CRLF_DOT}Apple Account > {list_to_str(Gb.usernames_setup_error_retry_list)}") + + # Remove any device related errors so a retry will not show previous alerts + _startup_alerts = [_startup_alert for _startup_alert in Gb.startup_alerts + if instr(_startup_alert.lower(), 'device') is False] + Gb.startup_alerts = _startup_alerts + for Device in Gb.Devices: + Device.set_fname_alert('') + + # Test code for invalid apple_account value. Set config value for dev #2 to invalid@gmail.com + # Then set it to the valid apple_account value here. It will fail in Stage 4 & retry #1 + # but pass in the last_retry + # if final_retry and Device.conf_apple_acct_username == 'invalid@gmail.com': + # Device.conf_apple_acct_username = 'valid@gmail.com' + # Gb.conf_devices[1]['apple_account'] = 'valid@gmail.com' + + try: + all_verified_flag = True + for username in Gb.usernames_setup_error_retry_list: + _log_into_apple_accounts(retry=True) + + PyiCloud = Gb.PyiCloud_by_username.get(username) + + if PyiCloud: + post_event(f"Verify Apple Acct > {PyiCloud.account_owner_username}, Verified") + start_ic3.setup_data_source_ICLOUD(retry=True) + start_ic3.set_devices_verified_status() + + if PyiCloud.account_locked: + post_error_msg( f"{EVLOG_ERROR}Apple Account {PyiCloud.account_owner} " + f"is Locked. Log onto www.icloud.com and unlock " + f"your account to reauthorize location services.") + post_startup_alert(f"Apple Account {PyiCloud.account_owner_USERNAME} is Locked") + else: + post_event(f"{EVLOG_ALERT}APPLE ACCT LOGIN ALERT > {username}, Login Unsuccessful") + + all_verified_flag = _are_all_devices_verified(retry=True) + + if all_verified_flag: + Gb.usernames_setup_error_retry_list = \ + list_del(Gb.usernames_setup_error_retry_list, PyiCloud.account_owner_username) + + except Exception as err: + log_exception(err) + all_verified_flag = False + + + post_event(f"{EVLOG_IC3_STAGE_HDR} {stage_title}") + Gb.EvLog.update_event_log_display("") + + if final_retry: + if is_empty(Gb.usernames_setup_error_retry_list): + post_event(f"{EVLOG_ALERT}ALL ICLOUD STARTUP ERRORS RESOLVED") + else: + post_startup_alert(f"Apple Acct Login Error-{list_to_str(Gb.usernames_setup_error_retry_list)}") + + return all_verified_flag + +#------------------------------------------------------------------ +def _log_into_apple_accounts(retry=False): + ''' + Verify that all Apple Account PyiCloud objects have been created + ''' + if Gb.use_data_source_ICLOUD is False: + return False + + if Gb.initial_icloud3_loading_flag is False: + return True + + # When iCloud3 starts, an executor job is started at the beginning of __init__ + # to connect to all Apple Accounts. Now, cycle through the PyiCloud object table + # and see if any are not set up that should be + # Get list of all unique Apple Acct usernames in config + Gb.conf_usernames = [apple_account[CONF_USERNAME] + for apple_account in Gb.conf_apple_accounts + if apple_account[CONF_USERNAME] in Gb.username_valid_by_username] + + # Verify that all apple accts have been setup. Restart the setup process for any that + # are not complete + for username in Gb.conf_usernames: + PyiCloud = Gb.PyiCloud_by_username.get(username) + if (PyiCloud is None + or PyiCloud.RawData_by_device_id == {}): + + conf_apple_acct, _idx = config_file.conf_apple_acct(username) + + PyiCloud = pyicloud_ic3_interface.log_into_apple_account( + username, + Gb.PyiCloud_password_by_username[username], + locate_all=conf_apple_acct[CONF_LOCATE_ALL]) + + if PyiCloud: + Gb.PyiCloud_by_username[username] = PyiCloud + + if is_empty(Gb.devices_without_location_data): + post_event("Apple Acct > All Devices Located") + else: + post_event(f"Apple Acct > Devices not Located > {list_to_str(Gb.devices_without_location_data)}") + return False + #------------------------------------------------------------------ def _are_all_devices_verified(retry=False): ''' @@ -257,23 +371,30 @@ def _are_all_devices_verified(retry=False): False - Some were not verified ''' - # Get a list of all tracked devices that have not been set p by icloud or the Mobile App - unverified_devices = [devicename + + # Get a list of all tracked devices that have not been set up by icloud or the Mobile App + unverified_devices = [Device.fname_devicename for devicename, Device in Gb.Devices_by_devicename.items() - if Device.verified_flag is False] + if Device.verified_flag is False and Device.isnot_inactive] + unverified_device_usernames = [Device.conf_apple_acct_username + for devicename, Device in Gb.Devices_by_devicename.items() + if Device.verified_flag is False and Device.isnot_inactive] + + Gb.usernames_setup_error_retry_list = unverified_device_usernames + Gb.devicenames_setup_error_retry_list = unverified_devices + + Gb.startup_lists['_.usernames_setup_error_retry_list'] = Gb.usernames_setup_error_retry_list + Gb.startup_lists['_.devicenames_setup_error_retry_list'] = Gb.devicenames_setup_error_retry_list - if unverified_devices == []: + if is_empty(unverified_devices): return True if retry: - post_startup_alert('Some devices could not be verified. Restart iCloud3') - event_msg = (f"{EVLOG_ALERT}Some devices could not be verified. iCloud3 needs to be " - f"restarted to see if the unverified devices are available for " - f"tracking. If not, check the device parameters in the iCloud3 Configure Settings:" - f"{more_info('configure_icloud3')}") + post_startup_alert("Some Tracked Devices could not be verified. Restart may be needed.") + event_msg = (f"{EVLOG_ALERT}Some Tracked Devices could not be verified. Review and correct " + f"any configuration errors. Then restart iCloud3") else: - event_msg = (f"{EVLOG_ALERT}ALERT > Some devices could not be verified. iCloud Location Service " - f"will be reinitialized") + event_msg = f"{EVLOG_ALERT}UNVERIFIED DEVICES ALERT > Some Tracked Devices could not be verified." event_msg += (f"{CRLF_DOT}Unverified Devices > {', '.join(unverified_devices)}") post_event(event_msg) @@ -287,8 +408,10 @@ def stage_5_configure_tracked_devices(): stage_title = f'Stage 5 > Device Configuration Summary' log_info_msg(f"* > {EVLOG_IC3_STAGE_HDR}{stage_title}") - if Gb.PyiCloud: - log_debug_msg(f"PyiCloud Instance Finialized > {Gb.PyiCloud.instance}") + # if Gb.PyiCloud: + # log_debug_msg(f"PyiCloud Finialized > {Gb.PyiCloud.account_owner}") + for username, PyiCloud in Gb.PyiCloud_by_username.items(): + log_debug_msg(f"PyiCloud Finialized > {PyiCloud.account_owner}") try: Gb.EvLog.display_user_message(stage_title) @@ -305,16 +428,15 @@ def stage_5_configure_tracked_devices(): except Exception as err: log_exception(err) - # write_debug_log() - post_event(f"{EVLOG_IC3_STAGE_HDR}{stage_title}") Gb.EvLog.display_user_message('') + #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> def stage_6_initialization_complete(): Gb.trace_prefix = 'STAGE6' - stage_title = f'iCloud3 Initialization Complete' + stage_title = f'{ICLOUD3} Initialization Complete' log_info_msg(f"* > {EVLOG_IC3_STAGE_HDR}{stage_title}") broadcast_info_msg(stage_title) @@ -326,7 +448,7 @@ def stage_6_initialization_complete(): try: start_ic3.display_object_lists() - start_ic3.write_debug_log() + start_ic3.dump_startup_lists_to_log() if Gb.startup_alerts: item_no = 1 @@ -352,23 +474,19 @@ def stage_6_initialization_complete(): #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> def stage_7_initial_locate(): ''' - The PyiCloud Authentication function updates the FamShr raw data after the account + The PyiCloud Authentication function updates the iCloud raw data after the account has been authenticated. Requesting the initial data update there speeds up loading iC3 - since the iCloud acct login & authentication was started in the __init__ module. + since the Apple Acct login & authentication was started in the __init__ module. - This routine processes the new raw FamShr data and set the initial location. - - If there are devices that use FmF and not FamShr, the FamF data will be requested - and those devices will be updated. + This routine processes the new raw iCloud data and set the initial location. ''' - # The restart will be requested if using iCloud as a data source and no data was returned # from PyiCloud - if Gb.PyiCloud is None: - return + if Gb.PyiCloud_by_username == {}: + return - if Gb.reinitialize_icloud_devices_flag and Gb.conf_famshr_device_cnt > 0: + if Gb.reinitialize_icloud_devices_flag and Gb.conf_icloud_device_cnt > 0: return_code = reinitialize_icloud_devices() Gb.trace_prefix = '1stLOC' @@ -376,15 +494,11 @@ def stage_7_initial_locate(): Gb.this_update_secs = time_now_secs() Gb.this_update_time = dt_util.now().strftime('%H:%M:%S') post_event("Requesting Initial Locate") - post_event(f"{EVLOG_IC3_STARTING}iCloud3 v{Gb.version} > Start up Complete") + post_event(f"{EVLOG_IC3_STARTING}{ICLOUD3_VERSION_MSG} > Start up Complete") for Device in Gb.Devices: - if Device.PyiCloud_RawData_famshr: - Device.update_dev_loc_data_from_raw_data_FAMSHR_FMF(Device.PyiCloud_RawData_famshr) - - elif Device.PyiCloud_RawData_fmf: - icloud_data_handler.update_PyiCloud_RawData_data(Device, results_msg_flag=False) - Device.update_dev_loc_data_from_raw_data_FAMSHR_FMF(Device.PyiCloud_RawData_fmf) + if Device.PyiCloud_RawData_icloud: + Device.update_dev_loc_data_from_raw_data_FAMSHR(Device.PyiCloud_RawData_icloud) else: continue @@ -402,10 +516,19 @@ def stage_7_initial_locate(): Device.update_sensors_flag = True - Gb.iCloud3.process_updated_location_data(Device, ICLOUD_FNAME) + Gb.iCloud3.process_updated_location_data(Device, ICLOUD) Device.icloud_initial_locate_done = True + # Update the distance between all devices not that they have all been located + # Then go back through and update the device_tracker entity to set the distance + # between devices for the first time + det_interval.set_dist_to_devices(post_event_msg=True) + for Device in Gb.Devices: + Device.sensors[DISTANCE_TO_DEVICES] = \ + det_interval.format_dist_to_devices_msg(Device, time=True, age=False) + Device.write_ha_device_tracker_state() + #------------------------------------------------------------------ def reinitialize_icloud_devices(): ''' @@ -425,39 +548,18 @@ def reinitialize_icloud_devices(): Gb.start_icloud3_inprocess_flag = False Gb.reinitialize_icloud_devices_flag = False Gb.initial_icloud3_loading_flag = False - Gb.all_tracking_paused_flag = True alert_msg = f"{EVLOG_ALERT}" if Gb.conf_data_source_ICLOUD: - unverified_devices = [devicename for devicename, Device in Gb.Devices_by_devicename.items() \ - if Device.verified_flag is False] - alert_msg +=(f"UNVERIFIED DEVICES > One or more devices was not verified. iCloud Location Svcs " - f"may be down, slow to respond or the internet may be down." - f"{CRLF_DOT}Unverified Devices > {', '.join(unverified_devices)}") + unverified_devices = [devicename + for devicename, Device in Gb.Devices_by_devicename.items() \ + if Device.verified_flag is False] + alert_msg +=(f"UNVERIFIED DEVICES > One or more devices was not verified. " + f"Apple Account access may be down, slow to respond or the internet may be down." + f"{CRLF_DOT}Unverified Devices > {', '.join(unverified_devices)}") post_event(alert_msg) - post_event(f"{EVLOG_IC3_STARTING}Restarting iCloud Location Service") - - if Gb.PyiCloud and Gb.PyiCloud.FamilySharing: - Gb.PyiCloud.FamilySharing.refresh_client() - - stage_4_success = stage_4_setup_data_sources(retry=None) - if stage_4_success is False: - stage_4_success = stage_4_setup_data_sources() - - stage_5_configure_tracked_devices() - stage_6_initialization_complete() - - Gb.all_tracking_paused_flag = False - - if stage_4_success is False: - post_event( f"{EVLOG_ALERT}UNVERIFIED DEVICES > One or more devices was still " - f"not verified" - f"{more_info('unverified_devices_caused_by')}") - - return False - except Exception as err: log_exception(err) diff --git a/custom_components/icloud3/support/stationary_zone.py b/custom_components/icloud3/support/stationary_zone.py index 2f25fd9..9439a50 100644 --- a/custom_components/icloud3/support/stationary_zone.py +++ b/custom_components/icloud3/support/stationary_zone.py @@ -17,7 +17,7 @@ from ..support import mobapp_interface from ..helpers.common import (isbetween, is_statzone, format_gps, zone_dname, ) from ..helpers.messaging import (post_event, post_error_msg, post_monitor_msg, - log_debug_msg, log_exception, log_rawdata, _trace, _traceha, ) + log_debug_msg, log_exception, log_rawdata, _evlog, _log, ) from ..helpers.time_util import (datetime_now, ) from ..helpers.dist_util import (format_dist_m, gps_distance_km, ) # from ..helpers import entity_io diff --git a/custom_components/icloud3/support/v2v3_config_migration.py b/custom_components/icloud3/support/v2v3_config_migration.py index 0f54c61..23e122b 100644 --- a/custom_components/icloud3/support/v2v3_config_migration.py +++ b/custom_components/icloud3/support/v2v3_config_migration.py @@ -21,7 +21,7 @@ CONFIG_IC3, CONF_VERSION_INSTALL_DATE, CONF_CONFIG_IC3_FILE_NAME, CONF_VERSION, CONF_EVLOG_CARD_DIRECTORY, CONF_EVLOG_CARD_PROGRAM, - CONF_USERNAME, CONF_PASSWORD, CONF_DEVICES, + CONF_USERNAME, CONF_PASSWORD, CONF_DEVICES, CONF_APPLE_ACCOUNT, CONF_TRACK_FROM_ZONES, CONF_TRACKING_MODE, CONF_PICTURE, CONF_DEVICE_TYPE, CONF_INZONE_INTERVALS, CONF_UNIT_OF_MEASUREMENT, CONF_TIME_FORMAT, CONF_MAX_INTERVAL, CONF_OFFLINE_INTERVAL, @@ -35,7 +35,7 @@ CONF_STAT_ZONE_BASE_LATITUDE, CONF_STAT_ZONE_BASE_LONGITUDE, CONF_DISPLAY_TEXT_AS, CONF_IC3_DEVICENAME, CONF_FNAME, CONF_FAMSHR_DEVICENAME, - CONF_MOBILE_APP_DEVICE, CONF_PICTURE, CONF_FMF_EMAIL, + CONF_MOBILE_APP_DEVICE, CONF_PICTURE, CONF_TRACK_FROM_ZONES, CONF_DEVICE_TYPE, CONF_INZONE_INTERVAL, CONF_NAME, NAME, BADGE, BATTERY, BATTERY_STATUS, INFO, @@ -107,11 +107,11 @@ CONF_SENSORS_OTHER_LIST = ['gps_accuracy', 'vertical_accuracy', 'altitude'] from ..helpers.common import (instr, ) -from ..helpers.messaging import (_traceha, log_info_msg, log_warning_msg, log_exception,) +from ..helpers.file_io import (file_exists, ) +from ..helpers.messaging import (_log, log_info_msg, log_warning_msg, log_exception,) from ..helpers.time_util import (time_str_to_secs, datetime_now, datetime_for_filename, ) from . import config_file -import os import json import yaml from homeassistant.util import slugify @@ -261,16 +261,16 @@ def _get_config_ic3_records(self): if instr(config_ic3_filename, "/"): pass - elif os.path.exists(f"{Gb.ha_config_directory}{config_ic3_filename}"): + elif file_exists(f"{Gb.ha_config_directory}{config_ic3_filename}"): config_ic3_filename = (f"{Gb.ha_config_directory}{config_ic3_filename}") - elif os.path.exists(f"{Gb.icloud3_directory}/{config_ic3_filename}"): + elif file_exists(f"{Gb.icloud3_directory}/{config_ic3_filename}"): config_ic3_filename = (f"{Gb.icloud3_directory}/{config_ic3_filename}") config_ic3_filename = config_ic3_filename.replace("//", "/") self.write_migration_log_msg(f"Converting parameters, Source: {config_ic3_filename}") - if os.path.exists(config_ic3_filename) is False: + if file_exists(config_ic3_filename) is False: self.write_migration_log_msg(f" -- Skipped, {config_ic3_filename} not used") return {} @@ -312,6 +312,7 @@ def _get_devices_list_from_config_devices_parm(self, conf_devices_parameter, sou fname, device_type = self._extract_name_device_type(pvalue) conf_device[CONF_IC3_DEVICENAME] = devicename conf_device[CONF_FNAME] = fname + conf_device[CONF_APPLE_ACCOUNT] = self.conf_parm_tracking[CONF_USERNAME] conf_device[CONF_FAMSHR_DEVICENAME] = devicename conf_device[CONF_DEVICE_TYPE] = device_type @@ -324,8 +325,8 @@ def _get_devices_list_from_config_devices_parm(self, conf_devices_parameter, sou tfz_list = pvalue.split(',') conf_device[CONF_TRACK_FROM_ZONES] = tfz_list - elif pname == CONF_EMAIL: - conf_device[CONF_FMF_EMAIL] = pvalue + # elif pname == CONF_EMAIL: + # conf_device[CONF_FMF_EMAIL] = pvalue elif pname == CONF_NAME: conf_device[CONF_FNAME] = pvalue @@ -345,11 +346,11 @@ def _get_devices_list_from_config_devices_parm(self, conf_devices_parameter, sou conf_device[CONF_MOBILE_APP_DEVICE] = 'None' elif pname == CONF_TRACKING_METHOD: - if pvalue == 'fmf': + # if pvalue == 'fmf': + # conf_device[CONF_FAMSHR_DEVICENAME] = 'None' + if pvalue == 'iosapp': conf_device[CONF_FAMSHR_DEVICENAME] = 'None' - elif pvalue == 'iosapp': - conf_device[CONF_FAMSHR_DEVICENAME] = 'None' - conf_device[CONF_FMF_EMAIL] = 'None' + # conf_device[CONF_FMF_EMAIL] = 'None' elif pname == CONF_PICTURE: @@ -612,7 +613,7 @@ def remove_ic3_devices_from_known_devices_yaml_file(self): known_devices_file = Gb.hass.config.path('known_devices.yaml') - if os.path.isfile(known_devices_file) is False: + if file_exists(known_devices_file) is False: return ic3_devicenames = [] diff --git a/custom_components/icloud3/support/waze.py b/custom_components/icloud3/support/waze.py index c499b26..ef3fa87 100644 --- a/custom_components/icloud3/support/waze.py +++ b/custom_components/icloud3/support/waze.py @@ -13,8 +13,8 @@ from ..support.waze_route_calc_ic3 import WazeRouteCalculator, WRCError from ..helpers.common import (instr, format_gps, ) -from ..helpers.messaging import (post_event, post_internal_error, log_info_msg, _trace, _traceha, ) -from ..helpers.time_util import (time_now_secs, datetime_now, secs_since, format_timer, ) +from ..helpers.messaging import (post_event, post_internal_error, log_info_msg, _evlog, _log, ) +from ..helpers.time_util import (time_now_secs, secs_to_time, format_timer, ) from ..helpers.dist_util import (km_to_um, ) import traceback @@ -49,6 +49,8 @@ def __init__(self, distance_method_waze_flag, waze_min_distance, waze_max_distan self.waze_manual_pause_flag = False #If Paused via iCloud command self.waze_close_to_zone_pause_flag = False #pause if dist from zone < 1 flag self.WazeRouteCalc = None + self.error_server_unavailable_secs = 0 # Time (secs) of first Server unavailable error + self.error_server_unavailable_cnt = 0 # Count of things error occurred try: if self.WazeRouteCalc is None: @@ -138,6 +140,14 @@ def get_route_time_distance(self, Device, FromZone, check_hist_db=True): elif Device.loc_data_zone == FromZone.from_zone: return (WAZE_NOT_USED, 0, 0, 0) + if self.error_server_unavailable_secs > 0: + if time_now_secs() < self.error_server_unavailable_secs: + return (WAZE_NO_DATA, 0, 0, 0) + + # Reset error and retry Waze after 10/30/60-mins + self.error_server_unavailable_secs = 0 + post_event("Waze Route Service > Resuming") + try: from_zone = FromZone.from_zone waze_status = WAZE_USED @@ -165,13 +175,16 @@ def get_route_time_distance(self, Device, FromZone, check_hist_db=True): FromZone.FromZone.longitude, ZONE) - if waze_status == WAZE_NO_DATA: - post_event(Device, - f"Waze Route Error > Problem connecting to Waze Servers. " - f"Distance will be calculated, Travel Time not available") + self._determine_next_waze_retry() + if waze_status == WAZE_NO_DATA: + self._determine_next_waze_retry() return (WAZE_NO_DATA, 0, 0, 0) + elif self.error_server_unavailable_cnt > 0: + self.error_server_unavailable_cnt = 0 + self.error_server_unavailable_secs = 0 + # Add a time/distance record to the waze history database try: if (self.is_historydb_USED @@ -350,6 +363,24 @@ def get_waze_distance(self, Device, FromZone, from_lat, from_long, return (WAZE_NO_DATA, 0, 0) +#-------------------------------------------------------------------- + def _determine_next_waze_retry(self): + self.error_server_unavailable_cnt += 1 + retry_interval = {10: 600, 20: 1800, 30: 3600}.get(self.error_server_unavailable_cnt, 0) + if retry_interval > 0: + self.error_server_unavailable_secs = time_now_secs() + retry_interval + post_event( f"Waze Route Service Error #{self.error_server_unavailable_cnt} > " + f"An error occurred connecting to Waze Servers, " + f"distance will be calculated, Travel Time not available. " + f"Waze will be paused for {retry_interval/60}-mins and will " + f"retry at {secs_to_time(self.error_server_unavailable_secs)}") + + if self.error_server_unavailable_cnt > 40: + self.error_server_unavailable_cnt = 0 + self.error_server_unavailable_secs = 0 + self.waze_status = WAZE_PAUSED + post_event( f"Waze Route Service > " + f"Waze has been paused after excessive errors") #-------------------------------------------------------------------- def _set_waze_not_available_error(self, err): diff --git a/custom_components/icloud3/support/waze_history.py b/custom_components/icloud3/support/waze_history.py index 9b8dab2..5fbfa47 100644 --- a/custom_components/icloud3/support/waze_history.py +++ b/custom_components/icloud3/support/waze_history.py @@ -7,10 +7,10 @@ post_event, post_internal_error, post_monitor_msg, post_startup_alert, post_evlog_greenbar_msg, refresh_event_log, log_info_msg, log_error_msg, log_exception, - _trace, _traceha, ) + _evlog, _log, ) from ..helpers.time_util import (datetime_now, format_timer, ) from ..helpers.dist_util import (mi_to_km, gps_distance_km, format_dist_km,) -from ..support.waze_route_calc_ic3 import WRCError +from ..support.waze_route_calc_ic3 import WazeRouteCalculator, WRCError import homeassistant.util.dt as dt_util @@ -18,6 +18,7 @@ import time import sqlite3 from sqlite3 import Error +import threading #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> @@ -159,28 +160,36 @@ class WazeRouteHistory(object): def __init__(self, wazehist_used, max_distance, track_direction): # Flags to control db maintenance - self.wazehist_recalculate_time_dist_abort_flag = False - self.wazehist_recalculate_time_dist_running_flag = False - - self.use_wazehist_flag = wazehist_used - self.max_distance = mi_to_km(max_distance) - self.track_direction_north_south_flag = track_direction in ['north-south', 'north_south'] - self.track_latitude = self.track_longitude = 0 - self.track_recd_cnt = 0 - self.sensor_map_recds_cnt = self.sensor_map_recd_cnt = 0 - self.total_recd_cnt = self.total_recds_cnt = self.total_update_cnt = self.total_deleted_cnt = 0 - self.is_refreshing_map_sensor = False - self.last_lat_long_key = '' # Last lat:long key read - self.last_location_recds = [] # List of the last recds retrieved for the lat:long + self.WazeRouteCalc = None + self.wazehist_database = Gb.wazehist_database_filename + self.use_wazehist_flag = wazehist_used + self.max_distance = mi_to_km(max_distance) + self.track_recd_cnt = 0 + self.total_recd_cnt = 0 + self.total_recds_cnt = 0 + self.total_update_cnt = 0 + self.total_deleted_cnt = 0 + self.sensor_map_recds_cnt = 0 + self.sensor_map_recd_cnt = 0 + self.track_latitude = 0 + self.track_longitude = 0 + self.last_lat_long_key = '' # Last lat:long key read + self.last_location_recds = [] # List of the last recds retrieved for the lat:long + + self.track_direction_north_south_flag = track_direction in ['north-south', 'north_south'] + self.is_refreshing_map_sensor = False + self.wazehist_recalculate_time_dist_abort_flag = False + self.wazehist_recalculate_time_dist_running_flag = False self.connection = None self.cursor = None - wazehist_database = Gb.wazehist_database_filename - post_event(Gb.devicename, f"Waze History Database > {format_filename(wazehist_database)}") + self.lock = None + + post_event(f"Waze History Database > {CRLF_DOT}{format_filename(self.wazehist_database)}") if self.use_wazehist_flag and self.connection is None: - self.open_waze_history_database(wazehist_database) + self.open_waze_history_database() #-------------------------------------------------------------------- @property @@ -188,26 +197,25 @@ def is_historydb_USED(self): return self.use_wazehist_flag #-------------------------------------------------------------------- - def open_waze_history_database(self, wazehist_database): + def open_waze_history_database(self): """ Create a database connection to the SQLite database specified by db_file - wazehist_database: The filename of the waze history sqlite3 database - Note: This sets the sql connection and cursor fields if the database opened successfully """ try: - self.connection = sqlite3.connect(wazehist_database, check_same_thread=False) + self.lock = threading.Lock() + self.lock.acquire(True) + self.connection = sqlite3.connect(self.wazehist_database, check_same_thread=False) self.cursor = self.connection.cursor() + self.lock.release() self._sql(CREATE_ZONES_TABLE) self._sql(CREATE_LOCATIONS_TABLE) - # self.compress_wazehist_database() - except: post_internal_error(traceback.format_exc) self.connection = None @@ -223,8 +231,10 @@ def close_waze_history_database(self): if self.connection is None: return try: + self.lock.acquire(True) self.connection.commit() self.connection.close() + self.lock.release() except: pass @@ -233,17 +243,48 @@ def close_waze_history_database(self): self.cursor = None #-------------------------------------------------------------------- - def _sql(self, sql): + def _sql(self, sql, data=None, fetchone=False, fetchall=False): + records = None try: - self.cursor.execute(sql) - self.connection.commit() - except: - post_internal_error(traceback.format_exc) + self.lock.acquire(True) + if data: + self.cursor.execute(sql, data) + self.connection.commit() + else: + self.cursor.execute(sql) + + if fetchone: + records = self.cursor.fetchone() + elif fetchall: + records = self.cursor.fetchall() + + self.lock.release() + + return records + + except Exception as err: + log_exception(err) #-------------------------------------------------------------------- - def _sql_data(self, sql, data): - self.cursor.execute(sql, data) - self.connection.commit() + def _execute(self, cursor, sql, fetchone=False, fetchall=False): + try: + records = None + self.lock.acquire(True) + cursor.execute(sql) + + if fetchone: + records = cursor.fetchone() + elif fetchall: + records = cursor.fetchall() + + self.lock.release() + + return records + + except Exception as err: + log_exception(err) + + return None #-------------------------------------------------------------------- def _add_record(self, sql, data): @@ -254,8 +295,11 @@ def _add_record(self, sql, data): :return rowid - id of the added row ''' try: - self.cursor.execute(sql, data) - self.connection.commit() + self._sql(sql, data=data) + # self.lock.acquire(True) + # self.cursor.execute(sql, data) + # self.connection.commit() + # self.lock.release() return self.cursor.lastrowid @@ -271,11 +315,15 @@ def _update_record(self, sql, data): data - list containing the data to be added that matches the sql stmt ''' try: - self.cursor.execute(sql, data) - self.connection.commit() + self._sql(sql, data=data) + # self.lock.acquire(True) + # self.cursor.execute(sql, data) + # self.connection.commit() + # self.lock.release() return True - except: + except Exception as err: + log_exception(err) error_msg = f"Error updating Waze History Database, {sql}, {data}" log_error_msg(error_msg) # post_internal_error(traceback.format_exc) @@ -291,8 +339,7 @@ def _delete_record(self, table, criteria=''): if criteria: sql += (f" WHERE {criteria}") - self.cursor.execute(sql) - self.connection.commit() + self._sql(sql) #-------------------------------------------------------------------- def _get_record(self, table, criteria=''): @@ -308,9 +355,7 @@ def _get_record(self, table, criteria=''): if criteria: sql += (f" WHERE {criteria}") - - self.cursor.execute(sql) - record = self.cursor.fetchone() + record = self._sql(sql, fetchone=True) try: if table == 'locations': @@ -351,8 +396,7 @@ def _get_all_records(self, table, criteria='', orderby=''): if orderby: sql += (f"ORDER BY {orderby} ") - self.cursor.execute(sql) - records = self.cursor.fetchall() + records = self._sql(sql, fetchall=True) post_monitor_msg( Gb.devicename, f"WazeHistDB > Get All Records, " @@ -449,16 +493,23 @@ def update_usage_cnt(self, location_id): sql = (f"SELECT * FROM locations WHERE loc_id={location_id}") - self.cursor.execute(sql) - record = self.cursor.fetchone() + + record = self._sql(sql, fetchone=True) + # self.lock.acquire(True) + # self.cursor.execute(sql) + # record = self.cursor.fetchone() + # self.lock.release() cnt = record[LOC_USAGE_CNT] + 1 usage_data = [datetime_now(), cnt, location_id] self._update_record(UPDATE_LOCATION_USED, usage_data) - self.cursor.execute(sql) - record = self.cursor.fetchone() + record = self._sql(sql, fetchone=True) + # self.lock.acquire(True) + # self.cursor.execute(sql) + # record = self.cursor.fetchone() + # self.lock.release() if self.wazehist_recalculate_time_dist_running_flag is False: post_monitor_msg( Gb.devicename, @@ -478,27 +529,32 @@ def compress_wazehist_database(self): if self.connection is None: return - cursor = self.connection.cursor() - + # cursor = self.connection.cursor() + vac_conn = sqlite3.connect(self.wazehist_database, + check_same_thread=False, + isolation_level=None) + vac_cursor = vac_conn.cursor() try: - cursor.execute(DUPLICATE_LOCATION_RECDS_SELECT) - records = cursor.fetchall() + # records = self._execute(cursor, DUPLICATE_LOCATION_RECDS_SELECT, fetchall=True) + records = self._execute(vac_cursor, DUPLICATE_LOCATION_RECDS_SELECT, fetchall=True) if records != []: post_event(f"Waze History Database > Deleted Duplicate Recds, Count-{len(records)}") - cursor.execute(DUPLICATE_LOCATION_RECDS_DELETE) - self.connection.commit() - cursor.execute("VACUUM;") - self.connection.commit() + self._execute(vac_cursor, DUPLICATE_LOCATION_RECDS_DELETE) + + self._execute(vac_cursor, "VACUUM;") + vac_conn.commit() except Exception as err: log_exception(err) - cursor.execute(GET_LOCATIONS_TABLE_RECD_COUNT) - recd_cnt = cursor.fetchone()[0] - post_event(f"Waze History Database > Compressed, Record Count-{recd_cnt}") - cursor.close() + recd_cnts = self._execute(vac_cursor, GET_LOCATIONS_TABLE_RECD_COUNT, fetchone=True) + post_event(f"Waze History Database > Compressed, Record Count-{recd_cnts[0]}") + + vac_cursor.close() + vac_conn.close() + #-------------------------------------------------------------------- def __repr__(self): @@ -595,7 +651,16 @@ def load_track_from_zone_table(self): def wazehist_recalculate_time_dist_all_zones(self): if self.connection is None: return - self.wazehist_recalculate_time_dist(all_zones_flag=True) + try: + # Set another WazeRouteCalc object so recalculating all of the history will not effect + # calling Waze when updating a tracked device's info + if self.WazeRouteCalc is None: + self.WazeRouteCalc = WazeRouteCalculator(Gb.Waze.waze_region, Gb.Waze.waze_realtime) + + self.wazehist_recalculate_time_dist(all_zones_flag=True) + except: + post_event("Waze Hist > Error encountered recalculating time/distance values") + def wazehist_recalculate_time_dist(self, all_zones_flag=False): ''' @@ -649,7 +714,7 @@ def wazehist_recalculate_time_dist(self, all_zones_flag=False): self.total_recds_cnt += recds_cnt self.total_update_cnt += update_cnt self.total_deleted_cnt += deleted_cnt - restart_cnt = 0 if recd_cnt == recds_cnt else recd_cnt + restart_cnt = 0 if recd_cnt == recds_cnt else recd_cnt zone_data = [Zone.latitude5, Zone.longitude5, 0, datetime_now(), restart_cnt, zone_id] @@ -743,13 +808,16 @@ def _cycle_through_wazehist_records(self, zone_id, zone_dname, zone_from_loc): f"ElapsedTime-{format_timer(running_time)}") post_event(log_msg) - if (recd_cnt % 10) == 0: + if (recd_cnt % 25) == 0: + # timer = format_timer(running_time).replace(' secs', 's') + # timer = timer.replace(' mins', 'm').replace(' min', 'm') + # timer = timer.replace(' hrs', 'h').replace(' hr', 'h') alert_message = (f"Waze Hist > " - f"{zone_dname}, " - # f"Recd-{recd_cnt}/{self.total_recd_cnt}, " - f"Checked-{(recd_cnt/self.total_recd_cnt*100):.0f}% " + f"{zone_dname[:6]}, " + f"Recd-{recd_cnt}/{self.total_recd_cnt} " + f"({(recd_cnt/self.total_recd_cnt*100):.0f}%), " f"Updated-{update_cnt} " - f"({format_timer(running_time)})" #.replace('mins', 'm').replace(' hrs', 'h')})" + # f"({timer})" f"{CRLF}Select Action > WazeHist Recalculate... again to cancel") post_evlog_greenbar_msg(alert_message) refresh_event_log() @@ -799,7 +867,7 @@ def _update_wazehist_record(self, recd_cnt, zone_from_loc, wazehist_to_loc, to_lat , to_long = wazehist_to_loc.split(',') new_time, new_dist = \ - Gb.Waze.WazeRouteCalc.calc_route_info (from_lat, from_long, + self.WazeRouteCalc.calc_route_info (from_lat, from_long, to_lat, to_long, log_results_flag=False) break @@ -811,8 +879,7 @@ def _update_wazehist_record(self, recd_cnt, zone_from_loc, wazehist_to_loc, # post_internal_error('Update WazeHist Recd', traceback.format_exc) #** 5/15/2022 Make sure values are above 0 - if (new_time < 0 - or new_dist < 0): + if (new_time < 0 or new_dist < 0): return False, False, 0, 0 update_time_flag = (abs(new_time - current_time) > .25) diff --git a/custom_components/icloud3/support/waze_route_calc_ic3.py b/custom_components/icloud3/support/waze_route_calc_ic3.py index 4566bb1..e3379e5 100644 --- a/custom_components/icloud3/support/waze_route_calc_ic3.py +++ b/custom_components/icloud3/support/waze_route_calc_ic3.py @@ -22,7 +22,7 @@ import requests import re -from ..helpers.messaging import (_traceha, log_exception, log_warning_msg, log_error_msg, log_info_msg, ) +from ..helpers.messaging import (_log, log_exception, log_warning_msg, log_error_msg, log_info_msg, ) #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> class WRCError(Exception): diff --git a/custom_components/icloud3/support/zone_handler.py b/custom_components/icloud3/support/zone_handler.py index c109833..de0ed45 100644 --- a/custom_components/icloud3/support/zone_handler.py +++ b/custom_components/icloud3/support/zone_handler.py @@ -6,13 +6,15 @@ # - select the zone and assigning it to a device # - display all zone information in the Event Log # - utilities for determining if a device can use a zone -# - requesting famshr updates for devices not using the mobile app +# - requesting icloud updates for devices not using the mobile app # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> import os import homeassistant.util.dt as dt_util -from homeassistant.helpers import event +from datetime import datetime, timedelta, timezone +import time +# from homeassistant.helpers import event from homeassistant.core import callback @@ -24,12 +26,13 @@ from ..support import stationary_zone as statzone from ..support import determine_interval as det_interval from ..helpers import entity_io +from ..helpers.file_io import (file_size, ) from ..helpers.common import (instr, is_zone, is_statzone, isnot_statzone, isnot_zone, zone_dname, - list_to_str, list_add, list_del,) + list_to_str, list_add, list_del, ) from ..helpers.messaging import (post_event, post_error_msg, post_monitor_msg, log_info_msg, log_exception, - _trace, _traceha, ) -from ..helpers.time_util import (time_now_secs, secs_to_time, secs_to, secs_since, time_now, + _evlog, _log, ) +from ..helpers.time_util import (time_now_secs, secs_to_time, secs_to, secs_since, mins_since, time_now, datetime_now, secs_to_datetime, ) from ..helpers.dist_util import (gps_distance_km, format_dist_km, format_dist_m, km_to_um, m_to_um, ) @@ -94,8 +97,8 @@ def update_current_zone(Device, display_zone_msg=True): # Get distance between zone selected and current zone to see if they overlap. # If so, keep the current zone - if (zone_selected != NOT_HOME - and is_overlapping_zone(Device.loc_data_zone, zone_selected)): + if (is_zone(zone_selected) + and is_same_or_overlapping_zone(Device.loc_data_zone, zone_selected)): zone_selected = Device.loc_data_zone ZoneSelected = Gb.Zones_by_zone[Device.loc_data_zone] @@ -104,7 +107,7 @@ def update_current_zone(Device, display_zone_msg=True): # See if any device without the mobapp was in this zone. If so, request a # location update since it was running on the inzone timer instead of # exit triggers from the Mobile App - if (Gb.mobapp_monitor_any_devices_false_flag + if (Gb.device_not_monitoring_mobapp and zone_selected == NOT_HOME and Device.loc_data_zone != NOT_HOME): request_update_devices_no_mobapp_same_zone_on_exit(Device) @@ -172,7 +175,7 @@ def select_zone(Device, latitude=None, longitude=None): zones_data = [[Zone.distance_m(latitude, longitude), Zone, Zone.zone, Zone.radius_m, Zone.dname] - for Zone in Gb.HAZones + for Zone in set(Gb.HAZones) if (Zone.passive is False)] # Do not select a new zone for the Device if it just left a zone. Set to Away and next_update will be soon @@ -224,14 +227,14 @@ def post_zone_selected_msg(Device, ZoneSelected, zone_selected, # Format distance msg zones_dist_msg = '' - zones_displayed = [zone_selected] + # zones_displayed = [zone_selected] for zone_distance_list in zones_distance_list: zdl_items = zone_distance_list.split('|') _zone = zdl_items[1] - _zone_dist = float(zdl_items[2]) + _zone_dist_m = float(zdl_items[2]) zones_dist_msg += ( f"{zone_dname(_zone)}" - f"-{m_to_um(_zone_dist)}") + f"-{m_to_um(_zone_dist_m)}") zones_dist_msg += ", " gps_accuracy_msg = '' @@ -300,7 +303,7 @@ def closest_zone(latitude, longitude): return None, 'unknown', 'Unknown', 0 #-------------------------------------------------------------------- -def is_overlapping_zone(zone1, zone2): +def is_same_or_overlapping_zone(zone1, zone2): ''' zone1 and zone2 overlap if their distance between centers is less than 2m ''' @@ -308,15 +311,19 @@ def is_overlapping_zone(zone1, zone2): if zone1 == zone2: return True - if zone1 == "": zone1 = HOME + if (isnot_zone(zone1) or zone1 == 'not_set' or zone2 == 'not_set' + or zone1 == "" or zone2 == ""): + return False + Zone1 = Gb.Zones_by_zone[zone1] Zone2 = Gb.Zones_by_zone[zone2] - zone_dist = Zone1.distance(Zone2.latitude, Zone2.longitude) + zone_dist_m = Zone1.distance_m(Zone2.latitude, Zone2.longitude) - return (zone_dist <= 2) + return (zone_dist_m <= 2) - except: + except Exception as err: + log_exception(err) return False #-------------------------------------------------------------------- @@ -396,40 +403,55 @@ def log_zone_enter_exit_activity(Device): if Device.log_zone == '': Device.log_zone = Device.loc_data_zone Device.log_zone_enter_secs = Gb.this_update_secs - post_event(Device, f"Log Zone Activity > Logging Started-{zone_dname(Device.log_zone)}") + post_event(Device, f"Log Zone Activity > Logging Started, {zone_dname(Device.log_zone)}") return # Must be in the zone for at least 4-minutes - inzone_secs = secs_since(Device.log_zone_enter_secs) - inzone_hrs = inzone_secs/3600 - if inzone_secs < 240: return + if mins_since(Device.log_zone_enter_secs) < 4: + return + + try: + Gb.hass.async_add_executor_job(write_log_zone_recd, Device) + except: + write_log_zone_recd(Device) + + if Device.loc_data_zone in Device.log_zones: + Device.log_zone = Device.loc_data_zone + Device.log_zone_enter_secs = Gb.this_update_secs + else: + Device.log_zone = '' + Device.log_zone_enter_secs = 0 + +#------------------------------------------------------------------------------ +def write_log_zone_recd(Device): + ''' + Write the record to the .csv file. Add a header record if the file is new + ''' filename = (f"zone-log-{dt_util.now().strftime('%Y')}-" f"{Device.log_zones_filename}.csv") with open(filename, 'a', encoding='utf8') as f: - if os.path.getsize(filename) == 0: - recd = "Date,Zone Enter Time,Zone Exit Time,Time (Mins),Time (Hrs),Distance (Home),Zone,Device\n" - f.write(recd) + if file_size(filename) == 0: + header = "Date,Zone Enter Time,Zone Exit Time,Time (Mins),Time (Hrs),Distance (Home),Zone,Device\n" + else: + header = '' - recd = (f"{datetime_now()[:10]}," + recd = (f"{header}" + f"{datetime_now()[:10]}," f"{secs_to_datetime(Device.log_zone_enter_secs)}," f"{secs_to_datetime(Gb.this_update_secs)}," - f"{inzone_secs/60:.0f}," - f"{inzone_hrs:.2f}," + f"{mins_since(Device.log_zone_enter_secs):.0f}," + f"{mins_since(Device.log_zone_enter_secs)/60:.2f}," f"{Device.sensors[HOME_DISTANCE]:.2f}," f"{Device.log_zone}," f"{Device.devicename}" "\n") f.write(recd) - post_event(Device, f"Log Zone Activity > Logging Ended-{zone_dname(Device.log_zone)}") - if Device.loc_data_zone in Device.log_zones: - Device.log_zone = Device.loc_data_zone - Device.log_zone_enter_secs = Gb.this_update_secs - else: - Device.log_zone = '' - Device.log_zone_enter_secs = 0 + post_event(Device, f"Log Zone Activity > Logging Ended, " + f"{zone_dname(Device.log_zone)}, " + f"Time-{mins_since(Device.log_zone_enter_secs)/60}h") #------------------------------------------------------------------------------ def request_update_devices_no_mobapp_same_zone_on_exit(Device): diff --git a/custom_components/icloud3/translations/en.json b/custom_components/icloud3/translations/en.json index 3c5564b..a0b639b 100644 --- a/custom_components/icloud3/translations/en.json +++ b/custom_components/icloud3/translations/en.json @@ -5,20 +5,26 @@ "config_update_complete": "iCloud3 configuration updated successfully", "already_configured": "iCloud3 is already installed and can not be installed again. Select CONFIGURE in the iCloud3 Integration entry to configure iCloud3.\n\nIf you are deleting and then reinstalling, restart HA first and then reinstall iCloud3", "disabled": "iCloud3 is DISABLED and can not be installed again. Enable iCloud3, then select CONFIGURE in the iCloud3 Integration entry to configure iCloud3.\n\nIf you are deleting and then reinstalling, restart HA first and then reinstall iCloud3", - "login_error": "An error occurred logging into the iCloud account. Verify the username and password.", + "login_error": "An error occurred logging into the Apple Account. Verify the username and password.", "reauth_successful": "The reauthentication has been successfully completed", "verification_code_accepted": "The Apple ID Verification Code was accepted. Reauthentication is complete", "verification_code_cancelled": "The Apple ID Verification was cancelled", "update_cancelled": "Update Cancelled", - "icloud3_init_error": "A problem was encountered initializing the iCloud3 Configure Settings screens. iCloud3 has probably encountered an error during initialization and was not started. Check the 'home-assistant.log' file for any errors related to iCloud3" + "icloud3_init_error": "A problem was encountered initializing the iCloud3 Configure Settings screens. iCloud3 has probably encountered an error during initialization and was not started. Check the 'home-assistant.log' file for any errors related to iCloud3", + "reauth_apple_acct_unknown": "Apple account requesting verification code is unknown", + "reauth_apple_acct_unused": "Apple account requesting verification code is not used" }, "error": { "invalid_path": "The path provided is not valid. Should be in the format `user/repo-name`", "verification_code_send_error": "Failed to send the Apple ID Verification Code", "verification_code_requested2": "The Apple ID Verification Code was requested", + "verification_code_accepted": "The Apple ID Verification Code was accepted. Reauthentication is complete", "verification_code_invalid": "The Verification Code is not correct. Reenter or request a new code", + "verification_code_needed": "The Apple Account Verification Code is needed", + "verification_code_cancelled": "The Apple ID Verification was cancelled", + "icloud_no_devices": "No devices were found in the iCloud `Family Sharing` list", - "icloud_other_error": "An unknown error was encountered authenticating the iCloud account. Try again later" + "icloud_other_error": "An unknown error was encountered authenticating the Apple Account. Try again later" }, "step": { "user": { @@ -33,8 +39,9 @@ "title": "Apple ID Verification Code (HA Notifications)", "description": "Enter the 6-digit verification code you just received from Apple", "data": { - "verification_code": "ICLOUD ACCOUNT VERIFICATION CODE", - "action_items": "═════════════════════════════════════════════════════" + "apple_account": "APPLE ACCOUNT TO BE AUTHENTICATED", + "verification_code": "APPLE ACCOUNT VERIFICATION CODE", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" } }, "restart_ha": { @@ -52,33 +59,38 @@ "already_configured": "iCloud3 is already installed and can not be installed again. Select CONFIGURE in the iCloud3 Integration entry to configure iCloud3", "disabled": "iCloud3 is DISABLED and can not be installed again. Enable iCloud, then select CONFIGURE in the iCloud3 Integration entry to configure iCloud3", "ha_restarting": "Home Assistant is Restarting\n\nTHE ICLOUD3 SCREEN MUST BE REFRESHED AFTER RESTARTING", - "ic3_reloading": "Reloading iCloud3\n\nTHE ICLOUD3 SCREEN MUST BE REFRESHED AFTER RESTARTING", + "ic3_restarting": "Restarting iCloud3", "reauth_successful": "The reauthentication has been successfully completed" }, "error": { "update_aborted": "Update aborted, an error was detected in one of the data fields", - "conf_updated": "iCloud3 Configuration Parameters were updated successfully", + "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": "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 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", + "icloud_acct_logging_into": "Logging into Apple Account", + "icloud_acct_logged_into": "✅ Logged into the Apple Account", + "icloud_acct_already_logged_into": "Already Logged into the Apple Account", + "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_503": "🍎 Apple is delaying displaying a new Verification code to prevent Suspicious Activity, probably due to too many requests. It should be displayed in about 20-30 minutes. Restart HA if it is not displayed", + "icloud_acct_login_error_srp_401": "❌ Python SRP Library Credentials Error. The Python module that creates the Secure Remote Password hash key has calculated an incorrect value for a valid Username/Password. Try changing the Password to see if the Apple Acct can be logged into. ", + "icloud_acct_username_password_error": "❌ Entry Error, Invalid Username or Password", + "icloud_acct_dup_username_error": "Error: Username is being used by another Data Source entry", + "icloud_acct_username_inuse_error": "Error: This Username is being used by another Data Source entry. This one cannot be changed to it until the other one is removed. Select the other one and STOP USING it first.", + "icloud_acct_not_available": "❌ Login Failed, Apple Account is not Available", + "icloud_acct_not_logged_into": "Not logged into the Apple Account", + "icloud_acct_updated_not_logged_into": "Apple Acct info was saved, Login Error, Will Complete Login Later", + "icloud_acct_data_source_warning": "Apple Acct is not selected as a data source, username/password are setup", + "icloud_acct_not_set_up": "Apple Account Username or Password needs to be entered", + "icloud_acct_no_data_source": "❌ No Data Source has been selected (Apple iCloud Account or Mobile App)", + "ic3_icloud_same_name": "iCloud3 dev_trkr.entity_id and name on the device (Settings > General > About) can not be exactly the same (letters & case)", "mobile_app_error": "Error, The Mobile App Integration is not installed. The Mobile App will not be used as a data source; location data and zone enter/exit triggers will not be monitored", - "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", - "verification_code_needed": "The Apple ID Verification Code is needed", - "verification_code_accepted": "The Apple ID Verification Code was accepted", - "verification_code_invalid": "The Verification Code was not correct. Reenter or request a new code", - "verification_code_send_error": "Failed to send the Apple ID Verification Code", + "verification_code_requested": "The Apple Account Verification Code was requested, BROWSER REFRESH MAY BE NEEDED", + "verification_code_requested2": "The Apple Account Verification Code was requested", + "verification_code_needed": "The Apple Account Verification Code is needed", + "verification_code_accepted": "✅ The Apple Account Verification Code was accepted", + "verification_code_invalid": "❌ The Verification Code was not correct. Reenter or request a new code", + "verification_code_send_error": "❌ Failed to send the Apple ID Verification Code", "inactive_device": "Device is INACTIVE. Change to `Track` to locate and track this device", "inactive_all_devices": "✪✪ ALL DEVICES ARE INACTIVE. NOTHING WILL BE TRACKED ✪✪", @@ -91,32 +103,25 @@ "away_time_zone_dup_devices_2": "One of these devices is also selected in the Otner Device List", "review_filledin_fields": "Review the 'Filled in' fields", - "not_numeric": "The value entered is not numeric", + "not_numeric": "❌ The value entered is not numeric", "waze_server_error_us": "The correct server for your location is: United States, Canada", "waze_server_error_il": "The correct server for your location is: Israel", "waze_server_error_row": "The correct server for your location is: Rest of the World", - "required_field": "This parameter must be specified", + "required_field": "❌ This parameter must be specified", "required_field_device": "A device providing location data must be selected from the Family Share, Find-my-Friends, or Mobile App devices lists", - "no_device_selected": "A device providing location data must be selected", + "no_device_selected": "❌ A device providing location data must be selected", "no_add_entities_device_tracker_fct": "The HA component for adding devices is not available. HA MUST BE RESTARTED", - "unknown_devicename": "The device that was previously selected can no longer be identified. It`s name may have been changed or it may have been deleted. Reselect the device to be tracked or monitored", - "unknown_value": "The value of this parameter in the iCloud3 configuration file is unknown or invalid. It must be selected again", - "unknown_famshr": "The FamShr device was not returned from iCloud when iCloud3 started. Check FindMy devices list & Family Share list. See Event Log Startup Stage 4 for more info and a list of the devices returned from the iCloud account", - "unknown_fmf": "The FmF device was not returned from iCloud when iCloud3 started. Check FindMy app devices Sharing location info. See Event Log Startup Stage 4 for more info and a list of the devices returned from the iCloud account", - "unknown_mobapp": "The mobile_app device_tracker entity was not found during HA Device Registry scan. Check Mobile App device list in HA Settings > Devices & Services > Devices. See Event Log Startup Stage 4 for more info and a list of mobile_app devices found in the HA Device Registry", - "unknown_picture": "The Picture filename was not found in the HA config/www directory. Reselect the filename or check to see if it has been deleted", + "unknown_value": "One of the selection parameters needs to be reviewed", + "unknown_devicename": "The configured device was not found in any of the Apple Accts or the Mobile App device list", + "unknown_icloud": "The configured device was not found in any of the Apple Accounts", + "unknown_mobapp": "The configured device was not found in the Mobile App devices list", + "unknown_picture": "The configured picture file was not found in `config/www/...` directories", - "unknown_famshr_fmf": "Check the FamShr and FmF parameter values (Not found or Invalid)", - "unknown_famshr_fmf_mobapp": "Check the FamShr, FmF and Mobile App parameter values (Not found or Invalid)", - "unknown_famshr_fmf_mobapp_picture": "Check the FamShr, FmF, Mobile App and Picture parameter values (Not found or Invalid)", - "unknown_famshr_mobapp": "Check the FamShr and Mobile App parameter values (Not found or Invalid)", - "unknown_famshr_mobapp_picture": "Check the FamShr, Mobile App and Picture parameter values (Not found or Invalid)", - "unknown_famshr_picture": "Check the FamShr and Picture parameter values (Not found or Invalid)", - "unknown_fmf_mobapp": "Check the FmF and Mobile App parameter values (Not found or Invalid)", - "unknown_fmf_mobapp_picture": "Check the FmF, Mobile App and Picture parameter values (Not found or Invalid)", - "unknown_fmf_picture": "Check the FmFand Picture parameter values (Not found or Invalid)", - "unknown_mobapp_picture": "Check the Mobile App and Picture parameter values (Not found or Invalid)", + "unknown_apple_acct": "Apple Account is not selected for the assigned iCloud device", + "unknown_icloud_mobapp": "Check the iCloud and Mobile App parameter values (Not found or Invalid)", + "unknown_icloud_mobapp_picture": "Check the iCloud, Mobile App and Picture parameter values (Not found or Invalid)", + "unknown_icloud_picture": "Check the iCloud and Picture parameter values (Not found or Invalid)", "tfz_selection_invalid": "The value must be a zone that is being tracked from", "time_factor_invalid_range": "The 'Travel Time Multiplier' must be between .1 and .9", @@ -128,7 +133,7 @@ "not_found_file": "The file was not found", "duplicate_ic3_devicename": "This name is already used by another iCloud3 device", "already_assigned": "This selection is already assigned to another device", - "mobapp_search_error": "WARNING: Search Failure - No Mobile App device that starts with the iCloud3 or FamShr devicename was found. Select 'None' or the device to be used.", + "mobapp_search_error": "WARNING: Search Failure - No Mobile App device that starts with the iCloud3 or Apple Acct devicename was found. Select 'None' or the device to be used.", "duplicate_other_devicename": "This name is already used in another integration or platform", "action_completed": "Requested Action has been completed", "action_cancelled": "Requested Action has been cancelled", @@ -140,91 +145,122 @@ "menu": { "title": "iCloud3 Configure Settings", "data": { - "menu_items": "", - "action_items": "═════════════════════════════════════════════════════" + "menu_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" } }, "menu_0": { "title": "Configure Devices and Sensors Menu", "data": { - "menu_items": "", - "action_items": "═════════════════════════════════════════════════════" + "xmenu_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯", + "menu_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" } }, "menu_1": { "title": "Configure Parameters Menu", "data": { - "menu_items": "", - "action_items": "═════════════════════════════════════════════════════" + "menu_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" } }, "restart_icloud3": { "title": "Confirm Restarting iCloud3", "description": "Note: Changes to tracked devices require restarting iCloud3", "data": { - "action_items": "═════════════════════════════════════════════════════" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" } }, "restart_ha_ic3": { "title": "Restart Home Assistant or iCloud3", "description": "Restart Home Assistant or reload and reinitialize the iCloud3 Integration\n\nTHE ICLOUD3 DASHBOARD SCREEN MUST BE REFRESHED AFTER RESTARTING", "data": { - "action_items": "═════════════════════════════════════════════════════" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" } }, "restart_ha_ic3_load_error": { "title": "iCloud3 Load Error", "description": "iCloud3 did not load and initialize when HA started. Reload iCloud3 again or restart HA", "data": { - "action_items": "═════════════════════════════════════════════════════" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" } }, "confirm_action": { "title": "Confirm Selected Action", "data": { - "action_items": "═════════════════════════════════════════════════════" + "confirm_action_form_hdr": "REQUESTED ACTION", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, - "icloud_account": { - "title": "iCloud Account & Mobile App Data Sources", - "description": "The data sources provide location and other information iCloud3 uses to track the iDevice.\n\nThey are:\n• ICLOUD ACCOUNT - Apple iCloud Web Services provides location and other\n  information for the devices in the Family Sharing list.\n• MOBILE APP - The HA Companion app installed on the iPhone and iPad\n  provides zone enter/exit triggers and location information. The Mobile App\n  Integration needs to be installed to use this data service.\n\nThe devices in the Family Sharing List and Mobile App are assigned to every device iCloud3 trackes on the Update Devices screen.", + "data_source": { + "title": "Data Source - Apple Account, Mobile App", + "description": "The data sources provide location and other information iCloud3 uses to track the iDevice.", "data": { - "data_source_icloud": "═════════════════════════════════════════════════════ ICLOUD ACCOUNT - Location data is provided by the Apple iCloud account", - "username": "APPLE ID (USERNAME) - The email address used to sign in to the iCloud Account", - "password": "PASSWORD - The iCloud Account Password", + "data_source": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ DATA SOURCES", + "data_source_icloud": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ DATA SOURCES", + "data_source_mobapp": "", + "apple_accts": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ APPLE ICLOUD ACCOUNTS", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" + }, + "data_description": { + } + }, + "update_apple_acct": { + "title": "Update Apple Account Username/Password", + "data": { + "account_selected": "ACCOUNT SELECTED", + "username": "USERNAME - Email address/username used to sign in to the Apple Account", + "password": "PASSWORD - Account Password", + "totp_key": "TOTP KEY - Time Based One-Time Password Key used to generate the 6-digit authentication code", + "locate_all": "ALWAYS LOCATE ALL DEVICES - Locate all the devices in the Apple Account, including those in the Family list. Unchecked will only locate the Family list devices when they are being updated (default)", "url_suffix_china": "CHINA USERS - Use Apple iCloud Web Servers located in China (.cn URL suffix)", - "data_source_mobapp": "═════════════════════════════════════════════════════ MOBILE APP - Location data is provided by the HA Mobile App", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { + "locate_all": "Devices fall into two categories, your devices and everything else. Locating all devices will get their location with fewer internet calls that might take slightly longer while Apple locates them, especially if you have a lot of devices. Not doing this results in more internet calls that are faster." + } + }, + "delete_apple_acct": { + "title": "Remove an Apple Account", + "data": { + "account_selected": "ACCOUNT SELECTED", + "device_action": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ HOW SHOULD DEVICES ASSIGNED TO THIS APPLE ACCT BE HANDLED", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ CONFIRM DELETING THE APPLE ACCOUNT" } }, "reauth": { "title": "Apple ID Verification Code", "description": "Enter the 6-digit verification code you just received from Apple", "data": { - "verification_code": "ICLOUD ACCOUNT VERIFICATION CODE", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "account_selected": "APPLE ACCOUNT TO BE AUTHENTICATED", + "verification_code": "APPLE ACCOUNT VERIFICATION CODE", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" + } + }, + "trusted_device": { + "title": "Apple Account Trusted Devices", + "description": "Select your Trusted Device", + "data": { + "trusted_device": "Trusted Device" } }, "device_list": { "title": "iCloud3 Devices", "description": "Up to 10 devices can be tracked or monitored by iCloud3. They are listed here.\n\nThis screen is used to select a device that needs to be updated, add a new device and delete a device that should not be tracked any longer.", "data": { - "xdevices": "═════════════════════════════════════════════════════ ICLOUD3 DEVICES", - "devices": "═════════════════════════════════════════════════════", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "devices": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, "add_device": { "title": "Add iCloud3 Device", "data": { - "ic3_devicename": "ICLOUD3 DEVICE_TRACKER ENTITY ID - The HA device_tracker entity assigned to this device", - "fname": "FRIENDLY NAME - Displayed in HA device_tracker and sensor names and on the Event Log", + "ic3_devicename": "ICLOUD3 ENTITY ID - The HA device_tracker entity forto this device", + "fname": "FRIENDLY NAME - Displayed in HA entities and on the Event Log", "device_type": "DEVICE TYPE - iPhone, iPad, Watch, etc.", - "tracking_mode": "TRACKING MODE - How location requests should be done (Full tracking, Monitor, Inactive)", - "mobapp": "MOBILE APP INSTALLED - The HA Mobile App is installed on this device ", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "tracking_mode": "TRACKING MODE - Location request method (Tracked, Monitored, Inactive)", + "mobapp": "MOBILE APP INSTALLED - HA Mobile App is installed on this device", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { } @@ -235,18 +271,18 @@ "data": { "ic3_devicename": "ICLOUD3 DEVICE_TRACKER ENTITY ID - The HA device_tracker entity assigned to this device", "fname": "FRIENDLY NAME - Displayed in HA device_tracker and sensor names and on the Event Log", - "device_type": "DEVICE TYPE - iPhone, iPad, Watch, etc.", + "device_type": "DEVICE TYPE - iPhone, iPad, Watch, etc.E", "tracking_mode": "TRACKING MODE - How location requests should be done (Full tracking, Monitor, Inactive)", - "famshr_devicename": "FAMILY SHARING LIST DEVICE - Use location data from this iCloud Acct Family Sharing member", - "fmf_email": "FIND-MY-FRIENDS DEVICE - Use location data from this FindMy device sharing their info with you", - "mobile_app_device": "MOBILE APP DEVICE_TRACKER ENTITY - Use this HA device location data & zone enter/exit triggers", - "log_zones": "LOG ACTIVITY FOR ZONES - Enter/exit zone info (date, time, distance) is saved to a spreadsheet .csv file", - "track_from_zones": "TRACK-FROM-ZONES - Track travel time & distance from Home and other zones", - "track_from_base_zone": "PRIMARY TRACK-FROM-HOME ZONE OVERRIDE - Use this zone instead of the Home for tracking results", - "picture": "PICTURE - Photo image of the person normally using this device (44x44 pixels is a good size)", + "famshr_devicename": "APPLE ACCOUNT iCLOUD DEVICE - Apple iCloud device providing location data", + "mobile_app_device": "MOBILE APP DEVICE_TRACKER ENTITY - Mobile App device providing location data & zone triggers", + "picture": "PICTURE - Image of the person normally using this device (44x44 pixels is a good size)", "inzone_interval": "INZONE INTERVAL", + "rarely_updated_parms": "RARELY UPDATED PARAMETERS - Select and Submit to update these items", + "log_zones": "ZONE LOG ACTIVITY - Enter/exit zone info (date, time, distance) is saved to a spreadsheet .csv file", + "track_from_zones": "TRACK-FROM-ZONES - Track travel time & distance from Home and other zones", + "track_from_base_zone": "TRACK-FROM-HOME ZONE OVERRIDE - Use this zone instead of the Home for tracking results", "fixed_interval": "FIXED INTERVAL", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { "inzone_interval": "Time between location requests when in a zone", @@ -257,23 +293,24 @@ "delete_device": { "title": "Delete iCloud3 Device", "data": { - "action_items": "═════════════════════════════════════════════════════ DELETE OPTIONS" + "device_selected": "SELECTED DEVICE", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ DELETE OPTIONS" } }, "review_inactive_devices": { "title": "Review Untracked (Inactive) Devices", "description": "The 'Tracking Mode' of devices are set to 'Inactive' and will not be located or tracked.", "data": { - "inactive_devices": "═════════════════════════════════════════════════════ INACTIVE ICLOUD3 DEVICES", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "inactive_devices": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ INACTIVE ICLOUD3 DEVICES", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, "change_device_order": { "title": "Event Log Device Display Sequence", "description": "The devices are displayed in the Event Log heading area and in various Event Log messages in the sequence below.\n\nThis screen lets you change the order of the devices.", "data": { - "device_desc": "═════════════════════════════════════════════════════ ICLOUD3 DEVICES", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "device_desc": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ICLOUD3 DEVICES", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, "away_time_zone": { @@ -284,7 +321,7 @@ "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" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { "time_zone_1_offset": "PRIMARY DEVICES & LOCATION TIME - Devices and the current location time when away and in another time zone", @@ -295,17 +332,18 @@ "title": "Format Settings", "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", + "log_level": "LOG LEVEL - The type of messages that are written to the iCloud3 Log file (icloud3-0.log}", + "log_level_devices": "RAWDATA LOG DEVICE FILTER - Write iCloud RawData to the log file for only these devices", "display_zone_format": "EVENT LOG ZONE DISPLAY NAME - How the Zone name is displayed in sensors and the Event Log", "device_tracker_state_source": "DEVICE TRACKER STATE VALUE - How the device's device_tracker entity state value is determined", "time_format": "TIME FORMAT - How time fields are displayed in sensors and in the Event Log", - "unit_of_measurement": "UNIT OF MEASUREMENT - How distance fieldsare displayed in sensors and in the Event Log", + "unit_of_measurement": "UNIT OF MEASUREMENT - How distance fields are displayed in sensors and in the Event Log", "display_gps_lat_long2": "DISPLAY GPS COORDINATES - Display the GPS (Latitude, Longitude/±Accuracy) or only the GPS (/±Accuracy) in the Event Log", "display_gps_lat_long": "DISPLAY GPS COORDINATES - Display GPS-(22.32771, -76.33073/±35m) instead of GPS-/±35m in the Event Log", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { + "log_level": "iCloud3 can log configuration parameters, startup activity and errors, tracking activity, error messages and the Apple Account requests for iCloud Device location RawData information (request and response). Log levels specify the type of records that should be written to the iCloud3 log file (`icloud3-0.log`) from basic (Info) to more detailed (Debug) to extremely detailed (RawData).", "device_tracker_state_source": "HA uses the device's gps coordinates to determine the zone. The gps accuracy is not considered so the zone may be exited when the gps wanders out of the zone. iCloud3 does consider the gps accuracy and will not exit the zone when this occurs. iCloud3 will display the Zone's Friendly Name or zone name displayed on the Event Log." } }, @@ -313,8 +351,8 @@ "title": "Event Log 'Display Text As'", "description": "There may be some text fields, such as email addresses or phone numbers, that are displayed on the Event Log screen that are private and should not be displayed. For example, you can replace 'geekstergary@apple.com' with 'gary@email.com'.\n\nThis screenis used to specify the original text and the text that should be displayed.", "data": { - "display_text_as": "═════════════════════════════════════════════════════ TEXT REPLACEMENT FIELDS - [Actual text > Displayed text]", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "display_text_as": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ TEXT REPLACEMENT FIELDS - [Actual text > Displayed text]", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, "display_text_as_update": { @@ -322,7 +360,7 @@ "data": { "text_from": "ORIGINAL TEXT - Text to be replaced (example: gary_real_email@gmail.com)", "text_to": "DISPLAYED TEXT- Text to be displayed (display: gary@email.com)", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { } @@ -330,10 +368,10 @@ "actions": { "title": "iCloud3 Action Commands", "data": { - "ic3_actions": "═════════════════════════════════════════════════════ ICLOUD3 GENERAL CONTROL ACTIONS", - "debug_actions": "═════════════════════════════════════════════════════ DEBUG LOG ACTIONS", - "other_actions": "═════════════════════════════════════════════════════ OTHER ACTIONS", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "ic3_actions": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ICLOUD3 GENERAL CONTROL ACTIONS", + "debug_actions": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ DEBUG LOG ACTIONS", + "other_actions": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ OTHER ACTIONS", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, "inzone_intervals": { @@ -347,7 +385,7 @@ "no_mobapp": "MOBILE APP IS NOT INSTALLED", "other": "OTHER DEVICE TYPE", "distance_between_devices": "Determine the distance between devices. Use a near by device's tracking results", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { "no_mobapp": "Default interval if the Mobile App is not used for location monitoring and zone enter/exit triggers", @@ -357,15 +395,15 @@ "waze_main": { "title": "Waze - Route Service Travel Time/Distance", "data": { - "waze_used": "═════════════════════════════════════════════════════ WAZE ROUTE SERVICE", + "waze_used": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ WAZE ROUTE SERVICE", "waze_region": "ROUTE SERVER LOCATION - Location of the Waze Route Server for your area", "waze_realtime": "USE REAL TIME DATA - Waze should consider traffic delays when determining travel time", "waze_min_distance": "WAZE MINIMUM DISTANCE", "waze_max_distance": "WAZE MAXIMUM DISTANCE", - "waze_history_database_used": "═════════════════════════════════════════════════════ WAZE HISTORY DATABASE", + "waze_history_database_used": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ WAZE HISTORY DATABASE", "waze_history_track_direction": "GENERAL TRAVEL DIRECTION - Used to display 'Map Trace Lines' between saved locations", "waze_history_max_distance": "HISTORY MAX DISTANCE", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { "waze_min_distance": "Use the Waze Route Service when the zone distance is greater than this value", @@ -377,16 +415,16 @@ "title": "Special Zones", "description": "This screen is used to configure:\n  • Stationary Zones - Created when the device is in the same location for a short\n    period of time\n  • Enter Zone Delay Time - Delay processing a Zone Enter Trigger\n  • A temporary “home” zone at another location", "data": { - "stat_zone_header": "═════════════════════════════════════════════════════ STATIONARY ZONE", + "stat_zone_header": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ STATIONARY ZONE", "stat_zone_fname": "FRIENDLY NAME BASE - Name to display when in a Stationary Zone (StatZone)", "stat_zone_still_time": "NO MOVEMENT TIME", "stat_zone_inzone_interval": "INZONE INTERVAL", - "passthru_zone_header": "═════════════════════════════════════════════════════ ENTER ZONE DELAY", + "passthru_zone_header": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ENTER ZONE DELAY", "passthru_zone_time": "ENTER ZONE DELAY TIME", - "track_from_base_zone_used": "═════════════════════════════════════════════════════ PRIMARY TRACK-FROM-HOME ZONE OVERRIDE", + "track_from_base_zone_used": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ PRIMARY TRACK-FROM-HOME ZONE OVERRIDE", "track_from_base_zone": "TRACK FROM ZONE - Use this zone instead of Home for tracking results for all devices. Global setting", "track_from_home_zone": "TRACK FROM HOME ZONE - Keep tracking from the Home zone when the Primary Track From Zone is not Home", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { "passthru_zone_time": "Delay processing an Enter Zone Trigger that you may be driving through and not actually entering", @@ -399,17 +437,17 @@ "title": "Sensors", "description": "Many sensors are used to display tracking results and other information for a device.\n\n This screen is used to select the sensors that should be created.", "data": { - "monitored_devices": "═════════════════════════════════════════════════════ MONITORED DEVICE SENSORS - Select the type of sensors to create for a Monitored Device", - "device": "═════════════════════════════════════════════════════ DEVICE SENSORS - Device status and information", - "tracking_update": "═════════════════════════════════════════════════════ LOCATION UPDATE SENSORS - Device location update times", - "tracking_time": "═════════════════════════════════════════════════════ TIME SENSORS - Device tracking timers", - "tracking_distance": "═════════════════════════════════════════════════════ DISTANCE SENSORS - Device tracking distances", - "track_from_zones": "═════════════════════════════════════════════════════ TRACK FROM MULTIPLE ZONE SENSORS - Used when tracking from more than one zone (not needed for tracking only from the Home zone)", - "tracking_other": "═════════════════════════════════════════════════════ OTHER TRACKING SENSORS - Not normally used but available", - "zone": "═════════════════════════════════════════════════════ ZONE SENSORS - Device zone status and information", - "other": "═════════════════════════════════════════════════════ OTHER SENSORS - Sensors not in the above areas", - "excluded_sensors": "═════════════════════════════════════════════════════ EXCLUDED SENSORS - Sensors that will not be created when iCloud3 starts", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "monitored_devices": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ MONITORED DEVICE SENSORS - Select the type of sensors to create for a Monitored Device", + "device": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ DEVICE SENSORS - Device status and information", + "tracking_update": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ LOCATION UPDATE SENSORS - Device location update times", + "tracking_time": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ TIME SENSORS - Device tracking timers", + "tracking_distance": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ DISTANCE SENSORS - Device tracking distances", + "track_from_zones": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ TRACK FROM MULTIPLE ZONE SENSORS - Used when tracking from more than one zone (not needed for tracking only from the Home zone)", + "tracking_other": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ OTHER TRACKING SENSORS - Not normally used but available", + "zone": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ZONE SENSORS - Device zone status and information", + "other": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ OTHER SENSORS - Sensors not in the above areas", + "excluded_sensors": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ EXCLUDED SENSORS - Sensors that will not be created when iCloud3 starts", + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, "exclude_sensors": { @@ -419,7 +457,7 @@ "excluded_sensors": "EXCLUDED SENSORS - Sensors that will not be created when iCloud3 starts", "filter": "FILTER DISPLAYED SENSORS - Select the Sensors that should be displayed", "filtered_sensors": "ICLOUD3 SENSORS - A list of Sensors that are created when iCloud3 starts", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" } }, "tracking_parameters": { @@ -441,7 +479,7 @@ "picture_www_dirs": "WWW DIRECTORIES WITH PICTURE IMAGES - Filter for `Update Devices > Pictures` file locations", "event_log_card_directory": "EVENT LOG CARD LOVELACE RESOURCES DIRECTORY - Event Log custom card .js file directory", "event_log_btnconfig_url": "EVENT LOG CONFIGURE BUTTON (GEAR) URL > Special URL that display's the HA Configure Settings screen", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" + "action_items": "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ACTION COMMANDS" }, "data_description": { "old_location_threshold": "Locations older than this value will be discarded", diff --git a/custom_components/icloud3/translations/zh-Hans.json b/custom_components/icloud3/translations/zh-Hans.json deleted file mode 100644 index 8fd6e8a..0000000 --- a/custom_components/icloud3/translations/zh-Hans.json +++ /dev/null @@ -1,508 +0,0 @@ -{ - "title": "iCloud3 v3:iDevice 跟踪器", - "config": { - "abort": { - "config_update_complete": "iCloud3 配置更新成功", - "already_configured": "iCloud3 已经安装,无法再次安装。请在 iCloud3 集成条目中选择 CONFIGURE 来配置 iCloud3。\n\n如果您正在删除然后重新安装,请先重启 HA 然后再重新安装 iCloud3", - "disabled": "iCloud3 已禁用,不能再次安装。启用 iCloud3,然后在 iCloud3 集成条目中选择 CONFIGURE 来配置 iCloud3。\n\n如果您正在删除然后重新安装,请先重启 HA 然后再重新安装 iCloud3", - "login_error": "登录 iCloud 账户时发生错误。请验证用户名和密码。", - "reauth_successful": "重新认证成功完成", - "verification_code_accepted": "苹果 ID 验证码已接受。重新认证完成", - "verification_code_cancelled": "苹果 ID 验证已取消", - "update_cancelled": "更新已取消", - "icloud3_init_error": "初始化 iCloud3 配置设置屏幕时遇到问题。iCloud3 可能在初始化期间遇到错误并未启动。检查 'home-assistant.log' 文件以获取与 iCloud3 相关的任何错误" - }, - "error": { - "invalid_path": "提供的路径无效。应为 'user/repo-name' 格式", - "verification_code_send_error": "发送苹果 ID 验证码失败", - "verification_code_requested2": "已请求苹果 ID 验证码", - "verification_code_invalid": "验证码不正确。请重新输入或请求新的验证码", - "icloud_no_devices": "在 iCloud '家庭共享' 列表中未找到设备", - "icloud_other_error": "认证 iCloud 账户时遇到未知错误。请稍后再试" - }, - "step": { - "user": { - "data": { - "continue": "选择继续 iCloud3 安装(及参数迁移)", - "continue_restart_ha": "选择继续 iCloud3 安装。然后重启家庭助理。" - }, - "description": "恭喜!iCloud3 v3 集成已添加到家庭助理。下一步是配置您想要跟踪的设备。如果您正在运行 iCloud3 v2,您的配置将从 v2 迁移到 v3。\n\n选择下面的提交按钮,然后选择 CONFIGURE 来配置 iCloud3。\n\niCloud3 用户指南将帮助您开始:\n• ICLOUD3 文档 - 在此屏幕右上角选择问号 (?) 打开 'iCloud 用户指南'\n• 从 v2 迁移 - 按照 '安装 iCloud3 > 从 v2 到 v3 的迁移' 章节中的步骤\n• 新安装 - 按照 '安装 iCloud3 > 作为新安装' 章节中的步骤", - "title": "iCloud3 v3 集成安装程序" - }, - "reauth": { - "title": "苹果 ID 验证码(家庭助理通知)", - "description": "输入您刚从苹果收到的六位数验证码", - "data": { - "verification_code": "ICLOUD 账户验证代码", - "action_items": "═════════════════════════════════════════════════════" - } - }, - "restart_ha": { - "title": "确认重新启动家庭助理", - "description": "iCloud3 需要重新启动家庭助理以防止 device_tracker 实体名称冲突", - "data": { - "action_items": "" - } - } - } - }, - "options": { - "abort": { - "config_update_complete": "iCloud3 配置更新成功", - "already_configured": "iCloud3 已经安装,不能再次安装。请在 iCloud3 集成条目中选择 CONFIGURE 来配置 iCloud3", - "disabled": "iCloud3 已禁用,不能再次安装。启用 iCloud,然后在 iCloud3 集成条目中选择 CONFIGURE 来配置 iCloud3", - "ha_restarting": "家庭助理正在重新启动\n\n重新启动后必须刷新 iCloud3 屏幕", - "ic3_reloading": "正在重新加载 iCloud3\n\n重新启动后必须刷新 iCloud3 屏幕", - "reauth_successful": "重新认证成功完成" - }, - "error": { - "update_aborted": "更新中止,检测到一个数据字段中的错误", - "conf_updated": "iCloud3 配置参数已成功更新", - "conf_reloaded": "iCloud3 配置文件已重新加载", - "icloud_acct_logging_into": "正在登录 iCloud 账户", - "icloud_acct_logged_into": "已登录新的 iCloud 账户。选择 SAVE 以保存更改并重启 iCloud3", - "icloud_acct_already_logged_into": "已登录 iCloud 账户", - "icloud_acct_login_error_user_pw": "登录错误,用户名或密码无效", - "icloud_acct_login_error_other": "登录错误,其他错误或 iCloud 不可用", - "icloud_acct_login_error_connection": "登录错误,无法连接到 iCloud 服务器", - "icloud_acct_username_password_error": "输入错误,用户名或密码无效", - "icloud_acct_not_available": "登录失败,iCloud 账户不可用", - "icloud_acct_not_logged_into": "警告:iCloud 账户未登录", - "icloud_acct_data_source_warning": "警告:未选择 iCloud 账户作为数据来源,但已设置用户名/密码", - "icloud_acct_not_set_up": "需要输入 iCloud 账户用户名或密码", - "icloud_acct_no_data_source": "未选择数据来源(iCloud 或移动应用)", - "mobile_app_error": "错误,移动应用集成未安装。移动应用将不会被用作数据来源;位置数据和区域进出触发器将不会被监控", - "verification_code_requested": "已请求苹果 ID 验证码,可能需要刷新浏览器", - "verification_code_requested2": "已请求苹果 ID 验证码", - "verification_code_needed": "需要苹果 ID 验证码", - "verification_code_accepted": "苹果 ID 验证码已接受", - "verification_code_invalid": "验证码不正确。请重新输入或请求新的验证码", - "verification_code_send_error": "发送苹果 ID 验证码失败", - - "inactive_device": "设备处于非活动状态。更改为 'Track' 以定位和跟踪此设备", - "inactive_all_devices": "✪✪ 所有设备均处于非活动状态。将不进行跟踪 ✪✪", - "inactive_most_devices": "大多数设备处于非活动状态,将不会被定位或跟踪", - "inactive_some_devices": "一些设备处于非活动状态,将不会被定位或跟踪", - "inactive_few_devices": "少数设备处于非活动状态,将不会被定位或跟踪", - "inactive_no_devices": "未设置设备。选择 'Add Device' 并提交", - - "away_time_zone_dup_devices_1": "这些设备中的一个也在其他设备列表中选择", - "away_time_zone_dup_devices_2": "这些设备中的一个也在其他设备列表中选择", - - "review_filledin_fields": "复查已填写的字段", - "not_numeric": "输入的值不是数字", - "waze_server_error_us": "您所在地的正确服务器是:美国,加拿大", - "waze_server_error_il": "您所在地的正确服务器是:以色列", - "waze_server_error_row": "您所在地的正确服务器是:世界其他地区", - "required_field": "必须指定此参数", - "required_field_device": "必须从家庭共享、查找我的朋友或移动应用设备列表中选择提供位置数据的设备", - "no_device_selected": "必须选择提供位置数据的设备", - "no_add_entities_device_tracker_fct": "添加设备的 HA 组件不可用。必须重启 HA", - "unknown_devicename": "先前选择的设备无法识别。它的名称可能已更改或已被删除。重新选择要跟踪或监控的设备", - "unknown_value": "iCloud3 配置文件中此参数的值未知或无效。必须重新选择", - "unknown_famshr": "当 iCloud3 启动时,未从 iCloud 返回 FamShr 设备。检查 FindMy 设备列表和家庭共享列表。查看启动阶段 4 的事件日志以获取更多信息以及从 iCloud 账户返回的设备列表", - "unknown_fmf": "当 iCloud3 启动时,未从 iCloud 返回 FmF 设备。检查 FindMy 应用设备共享位置信息。查看启动阶段 4 的事件日志以获取更多信息以及从 iCloud 账户返回的设备列表", - "unknown_mobapp": "在 HA 设备注册扫描期间未找到 mobile_app device_tracker 实体。检查 HA 设置 > 设备和服务 > 设备中的移动应用设备列表。查看启动阶段 4 的事件日志以获取更多信息以及在 HA 设备注册中找到的 mobile_app 设备列表", - "unknown_picture": "未在 HA config/www 目录中找到图片文件名。重新选择文件名或检查是否已被删除", - - "unknown_famshr_fmf": "检查 FamShr 和 FmF 参数值(未找到或无效)", - "unknown_famshr_fmf_mobapp": "检查 FamShr、FmF 和移动应用参数值(未找到或无效)", - "unknown_famshr_fmf_mobapp_picture": "检查 FamShr、FmF、移动应用和图片参数值(未找到或无效)", - "unknown_famshr_mobapp": "检查 FamShr 和移动应用参数值(未找到或无效)", - "unknown_famshr_mobapp_picture": "检查 FamShr、移动应用和图片参数值(未找到或无效)", - "unknown_famshr_picture": "检查 FamShr 和图片参数值(未找到或无效)", - "unknown_fmf_mobapp": "检查 FmF 和移动应用参数值(未找到或无效)", - "unknown_fmf_mobapp_picture": "检查 FmF、移动应用和图片参数值(未找到或无效)", - "unknown_fmf_picture": "检查 FmF 和图片参数值(未找到或无效)", - "unknown_mobapp_picture": "检查移动应用和图片参数值(未找到或无效)", - - "tfz_selection_invalid": "该值必须是被跟踪的区域", - "time_factor_invalid_range": "'旅行时间乘数' 必须在 .1 到 .9 之间", - "fixed_interval_invalid_range": "'固定间隔' 必须为 0 (未使用) 或 >= 3 (推荐 > 5)", - "display_text_as_no_gtsign": "实际文本和显示文本之间缺少 '>'", - "display_text_as_no_actual": "未指定 '实际文本'", - "display_text_as_no_display_as": "未指定 '显示文本'", - "not_found_directory": "未找到目录", - "not_found_file": "未找到文件", - "duplicate_ic3_devicename": "此名称已被另一台 iCloud3 设备使用", - "already_assigned": "此选择已分配给另一台设备", - "mobapp_search_error": "警告:搜索失败 - 未找到以 iCloud3 或 FamShr 设备名开头的移动应用设备。选择 '无' 或要使用的设备。", - "duplicate_other_devicename": "此名称已在另一集成或平台中使用", - "action_completed": "请求的操作已完成", - "action_cancelled": "请求的操作已取消", - "restart_ha": "需要重启家庭助理以继续", - "excluded_sensors_ha_restart": "已更新排除的传感器。需要重启 HA" - }, - "step": { - "menu": { - "title": "iCloud3 配置设置", - "data": { - "menu_items": "", - "action_items": "═════════════════════════════════════════════════════" - } - }, - "menu_0": { - "title": "配置设备和传感器菜单", - "data": { - "menu_items": "", - "action_items": "═════════════════════════════════════════════════════" - } - }, - "menu_1": { - "title": "配置参数菜单", - "data": { - "menu_items": "", - "action_items": "═════════════════════════════════════════════════════" - } - }, - "restart_icloud3": { - "title": "确认重新启动 iCloud3", - "description": "注意:跟踪设备的变更需要重新启动 iCloud3", - "data": { - "action_items": "═════════════════════════════════════════════════════" - } - }, - "restart_ha_ic3": { - "title": "重新启动家庭助理或 iCloud3", - "description": "重新启动家庭助理或重新加载并重新初始化 iCloud3 集成\n\n重新启动后必须刷新 iCloud3 仪表板屏幕", - "data": { - "action_items": "═════════════════════════════════════════════════════" - } - }, - "restart_ha_ic3_load_error": { - "title": "iCloud3 加载错误", - "description": "当 HA 启动时,iCloud3 未加载和初始化。再次重新加载 iCloud3 或重新启动 HA", - "data": { - "action_items": "═════════════════════════════════════════════════════" - } - }, - "confirm_action": { - "title": "确认所选操作", - "data": { - "action_items": "═════════════════════════════════════════════════════" - } - }, - "icloud_account": { - "title": "iCloud 账户和移动应用数据源", - "description": "数据源提供 iCloud3 用于跟踪 iDevice 的位置和其他信息。\n\n它们包括:\n• ICLOUD 账户 - 苹果 iCloud Web 服务为家庭共享列表中的设备提供位置和其他信息。\n• 移动应用 - 安装在 iPhone 和 iPad 上的 HA 伴侣应用提供区域进入/退出触发器和位置信息。使用此数据服务需要安装移动应用集成。\n\n家庭共享列表和移动应用中的设备被分配给 iCloud3 在更新设备屏幕上跟踪的每个设备。", - "data": { - "data_source_icloud": "═════════════════════════════════════════════════════ ICLOUD 账户 - 位置数据由苹果 iCloud 账户提供", - "username": "APPLE ID (用户名) - 用于登录 iCloud 账户的电子邮件地址", - "password": "密码 - iCloud 账户密码", - "url_suffix_china": "中国用户 - 使用位于中国的苹果 iCloud Web 服务器 (.cn URL 后缀)", - "data_source_mobapp": "═════════════════════════════════════════════════════ 移动应用 - 位置数据由 HA 移动应用提供", - "action_items": "═════════════════════════════════════════════════════ 操作命令" - }, - "data_description": {} - }, - "reauth": { - "title": "苹果 ID 验证码", - "description": "输入您刚从苹果收到的六位数验证码", - "data": { - "verification_code": "ICLOUD 账户验证代码", - "action_items": "═════════════════════════════════════════════════════ 操作命令" - } - }, - "device_list": { - "title": "iCloud3 设备", - "description": "iCloud3 可跟踪或监控最多 10 个设备。它们在此列出。\n\n此屏幕用于选择需要更新的设备,添加新设备并删除不再跟踪的设备。", - "data": { - "xdevices": "═════════════════════════════════════════════════════ ICLOUD3 设备", - "devices": "═════════════════════════════════════════════════════", - "action_items": "═════════════════════════════════════════════════════ 操作命令" - } - }, - "add_device": { - "title": "添加 iCloud3 设备", - "data": { - "ic3_devicename": "ICLOUD3 DEVICE_TRACKER ENTITY ID - 分配给此设备的 HA device_tracker 实体", - "fname": "友好名称 - 显示在 HA device_tracker 和传感器名称以及事件日志中", - "device_type": "设备类型 - iPhone,iPad,Watch 等", - "tracking_mode": "跟踪模式 - 应如何执行位置请求(完全跟踪,监控,非活动)", - "mobapp": "已安装移动应用 - 此设备上安装了 HA 移动应用", - "action_items": "═════════════════════════════════════════════════════ 操作命令" - }, - "data_description": {} - }, - "update_device": { - "title": "更新 iCloud3 设备", - "description": "此屏幕允许您配置可以使用 iCloud3 跟踪或监控的每个设备", - "data": { - "ic3_devicename": "ICLOUD3 DEVICE_TRACKER ENTITY ID - 分配给此设备的 HA device_tracker 实体", - "fname": "友好名称 - 显示在 HA device_tracker 和传感器名称以及事件日志中", - "device_type": "设备类型 - iPhone,iPad,Watch 等", - "tracking_mode": "跟踪模式 - 应如何执行位置请求(完全跟踪,监控,非活动)", - "famshr_devicename": "家庭共享列表设备 - 使用此 iCloud 账户家庭共享成员的位置数据", - "fmf_email": "查找我的朋友设备 - 使用与您共享其信息的 FindMy 设备的位置数据", - "mobile_app_device": "移动应用 DEVICE_TRACKER ENTITY - 使用此 HA 设备的位置数据和区域进入/退出触发器", - "log_zones": "记录区域活动 - 保存进入/退出区域信息(日期,时间,距离)到电子表格 .csv 文件", - "track_from_zones": "从区域跟踪 - 从家和其他区域跟踪旅行时间和距离", - "track_from_base_zone": "主要从家区域跟踪覆盖 - 使用此区域而不是家作为跟踪结果", - "picture": "图片 - 通常使用此设备的人的照片图像(44x44 像素是一个好的尺寸)", - "inzone_interval": "区域内间隔", - "fixed_interval": "固定间隔", - "action_items": "═════════════════════════════════════════════════════ 操作命令" - }, - "data_description": { - "inzone_interval": "区域内位置请求之间的时间", - "fixed_interval": "非区域内位置请求之间的固定时间。iCloud3 计算下一次定位请求的间隔并使用计算值(如果未设置 (= 0))。当计算间隔小于 5 分钟、当前位置数据过时、设备离线或设备不在区域内时,将不使用此值。", - "track_from_base_zone": "通常,家区域用作所有跟踪的主要跟踪区域(旅行时间,距离等)。然而,如果您长时间离家或设备通常在另一个位置(度假屋,第二家,父母家等),可以使用不同的区域作为主要跟踪区域。这可以在特殊区域屏幕上为所有设备全局设置。" - } - }, - "delete_device": { - "title": "删除 iCloud3 设备", - "data": { - "action_items": "═════════════════════════════════════════════════════ 删除选项" - } - }, - "review_inactive_devices": { - "title": "查看未跟踪(非活动)设备", - "description": "设备的 '跟踪模式' 设置为 '非活动',将不会被定位或跟踪。", - "data": { - "inactive_devices": "═════════════════════════════════════════════════════ INACTIVE ICLOUD3 设备", - "action_items": "═════════════════════════════════════════════════════ 操作命令" - } - }, - "change_device_order": { - "title": "事件日志设备显示顺序", - "description": "设备在事件日志标题区域和各种事件日志消息中按以下顺序显示。\n\n此屏幕允许您更改设备的顺序。", - "data": { - "device_desc": "═════════════════════════════════════════════════════ ICLOUD3 设备", - "action_items": "═════════════════════════════════════════════════════ 操作命令" - } - }, - "away_time_zone": { - "title": "外出时显示位置时区", - "description": "事件日志和传感器显示的时间显示事件发生时使用的家庭助理计算机的 '家庭时区' 时间。当您远离家并处于另一个时区时,您的跟踪事件仍然基于您的家庭 '时区' 时间,而不是您当前位置的时间。\n\n此屏幕允许您使用您当前位置的时区显示时间事件。", - "data": { - "away_time_zone_1_devices": "外出时区 #1 的设备", - "away_time_zone_1_offset": "当前位置 #1 的时间和时区调整", - "away_time_zone_2_devices": "外出时区 #2 的设备", - "away_time_zone_2_offset": "当前位置 #2 的时间和时区调整", - "action_items": "═════════════════════════════════════════════════════ 操作命令" - }, - "data_description": { - "time_zone_1_offset": "主要设备和位置时间 - 外出并处于另一个时区时的设备和当前位置时间", - "time_zone_2_offset": "次要设备和位置时间 - 外出并处于另一个时区时的设备和当前位置时间" - } - }, - "format_settings": { - "title": "格式设置", - "description": "跟踪活动、结果和信息消息显示在事件日志、传感器和跟踪和监控设备的 device_tracker 实体中。\n\n此屏幕用于指定如何显示这些结果。", - "data": { - "log_level": "日志级别 - iCloud3 添加到 HA 日志文件的消息类型", - "log_level_devices": "日志级别 RAWDATA 设备过滤器 - 仅为这些设备将原始数据转储到日志文件", - "display_zone_format": "事件日志区域显示名称 - 传感器和事件日志中区域名称的显示方式", - "device_tracker_state_source": "设备跟踪器状态值 - 确定设备的 device_tracker 实体状态值的方式", - "time_format": "时间格式 - 传感器和事件日志中时间字段的显示方式", - "unit_of_measurement": "度量单位 - 传感器和事件日志中距离字段的显示方式", - "display_gps_lat_long2": "显示 GPS 坐标 - 在事件日志中显示 GPS(纬度、经度/±精度)或仅显示 GPS(/±精度)", - "display_gps_lat_long": "显示 GPS 坐标 - 在事件日志中显示 GPS-(22.32771, -76.33073/±35m) 而不是 GPS-/±35m", - "action_items": "═════════════════════════════════════════════════════ 操作命令" - }, - "data_description": { - "device_tracker_state_source": "HA 使用设备的 GPS 坐标来确定区域。不考虑 GPS 精度,因此当 GPS 漫游到区域外时可能会退出区域。iCloud3 考虑 GPS 精度,在发生这种情况时不会退出区域。iCloud3 将显示区域的友好名称或在事件日志中显示的区域名称。" - } - }, - "display_text_as": { - "title": "事件日志 '显示文本为'", - "description": "事件日志屏幕上显示的某些文本字段,例如电子邮件地址或电话号码,是私人的,不应显示。例如,您可以将 'geekstergary@apple.com' 替换为 'gary@email.com'。\n\n此屏幕用于指定原始文本和应显示的文本。", - "data": { - "display_text_as": "═════════════════════════════════════════════════════ TEXT REPLACEMENT FIELDS - [Actual text > Displayed text]", - "action_items": "═════════════════════════════════════════════════════ 操作命令" - } - }, - "display_text_as_update": { - "title": "更新事件日志 '显示文本为' 值", - "data": { - "text_from": "原始文本 - 要替换的文本 (例如: gary_real_email@gmail.com)", - "text_to": "显示文本- 要显示的文本 (显示: gary@email.com)", - "action_items": "═════════════════════════════════════════════════════ 操作命令" - }, - "data_description": {} - }, - "actions": { - "title": "iCloud3 操作命令", - "data": { - "ic3_actions": "═════════════════════════════════════════════════════ ICLOUD3 GENERAL CONTROL ACTIONS", - "debug_actions": "═════════════════════════════════════════════════════ DEBUG LOG ACTIONS", - "other_actions": "═════════════════════════════════════════════════════ OTHER ACTIONS", - "action_items": "═════════════════════════════════════════════════════ 操作命令" - } - }, - "inzone_intervals": { - "title": "inZone 参数和默认间隔", - "description": "inZone 间隔是设备在区域内时位置请求之间的时间。\n\n此屏幕用于为不同类型的设备设置默认值。添加设备时会将此值分配给设备。\n\n注意:在更新设备屏幕上,可以为每个设备设置不同的 inZone 间隔。", - "data": { - "iphone": "IPHONE & IPOD", - "ipad": "IPAD", - "watch": "APPLE WATCH", - "airpods": "AIRPODS", - "no_mobapp": "移动应用未安装", - "other": "其他设备类型", - "distance_between_devices": "确定设备之间的距离。使用附近设备的跟踪结果", - "action_items": "═════════════════════════════════════════════════════ 操作命令" - }, - "data_description": { - "no_mobapp": "如果不使用移动应用进行位置监控和区域进入/退出触发器,则默认间隔", - "other": "未指定设备类型的 inzone 间隔" - } - }, - "waze_main": { - "title": "Waze - 路线服务旅行时间/距离", - "data": { - "waze_used": "═════════════════════════════════════════════════════ WAZE 路线服务", - "waze_region": "路线服务器位置 - 您所在地区的 Waze 路线服务器位置", - "waze_realtime": "使用实时数据 - Waze 在确定旅行时间时应考虑交通延迟", - "waze_min_distance": "WAZE 最小距离", - "waze_max_distance": "WAZE 最大距离", - "waze_history_database_used": "═════════════════════════════════════════════════════ WAZE 历史数据库", - "waze_history_track_direction": "一般旅行方向 - 用于在保存位置之间显示 '地图追踪线'", - "waze_history_max_distance": "历史最大距离", - "action_items": "═════════════════════════════════════════════════════ 操作命令" - }, - "data_description": { - "waze_min_distance": "当区域距离大于此值时使用 Waze 路线服务", - "waze_max_distance": "当区域距离大于此值时不使用 Waze 路线服务", - "waze_history_max_distance": "如果距离大于此值,则不将 Waze 旅行时间和距离保存到 Waze 历史数据库" - } - }, - "special_zones": { - "title": "特殊区域", - "description": "此屏幕用于配置:\n• 静止区域 - 当设备在短时间内位于同一位置时创建\n• 进入区域延迟时间 - 延迟处理区域进入触发器\n• 在另一个位置的临时\"家\"区域", - "data": { - "stat_zone_header": "═════════════════════════════════════════════════════ 静止区域", - "stat_zone_fname": "友好名称基础 - 在静止区域时显示的名称 (StatZone)", - "stat_zone_still_time": "无移动时间", - "stat_zone_inzone_interval": "区域内间隔", - "passthru_zone_header": "═════════════════════════════════════════════════════ 进入区域延迟", - "passthru_zone_time": "进入区域延迟时间", - "track_from_base_zone_used": "═════════════════════════════════════════════════════ 主要从家区域跟踪覆盖", - "track_from_base_zone": "跟踪来源区域 - 使用此区域而不是家作为所有设备的跟踪结果。全局设置", - "track_from_home_zone": "从家区域跟踪 - 当主要跟踪来源区域不是家时,保持从家区域跟踪", - "action_items": "═════════════════════════════════════════════════════ 操作命令" - }, - "data_description": { - "passthru_zone_time": "延迟处理您可能正在驾驶通过而实际上并未进入的进入区域触发器", - "stat_zone_still_time": "在同一位置停留一段时间后进入静止区域", - "stat_zone_inzone_interval": "在静止区域内位置请求之间的时间间隔", - "stat_zone_fname": "在设备处于静止区域时,在事件日志、device_tracker、旅行方向和区域实体中显示此值。iCloud3 为静止区域分配编号。使用通配符字符 '#' 在静止区域的名称中显示此值 (StatZon1, StatZon2, etc)。注意:7 个字母的名称留出空间以便在 iPhone 屏幕上显示名称和编号而不被截断。" - } - }, - "sensors": { - "title": "传感器", - "description": "许多传感器用于显示设备的跟踪结果和其他信息。\n\n此屏幕用于选择应创建的传感器。", - "data": { - "monitored_devices": "═════════════════════════════════════════════════════ MONITORED DEVICE SENSORS - 为受监控设备选择要创建的传感器类型", - "device": "═════════════════════════════════════════════════════ DEVICE SENSORS - 设备状态和信息", - "tracking_update": "═════════════════════════════════════════════════════ LOCATION UPDATE SENSORS - 设备位置更新时间", - "tracking_time": "═════════════════════════════════════════════════════ TIME SENSORS - 设备跟踪定时器", - "tracking_distance": "═════════════════════════════════════════════════════ DISTANCE SENSORS - 设备跟踪距离", - "track_from_zones": "═════════════════════════════════════════════════════ TRACK FROM MULTIPLE ZONE SENSORS - 当从多个区域跟踪时使用(仅从家区域跟踪时不需要)", - "tracking_other": "═════════════════════════════════════════════════════ OTHER TRACKING SENSORS - 通常不使用但可用", - "zone": "═════════════════════════════════════════════════════ ZONE SENSORS - 设备区域状态和信息", - "other": "═════════════════════════════════════════════════════ OTHER SENSORS - 不在上述区域中的传感器", - "excluded_sensors": "═════════════════════════════════════════════════════ EXCLUDED SENSORS - 当 iCloud3 启动时不会创建的传感器", - "action_items": "═════════════════════════════════════════════════════ 操作命令" - } - }, - "exclude_sensors": { - "title": "排除传感器", - "description": "为设备创建了许多传感器,但有时您可能希望不为特定设备创建传感器。例如,您可能希望建立一个电池传感器,但除了一个设备外。\n\n此屏幕让您指定不应创建的传感器实体名称。", - "data": { - "excluded_sensors": "EXCLUDED SENSORS - 当 iCloud3 启动时不会创建的传感器", - "filter": "FILTER DISPLAYED SENSORS - 选择应显示的传感器", - "filtered_sensors": "ICLOUD3 SENSORS - 当 iCloud3 启动时创建的传感器列表", - "action_items": "═════════════════════════════════════════════════════ ACTION COMMANDS" - } - }, - "tracking_parameters": { - "title": "跟踪和其他参数", - "description": "一些参数不属于其他任何一般类别,很少更改。\n\n此屏幕用于指定这些参数。", - "data": { - "log_level": "LOG LEVEL - 在 iCloud3 操作期间添加到 HA 日志文件的消息类型", - "gps_accuracy_threshold": "GPS 精度阈值", - "old_location_threshold": "旧位置阈值", - "old_location_adjustment": "旧位置调整", - "distance_between_devices": "使用附近设备的位置结果", - "max_interval": "最大间隔", - "exit_zone_interval": "退出区域间隔", - "mobapp_alive_interval": "请求移动应用位置间隔", - "tfz_tracking_max_distance": "跟踪来源区域显示距离", - "offline_interval": "设备离线间隔", - "travel_time_factor": "旅行时间间隔和下一次位置更新乘数", - "discard_poor_gps_inzone": "丢弃区域内 GPS 精度差的结果 - 当在区域内时丢弃 GPS 精度差的位置更新", - "picture_www_dirs": "包含图片图像的 WWW 目录 - Update Devices > Pictures 文件位置的筛选器", - "event_log_card_directory": "EVENT LOG CARD LOVELACE RESOURCES DIRECTORY - 事件日志自定义卡片 .js 文件目录", - "event_log_btnconfig_url": "EVENT LOG CONFIGURE BUTTON (GEAR) URL > 显示 HA 配置设置屏幕的特殊 URL", - "action_items": "═════════════════════════════════════════════════════ 操作命令" - }, - "data_description": { - "old_location_threshold": "早于此值的位置将被丢弃", - "old_location_adjustment": "增加这个时间来决定位置是否旧", - "distance_between_devices": "当更新跟踪结果时,会识别出附近的设备。当更新这些设备的跟踪结果时,可以使用最初更新的跟踪结果代替。这样可以提高性能,因为不需要再次请求 Waze 路线时间和距离", - "gps_accuracy_threshold": "GPS 精度高于此值的位置将被丢弃", - "tfz_tracking_max_distance": "通常显示家区域的时间和距离数据在设备的 device_tracker 和传感器实体上。当设备在跟踪来源区域的此距离内时,显示跟踪来源区域而不是家区域", - "max_interval": "位置请求之间的最大时间", - "exit_zone_interval": "退出区域后的第一次位置请求时间", - "mobapp_alive_interval": "如果在此时间后没有联系,则向移动应用发送位置请求。这将检查移动应用是否响应位置请求或是否休眠且未运行。", - "offline_interval": "离线时的位置请求间隔(飞行模式,无手机区域等)", - "travel_time_factor": "这用于计算前往家的间隔和下一个位置时间。较小的值将减少间隔时间并增加位置请求,较大的值将增加间隔并减少位置请求。", - "event_log_btnconfig_url": "通常,这是空白的,iCloud3 将确定其配置设置屏幕的 URL。然而,如果因在虚拟环境、docker 或另一设备上运行 HA 而出现问题,并且无法确定实际 URL,可能会遇到 404 未找到错误。如果发生这种情况,以正常方式选择它(HA 设备和服务 > 集成 > iCloud3 > 配置设置齿轮),并将浏览器中的 URL 复制到此字段。" - } - } - } - }, - "services": { - "action": { - "name": "Action", - "description": "此服务将向 iCloud3 发送操作命令", - "fields": { - "command": { - "name": "Command", - "description": "(必需) 要发送到 iCloud3 的操作命令" - }, - "device_name": { - "name": "Device Name", - "description": "(可选) 适用于所有设备或仅适用于所选设备" - } - } - }, - "update": { - "name": "Update", - "description": "更新服务已由 Action 服务替代" - }, - "restart": { - "name": "Restart", - "description": "此服务将重启 iCloud3" - }, - "find_iphone_alert": { - "name": "Find iPhone Alert Tone", - "description": "此服务将向您想要找到的设备发送警报音", - "fields": { - "device_name": { - "name": "Device Name", - "description": "应发送 Find iPhone 警报和消息的设备" - } - } - }, - "lost_device_alert": { - "name": "Send Lost Device Message", - "description": "此服务将向丢失的 iPhone 发送消息和电话号码", - "fields": { - "device_name": { - "name": "Device Name", - "description": "应发送 Find iPhone 警报和消息的设备" - }, - "number": { - "name": "Phone Number", - "description": "要发送消息的电话号码" - }, - "message": { - "name": "Message", - "description": "要发送的消息" - } - } - } - } -} \ No newline at end of file diff --git a/custom_components/icloud3/zone.py b/custom_components/icloud3/zone.py index b46b5db..8550fdd 100644 --- a/custom_components/icloud3/zone.py +++ b/custom_components/icloud3/zone.py @@ -25,7 +25,7 @@ from .helpers.common import (instr, is_statzone, format_gps, zone_dname, list_add, list_del, ) from .helpers.messaging import (post_event, post_error_msg, post_monitor_msg, - log_exception, log_rawdata,_trace, _traceha, ) + log_exception, log_rawdata,_evlog, _log, ) from .helpers.time_util import (time_now_secs, ) from .helpers.dist_util import (gps_distance_m, gps_distance_km, ) @@ -60,11 +60,11 @@ def __init__(self, zone, zone_data=None): self.initialize_zone_name(self.zone_data) self.setup_zone_display_name() - Gb.Zones = list_add(Gb.Zones, self) + list_add(Gb.Zones, self) Gb.Zones_by_zone[zone] = self if self.is_ha_zone: - Gb.HAZones = list_add(Gb.HAZones, self) + list_add(Gb.HAZones, self) Gb.HAZones_by_zone[self.zone] = self if zone_data: