From 4fd713f132023e11d6c8c1104edb56cd2f1ce88d Mon Sep 17 00:00:00 2001 From: gcobb321 Date: Thu, 9 Mar 2023 17:21:49 -0500 Subject: [PATCH] iCloud3 v3.0.0, Beta 14 --- ChangeLog.txt | 148 +- custom_components/icloud3/ChangeLog.txt | 75 +- custom_components/icloud3/__init__.py | 15 +- custom_components/icloud3/config_flow.py | 336 ++--- custom_components/icloud3/const.py | 26 +- custom_components/icloud3/const_sensor.py | 34 +- custom_components/icloud3/device.py | 74 +- custom_components/icloud3/device_fm_zone.py | 8 +- custom_components/icloud3/device_tracker.py | 153 +- .../event_log_card/icloud3-event-log-card.js | 69 +- custom_components/icloud3/global_variables.py | 3 +- custom_components/icloud3/helpers/common.py | 62 +- .../icloud3/helpers/dist_util.py | 38 +- .../icloud3/helpers/messaging.py | 36 +- .../icloud3/helpers/time_util.py | 9 + custom_components/icloud3/icloud3_main.py | 133 +- custom_components/icloud3/manifest.json | 2 +- custom_components/icloud3/sensor copy.py | 1312 +++++++++++++++++ custom_components/icloud3/sensor.py | 91 +- custom_components/icloud3/strings.json | 338 ++++- .../icloud3/support/config_file.py | 3 +- .../icloud3/support/determine_interval.py | 83 +- .../icloud3/support/event_log.py | 39 +- .../icloud3/support/icloud_data_handler.py | 2 +- .../icloud3/support/pyicloud_ic3.py | 372 +++-- .../icloud3/support/pyicloud_ic3_interface.py | 74 +- .../icloud3/support/restore_state.py | 2 + .../icloud3/support/start_ic3.py | 118 +- custom_components/icloud3/support/waze.py | 20 +- .../icloud3/support/waze_history.py | 5 + .../icloud3/translations/en.json | 116 +- custom_components/icloud3/zone.py | 45 +- 32 files changed, 2979 insertions(+), 862 deletions(-) create mode 100644 custom_components/icloud3/sensor copy.py diff --git a/ChangeLog.txt b/ChangeLog.txt index a11ad98..f565d17 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -1,14 +1,156 @@ -Beta 11 - mm/dd/2023 +Beta 14 - 3/4/2023 +~~~~~~~~~~~~~~~~~~~~ +1. iCloud Account Authentication - When the iCloud Account is verified using a password, a session access + token is created and stored as a cookie. The password login also generates an email saying someone has + logged into your iCloud account. This beta changes the method of authenticating the account to using + the token first and then using the password if it has expired. This should eliminate or greatly reduce + the login emails generated by Apple. +2. Configuration Wizard > iCloud Account Credentials screen - Updated it to improve readability and make + it easier to set up the account information. Several rare problems were also fixed where (1) logging + into an iCloud Account with an incorrect password would still allow access when there was a valid token + and (2) logging into an iCloud Account with a changed password would not work if the token has expired. + Now, logging in with a new password causes a new 2fa request to be made. +3. Zone enter/leave event for an iCloud3 device_tracker entity - This can now be used as an automation + trigger. +4. Latitude/longitude values - They are now stored as numeric fields in the device_tracker attributes instead + of a string field. They are now displayed with 2-decimal places on a Lovelace device_tracker/attributes + window (an HA limitation). A new gps attribute has been added to display the latitude/longitude with it's + full precision. +5. Passive Zones - Fixed a problem where passive zones could be selected. Also found out that the iOS App + will generate enter/exit triggers for passive zones. These will now be ignored by iCloud3. +6. Configuration Wizard > Sensors screen - Changed it for readability and to list the important sensors + first. +7. Event Log - Made some minor formatting changes. Also added a new icon to the top-right of the Event Log + so you can become an iCloud3 Stargazer. Click it to go to the iCloud3_v2 GitHub repository. Then click + the star in the top-right corner. +8. Distance Sensors: + (1) Changed the distance precision so the distance value is within 6-inches/1-millimeter of the + measurement between the device and the zone center - (4-decimal places for miles, 5 for meters) + (2) Changed the '_moved_distance' sensor to display in feet/meters when less than 1mi/1km + (3) Changed the '_moved_distance' to display the distance from one poll to the next when in a zone + (4) Added attribute to '_moved_distance' to show the from and to location time + (5) Added attribute to all 'zone_distance' sensors - 'meters_distance' shows the distance in meters + to the zone with 5-decimal place accuracy + (6) Added attribute to all 'zone_distance' sensors - 'miles_distance' shows the distance in miles + to the zone with 4-decimal place accuracy if the unit_of_measure is miles. + + + +Beta 13 2/21/2023 +~~~~~~~~~~~~~~~~~~~~ + +------------------------------------------------------------------------------------------------------------------------- +VERY IMPORTANT - HA CHANGED THE WAY SENSORS AND DEVICE_TRACKERS ARE SETUP - +THIS UPDATE MUST BE INSTALLED FOR THE 2023.3.1 HA RELEASE + +1. Updated the initilization routines to use the new device_tracker and sensor entity creation methods required by HA + for the 2023.3.0 release. +------------------------------------------------------------------------------------------------------------------------- +2. Battery entity - The battery sensors are not created if the device only uses the FmF tracking method (FamShr and + iOSApp are not used). The FmF tracking method does not report battery information. +3. iCloud Interface - There are 4-steps to set up the iCloud Account interface - Setup, Authentication, FamShr, and FmF. + Made some changes to this process to better report the steps (in the Event Log) that had not been completed, what was + in process and what still needed to be done when finalizing the set up process in Stage 4. +4. Changed the device's Info sensor to add details showing when the device left the Last Zone and arrived at the Current + Zones: + At-Home-10:23:00a (2 hrs ago), + Left-School-3:45:50p (10 mins ago). + Changed the dir_of_travel sensor for Towards, AwayFrom and InZone to show the zone's name. Examples: + Towards: `ᗒ Home`, `ᗒ School`, + AwayFrom: `Home ᗒ`, 'School ᗒ', + InZone: `@Home`, `@School` +5. Added options to the way the Device_Tracker state value is set on the Format Settings. It now includes the zone entity + name, the zone's friendly name, and the iCloud3 reformatted names based on the entity id. +6. There are some issues with the source of the zone's friendly name. It is stored in the zone definition file + (.storage/zone) and in the entity registry for the zone. These two areas are sometimes not in sync, particulary when + the name has been changed. iCloud3 not gets the friendly name from the zone file, not the entity registry. +7. Fixed a problem with overlaping zones. When a device is in two zones, the smaller one will now be picked. +8. Changed the Configurator screen on menu page 1 from `Event Log Parameters` to `Format Settings` since that is what it + really is. Also moved the Event Log directory which never gets changed to the Other Parameters screen. + THIS WILL REQUIRE A BROWSER REFRESH when using the configuration screen. +9. Adjusted the distance values when using metric values. The distance is now displayed in meters when it is less + than 1 km. +10. Fixed a problem when the device could be put into a passive zone. They are still displayed in the Zone's list when + starting. Now, an indicator shows it is not used. +11. The display option and the device_tracker state values for each zone are displayed in the Event Log during Stage 2 + when starting. +12. Tweaked sone setting in the Event Log card and bumped the version to 3.0.5. + THIS WILL REQUIRE A BROWSER REFRESH. + + + +Beta 12 2/10/2023 +~~~~~~~~~~~~~~~~~~~~ +1. iCloud3 Configurator - The HA 2023.2.2 seems to have disabled the time entry fields (hh:mm:ss) used + to enter a lot of parameters (inZone Interval, Stationary Still Time, Old Location adjustment, etc). + The screens display but none of thse fields are showing. This may be a HA bug that needs fixing or + they are removing it from HA. In any case, all of the configuration parameter entry screens have + been modified to use another method of entering the parameters. It actually works better, is easier + to use and takes up less screen real estate. The fields can be entered using the keyboard or through + a numeric slider. + THIS WILL REQUIRE A BROWSER REFRESH AFTER DISPLAYING ONE OF THE CONFIGURATOR SCREENS +2. New 'locate' Service call - Added the 'locate' Action to the icloud3.action Service Call. This action + lets you locate a device (or all devices) immediately or in after a specific time has elapsed. + This replaces the set_interval service call in v2. + The following examples explains how it can be used: + Calling method: + service: icloud3.action + data: {action: locate xx min, device_name: xxx} + + Examples: + - Locate gary_iphone in 4 minutes: {action: locate 4 min, device_name: gary_iphone} + - Locate gary_iphone immediately: {action: locate, device_name: gary_iphone} + - Locate all phones in 4 minutes: {action: locate 4 min} + - Locate all phones immediately: {action: locate} +2. Event Log > Action - Added the above 'locate' to the Action command manu to locate a specific device or + all devices using the FamShr or FmF methods. Also tweaked the debug/rawdata logging text color on the + heading lines displayed when an update is started or completed. It is now black. + YOU WILL GET A NOTIFICATION TO REFRESH YOUR BROWSER +3. Passthru Zone - Tried to fix the problem with the passthru zone enter delay. A device that was + not using the iOS App (Watch) would set up the passthru zone enter delay when entering a zone but + the next update time was never getting triggered so the zone enter delay was never be cleared. + This stopped the device from being tracked and updated. On a trial run, the passthrou zone delay did + not work but the Watch was tracking without any issues. It is disabled in this beta until I get it + working and have given it a comprehensive test. It should be available in beta 13 next weekend. + + +Beta 11 - 2/5/2023 ~~~~~~~~~~~~~~~~~~~~ 1. Added a Stationary Zone name parameter on the Update iCloud3 Devices parameter screen - Each device can now have it's own Stationary Zone friendly name. This overrides the generic friendly name on the Special Zones screen for the Stationary Zone. + NOTE: A BROWSER REFRESH WILL/MAY BE NEEDED IF THE FIELD DOES NOT DISPLAY The default name is 'Stationary'. It can be customized on the Special Zones screen to [name]StatZone (GarStatZone, LilStatZone). It can now be set by device (GaryZone, LillianZone). -2. Fixed a problem where 'Statioary' was always used for the device_tracker state for a Stationary Zone +2. Fixed a problem where 'Stationary' was always used for the device_tracker state for a Stationary Zone instead of the friendly name that was set up on the Special Zones/Stationary Zone screen. Also set the - Stationary Zone state to 0 when it was created so the person counter would work. + Stationary Zone state to 0 when it was created so the person counter would work (maybe). +3. Because of the way iCloud location data is processed, Device updates are handled serially. Only one + Device can be updated at a time, the next in line must wait until the previous one is complete. The + method used to control this process was changed to ensure iCloud3 tracking would not hang in up waiting + for one device update to finish when an unknown error occurred or it was taking a very long time to + get location data from the iCloud account. Now, everything is reset if one device has been in an + update process for more than 3-minutes. + [LET ME KNOW IF YOU HAVE ANY PROBLEMS WHERE TRACKING STOPS FOR ALL DEVICES OR ICLOUD3 SEEMS TO BE HUNG UP] +4. Changed the way the Next Update Time is displayed when the update will be done in 90-secs. For example, + the time was formatted as '9:35:15a -45s' indicating the update would be done in 45-secs. This will + break in a future HA release. Now, only the time until the next update will be displayed (45 secs) + when the update will be done within 90-seconds. +5. Added a new configuration screen to change the order the devices are tracked and displayed on the + Event Log. They are processed in the order they were first added to iCloud3 and listed on the iCloud3 + Devices List screen. This new screen lets you change this order. Select `Change Device Order` on the + iCloud3 Devices List or the Event Log Parameters screens. +6. If there is an error reading the configuration file when iCloud3 starts, the backup configuration file + is read and the configuration file with the error is overwritten. The configuration file with the + error is now saved with a timestamp before it is overwritten. +7. Fixed a problem where the latitude/longitude values for the Device were not being restored when iCloud3 + was starting. The latitude/longitude was set to 0/0 rather than the last known position, putting it in + Atlantic Ocean on the equator off the coast of Africa on the HA map until the device's location was + updated +8. Fixed the problem where the battery would go from a valye to 0 and back to the value during startup. + Again. Really, I think I have it this time, no battery = 0 in my debug logs. + Beta 10a - 1/29/2023 ~~~~~~~~~~~~~~~~~~~~ diff --git a/custom_components/icloud3/ChangeLog.txt b/custom_components/icloud3/ChangeLog.txt index c981b60..910ae59 100644 --- a/custom_components/icloud3/ChangeLog.txt +++ b/custom_components/icloud3/ChangeLog.txt @@ -1,4 +1,50 @@ -Beta 13 +Beta 14 - 3/4/2023 +~~~~~~~~~~~~~~~~~~~~ +1. iCloud Account Authentication: + - When the iCloud Account is verified using a password, a session access token is created and stored + as a cookie. The password login also generates an email saying someone has logged into your iCloud + account. This beta changes the method of authenticating the account to using the token first and + then using the password if it has expired. This should eliminate or greatly reduce the login emails + generated by Apple. + - Did a lot of code cleanup in the account authentication routines to insure the 2fa-code is requested + when needed, to be able to reauthenticate and log into the current account with a new password and to + be able to log into a different account in the Configuration Wizard. +2. Configuration Wizard > iCloud Account Credentials screen - Updated it to improve readability and make + it easier to set up the account information. Several rare problems were also fixed where (1) logging + into an iCloud Account with an incorrect password would still allow access when there was a valid token + and (2) logging into an iCloud Account with a changed password would not work if the token has expired. + Now, logging in with a new password causes a new 2fa request to be made. +3. Zone enter/leave event for an iCloud3 device_tracker entity - This can now be used as an automation + trigger. +4. Latitude/longitude values - They are now stored as numeric fields in the device_tracker attributes instead + of a string field. They are now displayed with 2-decimal places on a Lovelace device_tracker/attributes + window (an HA limitation). A new gps attribute has been added to display the latitude/longitude with it's + full precision. +5. Passive Zones - Fixed a problem where passive zones could be selected. Also found out that the iOS App + will generate enter/exit triggers for passive zones. These will now be ignored by iCloud3. +6. Configuration Wizard > Sensors screen - Changed it for readability and to list the important sensors + first. +7. Event Log - Made some minor formatting changes. Also added a new icon to the top-right of the Event Log + so you can become an iCloud3 Stargazer. Click it to go to the iCloud3_v2 GitHub repository. Then click + the star in the top-right corner. +8. Distance Sensors: + (1) Changed the distance precision so the distance value is within 6-inches/1-millimeter of the + measurement between the device and the zone center - (4-decimal places for miles, 5 for meters) + (2) Changed the '_moved_distance' sensor to display in feet/meters when less than 1mi/1km + (3) Changed the '_moved_distance' to display the distance from one poll to the next when in a zone + (4) Added attribute to '_moved_distance' sensors: + - '_moved_from' shows the from location time + - '_moved_to' shows current location time + (5) Added attribute to all 'zone_distance' sensors: + - '_meters_distance' shows the distance in meters to the zone with 5-decimal place accuracy, + - '_meters_distance_to_zone_edge' shows the distance in meters to the edge of the zone + (6) Added attribute to all 'zone_distance' sensors: + - '_miles_distance' shows the distance in miles to the zone with 4-decimal place accuracy if the + unit_of_measure is miles. + + + +Beta 13 2/21/2023 ~~~~~~~~~~~~~~~~~~~~ ------------------------------------------------------------------------------------------------------------------------- @@ -13,26 +59,29 @@ THIS UPDATE MUST BE INSTALLED FOR THE 2023.3.1 HA RELEASE 3. iCloud Interface - There are 4-steps to set up the iCloud Account interface - Setup, Authentication, FamShr, and FmF. Made some changes to this process to better report the steps (in the Event Log) that had not been completed, what was in process and what still needed to be done when finalizing the set up process in Stage 4. -4. Changed the device's Info sensor to add details showing when the device left the Last Zone and arrived at the Current Zonea +4. Changed the device's Info sensor to add details showing when the device left the Last Zone and arrived at the Current + Zones: At-Home-10:23:00a (2 hrs ago), Left-School-3:45:50p (10 mins ago). Changed the dir_of_travel sensor for Towards, AwayFrom and InZone to show the zone's name. Examples: Towards: `ᗒ Home`, `ᗒ School`, AwayFrom: `Home ᗒ`, 'School ᗒ', InZone: `@Home`, `@School` -5. Added options to the way the Device_Tracker state value is set on the Format Settings. It now includes the zone entity name, - the zone's friendly name, and the iCloud3 reformatted names based on the entity id. -6. There are some issues with the source of the zone's friendly name. It is stored in the zone definition file (.storage/zone) - and in the entity registry for the zone. These two areas are sometimes not in sync, particulary when the name has been changed. - iCloud3 not gets the friendly name from the zone file, not the entity registry. +5. Added options to the way the Device_Tracker state value is set on the Format Settings. It now includes the zone entity + name, the zone's friendly name, and the iCloud3 reformatted names based on the entity id. +6. There are some issues with the source of the zone's friendly name. It is stored in the zone definition file + (.storage/zone) and in the entity registry for the zone. These two areas are sometimes not in sync, particulary when + the name has been changed. iCloud3 not gets the friendly name from the zone file, not the entity registry. 7. Fixed a problem with overlaping zones. When a device is in two zones, the smaller one will now be picked. -8. Changed the Configurator screen on menu page 1 from `Event Log Parameters` to `Format Settings` since that is what it really is. - Moved the Event Log directory which never gets changed to the Other Parameters screen. +8. Changed the Configurator screen on menu page 1 from `Event Log Parameters` to `Format Settings` since that is what it + really is. Also moved the Event Log directory which never gets changed to the Other Parameters screen. THIS WILL REQUIRE A BROWSER REFRESH when using the configuration screen. -9. Adjusted the distance values when using metric values. The distance is now displayed in meters when it is less than 1 km. -10. Fixed a problem when the device could be put into a passive zone. They are still displayed in the Zone's list when starting. Now, - an indicator shows it is not used. -11. The display option and the device_tracker state values for each zone are displayed in the Event Log during Stage 2 when starting. +9. Adjusted the distance values when using metric values. The distance is now displayed in meters when it is less + than 1 km. +10. Fixed a problem when the device could be put into a passive zone. They are still displayed in the Zone's list when + starting. Now, an indicator shows it is not used. +11. The display option and the device_tracker state values for each zone are displayed in the Event Log during Stage 2 + when starting. 12. Tweaked sone setting in the Event Log card and bumped the version to 3.0.5. THIS WILL REQUIRE A BROWSER REFRESH. diff --git a/custom_components/icloud3/__init__.py b/custom_components/icloud3/__init__.py index 15147e2..185937b 100644 --- a/custom_components/icloud3/__init__.py +++ b/custom_components/icloud3/__init__.py @@ -108,16 +108,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=DOMAIN) hass_data = dict(entry.data) + # _traceha(f"{hass.data=}") # 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 # hass.data[DOMAIN][entry.entry_id] = hass_data + Gb.hass = hass Gb.config_entry = entry Gb.entry_id = entry.entry_id Gb.operating_mode = MODE_INTEGRATION + log_info_msg(f"Setting up iCloud3 {VERSION} - Using Integration method") + Gb.PyiCloud = None Gb.EvLog = event_log.EventLog(Gb.hass) Gb.start_icloud3_inprocess_flag = True @@ -137,10 +141,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # hass.config_entries.async_setup_platforms(entry, PLATFORMS) - log_info_msg(f"Setting up iCloud3 {VERSION} - Using Integration method") #----- hass test - start - #----- hass test - end # conf_version goes from: @@ -240,5 +242,10 @@ async def async_get_ha_location_info(): "use_metric": location_info.use_metric, } - Gb.country_code = Gb.ha_location_info.country_code.lower() - Gb.use_metric = Gb.ha_location_info.use_metric + try: + # Gb.country_code = Gb.ha_location_info[country_code].lower() + # Gb.use_metric = Gb.ha_location_info[use_metric] + Gb.country_code = Gb.ha_location_info.country_code.lower() + Gb.use_metric = Gb.ha_location_info.use_metric + except Exception as err: + log_exception(err) diff --git a/custom_components/icloud3/config_flow.py b/custom_components/icloud3/config_flow.py index c8c0b08..549fdc4 100644 --- a/custom_components/icloud3/config_flow.py +++ b/custom_components/icloud3/config_flow.py @@ -18,13 +18,13 @@ from .global_variables import GlobalVariables as Gb from .const import (DOMAIN, - RARROW, + RARROW, CRLF_DOT, EVLOG_NOTICE, EVLOG_ALERT, IPHONE_FNAME, IPHONE, IPAD, WATCH, AIRPODS, ICLOUD, OTHER, HOME, DEVICE_TYPES, DEVICE_TYPE_FNAME, DEVICE_TRACKER_DOT, IOSAPP, NO_IOSAPP, TRACK_DEVICE, MONITOR_DEVICE, INACTIVE_DEVICE, NAME, FRIENDLY_NAME, FNAME, TITLE, BATTERY, - ZONE, HOME_DISTANCE, + ZONE, HOME_DISTANCE, PASSIVE, WAZE_SERVERS_BY_COUNTRY_CODE, WAZE_SERVERS_FNAME, CONF_VERSION, CONF_EVLOG_CARD_DIRECTORY, CONF_USERNAME, CONF_PASSWORD, CONF_DEVICES, CONF_SETUP_ICLOUD_SESSION_EARLY, @@ -62,7 +62,7 @@ DEFAULT_DEVICE_REINITIALIZE_CONF, CONF_PARAMETER_TIME_STR, ) from .const_sensor import (SENSOR_GROUPS ) -from .helpers.common import (instr, isnumber, obscure_field, zone_display_as, ) +from .helpers.common import (instr, isnumber, obscure_field, zone_display_as, list_to_str,str_to_list, ) from .helpers.messaging import (log_exception, _traceha, _trace, post_event, close_reopen_ic3_debug_log_file, ) from .helpers import entity_io from . import sensor as ic3_sensor @@ -106,7 +106,7 @@ def dict_value_to_list(key_value_dict): 'device_list': 'ICLOUD3 DEVICES > Add, Change and Delete tracked and monitored devices', 'format_settings': 'FORMAT SETTINGS > Select the display format for Zones, DeviceTracker State, Unit of Measure, Time & Distance, Change Device Order', 'display_text_as': 'DISPLAY TEXT AS > Event Log Custom Card Text Replacement', - 'other_parms': 'OTHER PARAMETERS > Select tracking calculation values - Accuracy Thresholds, Maximum Interval, Device Offline Interval, Travel Time Interval Multiplier, Event Log Custom Card Directory, etc.', + 'tracking_parameters': 'TRACKING & OTHER PARAMETERS > Parameters used when tracking a device - Accuracy Thresholds, Maximum Interval, Device Offline Interval, Travel Time Interval Multiplier, Event Log Custom Card Directory, etc.', 'inzone_intervals': 'INZONE INTERVALS > Default inZone intervals for different device types, inZone Interval if the iOS App is not installed, Other inZone Controls ', 'waze': 'WAZE ROUTE DISTANCE & TIME, WAZE HISTORY DATABASE > Route Server Location, Min/Max Intervals, Waze History Database Parameters and Controls', 'special_zones': 'SPECIAL ZONES - Pass Through Zone, Stationary Zone, Track From Zone', @@ -130,7 +130,7 @@ def dict_value_to_list(key_value_dict): MENU_KEY_TEXT['waze'], MENU_KEY_TEXT['inzone_intervals'], MENU_KEY_TEXT['special_zones'], - MENU_KEY_TEXT['other_parms'], + MENU_KEY_TEXT['tracking_parameters'], ] menu_action = [ MENU_KEY_TEXT['select'], @@ -146,7 +146,7 @@ def dict_value_to_list(key_value_dict): 'select_form': 'SELECT > Select the parameter update form', - 'log_in_icloud_acct': 'LOGIN > Log into the iCloud Account', + 'log_in_icloud_acct': 'LOGIN > Log into the iCloud Account. Logged Into-Not Logged In', 'enter_verification_code': 'ENTER VERIFICATION CODE > Enter the 6-digit Verification Code', "icloud_acct_reauth": "RESET AND REAUTHENTICATE ACCOUNT > Request a new Verification Code", 'show_username_password': 'SHOW/HIDE USERNAME/PASSWORD > Show or hide the Username and Password', @@ -194,13 +194,12 @@ def dict_value_to_list(key_value_dict): OPT_LIST_KEY_TEXT_NONE = {'None': 'None'} OPT_PICTURE_TEXT = ['None'] UNKNOWN_DEVICE_TEXT = ' >>>>> VALUE NOT IN LIST, NEEDS REVIEW <<<<<' +LOG_IN_ICLOUD_ACCT_IDX = 0 ICLOUD_ACCOUNT_ACTIONS = [ OPT_ACTION_ITEMS_KEY_TEXT['log_in_icloud_acct'], OPT_ACTION_ITEMS_KEY_TEXT['enter_verification_code'], - OPT_ACTION_ITEMS_KEY_TEXT['icloud_acct_reauth'], - OPT_ACTION_ITEMS_KEY_TEXT['divider1'], - OPT_ACTION_ITEMS_KEY_TEXT['show_username_password']] + OPT_ACTION_ITEMS_KEY_TEXT['icloud_acct_reauth']] DEVICE_LIST_ACTIONS = [ OPT_ACTION_ITEMS_KEY_TEXT['update_device'], OPT_ACTION_ITEMS_KEY_TEXT['add_device'], @@ -223,6 +222,11 @@ def dict_value_to_list(key_value_dict): 'icloud': 'ICLOUD ONLY - iOS App is not monitored on any tracked device', 'iosapp': 'IOS APP ONLY - iCloud account is not used for location data on any tracked device' } +DATA_SOURCE_ITEMS_KEY_TEXT2 = { + # 'icloud,iosapp': 'ICLOUD & IOSAPP - iCloud account and iOS App are used for location data', + 'icloud': 'iCloud account is used for location data on any tracked device', + 'iosapp': 'iOS App is monitored on any tracked device' + } ICLOUD_SERVER_ENDPOINT_SUFFIX_ITEMS_KEY_TEXT = { 'none': 'Use normal Apple iCloud Servers', 'cn': 'China - Use Apple iCloud Servers located in China' @@ -251,15 +255,16 @@ def dict_value_to_list(key_value_dict): } DISPLAY_ZONE_FORMAT_ITEMS_KEY_TEXT = {} DISPLAY_ZONE_FORMAT_ITEMS_KEY_TEXT_BASE = { - 'fname': 'HA Zone Friendly Name used by zone automation triggers (TheShores)', - 'zone': 'HA Zone entity_id (the_shores)', + 'fname': 'HA Zone Friendly Name, (Home, Away, TheShores)', + '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_FORMAT_KEY_TEXT = {} DEVICE_TRACKER_STATE_FORMAT_KEY_TEXT_BASE = { - 'fname': 'HA Zone Friendly Name used by zone automation triggers (TheShores)', - 'zone': 'HA Zone entity_id (the_shores)', + 'fname': 'HA Zone Friendly Name, (home, not_home, TheShores)', +# 'fname/Home': 'Home (upper-case), HA Zone Friendly Name, (TheShores)', + '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)' } @@ -578,7 +583,7 @@ def initialize_options(self): # in the exclude_sensors screen # Format: ('gary_iphone_zone_distance': 'Gary ZoneDistance (gary_iphone_zone_distance)') self.sensors_fname_list = [] - self.excluded_sensors = [] + self.excluded_sensors = ['None'] self.excluded_sensors_removed = [] self.sensors_list_filter = '?' @@ -651,8 +656,8 @@ async def async_step_menu(self, user_input=None, errors=None): return await self.async_step_format_settings() elif menu_item == 'display_text_as': return await self.async_step_display_text_as() - elif menu_item == 'other_parms': - return await self.async_step_other_parms() + elif menu_item == 'tracking_parameters': + return await self.async_step_tracking_parameters() elif menu_item == 'inzone_intervals': return await self.async_step_inzone_intervals() elif menu_item == 'waze': @@ -745,8 +750,8 @@ def common_form_handler(self, user_input=None, action_item=None, errors=None): user_input = self._validate_format_settings(user_input) elif self.step_id == "display_text_as": pass - elif self.step_id == 'other_parms': - user_input = self._validate_other_parms(user_input) + elif self.step_id == 'tracking_parameters': + user_input = self._validate_tracking_parameters(user_input) elif self.step_id == 'inzone_intervals': user_input = self._validate_inzone_intervals(user_input) elif self.step_id == "waze_main": @@ -772,13 +777,6 @@ 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) - # self.opt_www_directory_list = [] - # path_filters = ['/.', 'deleted', '/x-'] - # for path, dirs, files in os.walk(Gb.hass.config.path('www')): - # if instr(path, path_filters) or path.count('/') > 4: - # continue - # self.opt_www_directory_list.append(path.replace('/config/', '')) - if action_item == 'change_device_order': self.cdo_devicenames = [self._format_device_info(conf_device) for conf_device in Gb.conf_devices] @@ -879,20 +877,12 @@ def _set_example_zone_name(self): self._dzf_set_example_zone_name_text(ZONE, 'the_shores', exZone.zone) self._dzf_set_example_zone_name_text(FNAME, 'The Shores', exZone.fname) self._dzf_set_example_zone_name_text(FNAME, 'TheShores', exZone.fname) + self._dzf_set_example_zone_name_text('fname/Home', 'TheShores', exZone.fname) self._dzf_set_example_zone_name_text(NAME, 'the_shores', exZone.zone) self._dzf_set_example_zone_name_text(NAME, 'TheShores', exZone.name) self._dzf_set_example_zone_name_text(TITLE, 'the_shores', exZone.zone) self._dzf_set_example_zone_name_text(TITLE, 'The Shores', exZone.title) - # stf_fname = Gb.conf_general[CONF_DISPLAY_ZONE_FORMAT] - # if stf_fname == FNAME: stf_fname = exZone.fname - # elif stf_fname == NAME: stf_fname = exZone.name - # elif stf_fname == TITLE: stf_fname = exZone.title - # else: stf_fname = exZone.zone - # self._dtf_set_example_zone_name_text(ZONE, 'the_shores', exZone.zone) - # self._dtf_set_example_zone_name_text(FNAME, 'The Shores', stf_fname) - # self._dtf_set_example_zone_name_text(FNAME, 'TheShores', stf_fname) - 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] = \ @@ -904,8 +894,8 @@ def _dzf_set_example_zone_name_text(self, key, example_text, real_text): DEVICE_TRACKER_STATE_FORMAT_KEY_TEXT[key].replace(example_text, real_text) #------------------------------------------------------------------------------------------- - async def async_step_other_parms(self, user_input=None, errors=None): - self.step_id = 'other_parms' + 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) self.opt_www_directory_list = [] @@ -976,6 +966,9 @@ async def async_step_sensors(self, user_input=None, errors=None): self.step_id = 'sensors' 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 not None: if HOME_DISTANCE not in user_input[CONF_SENSORS_TRACKING_DISTANCE]: user_input[CONF_SENSORS_TRACKING_DISTANCE].append(HOME_DISTANCE) @@ -1058,7 +1051,9 @@ def _update_excluded_sensors(self, user_input): self.sensors_fname_list = list(set(self.sensors_fname_list)) self.sensors_fname_list.sort() - if len(self.excluded_sensors) > 1 and 'None' in self.excluded_sensors: + if self.excluded_sensors == []: + self.excluded_sensors = ['None'] + elif len(self.excluded_sensors) > 1 and 'None' in self.excluded_sensors: self.excluded_sensors.remove('None') #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> @@ -1304,12 +1299,12 @@ def _validate_format_settings(self, user_input): return user_input #------------------------------------------------------------------------------------------- - def _validate_other_parms(self, user_input): + def _validate_tracking_parameters(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_LOG_LEVEL, LOG_LEVEL_ITEMS_KEY_TEXT) + # user_input = self._option_text_to_parm(user_input, CONF_LOG_LEVEL, LOG_LEVEL_ITEMS_KEY_TEXT) return user_input @@ -1361,12 +1356,6 @@ def _validate_waze_main(self, user_input): if user_input[CONF_WAZE_USED] != Gb.conf_general[CONF_WAZE_USED]: user_input[CONF_WAZE_HISTORY_DATABASE_USED] = user_input[CONF_WAZE_USED] - # correct_server = WAZE_SERVERS_BY_COUNTRY_CODE.get(Gb.ha_country_code, 'row') - # if (CONF_WAZE_SERVER in user_input - # and user_input[CONF_WAZE_SERVER] != correct_server): - # self.errors[CONF_WAZE_SERVER] = f"waze_server_error_{correct_server}" - # user_input[CONF_WAZE_SERVER] = correct_server - return user_input #------------------------------------------------------------------------------------------- @@ -1448,90 +1437,71 @@ async def async_step_icloud_account(self, user_input=None, errors=None): try: if user_input is not None: + user_input[CONF_DATA_SOURCE] = list_to_str(user_input[CONF_DATA_SOURCE]) + user_input[CONF_USERNAME] = user_input[CONF_USERNAME].lower() + + if user_input['opt_action'].startswith('LOGIN'): + user_input['opt_action'] = OPT_ACTION_ITEMS_KEY_TEXT['log_in_icloud_acct'] + user_input, action_item = self._action_text_to_item(user_input) user_input = self._option_text_to_parm(user_input, CONF_DATA_SOURCE, DATA_SOURCE_ITEMS_KEY_TEXT) + user_input = self._strip_spaces(user_input, [CONF_USERNAME, CONF_PASSWORD]) + user_input = self._strip_spaces(user_input) - username_password_changed_flag = self._unobscure_username_password(user_input) if action_item == 'cancel': return await self.async_step_menu() - elif action_item == 'show_username_password': - self.show_username_password = not self.show_username_password - - elif self.errors: - pass - - elif action_item == 'save': - if user_input[CONF_DATA_SOURCE] == IOSAPP: - self._update_configuration_file(user_input) - self.PyiCloud = None - return await self.async_step_menu() + # Data Source is iOS App only, iCloud was not selected + if user_input[CONF_DATA_SOURCE] == IOSAPP: + self._update_configuration_file(user_input) + self.PyiCloud = None + return await self.async_step_menu() - if user_input[CONF_DATA_SOURCE] != Gb.conf_tracking[CONF_DATA_SOURCE]: - user_input = {CONF_DATA_SOURCE: user_input[CONF_DATA_SOURCE]} + if action_item == 'enter_verification_code': + return await self.async_step_reauth(initial_display=True) - self._update_configuration_file(user_input) + if action_item == 'icloud_acct_reauth': + self.config_flow_updated_parms.update(['reauth']) + self.errors = {'base': 'icloud_reauth_scheduled'} + return await self.async_step_menu() - if username_password_changed_flag: + 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] = '' + + # Action Login or Save will login into the account if the username changed + if self.errors == {}: + if (user_input[CONF_USERNAME] != Gb.conf_tracking[CONF_USERNAME] + or user_input[CONF_PASSWORD] != Gb.conf_tracking[CONF_PASSWORD]): await self._log_into_icloud_account(user_input, self.step_id) + _traceha(f"logged inro {user_input=} {self.errors=}") + + _traceha(f"2fa {self.PyiCloud=} {self.PyiCloud.requires_2fa=} {self.errors=}") if (self.PyiCloud and self.PyiCloud.requires_2fa): + _traceha(f"going to reauth {user_input=} {self.errors=}") return await self.async_step_reauth(initial_display=True) - else: - return await self.async_step_menu() - elif action_item == 'enter_verification_code': - return await self.async_step_reauth(initial_display=True) + # Save the login username/password + if action_item == 'save' and self.errors == {}: + _traceha(f"going to update_config {user_input=} {self.errors=}") + self._update_configuration_file(user_input) + return await self.async_step_menu() - elif action_item == 'icloud_acct_reauth': - self.config_flow_updated_parms.update(['reauth']) - self.errors = {'base': 'icloud_reauth_scheduled'} + _traceha(f"at end redisplay {self.step_id} {user_input=} {self.errors=}") except Exception as err: log_exception(err) self.step_id = 'icloud_account' + _traceha(f"show form {self.step_id} {user_input=} {self.errors=}") return self.async_show_form(step_id=self.step_id, data_schema=self.form_schema(self.step_id), errors=self.errors) -#------------------------------------------------------------------------------------------- - def _unobscure_username_password(self, user_input): - ''' - Validate the iCloud Account credentials by logging into the iCloud Account via - pyicloud_ic3. This will set up the account access in the same manner as starting iCloud3. - The devices associated with FamShr and FmF are also retrieved so they are available - for selection in the Devices screen. - - Returns: - True - username or password was changed - ''' - original_username = self.username - original_password = self.password - - # Make sure the username and password are entered. - username = user_input[CONF_USERNAME].strip() - if username == self.obscure_username: - username = self.username - password = user_input[CONF_PASSWORD].strip() - if password == self.obscure_password: - password = self.password - - if username == '': - self.errors[CONF_USERNAME] = 'required_field' - self.errors_user_input[CONF_USERNAME] = '' - if password == '': - self.errors[CONF_PASSWORD] = 'required_field' - self.errors_user_input[CONF_PASSWORD] = '' - - if not self.errors: - self.username = user_input[CONF_USERNAME] = username - self.password = user_input[CONF_PASSWORD] = password - self.obscure_username = obscure_field(self.username) - self.obscure_password = obscure_field(self.password) - - return self.username != original_username or self.password != original_password - #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -1570,25 +1540,36 @@ async def _log_into_icloud_account(self, user_input, called_from_step_id): A dictionary with the devicename and identifiers used in the tracking configuration devices icloud_device parameter ''' + _traceha(f"{self.username=} {self.password=} {user_input=}") if CONF_USERNAME in user_input: - if user_input[CONF_USERNAME] != self.obscure_username: - self.username = user_input[CONF_USERNAME] - if user_input[CONF_PASSWORD] != self.obscure_password: - self.password = user_input[CONF_PASSWORD] + self.username = user_input[CONF_USERNAME].lower() + self.password = user_input[CONF_PASSWORD] else: self.username = Gb.conf_tracking[CONF_USERNAME] self.password = Gb.conf_tracking[CONF_PASSWORD] - self.obscure_username = obscure_field(self.username) - self.obscure_password = obscure_field(self.password) - self.called_from_step_id = called_from_step_id + # Already logged in with same username/password + if (self.PyiCloud + and self.username == self.PyiCloud.username + and self.password == self.PyiCloud.password): + # self.errors = {'base': 'icloud_already_logged_into'} + return + event_msg =(f"{EVLOG_NOTICE}Logging into iCloud Account with Configuration Wizard, " + f"{CRLF_DOT}New iCloud Account > {obscure_field(self.username)}, " + f"{CRLF_DOT}iCloud Account Currently Used > {obscure_field(Gb.username)}") + post_event(event_msg) + + verify_password = user_input[CONF_USERNAME] != Gb.conf_tracking[CONF_USERNAME] + self.called_from_step_id = called_from_step_id try: self.PyiCloud = await self.hass.async_add_executor_job( pyicloud_ic3_interface.create_PyiCloudService_secondary, self.username, self.password, - 'config_flow') + 'config_flow', + verify_password) + except (PyiCloudFailedLoginException) as err: _LOGGER.error(f"Error logging into iCloud service: {err}") @@ -1598,13 +1579,13 @@ async def _log_into_icloud_account(self, user_input, called_from_step_id): data_schema=self.form_schema(self.step_id), errors=self.errors) + except Exception as err: + log_exception(err) + + if self.PyiCloud.requires_2fa: return - if self.called_from_step_id == 'icloud_account': - user_input = {CONF_USERNAME: self.username, CONF_PASSWORD: self.password} - self._update_configuration_file(user_input) - self.errors = {'base': 'icloud_logged_into'} self.menu_msg = 'icloud_logged_into' @@ -1651,15 +1632,16 @@ async def async_step_reauth(self, user_input=None, errors=None, initial_display= ''' user_input = self._check_if_from_svc_call(user_input) - if (user_input is not None - and 'icloud3_service_call' in user_input): + if (user_input is not None and 'icloud3_service_call' in user_input): icloud3_service_call = True user_input = None else: icloud3_service_call = False # Will be from config_entries if came in from the HA settings on a red configuration screen + _traceha(f"in reauth 1637 {self.step_id=}{user_input=} {self.errors=}") self.step_id = config_entries.SOURCE_REAUTH + _traceha(f"in reauth 1639 {self.step_id=} {user_input=} {self.errors=}") self.errors = errors or {} if user_input is not None and user_input != {}: @@ -1709,8 +1691,6 @@ async def async_step_reauth(self, user_input=None, errors=None, initial_display= data_schema=self.form_schema(self.step_id), errors=self.errors) - # elif icloud3_service_call: - schema = vol.Schema({vol.Optional(CONF_VERIFICATION_CODE): selector.TextSelector(),}) @@ -2327,7 +2307,7 @@ async def _build_opt_famshr_devices_list(self): """ self.opt_famshr_text_by_fname_base = OPT_LIST_KEY_TEXT_NONE.copy() - if self.PyiCloud is None: + if self.PyiCloud is None or self.PyiCloud.FamilySharing is None: return _FamShr = self.PyiCloud.FamilySharing @@ -2345,7 +2325,7 @@ def _check_finish_v2v3conversion_for_famshr_fname(self): raw_model, model, model_display_name and device_id fields. ''' - if self.PyiCloud is None: + if self.PyiCloud is None or self.PyiCloud.FamilySharing is None: return _FamShr = self.PyiCloud.FamilySharing @@ -2585,6 +2565,9 @@ def _build_opt_zone_list(self): zone_data = entity_io.get_attributes(zone_entity) zone = zone_entity.replace('zone.', '') + # if zone_data[PASSIVE]: + # continue + if NAME in zone_data: ztitle = zone_data[NAME].title() else: @@ -2756,6 +2739,9 @@ def _sensor_form_identify_new_and_removed_sensors(self, user_input): new_sensors_list = [] remove_sensors_list = [] # base device sensors + if user_input[CONF_EXCLUDED_SENSORS] == []: + user_input[CONF_EXCLUDED_SENSORS] = ['None'] + for sensor_group, sensor_list in user_input.items(): if (sensor_group not in Gb.conf_sensors or user_input[sensor_group] == Gb.conf_sensors[sensor_group] @@ -2904,6 +2890,20 @@ def _menu_text_to_item(self, user_input, selection_list): return user_input, menu_item +#-------------------------------------------------------------------- + def _strip_spaces(self, user_input, parm_list=[]): + ''' + Remove leading or trailing spaces from items in the parameter list + + ''' + parm_list = [pname for pname, pvalue in user_input.items() + if type(pvalue) is str and pvalue != ''] + + for parm in parm_list: + user_input[parm] = user_input[parm].strip() + + return user_input + #-------------------------------------------------------------------- def _action_text_to_item(self, user_input): ''' @@ -3301,22 +3301,28 @@ def form_schema(self, step_id): self.opt_actions = ICLOUD_ACCOUNT_ACTIONS.copy() self.opt_actions.extend(OPT_ACTION_BASE) - self.obscure_username = self.username if self.show_username_password \ - else obscure_field(self.username) - self.obscure_password = self.password if self.show_username_password \ - else obscure_field(self.password) + if self.username or self.password: + obscure_username = obscure_field(self.username) or 'None' + obscure_password = obscure_field(self.password) or 'None' + username_password = f"({obscure_username}/{obscure_password})" + + logged_into_msg = self.opt_actions[LOG_IN_ICLOUD_ACCT_IDX].replace('Not Logged In', username_password) + self.opt_actions[LOG_IN_ICLOUD_ACCT_IDX] = logged_into_msg + + data_source_list = str_to_list(Gb.conf_tracking[CONF_DATA_SOURCE]) return vol.Schema({ - vol.Required(CONF_DATA_SOURCE, - default=self._option_parm_to_text(CONF_DATA_SOURCE, DATA_SOURCE_ITEMS_KEY_TEXT)): - selector.SelectSelector(selector.SelectSelectorConfig( - options=dict_value_to_list(DATA_SOURCE_ITEMS_KEY_TEXT), mode='dropdown')), + vol.Optional(CONF_DATA_SOURCE, + default=data_source_list): + cv.multi_select(DATA_SOURCE_ITEMS_KEY_TEXT2), vol.Optional(CONF_USERNAME, - default=self.obscure_username): - selector.TextSelector(), + default=self.username): + selector.TextSelector(selector.TextSelectorConfig( + type='password')), vol.Optional(CONF_PASSWORD, - default=self.obscure_password): - selector.TextSelector(), + default=self.password): + selector.TextSelector(selector.TextSelectorConfig( + type='password')), # vol.Required(CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX, # default=self._option_parm_to_text(CONF_ICLOUD_SERVER_ENDPOINT_SUFFIX, ICLOUD_SERVER_ENDPOINT_SUFFIX_ITEMS_KEY_TEXT)): # selector.SelectSelector( @@ -3324,8 +3330,8 @@ def form_schema(self, step_id): # options=dict_value_to_list(ICLOUD_SERVER_ENDPOINT_SUFFIX_ITEMS_KEY_TEXT), mode='dropdown')), vol.Required('opt_action', default=self._action_default_text('save')): - selector.SelectSelector( - selector.SelectSelectorConfig(options=self.opt_actions, mode='list')), + selector.SelectSelector(selector.SelectSelectorConfig( + options=self.opt_actions, mode='list')), }) #------------------------------------------------------------------------ @@ -3633,19 +3639,23 @@ def form_schema(self, step_id): }) #------------------------------------------------------------------------ - elif step_id == 'other_parms': - self.opt_actions = [OPT_ACTION_ITEMS_KEY_TEXT['change_device_order']] - self.opt_actions.extend(OPT_ACTION_BASE) - + elif step_id == 'tracking_parameters': + self.opt_actions = OPT_ACTION_BASE.copy() + _traceha(f"{self.opt_www_directory_list=}") + _traceha(f"{dict_value_to_list(self.opt_www_directory_list)=}") + _traceha(f"{self._parm_or_error_msg(CONF_EVLOG_CARD_DIRECTORY, conf_group=CF_PROFILE)=}") schema = vol.Schema({ - 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_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_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=500, step=5, unit_of_measurement='m')), + min=5, max=250, step=5, unit_of_measurement='m')), vol.Required(CONF_OLD_LOCATION_THRESHOLD, default=Gb.conf_general[CONF_OLD_LOCATION_THRESHOLD]): selector.NumberSelector(selector.NumberSelectorConfig( @@ -3654,9 +3664,6 @@ def form_schema(self, step_id): 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_DISTANCE_BETWEEN_DEVICES, - default=Gb.conf_general[CONF_DISTANCE_BETWEEN_DEVICES]): - selector.BooleanSelector(), vol.Required(CONF_MAX_INTERVAL, default=Gb.conf_general[CONF_MAX_INTERVAL]): selector.NumberSelector(selector.NumberSelectorConfig( @@ -3669,18 +3676,18 @@ def form_schema(self, step_id): default=Gb.conf_general[CONF_IOSAPP_ALIVE_INTERVAL]): selector.NumberSelector(selector.NumberSelectorConfig( min=15, max=240, step=15, 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=1000, unit_of_measurement='Km')), vol.Required(CONF_OFFLINE_INTERVAL, default=Gb.conf_general[CONF_OFFLINE_INTERVAL]): selector.NumberSelector(selector.NumberSelectorConfig( min=1, max=240, step=1, unit_of_measurement='minutes')), + vol.Required(CONF_TFZ_TRACKING_MAX_DISTANCE, + default=Gb.conf_general[CONF_TFZ_TRACKING_MAX_DISTANCE]): + selector.NumberSelector(selector.NumberSelectorConfig( + min=1, max=100, unit_of_measurement='Km')), vol.Required(CONF_TRAVEL_TIME_FACTOR, default=self._parm_or_error_msg(CONF_TRAVEL_TIME_FACTOR)): selector.NumberSelector(selector.NumberSelectorConfig( - min=.1, step=.1, max = 1)), + min=.1, max = 1, step=.1)), vol.Required(CONF_EVLOG_CARD_DIRECTORY, default=self._parm_or_error_msg(CONF_EVLOG_CARD_DIRECTORY, conf_group=CF_PROFILE)): selector.SelectSelector(selector.SelectSelectorConfig( @@ -3725,9 +3732,6 @@ def form_schema(self, step_id): vol.Optional(CONF_DISCARD_POOR_GPS_INZONE, default=Gb.conf_general[CONF_DISCARD_POOR_GPS_INZONE]): selector.BooleanSelector(), - vol.Optional(CONF_DISTANCE_BETWEEN_DEVICES, - default=Gb.conf_general[CONF_DISTANCE_BETWEEN_DEVICES]): - selector.BooleanSelector(), vol.Optional('opt_action', default=self._action_default_text('save')): @@ -3857,9 +3861,6 @@ def form_schema(self, step_id): Gb.conf_sensors[CONF_SENSORS_TRACKING_DISTANCE].append(HOME_DISTANCE) schema = vol.Schema({ - 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_DEVICE, default=Gb.conf_sensors[CONF_SENSORS_DEVICE]): cv.multi_select(CONF_SENSORS_DEVICE_KEY_TEXT), @@ -3872,18 +3873,21 @@ def form_schema(self, step_id): 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_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_TRACKING_OTHER, - default=Gb.conf_sensors[CONF_SENSORS_TRACKING_OTHER]): - cv.multi_select(CONF_SENSORS_TRACKING_OTHER_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( diff --git a/custom_components/icloud3/const.py b/custom_components/icloud3/const.py index 310676f..847710d 100644 --- a/custom_components/icloud3/const.py +++ b/custom_components/icloud3/const.py @@ -4,7 +4,7 @@ # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -VERSION = '3.0.0b13' +VERSION = '3.0.0b14' DOMAIN = 'icloud3' ICLOUD3 = 'iCloud3' @@ -249,10 +249,13 @@ CRLF = '⣇' #'
' CHECK_MARK = '✓ ' CIRCLE_X = '✪ ' +CIRCLE_X2 = '✪' DOT = '• ' DOT2 = '•' HDOT = '◦ ' HDOT2 = '◦' +LT = '<' +GT = '>' CRLF_DOT = f'{CRLF}{NBSP3}•{NBSP2}' CRLF_HDOT = f'{CRLF}{NBSP6}◦{NBSP2}' CRLF_CHK = f'{CRLF}{NBSP3}✓{NBSP}' @@ -290,12 +293,16 @@ #Zone field names NAME = 'name' FNAME = 'fname' +FNAME_HOME = 'fname/Home' TITLE = 'title' RADIUS = 'radius' NON_ZONE_ITEM_LIST = { 'not_home': 'Away', + 'Not_Home': 'Away', 'not_set': 'NotSet', + 'Not_Set': 'NotSet', 'stationary': 'Stationary', + 'Stationary': 'Stationary', 'unknown': 'Unknown'} #config_ic3.yaml parameter validation items @@ -615,11 +622,15 @@ TRAVEL_TIME_MIN = "travel_time_min" CONF_SENSORS_TRACKING_DISTANCE = 'tracking_distance' +ZONE_DISTANCE_M = 'meters_distance' +ZONE_DISTANCE_M_EDGE = 'meters_distance_to_zone_edge' ZONE_DISTANCE = "zone_distance" HOME_DISTANCE = "home_distance" DISTANCE_HOME = "distance_home" DIR_OF_TRAVEL = "dir_of_travel" MOVED_DISTANCE = "moved_distance" +MOVED_TIME_FROM = 'moved_from' +MOVED_TIME_TO = 'moved_to' CONF_SENSORS_TRACK_FROM_ZONES = 'track_from_zones' TFZ_ZONE_INFO = 'tfz_zone_info' @@ -771,15 +782,15 @@ RANGE_GENERAL_CONF = { # General Configuration Parameters + CONF_GPS_ACCURACY_THRESHOLD: [5, 250, 5, 'm'], + CONF_OLD_LOCATION_THRESHOLD: [1, 60], + CONF_OLD_LOCATION_ADJUSTMENT: [0, 60], CONF_MAX_INTERVAL: [15, 240], - CONF_OFFLINE_INTERVAL: [1, 240], CONF_EXIT_ZONE_INTERVAL: [.5, 10, .5], CONF_IOSAPP_ALIVE_INTERVAL: [15, 240], - CONF_OLD_LOCATION_THRESHOLD: [1, 60], - CONF_OLD_LOCATION_ADJUSTMENT: [0, 60], - CONF_GPS_ACCURACY_THRESHOLD: [5, 250, 5, 'm'], + CONF_OFFLINE_INTERVAL: [1, 240], + CONF_TFZ_TRACKING_MAX_DISTANCE: [1, 100, 1, 'km'], CONF_TRAVEL_TIME_FACTOR: [.1, 1, .1, ''], - CONF_TFZ_TRACKING_MAX_DISTANCE: [1, 1000, 1, 'km'], CONF_PASSTHRU_ZONE_TIME: [0, 5], # inZone Configuration Parameters @@ -833,7 +844,8 @@ CONF_SENSORS_ZONE: [ ZONE_NAME], CONF_SENSORS_OTHER: [], - CONF_EXCLUDED_SENSORS: [], + CONF_EXCLUDED_SENSORS: [ + NONE_FNAME], } DEFAULT_DATA_CONF = { diff --git a/custom_components/icloud3/const_sensor.py b/custom_components/icloud3/const_sensor.py index b2f04d2..acd39ef 100644 --- a/custom_components/icloud3/const_sensor.py +++ b/custom_components/icloud3/const_sensor.py @@ -11,8 +11,10 @@ ZONE_NAME, ZONE_FNAME, LAST_ZONE_NAME, LAST_ZONE_FNAME, INTERVAL, BATTERY_SOURCE, BATTERY, BATTERY_STATUS, - DISTANCE, ZONE_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD, HOME_DISTANCE, - MAX_DISTANCE, TRAVEL_TIME, TRAVEL_TIME_MIN, DIR_OF_TRAVEL, MOVED_DISTANCE, + 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, DIR_OF_TRAVEL, + MOVED_DISTANCE, MOVED_TIME_FROM, MOVED_TIME_TO, DEVICE_STATUS, LAST_UPDATE, LAST_UPDATE_DATETIME, NEXT_UPDATE, NEXT_UPDATE_DATETIME, @@ -29,7 +31,8 @@ ] SENSOR_LIST_TRACKING = [NEXT_UPDATE, LAST_UPDATE, LAST_LOCATED, TRAVEL_TIME, TRAVEL_TIME_MIN, MOVED_DISTANCE, DIR_OF_TRAVEL, - WAZE_DISTANCE, CALC_DISTANCE, ZONE_DISTANCE, HOME_DISTANCE, + WAZE_DISTANCE, CALC_DISTANCE, + ZONE_DISTANCE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, HOME_DISTANCE, ZONE, ZONE_FNAME, ZONE_NAME, ZONE_DATETIME, LAST_ZONE, LAST_ZONE_FNAME, LAST_ZONE_NAME, ] @@ -40,11 +43,11 @@ NEXT_UPDATE, LAST_UPDATE, LAST_LOCATED, TRAVEL_TIME, TRAVEL_TIME_MIN, ] -SENSOR_LIST_ZONE = [ZONE_DISTANCE, HOME_DISTANCE, +SENSOR_LIST_ZONE = [ZONE_DISTANCE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, HOME_DISTANCE, ZONE, ZONE_FNAME, ZONE_NAME, ZONE_DATETIME, LAST_ZONE, LAST_ZONE_FNAME, LAST_ZONE_NAME, ] -SENSOR_LIST_DISTANCE = [DISTANCE, ZONE_DISTANCE, HOME_DISTANCE, +SENSOR_LIST_DISTANCE = [DISTANCE, ZONE_DISTANCE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, HOME_DISTANCE, ] SENSOR_GROUPS = { 'battery': [BATTERY, BATTERY_STATUS], @@ -178,31 +181,34 @@ 'ZoneDistance', 'distance, km-mi', 'mdi:map-marker-distance', - [FROM_ZONE, MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD], + [FROM_ZONE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, + MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD], 0], DISTANCE: [ 'Distance', 'distance, km-mi', 'mdi:map-marker-distance', - [FROM_ZONE, MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD], + [FROM_ZONE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, + MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD], 0], HOME_DISTANCE: [ 'HomeDistance', 'distance, km-mi', 'mdi:map-marker-distance', - [FROM_ZONE, MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD], + [FROM_ZONE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, + MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD], '0'], DIR_OF_TRAVEL: [ 'Direction', 'text, title', 'mdi:compass-outline', - [FROM_ZONE, TRAVEL_TIME, TRAVEL_TIME_MIN, DISTANCE, MAX_DISTANCE], + [FROM_ZONE, TRAVEL_TIME, TRAVEL_TIME_MIN], BLANK_SENSOR_FIELD], MOVED_DISTANCE: [ 'MovedDistance', - 'distance, km-mi', + 'distance, km-mi, m-ft', 'mdi:map-marker-distance', - [FROM_ZONE, MAX_DISTANCE], + [MOVED_TIME_FROM, MOVED_TIME_TO], 0], ZONE_INFO: [ 'ZoneInfo', @@ -236,13 +242,15 @@ 'ZoneDistance', 'distance, km-mi', 'mdi:map-marker-distance', - [FROM_ZONE, DISTANCE, MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD], + [FROM_ZONE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, + MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD], '0 mi'], TFZ_ZONE_DISTANCE: [ 'ZoneDistance', 'distance, km-mi', 'mdi:map-marker-distance', - [FROM_ZONE, DISTANCE, MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD], + [FROM_ZONE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, + MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD], '0 mi'], TFZ_DIR_OF_TRAVEL: [ 'Direction', diff --git a/custom_components/icloud3/device.py b/custom_components/icloud3/device.py index 109e26c..047649b 100644 --- a/custom_components/icloud3/device.py +++ b/custom_components/icloud3/device.py @@ -4,10 +4,10 @@ # #------------------------------------------------------------------------------ from .global_variables import GlobalVariables as Gb -from .const import (DEVICE_TRACKER, DEVICE_TRACKER_DOT, +from .const import (DEVICE_TRACKER, DEVICE_TRACKER_DOT, CIRCLE_X2, LT, GT, NOTIFY, DISTANCE_TO_DEVICES, NEAR_DEVICE_DISTANCE, DISTANCE_TO_OTHER_DEVICES, DISTANCE_TO_OTHER_DEVICES_DATETIME, - HOME, NOT_HOME, NOT_SET, AWAY, UNKNOWN, DOT, RARROW, INFO_SEPARATOR, + HOME, HOME_FNAME, NOT_HOME, NOT_SET, AWAY, UNKNOWN, DOT, RARROW, INFO_SEPARATOR, PAUSED, TOWARDS, AWAY_FROM, INZONE, PAUSED_CAPS, RESUMING, DATETIME_ZERO, HHMMSS_ZERO, HIGH_INTEGER, TRACKING_NORMAL, TRACKING_PAUSED, TRACKING_RESUMED, @@ -28,8 +28,10 @@ ZONE, ZONE_DATETIME, LAST_ZONE, FROM_ZONE, LAST_ZONE_DATETIME, ZONE_NAME, ZONE_FNAME, LAST_ZONE_NAME, LAST_ZONE_FNAME, INTERVAL, BATTERY_SOURCE, BATTERY, BATTERY_LEVEL, BATTERY_STATUS, BATTERY_STATUS_FNAME, - ZONE_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD, HOME_DISTANCE, MAX_DISTANCE, - TRAVEL_TIME, TRAVEL_TIME_MIN, DIR_OF_TRAVEL, MOVED_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, DIR_OF_TRAVEL, + MOVED_DISTANCE, MOVED_TIME_FROM, MOVED_TIME_TO, DEVICE_STATUS, LOW_POWER_MODE, RAW_MODEL, MODEL, MODEL_DISPLAY_NAME, LAST_UPDATE, LAST_UPDATE_TIME, LAST_UPDATE_DATETIME, NEXT_UPDATE, NEXT_UPDATE_TIME, NEXT_UPDATE_DATETIME, @@ -73,6 +75,8 @@ class iCloud3_Device(TrackerEntity): def __init__(self, devicename, conf_device): self.conf_device = conf_device self.devicename = devicename + self.dr_device_id = '' # ha device_registry device_id + self.fname = devicename.title() self.StatZone = None self.stationary_zonename = (f"{self.devicename}_stationary") @@ -103,7 +107,6 @@ def initialize(self): 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.ha_device_id = '' # ha device_registry device_id self.model_display_name = DEVICE_TYPE_FNAME.get(self.device_type, self.device_type) # iPhone 14 Pro self.tracking_method = None self.tracking_status = TRACKING_NORMAL @@ -265,6 +268,8 @@ def initialize(self): self.loc_data_isold = False self.loc_data_ispoorgps = False self.loc_data_distance_moved = 0.0 + self.loc_data_moved_time_from = DATETIME_ZERO + self.loc_data_moved_time_to = DATETIME_ZERO self.last_loc_data_time_gps = f"{HHMMSS_ZERO}/0m" self.sensor_prefix = (f"sensor.{self.devicename}_") @@ -337,13 +342,17 @@ def initialize_sensors(self): self.sensors[TRAVEL_TIME] = 0 self.sensors[TRAVEL_TIME_MIN] = 0 self.sensors[ZONE_DISTANCE] = 0 - self.sensors[MAX_DISTANCE] = 0 + self.sensors[ZONE_DISTANCE_M] = 0 + self.sensors[ZONE_DISTANCE_M_EDGE] = 0 self.sensors[HOME_DISTANCE] = 0 + self.sensors[MAX_DISTANCE] = 0 self.sensors[WAZE_DISTANCE] = 0 self.sensors[WAZE_METHOD] = 0 self.sensors[CALC_DISTANCE] = 0 self.sensors[DIR_OF_TRAVEL] = NOT_SET self.sensors[MOVED_DISTANCE] = 0 + self.sensors[MOVED_TIME_FROM] = DATETIME_ZERO + self.sensors[MOVED_TIME_TO] = DATETIME_ZERO # Zone related items self.sensors[ZONE] = NOT_SET @@ -358,25 +367,23 @@ def initialize_sensors(self): # Initialize the Device sensors[xxx] value from the restore_state file if # the sensor is in the file. Otherwise, initialize to this value. This will preserve # non-tracking sensors across restarts - # for sensor, default_value in USE_RESTORE_STATE_VALUE_ON_STARTUP.items(): - # if (self.devicename in Gb.restore_state_devices - # and 'sensor' in Gb.restore_state_devices[self.devicename] - # and sensor in Gb.restore_state_devices[self.devicename]['sensors']): - # self.sensors[sensor] = Gb.restore_state_devices[self.devicename]['sensors'][sensor] - # else: - # self.sensors[sensor] = default_value self._restore_sensors_from_restore_state_file() + self._link_device_entities_sensor_device_tracker() +#------------------------------------------------------------------------------ + def _link_device_entities_sensor_device_tracker(self): # The DeviceTracker & Sensors entities are created before the Device object # using the configuration parameters. Cycle thru them now to set there - # self.Device variable to this evice object. This permits access to the - # sensors & attrs values. + # self.Device, device_id and area_id variables to this Device object. + # This permits access to the sensors & attrs values. # Link the DeviceTracker-Device objects if self.devicename in Gb.DeviceTrackers_by_devicename: self.DeviceTracker = Gb.DeviceTrackers_by_devicename[self.devicename] - self.DeviceTracker.Device = self + self.DeviceTracker.Device = self + self.DeviceTracker.device_id = Gb.dr_device_id_by_devicename[self.devicename] + self.DeviceTracker.area_id = Gb.dr_area_id_by_devicename[self.devicename] # Cycle through all sensors for this device. # Link the Sensor-Device objects to provide access the sensors dictionary @@ -428,7 +435,6 @@ def configure_device(self, conf_device): # Validate zone name and get Zone Object for a valid zone self.inzone_interval_secs = conf_device.get(CONF_INZONE_INTERVAL, 30) * 60 - # Get and validate track from zone config self.track_from_base_zone = conf_device.get(CONF_TRACK_FROM_BASE_ZONE, HOME) self.track_from_zones = conf_device.get(CONF_TRACK_FROM_ZONES, HOME).copy() @@ -504,7 +510,13 @@ def initialize_track_from_zones(self): f"Update `Track From Zone` parameter using the " f"iCloud3 Configurator") post_event(alert_msg) + continue + Zone = Gb.Zones_by_zone[zone] + if Zone.passive: + idx = self.track_from_zones.index(zone) + self.track_from_zones[idx] = f"{LT}{zone}-Passive>" + # self.track_from_zones[zone] = f"{CIRCLE_X2}{self.track_from_zones[zone]}" continue if zone in old_DeviceFmZones_by_zone: @@ -1153,18 +1165,23 @@ def calculate_distance_moved(self): self.loc_data_distance_moved = 0 else: self.loc_data_distance_moved = calc_distance_km(self.sensors[GPS], self.loc_data_gps) + self.loc_data_moved_time_from = self.sensors[LAST_LOCATED_DATETIME] + self.loc_data_moved_time_to = self.loc_data_datetime + #-------------------------------------------------------------------- def distance_m(self, to_latitude, to_longitude): to_gps = (to_latitude, to_longitude) distance = calc_distance_m(self.loc_data_gps, to_gps) - distance = 0 if distance < .2 else distance + distance = 0 if distance < .002 else distance + return distance def distance_km(self, to_latitude, to_longitude): to_gps = (to_latitude, to_longitude) distance = calc_distance_km(self.loc_data_gps, to_gps) - distance = 0 if distance < .002 else distance + distance = 0 if distance < .00002 else distance + return distance #-------------------------------------------------------------------- @@ -1344,8 +1361,8 @@ def format_info_msg(self): if self.tracking_method != self.dev_data_source.lower(): info_msg += (f"DataSource-{self.dev_data_source}, ") - if self.dev_data_battery_level > 0: - info_msg += f"Battery-{self.format_battery_level}, " + # if self.dev_data_battery_level > 0: + # info_msg += f"Battery-{self.format_battery_level}, " if self.is_gps_poor: info_msg += (f"Poor GPS Accuracy, Dist-{self.loc_data_gps_accuracy}m " @@ -1726,15 +1743,18 @@ def update_sensor_values_from_data_fields(self): self.sensors[GPS] = (self.loc_data_latitude, self.loc_data_longitude) self.sensors[LATITUDE] = self.loc_data_latitude self.sensors[LONGITUDE] = self.loc_data_longitude - self.sensors[GPS_ACCURACY] = m_to_ft_str(self.loc_data_gps_accuracy) - self.sensors[ALTITUDE] = m_to_ft_str(self.loc_data_altitude) - self.sensors[VERT_ACCURACY] = m_to_ft_str(self.loc_data_vert_accuracy) + self.sensors[GPS_ACCURACY] = self.loc_data_gps_accuracy + self.sensors[ALTITUDE] = self.loc_data_altitude + self.sensors[VERT_ACCURACY] = self.loc_data_vert_accuracy self.sensors[LOCATION_SOURCE] = self.dev_data_source self.sensors[TRIGGER] = self.trigger self.sensors[LAST_LOCATED_DATETIME]= self.loc_data_datetime self.sensors[LAST_LOCATED_TIME] = self.loc_data_time self.sensors[LAST_LOCATED] = self.loc_data_time self.sensors[DISTANCE_TO_DEVICES] = self.dist_apart_msg.rstrip(', ') + self.sensors[MOVED_DISTANCE] = self.loc_data_distance_moved + self.sensors[MOVED_TIME_FROM] = self.loc_data_moved_time_from + self.sensors[MOVED_TIME_TO] = self.loc_data_moved_time_to self.next_update_secs = self.DeviceFmZoneTracked.next_update_secs self.next_update_DeviceFmZone = self.DeviceFmZoneTracked @@ -1750,12 +1770,14 @@ def update_sensor_values_from_data_fields(self): self.sensors[TRAVEL_TIME_MIN] = self.DeviceFmZoneTracked.sensors[TRAVEL_TIME_MIN] self.sensors[TRAVEL_TIME] = self.DeviceFmZoneTracked.sensors[TRAVEL_TIME] self.sensors[ZONE_DISTANCE] = self.DeviceFmZoneTracked.sensors[ZONE_DISTANCE] + self.sensors[ZONE_DISTANCE_M] = self.DeviceFmZoneTracked.sensors[ZONE_DISTANCE_M] + self.sensors[ZONE_DISTANCE_M_EDGE] = self.DeviceFmZoneTracked.sensors[ZONE_DISTANCE_M_EDGE] self.sensors[MAX_DISTANCE] = self.DeviceFmZoneTracked.sensors[MAX_DISTANCE] self.sensors[WAZE_DISTANCE] = self.DeviceFmZoneTracked.sensors[WAZE_DISTANCE] self.sensors[WAZE_METHOD] = self.DeviceFmZoneTracked.sensors[WAZE_METHOD] self.sensors[CALC_DISTANCE] = self.DeviceFmZoneTracked.sensors[CALC_DISTANCE] - self.sensors[MOVED_DISTANCE] = self.DeviceFmZoneTracked.sensors[MOVED_DISTANCE] self.sensors[HOME_DISTANCE] = self.DeviceFmZoneHome.sensors[ZONE_DISTANCE] + # self.sensors[MOVED_DISTANCE] = self.DeviceFmZoneTracked.sensors[MOVED_DISTANCE] # If moving towards a tracked from zone, change the direction to 'To-[zonename]' # _trace(f"{self.devicename} {self.DeviceFmZoneTracked.from_zone} {self.DeviceFmZoneTracked.sensors[DIR_OF_TRAVEL]}") @@ -1794,6 +1816,8 @@ def update_sensor_values_from_data_fields(self): self.sensors[DEVICE_TRACKER_STATE_VALUE] = Zone.device_tracker_state self.sensors[DEVICE_TRACKER_STATE_ZONE] = Zone.zone + # if self.sensors[DEVICE_TRACKER_STATE_VALUE] == HOME_FNAME: + # self.sensors[DEVICE_TRACKER_STATE_VALUE] = HOME if Gb.is_stat_zone_used and self.StatZone: self.StatZone.update_stationary_zone_location() diff --git a/custom_components/icloud3/device_fm_zone.py b/custom_components/icloud3/device_fm_zone.py index ff35ea9..85acb06 100644 --- a/custom_components/icloud3/device_fm_zone.py +++ b/custom_components/icloud3/device_fm_zone.py @@ -21,8 +21,8 @@ DATETIME_ZERO, HHMMSS_ZERO, TOWARDS, AWAY_FROM, INTERVAL, - DISTANCE, ZONE_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, - WAZE_METHOD, MAX_DISTANCE, + DISTANCE, ZONE_DISTANCE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, + MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD, FROM_ZONE, ZONE_INFO, TRAVEL_TIME, TRAVEL_TIME_MIN, DIR_OF_TRAVEL, MOVED_DISTANCE, LAST_LOCATED, LAST_LOCATED_TIME, LAST_LOCATED_DATETIME, @@ -49,6 +49,7 @@ def __init__(self, Device, from_zone): self.from_zone = from_zone self.devicename_zone = (f"{self.devicename}:{from_zone}") self.from_zone_display_as = self.FromZone.display_as + self.from_zone_radius_m = self.FromZone.radius_m self.initialize() self.initialize_sensors() @@ -74,6 +75,7 @@ def initialize(self): self.waze_dist = 0 self.calc_dist = 0 self.zone_dist = 0 + self.zone_center_dist = 0 self.waze_results = None self.home_dist = calc_distance_km(Gb.HomeZone.gps, self.FromZone.gps) self.max_dist_km = 0 @@ -106,6 +108,8 @@ def initialize_sensors(self): self.sensors[DISTANCE] = 0 self.sensors[MAX_DISTANCE] = 0 self.sensors[ZONE_DISTANCE] = 0 + self.sensors[ZONE_DISTANCE_M] = 0 + self.sensors[ZONE_DISTANCE_M_EDGE] = 0 self.sensors[WAZE_DISTANCE] = 0 self.sensors[WAZE_METHOD] = '' self.sensors[CALC_DISTANCE] = 0 diff --git a/custom_components/icloud3/device_tracker.py b/custom_components/icloud3/device_tracker.py index eb6889c..31f6924 100644 --- a/custom_components/icloud3/device_tracker.py +++ b/custom_components/icloud3/device_tracker.py @@ -9,17 +9,17 @@ TRACK_DEVICE, INACTIVE_DEVICE, NAME, FNAME, PICTURE, - LATITUDE, LONGITUDE, + LATITUDE, LONGITUDE, GPS, DEVICE_TRACKER_STATE_VALUE, DEVICE_TRACKER_STATE_ZONE, LOCATION_SOURCE, TRIGGER, ZONE, ZONE_DATETIME, LAST_ZONE, FROM_ZONE, ZONE_FNAME, - BATTERY, + BATTERY, BATTERY_LEVEL, CALC_DISTANCE, WAZE_DISTANCE, HOME_DISTANCE, DEVICE_STATUS, LAST_UPDATE, LAST_UPDATE_DATETIME, NEXT_UPDATE, NEXT_UPDATE_DATETIME, LAST_LOCATED, LAST_LOCATED_DATETIME, - GPS_ACCURACY, + GPS_ACCURACY, ALTITUDE, VERT_ACCURACY, CONF_DEVICE_TYPE, CONF_RAW_MODEL, CONF_MODEL, CONF_MODEL_DISPLAY_NAME, CONF_TRACKING_MODE, CONF_IC3_DEVICENAME, @@ -34,12 +34,16 @@ log_info_msg, log_debug_msg, log_error_msg, log_exception, _trace, _traceha, ) from .support import start_ic3 +from .support import config_file from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker import device_trigger from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers import entity_registry as er, device_registry as dr +from homeassistant.const import (CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, CONF_EVENT, + CONF_PLATFORM, CONF_TYPE, CONF_ZONE, ) import logging # _LOGGER = logging.getLogger(__name__) @@ -68,7 +72,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e if Gb.conf_file_data == {}: Gb.hass = hass start_ic3.initialize_directory_filenames() - start_ic3.load_storage_icloud3_configuration_file() + config_file.load_storage_icloud3_configuration_file() NewDeviceTrackers = [] for conf_device in Gb.conf_devices: @@ -92,7 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e if NewDeviceTrackers is not []: async_add_entities(NewDeviceTrackers, True) - _get_ha_device_ids_from_device_registry(hass) + _get_dr_device_ids_from_device_registry(hass) except Exception as err: _LOGGER.exception(err) @@ -101,7 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e log_error_msg(log_msg) #------------------------------------------------------------------------------------------- -def _get_ha_device_ids_from_device_registry(hass): +def _get_dr_device_ids_from_device_registry(hass): ''' Cycle thru the ha device registry, extract the iCloud3 entries and associate the ha device_id with the ic3_devicename parameters @@ -110,19 +114,20 @@ def _get_ha_device_ids_from_device_registry(hass): ''' try: dev_reg = dr.async_get(hass) - Gb.ha_device_id_by_devicename = {} + Gb.dr_device_id_by_devicename = {} + Gb.dr_area_id_by_devicename = {} for device, device_entry in dev_reg.deleted_devices.items(): - _get_ha_device_id_from_device_entry(hass, device, device_entry) + _get_dr_device_id_from_device_entry(hass, device, device_entry) for device, device_entry in dev_reg.devices.items(): - _get_ha_device_id_from_device_entry(hass, device, device_entry) + _get_dr_device_id_from_device_entry(hass, device, device_entry) except Exception as err: log_exception(err) pass #------------------------------------------------------------------------------------------- -def _get_ha_device_id_from_device_entry(hass, device, device_entry): +def _get_dr_device_id_from_device_entry(hass, device, device_entry): ''' For each entry in the device registry, determine if it is an iCloud3 entry (iCloud3 is in the device_entry.identifiers field. If so, check the other items, determine if one is a @@ -134,7 +139,7 @@ def _get_ha_device_id_from_device_entry(hass, device, device_entry): configuration_url=None, connections=set(), disabled_by=None, entry_type=None, hw_version=None, id='306278916dc4a3b7bcc73b66dcd565b3', - identifiers={('iCloud3', 'gary_iphone', 'iPhone 14 Pro')}, manufacturer='Apple', + identifiers={('icloud3', 'gary_iphone')}, manufacturer='Apple', model='iPhone 14 Pro', name_by_user=None, name='Gary', suggested_area=None, sw_version=None, via_device_id=None, is_new=False) DeletedDeviceEntry(config_entries={'4ff81e71befd8994712d56eadf7232ae'}, @@ -144,20 +149,24 @@ def _get_ha_device_id_from_device_entry(hass, device, device_entry): try: de_identifiers = device_entry.identifiers for identifiers in de_identifiers: - if 'iCloud3' in identifiers: + if 'icloud3' in identifiers or 'iCloud3' in identifiers: + # Search thru identifier items for the devicename for item in identifiers: if item in Gb.conf_devicenames: - Gb.ha_device_id_by_devicename[item] = device_entry.id + Gb.dr_device_id_by_devicename[item] = device_entry.id + Gb.dr_area_id_by_devicename[item] = device_entry.area_id break - except: + except Exception as err: + log_exception(err) pass + #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> class iCloud3_DeviceTracker(TrackerEntity): """iCloud3 device_tracker entity definition.""" - def __init__(self, devicename, conf_device): + def __init__(self, devicename, conf_device, data=None): """Set up the iCloud3 device_tracker entity.""" try: @@ -165,6 +174,8 @@ def __init__(self, devicename, conf_device): self.devicename = devicename self.Device = None # Filled in after Device object has been created in start_ic3 self.entity_id = f"device_tracker.{devicename}" + self.dr_device_id = Gb.dr_device_id_by_devicename.get(self.devicename) + self.device_fname = conf_device[FNAME] self.device_type = conf_device[CONF_DEVICE_TYPE] self.tracking_mode = conf_device[CONF_TRACKING_MODE] @@ -177,9 +188,11 @@ def __init__(self, devicename, conf_device): except: self.default_value = BLANK_SENSOR_FIELD + self.triggers = None self.from_state_zonee = '' self.to_state_zone = '' self._state = self.default_value + self._data = data # Used by .see to issue change triggers self._attr_force_update = True self._unsub_dispatcher = None self._on_remove = [self.after_removal_cleanup] @@ -207,13 +220,14 @@ def name(self) -> str: @property def location_name(self): """Return the location name of the device.""" - try: - if self.state_value_not_set: - return self.default_value + return None + # try: + # if self.state_value_not_set: + # return self.default_value - return self._get_sensor_value(DEVICE_TRACKER_STATE_VALUE) - except: - return self.default_value + # return self._get_sensor_value(DEVICE_TRACKER_STATE_VALUE) + # except: + # return self.default_value @property def location_accuracy(self): @@ -223,12 +237,19 @@ def location_accuracy(self): @property def latitude(self): """Return latitude value of the device.""" - return str(self._get_sensor_value(LATITUDE, number=True)) + # return self.Device.sensors[LATITUDE] + return self._get_sensor_value(LATITUDE, number=True) @property def longitude(self): """Return longitude value of the device.""" - return str(self._get_sensor_value(LONGITUDE, number=True)) + # 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): @@ -259,6 +280,7 @@ def device_info(self): model = self.raw_model, name = f"{self.device_fname} ({self.devicename})", ) + #------------------------------------------------------------------------------------------- def _get_extra_attributes(self): ''' @@ -267,6 +289,7 @@ def _get_extra_attributes(self): try: extra_attrs = {} + extra_attrs[GPS] = f"({self.latitude}, {self.longitude})" extra_attrs['data_source'] = f"{self._get_sensor_value(LOCATION_SOURCE)} (iCloud3)" extra_attrs[DEVICE_STATUS] = self._get_sensor_value(DEVICE_STATUS) extra_attrs[NAME] = self._get_sensor_value(NAME) @@ -317,20 +340,21 @@ def _get_sensor_value(self, sensor, number=False): sensor_value = self.Device.sensors.get(sensor, None) if number and instr(sensor_value, ' '): - sensor_value = sensor_value.split(' ')[0] + sensor_value = float(sensor_value.split(' ')[0]) number = isnumber(sensor_value) - if number is False: + if number is False and type(sensor_value) is str: if sensor_value is None or sensor_value.strip() == '' or sensor_value == NOT_SET: sensor_value = BLANK_SENSOR_FIELD elif is_statzone(sensor_value): sensor_value = STATIONARY_FNAME except Exception as err: - log_exception(err) log_error_msg(f"►INTERNAL ERROR (Create device_tracker object-{err})") sensor_value = not_set_value + # Numeric fields are displayed in the attributes with 2-decimal places, Fix for gps + # return str(sensor_value) if sensor in [LATITUDE, LONGITUDE] else sensor_value return sensor_value #------------------------------------------------------------------------------------------- @@ -386,8 +410,7 @@ def state_value_not_set(self): def update_entity_attribute(self, new_fname=None): """ Update entity definition attributes """ - if (new_fname is None - or self.Device.ha_device_id == ''): + if new_fname is None or self.Device.dr_device_id == '': return self.device_fname = new_fname @@ -426,7 +449,7 @@ def update_entity_attribute(self, new_fname=None): kwargs['name_by_user'] = "" device_registry = dr.async_get(Gb.hass) - dr_entry = device_registry.async_update_device(self.Device.ha_device_id, **kwargs) + dr_entry = device_registry.async_update_device(self.Device.dr_device_id, **kwargs) #------------------------------------------------------------------------------------------- def remove_device_tracker(self): @@ -486,71 +509,23 @@ async def async_will_remove_from_hass(self): #------------------------------------------------------------------------------------------- def write_ha_device_tracker_state(self): """Update the entity's state.""" - try: - self.async_write_ha_state() - - # Save the updated zone (location_name) to see if a zone trigger enter/leave needs - # to be issued in device.write_ha_device_tracker_state, the function that calls - # this function - self.from_state_zone = self.to_state_zone - self.to_state_zone = self._get_sensor_value(DEVICE_TRACKER_STATE_ZONE) - - self._check_zone_change_trigger() - return + # Pass gps data to the HA .see which handles zone triggers + if self.Device and self.Device.sensors[LATITUDE] != 0: + data = {LATITUDE: self.Device.sensors[LATITUDE], + LONGITUDE: self.Device.sensors[LONGITUDE], + GPS: (self.Device.sensors[LATITUDE], self.Device.sensors[LONGITUDE]), + GPS_ACCURACY: self.Device.sensors[GPS_ACCURACY], + BATTERY: self.Device.sensors[BATTERY], + ALTITUDE: self.Device.sensors[ALTITUDE], + VERT_ACCURACY: self.Device.sensors[VERT_ACCURACY]} + self._data = data except Exception as err: log_exception(err) + self._data = None -#-------------------------------------------------------------------- - def _check_zone_change_trigger(self): - - # A one change triggers a zone enter/leave event. Determine the type of trigger and - # issue the appropriate one - - # zone --> not_home = leave zone - # zone --> zone = leave, enter - # not_home --> zone = enter zone - - return - - # if self.from_state_zone == self.to_state_zone: - # return - - # # Leaving a zone - # if self.from_state_zone != NOT_HOME: - # self._issue_zone_change_trigger('leave') - - # # entering a zone - # if self.to_state_value != NOT_HOME: - # self._issue_zone_change_trigger('enter') - -#-------------------------------------------------------------------- - # def _issue_zone_change_trigger(self, event): - # msg = (f"ZoneTrigger > {event}, {self.from_state_zone} --> {self.to_state_zone} ") - # _trace(self.devicename, msg) - # pass - - # description = (f"{self.entity_id}, {EVENT_DESCRIPTION[event]}, " - # f"{self._get_sensor_value(ZONE_FNAME)}") - # trigger_data = trigger_info["trigger_data"] - - # Gb.hass.async_run_hass_job( - # job, - # { - # "trigger": { - # **trigger_data, - # "platform": "zone", - # "entity_id": self.entity_id, - # "from_state": self.from_state_zone, - # "to_state": self.to_state_zone, - # "zone": self._get_sensor_value(ZONE), - # "event": event, - # "description": description, - # } - # }, - # to_s.context if to_s else None, - # ) + self.async_write_ha_state() #------------------------------------------------------------------------------------------- def __repr__(self): 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 88e6175..950e1a0 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,8 +22,8 @@ class iCloud3EventLogCard extends HTMLElement { } //--------------------------------------------------------------------------- setConfig(config) { - const version = "3.0.5" - const cardTitle = "iCloud3 Event Log v3" + const version = "3.0.6" + const cardTitle = "iCloud3 v3 - Event Log" const root = this.shadowRoot const hass = this._hass @@ -47,7 +47,7 @@ class iCloud3EventLogCard extends HTMLElement { const thisButtonId = document.createElement("div") thisButtonId.id = "thisButtonId" - thisButtonId.classList.add("themeTextColor") + // thisButtonId.classList.add("themeTextColor") thisButtonId.innerText = "setup" const logRecdCnt = document.createElement("div") @@ -296,6 +296,12 @@ class iCloud3EventLogCard extends HTMLElement { var btnActionOptAboutVersion = document.createTextNode(version) btnActionOptAbout.appendChild(btnActionOptAboutTxt) btnAction.appendChild(btnActionOptAbout) + + const btnRefresh = document.createElement('btnName') + btnRefresh.id = "btnRefresh" + btnRefresh.classList.add("btnRefresh") + btnRefresh.innerHTML = `` + //------------------------------------------------------------- // SVG Icons source -- https://heroicons.com/ const btnHelp = document.createElement('A') @@ -306,11 +312,6 @@ class iCloud3EventLogCard extends HTMLElement { btnHelp.setAttribute('target', '_blank') btnHelp.innerHTML = `` - const btnRefresh = document.createElement('btnName') - btnRefresh.id = "btnRefresh" - btnRefresh.classList.add("btnRefresh") - btnRefresh.innerHTML = `` - const btnIssues = document.createElement('A') btnIssues.id = "btnIssues" btnIssues.classList.add("btnIssues") @@ -319,7 +320,13 @@ class iCloud3EventLogCard extends HTMLElement { btnIssues.innerHTML = ` ` - + const btnStar = document.createElement('A') + btnStar.id = "btnStar" + btnStar.classList.add("btnStar") + btnStar.setAttribute('href', 'https://github.com/gcobb321/icloud3_v3/stargazers') + btnStar.setAttribute('target', '_blank') + btnStar.innerHTML = ` + ` const btnConfig = document.createElement('btnName') btnConfig.id = "btnConfig" @@ -330,6 +337,8 @@ class iCloud3EventLogCard extends HTMLElement { ` + + // Message Bar const statusBar = document.createElement("div") statusBar.id = "statusBar" @@ -517,6 +526,7 @@ class iCloud3EventLogCard extends HTMLElement { /*font-size: 2px;*/ visiblity: hidden; color: transparent; + /*color: red;*/ width: 5px; float: left; /*border: 1px solid green;*/ @@ -528,10 +538,11 @@ class iCloud3EventLogCard extends HTMLElement { } /* Store the theme's primary text color in the thisButtonId field */ - .themeTextColor { + /* .themeTextColor { color: var(--primary-text-color); background-color: var(--secondary-text-color); - } + visiblity: hidden; + } */ /* Message Bar setup - Name & Time below Name & Acion Buttons */ #statusBar { @@ -715,6 +726,21 @@ class iCloud3EventLogCard extends HTMLElement { background-color: transparent; box-shadow: transparent; } + #btnStar { + dispay: none; + /*display: inline-block;*/ + /*visibility: visible;*/ + color: var(--primary-text-color); + background-color: transparent; + margin: 0px 0px 1px 7px; + float: right; + } + + .btnStar { + border: 0px solid transparent; + background-color: transparent; + box-shadow: transparent; + } svg {stroke: #ff4d4d;} svg:hover {stroke: var(--primary-color);} @@ -855,6 +881,7 @@ class iCloud3EventLogCard extends HTMLElement { // Build title titleBar.appendChild(title) titleBar.appendChild(btnHelp) + titleBar.appendChild(btnStar) titleBar.appendChild(btnIssues) titleBar.appendChild(btnRefresh) @@ -892,7 +919,7 @@ class iCloud3EventLogCard extends HTMLElement { background.appendChild(titleBar) background.appendChild(utilityBar) background.appendChild(buttonBar) - background.appendChild(statusBar) + background.appendChild(statusBar) background.appendChild(tblEvlogContainer) background.appendChild(cssStyle) @@ -921,6 +948,8 @@ class iCloud3EventLogCard extends HTMLElement { // btnIssues.addEventListener("mousedown", event => { this._commandButtonPress("btnIssues"); }) btnIssues.addEventListener("mouseover", event => { this._btnClassMouseOver("btnIssues"); }) btnIssues.addEventListener("mouseout", event => { this._btnClassMouseOut("btnIssues"); }) + btnStar.addEventListener("mouseover", event => { this._btnClassMouseOver("btnStar"); }) + btnStar.addEventListener("mouseout", event => { this._btnClassMouseOut("btnStar"); }) // Add to root this._config = config @@ -1265,10 +1294,10 @@ class iCloud3EventLogCard extends HTMLElement { var tTrav = zoneDistTime[3] var tDist = zoneDistTime[4] - var maxStatZoneLength = 9 + var maxStatZoneLength = 10 if (iPhoneP) { tText = tText.replace('/icloud3', '... .../icloud3') - maxStatZoneLength = 9 + maxStatZoneLength = 10 if (tStat == 'stationary') { tStat = 'stationry' } if (tZone == 'stationary') { tZone = 'stationry' } if (tStat == 'Stationary') { tStat = 'Stationry' } @@ -1591,7 +1620,8 @@ class iCloud3EventLogCard extends HTMLElement { hdrCellWidthStr += cellWidthBCR + 'px,' tblEvlog.rows[0].cells[i].style.width = cellWidthBCR + 'px' } - //alert(hdrCellWidth.innerText = row + ',' + hdrCellWidthStr) + // alert(hdrCellWidth.innerText = row + ',' + hdrCellWidthStr) + // alert(hdrCellWidthStr) return } } @@ -1865,6 +1895,10 @@ class iCloud3EventLogCard extends HTMLElement { button.style.setProperty('border', '0px') this._displayInfoText("GitHub Issue") + } else if (buttonId == "btnStar") { + button.style.setProperty('border', '0px') + this._displayInfoText("Be an iCloud3 v3 Stargazer → Go, then click the ☆ (top-right corner)") + } else if (buttonId == "btnAction") { this._displayTimeMsgR("Show Action Command List") } @@ -1892,6 +1926,9 @@ class iCloud3EventLogCard extends HTMLElement { } else if (buttonId == 'btnIssues') { this._displayInfoText('') + + } else if (buttonId == 'btnStar') { + this._displayInfoText('') } if (devType.innerText == "") { @@ -2037,4 +2074,4 @@ class iCloud3EventLogCard extends HTMLElement { customElements.define('icloud3-event-log-card', iCloud3EventLogCard) -+' ' ++' ' \ No newline at end of file diff --git a/custom_components/icloud3/global_variables.py b/custom_components/icloud3/global_variables.py index d1f36a6..ee72f18 100644 --- a/custom_components/icloud3/global_variables.py +++ b/custom_components/icloud3/global_variables.py @@ -138,7 +138,8 @@ class GlobalVariables(object): Sensors_by_devicename = {} # HA sensor.[devicename]_[sensor_name]_[from_zone] objects Sensors_by_devicename_from_zone = {} # HA sensor.[devicename]_[sensor_name]_[from_zone] objects Sensor_EventLog = None # Event Log sensor object - ha_device_id_by_devicename = {} # HA device_registry device_id + dr_device_id_by_devicename = {} # HA device_registry device_id + dr_area_id_by_devicename = {} # HA device_registry area_id # System Wide variables control iCloud3 start/restart procedures diff --git a/custom_components/icloud3/helpers/common.py b/custom_components/icloud3/helpers/common.py index 6721f33..fd52c90 100644 --- a/custom_components/icloud3/helpers/common.py +++ b/custom_components/icloud3/helpers/common.py @@ -4,6 +4,7 @@ from ..const import (NOT_HOME, STATIONARY, CIRCLE_LETTERS_DARK, UNKNOWN, CRLF_DOT, CRLF, BATTERY_STATUS_FNAME,) from collections import OrderedDict +import os #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -39,6 +40,15 @@ def list_to_str(list_value, separator=None): else: return list_str +#-------------------------------------------------------------------- +def str_to_list(str_value): + ''' + Create a list of a comma separated strings + str_value - ('icloud,iosapp') + Return - ['icloud','iosapp'] + ''' + return list(str_value.split((','))) + #-------------------------------------------------------------------- def instr(string, substring): ''' @@ -80,7 +90,7 @@ def inlist(string, list_items): #-------------------------------------------------------------------- def round_to_zero(value): - if abs(value) < .001: value = 0 + if abs(value) < .0001: value = 0 return round(value, 8) #-------------------------------------------------------------------- @@ -130,7 +140,7 @@ def obscure_field(field): Return: The obscured field ''' - if field == '': + if field == '' or field is None: return '' if instr(field, '@'): @@ -140,12 +150,9 @@ def obscure_field(field): email_domain = field_parts[1] obscure_field = ( f"{email_name[0:2]}{'.'*(len(email_name)-2)}@" f"{email_domain[0:2]}{'.'*(len(email_domain)-2)}") - # obscure_field = ( f"{email_name[0:2]}{'.'*(len(email_name)-2)}{email_name[-2:]}@" - # f"{email_domain[0:2]}{'.'*(len(email_domain)-2)}{email_domain[-2:]}") return obscure_field obscure_field = (f"{field[0:2]}{'.'*(len(field)-2)}") - # obscure_field = (f"{field[0:2]}{'.'*(len(field)-2)}{field[-2:]}") return obscure_field #-------------------------------------------------------------------- @@ -177,3 +184,48 @@ def format_list(arg_list): #-------------------------------------------------------------------- def format_cnt(desc, n): return f", {desc}(#{n})" if n > 1 else '' + +#-------------------------------------------------------------------- +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 != "": + event_msg =(f"{file_desc} file > ({directory})" + f"{CRLF}•{file_msg}") + Gb.EvLog.post_event(event_msg) + + except Exception as err: + Gb.HALogger.exception(err) diff --git a/custom_components/icloud3/helpers/dist_util.py b/custom_components/icloud3/helpers/dist_util.py index 34d084c..34c6fdd 100644 --- a/custom_components/icloud3/helpers/dist_util.py +++ b/custom_components/icloud3/helpers/dist_util.py @@ -62,7 +62,8 @@ def calc_distance_m(from_gps, to_gps): return 0 distance_m = distance(from_lat, from_long, to_lat, to_long) - return round(round_to_zero(distance_m)) + return round_to_zero(distance_m) + #return round(round_to_zero(distance_m)) #-------------------------------------------------------------------- def format_km_to_mi(dist_km): @@ -75,28 +76,15 @@ def format_km_to_mi(dist_km): if Gb.um == 'mi': mi = dist_km * Gb.um_km_mi_factor - if mi > 20: + if mi > 10: return f"{mi:.1f} mi" if mi > 1: return f"{mi:.2f} mi" if round_to_zero(mi) == 0: return f"0 mi" - return f"{mi:.0f} mi" - - if dist_km >= 25: #25km/15mi - return f"{dist_km:.0f} km" - if dist_km >= 1: #1000m/.6mi - return f"{dist_km:.1f} km" - - return f"{dist_km*1000:.0f} m" + return f"{mi:.2f} mi" - - # if dist_km >= 25: #25km/15mi - # return f"{dist_km:.0f}km" - # if dist_km >= 1: #1000m/.6mi - # return f"{dist_km:.1f}km" - - # return f"{dist_km*1000:.0f}m" + return format_dist_km(dist_km) #-------------------------------------------------------------------- def format_dist_km(dist_km): @@ -105,12 +93,12 @@ def format_dist_km(dist_km): dist: Distance in kilometers ''' - if dist_km >= 25: #25km/15mi - return f"{dist_km:.0f}km" + if dist_km >= 10: #25km/15mi + return f"{dist_km:.1f} km" if dist_km >= 1: #1000m/.6mi - return f"{dist_km:.1f}km" + return f"{dist_km:.2f} km" - return f"{dist_km*1000:.0f}m" + return f"{dist_km*1000:.0f} m" #-------------------------------------------------------------------- def format_dist_m(dist_m): @@ -119,9 +107,9 @@ def format_dist_m(dist_m): dist: Distance in meters ''' - if dist_m >= 25000: #25km/15mi - return f"{dist_m/1000:.0f}km" + if dist_m >= 10000: #25km/15mi + return f"{dist_m/1000:.1f} km" if dist_m >= 1000: #1000m/.6mi - return f"{dist_m/1000:.1f}km" + return f"{dist_m/1000:.2f} km" - return f"{dist_m:.0f}m" + return f"{dist_m:.0f} m" diff --git a/custom_components/icloud3/helpers/messaging.py b/custom_components/icloud3/helpers/messaging.py index 095a2d9..d62eb17 100644 --- a/custom_components/icloud3/helpers/messaging.py +++ b/custom_components/icloud3/helpers/messaging.py @@ -31,7 +31,7 @@ import traceback from .common import obscure_field -FILTER_DATA_DICTS = ['data', 'userInfo', 'dsid', 'dsInfo', 'webservices', 'locations',] +FILTER_DATA_DICTS = ['items', 'userInfo', 'dsid', 'dsInfo', 'webservices', 'locations',] FILTER_DATA_LISTS = ['devices', 'content', 'followers', 'following', 'contactDetails',] FILTER_FIELDS = [ ICLOUD3_VERSION, AUTHENTICATED, @@ -52,11 +52,12 @@ 'deviceModel', 'rawDeviceModel', 'deviceDisplayName', 'modelDisplayName', 'deviceClass', 'isOld', 'isInaccurate', 'timeStamp', 'altitude', 'location', 'latitude', 'longitude', 'horizontalAccuracy', 'verticalAccuracy', - 'hsaVersion', 'hsaEnabled', 'hsaTrustedBrowser', 'locale', 'appleIdEntries', 'statusCode', + 'hsaVersion', 'hsaEnabled', 'hsaTrustedBrowser', 'hsaChallengeRequired', + 'locale', 'appleIdEntries', 'statusCode', 'familyEligible', 'findme', 'requestInfo', 'invitationSentToEmail', 'invitationAcceptedByEmail', 'invitationFromHandles', 'invitationFromEmail', 'invitationAcceptedHandles', - 'data', 'userInfo', 'dsid', 'dsInfo', 'webservices', 'locations', + 'items', 'userInfo', 'dsid', 'dsInfo', 'webservices', 'locations', 'devices', 'content', 'followers', 'following', 'contactDetails', ] @@ -73,7 +74,7 @@ def broadcast_info_msg(info_msg): return - Gb.broadcast_info_msg = f"{DOT}{info_msg}" + Gb.broadcast_info_msg = f"{info_msg}" try: for conf_device in Gb.conf_devices: @@ -424,6 +425,8 @@ def log_rawdata(title, rawdata, log_rawdata_flag=False): if Gb.log_rawdata_flag is False or rawdata is None: return + filtered_dicts = {} + filtered_lists = {} filtered_data = {} rawdata_data = {} @@ -433,24 +436,31 @@ def log_rawdata(title, rawdata, log_rawdata_flag=False): log_debug_msg(rawdata) return + rawdata_items = {k: v for k, v in rawdata['filter'].items() + if type(v) not in [dict, list]} rawdata_data['filter'] = {k: v for k, v in rawdata['filter'].items() if k in FILTER_FIELDS} except: + rawdata_items = {k: v for k, v in rawdata.items() + if type(v) not in [dict, list]} rawdata_data['filter'] = {k: v for k, v in rawdata.items() if k in FILTER_FIELDS} + rawdata_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) if filter_results: - filtered_data[f"◤{data_dict.upper()}◥ ({data_dict})"] = filter_results + filtered_dicts[f"▶{data_dict.upper()}◀ ({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_data[f"◤{data_list.upper()}◥ ({data_list})"] = filter_results + filtered_lists[f"▶{data_list.upper()}◀ ({data_list})"] = filter_results + filtered_data.update(filtered_dicts) + filtered_data.update(filtered_lists) try: log_msg = None if filtered_data: @@ -480,7 +490,6 @@ def _filter_data_dict(rawdata_data, data_dict_items): try: 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]}..." @@ -615,8 +624,8 @@ def _trace(devicename, log_text='+'): devicename, log_text = resolve_system_event_msg(devicename, log_text) log_text = log_text.replace('<', '《').replace('>', '》') - header_msg = _called_from() - post_event(devicename, f"^3^{header_msg} {log_text}") + called_from = _called_from() + post_event(devicename, f"^3^{called_from} {log_text}") #-------------------------------------------------------------------- def _traceha(log_text, v1='+++', v2='', v3='', v4='', v5=''): @@ -624,15 +633,18 @@ def _traceha(log_text, v1='+++', v2='', v3='', v4='', v5=''): Display a message or variable in the HA log file ''' try: + called_from = _called_from() if Gb.log_level == 'info' else '' if v1 == '+++': log_msg = '' else: log_msg = (f"|{v1}|-|{v2}|-|{v3}|-|{v4}|-|{v5}|") - if log_text in Gb.Devices_by_devicename: - trace_msg = (f"{Gb.trace_prefix}{log_text} ::: TRACE ::: {log_msg}") + if type(log_text) is str and log_text in Gb.Devices_by_devicename: + trace_msg = (f"{called_from}{log_text} ::: TRACE ::: {log_msg}") + # trace_msg = (f"{Gb.trace_prefix}{log_text} ::: TRACE ::: {log_msg}") else: - trace_msg = (f"{Gb.trace_prefix} ::: TRACE ::: {log_text}, {log_msg}") + trace_msg = (f"{called_from} ::: TRACE ::: {log_text}, {log_msg}") + # trace_msg = (f"{Gb.trace_prefix} ::: TRACE ::: {log_text}, {log_msg}") Gb.HALogger.info(trace_msg) write_ic3_debug_log_recd(trace_msg, force_write=True) diff --git a/custom_components/icloud3/helpers/time_util.py b/custom_components/icloud3/helpers/time_util.py index fcde4e8..9f074f2 100644 --- a/custom_components/icloud3/helpers/time_util.py +++ b/custom_components/icloud3/helpers/time_util.py @@ -22,9 +22,18 @@ def time_now(): #-------------------------------------------------------------------- def time_now_secs(): ''' Return the current timestamp seconds ''' + return int(time.time()) +#-------------------------------------------------------------------- +def time_secs(): + ''' Return the current timestamp seconds ''' return int(time.time()) +#-------------------------------------------------------------------- +def time_msecs(): + ''' Return the current timestamp milli-seconds ''' + return time.time() + #-------------------------------------------------------------------- def datetime_now(): ''' Return now in MM/DD/YYYY hh:mm:ss format''' diff --git a/custom_components/icloud3/icloud3_main.py b/custom_components/icloud3/icloud3_main.py index ce46557..d870718 100644 --- a/custom_components/icloud3/icloud3_main.py +++ b/custom_components/icloud3/icloud3_main.py @@ -76,6 +76,12 @@ secs_to_time_age_str, ) from .helpers.dist_util import (m_to_ft_str, calc_distance_km, format_dist_km, format_dist_m, ) +# zone_data constants - Used in the select_zone function +ZD_DIST_M = 0 +ZD_ZONE = 1 +ZD_NAME = 2 +ZD_RADIUS = 3 +ZD_DISPLAY_AS = 4 #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> class iCloud3: @@ -1109,112 +1115,58 @@ def _select_zone(self, Device, latitude=None, longitude=None): # for Zone in Gb.Zones # if (Zone.passive is False and Zone.radius_m > 1)] - zones_data = [[Zone.distance_m(latitude, longitude), Zone, Zone.zone, Zone.radius_m, Zone.display_as] + # ZD_DIST_M = 0 + # ZD_ZONE = 1 + # ZD_NAME = 2 + # ZD_RADIUS = 3 + # ZD_DISPLAY_AS = 4 + + # Get a list of all the zones, their distance, size and display_as + zones_data = [[Zone.distance_m(latitude, longitude), Zone, Zone.zone, + Zone.radius_m, Zone.display_as] + for Zone in Gb.Zones + if (Zone.passive is False + and Zone.radius_m > 1 + and Device.is_my_stat_zone(Zone))] + + # Verify that the stat_zone was not left without an exit trigger. Reset it if it was. + stat_zone = [zone_data_selected[ZD_ZONE] + for zone_data in zones_data + if is_statzone(zone_data[ZD_NAME]) + and zone_data[ZD_DIST_M] > zone_data[ZD_RADIUS]] + + if stat_zone != []: + Device.stationary_zone_update_control = STAT_ZONE_MOVE_TO_BASE + Device.StatZone.update_stationary_zone_location() + + zones_data = [[Zone.distance_m(latitude, longitude), Zone, Zone.zone, + Zone.radius_m, Zone.display_as] for Zone in Gb.Zones if (Zone.passive is False and Zone.radius_m > 1 and Device.is_my_stat_zone(Zone))] - # Select zones the device is in - inzone_zones = [zone_data for zone_data in zones_data if zone_data [0] <= zone_data[3]] + # Select all the zones the device is in + inzone_zones = [zone_data + for zone_data in zones_data + if zone_data[ZD_DIST_M] <= zone_data[ZD_RADIUS]] # Get the smallest zone for zone_data in inzone_zones: - if zone_data[3] < zone_data_selected[3]: + if zone_data[ZD_RADIUS] < zone_data_selected[ZD_RADIUS]: zone_data_selected = zone_data - ZoneSelected = zone_data_selected[1] - zone_selected = zone_data_selected[2] - zone_selected_dist_m = zone_data_selected[0] + + ZoneSelected = zone_data_selected[ZD_ZONE] + zone_selected = zone_data_selected[ZD_NAME] + zone_selected_dist_m = zone_data_selected[ZD_DIST_M] # diisplay_as = Device.StatZone.dislay_as if is_statzone(zone_selected) else zone_data_selected[1] - zones_distance_list = [f"{int(zone_data[0]):08}| {zone_data[4]}-{format_dist_m(zone_data[0])}" + zones_distance_list = [f"{int(zone_data[ZD_DIST_M]):08}| {zone_data[4]}-{format_dist_m(zone_data[ZD_DIST_M])}" for zone_data in zones_data] return ZoneSelected, zone_selected, zone_selected_dist_m, zones_distance_list - def x_select_zone(self, Device, latitude=None, longitude=None): - ''' - Cycle thru the zones and see if the Device is in a zone (or it's stationary zone). - - Parameters: - latitude, longitude - Override the normally used Device.loc_data_lat/long when - calculating the zone distance from the current location - Return: - ZoneSelected - Zone selected object or None - zone_selected - zone entity name - zone_selected_distance_m - distance to the zone (meters) - zones_distance_list - list of zone info [distance_m|zoneName-distance] - ''' - - if latitude is None: - latitude = Device.loc_data_latitude - longitude = Device.loc_data_longitude - - # iOSApp will trigger Enter Region when the edge of the devices's location area (100m) - # touches or is inside the zones radius. If enterina a zone, increase the zone's radius - # so the device will be in the zone when it is actually just outside of it. - # But don't do this for a Stationary Zone. - # if Close to zone, add 50m for iOS App Extra area - zone_radius_iosapp_enter_adjustment_m = 0 - # if (Device.is_data_source_IOSAPP - # and instr(Device.trigger.lower(), 'exit') is False): - # zone_radius_iosapp_enter_adjustment_m = 50 - - # Exit if no location data is available - if Device.no_location_data: - ZoneSelected = Gb.Zones_by_zone['unknown'] - zone_selected = 'unknown' - zone_selected_dist_m = 0 - # Device.loc_data_zone = zone_selected - zones_msg = f"Zone > Unknown, GPS-{Device.loc_data_fgps}" - post_event(Device.devicename, zones_msg) - return ZoneSelected, zone_selected, 0, [] - - zone_selected_dist_m = HIGH_INTEGER - zone_selected_radius_m = HIGH_INTEGER - ZoneSelected = None - zone_selected = None - zones_distance_list = [] - - - for Zone in Gb.Zones: - if Zone.passive: - continue - - zone = Zone.zone - zone_radius_m = Zone.radius_m - zone_dist_m = Zone.distance_m(latitude, longitude) - - # if (.100 < zone_dist_m <= .150 - # and zone_radius_iosapp_enter_adjustment_m > 0): - # zone_radius_m += zone_radius_iosapp_enter_adjustment_m - - #Skip another device's stationary zone or if at base location - if (is_statzone(zone) and instr(zone, Device.devicename) is False): - continue - - #Bypass stationary zone at base and Pseudo Zones (not_home, not_set, etc) - elif zone_radius_m <= 1: - continue - - #Do not check Stat Zone if radius=1 (at base loc) but include in log_msg - in_zone_flag = zone_dist_m <= zone_radius_m - closer_zone_flag = ZoneSelected is None or zone_dist_m < zone_selected_dist_m - smaller_zone_flag = zone_dist_m == zone_selected_dist_m and zone_radius_m <= zone_selected_radius_m - # smaller_zone_flag = closer_zone_flag and ZoneSelected and zone_radius_m <= zone_selected_radius_m - - if (in_zone_flag - and (closer_zone_flag or smaller_zone_flag)): - ZoneSelected = Zone - zone_selected = zone - zone_selected_dist_m = zone_dist_m - zone_selected_radius_m = ZoneSelected.radius_m + zone_radius_iosapp_enter_adjustment_m - - ThisZone = Device.StatZone if is_statzone(zone_selected) else Zone - zones_distance_list.append(f"{int(zone_dist_m):08}| {ThisZone.display_as}-{format_dist_m(zone_dist_m)}") - - return ZoneSelected, zone_selected, zone_selected_dist_m, zones_distance_list #-------------------------------------------------------------------- def _check_statzone_timer_expired(self, Device): @@ -1224,7 +1176,6 @@ def _check_statzone_timer_expired(self, Device): Reset the timer if the Device has moved further than the distance limit Move Device into the Stat Zone if it has not moved further than the limit ''' - calc_dist_last_poll_moved_km = calc_distance_km(Device.sensors[GPS], Device.loc_data_gps) Device.StatZone.update_distance_moved(calc_dist_last_poll_moved_km) diff --git a/custom_components/icloud3/manifest.json b/custom_components/icloud3/manifest.json index eb82002..c54603d 100644 --- a/custom_components/icloud3/manifest.json +++ b/custom_components/icloud3/manifest.json @@ -1,6 +1,6 @@ { "domain": "icloud3", - "name": "iCloud3 Device Tracker, Version 3", + "name": "iCloud3 v3", "documentation": "https://gcobb321.github.io/icloud3_v3/#/", "issue_tracker": "https://github.com/gcobb321/icloud3_v3/issues", "dependencies": [], diff --git a/custom_components/icloud3/sensor copy.py b/custom_components/icloud3/sensor copy.py new file mode 100644 index 0000000..7e7c4f7 --- /dev/null +++ b/custom_components/icloud3/sensor copy.py @@ -0,0 +1,1312 @@ +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# This module handles all activities related to updating a device's sensors. It contains +# the following modules: +# TrackFromZones - iCloud3 creates an object for each device/zone +# with the tracking data fields. +# +# The primary methods are: +# determine_interval - Determines the polling interval, update times, +# location data, etc for the device based on the distance from +# the zone. +# determine_interval_after_error - Determines the interval when the +# location data is to be discarded due to poor GPS, it is old or +# some other error occurs. +# +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + +from .global_variables import GlobalVariables as Gb +from .const import (DOMAIN, VERSION, + SENSOR_EVENT_LOG_NAME, + SENSOR_WAZEHIST_TRACK_NAME, + HOME, NOT_SET, NOT_SET_FNAME, NONE_FNAME, + DATETIME_ZERO, HHMMSS_ZERO, + BLANK_SENSOR_FIELD, DOT, UM_FNAME, + TRACK_DEVICE, MONITOR_DEVICE, INACTIVE_DEVICE, + DISTANCE_TO_OTHER_DEVICES, + DIR_OF_TRAVEL,ICON_DIR_OF_TRAVEL, TOWARDS, AWAY_FROM, INZONE, + NAME, FNAME, BADGE, + ZONE, ZONE_INFO, + BATTERY, BATTERY_STATUS, BATTERY_SOURCE, + ZONE_DISTANCE, + DISTANCE_TO_OTHER_DEVICES_DATETIME, + CONF_TRACK_FROM_ZONES, + CONF_IC3_DEVICENAME, CONF_MODEL, CONF_RAW_MODEL, CONF_FNAME, + CONF_FAMSHR_DEVICENAME, CONF_IOSAPP_DEVICE, + CONF_TRACKING_MODE, + ) +from .const_sensor import (SENSOR_DEFINITION, SENSOR_GROUPS, + SENSOR_FNAME, SENSOR_TYPE, SENSOR_ICON, + SENSOR_ATTRS, SENSOR_DEFAULT, SENSOR_LIST_DISTANCE, ) + +from .helpers.common import (instr, round_to_zero, ) +from .helpers.messaging import (log_info_msg, log_debug_msg, log_error_msg, log_exception, + _trace, _traceha, ) +from .helpers.time_util import (time_to_12hrtime, time_remove_am_pm, secs_to_time_str, mins_to_time_str, + time_now_secs, datetime_now, ) +from .helpers.dist_util import (km_to_mi, ) +from .helpers import entity_io +from .support import start_ic3 + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.helpers import entity_registry as er, device_registry as dr + +import homeassistant.util.dt as dt_util +# from homeassistant.helpers.entity import Entity + +import logging +# _LOGGER = logging.getLogger(__name__) +_LOGGER = logging.getLogger(f"icloud3") +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities): + '''Set up iCloud3 sensors''' + + # Save the hass `add_entities` call object for use in config_flow for adding new sensors + Gb.async_add_entities_sensor = async_add_entities + + try: + if Gb.conf_file_data == {}: + Gb.hass = hass + start_ic3.initialize_directory_filenames() + start_ic3.load_storage_icloud3_configuration_file() + + NewSensors = [] + if Gb.EvLogSensor is None: + Gb.EvLogSensor = Sensor_EventLog('iCloud3 Event Log', SENSOR_EVENT_LOG_NAME) + if Gb.EvLogSensor: + NewSensors.append(Gb.EvLogSensor) + else: + log_error_msg("Error setting up Event Log Sensor") + + if Gb.WazeHistTrackSensor is None: + Gb.WazeHistTrackSensor = Sensor_WazeHistTrack('iCloud3 Waze History Track', SENSOR_WAZEHIST_TRACK_NAME) + if Gb.WazeHistTrackSensor: + NewSensors.append(Gb.WazeHistTrackSensor) + else: + log_error_msg("Error setting up Waze History Track Sensor") + + # Create the selected sensors for each devicename + # Cycle through each device being tracked or monitored and create it's sensors + for conf_device in Gb.conf_devices: + devicename = conf_device[CONF_IC3_DEVICENAME] + + if conf_device[CONF_TRACKING_MODE] == INACTIVE_DEVICE: + continue + + if conf_device[CONF_TRACKING_MODE] == TRACK_DEVICE: + NewSensors.extend(create_tracked_device_sensors(devicename, conf_device)) + + elif conf_device[CONF_TRACKING_MODE] == MONITOR_DEVICE: + NewSensors.extend(create_monitored_device_sensors(devicename, conf_device)) + + # Set the total count of the sensors that will be created + if Gb.sensors_cnt == 0: + excluded_sensors_list = _excluded_sensors_list() + Gb.sensors_cnt = len(NewSensors) + log_info_msg(f'Sensor entities created: {len(NewSensors)}') + log_info_msg(f'Sensor entities excluded: {len(excluded_sensors_list)}') + + if NewSensors != []: + async_add_entities(NewSensors, True) + + except Exception as err: + log_exception(err) + log_msg = (f"►INTERNAL ERROR (UpdtSensorUpdate-{err})") + log_error_msg(log_msg) + +#-------------------------------------------------------------------- +def create_tracked_device_sensors(devicename, conf_device, new_sensors_list=None): + ''' + Add icloud3 sensors that have been selected via config_flow and + arein the Gb.conf_sensors for each device + ''' + try: + NewSensors = [] + + if new_sensors_list is None: + new_sensors_list = [] + + for sensor_group, sensor_list in Gb.conf_sensors.items(): + if sensor_group != 'monitored_devices': + new_sensors_list.extend(sensor_list) + + # The sensor group is a group of sensors combined under one conf_sensor item + # Build sensors to be created from the the sensor or the sensor's group + sensors_list = [] + for sensor in new_sensors_list: + if sensor in SENSOR_GROUPS: + sensors_list.extend(SENSOR_GROUPS[sensor]) + else: + sensors_list.append(sensor) + + if 'last_zone' in sensors_list: + if 'zone' not in sensors_list: sensors_list.pop('last_zone') + if 'zone_name' in sensors_list: sensors_list.append('last_zone_name') + if 'zone_fname' in sensors_list: sensors_list.append('last_zone_fname') + + NewSensors.extend(_create_device_sensors(devicename, conf_device, sensors_list)) + NewSensors.extend(_create_track_from_zone_sensors(devicename, conf_device, sensors_list)) + + return NewSensors + + except Exception as err: + log_exception(err) + log_msg = (f"►INTERNAL ERROR (UpdtSensorUpdate-{err})") + log_error_msg(log_msg) + +#-------------------------------------------------------------------- +def _create_device_sensors(devicename, conf_device, sensors_list): + + NewSensors = [] + devicename_sensors = Gb.Sensors_by_devicename.get(devicename, {}) + excluded_sensors_list = _excluded_sensors_list() + + # Cycle through the sensor definition names in the list of selected sensors, + # Get the sensor entity name and create the sensor.[ic3_devicename]_[sensor_name] entity + # The sensor_def name is the conf_sensor name set up in the Sensor_definition table. + # The table contains the actual ha sensor entity name. That permits support for track-from-zone + # suffixes. + + for sensor in sensors_list: + if (sensor not in SENSOR_DEFINITION + or sensor.startswith('tfz_')): + continue + if (instr(sensor, BATTERY) + and conf_device[CONF_FAMSHR_DEVICENAME] == NONE_FNAME + and conf_device[CONF_IOSAPP_DEVICE] == NONE_FNAME): + continue + + devicename_sensor = f"{devicename}_{sensor}" + if devicename_sensor in excluded_sensors_list: + log_debug_msg(f"Sensor entity excluded: sensor.{devicename_sensor}") + continue + + Sensor = None + if sensor in devicename_sensors: + # Sensor object might exist, use it to recreate the sensor entity + _Sensor = devicename_sensors[sensor] + if _Sensor.entity_removed_flag: + Sensor = _Sensor + log_info_msg(f"Reused Existing sensor.icloud3 entity: {Sensor.entity_id}") + Sensor.entity_removed_flag = False + + else: + Sensor = _create_sensor_by_type(devicename, sensor, conf_device) + + if Sensor: + devicename_sensors[sensor] = Sensor + NewSensors.append(Sensor) + + Gb.Sensors_by_devicename[devicename] = devicename_sensors + + return NewSensors + +#-------------------------------------------------------------------- +def _create_track_from_zone_sensors(devicename, conf_device, sensors_list): + + if conf_device[CONF_TRACK_FROM_ZONES] == [HOME]: + return [] + + ha_zones, zone_entity_data = entity_io.get_entity_registry_data(platform=ZONE) + devicename_from_zone_sensors = Gb.Sensors_by_devicename_from_zone.get(devicename, {}) + excluded_sensors_list = _excluded_sensors_list() + + NewSensors = [] + for sensor in sensors_list: + if (sensor not in SENSOR_DEFINITION + or sensor.startswith('tfz_') is False): + continue + + sensor = sensor.replace('tfz_', '') + + # Track_from_zone related sensors + if (conf_device[CONF_TRACK_FROM_ZONES] == [] + or HOME not in conf_device[CONF_TRACK_FROM_ZONES]): + conf_device[CONF_TRACK_FROM_ZONES].append(HOME) + + for from_zone in conf_device[CONF_TRACK_FROM_ZONES]: + if from_zone not in ha_zones: + continue + + Sensor = None + sensor_zone = f"{sensor}_{from_zone}" + devicename_sensor_zone = f"{devicename}_{sensor}_{from_zone}" + + if devicename_sensor_zone in excluded_sensors_list: + # Gb.sensors_created_cnt += 1 + log_debug_msg(f"Sensor entity excluded: sensor.{devicename_sensor_zone}") + continue + + if sensor_zone in devicename_from_zone_sensors: + continue + + # Sensor object might exist, use it to recreate the sensor entity + if sensor_zone in devicename_from_zone_sensors: + _Sensor = devicename_from_zone_sensors[sensor_zone] + if _Sensor.entity_removed_flag: + Sensor = _Sensor + log_info_msg(f"Reused Existing sensor.icloud3 entity: {Sensor.entity_id}") + Sensor.entity_removed_flag = False + + Sensor = _create_sensor_by_type(devicename, sensor, conf_device, from_zone) + + if Sensor: + devicename_from_zone_sensors[sensor_zone] = Sensor + NewSensors.append(Sensor) + + Gb.Sensors_by_devicename_from_zone[devicename] = devicename_from_zone_sensors + + return NewSensors + +#-------------------------------------------------------------------- +def create_monitored_device_sensors(devicename, conf_device, new_sensors_list=None): + ''' + Add icloud3 sensors that have been selected via config_flow and + arein the Gb.conf_sensors for each device + ''' + + try: + excluded_sensors_list = _excluded_sensors_list() + NewSensors = [] + if new_sensors_list is None: + new_sensors_list = [] + new_sensors_list.extend(Gb.conf_sensors['monitored_devices']) + + # The sensor group is a group of sensors combined under one conf_sensor item + # Build sensors to be created from the the sensor or the sensor's group + sensors_list = [] + for sensor in new_sensors_list: + if sensor in SENSOR_GROUPS: + sensors_list.extend(SENSOR_GROUPS[sensor]) + else: + sensors_list.append(sensor) + + devicename_sensors = Gb.Sensors_by_devicename.get(devicename, {}) + + # Cycle through the sensor definition names in the list of selected sensors, + # Get the sensor entity name and create the sensor.[ic3_devicename]_[sensor_name] entity + # The sensor_def name is the conf_sensor name set up in the Sensor_definition table. + # The table contains the actual ha sensor entity name. That permits support for track-from-zone + # suffixes. + for sensor in sensors_list: + Sensor = None + + devicename_sensor = f"{devicename}_{sensor}" + if devicename_sensor in excluded_sensors_list: + # Gb.sensors_created_cnt += 1 + log_debug_msg(f"Sensor entity excluded: sensor.{devicename_sensor}") + continue + + # Sensor object might exist, use it to recreate the sensor entity + if sensor in devicename_sensors: + _Sensor = devicename_sensors[sensor] + if _Sensor.entity_removed_flag: + Sensor = _Sensor + log_info_msg(f"Reused Existing sensor.icloud3 entity: {Sensor.entity_id}") + Sensor.entity_removed_flag = False + else: + Sensor = _create_sensor_by_type(devicename, sensor, conf_device) + + if Sensor: + devicename_sensors[sensor] = Sensor + NewSensors.append(Sensor) + + Gb.Sensors_by_devicename[devicename] = devicename_sensors + Gb.Sensors_by_devicename_from_zone[devicename] = {} + + return NewSensors + + except Exception as err: + log_exception(err) + log_msg = (f"►INTERNAL ERROR (UpdtSensorUpdate-{err})") + log_error_msg(log_msg) + +#-------------------------------------------------------------------- +def _excluded_sensors_list(): + return [sensor_fname.split('(')[1][:-1] + for sensor_fname in Gb.conf_sensors['excluded_sensors'] + if instr(sensor_fname, '(')] +#-------------------------------------------------------------------- +def _strip_sensor_def_table_item_prefix(sensor): + ''' + Remove the prefix for sensor names in the sensor definition table for + the 'track_from_zone (tfz_) and 'monitor_device` (md_) sensors. + ''' + return sensor.replace('tfz_', '').replace('md_', '') + +#-------------------------------------------------------------------- +def _create_sensor_by_type(devicename, sensor, conf_device, from_zone=None): + ''' + Create the Sensor object based on the type of sensor + + Return: + Sensor Object + ''' + sensor_type = SENSOR_DEFINITION[sensor][SENSOR_TYPE] + if sensor_type.startswith('battery'): + return Sensor_Battery(devicename, sensor, conf_device, from_zone) + elif sensor_type.startswith('text'): + return Sensor_Text(devicename, sensor, conf_device, from_zone) + elif sensor_type.startswith('timestamp'): + return Sensor_Timestamp(devicename, sensor, conf_device, from_zone) + elif sensor_type.startswith('timer'): + return Sensor_Timer(devicename, sensor, conf_device, from_zone) + elif sensor_type.startswith('distance'): + return Sensor_Distance(devicename, sensor, conf_device, from_zone) + elif sensor_type.startswith('zone_info'): + return Sensor_ZoneInfo(devicename, sensor, conf_device, from_zone) + elif sensor_type.startswith('zone'): + return Sensor_Zone(devicename, sensor, conf_device, from_zone) + elif sensor_type.startswith('info'): + return Sensor_Info(devicename, sensor, conf_device, from_zone) + elif sensor_type.startswith('badge'): + return Sensor_Badge(devicename, sensor, conf_device, from_zone) + else: + log_error_msg('iCloud3 Sensor Setup Error, Sensor-{sensor} > Invalid Sensor Type-{sensor_type}') + return None + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +class SensorBase(SensorEntity): + ''' iCloud base device sensor ''' + + def __init__(self, devicename, sensor_base, conf_device, from_zone=None): + try: + self.hass = Gb.hass + self.devicename = devicename + self.conf_device = conf_device + + self.from_zone = from_zone + if from_zone: + self.from_zone_fname = f" ({from_zone.title().replace('_', '').replace(' ', '')})" + self.sensor = f"{sensor_base}_{from_zone}" + else: + self.from_zone_fname = '' + self.sensor = sensor_base + + self.entity_name = f"{devicename}_{self.sensor}" + self.entity_id = f"sensor.{self.entity_name}" + self.device_id = Gb.dr_device_id_by_devicename.get(self.devicename) + + self.Device = Gb.Devices_by_devicename.get(devicename) + if self.Device and from_zone: + self.DeviceFmZone = self.Device.DeviceFmZones_by_zone.get(from_zone) + else: + self.DeviceFmZone = None + + + self._attr_force_update = True + self._unsub_dispatcher = None + self._on_remove = [self.after_removal_cleanup] + self.entity_removed_flag = False + + self.sensor_base = sensor_base + self.sensor_type = self._get_sensor_definition(sensor_base, SENSOR_TYPE).replace(' ', '') + self.sensor_fname = (f"{conf_device[FNAME]} " + f"{self._get_sensor_definition(sensor_base, SENSOR_FNAME)}" + f"{self.from_zone_fname}") + self._attr_native_unit_of_measurement = None + + self._state = self._get_restore_or_default_value(sensor_base) + self.current_state_value = '' + + # Add this sensor to the HA Recorder history exclude entity list + try: + if instr(self.sensor_type, 'ha_history_exclude'): + ha_history_recorder = Gb.hass.data['recorder_instance'] + ha_history_recorder.entity_filter._exclude_e.add(self.entity_id) + + except Exception as err: + log_exception(err) + pass + + Gb.sensors_created_cnt += 1 + log_debug_msg(f'Sensor entity created: {self.entity_id}, #{Gb.sensors_created_cnt}') + + except Exception as err: + log_exception(err) + log_msg = (f"►INTERNAL ERROR (UpdtSensorUpdate-{err})") + log_error_msg(log_msg) + +#------------------------------------------------------------------------------------------- + @property + def unique_id(self): + return f"{DOMAIN}_{self.entity_name}" + + @property + def name(self): + ''' Sensor friendly name ''' + return self.sensor_fname + + @property + def devicename_sensor(self): + '''Sensor friendly name.''' + return f"{self.entity_id}_{self.sensor}" + + @property + def fname_entity_name(self): + '''Sensor friendly name (devicename) ''' + return f"{self.sensor_fname} ({self.entity_name})" + + @property + def icon(self): + if self.Device and self.sensor_base.startswith(BATTERY): + battery_level = self.Device.sensors[BATTERY] + charging = (self.Device.sensors[BATTERY_STATUS].lower() == "charging") + icon = icon_for_battery_level(battery_level, charging) + + return icon + + elif self.Device and self.sensor_base == DIR_OF_TRAVEL: + if self.Device.sensors[DIR_OF_TRAVEL].startswith('ᗒ') or self.Device.sensors[DIR_OF_TRAVEL] == TOWARDS: + return ICON_DIR_OF_TRAVEL[TOWARDS] + elif self.Device.sensors[DIR_OF_TRAVEL].endswith('ᗒ') or self.Device.sensors[DIR_OF_TRAVEL] == AWAY_FROM: + return ICON_DIR_OF_TRAVEL[AWAY_FROM] + elif self.Device.sensors[DIR_OF_TRAVEL].startswith('@') or self.Device.sensors[DIR_OF_TRAVEL] == INZONE: + return ICON_DIR_OF_TRAVEL[INZONE] + else: + return self._get_sensor_definition(self.sensor, SENSOR_ICON) + else: + return self._get_sensor_definition(self.sensor, SENSOR_ICON) + + @property + def device_info(self) -> DeviceInfo: + """Return information about the device """ + return DeviceInfo( identifiers = {(DOMAIN, self.devicename)}, + manufacturer = "Apple", + model = self.conf_device[CONF_RAW_MODEL], + name = f"{self.conf_device[CONF_FNAME]} ({self.devicename})", + ) + +#------------------------------------------------------------------------------------------- + @property + def sensor_value(self): + return self._get_sensor_value(self.sensor) + +#------------------------------------------------------------------------------------------- + def _get_extra_attributes(self, sensor): + ''' + Get the extra attributes for the sensor defined in the + SENSOR_DEFINITION dictionary + ''' + extra_attrs = {} + extra_attrs['data_source'] = 'iCloud3' + extra_attrs['sensor_updated'] = datetime_now() + + attr_units_flag = False + for _sensor in self._get_sensor_definition(sensor, SENSOR_ATTRS): + _sensor_value = self._get_sensor_value(_sensor) + _sensor_attr_name = _sensor.replace('_date/time', '') + + if instr(_sensor_attr_name, 'distance'): + if attr_units_flag == False: + attr_units_flag = True + extra_attrs['distance_units_(attributes)'] = Gb.um + try: + _sensor_value = self._set_precision(_sensor_value) + #_sensor_value = self._set_precision(f"{_sensor_value:.4f}") + except: + pass + + extra_attrs[_sensor_attr_name] = _sensor_value + + if (self.Device is None or sensor not in SENSOR_LIST_DISTANCE): + return extra_attrs + + # Add distance apart from this device other devices to the attributes + # {devicename: [distance_m, gps_accuracy_factor, display_text]} + extra_attrs["Distance To Devices Determined"] = self.Device.sensors[DISTANCE_TO_OTHER_DEVICES_DATETIME] + for devicename, dist_to_other_devices in self.Device.sensors[DISTANCE_TO_OTHER_DEVICES].items(): + Device = Gb.Devices_by_devicename[devicename] + extra_attrs[f"Device Info.: {Device.fname_devtype}"] = self._set_precision(dist_to_other_devices[2]) + + for devicename, dist_to_other_devices in self.Device.sensors[DISTANCE_TO_OTHER_DEVICES].items(): + dist_m = dist_to_other_devices[0] + devicename_utf8 = devicename.replace('_', '-') + extra_attrs[f"DistTo (m)..: {devicename_utf8}"] = self._set_precision(dist_m, 'm') + + if Gb.um == 'km': + for devicename, dist_to_other_devices in self.Device.sensors[DISTANCE_TO_OTHER_DEVICES].items(): + dist_km = dist_to_other_devices[0] / 1000 + devicename_utf8 = devicename.replace('_', '-') + extra_attrs[f"DistTo (km): {devicename_utf8}"] = self._set_precision(dist_km) + else: + for devicename, dist_to_other_devices in self.Device.sensors[DISTANCE_TO_OTHER_DEVICES].items(): + dist_km = dist_to_other_devices[0] / 1000 + dist_mi = km_to_mi(dist_km) + devicename_utf8 = devicename.replace('_', '-') + extra_attrs[f"DistTo (mi).: {devicename_utf8}"] = self._set_precision(dist_mi, 'mi') + + return extra_attrs + +#------------------------------------------------------------------------------------------- + def _get_sensor_definition(self, sensor, field): + try: + sensor = sensor.replace(f"_{self.from_zone}", '') + return SENSOR_DEFINITION[sensor][field] + + except: + if field == SENSOR_ATTRS: + return [] + else: + return '' + +#------------------------------------------------------------------------------------------- + @property + def sensor_not_set(self): + sensor_value = self._get_sensor_value(self.sensor) + + if self.Device is None: + return True + + if (type(sensor_value) is str + and (sensor_value.startswith(BLANK_SENSOR_FIELD) + or sensor_value.strip() == '' + or sensor_value == HHMMSS_ZERO + or sensor_value == DATETIME_ZERO + or sensor_value == NOT_SET + or sensor_value == NOT_SET_FNAME)): + return True + else: + return False + +#------------------------------------------------------------------------------------------- + def _get_sensor_value(self, sensor): + ''' + Get the sensor value from: + - Device's attributes/sensor + - Device's DeviceFmZone attributes/sensors for a zone + ''' + + if self.from_zone: + return self._get_tfz_sensor_value(sensor) + else: + return self._get_device_sensor_value(sensor) + +#------------------------------------------------------------------------------------------- + def _set_precision(self, sensor_value, um=None): + ''' + Return the distance value as an integer or float value + ''' + try: + um = um if um else Gb.um + precision = 5 if um == 'km' else 2 if um == 'm' else 4 + sensor_value = round(float(sensor_value), precision) + if sensor_value == int(sensor_value): + return int(sensor_value) + + except Exception as err: + pass + + return sensor_value + +#------------------------------------------------------------------------------------------- + def _get_device_sensor_value(self, sensor): + ''' + Get the sensor value from: + - Device's attributes/sensor + - Device's DeviceFmZone attributes/sensors for a zone + ''' + + try: + if self.Device is None: + return self._get_restore_or_default_value(sensor) + + sensor_value = self.Device.sensors.get(sensor, None) + + if (sensor_value is None + or sensor_value == NOT_SET + or type(sensor_value) is str + and (sensor_value.strip() == '')): + + return self._get_restore_or_default_value(sensor) + + return sensor_value + + except Exception as err: + log_exception(err) + + return self._get_restore_or_default_value(sensor) + +#------------------------------------------------------------------------------------------- + def _get_restore_or_default_value(self, sensor): + ''' + Get a default value that is used when iCloud3 has not started or the Device for the + sensor has not veen created. + ''' + try: + if self.from_zone: + sensor_value = Gb.restore_state_devices[self.devicename]['from_zone'][self.from_zone][sensor] + else: + sensor_value = Gb.restore_state_devices[self.devicename]['sensors'][sensor] + except: + sensor_value = self._get_sensor_definition(sensor, SENSOR_DEFAULT) + + return sensor_value + +#------------------------------------------------------------------------------------------- + def _get_tfz_sensor_value(self, sensor): + ''' + Get the sensor value from: + - Device's DeviceFmZone attributes/sensors for a zone + ''' + try: + if (self.Device is None + or self.DeviceFmZone is None): + return self._get_restore_or_default_value(sensor) + + # Strip off zone to get the actual tfz dictionary item + tfz_sensor = sensor.replace(f"_{self.from_zone}", "") + sensor_value = self.DeviceFmZone.sensors.get(tfz_sensor, None) + + if (sensor_value is None + or sensor_value == NOT_SET + or (type(sensor_value) is str and sensor_value.strip() == '')): + return self._get_restore_or_default_value(sensor) + + return sensor_value + + except Exception as err: + log_exception(err) + + return self._get_restore_or_default_value(sensor) + +#------------------------------------------------------------------------------------------- + def _get_sensor_value_um(self, sensor, value_and_um=True): + ''' + Get the sensor value and determine if it has a value and unit_of_measurement. + + Return: + um specified: + [sensor_value, um] + um not specified (value only): + [sensor_value, None] + ''' + sensor_value = self._get_sensor_value(sensor) + + try: + if instr(sensor_value, ' '): + value_um_parts = sensor_value.split(' ') + return float(value_um_parts[0]), (self._get_sensor_um(sensor) or value_um_parts[1]) + + elif self.sensor_not_set: + return sensor_value, None + + else: + return float(sensor_value), None + + except ValueError: + return sensor_value, None + + except Exception as err: + log_exception(err) + return sensor_value, None + +#------------------------------------------------------------------------------------------- + def _get_sensor_um(self, sensor): + ''' + Get the sensor's special um override value from: + - Device's sensors_um dictionary + - Device's DeviceFmZone sensors_um dictionary for a zone + ''' + try: + if self.Device is None: + return None + + if self.from_zone and self.DeviceFmZone is None: + return None + + elif self.from_zone is None: + sensor_um = self.Device.sensors_um.get(sensor, None) + + elif self.from_zone and self.DeviceFmZone: + sensor_um = self.DeviceFmZone.sensors_um.get(sensor, None) + + except: + sensor_um = None + + return sensor_um + +#------------------------------------------------------------------------------------------- + @property + def should_poll(self): + ''' Do not poll to update the sensor ''' + return False + +#------------------------------------------------------------------------------------------- + def update_entity_attribute(self, new_fname=None): + """ Update entity definition attributes """ + + if new_fname is None: + return + + entity_registry = er.async_get(Gb.hass) + self.sensor_fname = (f"{new_fname} " + f"{self._get_sensor_definition(self.sensor, SENSOR_FNAME)}" + f"{self.from_zone_fname}") + + kwargs = {} + kwargs['original_name'] = self.sensor_fname + entity_registry.async_update_entity(self.entity_id, **kwargs) + + + """ + Typically used: + name: str | None | UndefinedType = UNDEFINED, + new_entity_id: str | UndefinedType = UNDEFINED, + device_id: str | None | UndefinedType = UNDEFINED, + original_name: str | None | UndefinedType = UNDEFINED, + config_entry_id: str | None | UndefinedType = UNDEFINED, + + Not used: + area_id: str | None | UndefinedType = UNDEFINED, + capabilities: Mapping[str, Any] | None | UndefinedType = UNDEFINED, + device_class: str | None | UndefinedType = UNDEFINED, + disabled_by: RegistryEntryDisabler | None | UndefinedType = UNDEFINED, + entity_category: EntityCategory | None | UndefinedType = UNDEFINED, + hidden_by: RegistryEntryHider | None | UndefinedType = UNDEFINED, + icon: str | None | UndefinedType = UNDEFINED, + new_unique_id: str | UndefinedType = UNDEFINED, + original_device_class: str | None | UndefinedType = UNDEFINED, + original_icon: str | None | UndefinedType = UNDEFINED, + supported_features: int | UndefinedType = UNDEFINED, + unit_of_measurement: str | None | UndefinedType = UNDEFINED, + """ + +#------------------------------------------------------------------------------------------- + def remove_entity(self): + try: + Gb.hass.async_create_task(self.async_remove(force_remove=True)) + + except Exception as err: + _LOGGER.exception(err) + +#------------------------------------------------------------------------------------------- + def after_removal_cleanup(self): + """ Cleanup sensor after removal + + Passed in the `self._on_remove` parameter during initialization + and called by HA after processing the async_remove request + """ + + log_info_msg(f"Unregistered sensor.icloud3 entity Removed: {self.entity_id}") + + self._remove_from_registries() + self.entity_removed_flag = True + + if self.Device is None: + return + + if self.Device.Sensors_from_zone and self.sensor in self.Device.Sensors_from_zone: + self.Device.Sensors_from_zone.pop(self.sensor) + + if self.Device.Sensors and self.sensor in self.Device.Sensors: + self.Device.Sensors.pop(self.sensor) + +#------------------------------------------------------------------------------------------- + def _remove_from_registries(self) -> None: + """ Remove entity/device from registry """ + + if not self.registry_entry: + return + + if entity_id := self.registry_entry.entity_id: + entity_registry = er.async_get(Gb.hass) + if entity_id in entity_registry.entities: + entity_registry.async_remove(self.entity_id) + +#------------------------------------------------------------------------------------------- + async def async_will_remove_from_hass(self): + '''Clean up after entity before removal.''' + + if self._unsub_dispatcher: + for unsub_dispatcher in self._unsub_dispatcher: + unsub_dispatcher() + +#------------------------------------------------------------------------------------------- + def write_ha_sensor_state(self): + """Update the entity's state if the state value has changed.""" + + try: + # if self.current_state_value != self.native_value: + # self.current_state_value = self.native_value + self.async_write_ha_state() + + except Exception as err: + log_exception(err) + +#------------------------------------------------------------------------------------------- + # async def async_added_to_hass(self): + # '''Register state update callback.''' + # self._unsub_dispatcher = async_dispatcher_connect( + # self.hass, + # signal_device_update, + # self.async_write_ha_state) + +#------------------------------------------------------------------------------------------- + def __repr__(self): + return (f"") + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +class Sensor_Badge(SensorBase): + ''' Sensor for displaying the device badge items ''' + + @property + def native_value(self): + + return str(self._get_sensor_value(BADGE)) + + @property + def extra_state_attributes(self): + if self.Device: + badge_attrs = self.Device.sensor_badge_attrs.copy() + badge_attrs.update(self._get_extra_attributes(self.sensor)) + return badge_attrs + else: + return None + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +class Sensor_ZoneInfo(SensorBase): + ''' Sensor for displaying the device zone time/distance items ''' + + @property + def native_value(self): + return str(self._get_sensor_value(ZONE_INFO)) + + @property + def extra_state_attributes(self): + return self._get_extra_attributes(self.sensor) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +class Sensor_Text(SensorBase): + ''' Sensor for handling text items ''' + + @property + def native_value(self): + sensor_value = self._get_sensor_value(self.sensor) + + # if instr(self.sensor_type, 'title'): + # sensor_value = sensor_value.title().replace('_', ' ') + + if instr(self.sensor_type, 'time'): + if instr(sensor_value, ' '): + text_um_parts = sensor_value.split(' ') + sensor_value = text_um_parts[0] + self._attr_unit_of_measurement = text_um_parts[1] + else: + self._attr_unit_of_measurement = None + + # Set to space if empty + if sensor_value.strip() == '': + sensor_value = BLANK_SENSOR_FIELD + + return sensor_value + + @property + def extra_state_attributes(self): + return self._get_extra_attributes(self.sensor) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +class Sensor_Info(SensorBase): + ''' + Sensor for handling info sensor messages. + 1. This will update a specific Device's info sensor using the + Device.update_info_message('msg') function. + broadcase_info_msg('msg') function in base.py by entering the + message into the 'Gb.broadcast_info_msg' field. This lets you display + an info message during startup before the devices have been created + or to everyone as a general notification. + ''' + + @property + def native_value(self): + self._attr_unit_of_measurement = None + + if Gb.broadcast_info_msg and Gb.broadcast_info_msg != '• ': + return Gb.broadcast_info_msg + + elif self.sensor_not_set: + return f"◈◈ Starting iCloud3 ◈◈" + + else: + return self.sensor_value + + @property + def extra_state_attributes(self): + return self._get_extra_attributes(self.sensor) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +class Sensor_Timestamp(SensorBase): + ''' + Sensor for handling timestamp (mm/dd/yy hh:mm:ss) items + Sensors: last_update_time, next_update_time, last_located + ''' + + @property + def native_value(self): + sensor_value = self._get_sensor_value(self.sensor) + sensor_value = time_to_12hrtime(sensor_value) + sensor_um = self._get_sensor_um(self.sensor) + self._attr_native_unit_of_measurement = sensor_um + + try: + # Drop the 'a' or 'p' so the field will fit on an iPhone + if int(sensor_value.split(':')[0]) >= 10: + sensor_value = time_remove_am_pm(sensor_value) + except: + pass + + return sensor_value + + @property + def extra_state_attributes(self): + return self._get_extra_attributes(self.sensor) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +class Sensor_Timer(SensorBase): + ''' + Sensor for handling timer items (30 secs, 1.5 hrs, 30 mins) + Sensors: inteval, travel_time, travel_time_mins + ''' + + @property + def native_value(self): + if instr(self.sensor_type, ','): + sensor_type_um = self.sensor_type.split(',')[1] + else: + sensor_type_um = '' + + sensor_value, unit_of_measurement = self._get_sensor_value_um(self.sensor) + + if sensor_value == 0: + self._attr_native_unit_of_measurement = 'min' + return 0 + + if unit_of_measurement: + self._attr_native_unit_of_measurement = unit_of_measurement + + elif sensor_type_um == 'min': + time_str = mins_to_time_str(sensor_value) + if time_str and instr(time_str, 'd') is False: # Make sure it is not a 4d2h34m12s item + time_min_hrs = time_str.split(' ') + sensor_value = time_min_hrs[0] + self._attr_native_unit_of_measurement = time_min_hrs[1] + + elif sensor_type_um == 'sec': + time_str = secs_to_time_str(sensor_value) + if time_str and instr(time_str, 'd') is False: # Make sure it is not a 4d2h34m12s item + time_secs_min_hrs = time_str.split(' ') + sensor_value = time_secs_min_hrs[0] + self._attr_native_unit_of_measurement = time_secs_min_hrs[1] + + else: + self._attr_native_unit_of_measurement = 'min' + + try: + # Try to convert sensor_value to integer. Just return it if it fails. + if (sensor_value and sensor_value != BLANK_SENSOR_FIELD): + if sensor_value == int(sensor_value): + sensor_value = int(sensor_value) + except Exception as err: + log_exception(err) + pass + + return sensor_value + + @property + def extra_state_attributes(self): + return self._get_extra_attributes(self.sensor) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +class Sensor_Distance(SensorBase): + ''' + Sensor for handling timer items (30 secs, 1.5 hrs, 30 mins) + Sensors: inteval, travel_time, travel_time_mins + ''' + + @property + def native_value(self): + if instr(self.sensor_type, ','): + sensor_type_um = self.sensor_type.split(',')[1] + + sensor_value, unit_of_measurement = self._get_sensor_value_um(self.sensor) + + + if unit_of_measurement: + self._attr_native_unit_of_measurement = unit_of_measurement + elif sensor_type_um == 'm-ft': + self._attr_native_unit_of_measurement = Gb.um_m_ft + elif sensor_type_um == 'km-mi': + self._attr_native_unit_of_measurement = Gb.um + elif sensor_type_um == 'm': + self._attr_native_unit_of_measurement = 'm' + else: + self._attr_native_unit_of_measurement = Gb.um + + if self._attr_native_unit_of_measurement == 'km': + if round_to_zero(sensor_value) == 0: + sensor_value = 0 + elif sensor_value >= 25: #25km/15mi + sensor_value = f"{sensor_value:.0f}" + elif sensor_value >= 1: #1000m/.6mi + sensor_value = f"{sensor_value:.1f}" + elif instr(self.sensor_type, 'm-ft'): + sensor_value = f"{sensor_value*1000:.2f}" + self._attr_native_unit_of_measurement = 'm' + else: + sensor_value = f"{sensor_value:.2f}" + + elif self._attr_native_unit_of_measurement == 'mi': + if round_to_zero(sensor_value) == 0: + sensor_value = 0 + elif sensor_value > 20: + sensor_value = f"{sensor_value:.1f}" + elif sensor_value > 1: + sensor_value = f"{sensor_value:.2f}" + elif instr(self.sensor_type, 'm-ft'): + sensor_value = f"{sensor_value*5280:.2f}" + self._attr_native_unit_of_measurement = 'ft' + else: + sensor_value = f"{sensor_value:.4f}" #6" accuracy + + return self._set_precision(sensor_value) + + @property + def extra_state_attributes(self): + return self._get_extra_attributes(self.sensor) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +class Sensor_Battery(SensorBase): + ''' + Sensor for handling battery items (30s) + Sensors: battery + ''' + + @property + def native_value(self): + self._attr_native_unit_of_measurement = '%' + sensor_value = self._get_sensor_value(self.sensor) + return sensor_value + + @property + def extra_state_attributes(self): + extra_attrs = self._get_extra_attributes(self.sensor) + extra_attrs.update({'device_class': 'battery', 'state_class': 'battery'}) + + return extra_attrs + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +class Sensor_Zone(SensorBase): + ''' + Sensor for handling zone items + Sensors: + zone, zone_name, zone_fname, + last_zone, last_zone_name, last_zone_fname + + zone or last_zone sensor: + Attributes = zone_name & zone_fname + zone_name, zone_fname, last_zone_name, last_zone_fname: + Attributes = zone + ''' + + @property + def native_value(self): + sensor_value = self._get_sensor_value(f"{self.sensor}") + + if self.sensor.endswith(ZONE): + return sensor_value + + zone = self._get_sensor_value(ZONE) + Zone = Gb.Zones_by_zone.get(zone, None) + if Zone is None: + pass + elif self.sensor.endswith(FNAME): + sensor_value = Zone.fname + else: + sensor_value = Zone.name + + return sensor_value + + @property + def extra_state_attributes(self): + extra_attrs = {'data_source': 'iCloud3'} + + zone = self._get_sensor_value(ZONE) + Zone = Gb.Zones_by_zone.get(zone, None) + + if Zone is None: + pass + elif self.sensor.endswith(ZONE): + extra_attrs[NAME] = Zone.name + extra_attrs[FNAME] = Zone.fname + else: + extra_attrs[ZONE] = Zone.zone + + return extra_attrs + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +class Support_SensorBase(SensorEntity): + ''' iCloud Support Sensor Base + - Event Log + - Waze History Track + ''' + + def __init__(self, fname, entity_name): + '''Initialize the Event Log sensor (icloud3_event_log).''' + self.fname = fname + self.sensor = entity_name + self.entity_name = entity_name + self.entity_id = f"sensor.{self.entity_name}" + self._unsub_dispatcher = None + self._device = f"{DOMAIN}" + self.current_state_value = '' + + Gb.sensors_created_cnt += 1 + log_debug_msg(f'Sensor entity created: {self.entity_id}, #{Gb.sensors_created_cnt}') + + # Add this sensor to the Recorder history exclude entity list + try: + ha_history_recorder = Gb.hass.data['recorder_instance'] + ha_history_recorder.entity_filter._exclude_e.add(self.entity_id) + except: + pass + + @property + def name(self): + '''Sensor friendly name.''' + return self.fname + + @property + def unique_id(self): + return f"{self.entity_name}" + + @property + def device(self): + return self.unique_id() + + @property + def device_info(self) -> DeviceInfo: + """Return information about the device """ + return DeviceInfo( identifiers = {(DOMAIN, DOMAIN)}, + manufacturer = 'iCloud3', + model = 'Internal', + name = 'iCloud3' + ) + +#------------------------------------------------------------------------------------------- + def __repr__(self): + return (f"") + + @property + def should_poll(self): + ''' Do not poll to update the sensor ''' + return False + +#------------------------------------------------------------------------------------------- + def async_update_sensor(self): + """Update the entity's state if the state value has changed.""" + + try: + # if self.current_state_value != self.native_value: + # self.current_state_value = self.native_value + self.async_write_ha_state() + + except Exception as err: + log_exception(err) + + async def async_will_remove_from_hass(self): + '''Clean up after entity before removal.''' + try: + self._unsub_dispatcher() + + except TypeError: + pass + except Exception as err: + log_exception(err) + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +class Sensor_EventLog(Support_SensorBase): + + @property + def icon(self): + return 'mdi:message-text-clock-outline' + + @property + def native_value(self): + '''State value - (devicename:time)''' + try: + if Gb.EvLog is None: + return 'Unavailable' + + time_suffix = (f"{dt_util.now().strftime('%a, %m/%d')}, " + f"{dt_util.now().strftime(Gb.um_time_strfmt)}." + f"{dt_util.now().strftime('%f')}") + + return (f"{Gb.EvLog.evlog_sensor_state_value}:{time_suffix}") + + except Exception as err: + log_exception(err) + return 'Unavailable' + + @property + def extra_state_attributes(self): + '''Return default attributes for the iCloud device entity.''' + log_update_time = ( f"{dt_util.now().strftime('%a, %m/%d')}, " + f"{dt_util.now().strftime(Gb.um_time_strfmt)}") + + if Gb.EvLog: + return Gb.EvLog.evlog_attrs + + return {'log_level_debug': '', + 'filtername': 'Initialize', + 'update_time': log_update_time, + 'popup_message': 'Starting', + 'names': {'Loading': 'Initializing iCloud3'}, + 'logs': [], + 'platform': Gb.operating_mode} + + +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +class Sensor_WazeHistTrack(Support_SensorBase): + '''iCloud Waze History Track GPS Values Sensor.''' + + @property + def icon(self): + return 'mdi:map-check-outline' + + @property + def native_value(self): + '''State value - (latitude, longitude)''' + if Gb.WazeHist is None: + return 'Not Used' + + return f"{Gb.WazeHist.track_latitude}, {Gb.WazeHist.track_longitude}" + + @property + def extra_state_attributes(self): + '''Return default attributes for the iCloud device entity.''' + if Gb.WazeHist is None: + return None + + return {'data_source': 'iCloud3', + 'latitude': Gb.WazeHist.track_latitude, + 'longitude': Gb.WazeHist.track_longitude, + 'friendly_name': 'WazeHist'} + + +#<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>< diff --git a/custom_components/icloud3/sensor.py b/custom_components/icloud3/sensor.py index d50715f..3c72256 100644 --- a/custom_components/icloud3/sensor.py +++ b/custom_components/icloud3/sensor.py @@ -28,7 +28,7 @@ NAME, FNAME, BADGE, ZONE, ZONE_INFO, BATTERY, BATTERY_STATUS, BATTERY_SOURCE, - ZONE_DISTANCE, + ZONE_DISTANCE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, DISTANCE_TO_OTHER_DEVICES_DATETIME, CONF_TRACK_FROM_ZONES, CONF_IC3_DEVICENAME, CONF_MODEL, CONF_RAW_MODEL, CONF_FNAME, @@ -391,7 +391,7 @@ def __init__(self, devicename, sensor_base, conf_device, from_zone=None): self.entity_name = f"{devicename}_{self.sensor}" self.entity_id = f"sensor.{self.entity_name}" - self.device_id = Gb.ha_device_id_by_devicename.get(self.devicename) + self.device_id = Gb.dr_device_id_by_devicename.get(self.devicename) self.Device = Gb.Devices_by_devicename.get(devicename) if self.Device and from_zone: @@ -498,12 +498,26 @@ def _get_extra_attributes(self, sensor): extra_attrs['data_source'] = 'iCloud3' extra_attrs['sensor_updated'] = datetime_now() - if instr(self.sensor_type, 'distance'): - extra_attrs["Units"] = UM_FNAME.get(Gb.um, Gb.um) - + attr_units_flag = False for _sensor in self._get_sensor_definition(sensor, SENSOR_ATTRS): - _sensor_value = self._get_sensor_value(_sensor) _sensor_attr_name = _sensor.replace('_date/time', '') + _sensor_value = self._get_sensor_value(_sensor) + try: + _sensor_value = self._set_precision(_sensor_value) + except: + pass + + if instr(_sensor_attr_name, ZONE_DISTANCE_M_EDGE): + extra_attrs[_sensor_attr_name] = _sensor_value + + # if attr_units_flag == False: + # attr_units_flag = True + if Gb.um == 'mi': + extra_attrs['distance_units_(attributes)'] = 'mi' + if self._get_sensor_value(ZONE_DISTANCE_M): + sensor_value = self._get_sensor_value(ZONE_DISTANCE_M)*Gb.um_km_mi_factor/1000 + extra_attrs['miles_distance'] = self._set_precision(sensor_value) + extra_attrs[_sensor_attr_name] = _sensor_value if (self.Device is None or sensor not in SENSOR_LIST_DISTANCE): @@ -514,24 +528,24 @@ def _get_extra_attributes(self, sensor): extra_attrs["Distance To Devices Determined"] = self.Device.sensors[DISTANCE_TO_OTHER_DEVICES_DATETIME] for devicename, dist_to_other_devices in self.Device.sensors[DISTANCE_TO_OTHER_DEVICES].items(): Device = Gb.Devices_by_devicename[devicename] - extra_attrs[f"Device Info.: {Device.fname_devtype}"] = dist_to_other_devices[2] + extra_attrs[f"Device Info.: {Device.fname_devtype}"] = self._set_precision(dist_to_other_devices[2]) for devicename, dist_to_other_devices in self.Device.sensors[DISTANCE_TO_OTHER_DEVICES].items(): dist_m = dist_to_other_devices[0] devicename_utf8 = devicename.replace('_', '-') - extra_attrs[f"DistTo (m)..: {devicename_utf8}"] = dist_m + extra_attrs[f"DistTo (m)..: {devicename_utf8}"] = self._set_precision(dist_m, 'm') if Gb.um == 'km': for devicename, dist_to_other_devices in self.Device.sensors[DISTANCE_TO_OTHER_DEVICES].items(): dist_km = dist_to_other_devices[0] / 1000 devicename_utf8 = devicename.replace('_', '-') - extra_attrs[f"DistTo (km): {devicename_utf8}"] = dist_km + extra_attrs[f"DistTo (km): {devicename_utf8}"] = self._set_precision(dist_km) else: for devicename, dist_to_other_devices in self.Device.sensors[DISTANCE_TO_OTHER_DEVICES].items(): dist_km = dist_to_other_devices[0] / 1000 dist_mi = km_to_mi(dist_km) devicename_utf8 = devicename.replace('_', '-') - extra_attrs[f"DistTo (mi).: {devicename_utf8}"] = dist_mi + extra_attrs[f"DistTo (mi).: {devicename_utf8}"] = self._set_precision(dist_mi, 'mi') return extra_attrs @@ -579,6 +593,23 @@ def _get_sensor_value(self, sensor): else: return self._get_device_sensor_value(sensor) +#------------------------------------------------------------------------------------------- + def _set_precision(self, sensor_value, um=None): + ''' + Return the distance value as an integer or float value + ''' + try: + um = um if um else Gb.um + precision = 5 if um == 'km' else 2 if um == 'm' else 4 + sensor_value = round(float(sensor_value), precision) + if sensor_value == int(sensor_value): + return int(sensor_value) + + except Exception as err: + pass + + return sensor_value + #------------------------------------------------------------------------------------------- def _get_device_sensor_value(self, sensor): ''' @@ -909,7 +940,7 @@ def native_value(self): return Gb.broadcast_info_msg elif self.sensor_not_set: - return f"{DOT}{DOT} Starting iCloud3 {DOT}{DOT}" + return f"◈◈ Starting iCloud3 ◈◈" else: return self.sensor_value @@ -1030,30 +1061,30 @@ def native_value(self): self._attr_native_unit_of_measurement = Gb.um if self._attr_native_unit_of_measurement == 'km': - if sensor_value >= 25: #25km/15mi - sensor_value = f"{sensor_value:.0f}" - elif sensor_value >= 1: #1000m/.6mi - sensor_value = f"{sensor_value:.1f}" - else: - sensor_value = f"{sensor_value*1000:.0f}" + if round_to_zero(sensor_value) == 0: + sensor_value = 0 + elif sensor_value > 20: + sensor_value = round(sensor_value, 0) + elif sensor_value >= 1: + sensor_value = round(sensor_value, 2) + elif instr(self.sensor_type, 'm-ft'): + sensor_value = round(sensor_value*1000, 2) self._attr_native_unit_of_measurement = 'm' + else: + sensor_value = round(sensor_value, 2) elif self._attr_native_unit_of_measurement == 'mi': - if sensor_value > 20: - sensor_value = f"{sensor_value:.1f}" - elif sensor_value > 1: - sensor_value = f"{sensor_value:.2f}" - elif round_to_zero(sensor_value) == 0: + if round_to_zero(sensor_value) == 0: sensor_value = 0 + elif sensor_value > 20: + sensor_value = round(sensor_value, 1) + elif sensor_value > 1: + sensor_value = round(sensor_value, 2) + elif instr(self.sensor_type, 'm-ft'): + sensor_value = round(sensor_value*5280, 2) + self._attr_native_unit_of_measurement = 'ft' else: - sensor_value = f"{sensor_value:.0f}" - - - try: - if sensor_value == int(sensor_value): - sensor_value = int(sensor_value) - except: - pass + sensor_value = round(sensor_value, 2) #6" accuracy return sensor_value diff --git a/custom_components/icloud3/strings.json b/custom_components/icloud3/strings.json index f80089b..b7534b6 100644 --- a/custom_components/icloud3/strings.json +++ b/custom_components/icloud3/strings.json @@ -1,43 +1,339 @@ { "config": { + "abort": { + "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", + "login_error": "An error occurred logging into the iCloud account. Verify the username and password.", + "reauth_successful": "The reauthentication has been successfully completed", + "update_cancelled": "Update Cancelled" + }, "error": { - "auth": "The auth token provided is not valid.", - "invalid_path": "The path provided is not valid. Should be in the format `user/repo-name`." + "invalid_path": "The path provided is not valid. Should be in the format `user/repo-name`", + "send_verification_code": "Failed to send the Apple ID Verification Code", + "invalid_verification_code": "The Apple ID Verification Code is invalid. Requested a new code", + "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" }, "step": { "user": { "data": { - "access_token": "GitHub Access Token", - "url": "GitHub Enterprise server URL" + "continue": "Select to continue iCloud3 installation" }, - "description": "Enter your GitHub credentials.", - "title": "Authentication" + "description": "iCloud3 Integration is being set up. Select Submit and then select CONFIGURE to configure iCloud3", + "title": "iCloud3 Integration Installation" }, - "repo": { + "reauth": { + "title": "Apple ID Verification Code", + "description": "Enter the 6-digit verification code you just received from Apple", "data": { - "add_another": "Add another repo?", - "path": "Path to the repository e.g. home-assistant/core", - "name": "Name of the sensor." - }, - "description": "Add a GitHub repo, check the box to add another.", - "title": "Add GitHub Repository" + "verification_code": "ICLOUD ACCOUNT VERIFICATION CODE" + } } } }, "options": { + "abort": { + "no_device": "None of your devices have \"Find my iPhone\" activated", + "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", + "reauth_successful": "The reauthentication has been successfully completed" + }, "error": { - "invalid_path": "The path provided is not valid. Should be in the format `user/repo-name` and should be a valid github repository." + "update_aborted": "Update aborted, an error was detected in one of the data fields", + "conf_updated": "iCloud3 Configuration was Data Updated", + "conf_reloaded": "iCloud3 Configuration File was Reloaded", + "icloud_logging_into": "Logging into iCloud Account", + "icloud_logged_into": "Successfully Logged into the iCloud Account", + "icloud_already_logged_into": "Already Logged into the iCloud Account", + "icloud_invalid_auth": "iCloud Account Login Failed, Check Username & Password", + "icloud_reauth_scheduled": "Reauhentication scheduled after exiting, BROWSER REFRESH MAY BE NEEDED", + "icloud_acct_not_set_up": "The iCloud Account Username or Password has not been set up", + "send_verification_code": "Failed to send the Apple ID Verification Code", + "verification_code_accepted": "The Apple ID Verification Code was Accepted", + "invalid_verification_code": "The Apple ID Verification Code is not correct", + "review_filledin_fields": "Review the 'Filled in' fields", + "not_numeric": "The value entered is not numeric", + "stat_zone_base_lat_range_error": "The value must be between -90 and 90", + "stat_zone_base_long_range_error": "The value must be between -180 and 180", + "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_device": "At least one device (Family Share List, Find-my-Friends, iOS App device must be specified", + "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_fmf_iosapp_picture": "Check the FamShr, FmF, iOS App or Picture parameter value (Not found or Invalid). Reset to `None`", + "tfz_selection_invalid": "The value must be a zone that is being tracked from", + "time_factor_invalid_range": "The 'travel_time_factor' must be between .1 and .9", + "display_text_as_no_gtsign": "The '>' between the ActualText and DisplayAsText is missing", + "display_text_as_no_actual": "The 'ActualText' is not specified", + "display_text_as_no_display_as": "The 'DisplayAsText' is not specified", + "not_found_directory": "The directory was not found", + "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", + "iosapp_search_error": "WARNING: Search Failure - No iOS App device that starts with the iCloud3 or FamShr 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", + "excluded_sensors_ha_restart": "Excluded Sensors were Updated. An HA restart is required" + }, "step": { - "init": { - "title": "Manage Repos-s", + "menu": { + "title": "iCloud3 v3 Configuration Wizard", + "data": { + "menu_item": "", + "menu_action": "═════════════════════════════════════════════════════" + } + }, + "restart_icloud3": { + "title": "Confirm Restarting iCloud3", + "description": "Note: Changes to tracked devices require restarting iCloud3", + "data": { + "restart_now_later": "" + } + }, + "icloud_account": { + "title": "iCloud Account Login Credentials", + "description": "The iCloud Account and the iOS App are used to provide location information (the data source) for tracking a device. This screen is used to configure the username/password that provides access to your iCloud account and to specify if the iOS App will be used on any of your devices. The iCloud Account can provide location information for devices in the Family Sharing list and from those that are sharing location information on the FindMy app. The HA iOS Companion app can also provide location information for the device.", + "data": { + "username": "APPPLE ID - The email address used to sign in to the iCloud Acount", + "password": "PASSWORD - The Password of the iCloud Acount", + "data_source1": "LOCATION DATA SOURCE - The services used for location and other data (iCloud, iOS App, both)", + "data_source2": "LOCATION DATA SOURCE - The services providing location and other data", + "data_source": "", + "icloud_server_endpoint_suffix": "ICLOUD SERVER LOCATION - Countries having localized Apple iCloud Servers (Not Normally Used)s", + "opt_action": "═════════════════════════════════════════════════════" + }, + "data_description": { + } + }, + "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" + } + }, + "device_list": { + "title": "iCloud3 Device Tracker Entities", + "description": "All of the devices that are tracked or monitored by iCloud3 are listed on this screen. Here, new devices are added and existing devices are selected for updating or deletion.", + "data": { + "devices": "iCloud3 devices", + "opt_action": "═════════════════════════════════════════════════════" + } + }, + "add_device": { + "title": "Add Tracked 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", + "device_type": "DEVICE TYPE - iPhone, iPad, Watch, etc.", + "tracking_mode": "TRACKING MODE - How location requests should be done (Full tracking, Monitor, Inactive)", + "iosapp": "iOS APP MONITORED - Is the iOS App is installed on this device and will it monitored", + "opt_action": "═════════════════════════════════════════════════════" + }, + "data_description": { + } + }, + "update_device": { + "title": "Update Tracked iCloud3 Device", + "description": "This screen lets you configure each of the devices that can be tracked or monitored using iCloud3", + "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.", + "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", + "iosapp_device": "IOS APP DEVICE_TRACKER ENTITY - Use this HA device location data & zone enter/exit triggers", + "stat_zone_fname": "STATIONARY ZONE NAME - Override the generic Stationary zone name set on the Special Zones screen", + "track_from_zones": "TRACK-FROM-ZONES - Track travel time & distance from Home and other zones", + "track_from_base_zone": "PRIMARY HOME ZONE - Use another zone if you are away from Home for an extended period", + "picture": "PICTURE - Photo image of the person normally using this device (40x40 pixels is best size)", + "inzone_interval": "INZONE INTERVAL", + "opt_action": "═════════════════════════════════════════════════════" + }, + "data_description": { + "inzone_interval": "Time between location requests when in a zone", + "track_from_base_zone": "Normally, the Home zone is used as the base location for all tracking (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.)" + } + }, + "delete_device": { + "title": "Delete Device(s), Other Device Maintenance", + "data": { + "opt_action": "" + } + }, + "change_device_order": { + "title": "Event Log Device Display Sequence", + "data": { + "device_desc": "DEVICES - The devices are displayed in the Event Log heading area and in various Event Log messages in this sequence", + "opt_action": "═════════════════════════════════════════════════════" + } + }, + "format_settings": { + "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. With this screen, you can specify the how these results should be displayed.", "data": { - "repos": "Existing Repos: Uncheck any repos you want to remove-s", - "path": "New Repo: Path to the repository e.g. home-assistant-core-s", - "name": "New Repo: Name of the sensor-s", - "tracking_method": "Tracking Method-s" + "log_level": "LOG LEVEL - The type of messages that are added to the HA log file by iCloud3", + "display_zone_format": "ZONE NAME FORMAT - How the Zone name is displayed in sensors and in the Event Log", + "device_tracker_state_format": "DEVICE_TRACKER STATE FORMAT - How the Zone name is displayed in the device_tracker entity", + "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", + "opt_action": "═════════════════════════════════════════════════════" }, - "description": "Remove existing repos or add a new repo." + "data_description": { + "zone_sensor_evlog_format": "The HA entity value standard is to display the state value in lower_case with underscores ('_'). This overides that standard and displays the Zone Name Format selected above" + } + }, + "display_text_as": { + "title": "Event Log 'Display Text As'", + "description": "There may be some text fields that are displayed on the Event Log screen that may be sensitive in nature. Some examples include email addresses or phone numbers. With this screen, you can select the Original Text and what should be displayed instead. For example, you can replace 'geekstergary@apple.com' with 'gary@email.com'", + "data": { + "display_text_as": "Text Replacement Fields", + "opt_action": "═════════════════════════════════════════════════════" + } + }, + "display_text_as_update": { + "title": "Update Event Log 'Display Text As' Value", + "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)", + "opt_action": "═════════════════════════════════════════════════════" + }, + "data_description": { + } + }, + "actions": { + "title": "iCloud3 Action Commands", + "data": { + "opt_action": "" + } + }, + "tracking_parameters": { + "title": "Tracking & Other Parameters", + "description": "The parameters on this screen do not fall into any of the other general categories and are rarely changed.", + "data": { + "log_level": "LOG LEVEL - The type of messages that are added to the HA log file during iCloud3 operations", + "gps_accuracy_threshold": "GPS ACCURACY THRESHOLD", + "old_location_threshold": "OLD LOCATION THRESHOLD", + "old_location_adjustment": "OLD LOCATION ADJUSTMENT", + "distance_between_devices": "USE LOCATION RESULTS FROM A NEAR-BY DEVICE", + "max_interval": "MAXIMUM INTERVAL", + "exit_zone_interval": "EXIT_ZONE_INTERVAL", + "iosapp_alive_interval": "REQUEST IOSAPP LOCATION INTERVAL", + "tfz_tracking_max_distance": "TRACK-FROM-ZONE DISPLAY DISTANCE", + "offline_interval": "DEVICE OFFLINE INTERVAL", + "travel_time_factor": "TRAVEL TIME INTERVAL MULTIPLIER", + "event_log_card_directory": "EVENT LOG CARD LOVELACE RESOURCES DIRECTORY - Event Log custom card .js file directory", + "opt_action": "═════════════════════════════════════════════════════" + }, + "data_description": { + "old_location_threshold": "Locations older than this value will be discarded", + "old_location_adjustment": "Add this to the time that determines if a location is old", + "distance_between_devices": "See if there are any other devices that are near the device being updated. If there is, use that device's location results instead of going through the calculation process. This can speed up the update since Waze travel time and distance information does not have to be requested", + "gps_accuracy_threshold": "Locations with GPS Accuracy above this value will be discarded", + "tfz_tracking_max_distance": "Normally the Home zone's time and distance data is displayed on the Device's device_tracker and sensor entities. Display the Track-from-Zone instead when the Device is within this distance of the Track-from-Zone", + "max_interval": "The maximum time between location requests", + "exit_zone_interval": "The time to the first location request after exiting a zone", + "iosapp_alive_interval": "Send a location request to the iOS App if there has been no contact after this amount of time. This will check to see if the iOS App is responding to location requests or is asleep and not running.", + "offline_interval": "Location request interval when offline (Airplane mode, dead cell area, etc.)", + "travel_time_factor": "Next location time = Waze travel time to zone * this value" + } + }, + "inzone_intervals": { + "title": "inZone Parameters and Default Intervals", + "description": "An inZone interval is the time between location requests when the Device is in a zone. This screen sets the default values for different types of devices. This value is assigned to a device when it is added and can then be changed on the Update Device screen", + "data": { + "iphone": "IPHONE & IPOD", + "ipad": "IPAD", + "watch": "APPLE WATCH", + "airpods": "AIRPODS", + "no_iosapp": "IOS APP IS NOT INSTALLED ", + "other": "OTHER DEVICE TYPE", + "discard_poor_gps_inzone": "Discard Location Updates with Poor GPS Accuracy when in a Zone", + "distance_between_devices": "Determine the distance between devices. Use a near by device's tracking results", + "center_in_zone": "Change Device's Location to the Zone's Center when in a Zone", + "opt_action": "═════════════════════════════════════════════════════" + }, + "data_description": { + "no_iosapp": "Default interval if the iOS App is not used for location monitoring and zone enter/exit triggers", + "other": "Unspecified device type inzone interval" + } + }, + "waze_main": { + "title": "Waze - Route Service Travel Time/Distance", + "data": { + "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_track_direction": "GENERAL TRAVEL DIRECTION - Used to display 'Map Trace Lines' between saved locations", + "waze_history_max_distance": "HISTORY MAX DISTANCE", + "opt_action": "═════════════════════════════════════════════════════" + }, + "data_description": { + "waze_min_distance": "Use the Waze Route Service when the zone distance is greater than this value", + "waze_max_distance": "Do not use the Waze Route Service when the zone distance is greater than than this value", + "waze_history_max_distance": "Do not save the Waze travel time & distance to the Waze History Database if the distance is greater than this value" + } + }, + "special_zones": { + "title": "Special Zones - Pass Through, Stationary Zone, Primay Home Base Zone", + "data": { + "passthru_zone_header": "═══════════════════════════════════════════════════ PASSTHRU ZONE ZONE", + "passthru_zone_time": "ENTER ZONE DELAY TIME ", + "stat_zone_header": "═══════════════════════════════════════════════════ STATIONARY ZONE", + "stat_zone_still_time": "NO MOVEMENT TIME ", + "stat_zone_inzone_interval": "INZONE INTERVAL ", + "stat_zone_fname": "FRIENDLY NAME - Name to display when in a Stationary Zone", + "base_offset_header": ".══════════════════════════════════════════ STATIONARY ZONE BASE LOCATION", + "stat_zone_base_latitude": "BASE LOCATION NORTH-SOUTH OFFSET", + "stat_zone_base_longitude": "BASE LOCATION EAST/WEST OFFSET", + "track_from_zone_header": "═══════════════════════════════════════════════════ PRIMARY BASE ZONE = HOME", + "track_from_base_zone": "PRIMARY 'HOME' ZONE - Use another zone if you are away from Home for an extended period (Global Override)", + "track_from_home_zone": "ALSO TRACK FROM HOME ZONE - Continue to track from the Home zone when the Primary Home Zone is not Home", + "opt_action": "═══════════════════════════════════════════════════" + }, + "data_description": { + "passthru_zone_time": "Delay processing a Zone Enter Trigger in case you are just passing through a zone", + "stat_zone_still_time": "Time at the same location before moving into a Stationary Zone", + "stat_zone_inzone_interval": "Time interval between location requests when in a Stationary Zone", + "stat_zone_fname": "Set a generic name here or set it for each Device on the Update Devices screen. All devices can display the same name (Stationary) or add the '[name]' wildcard to display part of the device name, followed by some text ('[name]Zone' --> 'GaryZone')", + "stat_zone_base_latitude": "Distance (±km) north-south of the Home Zone (or it's GPS Latitude)", + "stat_zone_base_longitude": "Distance (±km) east-west of the Home Zone (or it's GPS Longitude)" + } + }, + "sensors": { + "title": "Device and Tracking Sensors created by iCloud3", + "description": "Many sensors are used to display tracking results and other information for a device. This screen is used to select the sensors that should be created.", + "data": { + "monitored_devices": "MONITORED DEVICE SENSORS - Select the type of sensors to create for a Monitored Device", + "device": "DEVICE SENSORS - Device status and information", + "tracking_update": "LOCATION UPDATE SENSORS - Device location update times", + "tracking_time": "TIME SENSORS - Device tracking timers", + "tracking_distance": "DISTANCE SENSORS - Device tracking distances", + "track_from_zones": "TRACK FROM MULTIPLE ZONE SENSORS - Used when tracking from more than one zone (not needed for tracking only from the Home zone)", + "tracking_other": "OTHER TRACKING SENSORS - Not normally used but available", + "zone": "ZONE SENSORS - Device zone status and information", + "other": "OTHER SENSORS - Sensors not in the above areas", + "excluded_sensors": "EXCLUDED SENSORS - Sensors that will not be created when iCloud3 starts", + "opt_action": "═════════════════════════════════════════════════════" + } + }, + "exclude_sensors": { + "title": "Exclude Sensors", + "description": "Many sensors are created for the devices but there may be times when you want to not create a sensor for a specific device. For example, you may want to create a bettery sensor for all devices except one. This screen lets you specify the sensor entity name that should not be created.", + "data": { + "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", + "opt_action": "═════════════════════════════════════════════════════" + } } } } diff --git a/custom_components/icloud3/support/config_file.py b/custom_components/icloud3/support/config_file.py index 9d7f69c..a1692cc 100644 --- a/custom_components/icloud3/support/config_file.py +++ b/custom_components/icloud3/support/config_file.py @@ -368,8 +368,7 @@ 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): return password return f"««{base64_encode(password)}»»" diff --git a/custom_components/icloud3/support/determine_interval.py b/custom_components/icloud3/support/determine_interval.py index dd86b92..96852f0 100644 --- a/custom_components/icloud3/support/determine_interval.py +++ b/custom_components/icloud3/support/determine_interval.py @@ -34,7 +34,8 @@ EXIT_ZONE, ZONE, ZONE_INFO, INTERVAL, - DISTANCE, ZONE_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD, MAX_DISTANCE, + DISTANCE, ZONE_DISTANCE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, + MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD, TRAVEL_TIME, TRAVEL_TIME_MIN, DIR_OF_TRAVEL, MOVED_DISTANCE, LAST_LOCATED, LAST_LOCATED_TIME, LAST_LOCATED_DATETIME, LAST_UPDATE, LAST_UPDATE_TIME, LAST_UPDATE_DATETIME, @@ -58,13 +59,14 @@ import traceback # location_data fields -LD_STATUS = 0 -LD_ZONE_DIST = 1 -LD_MOVED = 2 -LD_WAZE_DIST = 3 -LD_CALC_DIST = 4 -LD_WAZE_TIME = 5 -LD_DIRECTION = 6 +LD_STATUS = 0 +LD_ZONE_DIST = 1 +LD_ZONE_DIST_M = 2 +LD_WAZE_DIST = 3 +LD_CALC_DIST = 4 +LD_WAZE_TIME = 5 +LD_MOVED = 6 +LD_DIRECTION = 7 #waze_from_zone fields WAZ_STATUS = 0 @@ -146,10 +148,11 @@ def determine_interval(Device, DeviceFmZone): return location_data[LD_ZONE_DIST] dist_from_zone_km = location_data[LD_ZONE_DIST] - dist_moved_km = location_data[LD_MOVED] + dist_from_zone_m = location_data[LD_ZONE_DIST_M] waze_dist_from_zone_km = location_data[LD_WAZE_DIST] calc_dist_from_zone_km = location_data[LD_CALC_DIST] waze_time_from_zone = location_data[LD_WAZE_TIME] + dist_moved_km = location_data[LD_MOVED] dir_of_travel = location_data[LD_DIRECTION] log_msg = ( f"DistFmZome-{dist_from_zone_km}, Moved-{dist_moved_km}, " @@ -448,16 +451,18 @@ def determine_interval(Device, DeviceFmZone): and Device.is_gps_poor and dist_moved_km < 1): dist_from_zone_km = DeviceFmZone.zone_dist + dist_from_zone_m = DeviceFmZone.zone_dist_m waze_dist_from_zone_km = DeviceFmZone.waze_dist calc_dist_from_zone_km = DeviceFmZone.calc_dist waze_time_from_zone = DeviceFmZone.waze_time else: #save for next poll if poor gps - DeviceFmZone.zone_dist = dist_from_zone_km - DeviceFmZone.waze_dist = waze_dist_from_zone_km - DeviceFmZone.waze_time = waze_time_from_zone - DeviceFmZone.calc_dist = calc_dist_from_zone_km + DeviceFmZone.zone_dist = dist_from_zone_km + DeviceFmZone.zone_dist_m = dist_from_zone_m + DeviceFmZone.waze_dist = waze_dist_from_zone_km + DeviceFmZone.waze_time = waze_time_from_zone + DeviceFmZone.calc_dist = calc_dist_from_zone_km waze_time_msg = Gb.Waze.waze_mins_to_time_str(waze_time_from_zone) @@ -489,11 +494,14 @@ def determine_interval(Device, DeviceFmZone): sensors[DISTANCE] = km_to_mi(dist_from_zone_km) sensors[MAX_DISTANCE] = km_to_mi(DeviceFmZone.max_dist_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 - DeviceFmZone.from_zone_radius_m) sensors[WAZE_DISTANCE] = km_to_mi(waze_dist_from_zone_km) 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.is_inzone: sensors[ZONE_INFO] = f"@{Device.loc_data_zone_fname}" else: @@ -569,11 +577,11 @@ def post_zone_time_dist_event_msg(Device, DeviceFmZone): ''' if Device.iosapp_monitor_flag: - iosapp_state = Device.iosapp_data_state + iosapp_state = zone_display_as(Device.iosapp_data_state) if iosapp_state == NOT_SET: iosapp_state = '──' - elif iosapp_state in Gb.Zones_by_zone: - iosapp_state = Gb.Zones_by_zone[iosapp_state].display_as + # elif iosapp_state in Gb.Zones_by_zone: + # iosapp_state = Gb.Zones_by_zone[iosapp_state].display_as else: iosapp_state = '──' @@ -708,14 +716,17 @@ def determine_interval_after_error(Device, counter=OLD_LOC_POOR_GPS_CNT): f"Retry at {secs_to_time(next_update_secs)} " f"({Device.DeviceFmZoneHome.interval_str})") - if event_msg == '': + if event_msg == '' and Device.update_sensors_error_msg != '': event_msg =(f"LocationData > {Device.update_sensors_error_msg}, " #f"(#{Device.old_loc_poor_gps_cnt}), " f"Update-{secs_to_time(next_update_secs)} " f"({Device.DeviceFmZoneHome.interval_str})") - post_event(devicename, event_msg) - log_info_msg(Device.devicename, f"Old Location/Other Error-{event_msg}") - # log_rawdata(f"{Device_devicename} - {from_zone}", DeviceFmZone.sensors) + Device.icloud_update_reason = "Newer Data is Available" + + if event_msg: + post_event(devicename, event_msg) + log_info_msg(Device.devicename, f"Old Location/Other Error-{event_msg}") + # log_rawdata(f"{Device_devicename} - {from_zone}", DeviceFmZone.sensors) except Exception as err: log_exception(err) @@ -735,9 +746,9 @@ def get_error_retry_interval(Device, counter=OLD_LOC_POOR_GPS_CNT): ''' if Device.is_offline: if Device.sensor_zone == NOT_SET: - return 120, 0, 0 + return 120, 0, 20 else: - return Gb.offline_interval_secs, 0, 0 + return Gb.offline_interval_secs, 0, 20 interval = 0 @@ -753,9 +764,12 @@ def get_error_retry_interval(Device, counter=OLD_LOC_POOR_GPS_CNT): error_cnt = Device.iosapp_request_loc_retry_cnt range_tbl = RETRY_INTERVAL_RANGE_2 else: + error_cnt = Device.old_loc_poor_gps_cnt + range_tbl = RETRY_INTERVAL_RANGE_1 interval = 60 max_error_cnt = int(list(range_tbl.keys())[-1]) + if max_error_cnt < 20: max_error_cnt = 20 # Retry in 10-secs if this is the first time retried #** 5/14 Change 10-secs to 5-secs @@ -868,12 +882,14 @@ def _get_distance_data(Device, DeviceFmZone): calc_dist_from_zone_km = dist_from_zone_km = DeviceFmZone.distance_km + dist_from_zone_m = dist_from_zone_km * 1000 waze_dist_moved_km = dist_moved_km = Device.loc_data_distance_moved waze_dist_from_zone_km = calc_dist_from_zone_km waze_time_from_zone = 0 last_dir_of_travel = DeviceFmZone.dir_of_travel from_zone = DeviceFmZone.from_zone + #if (calc_dist_from_zone_km <= .05 or Device.loc_data_zone == from_zone): # Device.loc_data_zone = from_zone # calc_dist_from_zone_km = 0 @@ -884,12 +900,14 @@ def _get_distance_data(Device, DeviceFmZone): Gb.Waze.waze_status = WAZE_PAUSED Gb.Waze.waze_close_to_zone_pause_flag = True distance_data = [VALID_DATA, - 0, # dist_from_zone_km, + 0, # dist_from_zone_km, + dist_from_zone_m, # # dist_from_zone_m, + 0, # waze_dist_from_zone_km, + calc_dist_from_zone_km, # calc_dist_from_zone_km, dist_moved_km, - 0, # waze_dist_from_zone_km, - 0, # calc_dist_from_zone_km, - 0, # waze_time_from_zone, + 0, # waze_time_from_zone, INZONE] + return distance_data #-------------------------------------------------------------------------------- @@ -897,13 +915,14 @@ def _get_distance_data(Device, DeviceFmZone): waze_source_msg = '' if Gb.Waze.is_status_USED: # See if this location hasn't changed or is in the history db - if calc_dist_from_zone_km < Gb.WazeHist.max_distance: + if Gb.WazeHist.is_historydb_USED and calc_dist_from_zone_km < Gb.WazeHist.max_distance: waze_status, waze_time_from_zone, waze_dist_from_zone_km, dist_moved_km, \ hist_db_location_id, waze_source_msg = \ Gb.Waze.get_history_time_distance(Device, DeviceFmZone, check_hist_db=True) else: hist_db_location_id = 0 + # Not in history db or history db is not used if hist_db_location_id == 0: waze_dist_from_zone_km = calc_dist_from_zone_km waze_time_from_zone = 0 @@ -933,6 +952,8 @@ def _get_distance_data(Device, DeviceFmZone): else: waze_source_msg += f"(< {format_dist_km(Gb.Waze.waze_min_distance)})" + dist_from_zone_m = dist_from_zone_km * 1000 + # Get Waze travel_time & distance if Gb.Waze.is_status_USED: if hist_db_location_id == 0: @@ -942,6 +963,7 @@ def _get_distance_data(Device, DeviceFmZone): Gb.Waze.waze_status = waze_status # Don't reset data if poor gps, use the best we have + dist_from_zone_m = dist_from_zone_km * 1000 Device.display_info_msg( f"Finalizing-{DeviceFmZone.info_status_msg}") if Device.loc_data_zone == from_zone: dist_from_zone_km = 0 @@ -949,7 +971,11 @@ def _get_distance_data(Device, DeviceFmZone): elif Gb.Waze.is_status_USED: dist_from_zone_km = waze_dist_from_zone_km + dist_from_zone_m = waze_dist_from_zone_km * 1000 dist_moved_km = waze_dist_moved_km + else: + waze_dist_from_zone_km = 0 + # waze_dist_moved_km = 0 if waze_source_msg: event_msg = f"Waze Route Info > {waze_source_msg}" @@ -1016,10 +1042,11 @@ def _get_distance_data(Device, DeviceFmZone): distance_data = [VALID_DATA, dist_from_zone_km, - dist_moved_km, + dist_from_zone_m, waze_dist_from_zone_km, calc_dist_from_zone_km, waze_time_from_zone, + dist_moved_km, dir_of_travel] return distance_data diff --git a/custom_components/icloud3/support/event_log.py b/custom_components/icloud3/support/event_log.py index 5fe7ae9..5261549 100644 --- a/custom_components/icloud3/support/event_log.py +++ b/custom_components/icloud3/support/event_log.py @@ -18,7 +18,7 @@ EVLOG_TIME_RECD, EVLOG_HIGHLIGHT, EVLOG_MONITOR, EVLOG_ERROR, EVLOG_ALERT, EVLOG_UPDATE_START, EVLOG_UPDATE_END, EVLOG_IC3_STARTING, EVLOG_IC3_STAGE_HDR, - CRLF, CRLF_DOT, CRLF_CHK, RARROW, DOT, + CRLF, CRLF_DOT, CRLF_CHK, RARROW, DOT, LT, GT, ) from ..helpers.common import instr, is_statzone, circle_letter @@ -235,13 +235,7 @@ def post_event(self, devicename, event_text='+'): event_text = event_text.replace('N/A', '') if len(event_text) == 0: event_text = 'Info Message' - event_text = event_text.replace('__', '') - event_text = event_text.replace('"', '`') - event_text = event_text.replace("'", "`") - event_text = event_text.replace('~','--') - event_text = event_text.replace('Background','Bkgnd') - event_text = event_text.replace('Geographic','Geo') - event_text = event_text.replace('Significant','Sig') + event_text = self._replace_special_chars(event_text) if self.display_text_as is None: self.display_text_as = {} @@ -669,15 +663,15 @@ def _export_ic3_event_log_reformat_recds(self, devicename, el_records): el_records.reverse() for record in el_records: devicename = record[ELR_DEVICENAME] - time = record[ELR_TIME] + time = record[ELR_TIME] if record[ELR_TIME] not in ['Debug', 'Rawdata'] else '' text = record[ELR_TEXT] # Time-record = {iosapp_state},{ic3_zone},{interval},{travel_time},{distance if text.startswith(EVLOG_UPDATE_START): - block_char = '\t┌─ ' + block_char = '\t\t\t┌─ ' inside_home_det_interval_flag = True elif text.startswith(EVLOG_UPDATE_END): - block_char = '\t└─ ' + block_char = '\t\t\t└─ ' inside_home_det_interval_flag = False elif text.startswith('Results:'): if time.startswith('»') and time.startswith('»Home') is False: @@ -694,10 +688,11 @@ def _export_ic3_event_log_reformat_recds(self, devicename, el_records): if text.startswith(EVLOG_TIME_RECD): text = text[3:] item = text.split(',') - text = (f"iOSAppState-{item[ELR_DEVICENAME]}, " - f"iCloud3Zone-{item[ELR_TIME]}, " - f"Interval-{item[ELR_TEXT]}, " - f"TravelTime-{item[3]}, Distance-{item[4]}") + text = (f"iOSAppState-{item[0]}, " + f"iCloud3Zone-{item[1]}, " + f"Interval-{item[2]}, " + f"TravelTime-{item[3]}, " + f"Distance-{item[4]}") text = text.replace("'", "").replace(' ', ' ').replace('
', ', ') text = text.replace(", ", ",").replace(' ', ' ') @@ -724,6 +719,20 @@ def _export_ic3_event_log_reformat_recds(self, devicename, el_records): except Exception as err: log_exception(err) +#-------------------------------------------------------------------- + @staticmethod + def _replace_special_chars(event_text): + event_text = event_text.replace('<', LT) + # event_text = event_text.replace('>', GT) + event_text = event_text.replace('__', '') + event_text = event_text.replace('"', '`') + event_text = event_text.replace("'", "`") + event_text = event_text.replace('~','--') + event_text = event_text.replace('Background','Bkgnd') + event_text = event_text.replace('Geographic','Geo') + event_text = event_text.replace('Significant','Sig') + + return event_text #-------------------------------------------------------------------- @staticmethod def uncompress_evlog_recd_special_characters(recd): diff --git a/custom_components/icloud3/support/icloud_data_handler.py b/custom_components/icloud3/support/icloud_data_handler.py index 991bc4b..e269b62 100644 --- a/custom_components/icloud3/support/icloud_data_handler.py +++ b/custom_components/icloud3/support/icloud_data_handler.py @@ -349,7 +349,7 @@ def update_device_with_latest_raw_data(Device, all_devices=False): else: reason_msg = ( f"NewData-{_RawData.location_time}/±{_RawData.gps_accuracy:.0f}m " f"vs {_Device.loc_data_time_gps}, ") - event_msg =(f"Rejected (#{Device.old_loc_poor_gps_cnt+1}) > " + event_msg =(f"Rejected (#{Device.old_loc_poor_gps_cnt}) > " f"{reason_msg}" f"Updated-{_RawData.tracking_method} data, " f"{Device.device_status_msg}") diff --git a/custom_components/icloud3/support/pyicloud_ic3.py b/custom_components/icloud3/support/pyicloud_ic3.py index 4b73e80..664a7ca 100644 --- a/custom_components/icloud3/support/pyicloud_ic3.py +++ b/custom_components/icloud3/support/pyicloud_ic3.py @@ -36,9 +36,10 @@ CONF_IC3_DEVICENAME, CONF_FAMSHR_DEVICENAME, CONF_FMF_EMAIL, ) from ..helpers.time_util import (time_now_secs, secs_to_time, timestamp_to_time_utcsecs, ) -from ..helpers.common import (instr, obscure_field, list_to_str, ) +from ..helpers.common import (instr, obscure_field, list_to_str, delete_file, ) from ..helpers.messaging import (post_event, post_monitor_msg, _trace, _traceha, log_info_msg, log_error_msg, log_debug_msg, log_warning_msg, log_rawdata, log_exception) +from .config_file import (encode_password, decode_password) from uuid import uuid1 from requests import Session, adapters @@ -101,7 +102,7 @@ def __init__(self, password): def filter(self, record): - #return True + # return True message = record.getMessage() if self.name in message: record.msg = message.replace(self.name, "*" * 8) @@ -164,7 +165,7 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ if (retry_cnt == 3 or response.status_code != 200 or response.ok is False): log_msg += (f", ResponseOK-{response.ok}, Headers-{response.headers}") #log_rawdata("PyiCloud_ic3 iCloud Response-Header", {'raw': log_msg}) - # log_rawdata("PyiCloud_ic3 iCloud Response-Data", {'filter': self.prefilter_rawdata(data)}) + log_rawdata("PyiCloud_ic3 iCloud Response-Data", {'filter': self.prefilter_rawdata(data)}) # log_rawdata("PyiCloud_ic3 iCloud Response-Data", {'raw': data}) for header in HEADER_DATA: @@ -186,7 +187,7 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ # 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("Re-authenticating Find My iPhone service") + log_debug_msg(f"Re-authenticating iCloud Account ({response.status_code})") try: # If 450, authentication requires a sign in to the account @@ -282,14 +283,19 @@ def _raise_error(code, reason): elif reason == 'Missing X-APPLE-WEBAUTH-TOKEN cookie': log_info_msg(f"iCloud Account Authentication is needed, No WebAuth Token") - api_error = PyiCloud2FARequiredException() + return + # api_error = PyiCloud2FARequiredException() # 2fa needed that has already been requested and processed elif reason == '2fa Already Processed': return + elif code == 403: + reason = f"Apple Verification Code not requested ({code})" + return + elif code in [400, 404]: - reason = f"Apple ID Validation Code Invalid ({code})" + reason = f"Apple Verification Code Invalid ({code})" elif reason == "ACCESS_DENIED": reason = (reason + ". Please wait a few minutes then try again." @@ -381,7 +387,8 @@ class PyiCloudService(): def __init__( self, apple_id, password=None, cookie_directory=None, session_directory=None, verify=True, client_id=None, with_family=True, - called_from='notset'): + called_from='notset', + verify_password=False): if not apple_id: msg = "The Apple iCloud account username is not specified" @@ -390,9 +397,16 @@ def __init__( self, apple_id, password=None, msg = "The Apple iCloud account password is not specified" raise PyiCloudFailedLoginException(msg) - self.user = {"accountName": apple_id, "password": password} - self.apple_id = apple_id - self.called_from = called_from + self.user = {"accountName": apple_id, "password": password} + self.apple_id = apple_id + self.username = apple_id + self.password = password + self.requires_2sa = self._check_2sa_needed + self.requires_2fa = False # This is set during the authentication function + self.token_password = password + self.called_from = called_from + self.verify_password = verify_password + self.update_requested_by = '' try: if 'Complete' in self.init_step_complete: @@ -416,7 +430,7 @@ def __init__( self, apple_id, password=None, Gb.PyiCloud = self if 'Authenticate' in self.init_step_needed: - post_monitor_msg(f"AUTHENTICATING iCloud Account Access, -{obscure_field(apple_id)} ({called_from})") + post_monitor_msg(f"AUTHENTICATING iCloud Account Access, {obscure_field(apple_id)} ({called_from})") self._set_step_inprocess('Authenticate') self.authenticate() self._set_step_completed('Authenticate') @@ -480,72 +494,86 @@ def authenticate(self, refresh_session=False, service=None): subsequent logins will not cause additional e-mails from Apple. ''' - login_successful = False + login_successful = False self.authenticate_method = "" + # Do not reset requires_2fa flag on a reautnenticate session + # It may have been set on first authentication + if refresh_session is False: + self.requires_2fa = False + + self.requires_2fa = self.requires_2fa or self._check_2fa_needed + # Validate token - Consider authenticated if token is valid (POST=validate) if (refresh_session is False and self.session_data.get("session_token") and 'dsid' in self.params): log_info_msg("Checking session token validity") - try: - self.data = self._validate_token() + # try: + if self._validate_token(): login_successful = True self.authenticate_method += ", ValidateToken" - except PyiCloudAPIResponseException: - msg = "Invalid authentication token, will log in from scratch." + + # except PyiCloudAPIResponseException: + # msg = "Invalid authentication token, will log in from scratch." # 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("Authenticating as %s for %s" % (self.user["accountName"], service)) - try: - self._authenticate_with_credentials_service(service) - + log_debug_msg( f"AUTHENTICATING iCloud Account Access, " + f"{obscure_field(self.user['accountName'])}, " + f"Service-{service}") + # try: + if self._authenticate_with_password_service(service): login_successful = True - self.authenticate_method += (f", TrustToken/ServiceLogin") + self.authenticate_method += (f", Password") - except: + # except: + else: log_debug_msg("Could not log into service. Attempting brand new login.") # Authenticate - Sign into icloud account (POST=/signin) if login_successful is False: - log_info_msg(f"Authenticating account {obscure_field(self.user['accountName'])}, Using Account/PasswordSignin") - - data = dict(self.user) - data["rememberMe"] = True - data["trustTokens"] = [] + log_info_msg(f"Authenticating account {obscure_field(self.user['accountName'])}") - if self.session_data.get("trust_token"): - data["trustTokens"] = [self.session_data.get("trust_token")] + 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 += ", TrustToken" + login_successful = True - headers = self._get_auth_headers() + if login_successful is False or self.verify_password: + try: + if self._authenticate_with_password(): + login_successful = False + self.authenticate_method += ", Password" + else: + login_successful = False + msg = "Login Error (Invalid username/password)/555" + raise PyiCloudFailedLoginException(msg) - 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") + except PyiCloudAPIResponseException as error: + login_successful = False + msg = "Login Error (Invalid username/password)/562" + raise PyiCloudFailedLoginException(msg) - try: - req = self.Session.post( - f"{self.AUTH_ENDPOINT}/signin", - params={"isRememberMeEnabled": "true"}, - data=json.dumps(data), - headers=headers,) - self.authenticate_method += ", Account/PasswordSignin" + if self._authenticate_with_token(): + login_successful = True + self.authenticate_method += "+TrustToken" - except PyiCloudAPIResponseException as error: - msg = "Invalid email/password combination." - raise PyiCloudFailedLoginException(msg, error) + if login_successful == False: + self.authenticate_method += ", ERROR-Invalid username/password" + msg = "Login Error (Invalid username/password)/571" + raise PyiCloudFailedLoginException(msg) - self._authenticate_with_token() + self.requires_2fa = self.requires_2fa or self._check_2fa_needed - self._webservices = self.data["webservices"] + self._update_token_password_file() self.authenticate_method = self.authenticate_method[2:] log_info_msg(f"Authentication completed successfully, method-{self.authenticate_method}") @@ -553,6 +581,7 @@ def authenticate(self, refresh_session=False, service=None): #---------------------------------------------------------------------------- def _authenticate_with_token(self): '''Authenticate using session token. Return True if successful.''' + data = {"accountCountryCode": self.session_data.get("account_country"), "dsWebAuthToken": self.session_data.get("session_token"), "extended_login": True, @@ -564,19 +593,68 @@ def _authenticate_with_token(self): f"?clientBuildNumber=2021Project52&clientMasteringNumber=2021B29" f"&clientId={self.client_id[5:]}", data=json.dumps(data)) - data = req.json() + except PyiCloudAPIResponseException as error: - msg = "Invalid authentication token." - raise PyiCloudFailedLoginException(msg, error) + msg = "Invalid authentication token" + return False + # raise PyiCloudFailedLoginException(msg, error) + + except Exception as err: + return False self.data = req.json() + + if 'webservices' not in self.data: + if (self.data.get('success', False) is False + or self.data.get('error', 1) == 1): + return False + self._webservices = self.data["webservices"] self._update_dsid(self.data) + return True #---------------------------------------------------------------------------- - def _authenticate_with_credentials_service(self, service): + def _authenticate_with_password(self): + ''' + Sign into Apple account with password + + Return - True - No errors, + ''' + + 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") + + try: + req = self.Session.post( + f"{self.AUTH_ENDPOINT}/signin", + params={"isRememberMeEnabled": "true"}, + data=json.dumps(data), + headers=headers,) + + return True + + except PyiCloudAPIResponseException as error: + login_successful = False + msg = "Login Error (Invalid username/password)/557" + raise PyiCloudFailedLoginException(msg) + + return False + +#---------------------------------------------------------------------------- + def _authenticate_with_password_service(self, service): '''Authenticate to a specific service using credentials.''' data = {"appName": service, "apple_id": self.user["accountName"], @@ -588,35 +666,51 @@ def _authenticate_with_credentials_service(self, service): } try: - log_debug_msg("Authenticate with credentials requested") + log_debug_msg(f"Authenticating Service with Credentials, Service-{service}") + + self.Session.post(f"{self.SETUP_ENDPOINT}/accountLogin" f"?clientBuildNumber=2021Project52&clientMasteringNumber=2021B29" f"&clientId={self.client_id[5:]}", data=json.dumps(data)) - self.data = self._validate_token() - except Exception as err: - log_exception(err) + self._validate_token() + + # self.requires_2fa = self.requires_2fa or self._check_2fa_needed + + return True except PyiCloudAPIResponseException as error: log_exception(error) - msg = "Invalid username/password combination." + msg = "Login Error (Invalid username/password)" raise PyiCloudFailedLoginException(msg, error) + except Exception as err: + log_exception(err) + + return False #---------------------------------------------------------------------------- def _validate_token(self): '''Checks if the current access token is still valid.''' log_debug_msg("Checking session token validity") + try: req = self.Session.post("%s/validate" % self.SETUP_ENDPOINT, data="null") - log_debug_msg("Session token is still valid") - return req.json() + self.data = req.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}") + + return True except PyiCloudAPIResponseException as err: log_debug_msg("Invalid authentication token") raise err + return False + #---------------------------------------------------------------------------- def _update_dsid(self, data): try: @@ -692,6 +786,11 @@ def _setup_PyiCloudSession(self, session_directory): if not path.exists(self.session_directory): mkdir(self.session_directory) + self._read_token_password_file() + + if self.password != self.token_password: + delete_file('session', self.session_directory, self.cookie_filename) + try: self.session_data = {} with open(self.session_directory_filename) as session_f: @@ -721,18 +820,60 @@ def _setup_PyiCloudSession(self, session_directory): log_warning_msg(f"Failed to read cookie file {self.cookie_directory_filename}") #---------------------------------------------------------------------------- + ''' + The token password file stores the encoded password associated with the session + token. It is used to see if the user is changing the username's password. It so, + the session and it's token must be deleted to create a session token and cause a + 2fa verification. It this is not done, the user will be logged into the session + without checking the password and a password change will not be handled until the + token wxpires. + ''' + def _read_token_password_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']) + + except: + self.token_password = self.password + + def _update_token_password_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) + + except: + log_warning_msg(f"Failed to update tokenpw file {self.tokenpw_directory_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)]) + @property def cookie_directory_filename(self): '''Get path for cookiejar file.''' - return path.join( - self.cookie_directory, - "".join([c for c in self.user.get("accountName") if match(r"\w", c)]),) + return path.join(self.cookie_directory, + "".join([c for c in self.user.get("accountName") if match(r"\w", c)]),) @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)]),) + "".join([c for c in self.user.get("accountName") if match(r"\w", c)]),) + + @property + def tokenpw_directory_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" @property def authentication_method(self): @@ -748,35 +889,41 @@ def authentication_method(self): return authenticate_method @property - def requires_2sa(self): + def _check_2sa_needed(self): '''Returns True if two-step authentication is required.''' - needs_2sa_flag = (self.data.get("dsInfo", {}).get("hsaVersion", 0) >= 1 - and (self.data.get("hsaChallengeRequired", False)) - or not self.is_trusted_session) + 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 + return needs_2sa_flag + + except: + return False @property - def requires_2fa(self): + def _check_2fa_needed(self): '''Returns True if two-factor authentication is required.''' try: needs_2fa_flag = (self.data.get("dsInfo", {}).get("hsaVersion", 0) == 2 - and (self.data.get("hsaChallengeRequired", False) - or not self.is_trusted_session)) + and (self.is_challenge_required or self.is_trusted_browser is False)) - except KeyError: - needs_2fa_flag = True except Exception as err: log_exception(err) return False if needs_2fa_flag: - log_debug_msg(f"NEEDS-2FA, is_trusted_session-{self.is_trusted_session}") - + log_debug_msg(f"NEEDS-2FA, ChallengeRequired-{self.is_challenge_required}, " + f"TrustedBrowser-{self.is_trusted_browser}") return needs_2fa_flag @property - def is_trusted_session(self): + def is_challenge_required(self): + '''Returns True if the challenge code is needed.''' + return self.data.get("hsaChallengeRequired", False) + + @property + def is_trusted_browser(self): '''Returns True if the session is trusted.''' return self.data.get("hsaTrustedBrowser", False) @@ -800,29 +947,6 @@ def send_verification_code(self, device): return request.json().get("success", False) -#---------------------------------------------------------------------------- - def validate_verification_code(self, device, code): - '''Verifies a verification code received on a trusted device.''' - device.update({"verificationCode": code, "trustBrowser": True}) - data = json.dumps(device) - - try: - self.Session.post("%s/validateVerificationCode" % self.SETUP_ENDPOINT, - params=self.params, - data=data,) - - except PyiCloudAPIResponseException as error: - if error.code == -21669: - # Wrong verification code - return False - raise - - # Re-authenticate, which will both update the HSA data, and - # ensure that we save the X-APPLE-WEBAUTH-HSA-TRUST cookie. - self.authenticate() - - return not self.requires_2sa - #---------------------------------------------------------------------------- def validate_2fa_code(self, code): '''Verifies a verification code received via Apple's 2FA system (HSA2).''' @@ -837,21 +961,39 @@ def validate_2fa_code(self, code): headers["X-Apple-ID-Session-Id"] = self.session_data.get("session_id") try: - self.Session.post(f"{self.AUTH_ENDPOINT}/verify/trusteddevice/securitycode", + req = self.Session.post(f"{self.AUTH_ENDPOINT}/verify/trusteddevice/securitycode", data=json.dumps(data), headers=headers,) except PyiCloudAPIResponseException as error: # Wrong verification code if error.code == -21669: - log_error_msg("Code verification failed") + log_error_msg("Incorrect verification code") return False raise - log_debug_msg("Code verification successful") + except Exception as err: + log_exception(err) + return False + + # _trace(f"{len(req)=} {req=}") + try: + data = req.json() + 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 was accepted") self.trust_session() - return not self.requires_2sa + + self.requires_2fa = self.requires_2fa or self._check_2fa_needed + + # Return true if 2fa code was successful + return not self.requires_2fa #---------------------------------------------------------------------------- def trust_session(self): @@ -867,12 +1009,16 @@ def trust_session(self): try: self.Session.get(f"{self.AUTH_ENDPOINT}/2sv/trust", headers=headers,) - self._authenticate_with_token() + if self._authenticate_with_token(): + self.authenticate_method += "+TrustToken" + self.requires_2fa = self._check_2fa_needed return True except PyiCloudAPIResponseException: log_error_msg("Session trust failed") - return False + self.requires_2fa = self.requires_2fa or self._check_2fa_needed + + return False #---------------------------------------------------------------------------- def _get_webservice_url(self, ws_key): @@ -1115,7 +1261,9 @@ def update_device_location_data(self, requested_by_devicename=None, device_data= if device_data is None: return + self.PyiCloud.update_requested_by = requested_by_devicename monitor_msg = f"UPDATED FamShr Data > RequestedBy-{requested_by_devicename}" + for device_info in device_data: device_id = device_info[ID] device_data_name = device_info[NAME] @@ -1153,8 +1301,12 @@ def update_device_location_data(self, requested_by_devicename=None, device_data= _RawData.save_new_device_data(device_info) requested_by_flag = ' *' if requested_by_devicename == _RawData.devicename else '' - log_rawdata(f"FamShr Data - " - f"<{device_data_name}/{_RawData.devicename}>", _RawData.device_data) + #log_rawdata(f"FamShr Data - " + # f"<{device_data_name}/{_RawData.devicename}>", _RawData.device_data) + if _RawData.devicename == 'gary_iphone': + log_rawdata(f"FamShr Data - " + f"<{device_data_name}/{_RawData.devicename}>", {'raw': _RawData.device_data}) + monitor_msg += (f"{CRLF_DOT}" f"{_RawData.devicename}, " @@ -1373,12 +1525,11 @@ def refresh_client(self, requested_by_devicename=None, refreshing_poor_loc_flag= self.response = {} log_debug_msg("No data returned on friends refresh request") - if requested_by_devicename: - post_monitor_msg(f"FmF iCloudData Update RequestedBy-{requested_by_devicename}") + self.PyiCloud.update_requested_by = requested_by_devicename + monitor_msg = (f"FmF iCloudData Update RequestedBy-{requested_by_devicename}") try: device_data_name = '' - monitor_msg = f"UPDATED FmF Data > " for device_info in self.response.get('locations', {}): device_id = device_info[ID] @@ -1397,7 +1548,10 @@ def refresh_client(self, requested_by_devicename=None, refreshing_poor_loc_flag= _RawData.save_new_device_data(device_info) requested_by_flag = ' *' if requested_by_devicename == _RawData.devicename else '' - log_rawdata(f"FmF Data - <{_RawData.devicename}>", _RawData.device_data) + #log_rawdata(f"FmF Data - <{_RawData.devicename}>", _RawData.device_data) + if _RawData.devicename == 'gary_iphone': + log_rawdata(f"FmF Data - " + f"<{device_data_name}/{_RawData.devicename}>", {'raw': _RawData.device_data}) monitor_msg += (f"{CRLF_DOT}" f"{_RawData.devicename}, " diff --git a/custom_components/icloud3/support/pyicloud_ic3_interface.py b/custom_components/icloud3/support/pyicloud_ic3_interface.py index d4ac377..c60a20f 100644 --- a/custom_components/icloud3/support/pyicloud_ic3_interface.py +++ b/custom_components/icloud3/support/pyicloud_ic3_interface.py @@ -11,10 +11,10 @@ from ..support.pyicloud_ic3 import (PyiCloudService, PyiCloudFailedLoginException, PyiCloudNoDevicesException, PyiCloudAPIResponseException, PyiCloud2FARequiredException,) -from ..helpers.common import (instr, is_statzone, list_to_str, ) +from ..helpers.common import (instr, is_statzone, list_to_str, delete_file, ) from ..helpers.messaging import (post_event, post_error_msg, post_monitor_msg, log_debug_msg, log_info_msg, log_exception, log_error_msg, internal_error_msg2, _trace, _traceha, ) -from ..helpers.time_util import (time_now_secs, secs_to_time, secs_to_datetime, secs_to_time_str, format_age, +from ..helpers.time_util import (time_secs, secs_to_time, secs_to_datetime, secs_to_time_str, format_age, secs_to_time_age_str, ) import os @@ -142,7 +142,7 @@ def authenticate_icloud_account(PyiCloud, called_from='unknown', initial_setup=F return try: - Gb.pyicloud_auth_started_secs = time.time() + Gb.pyicloud_auth_started_secs = time_secs() if PyiCloud and 'Complete' in Gb.PyiCloudInit.init_step_complete: PyiCloud.authenticate(refresh_session=True, service='find') @@ -159,7 +159,7 @@ def authenticate_icloud_account(PyiCloud, called_from='unknown', initial_setup=F session_directory=(f"{Gb.icloud_cookies_dir}/session"), called_from=called_from) - authentication_took_secs = time.time() - Gb.pyicloud_auth_started_secs + authentication_took_secs = time_secs() - Gb.pyicloud_auth_started_secs Gb.pyicloud_calls_time += authentication_took_secs if Gb.authentication_error_retry_secs != HIGH_INTEGER: Gb.authenticated_time = 0 @@ -169,13 +169,16 @@ def authenticate_icloud_account(PyiCloud, called_from='unknown', initial_setup=F is_authentication_2fa_code_needed(PyiCloud, initial_setup=True) reset_authentication_time(PyiCloud, authentication_took_secs) - except (PyiCloudAPIResponseException, PyiCloudFailedLoginException, - PyiCloudNoDevicesException) as err: - event_msg =(f"iCloud3 Error > An error occurred communicating with " + 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}") + + except PyiCloudFailedLoginException as err: + event_msg =(f"{EVLOG_ALERT}iCloud3 Error > An error occurred logging into the iCloud Account. " + f"Authentication Process/Error-{Gb.PyiCloud.authenticate_method[2:]})") post_error_msg(event_msg) check_all_devices_online_status() return False @@ -200,10 +203,14 @@ def reset_authentication_time(PyiCloud, authentication_took_secs): if authentication_method == '': return - Gb.pyicloud_auth_started_secs = 0 - Gb.pyicloud_authentication_cnt += 1 last_authenticated_time = Gb.authenticated_time - Gb.authenticated_time = time_now_secs() + last_authenticated_age = time_secs() - last_authenticated_time + if last_authenticated_age <= 1 or authentication_took_secs > 360: + authentication_took_secs = 0 + + Gb.authenticated_time = time_secs() + Gb.pyicloud_authentication_cnt += 1 + Gb.pyicloud_auth_started_secs = 0 event_msg =(f"iCloud Account Authenticated " f"(#{Gb.pyicloud_authentication_cnt}) > LastAuth-") @@ -211,9 +218,11 @@ def reset_authentication_time(PyiCloud, authentication_took_secs): event_msg += "Never (Initializing)" else: event_msg += (f"{secs_to_time(last_authenticated_time)} " - f" ({format_age(time_now_secs() - last_authenticated_time)})") - event_msg += (f", Method-{authentication_method}, " - f"Took-{secs_to_time_str(authentication_took_secs)}") + f" ({format_age(last_authenticated_age)})") + event_msg += f", Method-{authentication_method}" + event_msg += f", By-{Gb.PyiCloud.update_requested_by}" + if authentication_took_secs > 2: + event_msg += f", Took-{secs_to_time_str(authentication_took_secs)}" post_event(event_msg) #-------------------------------------------------------------------- @@ -332,8 +341,13 @@ def pyicloud_reset_session(): try: post_event(f"{EVLOG_IC3_STARTING}Apple ID Verification - Started") - _cookies_file_rename('cookies', Gb.PyiCloud.cookie_directory_filename) - _cookies_file_rename('session', Gb.PyiCloud.session_directory_filename) + cookie_directory = Gb.PyiCloud.cookie_directory + cookie_filename = Gb.PyiCloud.cookie_filename + session_directory = f"{cookie_directory}/session" + + delete_file('iCloud Acct cookies', cookie_directory, cookie_filename, delete_old_sv_file=True) + delete_file('iCloud Acct session', session_directory, cookie_filename, delete_old_sv_file=True) + delete_file('iCloud Acct tokenpw', session_directory, f"{cookie_filename}.tpw") post_event(f"iCloud initializing interface") Gb.PyiCloud.__init__( Gb.username, Gb.password, @@ -355,7 +369,7 @@ def pyicloud_reset_session(): log_exception(err) #-------------------------------------------------------------------- -def create_PyiCloudService_secondary(username, password, called_from): +def create_PyiCloudService_secondary(username, password, called_from, verify_password): ''' Create the PyiCloudService object without going through the error checking and authentication test routines. This is used by config_flow to open a second @@ -364,29 +378,5 @@ def create_PyiCloudService_secondary(username, password, called_from): return PyiCloudService( username, password, cookie_directory=Gb.icloud_cookies_dir, session_directory=(f"{Gb.icloud_cookies_dir}/session"), - called_from=called_from) - - -#-------------------------------------------------------------------- -def _cookies_file_rename(file_desc, directory_filename, save_extn='sv'): - try: - file_msg = "" - directory_filename_sv = (f"{directory_filename}.{save_extn}") - filename = directory_filename.replace(Gb.PyiCloud.cookie_directory, '') - filename_sv = directory_filename_sv.replace(Gb.PyiCloud.cookie_directory, '') - - if os.path.isfile(directory_filename_sv): - os.remove(directory_filename_sv) - file_msg += (f"{CRLF_DOT}Delete backup file (...{filename_sv})") - - if os.path.isfile(directory_filename): - os.rename(directory_filename, directory_filename_sv) - file_msg += (f"{CRLF_DOT}Rename current file to ...{filename}.{save_extn})") - - if file_msg != "": - event_msg =(f"Current iCloud {file_desc} file > " - f"{CRLF}•{directory_filename}{file_msg}") - post_event(event_msg) - - except Exception as err: - log_exception(err) + called_from=called_from, + verify_password=verify_password) diff --git a/custom_components/icloud3/support/restore_state.py b/custom_components/icloud3/support/restore_state.py index 51fbc34..f0ab038 100644 --- a/custom_components/icloud3/support/restore_state.py +++ b/custom_components/icloud3/support/restore_state.py @@ -89,6 +89,8 @@ def read_storage_icloud3_restore_state_file(): devicename_data['sensors'][DISTANCE_TO_OTHER_DEVICES_DATETIME] = HHMMSS_ZERO return True + except JSONDecodeError: + pass except Exception as err: log_exception(err) return False diff --git a/custom_components/icloud3/support/start_ic3.py b/custom_components/icloud3/support/start_ic3.py index ea41d99..cf81d41 100644 --- a/custom_components/icloud3/support/start_ic3.py +++ b/custom_components/icloud3/support/start_ic3.py @@ -66,6 +66,7 @@ open_ic3_debug_log_file, close_ic3_debug_log_file, _trace, _traceha, ) from ..helpers.dist_util import (format_dist_km, ) +from ..helpers.time_util import (secs_to_time_str, ) import os import json @@ -317,8 +318,6 @@ def set_global_variables_from_conf_parameters(evlog_msg=True): evlog_table_max_cnt = set_evlog_table_max_cnt() config_event_msg += f"{CRLF_DOT}Set Event Log Record Limits ({evlog_table_max_cnt} Events)" - # set_zone_display_as() - if evlog_msg: post_event(config_event_msg) @@ -355,7 +354,7 @@ def set_icloud_username_password(): ''' Set up icloud username/password and devices from the configuration parameters ''' - Gb.username = Gb.conf_tracking[CONF_USERNAME] + Gb.username = Gb.conf_tracking[CONF_USERNAME].lower() Gb.username_base = Gb.username.split('@')[0] Gb.password = Gb.conf_tracking[CONF_PASSWORD] Gb.encode_password_flag = Gb.conf_tracking[CONF_ENCODE_PASSWORD] @@ -512,12 +511,11 @@ def set_zone_display_as(): return zone_msg = '' - Gb.zone_display_as = {} + Gb.zone_display_as = NON_ZONE_ITEM_LIST.copy() for zone, Zone in Gb.Zones_by_zone.items(): if is_statzone(zone) is False: Zone.setup_zone_display_name() - Gb.zone_display_as[zone] = Zone.display_as if Zone.radius_m > 1: crlf_dot_x = CRLF_X if Zone.passive else CRLF_DOT @@ -805,38 +803,40 @@ def create_Zones_object(): #log_msg = (f"Reloading Zone.yaml config file") #log_debug_msg(log_msg) - # zone_entities = Gb.hass.states.entity_ids(ZONE) - ha_zones, zone_entity_data = entity_io.get_entity_registry_data(platform=ZONE) + zone_entities = Gb.hass.states.entity_ids(ZONE) + er_zones, zone_entity_data = entity_io.get_entity_registry_data(platform=ZONE) Gb.state_to_zone = STATE_TO_ZONE_BASE.copy() OldZones_by_zone = Gb.Zones_by_zone.copy() Gb.Zones = [] Gb.Zones_by_zone = {} - Gb.zone_display_as = {} + Gb.zone_display_as = NON_ZONE_ITEM_LIST.copy() # Add away, not_set, not_home, stationary, etc. so display_name is set # for these zones/states. Radius=0 is used to ypass normal zone processing. for zone, display_as in NON_ZONE_ITEM_LIST.items(): + # if zone.lower() in Gb.Zones_by_zone: + # continue + if zone in OldZones_by_zone: Zone = OldZones_by_zone[zone] else: - zone_data ={NAME: display_as, TITLE: display_as, FNAME: display_as, + zone_data ={ZONE: zone, NAME: display_as, TITLE: display_as, FNAME: display_as, FRIENDLY_NAME: display_as, RADIUS: 0} Zone = iCloud3_Zone(zone, zone_data) - # Zone.display_as = display_as + Gb.Zones.append(Zone) Gb.Zones_by_zone[zone] = Zone - _traceha(f"{Zone.name=} {Zone.fname=} {Zone.title=} {Zone.display_as=}") - _traceha(f"{Gb.zone_display_as=}") zone_msg = '' - for zone in ha_zones: + for zone in er_zones: try: zone_entity_name = f"zone.{zone}" zone_data = entity_io.get_attributes(zone_entity_name) if (zone_entity_name in zone_entity_data and ID in zone_entity_data[zone_entity_name]): + zone_data[ZONE] = zone zone_data[ID] = zone_entity_data[zone_entity_name][ID] zone_data['unique_id'] = zone_entity_data[zone_entity_name]['unique_id'] zone_data['original_name'] = zone_entity_data[zone_entity_name]['original_name'] @@ -889,26 +889,30 @@ def create_Zones_object(): f"{circle_letter(Gb.track_from_base_zone)}") post_event(event_msg) + + event_msg = "Special Zone Setup >" + if Gb.is_passthru_zone_used: + event_msg += f"{CRLF_DOT}PassThru Zone > Delay-{secs_to_time_str(Gb.passthru_zone_interval_secs)}" + else: + event_msg += f"{CRLF_DOT}PASSTHRU ZONE IS NOT BEING USED" + dist = Gb.HomeZone.distance_km(Gb.stat_zone_base_latitude, Gb.stat_zone_base_longitude) home_zone_radius_km = Gb.HomeZone.radius_km min_dist_from_zone_km = round(home_zone_radius_km * 2, 2) dist_move_limit = round(home_zone_radius_km * 1.5, 2) - event_msg = "Special Zone Setup >" if Gb.is_stat_zone_used: - event_msg += ( f"{CRLF_DOT}Stationary Zone Base Information > " - f"BaseLocation-{format_gps(Gb.stat_zone_base_latitude, Gb.stat_zone_base_longitude, 0)}, " + event_msg += ( f"{CRLF_DOT}Stationary Zone > " f"Radius-{Gb.HomeZone.radius_m * 2}m, " - f"DistFromHome-{format_dist_km(dist)}, " - f"MinDistFromZone-{format_dist_km(min_dist_from_zone_km)}, " - f"DistMoveLimit-{format_dist_km(dist_move_limit)}") + f"DistMoveLimit-{format_dist_km(dist_move_limit)}, " + f"MinDistFromAnotherZone-{format_dist_km(min_dist_from_zone_km)}, " + f"BaseDistFromHome-{format_dist_km(dist)}, " + f"BaseLocation-{format_gps(Gb.stat_zone_base_latitude, Gb.stat_zone_base_longitude, 0)}") else: event_msg += f"{CRLF_DOT}STATIONARY ZONES ARE NOT BEING USED" - if Gb.is_passthru_zone_used is False: - event_msg += f"{CRLF_DOT}PASSTHRU ZONE IS NOT BEING USED" - post_event(event_msg) - + post_event(event_msg + ) # Cycle thru the Device's conf and get all zones that are tracked from for all devices Gb.TrackedZones_by_zone = {} for conf_device in Gb.conf_devices: @@ -1056,13 +1060,11 @@ def create_Devices_object(): return - -######################################################### +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # -# INITIALIZE PYICLOUD DEVICE API -# DEVICE SETUP SUPPORT FUNCTIONS FOR MODES FMF, FAMSHR, IOSAPP +# ICLOUD3 STARTUP MODULES -- STAGE 4 # -######################################################### +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> def setup_tracked_devices_for_famshr(PyiCloud=None): ''' @@ -1090,8 +1092,7 @@ def setup_tracked_devices_for_famshr(PyiCloud=None): post_event(event_msg) return - elif (_FamShr is None - or _FamShr.device_fname_by_device_id == {}): + elif _FamShr is None or _FamShr.device_fname_by_device_id == {}: event_msg += "NO DEVICES FOUND" post_event(event_msg) return @@ -1273,8 +1274,7 @@ def setup_tracked_devices_for_fmf(PyiCloud=None): post_event(event_msg) return - elif (_FmF is None - or _FmF.device_id_by_fmf_email == {}): + elif _FmF is None or _FmF.device_id_by_fmf_email == {}: event_msg += "NO DEVICES FOUND" post_event(event_msg) return @@ -1730,35 +1730,13 @@ def setup_notify_service_name_for_iosapp_devices(post_event_msg=False): # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -def display_object_lists(): - ''' - Display the object list values - ''' - broadcast_info_msg(f"Logging Initial Monitor Info") - monitor_msg = (f"StatZones-{Gb.StatZones_by_devicename}") - post_monitor_msg(monitor_msg) - - monitor_msg = (f"Devices-{Gb.Devices_by_devicename}") - post_monitor_msg(monitor_msg) - - for Device in Gb.Devices: - monitor_msg = (f"Device-{Device.devicename}, " - f"DeviceFmZones-{Device.DeviceFmZones_by_zone}") - post_monitor_msg(monitor_msg) - - monitor_msg = (f"Zones-{Gb.Zones_by_zone}") - post_monitor_msg(monitor_msg) - - -#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -# -# ICLOUD3 STARTUP MODULES -- STAGE 4 -# -#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - def remove_unverified_untrackable_devices(PyiCloud=None): - if PyiCloud is None: PyiCloud = Gb,PyiCloud + 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 @@ -1856,7 +1834,7 @@ def setup_trackable_devices(): create_Device_StationaryZone_object(Device) info_msg = f"Stationary Zone: {Device.stationary_zonename} ({Device.StatZone.display_as})" - Device.display_info_msg(f"{DOT2}{info_msg}") + Device.display_info_msg(f"{info_msg}") event_msg += f"{CRLF_DOT}{info_msg}" if Device.track_from_base_zone != HOME: @@ -1895,6 +1873,26 @@ def display_inactive_devices(): f"{CRLF}3. Exit the Configuration and Restart iCloud3") post_event(event_msg) +#------------------------------------------------------------------------------ +def display_object_lists(): + ''' + Display the object list values + ''' + broadcast_info_msg(f"Logging Initial Monitor Info") + monitor_msg = (f"StatZones-{Gb.StatZones_by_devicename}") + post_monitor_msg(monitor_msg) + + monitor_msg = (f"Devices-{Gb.Devices_by_devicename}") + post_monitor_msg(monitor_msg) + + for Device in Gb.Devices: + monitor_msg = (f"Device-{Device.devicename}, " + f"DeviceFmZones-{Device.DeviceFmZones_by_zone}") + post_monitor_msg(monitor_msg) + + monitor_msg = (f"Zones-{Gb.Zones_by_zone}") + post_monitor_msg(monitor_msg) + #------------------------------------------------------------------------------ def create_Device_StationaryZone_object(Device): diff --git a/custom_components/icloud3/support/waze.py b/custom_components/icloud3/support/waze.py index f139b52..3650a68 100644 --- a/custom_components/icloud3/support/waze.py +++ b/custom_components/icloud3/support/waze.py @@ -70,6 +70,10 @@ def is_status_USED(self): return (self.waze_status == WAZE_USED and Gb.Waze.distance_method_waze_flag) + @property + def is_historydb_USED(self): + return Gb.WazeHist.use_wazehist_flag + @property def is_status_NOT_USED(self): return self.waze_status == WAZE_NOT_USED @@ -130,11 +134,12 @@ def get_route_time_distance(self, Device, DeviceFmZone, check_hist_db=True): waze_source_msg = "" location_id = 0 - waze_status, route_time, route_dist_km, dist_moved_km, \ - location_id, waze_source_msg = \ - self.get_history_time_distance(Device, DeviceFmZone, check_hist_db=True) + if self.is_historydb_USED: + waze_status, route_time, route_dist_km, dist_moved_km, \ + location_id, waze_source_msg = \ + self.get_history_time_distance(Device, DeviceFmZone, check_hist_db=True) - # Get data from Waze if not in history and not being reused + # Get data from Waze if not in history and not being reused or if history is not used if location_id == 0: waze_status, route_time, route_dist_km = \ self.get_waze_distance( @@ -155,7 +160,7 @@ def get_route_time_distance(self, Device, DeviceFmZone, check_hist_db=True): # Add a time/distance record to the waze history database try: - if (Gb.waze_history_database_used + if (self.is_historydb_USED and Gb.wazehist_zone_id and DeviceFmZone.distance_km < Gb.WazeHist.max_distance and route_time > .25 @@ -258,7 +263,7 @@ def get_history_time_distance(self, Device, DeviceFmZone, check_hist_db=True): location_id = -2 waze_source_msg = "Using Previous Waze Location Info " - elif check_hist_db is False or Gb.WazeHist.use_wazehist_flag is False: + elif check_hist_db is False or self.is_historydb_USED is False: location_id = 0 else: @@ -325,7 +330,8 @@ def get_waze_distance(self, Device, DeviceFmZone, from_lat, from_long, continue route_time = round(route_time, 2) - route_dist_km = round(route_dist_km, 2) + # route_dist_km = round(route_dist_km, 2) + route_dist_km = route_dist_km Device.count_waze_locates += 1 Device.time_waze_calls += (time_now_secs() - waze_call_start_time) diff --git a/custom_components/icloud3/support/waze_history.py b/custom_components/icloud3/support/waze_history.py index 7997808..3d7b846 100644 --- a/custom_components/icloud3/support/waze_history.py +++ b/custom_components/icloud3/support/waze_history.py @@ -178,6 +178,11 @@ def __init__(self, wazehist_used, max_distance, track_direction): if self.use_wazehist_flag and self.connection is None: self.open_waze_history_database(wazehist_database) +#-------------------------------------------------------------------- + @property + def is_historydb_USED(self): + return self.use_wazehist_flag + #-------------------------------------------------------------------- def open_waze_history_database(self, wazehist_database): """ diff --git a/custom_components/icloud3/translations/en.json b/custom_components/icloud3/translations/en.json index 27c6a8e..4e6d5bb 100644 --- a/custom_components/icloud3/translations/en.json +++ b/custom_components/icloud3/translations/en.json @@ -23,11 +23,11 @@ "title": "iCloud3 Integration Installation" }, "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" - }, - "description": "Enter the 6-digit verification code you just received from Apple", - "title": "Apple ID Verification Code" + } } } }, @@ -44,12 +44,13 @@ "conf_reloaded": "iCloud3 Configuration File was Reloaded", "icloud_logging_into": "Logging into iCloud Account", "icloud_logged_into": "Successfully Logged into the iCloud Account", + "icloud_already_logged_into": "Already Logged into the iCloud Account", "icloud_invalid_auth": "iCloud Account Login Failed, Check Username & Password", "icloud_reauth_scheduled": "Reauhentication scheduled after exiting, BROWSER REFRESH MAY BE NEEDED", "icloud_acct_not_set_up": "The iCloud Account Username or Password has not been set up", "send_verification_code": "Failed to send the Apple ID Verification Code", "verification_code_accepted": "The Apple ID Verification Code was Accepted", - "invalid_verification_code": "The Apple ID Verification Code is invalid. Requested a new code", + "invalid_verification_code": "The Apple ID Verification Code is not correct", "review_filledin_fields": "Review the 'Filled in' fields", "not_numeric": "The value entered is not numeric", "stat_zone_base_lat_range_error": "The value must be between -90 and 90", @@ -80,47 +81,51 @@ }, "step": { "menu": { + "title": "iCloud3 v3 Configuration Wizard", "data": { "menu_item": "", "menu_action": "═════════════════════════════════════════════════════" - }, - "title": "iCloud3 Update Configuration Main Menu" + } }, "restart_icloud3": { + "title": "Confirm Restarting iCloud3", + "description": "Note: Changes to tracked devices require restarting iCloud3", "data": { "restart_now_later": "" - }, - "description": "Note: Changes to tracked devices require restarting iCloud3", - "title": "Confirm Restarting iCloud3" + } }, "icloud_account": { + "title": "iCloud Account Login Credentials", + "description": "The iCloud Account and the iOS App are used to provide location information (the data source) for tracking a device. This screen is used to configure the username/password that provides access to your iCloud account and to specify if the iOS App will be used on any of your devices. The iCloud Account can provide location information for devices in the Family Sharing list and from those that are sharing location information on the FindMy app. The HA iOS Companion app can also provide location information for the device.", "data": { "username": "APPPLE ID - The email address used to sign in to the iCloud Acount", "password": "PASSWORD - The Password of the iCloud Acount", - "data_source": "LOCATION DATA SOURCE - The services used for location and other data (iCloud, iOS App, both)", + "data_source1": "LOCATION DATA SOURCE - The services used for location and other data (iCloud, iOS App, both)", + "data_source2": "LOCATION DATA SOURCE - The services providing location and other data", + "data_source": "", "icloud_server_endpoint_suffix": "ICLOUD SERVER LOCATION - Countries having localized Apple iCloud Servers (Not Normally Used)s", "opt_action": "═════════════════════════════════════════════════════" }, "data_description": { - }, - "description": "The iCloud Account and the iOS App are used to provide location information (the data source) for tracking a device. This screen is used to configure the username/password that provides access to your iCloud account and to specify if the iOS App will be used on any of your devices. The iCloud Account can provide location information for devices in the Family Sharing list and from those that are sharing location information on the FindMy app. The HA iOS Companion app can also provide location information for the device.", - "title": "iCloud Account Login Credentials" + } }, "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 - The 6-digit verification code received from Apple" - }, - "title": "Apple ID Verification Code" + "verification_code": "ICLOUD ACCOUNT VERIFICATION CODE" + } }, "device_list": { + "title": "iCloud3 Device Tracker Entities", + "description": "All of the devices that are tracked or monitored by iCloud3 are listed on this screen. Here, new devices are added and existing devices are selected for updating or deletion.", "data": { "devices": "iCloud3 devices", "opt_action": "═════════════════════════════════════════════════════" - }, - "description": "All of the devices that are tracked or monitored by iCloud3 are listed on this screen. Here, new devices are added and existing devices are selected for updating or deletion.", - "title": "iCloud3 Device Tracker Entities" + } }, "add_device": { + "title": "Add Tracked 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", @@ -130,10 +135,11 @@ "opt_action": "═════════════════════════════════════════════════════" }, "data_description": { - }, - "title": "Add Tracked iCloud3 Device" + } }, "update_device": { + "title": "Update Tracked iCloud3 Device", + "description": "This screen lets you configure each of the devices that can be tracked or monitored using iCloud3", "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", @@ -152,24 +158,24 @@ "data_description": { "inzone_interval": "Time between location requests when in a zone", "track_from_base_zone": "Normally, the Home zone is used as the base location for all tracking (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.)" - }, - "description": "This screen lets you configure each of the devices that can be tracked or monitored using iCloud3", - "title": "Update Tracked iCloud3 Device" + } }, "delete_device": { + "title": "Delete Device(s), Other Device Maintenance", "data": { "opt_action": "" - }, - "title": "Delete Device(s), Other Device Maintenance" + } }, "change_device_order": { + "title": "Event Log Device Display Sequence", "data": { "device_desc": "DEVICES - The devices are displayed in the Event Log heading area and in various Event Log messages in this sequence", "opt_action": "═════════════════════════════════════════════════════" - }, - "title": "Event Log Device Display Sequence" + } }, "format_settings": { + "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. With this screen, you can specify the 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", "display_zone_format": "ZONE NAME FORMAT - How the Zone name is displayed in sensors and in the Event Log", @@ -180,35 +186,35 @@ }, "data_description": { "zone_sensor_evlog_format": "The HA entity value standard is to display the state value in lower_case with underscores ('_'). This overides that standard and displays the Zone Name Format selected above" - }, - "description": "Tracking activity, results and information messages are displayed in the Event log, sensors and device_tracker entities for tracked and monitored devices. With this screen, you can specify the how these results should be displayed.", - "title": "Format Settings" + } }, "display_text_as": { + "title": "Event Log 'Display Text As'", + "description": "There may be some text fields that are displayed on the Event Log screen that may be sensitive in nature. Some examples include email addresses or phone numbers. With this screen, you can select the Original Text and what should be displayed instead. For example, you can replace 'geekstergary@apple.com' with 'gary@email.com'", "data": { "display_text_as": "Text Replacement Fields", "opt_action": "═════════════════════════════════════════════════════" - }, - "description": "There may be some text fields that are displayed on the Event Log screen that may be sensitive in nature. Some examples include email addresses or phone numbers. With this screen, you can select the Original Text and what should be displayed instead. For example, you can replace 'geekstergary@apple.com' with 'gary@email.com'", - "title": "Event Log 'Display Text As'" + } }, "display_text_as_update": { + "title": "Update Event Log 'Display Text As' Value", "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)", "opt_action": "═════════════════════════════════════════════════════" }, "data_description": { - }, - "title": "Update Event Log 'Display Text As' Value" + } }, "actions": { + "title": "iCloud3 Action Commands", "data": { "opt_action": "" - }, - "title": "iCloud3 Action Commands" + } }, - "other_parms": { + "tracking_parameters": { + "title": "Tracking & Other Parameters", + "description": "The parameters on this screen do not fall into any of the other general categories and are rarely changed.", "data": { "log_level": "LOG LEVEL - The type of messages that are added to the HA log file during iCloud3 operations", "gps_accuracy_threshold": "GPS ACCURACY THRESHOLD", @@ -216,7 +222,7 @@ "old_location_adjustment": "OLD LOCATION ADJUSTMENT", "distance_between_devices": "USE LOCATION RESULTS FROM A NEAR-BY DEVICE", "max_interval": "MAXIMUM INTERVAL", - "exit_zone_interval": "EXIT_ZONE_INTERVAL", + "exit_zone_interval": "EXIT ZONE INTERVAL", "iosapp_alive_interval": "REQUEST IOSAPP LOCATION INTERVAL", "tfz_tracking_max_distance": "TRACK-FROM-ZONE DISPLAY DISTANCE", "offline_interval": "DEVICE OFFLINE INTERVAL", @@ -235,11 +241,11 @@ "iosapp_alive_interval": "Send a location request to the iOS App if there has been no contact after this amount of time. This will check to see if the iOS App is responding to location requests or is asleep and not running.", "offline_interval": "Location request interval when offline (Airplane mode, dead cell area, etc.)", "travel_time_factor": "Next location time = Waze travel time to zone * this value" - }, - "description": "The parameters on this screen do not fall into any of the other general categories and are rarely changed.", - "title": "Miscellaneous Parameters" + } }, "inzone_intervals": { + "title": "inZone Parameters and Default Intervals", + "description": "An inZone interval is the time between location requests when the Device is in a zone. This screen sets the default values for different types of devices. This value is assigned to a device when it is added and can then be changed on the Update Device screen", "data": { "iphone": "IPHONE & IPOD", "ipad": "IPAD", @@ -255,11 +261,10 @@ "data_description": { "no_iosapp": "Default interval if the iOS App is not used for location monitoring and zone enter/exit triggers", "other": "Unspecified device type inzone interval" - }, - "description": "An inZone interval is the time between location requests when the Device is in a zone. This screen sets the default values for different types of devices. This value is assigned to a device when it is added and can then be changed on the Update Device screen", - "title": "inZone Parameters and Default Intervals" + } }, "waze_main": { + "title": "Waze - Route Service Travel Time/Distance", "data": { "waze_used": "═══════════════════════════════════════════════════ WAZE ROUTE SERVICE", "waze_region": "ROUTE SERVER LOCATION - Location of the Waze Route Server for your area", @@ -275,10 +280,10 @@ "waze_min_distance": "Use the Waze Route Service when the zone distance is greater than this value", "waze_max_distance": "Do not use the Waze Route Service when the zone distance is greater than than this value", "waze_history_max_distance": "Do not save the Waze travel time & distance to the Waze History Database if the distance is greater than this value" - }, - "title": "Waze - Route Service Travel Time/Distance" + } }, "special_zones": { + "title": "Special Zones - Pass Through, Stationary Zone, Primay Home Base Zone", "data": { "passthru_zone_header": "═══════════════════════════════════════════════════ PASSTHRU ZONE ZONE", "passthru_zone_time": "ENTER ZONE DELAY TIME ", @@ -301,10 +306,11 @@ "stat_zone_fname": "Set a generic name here or set it for each Device on the Update Devices screen. All devices can display the same name (Stationary) or add the '[name]' wildcard to display part of the device name, followed by some text ('[name]Zone' --> 'GaryZone')", "stat_zone_base_latitude": "Distance (±km) north-south of the Home Zone (or it's GPS Latitude)", "stat_zone_base_longitude": "Distance (±km) east-west of the Home Zone (or it's GPS Longitude)" - }, - "title": "Special Zones - Pass Through, Stationary Zone, Primay Home Base Zone" + } }, "sensors": { + "title": "Device and Tracking Sensors created by iCloud3", + "description": "Many sensors are used to display tracking results and other information for a device. This screen is used to select the sensors that should be created.", "data": { "monitored_devices": "MONITORED DEVICE SENSORS - Select the type of sensors to create for a Monitored Device", "device": "DEVICE SENSORS - Device status and information", @@ -317,19 +323,17 @@ "other": "OTHER SENSORS - Sensors not in the above areas", "excluded_sensors": "EXCLUDED SENSORS - Sensors that will not be created when iCloud3 starts", "opt_action": "═════════════════════════════════════════════════════" - }, - "description": "Many sensors are used to display tracking results and other information for a device. This screen is used to select the sensors that should be created.", - "title": "Device and Tracking Sensors created by iCloud3" + } }, "exclude_sensors": { + "title": "Exclude Sensors", + "description": "Many sensors are created for the devices but there may be times when you want to not create a sensor for a specific device. For example, you may want to create a bettery sensor for all devices except one. This screen lets you specify the sensor entity name that should not be created.", "data": { "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", "opt_action": "═════════════════════════════════════════════════════" - }, - "description": "Many sensors are created for the devices but there may be times when you want to not create a sensor for a specific device. For example, you may want to create a bettery sensor for all devices except one. This screen lets you specify the sensor entity name that should not be created.", - "title": "Exclude Sensors" + } } } } diff --git a/custom_components/icloud3/zone.py b/custom_components/icloud3/zone.py index 7fe4a93..da4a162 100644 --- a/custom_components/icloud3/zone.py +++ b/custom_components/icloud3/zone.py @@ -16,7 +16,7 @@ #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> from .global_variables import GlobalVariables as Gb -from .const import (HOME, STATIONARY, HIGH_INTEGER, +from .const import (HOME, NOT_HOME, STATIONARY, HIGH_INTEGER, ZONE, TITLE, FNAME, ZONE_FNAME, STAT_ZONE_NO_UPDATE, STAT_ZONE_MOVE_DEVICE_INTO, STAT_ZONE_MOVE_TO_BASE, NAME, STATIONARY_FNAME, ID, @@ -56,8 +56,9 @@ class iCloud3_Zone(object): def __init__(self, zone, zone_data): - self.zone = zone + self.zone = zone + # _traceha(f"{zone=} {zone_data=}") if NAME in zone_data: ztitle = zone_data[NAME].title() else: @@ -79,7 +80,9 @@ def __init__(self, zone, zone_data): self.radius_m = round(zone_data.get(RADIUS, 100)) self.passive = zone_data.get(PASSIVE, True) - self.entity_id = zone_data.get(ID, zone.lower()).replace('sensor.', '') + # self.entity_id = zone_data.get(ID, zone.lower())[:6] + self.er_zone_id = zone_data.get(ID, zone.lower()) # HA entity_registry id + self.entity_id = self.er_zone_id[:6] self.unique_id = zone_data.get('unique_id', zone.lower()) self.dist_time_history = [] #Entries are a list - [lat, long, distance, travel time] @@ -114,6 +117,8 @@ def setup_zone_display_name(self): if Gb.device_tracker_state_format == ZONE: self.device_tracker_state = self.zone elif Gb.device_tracker_state_format == FNAME: + self.device_tracker_state = self.zone if self.zone in [HOME, NOT_HOME] else self.fname + elif Gb.device_tracker_state_format == 'fname/Home': self.device_tracker_state = self.fname elif Gb.device_tracker_state_format == NAME: self.device_tracker_state = self.name @@ -335,6 +340,7 @@ def update_stationary_zone_location(self): def move_stationary_zone_to_new_location(self): if self.Device.old_loc_poor_gps_cnt > 0: + post_event("Move into Stationary Zone delayed > Old Location") return try: @@ -343,21 +349,24 @@ def move_stationary_zone_to_new_location(self): # Make sure stationary zone is not being moved to another zone's location unless it a # Stationary Zone - for Zone in Gb.Zones: - if Zone.radius_m <= 1: - continue - - zone_dist_km = Zone.distance_km(latitude, longitude) - - if is_statzone(Zone.zone) is False: - if zone_dist_km < self.min_dist_from_zone_km: #self.inzone_radius: - event_msg =(f"Move into stationary zone cancelled > " - f"Too close to zone-{Zone.display_as}, " - f"DistFmZone-{format_dist_km(zone_dist_km)}") - post_event(self.devicename, event_msg) - self.timer = Gb.this_update_secs + self.still_time - - return False + close_zone = [{ 'name': Zone.zone, + 'display_as': Zone.display_as, + 'dist_m': Zone.distance_km(latitude, longitude)} + for Zone in Gb.Zones + if (Zone.radius_m > 1 + and is_statzone(Zone.zone) is False + and Zone.passive is False + and Zone.distance_km(latitude, longitude) < self.min_dist_from_zone_km)] + + if close_zone != []: + close_zone_1st = close_zone[0] + event_msg =(f"Move into stationary zone cancelled > " + f"Too close to zone-{close_zone_1st['display_as']}, " + f"DistFmZone-{format_dist_km(close_zone_1st['dist_m'])}") + post_event(self.devicename, event_msg) + self.timer = Gb.this_update_secs + self.still_time + + return False # Set new location, it will be updated when Device's attributes are updated in main routine self.latitude = latitude