From 82282284c12131767ce8ca68c39b2fa0e1c58907 Mon Sep 17 00:00:00 2001 From: gcobb321 Date: Tue, 6 Feb 2024 16:15:31 -0500 Subject: [PATCH] iCloud3 v3.0.rc10.2 --- README.md | 72 ++- custom_components/icloud3/ChangeLog.txt | 21 +- custom_components/icloud3/config_flow.py | 34 +- custom_components/icloud3/const.py | 50 +- custom_components/icloud3/const_sensor.py | 39 +- custom_components/icloud3/device.py | 259 ++++++--- custom_components/icloud3/device_fm_zone.py | 9 +- custom_components/icloud3/device_tracker.py | 79 +-- custom_components/icloud3/global_variables.py | 7 +- custom_components/icloud3/helpers/common.py | 19 +- .../icloud3/helpers/dist_util.py | 136 ++--- .../icloud3/helpers/time_util.py | 12 +- custom_components/icloud3/icloud3_main.py | 545 ++---------------- custom_components/icloud3/sensor.py | 76 +-- .../icloud3/support/config_file.py | 7 +- .../icloud3/support/determine_interval.py | 144 +++-- .../icloud3/support/event_log.py | 16 +- .../icloud3/support/icloud_data_handler.py | 24 +- .../icloud3/support/mobapp_data_handler.py | 118 +++- .../icloud3/support/mobapp_interface.py | 2 +- .../icloud3/support/pyicloud_ic3.py | 3 + .../icloud3/support/pyicloud_ic3_interface.py | 4 +- .../icloud3/support/restore_state.py | 8 +- .../icloud3/support/service_handler.py | 10 +- .../icloud3/support/start_ic3.py | 300 ++++------ .../icloud3/support/start_ic3_control.py | 2 +- .../icloud3/support/stationary_zone.py | 104 ++-- custom_components/icloud3/support/waze.py | 12 +- .../icloud3/support/zone_handler.py | 532 +++++++++++++++++ custom_components/icloud3/zone.py | 20 +- info.md | 91 ++- 31 files changed, 1507 insertions(+), 1248 deletions(-) create mode 100644 custom_components/icloud3/support/zone_handler.py diff --git a/README.md b/README.md index 587c19d..7cc6d03 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,81 @@ -# iCloud3 v3 - An iDevice Tracker Custom Component +# iCloud3 v3 ------ -### Latest Release Candidate 9 - v3.0.rc9 (12/20/2023) +### Release Candidate - v3.0.rc10.2 (2/6/2024) ------ [![CurrentVersion](https://img.shields.io/badge/Current_Version-v3.0-blue.svg)](https://github.com/gcobb321/icloud3_v3) [![Type](https://img.shields.io/badge/Type-Custom_Component-orange.svg)](https://github.com/gcobb321/icloud3_v3) [![HACS](https://img.shields.io/badge/HACS-Custom_Repository-orange.svg)](https://github.com/gcobb321/icloud3_v3) -[![ProjectStage](https://img.shields.io/badge/Project_Stage-Prerelease-forestgreen.svg)](https://github/gcobb321/icloud3_v3) [![Released](https://img.shields.io/badge/Released-December,_2023-forestgreen.svg)](https://github.com/gcobb321/icloud3_v3) +[![ProjectStage](https://img.shields.io/badge/Project_Stage-Release_Candidate_10-forestgreen.svg)](https://github/gcobb321/icloud3_v3) [![Released](https://img.shields.io/badge/Released-February,_2024-forestgreen.svg)](https://github.com/gcobb321/icloud3_v3) -iCloud3 is a device tracker custom component that tracks your iPhones, iPads, Apple Watches, AirPods and other Apple devices. It requests location data from Apple's iCloud Location Services and monitors various triggers sent from the Home Assistant Companion App (iOS App) to Home Assistant. Sensors are updated with the device's location, distance from zones, travel time to zones, etc. +iCloud3 is a device tracker custom component that tracks your iPhones, iPads and Apple Watches. iDevices in the Family Sharing List and the HA Mobile App Integration are trackable. The iDevice requests location data from from Apple's iCloud Location Services and monitors various triggers sent from the Home Assistant Mobile App to Home Assistant. Sensors are updated with the device's location, distance from zones, travel time to zones, etc. + +Although AirPods and AirTags are in the iCloud Family Sharing list, they can not be tracked. They do not have the internal components to provide location data using cell towers and gps location information to Apple. ### iCloud3 v3 Highlights Although Home Assistant has it's own official iCloud component, iCloud3 goes far beyond it's capabilities. The important highlights include: - **HA Integration** - iCloud3 is a Home Assistant custom integration that is set up and configured from the *HA Settings > Devices & Services > Integrations* screen. -- **Configuration Settings** - All of the parameters are updated on configuration screens selected from the *iCloud3 Integrations* entry. -- **Track devices from several sources** - Family members on the iCloud Account Family Sharing list, those who are sharing their location on FindMy App and devices that have installed the HA Companion App (iOS App) can be tracked. -- **Actively track a device** - The device will request it's location on a regular interval. -- **Passively monitor a device** - The device does not request it's location but is tracked when another tracked device requests theirs. -- **Waze Route Service** - The travel time and route distance to a tracked zone (Home) is provided by Waze. -- **Waze Route Service History Database** - The travel time and route distance received from Waze is saved to a local database to improve performance and eliminate the response delay due to poor cell service and slow internet speed. -- **Track from Multiple Zones** - The device is always tracked from the Home Zone. Now it can also be tracked from another zone (office, second home, parents, etc.). Travel time and distance to the other zone is reported just like the Home zone. Additionally, another zone can act as the primary 'Home' zone (vacation home, parents home, etc). This can be configured by device or globally. -- **Improved GPS Accuracy** - GPS wandering errors leading to incorrect zone exits are eliminated. -- **Passthru Zone** - Delay processing an Enter Zone trigger in case you are just driving through it. +- **Configuration Settings** - Configuration parameters are updated online using various screens and take effect immediately without restarting HA. +- **Track iPhones, iPads and Apple Watches** - Track or monitor your iDevices. +- **Location data sources** - Location data comes from the iCloud Account and the HA Companion App (Mobile App). +- **Actively track a device** - The device will request it's location on a regular interval based on its distance from Home or another zone. +- **Passively monitor a device** - The device does not request it's location. It is updated when another tracked device requests theirs. +- **Waze Route Service** - The travel time and distance to Home or another tracked zone is provided by Waze. +- **Waze Route Service History Database** - The travel time and distance data from Waze is saved to a local database and reused when the device is in a previous location. +- **Track from multiple zones** - Tracking results (location, travel time, distance, arrival time, etc.) are reported from the Home zone or another zone (office, second home, parents, etc.). +- **Primary Home Zone** - Set another zone as the primary zone for the device and report tracking results based on that location. This is useful when you have two homes, on a vacation at another location, triggering automations at your parents house with true devices, etc. +- **Improved GPS accuracy** - GPS wandering errors leading to incorrect zone exits are eliminated. +- **Monitors Mobile App activity** - Looks for location and trigger changes every 5-seconds. +- **Enter Zone delay** - Delay processing Zone Enter triggers in case you are just driving through it. - **Stationary Zone** - A dynamic *Stationary Zone* is created when the device has not moved for a while (doctors office, store, friend's house). This helps conserve battery life. -- **Sensors and more sensors** - Many sensors are created and updated with distance, travel time, polling data, battery status, zone attributes, etc. The sensors that are created is customizable. -- **Event Log** - The current status and event history of every tracked and monitored device is displayed on the iCloud3 Event Log custom Lovelace card. It shows information about devices that can be tracked, errors and alerts, nearby devices, tracking results, debug information and location request results. -- **Detailed debugging information** - Several levels location history transactions can be displayed in the Event Log or in the Home Assistant Log File. These include general information, debug data and the raw device location data received from iCloud Location Services. -- **Updating and Restarting** - iCloud3 can be restarted without restarting Home Assistant. The current device_tracker and sensor entity states are restored on a restart. It can also be reloaded from the *Configure Settings > Action* screen without restarting Home Assistant. -- **Device_tracker and sensor entities** - iCloud3 devices and sensors are true Home Assistant entities. They are added, deleted and updated using *Configuration Settings > Sensors* selection screens. -- **Nearby Devices** - The location of all devices is monitored and the distance between devices is determined. Information from devices close to each other is shared. -- **Zone Exits for devices not using the iOS App** - Devices that do not or can not (Apple Watch) use the iOS App respond to a zone exit when it detects another nearby device has left a zone. -- **And More** - Review the following documentation to see if it will help you track and monitor the locations of your family members and friends. +- **Nearby devices** - The distance to other devices is displayed and used to determine tracking results. +- **Zone monitoring** - The number of devices in each zone is displayed when a device is updated. +- **Local Time Zone** - Event times are normally displayed using the time zone your HA server is in. If, hoowever, you are away from home and in another time zone can, the Event times can be displayed for the time zone you are in. +- **Zone Activity Log** - A log can be kept for each time you are in a zone. This log file (.csv format) can be imported into a spreadsheet program and used for expense reporting, travel history, device location monitoring, etc. +- **Sensors and more sensors** - Many sensors are created and updated with distance, travel time, polling data, battery status, zone attributes, etc. Select only the ones you want to use. +- **Battery status** - Updates the battery level and status (charging/not charging) from iCloud data during a tracking event and from the Mobile App every 5-seconds. +- **Distance Sensor Attributes** - Shows the distance to the center and edge of the Home zone, distance to other zones and distance to other devices. +- **Event Log** - The current status and event history of every tracked and monitored device is displayed on the iCloud3 Event Log custom Lovelace card. Information about device configuration, errors and alerts, nearby devices, tracking results, debug information and location request results is displayed. +- **Updating and Restarting** - iCloud3 can be restarted without restarting Home Assistant. +- **Restore state values on restart** - The current device_tracker and sensor entity states are restored on a restart. The attributes are not restored but are reset on the first tracking Event. +- **Device_tracker and sensor entities** - iCloud3 devices and sensors are Home Assistant entities that are added, deleted and changed on the *Update iCloud3 Devices* and *Sensors* configuration screens. +- **Zone Exits for devices not using the Mobile App** - Devices that do not or can not (Apple Watch) use the Mobile App respond to a zone exit when it detects another nearby device has left a zone. +- **Extensive Documentation** - The iCloud3 User Guide explains the three main components, hot to get started, how to migrate from v2, how to install the integration, each of the screens and special features, the service calls that can request updates, locate iPhones and send notification alerts, examples of how to automate opening your garage door when you arrive home, etc. +- **And More** - Review the following documentation to see if it will help you track and monitor the locations of your iPhones, iPads and Apple Watches. ### Tracking Information Screen with Event Log The screens below are an example of how the many tracking sensors can be displayed. The screen on the left shows the current tracking formation for Gary while the Event Log on the right shows a history of important tracking events. -![](./docs/images/track-evlog-gary-tfz-away-lillian-home.png) +![evlimg](https://gcobb321.github.io/icloud3_v3_docs/images/track-evlog-gary-tfz-lillian-home.png) ### iCloud3 Documentation - Introduces the many features and components of iCloud3 - Describes how to migration from v2.4.7 to v3.0 -- Provides step-by-step to install and configure iCloud3, it's components and it's supporting components (iCloud Account and the iOS App) +- Provides step-by-step to install and configure iCloud3, it's components and it's supporting components (iCloud Account and the Mobile App) - Highlights the configuration screens and parameters - Provides example screens, automations and scripts -- The [User Guide is here](https://gcobb321.github.io/icloud3_v3/#/). +- The User Guide is [here](https://gcobb321.github.io/icloud3_v3_docs/#/) ### Installing or Upgrading to iCloud3 v3 -- iCloud3 v3 is now available on the iCloud3 HACS base as a prerelease version. Instructions are in the [*iCloud3 User Guide > Migrating iCloud3 from v2.4.x to v3* here](https://gcobb321.github.io/icloud3_v3_docs/#/chapters/0.1-migrating-v2.4-to-v3.0). There are also instructions on doing a manual download. It's about one screen down after the Introduction. +- iCloud3 v3 is now available on the iCloud3 HACS base as a prerelease version. ### Important Links -- **iCloud3 GitHub Repository (Prerelease Version)** - The [GitHub Repository is here.](https://github.com/gcobb321/icloud3/releases) -- **Install using HACS ** - iCloud3 v3 is now available on the HACS base as a prerelease version. Instructions are in the [*iCloud3 User Guide > Migrating iCloud3 from v2.4.x to v3* here](https://gcobb321.github.io/icloud3_v3_docs/#/chapters/0.1-migrating-v2.4-to-v3.0?id=step-1-install-icloud3). -- **Download and Install Manually** - This is also described in the iCloud3 User Guide. Select the above link. -- **Migrating from v2.4._** - The User Guide desctibes the [v2 to v3 Migration Process here](https://gcobb321.github.io/icloud3_v3_docs/#/chapters/0.1-migrating-v2.4-to-v3.0?id=migrating-icloud3-from-v24x-to-v30. -- **iCloud3 v3 User Guide** -The User Guide is quite extensive and can be found [here](https://gcobb321.github.io/icloud3_v3_docs/#/). +- **iCloud3 v3 User Guide** -The User Guide is quite extensive and can be found [here](https://gcobb321.github.io/icloud3_v3_docs/#/) +- **iCloud3 v3 GitHub Repository (Prerelease Version)** - The primary GitHub Repository is [here](https://github.com/gcobb321/icloud3) +- **iCloud3 v3 Development GitHub Repository** - The Development Repository is used for beta version changes that have not been released yet is [here](https://github.com/gcobb321_v3) +- **Installing as a New Installation** - iCloud3 v3 is available in HACS as a prerelease version. Installation instructions are [here](https://gcobb321.github.io/icloud3_v3_docs/#/chapters/3.2-installing-and-configuring) +- **Migrating from v2.4.x** - This includes installing iCloud3 v3, migrating your current configuration and reviewing it to insure it was migrated correctly. Instructions are [here](https://gcobb321.github.io/icloud3_v3_docs/#/chapters/3.1-migrating-v2-to-v3) + diff --git a/custom_components/icloud3/ChangeLog.txt b/custom_components/icloud3/ChangeLog.txt index 06e8acb..9aa7705 100644 --- a/custom_components/icloud3/ChangeLog.txt +++ b/custom_components/icloud3/ChangeLog.txt @@ -1,4 +1,23 @@ -rc10 +rc10.2 +............................... +1. Outside of Zone with no Exit Trigger message - Changed the way the Exit Trigger was being logged so this wouldn't be issued when the trigger was actually issued. +2. Passive Zones and the Mobile App - The Mobile App does not issue an Enter Zone Trigger for passive zones (which is correct), but it will issue an Exit Trigger for them (which it should not do). This was creating problems by issuing an Exit Zone Trigger when leaving the real zone and then issuing another Exit Trigger when leaving a zone it had never entered (Passive or otherwise). This Mobile App error is now identified and changed into a Verify Location trigger. +3. Nearby devices - Nearby device info will not be used if the device being updated is in a non-Stationary zone. +4. Configure Settings - inZone Interval, Fixed Interval, Max Interval, etc. - Changed the minimum value to 5-minutes to prevent conflicts with old location threshold and reusing a location when it should be requested again. +5. Other code cleanup. + + + +rc10.1 1/27/2024 +............................... +1. BUG FIXES: + - Fixed an AttributeError: 'NoneType' object has no attribute 'next_update_secs' error message + - Fixed an 'UnboundLocalError: cannot access local variable 'from_zone' error message +2. All distances in the Event Log are now displayed in miles/feet or kilometers/meters based on the parameter settings. +3. Code cleanup + + +rc10 1/21/2024 ............................... 1. NEARBY DEVICES - Changed the distance to another device routine so it would not calculate the distance to 'itself', which is always 0. 2. WAZE HISTORY MAP - Fixed a problem displaying the Waze History gps locations on a map. diff --git a/custom_components/icloud3/config_flow.py b/custom_components/icloud3/config_flow.py index d68a695..56ab744 100644 --- a/custom_components/icloud3/config_flow.py +++ b/custom_components/icloud3/config_flow.py @@ -116,7 +116,7 @@ def dict_value_to_list(key_value_dict): 'Menu > Configure Parameters Menu' ] MENU_KEY_TEXT = { - 'icloud_account': 'iCLOUD ACCOUNT & Mobile App ᐳ •Set iCloud Account Username/Password, •Set Location Data Sources', + 'icloud_account': 'iCLOUD ACCOUNT & MOBILE APP ᐳ •Set iCloud Account Username/Password, •Set Location Data Sources', 'device_list': 'ICLOUD3 DEVICES ᐳ •Add, Change and Delete tracked and monitored devices', 'verification_code': 'ENTER/REQUEST AN APPLE ID VERIFICATION CODE ᐳ •Enter or Request the 6-digit Apple ID Verification Code', 'away_time_zone': 'AWAY TIME ZONE ᐳ •Select the time zone used to display time based tracking events for a device when in another time zone', @@ -134,7 +134,7 @@ def dict_value_to_list(key_value_dict): 'select': 'SELECT ᐳ Select the parameter update form', 'next_page_0': f'{MENU_PAGE_TITLE[0].upper()} ᐳ •iCloud Account & Mobile App, •iCloud3 Devices, •Enter & Request Verification Code, •Change Device Order, •Sensors, •Action Commands', 'next_page_1': f'{MENU_PAGE_TITLE[1].upper()} ᐳ •Format Parameters, •Display Text As, •Waze Route Distance, Time & History, •inZone Intervals, •Special Zones, • Other Parameters', - 'exit': f'EXIT AND RESTART ICLOUD3 {".. "*22}(Version: {Gb.version})' + 'exit': f'EXIT AND RESTART ICLOUD3 v{Gb.version}' } MENU_KEY_TEXT_PAGE_0 = [ @@ -298,7 +298,7 @@ def dict_value_to_list(key_value_dict): 'name-zone': ' → [year]-[zone].csv', 'name-device': ' → [year]-[device].csv', 'name-device-zone': ' → [year]-[device]-[zone].csv', - 'name-zone-device': ' → [year]-[zone]-[device]-[zone].csv', + 'name-zone-device': ' → [year]-[zone]-[device].csv', } TRACKING_MODE_ITEMS_KEY_TEXT = { 'track': 'Track - Request Location and track the device', @@ -4453,11 +4453,11 @@ def form_schema(self, step_id, actions_list=None, actions_list_default=None): default=self.conf_device_selected[CONF_INZONE_INTERVAL]): # default=self._parm_or_device(CONF_INZONE_INTERVAL)): selector.NumberSelector(selector.NumberSelectorConfig( - min=1, max=300, step=1, unit_of_measurement='minutes')), + min=5, max=480, step=5, unit_of_measurement='minutes')), vol.Required(CONF_FIXED_INTERVAL, default=self.conf_device_selected[CONF_FIXED_INTERVAL]): selector.NumberSelector(selector.NumberSelectorConfig( - min=0, max=300, step=1, unit_of_measurement='minutes')), + min=0, max=480, step=5, unit_of_measurement='minutes')), vol.Required(CONF_TRACK_FROM_BASE_ZONE, default=self._option_parm_to_text(CONF_TRACK_FROM_BASE_ZONE, self.zone_name_key_text, conf_device=True)): selector.SelectSelector(selector.SelectSelectorConfig( @@ -4664,7 +4664,7 @@ def form_schema(self, step_id, actions_list=None, actions_list_default=None): vol.Required(CONF_GPS_ACCURACY_THRESHOLD, default=Gb.conf_general[CONF_GPS_ACCURACY_THRESHOLD]): selector.NumberSelector(selector.NumberSelectorConfig( - min=5, max=250, step=5, unit_of_measurement='m')), + min=5, max=300, step=5, unit_of_measurement='m')), vol.Required(CONF_OLD_LOCATION_THRESHOLD, default=Gb.conf_general[CONF_OLD_LOCATION_THRESHOLD]): selector.NumberSelector(selector.NumberSelectorConfig( @@ -4676,7 +4676,7 @@ def form_schema(self, step_id, actions_list=None, actions_list_default=None): vol.Required(CONF_MAX_INTERVAL, default=Gb.conf_general[CONF_MAX_INTERVAL]): selector.NumberSelector(selector.NumberSelectorConfig( - min=15, max=240, step=1, unit_of_measurement='minutes')), + min=15, max=480, step=5, unit_of_measurement='minutes')), vol.Required(CONF_EXIT_ZONE_INTERVAL, default=Gb.conf_general[CONF_EXIT_ZONE_INTERVAL]): selector.NumberSelector(selector.NumberSelectorConfig( @@ -4684,11 +4684,11 @@ def form_schema(self, step_id, actions_list=None, actions_list_default=None): vol.Required(CONF_MOBAPP_ALIVE_INTERVAL, default=Gb.conf_general[CONF_MOBAPP_ALIVE_INTERVAL]): selector.NumberSelector(selector.NumberSelectorConfig( - min=15, max=240, step=15, unit_of_measurement='minutes')), + min=15, max=240, step=5, unit_of_measurement='minutes')), vol.Required(CONF_OFFLINE_INTERVAL, default=Gb.conf_general[CONF_OFFLINE_INTERVAL]): selector.NumberSelector(selector.NumberSelectorConfig( - min=1, max=300, step=1, unit_of_measurement='minutes')), + min=5, max=240, step=5, unit_of_measurement='minutes')), vol.Required(CONF_TFZ_TRACKING_MAX_DISTANCE, default=Gb.conf_general[CONF_TFZ_TRACKING_MAX_DISTANCE]): selector.NumberSelector(selector.NumberSelectorConfig( @@ -4723,27 +4723,27 @@ def form_schema(self, step_id, actions_list=None, actions_list_default=None): vol.Optional(IPHONE, default=Gb.conf_general[CONF_INZONE_INTERVALS][IPHONE]): selector.NumberSelector(selector.NumberSelectorConfig( - min=1, max=300, step=1, unit_of_measurement='minutes')), + min=5, max=480, step=5, unit_of_measurement='minutes')), vol.Optional(IPAD, default=Gb.conf_general[CONF_INZONE_INTERVALS][IPAD]): selector.NumberSelector(selector.NumberSelectorConfig( - min=1, max=300, step=1, unit_of_measurement='minutes')), + min=5, max=480, step=5, unit_of_measurement='minutes')), vol.Optional(WATCH, default=Gb.conf_general[CONF_INZONE_INTERVALS][WATCH]): selector.NumberSelector(selector.NumberSelectorConfig( - min=1, max=300, step=1, unit_of_measurement='minutes')), + min=5, max=480, step=5, unit_of_measurement='minutes')), vol.Optional(AIRPODS, default=Gb.conf_general[CONF_INZONE_INTERVALS][AIRPODS]): selector.NumberSelector(selector.NumberSelectorConfig( - min=1, max=300, step=1, unit_of_measurement='minutes')), + min=5, max=480, step=5, unit_of_measurement='minutes')), vol.Optional(NO_MOBAPP, default=Gb.conf_general[CONF_INZONE_INTERVALS][NO_MOBAPP]): selector.NumberSelector(selector.NumberSelectorConfig( - min=1, max=300, step=1, unit_of_measurement='minutes')), + min=5, max=480, step=5, unit_of_measurement='minutes')), vol.Optional(OTHER, default=Gb.conf_general[CONF_INZONE_INTERVALS][OTHER]): selector.NumberSelector(selector.NumberSelectorConfig( - min=1, max=300, step=1, unit_of_measurement='minutes')), + min=5, max=480, step=5, unit_of_measurement='minutes')), vol.Optional('action_items', default=self.action_default_text('save')): @@ -4826,11 +4826,11 @@ def form_schema(self, step_id, actions_list=None, actions_list_default=None): vol.Required(CONF_STAT_ZONE_STILL_TIME, default=Gb.conf_general[CONF_STAT_ZONE_STILL_TIME]): selector.NumberSelector(selector.NumberSelectorConfig( - min=0, max=60, unit_of_measurement='minutes')), + min=5, max=60, unit_of_measurement='minutes')), vol.Required(CONF_STAT_ZONE_INZONE_INTERVAL, default=Gb.conf_general[CONF_STAT_ZONE_INZONE_INTERVAL]): selector.NumberSelector(selector.NumberSelectorConfig( - min=5, max=60, unit_of_measurement='minutes')), + min=5, max=60, srep=5, unit_of_measurement='minutes')), vol.Optional(CONF_TRACK_FROM_BASE_ZONE_USED, default=tfzh_default): diff --git a/custom_components/icloud3/const.py b/custom_components/icloud3/const.py index 9259ba8..622aca4 100644 --- a/custom_components/icloud3/const.py +++ b/custom_components/icloud3/const.py @@ -4,7 +4,7 @@ # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -VERSION = '3.0.rc10' +VERSION = '3.0.rc10.2' #----------------------------------------- DOMAIN = 'icloud3' ICLOUD3 = 'iCloud3' @@ -49,17 +49,17 @@ HOME_FNAME = 'Home' NOT_HOME = 'not_home' NOT_HOME_FNAME = 'NotHome' +AWAY = 'Away' NEAR_HOME = 'NearHome' NOT_SET = 'not_set' NOT_SET_FNAME = 'NotSet' UNKNOWN = 'Unknown' -#STATIONARY = 'statzone' -#STATIONARY_FNAME = 'StatZone' STATIONARY = 'stationary' STATIONARY_FNAME = 'Stationary' +NOT_HOME_ZONES = [NOT_HOME, AWAY, NOT_SET] + AWAY_FROM = 'AwayFrom' AWAY_FROM_HOME = 'AwayFromHome' -AWAY = 'Away' NEAR = 'Near' TOWARDS = 'Towards' TOWARDS_HOME = 'TowardsHome' @@ -250,7 +250,7 @@ dark_circled_letters = "🅐 🅑 🅒 🅓 🅔 🅕 🅖 🅗 🅘 🅙 🅚 🅛 🅜 🅝 🅞 🅟 🅠 🅡 🅢 🅣 🅤 🅥 🅦 🅧 🅨 🅩 ✪" Symbols = ±▪•●▬⮾ ⊗ ⊘✓×ø¦ ▶◀ ►◄▲▼ ∙▪ »« oPhone=►▶→⟾➤➟➜➔➤🡆🡪🡺⟹🡆➔ᐅ◈🝱☒☢⛒⊘Ɵ⊗ⓧⓍ⛒🜔 Important = ❗❌⚠️❓🛑⛔⚡⭐⭕ - — –ᗒ ⁃ » ━▶ ━➤🡺 —> > > ❯↦ … 🡪ᗕ ᗒ ᐳ ─🡢 ──ᗒ 🡢 ─ᐅ ↣ ➙ →《》◆◈◉●▐‖ ▹▻▷◁◅◃▶➤➜➔❰❰❱❱ ⠤ + — –ᗒ ⁃ » ━▶ ━➤🡺 —> > > ❯↦ … 🡪ᗕ ᗒ ᐳ ─🡢 ──ᗒ 🡢 ─ᐅ ↣ ➙ →《》◆◈◉●▐‖ ▹▻▷◁◅◃▶➤➜➔❰❰❱❱ ⠤ ² ⣇⠈⠉⠋⠛⠟⠿⡿⣿ https://www.fileformat.info/info/unicode/block/braille_patterns/utf8test.htm ''' NBSP = '⠈' #' ' @@ -289,8 +289,10 @@ CRLF_CIRCLE_X = f'{CRLF}{NBSP2}ⓧ{NBSP}' CRLF_SP3_DOT = f'{CRLF}{NBSP3}•{NBSP}' CRLF_SP5_DOT = f'{CRLF}{NBSP5}•{NBSP}' +CRLF_SP8_DOT = f'{CRLF}{NBSP4}{NBSP4}•{NBSP}' CRLF_SP3_HDOT = f'{CRLF}{NBSP3}◦{NBSP}' CRLF_SP3_STAR = f'{CRLF}{NBSP3}✪{NBSP}' +CRLF_TAB = f'{CRLF}{NBSP6}' CRLF_INDENT = f'{CRLF}{NBSP6}{NBSP6}' CRLF_DASH_75 = f'{CRLF}{"-"*75}' @@ -301,7 +303,7 @@ LARROW = ' <-- ' #U+27F5 (Long Arrow Left) ⟸ ⟽ LARROW2 = '<--' #U+27F5 (Long Arrow Left) ⟸ ⟽ INFO_SEPARATOR = '/' #'∻' -DASH_20 = '━'*20 +DASH_20 = '━'*2 OPT_NONE = 0 #tracking_method config parameter being used @@ -325,6 +327,11 @@ TRACK_DEVICE = 'track' MONITOR_DEVICE = 'monitor' INACTIVE_DEVICE = 'inactive' +TRACKING_MODE_FNAME = { + TRACK_DEVICE: 'Tracked', + MONITOR_DEVICE: 'Monitored', + INACTIVE_DEVICE: 'INACTIVE', +} # Zone field names NAME = 'name' @@ -509,6 +516,7 @@ BATTERY_MOBAPP = 'battery_last_mobapp_data' WAZE_METHOD = 'waze_method' MAX_DISTANCE = 'max_distance' +WENT_3KM = 'went_3km' DEVICE_STATUS = 'device_status' LOW_POWER_MODE = 'low_power_mode' @@ -716,8 +724,8 @@ CONF_SENSORS_TRACKING_DISTANCE = 'tracking_distance' -ZONE_DISTANCE_M = 'meters_distance' -ZONE_DISTANCE_M_EDGE = 'meters_distance_to_zone_edge' +ZONE_DISTANCE_M = 'distance_(meters)' +ZONE_DISTANCE_M_EDGE = 'distance_to_zone_edge_(meters)' ZONE_DISTANCE = "zone_distance" HOME_DISTANCE = "home_distance" DISTANCE_HOME = "distance_home" @@ -750,12 +758,12 @@ CONF_SENSORS_ZONE = 'zone' ZONE_INFO = 'zone_info' ZONE = "zone" -ZONE_DISPLAY_AS = "zone_display_as" +ZONE_DNAME = "zone_dname" ZONE_FNAME = "zone_fname" ZONE_NAME = "zone_name" ZONE_DATETIME = "zone_changed" LAST_ZONE = "last_zone" -LAST_ZONE_DISPLAY_AS= "last_zone_display_as" +LAST_ZONE_DNAME = "last_zone_dname" LAST_ZONE_FNAME = "last_zone_fname" LAST_ZONE_NAME = "last_zone_name" LAST_ZONE_DATETIME = "last_zone_changed" @@ -818,8 +826,8 @@ } RANGE_DEVICE_CONF = { - CONF_INZONE_INTERVAL: [5, 240], - CONF_FIXED_INTERVAL: [0, 240], + CONF_INZONE_INTERVAL: [5, 480], + CONF_FIXED_INTERVAL: [0, 480], } # Used in conf_flow to reinialize the Configuration Devices @@ -898,22 +906,22 @@ 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_MAX_INTERVAL: [15, 480], CONF_EXIT_ZONE_INTERVAL: [.5, 10, .5], - CONF_MOBAPP_ALIVE_INTERVAL: [15, 240], - CONF_OFFLINE_INTERVAL: [1, 240], + CONF_MOBAPP_ALIVE_INTERVAL: [15, 480], + CONF_OFFLINE_INTERVAL: [5, 480], CONF_TFZ_TRACKING_MAX_DISTANCE: [1, 100, 1, 'km'], CONF_TRAVEL_TIME_FACTOR: [.1, 1, .1, ''], CONF_PASSTHRU_ZONE_TIME: [0, 5], # inZone Configuration Parameters # CONF_INZONE_INTERVALS: { - # IPHONE: [5, 240], - # IPAD: [5, 240], - # WATCH: [5, 240], - # AIRPODS: [5, 240], - # NO_MOBAPP: [5, 240], - # OTHER: [5, 240], + # IPHONE: [5, 480], + # IPAD: [5, 480], + # WATCH: [5, 480], + # AIRPODS: [5, 480], + # NO_MOBAPP: [5, 480], + # OTHER: [5, 480], # }, # Waze Configuration Parameters diff --git a/custom_components/icloud3/const_sensor.py b/custom_components/icloud3/const_sensor.py index d02047a..2d4677f 100644 --- a/custom_components/icloud3/const_sensor.py +++ b/custom_components/icloud3/const_sensor.py @@ -8,15 +8,15 @@ BADGE, TRIGGER, FROM_ZONE, ZONE_INFO, NEAR_DEVICE_USED, - ZONE, ZONE_DISPLAY_AS, ZONE_NAME, ZONE_FNAME, ZONE_DATETIME, - LAST_ZONE, LAST_ZONE_DISPLAY_AS, LAST_ZONE_NAME, LAST_ZONE_FNAME, LAST_ZONE_DATETIME, + ZONE, ZONE_DNAME, ZONE_NAME, ZONE_FNAME, ZONE_DATETIME, + LAST_ZONE, LAST_ZONE_DNAME, LAST_ZONE_NAME, LAST_ZONE_FNAME, LAST_ZONE_DATETIME, INTERVAL, BATTERY_SOURCE, BATTERY, BATTERY_STATUS, BATTERY_UPDATE_TIME, BATTERY_FAMSHR, BATTERY_MOBAPP, DISTANCE, ZONE_DISTANCE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, HOME_DISTANCE, MAX_DISTANCE,CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD, TRAVEL_TIME, TRAVEL_TIME_MIN, TRAVEL_TIME_HHMM, ARRIVAL_TIME, DIR_OF_TRAVEL, - MOVED_DISTANCE, MOVED_TIME_FROM, MOVED_TIME_TO, + MOVED_DISTANCE, MOVED_TIME_FROM, MOVED_TIME_TO, WENT_3KM, DEVICE_STATUS, LAST_UPDATE, LAST_UPDATE_DATETIME, NEXT_UPDATE, NEXT_UPDATE_DATETIME, @@ -53,12 +53,13 @@ NEXT_UPDATE, LAST_UPDATE, LAST_LOCATED, TRAVEL_TIME, TRAVEL_TIME_MIN, TRAVEL_TIME_HHMM, ARRIVAL_TIME, ] -SENSOR_LIST_ZONE_NAME =[ZONE, ZONE_DISPLAY_AS, ZONE_FNAME, ZONE_NAME, ZONE_NAME, ZONE_FNAME, - LAST_ZONE_NAME, LAST_ZONE_DISPLAY_AS, LAST_ZONE_FNAME, LAST_ZONE, +SENSOR_LIST_ZONE_NAME =[ZONE, ZONE_DNAME, ZONE_FNAME, ZONE_NAME, ZONE_NAME, ZONE_FNAME, + LAST_ZONE_NAME, LAST_ZONE_DNAME, LAST_ZONE_FNAME, LAST_ZONE, LAST_ZONE_FNAME, LAST_ZONE_NAME, ] SENSOR_LIST_DISTANCE = [DISTANCE, ZONE_DISTANCE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, HOME_DISTANCE, ] + SENSOR_GROUPS = { 'battery': [BATTERY, BATTERY_STATUS], 'md_badge': [BADGE], @@ -119,7 +120,7 @@ - *_last_update - *_next_update - *_last_zone - - *_last-zone_display_as + - *_last_zone_dname - *_last_zone_name - *_last_zone_fname - *_last_zone_datetime @@ -151,11 +152,11 @@ 'Badge', 'badge', 'mdi:shield-account', - [NAME, BATTERY, ZONE, ZONE_FNAME, ZONE_NAME, - HOME_DISTANCE, DISTANCE_TO_DEVICES, + [NAME, BATTERY, ZONE, ZONE_FNAME, + HOME_DISTANCE, MAX_DISTANCE, TRAVEL_TIME, DIR_OF_TRAVEL, INTERVAL, - ZONE_DATETIME, - LAST_LOCATED_DATETIME, LAST_UPDATE_DATETIME, + DISTANCE_TO_DEVICES, + ZONE_DATETIME, LAST_LOCATED_DATETIME, LAST_UPDATE_DATETIME, DEVICE_STATUS, ], BLANK_SENSOR_FIELD], BATTERY: [ @@ -363,23 +364,23 @@ 'mdi:crosshairs-gps', [ZONE, ZONE_FNAME, ZONE_NAME, ZONE_DATETIME], BLANK_SENSOR_FIELD], - ZONE_DISPLAY_AS: [ + ZONE_DNAME: [ 'Zone', 'zone', 'mdi:crosshairs-gps', - [ZONE, ZONE_DISPLAY_AS, ZONE_FNAME, ZONE_NAME, ZONE_DATETIME], + [ZONE, ZONE_DNAME, ZONE_FNAME, ZONE_NAME, ZONE_DATETIME], BLANK_SENSOR_FIELD], ZONE_FNAME: [ 'ZoneFname', 'zone', 'mdi:crosshairs-gps', - [ZONE, ZONE_DISPLAY_AS, ZONE_FNAME, ZONE_NAME, ZONE_DATETIME], + [ZONE, ZONE_DNAME, ZONE_FNAME, ZONE_NAME, ZONE_DATETIME], BLANK_SENSOR_FIELD], ZONE_NAME: [ 'ZoneName', 'zone', 'mdi:crosshairs-gps', - [ZONE, ZONE_DISPLAY_AS, ZONE_FNAME, ZONE_NAME, ZONE_DATETIME], + [ZONE, ZONE_DNAME, ZONE_FNAME, ZONE_NAME, ZONE_DATETIME], BLANK_SENSOR_FIELD], ZONE_DATETIME: [ 'ZoneChanged', @@ -391,25 +392,25 @@ 'LastZone', 'zone', #, ha_history_exclude', 'mdi:crosshairs-gps', - [LAST_ZONE, LAST_ZONE_DISPLAY_AS, LAST_ZONE_FNAME, LAST_ZONE_NAME, LAST_ZONE_DATETIME], + [LAST_ZONE, LAST_ZONE_DNAME, LAST_ZONE_FNAME, LAST_ZONE_NAME, LAST_ZONE_DATETIME], BLANK_SENSOR_FIELD], - LAST_ZONE_DISPLAY_AS: [ + LAST_ZONE_DNAME: [ 'LastZone', 'zone', #, ha_history_exclude', 'mdi:crosshairs-gps', - [LAST_ZONE, LAST_ZONE_DISPLAY_AS, LAST_ZONE_FNAME, LAST_ZONE_NAME, LAST_ZONE_DATETIME], + [LAST_ZONE, LAST_ZONE_DNAME, LAST_ZONE_FNAME, LAST_ZONE_NAME, LAST_ZONE_DATETIME], BLANK_SENSOR_FIELD], LAST_ZONE_FNAME: [ 'LastZone', 'zone', #, ha_history_exclude', 'mdi:crosshairs-gps', - [LAST_ZONE, LAST_ZONE_DISPLAY_AS, LAST_ZONE_FNAME, LAST_ZONE_NAME, LAST_ZONE_DATETIME], + [LAST_ZONE, LAST_ZONE_DNAME, LAST_ZONE_FNAME, LAST_ZONE_NAME, LAST_ZONE_DATETIME], BLANK_SENSOR_FIELD], LAST_ZONE_NAME: [ 'LastZone', 'zone', #, ha_history_exclude', 'mdi:crosshairs-gps', - [LAST_ZONE, LAST_ZONE_DISPLAY_AS, LAST_ZONE_FNAME, LAST_ZONE_NAME, LAST_ZONE_DATETIME], + [LAST_ZONE, LAST_ZONE_DNAME, LAST_ZONE_FNAME, LAST_ZONE_NAME, LAST_ZONE_DATETIME], BLANK_SENSOR_FIELD], LAST_ZONE_DATETIME: [ 'ZoneChanged', diff --git a/custom_components/icloud3/device.py b/custom_components/icloud3/device.py index 440cdcf..c4e4478 100644 --- a/custom_components/icloud3/device.py +++ b/custom_components/icloud3/device.py @@ -7,7 +7,7 @@ from .const import (DEVICE_TRACKER, DEVICE_TRACKER_DOT, CIRCLE_STAR2, LTE, GTE, NOTIFY, DISTANCE_TO_DEVICES, NEAR_DEVICE_DISTANCE, DISTANCE_TO_OTHER_DEVICES, DISTANCE_TO_OTHER_DEVICES_DATETIME, - HOME, HOME_FNAME, NOT_HOME, NOT_SET, UNKNOWN, + HOME, HOME_FNAME, NOT_HOME, NOT_SET, UNKNOWN, NOT_HOME_ZONES, DOT, RED_X, RARROW, INFO_SEPARATOR, YELLOW_ALERT, CRLF_DOT, CRLF_HDOT, TOWARDS, AWAY, AWAY_FROM, INZONE, STATIONARY, STATIONARY_FNAME, TOWARDS_HOME, AWAY_FROM_HOME, INZONE_HOME, INZONE_STATIONARY, @@ -21,7 +21,7 @@ ICLOUD, FMF, FAMSHR, FMF_FNAME, FAMSHR_FNAME, MOBAPP, MOBAPP_FNAME, DATA_SOURCE_FNAME, - TRACK_DEVICE, MONITOR_DEVICE, INACTIVE_DEVICE, + TRACK_DEVICE, MONITOR_DEVICE, INACTIVE_DEVICE, TRACKING_MODE_FNAME, NAME, DEVICE_TYPE_FNAME, ICLOUD_HORIZONTAL_ACCURACY, ICLOUD_VERTICAL_ACCURACY, ICLOUD_BATTERY_STATUS, ICLOUD_BATTERY_LEVEL, ICLOUD_DEVICE_CLASS, ICLOUD_DEVICE_STATUS, ICLOUD_LOW_POWER_MODE, ID, @@ -29,15 +29,15 @@ LATITUDE, LONGITUDE, LOCATION, LOCATION_SOURCE, TRIGGER, TRACKING, NEAR_DEVICE_USED, FROM_ZONE, INTERVAL, - ZONE, ZONE_DISPLAY_AS, ZONE_NAME, ZONE_FNAME, ZONE_DATETIME, - LAST_ZONE, LAST_ZONE_DISPLAY_AS, LAST_ZONE_NAME, LAST_ZONE_FNAME, LAST_ZONE_DATETIME, + ZONE, ZONE_DNAME, ZONE_NAME, ZONE_FNAME, ZONE_DATETIME, + LAST_ZONE, LAST_ZONE_DNAME, LAST_ZONE_NAME, LAST_ZONE_FNAME, LAST_ZONE_DATETIME, BATTERY_SOURCE, BATTERY, BATTERY_LEVEL, BATTERY_STATUS, BATTERY_FAMSHR, BATTERY_MOBAPP, BATTERY_STATUS_CODES, BATTERY_STATUS_FNAME, BATTERY_UPDATE_TIME, ZONE_DISTANCE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, HOME_DISTANCE, MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, WAZE_METHOD, TRAVEL_TIME, TRAVEL_TIME_MIN, TRAVEL_TIME_HHMM, ARRIVAL_TIME, DIR_OF_TRAVEL, - MOVED_DISTANCE, MOVED_TIME_FROM, MOVED_TIME_TO, + MOVED_DISTANCE, MOVED_TIME_FROM, MOVED_TIME_TO, WENT_3KM, 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, @@ -62,15 +62,17 @@ from .helpers.common import (instr, is_zone, isnot_zone, is_statzone, list_add, list_del, circle_letter, format_gps, zone_dname, round_to_zero, set_precision, ) -from .helpers.messaging import (post_event, post_error_msg, post_monitor_msg, log_exception, log_debug_msg, +from .helpers.messaging import (post_event, post_error_msg, post_monitor_msg, + log_exception, log_debug_msg, log_error_msg, + post_startup_alert, post_internal_error, _trace, _traceha, ) from .helpers.time_util import ( time_now_secs, secs_to_time, secs_to_time_str, - secs_since, mins_since, secs_to,mins_to, + secs_since, mins_since, secs_to,mins_to, secs_to_time_hhmm, time_to_12hrtime, secs_since_to_time_str, datetime_to_secs, secs_to_datetime, datetime_now, secs_to_age_str, secs_to_time_age_str, ) -from .helpers.dist_util import (calc_distance_m, calc_distance_km, format_km_to_mi, m_to_ft_str, - format_dist_km, format_dist_m, km_to_mi, m_to_ft, ) +from .helpers.dist_util import (calc_distance_m, calc_distance_km, + km_to_um, m_to_um, m_to_um_ft, ) from .helpers.format import (icon_circle, icon_box, ) from homeassistant.components.device_tracker.config_entry import TrackerEntity @@ -90,14 +92,14 @@ def __init__(self, devicename, conf_device): self.StatZone = None # The StatZone this Device is in or None if not in a StatZone - self.FromZones_by_zone = {} # DeviceFmZones objects for the track_from_zones parameter for this Device - self.FromZone_Home = None # DeviceFmZone object for the Home zone - self.from_zone_names = [] # List of the from_zones in the FromZones_by_zone dictionary + self.FromZones_by_zone = {} # DeviceFmZones objects for the track_from_zones parameter for this Device + self.FromZone_Home = None # DeviceFmZone object for the Home zone + self.from_zone_names = [] # List of the from_zones in the FromZones_by_zone dictionary self.only_track_from_home = True # Track from only Home (True) or also track from other zones (False) self.FromZone_BeingUpdated = None # DeviceFmZone object being updated in determine_interval for EvLog TfZ info self.FromZone_NextToUpdate = None # Set to the DeviceFmZone when it's next_update_time is reached - self.FromZone_TrackFrom = None # DeviceFmZone object for the Closest tfz - used to set the Device's sensors - self.FromZone_LastIn = None # DeviceFmZone object the device was last in + self.FromZone_TrackFrom = None # DeviceFmZone object for the Closest tfz - used to set the Device's sensors + self.FromZone_LastIn = None # DeviceFmZone object the device was last in self.TrackFromBaseZone = None # DeviceFmZone of Home or secondary tracked from zone self.track_from_base_zone = HOME # Name of secondary tracked from base zone (normally Home) self.NearDevice = None # Device in the same location as this Device @@ -221,11 +223,12 @@ def initialize(self): self.check_zone_exit_secs = 0 # Time when a MobApp exited a zone and a non-MobApp was exit check was issued self.offline_secs = 0 # Time the device went offline self.pending_secs = 0 # Time the device went into a pending status (checked after authentication) + self.dist_to_other_devices_secs = 0 self.dist_to_other_devices = {} # A dict of other devices distances # {devicename: [distance_m, gps_accuracy_factor, location_old_flag]} - self.dist_to_other_devices_datetime = DATETIME_ZERO - self.loc_time_updates_famshr = [HHMMSS_ZERO] # Histoty of update times from one results to the next - self.loc_time_updates_mobapp = [HHMMSS_ZERO] # Histoty of update times from one results to the next + self.loc_time_updates_famshr = [HHMMSS_ZERO] # History of update times from one results to the next + self.loc_time_updates_mobapp = [HHMMSS_ZERO] # History of update times from one results to the next + self.loc_msg_famshr_mobapp_time = '' # Time string the locate msg was displayed to prevent dup msgs (icl_data_handlr) self.dev_data_useable_chk_secs = 0 # The device data is checked several times during an update self.dev_data_useable_chk_results = [] # If this check is the same as the last one, return the previous results @@ -426,6 +429,7 @@ def initialize_sensors(self): self.sensors[ZONE_DISTANCE_M_EDGE] = 0 self.sensors[HOME_DISTANCE] = 0 self.sensors[MAX_DISTANCE] = 0 + self.sensors[WENT_3KM] = False self.sensors[WAZE_DISTANCE] = 0 self.sensors[WAZE_METHOD] = 0 self.sensors[CALC_DISTANCE] = 0 @@ -436,12 +440,12 @@ def initialize_sensors(self): # Zone related items self.sensors[ZONE] = NOT_SET - self.sensors[ZONE_DISPLAY_AS] = NOT_SET + self.sensors[ZONE_DNAME] = NOT_SET self.sensors[ZONE_FNAME] = NOT_SET self.sensors[ZONE_NAME] = NOT_SET self.sensors[ZONE_DATETIME] = DATETIME_ZERO self.sensors[LAST_ZONE] = NOT_SET - self.sensors[LAST_ZONE_DISPLAY_AS]=NOT_SET + self.sensors[LAST_ZONE_DNAME] =NOT_SET self.sensors[LAST_ZONE_FNAME] = NOT_SET self.sensors[LAST_ZONE_NAME] = NOT_SET self.sensors[LAST_ZONE_DATETIME] = DATETIME_ZERO @@ -486,7 +490,7 @@ def configure_device(self, conf_device): # a monitored device only monitors the iCloud data aand iOS Data may be available self.tracking_mode = conf_device.get(CONF_TRACKING_MODE, 'track') self.fname = conf_device.get(CONF_FNAME, self.devicename.title()) - self.sensors[NAME] = self.fname + self.sensors[NAME] = self.fname_devicename self.sensors['dev_id'] = self.devicename self.sensors['host_name'] = self.fname self.evlog_fname_alert_char = '' # Character added to the fmame in the EvLog (❗❌⚠️) @@ -519,9 +523,10 @@ def configure_device(self, conf_device): self.fixed_interval_secs = conf_device.get(CONF_FIXED_INTERVAL, 0) * 60 self.statzone_inzone_interval_secs = min(self.inzone_interval_secs, Gb.statzone_inzone_interval_secs) - self.log_zones = conf_device.get(CONF_LOG_ZONES, []) - self.track_from_zones = conf_device.get(CONF_TRACK_FROM_ZONES, HOME).copy() self._validate_zone_parameters() + self.log_zones = conf_device.get(CONF_LOG_ZONES, ['none']) + self.track_from_zones = conf_device.get(CONF_TRACK_FROM_ZONES, [HOME]).copy() + self.track_from_base_zone = conf_device.get(CONF_TRACK_FROM_BASE_ZONE, HOME) try: # Update tfz with master base zone, also remove Home zone if necessaryself.track_from_base_zone @@ -536,6 +541,7 @@ def configure_device(self, conf_device): if self.track_from_base_zone == HOME: self.track_from_zones = list_add(self.track_from_zones, HOME) + # Put it at the end of the track-from list if self.track_from_base_zone != self.track_from_zones[-1]: self.track_from_zones = list_del(self.track_from_zones, self.track_from_base_zone) self.track_from_zones = list_add(self.track_from_zones, self.track_from_base_zone) @@ -602,8 +608,8 @@ def initialize_track_from_zones(self): # Validate the zone in the config parameter. If valid, get the Zone object # and add to the device's FromZones_by_zone object list - if self.track_from_zones == []: - self.track_from_zones.append(HOME) + if self.track_from_zones == [] or self.track_from_zones == '': + self.track_from_zones = [HOME] # Reuse current DeviceFmZones if it exists. #track_from_zones = self.track_from_zones.copy() @@ -655,29 +661,57 @@ def _validate_zone_parameters(self): parameters. If one is found, remove it and update the Device's configuration ''' invalid_zone_msg = '' - invalid_zones = set() - for zone in self.track_from_zones.copy(): - if zone not in Gb.HAZones_by_zone: - invalid_zones.add(zone) - invalid_zone_msg += f"{CRLF_HDOT}Track-from-Zone setting" + tfz_zones = lza_zones = tfbz_zone = '' - for zone in self.log_zones.copy(): + # Check track-from-zones + if self.conf_device[CONF_TRACK_FROM_ZONES] in ['', []]: + self.conf_device[CONF_TRACK_FROM_ZONES] = [HOME] + tfz_zones += "Initialized" + for zone in self.conf_device[CONF_TRACK_FROM_ZONES].copy(): + if zone not in Gb.HAZones_by_zone: + tfz_zones += f"{zone}, " + self.conf_device[CONF_TRACK_FROM_ZONES] = list_del(self.conf_device[CONF_TRACK_FROM_ZONES], zone) + + # Check log-zone-activity + if self.conf_device[CONF_LOG_ZONES] in ['', []]: + self.conf_device[CONF_LOG_ZONES] = ['none'] + lza_zones += "Initialized" + for zone in self.conf_device[CONF_LOG_ZONES].copy(): if zone.startswith('name-') or zone == 'none': continue if zone not in Gb.HAZones_by_zone: - invalid_zones.add(zone) - invalid_zone_msg += f"{CRLF_HDOT}Log Zone Activity setting" + lza_zones += f"{zone}, " + self.conf_device[CONF_LOG_ZONES] = list_del(self.conf_device[CONF_LOG_ZONES], zone) + + #Check Track from base zone + if self.conf_device[CONF_TRACK_FROM_BASE_ZONE] not in Gb.Zones_by_zone: + tfbz_zone += f"{zone}" + invalid_zone_msg += f"{CRLF_HDOT}Track-from-base-Home zone setting" + self.conf_device[CONF_TRACK_FROM_BASE_ZONE] = HOME - if invalid_zone_msg: - for zone in invalid_zones: - self.remove_zone_from_settings(zone) + if lza_zones or tfz_zones or tfbz_zone: + config_file.write_storage_icloud3_configuration_file() + self.set_fname_alert(YELLOW_ALERT) + + post_startup_alert( f"Device Config Error > Unknown Zone removed " + f"{CRLF_DOT}{self.fname_devicename}") alert_msg = (f"{EVLOG_ALERT}CONFIGURATION PARAMETER ERROR > " - f"An unknown zone has been removed from the Device's " - f"configuration parameters." - f"{CRLF_DOT}{self.fname_devicename}, Zone-{zone}" - f"{invalid_zone_msg}") + f"Unknown zones have been removed from the Device's " + f"configuration parameters. Verify these parameters " + f"on the Configure Settings > Update Device screen." + f"{CRLF_DOT}{self.fname_devicename}") + zone_msg = "" + if lza_zones: + zone_msg = f"{CRLF_HDOT}Log Zone Activity ({lza_zones}), " + if tfz_zones: + zone_msg = f"{CRLF_HDOT}Track From Zone ({tfz_zones}), " + if tfbz_zone: + zone_msg = f"{CRLF_HDOT}Track From Base Home Zone ({tfbz_zone})" + alert_msg += zone_msg post_event(alert_msg) + log_error_msg( f"ICLOUD3 ERROR > Unknown Zone removed from device parameter. " + f"Device-{self.fname_devicename}, {zone_msg}") #-------------------------------------------------------------------- def remove_zone_from_settings(self, zone): @@ -688,6 +722,11 @@ def remove_zone_from_settings(self, zone): - primary home zone ''' try: + if (zone == HOME + or Gb.start_icloud3_inprocess_flag + or Gb.restart_icloud3_request_flag): + return + conf_file_updated_flag = False if zone in self.track_from_zones: conf_file_updated_flag = True @@ -697,14 +736,21 @@ def remove_zone_from_settings(self, zone): if zone in self.FromZones_by_zone: del self.FromZones_by_zone[zone] + if (self.conf_device[CONF_TRACK_FROM_ZONES] == [] + or HOME not in self.conf_device[CONF_TRACK_FROM_ZONES]): + conf_file_updated_flag = True + self.track_from_base_zone = HOME + self.conf_device[CONF_TRACK_FROM_ZONES] = [HOME] + # Cycle through the zones that are no longer tracked from for the device, then cycle # through the Device's sensor list and remove all track_from_zone sensors ending with # that zone. - device_tfz_sensors = Gb.Sensors_by_devicename_from_zone.get(self.devicename) + device_tfz_sensors = Gb.Sensors_by_devicename_from_zone.get(self.devicename, []) for sensor, Sensor in device_tfz_sensors.items(): if sensor.endswith(f"_{zone}") and Sensor.entity_removed_flag is False: Sensor.remove_entity() + # Update log_zone_activity zone if zone in self.log_zones: conf_file_updated_flag = True self.log_zones = list_del(self.log_zones, zone) @@ -712,7 +758,9 @@ def remove_zone_from_settings(self, zone): if len(self.conf_device[CONF_LOG_ZONES]) <= 1: self.conf_device[CONF_LOG_ZONES] = ['none'] - if self.track_from_base_zone == zone: + # Update track_from_base_home_zone, set back to Home + if (self.track_from_base_zone == zone + or self.conf_device[CONF_TRACK_FROM_BASE_ZONE] not in Gb.Zones_by_zone): conf_file_updated_flag = True self.track_from_base_zone = HOME self.conf_device[CONF_TRACK_FROM_BASE_ZONE] = HOME @@ -767,9 +815,20 @@ def device_id8_fmf(self): return f"#{self.device_id_fmf[:8]}" return 'None' + @property + def tracking_mode_fname(self, track_fname=False): + if self.tracking_mode == TRACK_DEVICE and track_fname is False: + return '' + else: + return f"({TRACKING_MODE_FNAME[self.tracking_mode]})" + def is_statzone_name(self, zone_name): return zone_name in Gb.StatZones_by_zone + def set_fname_alert(self, alert_char): + if instr(self.evlog_fname_alert_char, alert_char) is False: + self.evlog_fname_alert_char += alert_char + @property def PyiCloud_RawData_famshr(self): if Gb.PyiCloud is None: @@ -1094,32 +1153,33 @@ def isnot_set(self): return (self.sensors[ZONE] == NOT_SET) @property - def is_inzone(self): - return (self.loc_data_zone not in [NOT_HOME, NOT_SET]) + def isin_zone(self): + return (self.loc_data_zone not in NOT_HOME_ZONES) + #return (self.loc_data_zone not in [NOT_HOME, NOT_SET]) @property - def isnot_inzone(self): - return (self.loc_data_zone in [NOT_HOME, NOT_SET]) + def isnotin_zone(self): + return (self.loc_data_zone in NOT_HOME_ZONES) @property - def is_inzone_mobapp_state(self): - return (self.mobapp_data_state not in [NOT_HOME, NOT_SET]) + def isin_zone_mobapp_state(self): + return (self.mobapp_data_state not in NOT_HOME_ZONES) @property - def isnot_inzone_mobapp_state(self): - return (self.mobapp_data_state in [NOT_HOME, NOT_SET]) + def isnotin_zone_mobapp_state(self): + return (self.mobapp_data_state in NOT_HOME_ZONES) @property def is_tracking_from_another_zone(self): - return self.sensors[FROM_ZONE] and self.sensors[FROM_ZONE] not in [NOT_SET, HOME] + return (self.sensors[FROM_ZONE] and self.sensors[FROM_ZONE] not in [NOT_SET, HOME]) @property - def was_inzone(self): - return (self.sensors[ZONE] not in [NOT_HOME, AWAY, AWAY_FROM, NOT_SET]) + def wasin_zone(self): + return (self.sensors[ZONE] not in NOT_HOME_ZONES) @property - def wasnot_inzone(self): - return (self.sensors[ZONE] in [NOT_HOME, AWAY, AWAY_FROM]) + def wasnotin_zone(self): + return (self.sensors[ZONE] in NOT_HOME_ZONES) @property def is_statzone_trigger_reached(self): @@ -1127,19 +1187,23 @@ def is_statzone_trigger_reached(self): #-------------------------------------------------------------------- @property - def is_in_statzone(self): + def isin_nonstatzone(self): + return (self.isin_zone and self.isnotin_statzone) + + @property + def isin_statzone(self): return self.StatZone is not None @property - def isnot_in_statzone(self): + def isnotin_statzone(self): return self.StatZone is None @property - def was_in_statzone(self): + def wasin_statzone(self): return (is_statzone(self.sensors[ZONE])) @property - def wasnot_in_statzone(self): + def wasnotin_statzone(self): return (is_statzone(self.sensors[ZONE]) is False) @property @@ -1188,11 +1252,13 @@ def update_distance_moved(self, distance): if Gb.evlog_trk_monitors_flag: log_msg = (f"StatZone Movement > " - f"TotalMoved-{format_dist_km(self.statzone_dist_moved_km)}, " - f"UnderMoveLimit-{self.statzone_dist_moved_km <= Gb.statzone_dist_move_limit_km}, " - f"Timer-{secs_to_time(self.statzone_timer)}, " - f"TimerLeft- {self.statzone_timer_left} secs, " - f"TimerExpired-{self.is_statzone_timer_reached}") + f"TotalMoved-{km_to_um(self.statzone_dist_moved_km)}") + if self.is_statzone_timer_set: + log_msg += (f", Timer-{secs_to_time(self.statzone_timer)}" + f"UnderMoveLimit-" + f"{self.statzone_dist_moved_km <= Gb.statzone_dist_move_limit_km}, " + f"TimerLeft- {self.statzone_timer_left} secs, " + f"TimerExpired-{self.is_statzone_timer_reached}") post_monitor_msg(self.devicename, log_msg) return self.statzone_dist_moved_km @@ -1566,12 +1632,13 @@ def badge_sensor_value(self): sensor_value = PAUSED_CAPS # Display zone name if in a zone - elif self.loc_data_zone != NOT_HOME and self.isnot_in_statzone: + # elif self.loc_data_zone != NOT_HOME and self.isnotin_statzone: + elif self.isin_zone and self.isnotin_statzone: sensor_value = self.loc_data_zone_fname # Display the distance to Home elif self.FromZone_Home: - sensor_value = format_km_to_mi(self.FromZone_Home.zone_dist) + sensor_value = (self.FromZone_Home.zone_dist) else: sensor_value = BLANK_SENSOR_FIELD @@ -1635,7 +1702,7 @@ def calculate_old_location_threshold(self): interval_secs = FromZone.interval_secs threshold_secs = 60 - if self.is_inzone: + if self.isin_zone: threshold_secs = interval_secs * .025 # 2.5% of interval_secs time if threshold_secs < 120: threshold_secs = 120 @@ -1716,31 +1783,29 @@ def update_distance_to_other_devices(self): Cycle through all devices and update this device's and the other device's dist_to_other_device_info field - {devicename: [distance_m, gps_accuracy_factor, display_text]} + {devicename: [distance_m, gps_accuracy_factor, loc_time (newer), display_text]} ''' + update_at_time = secs_to_time_hhmm(self.loc_data_secs) + self.dist_to_other_devices_secs = self.loc_data_secs for _devicename, _Device in Gb.Devices_by_devicename_tracked.items(): if _Device is self: continue - dist_apart_m = _Device.distance_m(self.loc_data_latitude, self.loc_data_longitude) - min_gps_accuracy = (min(self.loc_data_gps_accuracy, _Device.loc_data_gps_accuracy)) - loc_data_secs = max(self.loc_data_secs, _Device.loc_data_secs) + dist_apart_m = _Device.distance_m(self.loc_data_latitude, self.loc_data_longitude) + min_gps_accuracy = (min(self.loc_data_gps_accuracy, _Device.loc_data_gps_accuracy)) + gps_msg = f"±{min_gps_accuracy}" if min_gps_accuracy > Gb.gps_accuracy_threshold else '' + loc_data_time = secs_to_time_hhmm(_Device.loc_data_secs) + #time_msg = '' if update_at_time == loc_data_time else f" ({loc_data_time})" + time_msg = f" ({loc_data_time})" + display_text = f"{m_to_um(dist_apart_m)}{gps_msg}{time_msg}" - age_secs = secs_since(loc_data_secs) - time_msg = f"/{secs_to_time_str(age_secs).replace(' ', '+ ')} ago" if age_secs > 120 else '' - gps_msg = f"±{min_gps_accuracy}" if min_gps_accuracy > Gb.gps_accuracy_threshold else '' - display_text = f"{format_dist_m(dist_apart_m)}{gps_msg}{time_msg}" - - dist_apart_data = [dist_apart_m, min_gps_accuracy, loc_data_secs, display_text] + dist_apart_data = [dist_apart_m, min_gps_accuracy, _Device.loc_data_secs, display_text] if (_devicename not in self.dist_to_other_devices or self.devicename not in _Device.dist_to_other_devices or _Device.dist_to_other_devices[self.devicename] != dist_apart_data or self.dist_to_other_devices[_devicename] != dist_apart_data): - before_s = self.dist_to_other_devices.get(_devicename) - - self.dist_to_other_devices_datetime = datetime_now() self.dist_to_other_devices[_devicename] = dist_apart_data _Device.dist_to_other_devices[self.devicename] = dist_apart_data @@ -1780,8 +1845,9 @@ def update_battery_data_from_mobapp(self): return False battery_level = int(battery_level_attrs[STATE]) - battery_status = 'charging' if battery_level < 100 and instr(battery_level_attrs['icon'], 'charging') \ - else 'not charging' + battery_status = 'charging' \ + if battery_level < 100 and instr(battery_level_attrs['icon'], 'charging') \ + else 'not charging' battery_update_secs = battery_level_attrs[LAST_CHANGED_SECS] except Exception as err: @@ -1969,7 +2035,7 @@ def display_update_location_msg(self): if self.loc_data_time_gps == self.last_loc_data_time_gps: return - if self.isnot_inzone or self.loc_data_dist_moved_km > .015: + if self.isnotin_zone or self.loc_data_dist_moved_km > .015: event_msg =(f"Updated > " f"{self.last_loc_data_time_gps}" f"{RARROW}{self.dev_data_source}-{self.loc_data_time_gps}") @@ -2049,6 +2115,7 @@ def update_sensor_values_from_data_fields(self): self.sensors[ZONE_DISTANCE_M] = self.FromZone_TrackFrom.sensors[ZONE_DISTANCE_M] self.sensors[ZONE_DISTANCE_M_EDGE] = self.FromZone_TrackFrom.sensors[ZONE_DISTANCE_M_EDGE] self.sensors[MAX_DISTANCE] = self.FromZone_TrackFrom.sensors[MAX_DISTANCE] + self.sensors[WENT_3KM] = 'true' if self.went_3km else 'false' self.sensors[WAZE_DISTANCE] = self.FromZone_TrackFrom.sensors[WAZE_DISTANCE] self.sensors[WAZE_METHOD] = self.FromZone_TrackFrom.sensors[WAZE_METHOD] self.sensors[CALC_DISTANCE] = self.FromZone_TrackFrom.sensors[CALC_DISTANCE] @@ -2063,17 +2130,17 @@ def update_sensor_values_from_data_fields(self): # Update the last zone info if the device was in a zone and now not in a zone or went immediatelly from # one zone to another (it was in a zone and still is in a zone and the old zone is differenent than the new zone) - if (self.wasnot_in_statzone - and (self.was_inzone and self.isnot_inzone) - or (self.was_inzone and self.is_inzone and self.sensors[ZONE] != self.loc_data_zone)): - self.last_zone = self.sensors[ZONE] + if (self.wasnotin_statzone + and (self.wasin_zone and self.isnotin_zone) + or (self.wasin_zone and self.isin_zone and self.sensors[ZONE] != self.loc_data_zone)): + self.last_zone = self.sensors[ZONE] if self.last_zone in self.from_zone_names: self.last_tracked_from_zone = self.last_zone - self.sensors[LAST_ZONE] = self.sensors[ZONE] - self.sensors[LAST_ZONE_DISPLAY_AS] = self.sensors[ZONE_DISPLAY_AS] - self.sensors[LAST_ZONE_NAME] = self.sensors[ZONE_NAME] - self.sensors[LAST_ZONE_FNAME] = self.sensors[ZONE_FNAME] - self.sensors[LAST_ZONE_DATETIME] = secs_to_datetime(time_now_secs()) + self.sensors[LAST_ZONE] = self.sensors[ZONE] + self.sensors[LAST_ZONE_DNAME] = self.sensors[ZONE_DNAME] + self.sensors[LAST_ZONE_NAME] = self.sensors[ZONE_NAME] + self.sensors[LAST_ZONE_FNAME] = self.sensors[ZONE_FNAME] + self.sensors[LAST_ZONE_DATETIME] = secs_to_datetime(time_now_secs()) if Zone := Gb.Zones_by_zone.get(self.loc_data_zone): self.sensors[ZONE] = self.loc_data_zone @@ -2081,7 +2148,7 @@ def update_sensor_values_from_data_fields(self): Zone = Gb.HomeZone self.sensors[ZONE] = self.loc_data_zone = HOME - self.sensors[ZONE_DISPLAY_AS] = Zone.dname + self.sensors[ZONE_DNAME] = Zone.dname self.sensors[ZONE_NAME] = Zone.name self.sensors[ZONE_FNAME] = Zone.fname self.sensors[DEVICE_TRACKER_STATE] = self.format_device_tracker_state(Zone) @@ -2129,7 +2196,7 @@ def _set_sensors_special_icon(self): self.sensors_icon[DIR_OF_TRAVEL] = self.sensors_icon[ARRIVAL_TIME] = \ SENSOR_ICONS[INZONE_HOME] #['arrival_time_in_home'] - elif self.is_in_statzone: + elif self.isin_statzone: self.sensors_icon[DIR_OF_TRAVEL] = SENSOR_ICONS[INZONE_STATIONARY] else: @@ -2146,7 +2213,7 @@ def _set_sensors_special_icon(self): SENSOR_ICONS[TOWARDS] icon = INZONE - if self.is_inzone:# else TOWARDS + if self.isin_zone:# else TOWARDS self.sensors_icon[ARRIVAL_TIME] = SENSOR_ICONS[icon] else: self.sensors_icon[ARRIVAL_TIME] = icon_box(self.sensors[FROM_ZONE]) @@ -2212,7 +2279,7 @@ def format_info_msg(self): info_msg += (f"IntoStatZone-{secs_to_time(self.statzone_timer)}, ") elif self.zone_change_secs > 0: - if self.is_inzone: + if self.isin_zone: info_msg +=( f"@{zone_dname(self.loc_data_zone)}-" f"{secs_to_time_age_str(self.zone_change_secs)}, ") elif self.mobapp_zone_exit_zone != '': @@ -2229,7 +2296,7 @@ def format_info_msg(self): if self.NearDeviceUsed: info_msg +=(f"UsedNearbyDevice-{self.NearDeviceUsed.fname}, " - f"({format_dist_m(self.near_device_distance)}") + f"({m_to_um_ft(Device.near_device_distance, as_integer=True)}") # if self.data_source != self.dev_data_source.lower(): #info_msg += f"LocationData-{self.dev_data_source}, " diff --git a/custom_components/icloud3/device_fm_zone.py b/custom_components/icloud3/device_fm_zone.py index b718c00..ac6efe1 100644 --- a/custom_components/icloud3/device_fm_zone.py +++ b/custom_components/icloud3/device_fm_zone.py @@ -29,7 +29,7 @@ LAST_UPDATE, LAST_UPDATE_TIME, LAST_UPDATE_DATETIME, NEXT_UPDATE, NEXT_UPDATE_TIME, NEXT_UPDATE_DATETIME, ) -from .helpers.dist_util import (km_to_mi, calc_distance_km, format_km_to_mi,) +from .helpers.dist_util import (calc_distance_km, km_to_um,) from .helpers.time_util import (datetime_to_12hrtime, ) from .helpers.messaging import (log_exception, post_internal_error, _trace, _traceha, ) @@ -83,7 +83,8 @@ def initialize(self): self.max_dist_km = 0 self.sensor_prefix = (f"sensor.{self.devicename}_") \ - if self.from_zone== HOME else (f"sensor.{self.devicename}_{self.from_zone}_") + if self.from_zone== HOME \ + else (f"sensor.{self.devicename}_{self.from_zone}_") self.sensor_prefix_zone = '' if self.from_zone== HOME else (f"{self.from_zone}_") self.info_status_msg = (f"From-({self.from_zone})") @@ -122,7 +123,7 @@ def initialize_sensors(self): self.sensors[ZONE_INFO] = '' Sensors_from_zone = Gb.Sensors_by_devicename_from_zone.get(self.devicename, {}) - from_this_zone_sensors = {k:v for k, v in Sensors_from_zone.items() + from_this_zone_sensors = {k:v for k, v in Sensors_from_zone.items() if v.from_zone == self.from_zone} for sensor, Sensor in from_this_zone_sensors.items(): Sensor.FromZone = self @@ -132,7 +133,7 @@ def __repr__(self): @property def zone_distance_str(self): - return ('' if self.zone_dist == 0 else (f"{format_km_to_mi(self.zone_dist)}")) + return ('' if self.zone_dist == 0 else (f"{km_to_um(self.zone_dist)}")) @property def distance_km(self): diff --git a/custom_components/icloud3/device_tracker.py b/custom_components/icloud3/device_tracker.py index e501473..dde276d 100644 --- a/custom_components/icloud3/device_tracker.py +++ b/custom_components/icloud3/device_tracker.py @@ -13,9 +13,10 @@ LOCATION_SOURCE, TRIGGER, ZONE, ZONE_DATETIME, LAST_ZONE, FROM_ZONE, ZONE_FNAME, BATTERY, BATTERY_LEVEL, - CALC_DISTANCE, WAZE_DISTANCE, HOME_DISTANCE, + MAX_DISTANCE, CALC_DISTANCE, WAZE_DISTANCE, + HOME_DISTANCE, ZONE_DISTANCE, DEVICE_STATUS, - LAST_UPDATE, LAST_UPDATE_DATETIME, + LAST_UPDATE, LAST_UPDATE_DATETIME, WENT_3KM, NEXT_UPDATE, NEXT_UPDATE_DATETIME, LAST_LOCATED, LAST_LOCATED_DATETIME, GPS_ACCURACY, ALTITUDE, VERT_ACCURACY, @@ -242,6 +243,10 @@ def __init__(self, devicename, conf_device, data=None): self._on_remove = [self.after_removal_cleanup] self.entity_removed_flag = False + self.extra_attrs_track_from_zones = 'home' + self.extra_attrs_primary_home_zone = 'Home' + self.extra_attrs_away_time_zone_offset = 'HomeZone' + Gb.device_trackers_created_cnt += 1 log_debug_msg(f'Device Tracker entity created: {self.entity_id}, #{Gb.device_trackers_created_cnt}') @@ -347,43 +352,51 @@ def _get_extra_attributes(self): Get the extra attributes for the device_tracker ''' try: - extra_attrs = {} - - extra_attrs[GPS] = f"({self.latitude}, {self.longitude})" - extra_attrs['integration'] = ICLOUD3 - extra_attrs['data_source'] = f"{self._get_sensor_value(LOCATION_SOURCE)}" - extra_attrs[DEVICE_STATUS] = self._get_sensor_value(DEVICE_STATUS) - extra_attrs[NAME] = self._get_sensor_value(NAME) - extra_attrs[PICTURE] = self._get_sensor_value(PICTURE) - extra_attrs[ZONE] = self._get_sensor_value(ZONE) - extra_attrs[LAST_ZONE] = self._get_sensor_value(LAST_ZONE) - extra_attrs[ZONE_DATETIME] = self._get_sensor_value(ZONE_DATETIME) - extra_attrs[LAST_LOCATED] = self._get_sensor_value(LAST_LOCATED_DATETIME) - extra_attrs[LAST_UPDATE] = self._get_sensor_value(LAST_UPDATE_DATETIME) - extra_attrs[HOME_DISTANCE] = self._get_sensor_value(HOME_DISTANCE) - extra_attrs[DISTANCE_TO_DEVICES] = self._get_sensor_value(DISTANCE_TO_DEVICES) - - if self.Device and self.Device.is_tracked: - extra_attrs[NEXT_UPDATE] = self._get_sensor_value(NEXT_UPDATE_DATETIME) - extra_attrs[TRIGGER] = self._get_sensor_value(TRIGGER) - extra_attrs[FROM_ZONE] = self._get_sensor_value(FROM_ZONE) - extra_attrs[WAZE_DISTANCE] = self._get_sensor_value(WAZE_DISTANCE) - extra_attrs[CALC_DISTANCE] = self._get_sensor_value(CALC_DISTANCE) - if self.Device: if self.Device.track_from_zones != [HOME]: - extra_attrs['track_from_zones'] = ', '.join(self.Device.track_from_zones) + self.extra_attrs_track_from_zones = ', '.join(self.Device.track_from_zones) if self.Device.track_from_base_zone != HOME: - extra_attrs['primary_home_zone'] = zone_dname(self.Device.track_from_base_zone) + self.extra_attrs_primary_home_zone = zone_dname(self.Device.track_from_base_zone) if self.Device.away_time_zone_offset != 0: plus_minus = '+' if self.Device.away_time_zone_offset > 0 else '' - extra_attrs['away_time_zone_offset'] = \ + self.extra_attrs_away_time_zone_offset = \ f"HomeZone {plus_minus}{self.Device.away_time_zone_offset} hours" - extra_attrs['icloud3_version'] = Gb.version - extra_attrs['event_log_version'] = Gb.version_evlog - extra_attrs['tracking'] = ', '.join(Gb.Devices_by_devicename.keys()) - extra_attrs['icloud3_directory'] = Gb.icloud3_directory + extra_attrs = {} + + extra_attrs[GPS] = f"({self.latitude}, {self.longitude})" + extra_attrs[f"{'-'*24}"] = f"{'-'*25}" + extra_attrs['integration'] = ICLOUD3 + extra_attrs[NAME] = self._get_sensor_value(NAME) + extra_attrs[PICTURE] = self._get_sensor_value(PICTURE) + extra_attrs['picture_file'] = self._get_sensor_value(PICTURE) + extra_attrs['track_from_zones'] = self.extra_attrs_track_from_zones + extra_attrs['primary_home_zone'] = self.extra_attrs_primary_home_zone + extra_attrs['away_time_zone_offset'] = self.extra_attrs_away_time_zone_offset + + extra_attrs[f"{'-'*25}"] = f"{'-'*25}" + extra_attrs['data_source'] = f"{self._get_sensor_value(LOCATION_SOURCE)}" + extra_attrs[DEVICE_STATUS] = self._get_sensor_value(DEVICE_STATUS) + extra_attrs[TRIGGER] = self._get_sensor_value(TRIGGER) + extra_attrs[ZONE] = self._get_sensor_value(ZONE) + extra_attrs[LAST_ZONE] = self._get_sensor_value(LAST_ZONE) + extra_attrs[FROM_ZONE] = self._get_sensor_value(FROM_ZONE) + extra_attrs[HOME_DISTANCE] = self._get_sensor_value(HOME_DISTANCE) + extra_attrs[ZONE_DISTANCE] = self._get_sensor_value(ZONE_DISTANCE) + extra_attrs[MAX_DISTANCE] = self._get_sensor_value(MAX_DISTANCE) + extra_attrs[CALC_DISTANCE] = self._get_sensor_value(CALC_DISTANCE) + extra_attrs[WAZE_DISTANCE] = self._get_sensor_value(WAZE_DISTANCE) + extra_attrs[DISTANCE_TO_DEVICES] = self._get_sensor_value(DISTANCE_TO_DEVICES) + extra_attrs[ZONE_DATETIME] = self._get_sensor_value(ZONE_DATETIME) + extra_attrs[LAST_LOCATED] = self._get_sensor_value(LAST_LOCATED_DATETIME) + extra_attrs[LAST_UPDATE] = self._get_sensor_value(LAST_UPDATE_DATETIME) + extra_attrs[NEXT_UPDATE] = self._get_sensor_value(NEXT_UPDATE_DATETIME) + + extra_attrs[f"{'-'*26}"] = f"{'-'*25}" + extra_attrs['icloud3_devices'] = ', '.join(Gb.Devices_by_devicename.keys()) + extra_attrs['icloud3_version'] = f"v{Gb.version}" + extra_attrs['event_log_version'] = f"v{Gb.version_evlog}" + extra_attrs['icloud3_directory'] = Gb.icloud3_directory return extra_attrs @@ -403,7 +416,7 @@ def _get_sensor_value(self, sensor, number=False): return self._get_restore_or_default_value(sensor, not_set_value) sensor_value = self.Device.sensors.get(sensor, None) - if self.Device and self.Device.away_time_zone_offset != 0: + if self.Device.away_time_zone_offset != 0: sensor_value = adjust_time_hour_values(sensor_value, self.Device.away_time_zone_offset) if instr(sensor, DEVICE_TRACKER_STATE): diff --git a/custom_components/icloud3/global_variables.py b/custom_components/icloud3/global_variables.py index dd92f6e..6c61b8c 100644 --- a/custom_components/icloud3/global_variables.py +++ b/custom_components/icloud3/global_variables.py @@ -135,15 +135,16 @@ class GlobalVariables(object): Devices_by_devicename_tracked = {} # All monitored Devices by devicename Devices_by_icloud_device_id = {} # Devices by the icloud device_id receive from Apple Devices_by_ha_device_id = {} # Device by the device_id in the entity/device registry - Devices_by_mobapp_devicename = {} # All Devices by the mobapp device_tracker.mobapp_devicename + Devices_by_mobapp_devicename = {} # All verified Devices by the conf_mobapp_devicename + devicenames_x_mobapp_devicename = {} # All devicenames by conf_mobapp_devicename from conf_devices (both ways) PairedDevices_by_paired_with_id = {} # Paired Devices by the paired_with_id (famshr prsID) id=[Dev1, Dev2] Zones = [] # Zones object list Zones_by_zone = {} # Zone object by zone name for HA Zones and iC3 Pseudo Zones HAZones = [] # Zones object list for only valid HA Zones HAZones_by_zone = {} # Zone object by zone name for only valid HA Zones - HAZones_by_zone_deleted = {} # Zone object by zone name for Zones deleted from HA + HAZones_by_zone_deleted = {} # Zone object by zone name for Zones deleted from HA ha_zone_settings_check_secs = 0 # Last time the ha.states Zone config was checked for changes - zone_display_as = {} # Zone display_as by zone distionary to ease displaying zone fname + zones_dname = {} # Zone display_as by zone distionary to ease displaying zone fname TrackedZones_by_zone = {HOME, None} # Tracked zones object by zone name set up with Devices.DeviceFmZones object StatZones = [] # Stationary Zone objects StatZones_to_delete = [] # Stationary Zone to delete after the devices that we're in it have been updated diff --git a/custom_components/icloud3/helpers/common.py b/custom_components/icloud3/helpers/common.py index 11af1ef..c0bdd43 100644 --- a/custom_components/icloud3/helpers/common.py +++ b/custom_components/icloud3/helpers/common.py @@ -96,7 +96,7 @@ def is_statzone(zone): #-------------------------------------------------------------------- def isnot_statzone(zone): - return instr(zone, STATIONARY) is False + return (instr(zone, STATIONARY) is False) #-------------------------------------------------------------------- def isnumber(string): @@ -131,6 +131,9 @@ def set_precision(value, um=None): Return the distance value as an integer or float value ''' try: + if type(value) not in ['float', 'int']: + return value + um = um if um else Gb.um precision = 5 if um in ['km', 'mi'] else 2 if um in ['m', 'ft'] else 4 value = round(float(value), precision) @@ -207,22 +210,22 @@ def obscure_field(field): #-------------------------------------------------------------------- def zone_dname(zone): try: - return Gb.zone_display_as[zone] + return Gb.zones_dname[zone] except: if zone in Gb.Zones_by_zone: Zone = Gb.Zones_by_zone[zone] - Gb.zone_display_as[zone] = Zone.dname + Gb.zones_dname[zone] = Zone.dname elif is_statzone(zone): - Gb.zone_display_as[zone] = f"StatZone{zone[-1]}" + Gb.zones_dname[zone] = f"StatZone{zone[-1]}" else: - Gb.zone_display_as[zone] = zone.title() - return Gb.zone_display_as[zone] + Gb.zones_dname[zone] = zone.title() + return Gb.zones_dname[zone] #-------------------------------------------------------------------- def zone_display_as(zone): - if is_statzone(zone) and zone not in Gb.zone_display_as: + if is_statzone(zone) and zone not in Gb.zones_dname: return 'StatZone' - return Gb.zone_display_as.get(zone, zone.title()) + return Gb.zones_dname.get(zone, zone.title()) #-------------------------------------------------------------------- def format_gps(latitude, longitude, accuracy, latitude_to=None, longitude_to=None): diff --git a/custom_components/icloud3/helpers/dist_util.py b/custom_components/icloud3/helpers/dist_util.py index faacd47..accb005 100644 --- a/custom_components/icloud3/helpers/dist_util.py +++ b/custom_components/icloud3/helpers/dist_util.py @@ -9,100 +9,92 @@ #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # -# Distance conversion and formatting functions +# Distance calculation and conversion functions # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +def calc_distance_km(from_gps, to_gps): + + dist_m = calc_distance_m(from_gps, to_gps) + return round_to_zero(dist_m/1000) + +#-------------------------------------------------------------------- +def calc_distance_m(from_gps, to_gps): + from_lat, from_long = from_gps + to_lat, to_long = to_gps + + if (from_lat is None or from_long is None or to_lat is None or to_long is None + or from_lat == 0 or from_long == 0 or to_lat == 0 or to_long == 0): + return 0 + + dist_m = distance(from_lat, from_long, to_lat, to_long) + dist_m = round_to_zero(dist_m) + dist_m = 0 if dist_m < .002 else dist_m + return dist_m + +#-------------------------------------------------------------------- def km_to_mi(dist_km): return float(dist_km) * Gb.um_km_mi_factor +#-------------------------------------- def mi_to_km(dist_mi): return round(float(dist_mi) / Gb.um_km_mi_factor, 2) -def km_to_mi_str(dist_km): - return f"{km_to_mi(dist_km)} {Gb.um}" - -#-------------------------------------------------------------------- +#-------------------------------------- def m_to_ft(dist_m): return float(dist_m) * Gb.um_m_ft_factor -def m_to_ft_str(dist_m): - return f"{m_to_ft(dist_m)} {Gb.um_m_ft}" +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# Distance string formatting functions +# +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>- +def m_to_um_ft(dist_m, as_integer=False): -#-------------------------------------------------------------------- -def calc_distance_km(from_gps, to_gps): + if Gb.um_KM: + if round_to_zero(dist_m) == 0: return "0m" + if as_integer: return f"{int(dist_m)}m" + return f"{dist_m:.1f}m" - distance_m = calc_distance_m(from_gps, to_gps) - return round_to_zero(distance_m/1000) + dist_ft = m_to_ft(dist_m) + if round_to_zero(dist_ft) == 0: return "0ft" + if as_integer: return f"{int(dist_ft)}ft" + return f"{dist_ft:.1f}ft" #-------------------------------------------------------------------- -def calc_distance_m(from_gps, to_gps): - from_lat, from_long = from_gps - to_lat, to_long = to_gps +def m_to_um(dist_m): + return km_to_um(dist_m / 1000) - if (from_lat is None or from_long is None or to_lat is None or to_long is None - or from_lat == 0 or from_long == 0 or to_lat == 0 or to_long == 0): - return 0 +def km_to_um(dist_km): + if Gb.um_KM: + return format_dist_km(dist_km) - distance_m = distance(from_lat, from_long, to_lat, to_long) - distance_m = round_to_zero(distance_m) - distance_m = 0 if distance_m < .002 else distance_m - return distance_m - return round_to_zero(distance_m) + dist_mi = dist_km * Gb.um_km_mi_factor + return format_dist_mi(dist_mi) - # distance_m = distance(from_lat, from_long, to_lat, to_long) - # return round_to_zero(distance_m) - -#-------------------------------------------------------------------- -def format_km_to_mi(dist_km): - ''' - Reformat the distance based on it's value - - dist: Distance in kilometers - ''' - - if Gb.um_MI: - mi = dist_km * Gb.um_km_mi_factor - - if mi >= 100: - return f"{mi:.0f} mi" - 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:.2f} mi" +#-------------------------------------- +def format_dist_m(dist_m): + dist_km = dist_m / 1000 return format_dist_km(dist_km) -#-------------------------------------------------------------------- +#-------------------------------------- def format_dist_km(dist_km): - ''' - Reformat the distance based on it's value - dist: Distance in kilometers - ''' - if dist_km >= 100: - return f"{dist_km:.0f}km" - if dist_km >= 10: - return f"{dist_km:.1f}km" - if dist_km >= 1: - return f"{dist_km:.2f}km" - - return f"{dist_km*1000:.0f}m" + if dist_km >= 100: return f"{dist_km:.0f}km" + if dist_km >= 10: return f"{dist_km:.1f}km" + if dist_km >= 1: return f"{dist_km:.2f}km" + if round_to_zero(dist_km) == 0: return f"0km" + return f"{dist_km*1000:.1f}m" #-------------------------------------------------------------------- -def format_dist_m(dist_m): - ''' - Reformat the distance based on it's value - - dist: Distance in meters - ''' - if dist_m >= 1000000: #100km - return f"{dist_m/1000:.0f}km" - if dist_m >= 10000: #10km - return f"{dist_m/1000:.1f}km" - if dist_m >= 1000: #1km/1000m/.6mi - return f"{dist_m/1000:.2f}km" - - return f"{dist_m:.0f}m" +def format_dist_mi(dist_mi): + + if dist_mi >= 100: return f"{dist_mi:.0f}mi" + if dist_mi >= 10: return f"{dist_mi:.1f}mi" + if dist_mi >= 1: return f"{dist_mi:.1f}mi" + if dist_mi >= .0947: return f"{dist_mi:.2f}mi" + if round_to_zero(dist_mi) == 0: return f"0mi" + + dist_ft = dist_mi * 5280 + if dist_ft > 1: return f"{int(dist_ft)}ft" + return f"{dist_ft:.2f}ft" diff --git a/custom_components/icloud3/helpers/time_util.py b/custom_components/icloud3/helpers/time_util.py index f6ab36d..63bab7d 100644 --- a/custom_components/icloud3/helpers/time_util.py +++ b/custom_components/icloud3/helpers/time_util.py @@ -143,8 +143,8 @@ def secs_to_hhmm(secs): """ secs --> hh:mm """ try: - if instr(secs, ':'): - return secs + if secs == 0: return '00:00' + if instr(secs, ':'): return secs w_secs = float(secs) + 30 @@ -161,10 +161,12 @@ def secs_to_hhmm(secs): def secs_to_time_hhmm(secs): """ secs --> hh:mm or hh:mma or hh:mmp""" try: + if secs == 0: return '00:00' + if Gb.time_format_24_hour: - return secs_to_24hr_time(secs + 30)[:-3] + return secs_to_24hr_time(secs)[:-3] - hhmmss = secs_to_time(secs + 30) + hhmmss = secs_to_time(secs) return hhmmss[:-4] + hhmmss[-1:] except: @@ -411,8 +413,6 @@ def datetime_to_secs(datetime, utc_local=False) -> int: secs_utc = secs = time.mktime(time.strptime(datetime, "%Y-%m-%d %H:%M:%S")) if secs > 0 and utc_local is True: secs += Gb.time_zone_offset_seconds - # elif secs == 0: - # _trace(f"{datetime} {secs} {utc_local} {secs_to_time(secs_utc)}->{secs_to_time(secs)}") except: secs = 0 diff --git a/custom_components/icloud3/icloud3_main.py b/custom_components/icloud3/icloud3_main.py index 1c824e1..1c3e652 100644 --- a/custom_components/icloud3/icloud3_main.py +++ b/custom_components/icloud3/icloud3_main.py @@ -35,17 +35,17 @@ from .global_variables import GlobalVariables as Gb from .const import (VERSION, - HOME, NOT_HOME, NOT_SET, NOT_SET_FNAME, HIGH_INTEGER, RARROW, - STATIONARY, TOWARDS, AWAY_FROM, EVLOG_IC3_STAGE_HDR, + HOME, NOT_HOME, NOT_SET, HIGH_INTEGER, RARROW, LT, + TOWARDS, EVLOG_IC3_STAGE_HDR, ICLOUD, ICLOUD_FNAME, TRACKING_NORMAL, CMD_RESET_PYICLOUD_SESSION, NEAR_DEVICE_DISTANCE, DISTANCE_TO_OTHER_DEVICES, DISTANCE_TO_OTHER_DEVICES_DATETIME, OLD_LOCATION_CNT, AUTH_ERROR_CNT, - MOBAPP_UPDATE, ICLOUD_UPDATE, ARRIVAL_TIME, HOME_DISTANCE, + MOBAPP_UPDATE, ICLOUD_UPDATE, ARRIVAL_TIME, EVLOG_UPDATE_START, EVLOG_UPDATE_END, EVLOG_ALERT, EVLOG_NOTICE, FMF, FAMSHR, MOBAPP, MOBAPP_FNAME, - ENTER_ZONE, EXIT_ZONE, GPS, INTERVAL, NEXT_UPDATE, NEXT_UPDATE_TIME, - ZONE, CONF_LOG_LEVEL, STATZONE_RADIUS_1M, + ENTER_ZONE, EXIT_ZONE, INTERVAL, NEXT_UPDATE, + CONF_LOG_LEVEL, STATZONE_RADIUS_1M, ) from .const_sensor import (SENSOR_LIST_DISTANCE, ) from .support import start_ic3 @@ -56,29 +56,21 @@ from .support import pyicloud_ic3_interface from .support import icloud_data_handler from .support import service_handler +from .support import zone_handler from .support import determine_interval as det_interval -from .helpers import entity_io -from .helpers.common import (instr, is_zone, is_statzone, isnot_statzone, isnot_zone, zone_dname, - list_to_str,) +from .helpers.common import (instr, is_zone, is_statzone, isnot_statzone, list_to_str,) from .helpers.messaging import (broadcast_info_msg, post_event, post_error_msg, post_monitor_msg, post_internal_error, - open_ic3_log_file, post_alert, clear_alert, + post_alert, clear_alert, log_info_msg, log_exception, log_start_finish_update_banner, log_debug_msg, close_reopen_ic3_log_file, archive_log_file, _trace, _traceha, ) from .helpers.time_util import (time_now_secs, secs_to_time, secs_to, secs_since, time_now, - secs_to_time, secs_to_time_str, secs_to_age_str, - datetime_now, calculate_time_zone_offset, secs_to_24hr_time, - secs_to_time_age_str, secs_to_datetime, ) -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 -ZD_CNT = 5 + secs_to_time, secs_to_time_str, secs_to_age_str, secs_to_time_hhmm, + secs_to_datetime, calculate_time_zone_offset, + secs_to_time_age_str, ) +from .helpers.dist_util import (km_to_um, m_to_um_ft, ) + #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> class iCloud3: @@ -338,7 +330,8 @@ def _polling_loop_5_sec_device(self, ha_timer_secs): for devicename in Gb.dist_to_other_devices_update_sensor_list: Device = Gb.Devices_by_devicename[devicename] Device.sensors[DISTANCE_TO_OTHER_DEVICES] = Device.dist_to_other_devices.copy() - Device.sensors[DISTANCE_TO_OTHER_DEVICES_DATETIME] = Device.dist_to_other_devices_datetime + Device.sensors[DISTANCE_TO_OTHER_DEVICES_DATETIME] = \ + secs_to_datetime(Device.dist_to_other_devices_secs) Device.write_ha_sensors_state(SENSOR_LIST_DISTANCE) Gb.dist_to_other_devices_update_sensor_list = set() @@ -399,7 +392,7 @@ def _main_5sec_loop_update_tracked_devices_mobapp(self, Device): # The Device is in a StatZone but the mobapp is not home. Send a location request to try to # sync them. Do this every 10-mins if the time since the last request is older than 10-min ago - elif (Device.is_in_statzone + elif (Device.isin_statzone and Device.mobapp_data_state == NOT_HOME and (secs_since(Device.mobapp_request_loc_sent_secs) > 36000 or Device.mobapp_request_loc_sent_secs == 0) @@ -416,9 +409,6 @@ def _main_5sec_loop_update_tracked_devices_mobapp(self, Device): event_msg = f"Trigger > {Device.mobapp_data_change_reason}" post_event(devicename, event_msg) - # If using the passthru zone delay: - # If entering a zone, set it if it is not set - # If exiting, reset it if Gb.is_passthru_zone_used: if instr(Device.mobapp_data_change_reason, ENTER_ZONE): if Device.set_passthru_zone_delay(MOBAPP, @@ -498,7 +488,8 @@ def _main_5sec_loop_update_tracked_devices_icloud(self, Device): # See if the Stat Zone timer has expired or if the Device has moved a lot. Do this # even if the sensors are not updated to make sure the Stat Zone is set up and be # seleted for the Device - self._move_into_statzone_if_timer_reached(Device) + statzone.move_into_statzone_if_timer_reached(Device) + # self._move_into_statzone_if_timer_reached(Device) # If entering a zone, set the passthru expire time (if needed) if self._started_passthru_zone_delay(Device): @@ -553,8 +544,7 @@ def _main_5sec_loop_update_monitored_devices(self, Device): Device.icloud_initial_locate_done = True Device.icloud_update_reason = 'Monitored Device Update' - event_msg =(f"Trigger > Moved {format_dist_km(Device.loc_data_dist_moved_km)}") #{Gb.any_device_was_updated_reason}") - post_event(Device.devicename, event_msg) + event_msg =(f"Trigger > Moved {km_to_um(Device.loc_data_dist_moved_km)}") self.process_updated_location_data(Device, '') Device.update_sensor_values_from_data_fields() @@ -630,9 +620,11 @@ def _main_5sec_loop_special_time_control(self): for devicename, Device in Gb.Devices_by_devicename.items(): if Device.dist_apart_msg: - event_msg =(f"Nearby Devices > (<{NEAR_DEVICE_DISTANCE}m), " - f"{Device.dist_apart_msg}, " - f"Checked-{secs_to_time(Device.near_device_checked_secs)}") + device_time = secs_to_time_hhmm(Device.dist_to_other_devices_secs) + event_msg =(f"Nearby Devices " + f"({LT}{m_to_um_ft(NEAR_DEVICE_DISTANCE, as_integer=True)}) > " + f"@{device_time}, " + f"{Device.dist_apart_msg.replace(f' ({device_time})', '')}") if event_msg != Device.last_near_devices_msg: Device.last_near_devices_msg = event_msg post_event(devicename, event_msg) @@ -682,7 +674,7 @@ def _validate_new_mobapp_data(self, Device): # Check to see if the location is outside the zone without an exit trigger for from_zone, FromZone in Device.FromZones_by_zone.items(): if is_zone(from_zone): - info_msg = self._is_outside_zone_no_exit( Device, from_zone, '', + info_msg = zone_handler.is_outside_zone_no_exit( Device, from_zone, '', Device.mobapp_data_latitude, Device.mobapp_data_longitude) @@ -703,7 +695,7 @@ def _validate_new_mobapp_data(self, Device): and Device.is_next_update_time_reached and Device.FromZone_NextToUpdate.zone_dist < 1 and Device.FromZone_NextToUpdate.dir_of_travel == TOWARDS - and Device.isnot_inzone): + and Device.isnotin_zone): mobapp_interface.request_location(Device) @@ -776,7 +768,7 @@ def _validate_new_icloud_data(self, Device): else: Device.update_sensors_error_msg = \ - self._is_outside_zone_no_exit(Device, zone, '', latitude, longitude) + zone_handler.is_outside_zone_no_exit(Device, zone, '', latitude, longitude) # if Device.is_offline or Device.is_pending: if Device.is_offline: @@ -784,7 +776,7 @@ def _validate_new_icloud_data(self, Device): f"{Device.device_status_msg}") if instr(Device.update_sensors_error_msg, 'Offline') is False: log_info_msg(Device.devicename, offline_msg) - post_event(Device.devicename, offline_msg) + post_event(Device, offline_msg) # 'Verify Location' update reason overrides all other checks and forces an iCloud update # if Device.icloud_update_reason == 'Verify Location': @@ -802,7 +794,7 @@ def _validate_new_icloud_data(self, Device): # let normal next time update check process elif (Device.is_gps_poor and Gb.discard_poor_gps_inzone_flag - and Device.is_inzone + and Device.isin_zone and Device.outside_no_exit_trigger_flag is False): Device.old_loc_cnt -= 1 @@ -833,7 +825,7 @@ def _validate_new_icloud_data(self, Device): event_msg = (f"Location Old > Using Anyway, " f"{Device.last_update_loc_time}{RARROW}" f"{Device.loc_data_time}") - post_event(Device.devicename, event_msg) + post_event(Device, event_msg) Device.old_loc_cnt = 0 Device.old_loc_msg = '' Device.last_update_loc_secs = Device.loc_data_secs @@ -858,7 +850,8 @@ def _validate_new_icloud_data(self, Device): # See if the Stat Zone timer has expired or if the Device has moved a lot. Do this # again (after the initial update needed check) since the data has been updated # and the gps might be good now where it was bad earlier. - if self._move_into_statzone_if_timer_reached(Device): + # if self._move_into_statzone_if_timer_reached(Device): + if statzone.move_into_statzone_if_timer_reached(Device): Device.icloud_update_reason = "Stationary Zone Time Reached" Device.update_sensors_flag = True Device.selected_zone_result = [] @@ -905,7 +898,7 @@ def process_updated_location_data(self, Device, update_requested_by): Device.write_ha_sensors_state() Device.write_ha_device_from_zone_sensors_state() Device.write_ha_device_tracker_state() - self._log_zone_enter_exit_activity(Device) + zone_handler.log_zone_enter_exit_activity(Device) # Refresh the EvLog if this is an initial locate or the devicename is displayed if (devicename == Gb.EvLog.evlog_attrs["devicename"] @@ -934,7 +927,7 @@ def _started_passthru_zone_delay(self, Device): # Get in-zone name or away, will be used in process_updated_location_data routine # when results are calculted. We need to get it now to see if the passthru is # needed or still active - Device.selected_zone_results = self._select_zone(Device) + Device.selected_zone_results = zone_handler.select_zone(Device) ZoneSelected, zone_selected, zone_selected_dist_m, zones_distance_list = \ Device.selected_zone_results @@ -1027,7 +1020,7 @@ def _determine_interval_and_next_update(self, Device): else: Device.trigger = (f"{Device.dev_data_source}@{Device.loc_data_datetime[11:19]}") - self._update_current_zone(Device) + zone_handler.update_current_zone(Device) except Exception as err: post_internal_error('Update Stat Zone', traceback.format_exc) @@ -1062,7 +1055,7 @@ def _determine_interval_and_next_update(self, Device): f"OldLocRetryUpdate-{secs_to_age_str(error_interval_secs)}") Device.old_loc_cnt = 0 - log_start_finish_update_banner('finish', devicename, Device.dev_data_source, from_zone) + log_start_finish_update_banner('finish', devicename, Device.dev_data_source, '') except Exception as err: log_exception(err) @@ -1070,392 +1063,6 @@ def _determine_interval_and_next_update(self, Device): return True -#------------------------------------------------------------------------------ - def _request_update_devices_no_mobapp_same_zone_on_exit(self, Device): - ''' - The Device is exiting a zone. Check all other Devices that were in the same - zone that do not have the mobapp installed and set the next update time to - 5-seconds to see if that device also exited instead of waiting for the other - devices inZone interval time to be reached. - - Check the next update time to make sure it has not already been updated when - the device without the Mobile App is with several devices that left the zone. - ''' - devices_to_update = [_Device - for _Device in Gb.Devices_by_devicename_tracked.values() - if (Device is not _Device - and _Device.is_data_source_MOBAPP is False - and _Device.loc_data_zone == Device.loc_data_zone - and secs_to(_Device.FromZone_Home.next_update_secs) > 60)] - - if devices_to_update == []: - return - - for _Device in devices_to_update: - _Device.icloud_force_update_flag = True - _Device.trigger = 'Check Zone Exit' - _Device.check_zone_exit_secs = time_now_secs() - det_interval.update_all_device_fm_zone_sensors_interval(_Device, 15) - event_msg = f"Trigger > Check Zone Exit, GeneratedBy-{Device.fname}" - post_event(_Device.devicename, event_msg) - -#------------------------------------------------------------------------------ - def _log_zone_enter_exit_activity(self, Device): - ''' - An entry can be written to the 'zone-log-[year]-[device-[zone].csv' file. - This file shows when a device entered & exited a zone, the time the device was in - the zone, the distance to Home, etc. It can be imported into a spreadsheet and used - at year end for expense calculations. - ''' - # Uncomment the following for testing - # if Gb.this_update_time.endswith('0:00') or Gb.this_update_time.endswith('5:00'): - # Device.mobapp_zone_exit_secs = time_now_secs() - # Device.mobapp_zone_exit_time = time_now() - # Device.last_zone = HOME - # pass - # elif 'none' in Device.log_zones: - - if ('none' in Device.log_zones - or Device.log_zone == Device.loc_data_zone - or (Device.log_zone == '' and Device.loc_data_zone not in Device.log_zones)): - return - - if Device.log_zone == '': - Device.log_zone = Device.loc_data_zone - Device.log_zone_enter_secs = Gb.this_update_secs - event_msg = f"Log Zone Activity > Logging Started-{zone_dname(Device.log_zone)}" - post_event(Device.devicename, event_msg) - return - - # Must be in the zone for at least 4-minutes - inzone_secs = secs_since(Device.log_zone_enter_secs) - inzone_hrs = inzone_secs/3600 - if inzone_secs < 240: return - - filename = (f"zone-log-{dt_util.now().strftime('%Y')}-" - f"{Device.log_zones_filename}.csv") - - with open(filename, 'a', encoding='utf8') as f: - if os.path.getsize(filename) == 0: - recd = "Date,Zone Enter Time,Zone Exit Time,Time (Mins),Time (Hrs),Distance (Home),Zone,Device\n" - f.write(recd) - - recd = (f"{datetime_now()[:10]}," - f"{secs_to_datetime(Device.log_zone_enter_secs)}," - f"{secs_to_datetime(Gb.this_update_secs)}," - f"{inzone_secs/60:.0f}," - f"{inzone_hrs:.2f}," - f"{Device.sensors[HOME_DISTANCE]:.2f}," - f"{Device.log_zone}," - f"{Device.devicename}" - "\n") - f.write(recd) - event_msg = f"Log Zone Activity > Logging Ended-{zone_dname(Device.log_zone)}" - post_event(Device.devicename, event_msg) - - if Device.loc_data_zone in Device.log_zones: - Device.log_zone = Device.loc_data_zone - Device.log_zone_enter_secs = Gb.this_update_secs - else: - Device.log_zone = '' - Device.log_zone_enter_secs = 0 - -#------------------------------------------------------------------------------ -# -# DETERMINE THE ZONE THE DEVICE IS CURRENTLY IN -# -#------------------------------------------------------------------------------ - def _update_current_zone(self, Device, display_zone_msg=True): - - ''' - Get current zone of the device based on the location - - Parameters: - selected_zone_results - The zone may have already been selected. If so, this list - is the results from a previous _select_zone - display_zone_msg - True if the msg should be posted to the Event Log - - Returns: - Zone Zone object - zone zone name or not_home if not in a zone - - NOTE: This is the same code as (active_zone/async_active_zone) in zone.py - but inserted here to use zone table loaded at startup rather than - calling hass on all polls - ''' - - # Zone selected may have been done when determing if the device just entered a zone - # during the passthru check. If so, use it and then reset it - if Device.selected_zone_results == []: - ZoneSelected, zone_selected, zone_selected_dist_m, zones_distance_list = \ - self._select_zone(Device) - else: - ZoneSelected, zone_selected, zone_selected_dist_m, zones_distance_list = \ - Device.selected_zone_results - Device.selected_zone_results = [] - - if zone_selected == 'unknown': - return ZoneSelected, zone_selected - - if ZoneSelected is None: - ZoneSelected = Gb.Zones_by_zone[NOT_HOME] - zone_selected = NOT_HOME - zone_selected_dist_m = 0 - - # In a zone but if not in a track from zone and was in a Stationary Zone, - # reset the stationary zone - elif Device.is_in_statzone and isnot_statzone(zone_selected): - statzone.exit_statzone(Device) - - - # Get distance between zone selected and current zone to see if they overlap. - # If so, keep the current zone - if (zone_selected != NOT_HOME - and self._is_overlapping_zone(Device.loc_data_zone, zone_selected)): - zone_selected = Device.loc_data_zone - ZoneSelected = Gb.Zones_by_zone[Device.loc_data_zone] - - # The zone changed - elif Device.loc_data_zone != zone_selected: - # See if any device without the mobapp was in this zone. If so, request a - # location update since it was running on the inzone timer instead of - # exit triggers from the Mobile App - if (Gb.mobapp_monitor_any_devices_false_flag - and zone_selected == NOT_HOME - and Device.loc_data_zone != NOT_HOME): - self._request_update_devices_no_mobapp_same_zone_on_exit(Device) - - Device.loc_data_zone = zone_selected - Device.zone_change_secs = time_now_secs() - Device.zone_change_datetime = datetime_now() - - # The zone changed, update the enter/exit zone times if the - # Device does not use the Mobile App - if zone_selected == NOT_HOME: - if (Device.mobapp_monitor_flag is False - or Device.mobapp_zone_exit_secs == 0): - Device.mobapp_zone_exit_secs = time_now_secs() - Device.mobapp_zone_exit_time = time_now() - - else: - if (Device.mobapp_monitor_flag is False - or Device.mobapp_zone_enter_secs == 0): - Device.mobapp_zone_enter_secs = time_now_secs() - Device.mobapp_zone_enter_time = time_now() - - if display_zone_msg: - self._post_zone_selected_msg(Device, ZoneSelected, zone_selected, - zone_selected_dist_m, zones_distance_list) - - return ZoneSelected, zone_selected - -#-------------------------------------------------------------------- - def _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 - gps_accuracy_adj = int(Device.loc_data_gps_accuracy / 2) - - # [distance from zone, Zone, zone_name, redius, display_as] - zone_data_selected = [HIGH_INTEGER, None, '', HIGH_INTEGER, '', 1] - - # 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 - zones_msg = f"Zone > Unknown, GPS-{Device.loc_data_fgps}" - post_event(Device.devicename, zones_msg) - return ZoneSelected, zone_selected, 0, [] - - # Verify that the statzone was not left without an exit trigger. If so, move this device out of it. - if (Device.is_in_statzone - and Device.StatZone.distance_m(latitude, longitude) > Device.StatZone.radius_m): - statzone.exit_statzone(Device) - - zones_data = [[Zone.distance_m(latitude, longitude), Zone, Zone.zone, - Zone.radius_m, Zone.dname] - for Zone in Gb.HAZones - if (Zone.passive is False)] - - # Do not select a new zone for the Device if it just left a zone. Set to Away and next_update will be soon - # if Device.was_inzone is False or secs_since(Device.mobapp_zone_exit_secs) >= Gb.exit_zone_interval_secs/2: - # 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] + gps_accuracy_adj] - - for zone_data in inzone_zones: - if zone_data[ZD_RADIUS] <= zone_data_selected[ZD_RADIUS]: - zone_data_selected = zone_data - - ZoneSelected = zone_data_selected[ZD_ZONE] - zone_selected = zone_data_selected[ZD_NAME] - zone_selected_dist_m = zone_data_selected[ZD_DIST_M] - - # Selected a statzone - if zone_selected in Gb.StatZones_by_zone: - Device.StatZone = Gb.StatZones_by_zone[zone_selected] - - # In a zone and the mobapp enter zone info was not set, set it now - if (zone_selected != Device.mobapp_zone_enter_zone - and is_zone(zone_selected) and isnot_zone(Device.mobapp_zone_enter_zone)): - Device.mobapp_zone_enter_secs = Gb.this_update_secs - Device.mobapp_zone_enter_time = Gb.this_update_time - Device.mobapp_zone_enter_zone = zone_selected - - # Build an item for each zone (dist-from-zone|zone_name|display_name-##km) - zones_distance_list = \ - [(f"{int(zone_data[ZD_DIST_M]):08}|{zone_data[ZD_NAME]}|{zone_data[ZD_DIST_M]}") - for zone_data in zones_data if zone_data[ZD_NAME] != zone_selected] - - return ZoneSelected, zone_selected, zone_selected_dist_m, zones_distance_list - -#-------------------------------------------------------------------- - @staticmethod - def _post_zone_selected_msg(Device, ZoneSelected, zone_selected, - zone_selected_dist_m, zones_distance_list): - - device_zones = [_Device.loc_data_zone for _Device in Gb.Devices] - zones_cnt_by_zone = {_zone:device_zones.count(_zone) for _zone in set(device_zones)} - - # Format the Zone Selected Msg (ZoneName (#)) - zone_selected_msg = zone_dname(zone_selected) - if zone_selected in zones_cnt_by_zone: - zone_selected_msg += f"({zones_cnt_by_zone[zone_selected]})" - if ZoneSelected.radius_m > 0: - zone_selected_msg += f"-{format_dist_m(zone_selected_dist_m)}" - - # Format distance msg - zones_dist_msg = '' - zones_displayed = [zone_selected] - zones_distance_list.sort() - for zone_distance_list in zones_distance_list: - zdl_items = zone_distance_list.split('|') - _zone = zdl_items[1] - _zone_dist = float(zdl_items[2]) - - zones_dist_msg += ( f"{zone_dname(_zone)}" - f"-{format_dist_m(_zone_dist)}") - # zones_dist_msg += f"-r{int(Gb.Zones_by_zone[_zone].radius_m)}m" - zones_dist_msg += ", " - - gps_accuracy_msg = '' - if zone_selected_dist_m > ZoneSelected.radius_m: - gps_accuracy_msg = (f"AccuracyAdjustment-" - f"{int(Device.loc_data_gps_accuracy / 2)}m, ") - - # Format distance and count msg - zones_cnt_msg = '' - for _zone, cnt in zones_cnt_by_zone.items(): - if zone_dname(_zone) in zones_dist_msg: - zones_dist_msg = zones_dist_msg.replace( - zone_dname(_zone), f"{zone_dname(_zone)}({cnt})") - elif _zone != zone_selected: - zones_dist_msg += f"{zone_dname(_zone)}({cnt}), " - zones_cnt_msg += f"{zone_dname(_zone)}({cnt}), " - - zones_dist_msg = zones_dist_msg.replace('──', 'NotSet') - zones_cnt_msg = zones_cnt_msg.replace('──', 'NotSet') - - if is_zone(zone_selected) and isnot_statzone(zone_selected): - post_monitor_msg(Device.devicename, f"Zone Distance > {zones_dist_msg}") - zones_dist_msg = '' - else: - zones_cnt_msg = '' - - zones_msg =(f"Zone > " - f"{zone_selected_msg} > " - f"{zones_dist_msg}" - f"{zones_cnt_msg}" - f"{gps_accuracy_msg}" - f"GPS-{Device.loc_data_fgps}") - - if zone_selected == Device.log_zone: - zones_msg += ' (Logged)' - - post_event(Device.devicename, zones_msg) - - if (zones_cnt_msg - and Device.loc_data_zone != Device.sensors[ZONE] - and NOT_SET not in zones_cnt_by_zone): - for _Device in Gb.Devices: - if Device is not _Device: - event_msg = f"Zone-Device Counts > {zones_cnt_msg}" - post_event(_Device.devicename, event_msg) - -#-------------------------------------------------------------------- - def _move_into_statzone_if_timer_reached(self, Device): - ''' - Check the Device's Stationary Zone expired timer and distance moved: - Update the Device's Stat Zone distance moved - 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 - ''' - if Gb.is_statzone_used is False: - return False - - calc_dist_last_poll_moved_km = calc_distance_km(Device.sensors[GPS], Device.loc_data_gps) - Device.update_distance_moved(calc_dist_last_poll_moved_km) - - # See if moved less than the stationary zone movement limit - # If updating via the Mobile App and the current state is stationary, - # make sure it is kept in the stationary zone - if Device.is_statzone_timer_reached is False or Device.is_location_old_or_gps_poor: - return False - - if Device.is_statzone_move_limit_exceeded: - Device.statzone_reset_timer - - # Monitored devices can move into a tracked zone but can not create on for itself - elif Device.is_monitored: #beta 4/13b16 - pass - - elif (Device.isnot_in_statzone - or (is_statzone(Device.mobapp_data_state) and Device.loc_data_zone == NOT_SET)): - statzone.move_device_into_statzone(Device) - - return True - -#-------------------------------------------------------------------- - def _is_overlapping_zone(self, current_zone, new_zone): - ''' - Check to see if two zones overlap each other. The current_zone and - new_zone overlap if their distance between centers is less than 2m. - - Return: - True They overlap - False They do not oerlap, ic3 is starting - ''' - try: - if current_zone == NOT_SET: - return False - elif current_zone == new_zone: - return True - - if current_zone == "": current_zone = HOME - CurrentZone = Gb.Zones_by_zone[current_zone] - NewZone = Gb.Zones_by_zone[new_zone] - - zone_dist = CurrentZone.distance_m(NewZone.latitude, NewZone.longitude) - - return (zone_dist <= 2) - - except: - return False - #-------------------------------------------------------------------- def _get_icloud_data_prefetch_device(self): ''' @@ -1547,26 +1154,6 @@ def _format_fname_devtype(self, Device): except: return '' -#-------------------------------------------------------------------- - def _is_overlapping_zone(self, zone1, zone2): - ''' - zone1 and zone2 overlap if their distance between centers is less than 2m - ''' - try: - if zone1 == zone2: - return True - - if zone1 == "": zone1 = HOME - Zone1 = Gb.Zones_by_zone[zone1] - Zone2 = Gb.Zones_by_zone[zone2] - - zone_dist = Zone1.distance(Zone2.latitude, Zone2.longitude) - - return (zone_dist <= 2) - - except: - return False - #-------------------------------------------------------------------- def _wait_if_update_in_process(self, Device=None): # An update is in process, must wait until done @@ -1620,10 +1207,9 @@ def _timer_tasks_midnight(self): start_ic3.set_log_level('info') start_ic3.update_conf_file_log_level('info') - for devicename, Device in Gb.Devices_by_devicename.items(): - Gb.pyicloud_authentication_cnt = 0 - Gb.pyicloud_location_update_cnt = 0 - Gb.pyicloud_calls_time = 0.0 + Gb.pyicloud_authentication_cnt = 0 + Gb.pyicloud_location_update_cnt = 0 + Gb.pyicloud_calls_time = 0.0 if Gb.WazeHist: Gb.WazeHist.wazehist_delete_invalid_records() @@ -1685,59 +1271,6 @@ def _check_old_loc_poor_gps(self, Device): Device.old_loc_cnt = 0 Device.old_loc_msg = '' -#-------------------------------------------------------------------- - def _is_outside_zone_no_exit(self, Device, zone, trigger, latitude, longitude): - ''' - If the device is outside of the zone and less than the zone radius + gps_acuracy_threshold - and no Geographic Zone Exit trigger was received, it has probably wandered due to - GPS errors. If so, discard the poll and try again later - - Updates: Set the Device.outside_no_exit_trigger_flag - Increase the old_location_poor_gps count when this innitially occurs - Return: Reason message - ''' - if Device.mobapp_monitor_flag is False: - return '' - - trigger = Device.trigger if trigger == '' else trigger - if (instr(trigger, ENTER_ZONE) - or Device.sensor_zone == NOT_SET - or zone not in Gb.HAZones_by_zone - or Device.icloud_initial_locate_done is False): - Device.outside_no_exit_trigger_flag = False - return '' - - Zone = Gb.Zones_by_zone[zone] - dist_fm_zone_m = Zone.distance_m(latitude, longitude) - zone_radius_m = Zone.radius_m - zone_radius_accuracy_m = zone_radius_m + Gb.gps_accuracy_threshold - - info_msg = '' - if (dist_fm_zone_m > zone_radius_m - and Device.got_exit_trigger_flag is False - and Zone.is_statzone is False): - if (dist_fm_zone_m < zone_radius_accuracy_m - and Device.outside_no_exit_trigger_flag == False): - Device.outside_no_exit_trigger_flag = True - Device.old_loc_cnt += 1 - - info_msg = ("Outside of Zone without MobApp `Exit Zone` Trigger, " - f"Keeping in Zone-{Zone.dname} > ") - else: - Device.got_exit_trigger_flag = True - info_msg = ("Outside of Zone without MobApp `Exit Zone` Trigger " - f"but outside threshold, Exiting Zone-{Zone.dname} > ") - - info_msg += (f"Distance-{format_dist_m(dist_fm_zone_m)}, " - f"KeepInZoneThreshold-{format_dist_m(zone_radius_m)} " - f"to {format_dist_m(zone_radius_accuracy_m)}, " - f"Located-{Device.loc_data_time_age}") - - if Device.got_exit_trigger_flag: - Device.outside_no_exit_trigger_flag = False - - return info_msg - #-------------------------------------------------------------------- def _display_secs_to_next_update_info_msg(self, Device): ''' diff --git a/custom_components/icloud3/sensor.py b/custom_components/icloud3/sensor.py index 5094acb..1d4d41c 100644 --- a/custom_components/icloud3/sensor.py +++ b/custom_components/icloud3/sensor.py @@ -16,32 +16,31 @@ #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> from .global_variables import GlobalVariables as Gb -from .const import (DOMAIN, VERSION, ICLOUD3,RARROW, +from .const import (DOMAIN, VERSION, ICLOUD3, RARROW, SENSOR_EVENT_LOG_NAME, SENSOR_WAZEHIST_TRACK_NAME, HOME, HOME_FNAME, NOT_SET, NOT_SET_FNAME, NONE_FNAME, DATETIME_ZERO, HHMMSS_ZERO, BLANK_SENSOR_FIELD, DOT, HDOT, HDOT2, UM_FNAME, NBSP, TRACK_DEVICE, MONITOR_DEVICE, INACTIVE_DEVICE, - DISTANCE_TO_OTHER_DEVICES, NAME, FNAME, BADGE, FROM_ZONE, - ZONE, ZONE_INFO, LAST_ZONE, - ZONE_DISTANCE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, - BATTERY, BATTERY_STATUS, BATTERY_SOURCE, - DISTANCE_TO_OTHER_DEVICES_DATETIME, + ZONE, ZONE_DISTANCE_M, ZONE_DISTANCE_M_EDGE, + BATTERY, BATTERY_STATUS, CONF_TRACK_FROM_ZONES, CONF_IC3_DEVICENAME, CONF_MODEL, CONF_RAW_MODEL, CONF_FNAME, CONF_FAMSHR_DEVICENAME, CONF_MOBILE_APP_DEVICE, CONF_TRACKING_MODE, ) -from .const_sensor import (SENSOR_DEFINITION, SENSOR_GROUPS, +from .const_sensor import (SENSOR_DEFINITION, SENSOR_GROUPS, SENSOR_LIST_DISTANCE, SENSOR_FNAME, SENSOR_TYPE, SENSOR_ICON, - SENSOR_ATTRS, SENSOR_DEFAULT, SENSOR_LIST_DISTANCE, ) + SENSOR_ATTRS, SENSOR_DEFAULT, ) from .helpers.common import (instr, round_to_zero, is_statzone, set_precision, ) 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, adjust_time_hour_value, adjust_time_hour_values) +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, + secs_to_datetime, adjust_time_hour_values, + adjust_time_hour_value) from .helpers.dist_util import (km_to_mi, m_to_ft, ) from .helpers.format import (icon_circle, icon_box, ) from collections import OrderedDict @@ -50,10 +49,8 @@ from .support import recorder_prefilter from homeassistant.components.sensor import SensorEntity -from homeassistant.helpers.entityfilter import convert_include_exclude_filter 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 @@ -219,8 +216,6 @@ def _create_track_from_zone_sensors(devicename, conf_device, sensors_list): return [] ha_zones, zone_entity_data = entity_io.get_entity_registry_data(platform=ZONE) - # zone_entity_ids = entity_io.ha_zone_entity_ids() - # ha_zones = [zone_entity_id.replace('zone.', '') for zone_entity_id in zone_entity_ids] devicename_from_zone_sensors = Gb.Sensors_by_devicename_from_zone.get(devicename, {}) excluded_sensors_list = _excluded_sensors_list() @@ -505,7 +500,7 @@ def _get_extra_attributes(self, sensor): _sensor_attr_name = _sensor.replace('_date/time', '') _sensor_value = self._get_sensor_value(_sensor) try: - _sensor_value = self._set_precision(_sensor_value) + _sensor_value = set_precision(_sensor_value) except: pass @@ -513,10 +508,10 @@ def _get_extra_attributes(self, sensor): extra_attrs[_sensor_attr_name] = _sensor_value if Gb.um_MI: + zone_dist_m = self._get_sensor_value(ZONE_DISTANCE_M) + sensor_value_mi = zone_dist_m*Gb.um_km_mi_factor/1000 + extra_attrs['distance_(miles)'] = set_precision(sensor_value_mi) extra_attrs['distance_units_(attributes)'] = 'mi' - if self._get_sensor_value(ZONE_DISTANCE_M): - sensor_value_mi = self._get_sensor_value(ZONE_DISTANCE_M)*Gb.um_km_mi_factor/1000 - extra_attrs['miles_distance'] = self._set_precision(sensor_value_mi) if self.Device and self.Device.away_time_zone_offset != 0: _sensor_value = adjust_time_hour_values(_sensor_value, self.Device.away_time_zone_offset) @@ -569,23 +564,6 @@ 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 in ['km', 'mi'] else 2 if um in ['m', 'ft'] 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): ''''] @@ -1084,21 +1062,20 @@ def _format_zone_distance_extra_attrs(self): zone_dist_mi = {zone_da: set_precision(km_to_mi(dist_km), 'mi') for zone_da, dist_km in zone_dist_km.items()} - # zone_dist_m = {f" - {Zone.dname}.": set_precision(self.Device.Distance_m(Zone), 'm') - zone_dist_m = {f" - {Zone.dname}²": set_precision(self.Device.Distance_m(Zone), 'm') + zone_dist_m = {f" - {Zone.dname}*": set_precision(self.Device.Distance_m(Zone), 'm') for Zone in Gb.HAZones if self.Device.Distance_m(Zone) < 500} - zone_dist_ft = {zone_da: self._set_precision(m_to_ft(dist_m), 'ft') + zone_dist_ft = {zone_da: set_precision(m_to_ft(dist_m), 'ft') for zone_da, dist_m in zone_dist_m.items()} - dist_attrs[f"zone_distance"] = f"({Gb.um}) @{datetime_now()}" + dist_attrs[f"zone_distance"] = f"({Gb.um}) @{datetime_now()}" if Gb.um_KM: dist_attrs.update(zone_dist_km) else: dist_attrs.update(zone_dist_mi) if zone_dist_m or zone_dist_ft: - dist_attrs[f"zone_distance (< 500m)"] = f"({Gb.um_m_ft}) @{datetime_now()}" + dist_attrs[f"zone_distance* (< 500m)"] = f"({Gb.um_m_ft}) @{datetime_now()}" if Gb.um_KM: dist_attrs.update(zone_dist_m) else: @@ -1114,29 +1091,28 @@ def _format_devices_distance_extra_attrs(self): ''' dist_attrs = OrderedDict() device_dist_m = {f" - {self._fname(devicename)}": set_precision(dist_to_other_devices[0], 'm') - # for devicename, dist_to_other_devices in self.Device.sensors[DISTANCE_TO_OTHER_DEVICES].items() for devicename, dist_to_other_devices in self.Device.dist_to_other_devices.items() if dist_to_other_devices[0] > .001 and dist_to_other_devices[0] < 500} - device_dist_ft = {device_fn: self._set_precision(m_to_ft(dist_m), 'ft') + device_dist_ft = {device_fn: set_precision(m_to_ft(dist_m), 'ft') for device_fn, dist_m in device_dist_m.items()} - # device_dist_km = {f" - {self._fname(devicename)}.": set_precision(dist_to_other_devices[0]/1000, 'km') - device_dist_km = {f" - {self._fname(devicename)}²": set_precision(dist_to_other_devices[0]/1000, 'km') - # for devicename, dist_to_other_devices in self.Device.sensors[DISTANCE_TO_OTHER_DEVICES].items() + device_dist_km = {f" - {self._fname(devicename)}*": set_precision(dist_to_other_devices[0]/1000, 'km') for devicename, dist_to_other_devices in self.Device.dist_to_other_devices.items() if dist_to_other_devices[0] >= 500} device_dist_mi = {device_fn: set_precision(km_to_mi(dist_km), 'mi') for device_fn, dist_km in device_dist_km.items()} if device_dist_m or device_dist_ft: - dist_attrs["device_distance (< 500m)"] = f"({Gb.um_m_ft}) @{self.Device.dist_to_other_devices_datetime}" + dist_attrs["device_distance (< 500m)"] = (f"({Gb.um_m_ft}) " + f"@{secs_to_datetime(self.Device.dist_to_other_devices_secs)}") if Gb.um_KM: dist_attrs.update(device_dist_m) else: dist_attrs.update(device_dist_ft) if device_dist_km or device_dist_mi: - dist_attrs["device_distance"] = f"({Gb.um}) @{self.Device.dist_to_other_devices_datetime}" + dist_attrs["device_distance"] = (f"({Gb.um}) " + f"@{secs_to_datetime(self.Device.dist_to_other_devices_secs)}") if Gb.um_KM: dist_attrs.update(device_dist_km) else: @@ -1218,7 +1194,6 @@ def __init__(self, fname, entity_name): self.entity_id = f"sensor.{self.entity_name}" self._unsub_dispatcher = None self._device = DOMAIN - # self.ic3_device_id = Gb.ic3_device_id = Gb.ha_device_id_by_devicename.get(DOMAIN) self.current_state_value = '' self.history_exclude_flag = False \ if self.entity_name == SENSOR_WAZEHIST_TRACK_NAME \ @@ -1316,11 +1291,6 @@ def native_value(self): @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)}") - # log_update_time = (f"{dt_util.now().strftime('%a, %m/%d')}, " - # f"{dt_util.now().strftime(Gb.um_time_strfmt)}." - # f"{dt_util.now().strftime('%f')}") Gb.EvLog.evlog_attrs['update_time'] =\ (f"{dt_util.now().strftime('%a, %m/%d')}, " f"{dt_util.now().strftime(Gb.um_time_strfmt)}." diff --git a/custom_components/icloud3/support/config_file.py b/custom_components/icloud3/support/config_file.py index 3d89131..f1d6821 100644 --- a/custom_components/icloud3/support/config_file.py +++ b/custom_components/icloud3/support/config_file.py @@ -405,8 +405,8 @@ def config_file_check_devices(): if conf_device[CONF_PICTURE] == '': conf_device[CONF_PICTURE] = 'None' update_configuration_flag = True - if conf_device[CONF_INZONE_INTERVAL] < 1: - conf_device[CONF_INZONE_INTERVAL] = 120 + if conf_device[CONF_INZONE_INTERVAL] < 5: + conf_device[CONF_INZONE_INTERVAL] = 5 update_configuration_flag = True if conf_device[CONF_LOG_ZONES]== []: conf_device[CONF_LOG_ZONES] = ['none'] @@ -414,6 +414,9 @@ def config_file_check_devices(): if conf_device[CONF_TRACK_FROM_ZONES] == []: conf_device[CONF_TRACK_FROM_ZONES] = [HOME] update_configuration_flag = True + if conf_device[CONF_FIXED_INTERVAL] > 0 and conf_device[CONF_FIXED_INTERVAL] < 5: + conf_device[CONF_FIXED_INTERVAL] = 5 + update_configuration_flag = True if update_configuration_flag: write_storage_icloud3_configuration_file() diff --git a/custom_components/icloud3/support/determine_interval.py b/custom_components/icloud3/support/determine_interval.py index aadcdc4..f6abe23 100644 --- a/custom_components/icloud3/support/determine_interval.py +++ b/custom_components/icloud3/support/determine_interval.py @@ -19,7 +19,7 @@ from ..global_variables import GlobalVariables as Gb -from ..const import (HOME, NOT_HOME, AWAY, NOT_SET, HIGH_INTEGER, +from ..const import (HOME, NOT_HOME, AWAY, NOT_SET, NOT_HOME_ZONES, HIGH_INTEGER, CRLF, CHECK_MARK, CIRCLE_X, LTE, LT, PLUS_MINUS, RED_X, CIRCLE_STAR2, CRLF_DOT, STATIONARY, STATIONARY_FNAME, WATCH, MOBAPP_FNAME, AWAY_FROM, TOWARDS, PAUSED, INZONE, NEAR, NEAR_HOME, @@ -28,8 +28,6 @@ WAZE, NEAR_DEVICE_DISTANCE, WAZE_USED, WAZE_NOT_USED, WAZE_PAUSED, WAZE_OUT_OF_RANGE, WAZE_NO_DATA, - # OLD_LOCATION_CNT, AUTH_ERROR_CNT, RETRY_INTERVAL_RANGE_1, MOBAPP_REQUEST_LOC_CNT, - # RETRY_INTERVAL_RANGE_2, EVLOG_TIME_RECD, EVLOG_ALERT, RARROW, NEAR_DEVICE_USEABLE_SYM, EXIT_ZONE, @@ -43,8 +41,8 @@ LAST_LOCATED, ) -from ..support import mobapp_interface -from ..support import stationary_zone as statzone +# from ..support import mobapp_interface +# from ..support import stationary_zone as statzone from ..helpers.common import (instr, round_to_zero, is_zone, is_statzone, isnot_zone, zone_dname, ) from ..helpers.messaging import (post_event, post_error_msg, @@ -53,7 +51,8 @@ from ..helpers.time_util import (secs_to_time, secs_to_time_str, secs_to_time_age_str, waze_mins_to_time_str, secs_since, time_to_12hrtime, secs_to_datetime, secs_to, secs_to_age_str, datetime_now, time_now, time_now_secs, secs_to_time_hhmm, secs_to_hhmm, ) -from ..helpers.dist_util import (km_to_mi, km_to_mi_str, format_dist_km, format_dist_m, format_km_to_mi, ) +from ..helpers.dist_util import (km_to_mi, km_to_um, format_dist_km, format_dist_m, + km_to_um, m_to_um_ft, ) import homeassistant.util.dt as dt_util @@ -100,15 +99,15 @@ def determine_interval(Device, FromZone): devicename = Device.devicename - battery10_flag = (0 > Device.dev_data_battery_level >= 10) - battery5_flag = (0 > Device.dev_data_battery_level >= 5) + battery10_flag = (0 > Device.dev_data_battery_level >= 10) + battery5_flag = (0 > Device.dev_data_battery_level >= 5) - inzone_flag = (Device.loc_data_zone != NOT_HOME) - not_inzone_flag = (Device.loc_data_zone == NOT_HOME) - was_inzone_flag = (Device.sensors[ZONE] not in [NOT_HOME, AWAY, NOT_SET]) + isin_zone = (Device.loc_data_zone not in NOT_HOME_ZONES) + isnotin_zone = (Device.loc_data_zone in NOT_HOME_ZONES) + wasin_zone = (Device.sensors[ZONE] not in NOT_HOME_ZONES) - inzone_home_flag = (Device.loc_data_zone == HOME) - was_inzone_home_flag = (Device.sensor_zone == HOME) + isin_zone_home = (Device.loc_data_zone == HOME) + wasin_zone_home = (Device.sensor_zone == HOME) Device.FromZone_BeingUpdated = FromZone @@ -168,7 +167,7 @@ def determine_interval(Device, FromZone): # Reset got zone exit trigger since now in a zone for next # exit distance check. Also reset Stat Zone timer and dist moved. - if inzone_flag: + if isin_zone: Device.got_exit_trigger_flag = False Device.statzone_clear_timer @@ -181,20 +180,9 @@ def determine_interval(Device, FromZone): #-------------------------------------------------------------------------------- #if more than 3km(1.8mi) then assume driving - last_went_3km = Device.went_3km - if FromZone is Device.FromZone_LastIn: - if dist_from_zone_km > 3: - oldway_went_3km = True - elif dist_from_zone_km < .03: # back in the zone, reset flag - oldway_went_3km = False - - #-------------------------------------------------------------------------------- - #if more than 3km(1.8mi) then assume driving - if FromZone is Device.FromZone_Home: #Device.FromZone_LastIn: - if dist_from_zone_km > 3: + if FromZone is Device.FromZone_Home: + if dist_from_zone_km >= 3: Device.went_3km = True - elif dist_from_zone_km < .03: # back in the zone, reset flag - Device.went_3km = False #-------------------------------------------------------------------------------- interval_secs = 15 @@ -203,13 +191,13 @@ def determine_interval(Device, FromZone): interval_multiplier = 1 if Device.state_change_flag: - if inzone_flag: + if isin_zone: #inzone & old location if Device.is_location_old_or_gps_poor and battery10_flag is False: interval_method = '1.OldLocPoorGPS' interval_secs = _get_interval_for_error_retry_cnt(Device, OLD_LOCATION_CNT) - elif Device.isnot_in_statzone: + elif Device.isnotin_statzone: interval_method = "1.EnterZone" interval_secs = Device.inzone_interval_secs @@ -224,12 +212,13 @@ def determine_interval(Device, FromZone): interval_secs = Device.statzone_inzone_interval_secs #exited zone, set to short interval if other devices are in same zone - elif not_inzone_flag and was_inzone_flag: + elif isnotin_zone and wasin_zone: interval_method = "2.ExitZone" interval_secs = Gb.exit_zone_interval_secs if Device.loc_data_zone == HOME: FromZone.max_dist_km = 0 + Device.went_3km = False elif Device.fixed_interval_secs > 0: interval_method = "1.Fixed" @@ -241,15 +230,15 @@ def determine_interval(Device, FromZone): # Exit_Zone trigger & away & exited less than 1 min ago elif (instr(Device.trigger, EXIT_ZONE) - and not_inzone_flag + and isnotin_zone and secs_since(Device.mobapp_zone_exit_secs) < 60): interval_method = '3.ExitTrigger' interval_secs = Gb.exit_zone_interval_secs #inzone & poor gps & check gps accuracy when inzone elif (Device.is_gps_poor - and inzone_flag - and Gb.discard_poor_gps_inzone_flag is False): + and isin_zone + and Gb.discard_poor_gps_isin_zone is False): interval_method = '3.PoorGPSinZone' interval_secs = _get_interval_for_error_retry_cnt(Device, OLD_LOCATION_CNT) @@ -261,7 +250,7 @@ def determine_interval(Device, FromZone): interval_method = '3.OldLocPoorGPS' interval_secs = _get_interval_for_error_retry_cnt(Device, OLD_LOCATION_CNT) - # elif Device.is_in_statzone: + # elif Device.isin_statzone: # interval_method = "3.StatZone" # interval_secs = Device.statzone_inzone_interval_secs @@ -269,12 +258,12 @@ def determine_interval(Device, FromZone): interval_method = "3.Battery10%" interval_secs = Device.statzone_inzone_interval_secs - elif inzone_home_flag or (dist_from_zone_km < .05 and dir_of_travel == TOWARDS): + elif isin_zone_home or (dist_from_zone_km < .05 and dir_of_travel == TOWARDS): interval_method = '3.InHomeZone' interval_secs = Device.inzone_interval_secs #in another zone and inzone time > travel time - elif inzone_flag and Device.inzone_interval_secs > waze_interval_secs: + elif isin_zone and Device.inzone_interval_secs > waze_interval_secs: interval_method = '3.InZone' interval_secs = Device.inzone_interval_secs @@ -350,7 +339,7 @@ def determine_interval(Device, FromZone): #Turn off waze close to zone flag to use waze after leaving zone or getting more than 1km from it if Gb.Waze.waze_close_to_zone_pause_flag: - if inzone_flag or calc_dist_from_zone_km >= 1: + if isin_zone or calc_dist_from_zone_km >= 1: Gb.Waze.waze_close_to_zone_pause_flag = False #if triggered by Mobile App (Zone Enter/Exit, Manual, Fetch, etc.) @@ -380,16 +369,16 @@ def determine_interval(Device, FromZone): # Use interval_secs if > StatZone interval unless the StatZone interval is the device's # inzone interval - if Device.is_in_statzone: + if Device.isin_statzone: interval_method = "7.StatZone" interval_secs = Device.statzone_inzone_interval_secs #check for max interval_secs, override in zone times elif interval_secs > Gb.max_interval_secs: - if Device.is_in_statzone: + if Device.isin_statzone: interval_method = f"7.inZoneMax" interval_secs = Device.statzone_inzone_interval_secs - elif inzone_flag: + elif isin_zone: pass else: @@ -437,7 +426,7 @@ def determine_interval(Device, FromZone): monitor_msg = (f"DirHist-{FromZone.format_dir_of_travel_history}") post_monitor_msg(devicename, monitor_msg) - if inzone_home_flag: FromZone.dir_of_travel_history = '' + if isin_zone_home: FromZone.dir_of_travel_history = '' except Exception as err: sensor_msg = post_internal_error('Update FromZone Times', traceback.format_exc) @@ -491,7 +480,7 @@ def determine_interval(Device, FromZone): sensors[TRAVEL_TIME_MIN] = f"{waze_time_from_zone:.0f} min" sensors[TRAVEL_TIME_HHMM] = secs_to_hhmm(waze_time_from_zone * 60) - if (Device.is_inzone + if (Device.isin_zone and Device.loc_data_zone == FromZone.from_zone and is_statzone(Device.loc_data_zone) is False): sensors[ARRIVAL_TIME] =f"@{secs_to_time_hhmm(Device.zone_change_secs)}" @@ -509,16 +498,16 @@ def determine_interval(Device, FromZone): sensors[CALC_DISTANCE] = km_to_mi(calc_dist_from_zone_km) sensors[MOVED_DISTANCE] = km_to_mi(dist_moved_km) - if Device.is_inzone: + if Device.isin_zone: sensors[ZONE_INFO] = f"@{Device.loc_data_zone_fname}" else: - sensors[ZONE_INFO] = km_to_mi_str(dist_from_zone_km) + sensors[ZONE_INFO] = km_to_um(dist_from_zone_km) #save for event log if type(waze_time_msg) != str: waze_time_msg = '' FromZone.last_travel_time = waze_time_msg FromZone.last_distance_km = dist_from_zone_km - FromZone.last_distance_str = (f"{format_km_to_mi(dist_from_zone_km)}") + FromZone.last_distance_str = (f"{km_to_um(dist_from_zone_km)}") Device.loc_time_updates_famshr = [Device.loc_data_time] if Device.is_location_gps_good: @@ -543,15 +532,15 @@ def post_results_message_to_event_log(Device, FromZone): else: event_msg = (f"Results: From-{FromZone.from_zone_dname} > ") - if (Device.is_inzone and FromZone.from_zone != Device.loc_data_zone - or Device.isnot_inzone): + if (Device.isin_zone and FromZone.from_zone != Device.loc_data_zone + or Device.isnotin_zone): event_msg += f"Arrive-{FromZone.sensors[ARRIVAL_TIME]}, " event_msg += ( f"NextUpdate-{FromZone.next_update_time}, ") # f"Interval-{FromZone.interval_str}, ") # if FromZone.zone_dist > 0: # event_msg += ( f"TravTime-{FromZone.last_travel_time}, " - # f"Distance-{format_km_to_mi(FromZone.zone_dist)}, ") + # f"Distance-{km_to_um(FromZone.zone_dist)}, ") if FromZone.dir_of_travel == STATIONARY_FNAME: event_msg += STATIONARY_FNAME + ", " elif FromZone.dir_of_travel not in [INZONE, '_', '___', ' ', '']: @@ -559,16 +548,16 @@ def post_results_message_to_event_log(Device, FromZone): if Device.is_statzone_timer_set and Device.is_tracked and Gb.is_statzone_used: event_msg += f"IntoStatZone-{secs_to_time(Device.statzone_timer)}, " # if Device.statzone_dist_moved_km > 0: - # event_msg += f"Moved-{format_dist_km(Device.statzone_dist_moved_km)}, " + # event_msg += f"Moved-{km_to_um(Device.statzone_dist_moved_km)}, " if Device.loc_data_dist_moved_km > 0: - event_msg += f"Moved-{format_dist_km(Device.loc_data_dist_moved_km)}, " + event_msg += f"Moved-{km_to_um(Device.loc_data_dist_moved_km)}, " if Device.dev_data_battery_level > 0 and FromZone is Device.FromZone_Home: event_msg += f"Battery-{Device.dev_data_battery_level}%, " #if Device.is_monitored: # event_msg += f"Source-{Device.dev_data_source}, " + event_msg += f"{'✓' if Device.went_3km else '×'}Went3km, " if Gb.log_debug_flag and FromZone.interval_method and Device.is_tracked: - event_msg += ( f"Method-{FromZone.interval_method}, " - f"{'Went3km, ' if Device.went_3km else ''}") + event_msg += f"Method-{FromZone.interval_method}, " if Gb.Waze.waze_status == WAZE_OUT_OF_RANGE: event_msg += "Waze-OverMaxDist, " if (Device.mobapp_monitor_flag @@ -576,7 +565,7 @@ def post_results_message_to_event_log(Device, FromZone): event_msg += ( f"MobAppLocated-" f"{secs_to_age_str(Device.mobapp_data_secs)}, ") - post_event(Device.devicename, event_msg[:-2]) + post_event(Device, event_msg[:-2]) log_msg = ( f"RESULTS: From-{FromZone.from_zone_dname} > " @@ -584,9 +573,9 @@ def post_results_message_to_event_log(Device, FromZone): f"iC3Zone-{Device.loc_data_zone}, " f"Interval-{FromZone.interval_str}, " f"TravTime-{FromZone.last_travel_time}, " - f"Dist-{format_km_to_mi(FromZone.zone_dist)}, " + f"Dist-{km_to_um(FromZone.zone_dist)}, " f"NextUpdt-{FromZone.next_update_time}, " - f"MaxDist-{format_km_to_mi(FromZone.max_dist_km)}, " + f"MaxDist-{km_to_um(FromZone.max_dist_km)}, " f"Dir-{FromZone.dir_of_travel}, " f"Moved-{format_dist_km(Device.statzone_dist_moved_km)}, " f"Battery-{Device.dev_data_battery_level}%, " @@ -621,7 +610,7 @@ def post_zone_time_dist_event_msg(Device, FromZone): distance = FromZone.zone_distance_str event_msg =(f"{EVLOG_TIME_RECD}{mobapp_state},{ic3_zone},{interval_str},{travel_time},{distance}") - post_event(Device.devicename, event_msg) + post_event(Device, event_msg) #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # @@ -659,7 +648,7 @@ def determine_interval_monitored_device_offline(Device): event_msg =(f"{RED_X}Offline > " f"Since-{secs_to_time_age_str(Device.loc_data_secs)}, " f"CheckNext-{Device.sensors[NEXT_UPDATE_TIME]}") - post_event(Device.devicename, event_msg) + post_event(Device, event_msg) return True @@ -702,7 +691,7 @@ def determine_interval_after_error(Device, counter=OLD_LOCATION_CNT): event_msg = (f"{EVLOG_ALERT}Location Old > Tracking Reinitialized, " f"LastLocated-{secs_to_time_age_str(Device.loc_data_secs)}, " f"RetryAt-{Device.FromZone_Home.next_update_time}") - post_event(Device.devicename, event_msg) + post_event(Device, event_msg) return if (Device.is_offline and Device.offline_secs == 0): @@ -941,7 +930,7 @@ def _get_distance_data(Device, FromZone): if Device.no_location_data: event_msg = "No location data available, will retry" - post_event(Device.devicename, event_msg) + post_event(Device, event_msg) return (ERROR, {}) @@ -1028,7 +1017,7 @@ def _get_distance_data(Device, FromZone): if waze_source_msg: event_msg = f"Waze Route Info > {waze_source_msg}" - post_event(Device.devicename, event_msg) + post_event(Device, event_msg) #-------------------------------------------------------------------------------- dir_of_travel = '___' @@ -1039,10 +1028,10 @@ def _get_distance_data(Device, FromZone): if FromZone.dir_of_travel in [INZONE, STATIONARY_FNAME]: FromZone.dir_of_travel = STATIONARY_FNAME - if Device.is_in_statzone: + if Device.isin_statzone: dir_of_travel = STATIONARY_FNAME - elif Device.is_inzone: + elif Device.isin_zone: dir_of_travel = INZONE elif Device.sensors[ZONE] == NOT_SET or FromZone.dir_of_travel == NOT_SET: @@ -1085,9 +1074,9 @@ def _get_distance_data(Device, FromZone): event_msg =(f"StatZone Timer Reset > " f"NewTime-{secs_to_time(Device.statzone_timer)}, " - f"Moved-{format_dist_km(Device.loc_data_dist_moved_km)}") + f"Moved-{km_to_um(Device.loc_data_dist_moved_km)}") # f"(>{format_dist_km(Gb.statzone_dist_move_limit_km)})") - post_event(Device.devicename, event_msg) + post_event(Device, event_msg) dist_from_zone_km = round_to_zero(dist_from_zone_km) dist_moved_km = round_to_zero(dist_moved_km) @@ -1159,6 +1148,7 @@ def _get_interval_for_error_retry_cnt(Device, counter=OLD_LOCATION_CNT, pause_co #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> def used_near_device_results(Device, FromZone): if (Device.NearDevice is None + or Device.isin_nonstatzone or Device.NearDevice.NearDevice is Device or FromZone.from_zone not in Device.NearDevice.FromZones_by_zone): Device.near_device_used = '' @@ -1168,10 +1158,11 @@ def used_near_device_results(Device, FromZone): neardevice_fname_chk = f"{NEAR_DEVICE_USEABLE_SYM}{neardevice_fname}" Device.dist_apart_msg = Device.dist_apart_msg.replace(NEAR_DEVICE_USEABLE_SYM, '') Device.dist_apart_msg = Device.dist_apart_msg.replace(neardevice_fname, neardevice_fname_chk) - Device.near_device_used = f"{Device.NearDevice.fname_devtype} ({format_dist_m(Device.near_device_distance)})" + Device.near_device_used = ( f"{Device.NearDevice.fname_devtype} " + f"({m_to_um_ft(Device.near_device_distance)})") event_msg =(f"Using Nearby Device Results > {Device.NearDevice.fname}, " - f"Distance-{format_dist_m(Device.near_device_distance)}") - post_event(Device.devicename, event_msg) + f"Distance-{m_to_um_ft(Device.near_device_distance)}") + post_event(Device, event_msg) copy_near_device_results(Device, FromZone) @@ -1212,9 +1203,9 @@ def copy_near_device_results(Device, FromZone): FromZone.sensors.update(NearFromZone.sensors) - if Device.is_in_statzone: + if Device.isin_statzone: interval_secs = Device.statzone_inzone_interval_secs - elif Device.is_inzone: + elif Device.isin_zone: interval_secs = Device.inzone_interval_secs else: interval_secs = NearFromZone.interval_secs @@ -1250,6 +1241,7 @@ def update_near_device_info(Device): Device.NearDevice = None Device.near_device_distance = 0 Device.near_device_checked_secs = time_now_secs() + device_time = secs_to_time_hhmm(Device.dist_to_other_devices_secs) for devicename, dist_apart_data in Device.dist_to_other_devices.items(): _Device = Gb.Devices_by_devicename[devicename] @@ -1265,7 +1257,7 @@ def update_near_device_info(Device): and dist_apart_m < NEAR_DEVICE_DISTANCE): useable_symbol = '×' elif dist_apart_m > NEAR_DEVICE_DISTANCE: - useable_symbol = '>' + useable_symbol = '×' elif min_gps_accuracy > NEAR_DEVICE_DISTANCE: useable_symbol = '±' elif instr(display_text, '+') or instr(display_text, '±'): # old or gps accuracy issue @@ -1279,12 +1271,7 @@ def update_near_device_info(Device): else: useable_symbol = NEAR_DEVICE_USEABLE_SYM - age_secs = secs_since(loc_data_secs) - time_msg = f"/{secs_to_time_str(age_secs).replace(' ', '+ ')} ago" if age_secs > 120 else '' - gps_msg = f"±{min_gps_accuracy}" if min_gps_accuracy > Gb.gps_accuracy_threshold else '' - - Device.dist_apart_msg += ( f"{useable_symbol}{_Device.fname_devtype}-" - f"{format_dist_m(dist_apart_m)}{gps_msg}{time_msg}, ") + Device.dist_apart_msg += f"{useable_symbol}{_Device.fname_devtype}-{display_text}, " # The nearby devices can not point to each other and other criteria if (Device.is_tracked @@ -1299,7 +1286,10 @@ def update_near_device_info(Device): Device.NearDevice = _Device Device.near_device_distance = dist_apart_m - monitor_msg = f"Nearby Devices > ({LT}{NEAR_DEVICE_DISTANCE}m), {Device.dist_apart_msg}" + monitor_msg = ( f"Nearby Devices " + f"({LT}{m_to_um_ft(NEAR_DEVICE_DISTANCE, as_integer=True)}) > " + f"@{device_time}, " + f"{Device.dist_apart_msg.replace(f' ({device_time})', '')}") post_monitor_msg(Device.devicename, monitor_msg) return diff --git a/custom_components/icloud3/support/event_log.py b/custom_components/icloud3/support/event_log.py index 78c52f5..68f4481 100644 --- a/custom_components/icloud3/support/event_log.py +++ b/custom_components/icloud3/support/event_log.py @@ -203,7 +203,7 @@ def _format_evlog_device_fname(self, Device): return f"{Device.evlog_fname_alert_char}{Device.fname}{tracked}" #------------------------------------------------------ - def post_event(self, devicename, event_text='+'): + def post_event(self, devicename_or_Device, event_text='+'): ''' Add records to the Event Log table the device. If the device="*", the event_text is added to all deviceFNAMEs table. @@ -216,9 +216,9 @@ def post_event(self, devicename, event_text='+'): ''' if event_text == '+': - event_text = devicename + event_text = devicename_or_Device devicename = "*" if Gb.start_icloud3_inprocess_flag else '**' - elif devicename is None or devicename in ['', '*']: + elif devicename_or_Device is None or devicename_or_Device in ['', '*']: devicename = "*" if Gb.start_icloud3_inprocess_flag else '**' if (instr(event_text, "▼") or instr(event_text, "▲") @@ -226,9 +226,17 @@ def post_event(self, devicename, event_text='+'): or instr(event_text, "event_log")): return + if devicename_or_Device in Gb.Devices: + Device = devicename_or_Device + devicename = Device.devicename + elif devicename_or_Device in Gb.Devices_by_devicename: + devicename = devicename_or_Device + Device = Gb.Devices_by_devicename[devicename] + else: + Device = None + # If monitored device and the event msg is a status msg for other devices, # do not display it on a monitoed device screen - Device = Gb.Devices_by_devicename.get(devicename) if Device and Device.is_monitored: start_pos = 2 if event_text.startswith('^') else 0 for filter_text in MONITORED_DEVICE_EVENT_FILTERS: diff --git a/custom_components/icloud3/support/icloud_data_handler.py b/custom_components/icloud3/support/icloud_data_handler.py index 46e32db..4c8e4c4 100644 --- a/custom_components/icloud3/support/icloud_data_handler.py +++ b/custom_components/icloud3/support/icloud_data_handler.py @@ -33,8 +33,8 @@ def no_icloud_update_needed_tracking(Device): Device.icloud_no_update_reason = 'Paused' elif (Device.is_next_update_time_reached is False - and Device.is_inzone - and Device.isnot_in_statzone): + and Device.isin_zone + and Device.isnotin_statzone): Device.icloud_no_update_reason = 'inZone & Next Update Time not Reached' elif Gb.primary_data_source_ICLOUD is False: @@ -72,7 +72,7 @@ def is_icloud_update_needed_timers(Device): if Device.is_statzone_timer_reached: Device.icloud_update_reason = ( f"Next Update & Stat Zone Time Reached@" f"{secs_to_time(Device.statzone_timer)}") - elif Device.isnot_inzone and Device.FromZone_NextToUpdate.from_zone != HOME: + elif Device.isnotin_zone and Device.FromZone_NextToUpdate.from_zone != HOME: Device.icloud_update_reason += f" ({Device.FromZone_NextToUpdate.from_zone_dname})" elif Device.is_next_update_overdue: @@ -259,7 +259,7 @@ def update_PyiCloud_RawData_data(Device, results_msg_flag=True): event_msg+=(f"{CRLF_DOT}Error-{err}" f"{CRLF_DOT}iCloud may be down or there is a network connection or other issue." f"{CRLF}iCloud3 will try again later.") - post_event(Device.devicename, event_msg) + post_event(Device, event_msg) post_error_msg(event_msg) except Exception as err: @@ -377,6 +377,18 @@ def update_device_with_latest_raw_data(Device, all_devices=False): if Device.is_location_old_or_gps_poor: continue + # This fct may be run a second time to recheck loc times for different data + # sources. Don't redisplay it it's nothing changed. + if Device.loc_msg_famshr_mobapp_time == \ + (f"{_Device.dev_data_source}-" + f"{_Device.loc_data_time_gps}-" + f"{_Device.mobapp_data_time_gps}"): + continue + Device.loc_msg_famshr_mobapp_time = \ + (f"{_Device.dev_data_source}-" + f"{_Device.loc_data_time_gps}-" + f"{_Device.mobapp_data_time_gps}") + other_times = "" if famshr_secs > 0 and Gb.used_data_source_FAMSHR and _Device.dev_data_source != 'FamShr': other_times += f"FamShr-{famshr_time}" @@ -478,7 +490,7 @@ def is_PyiCloud_RawData_data_useable(Device, results_msg_flag=True): event_msg += "Requesting New Location" if results_msg_flag: - post_event(Device.devicename, event_msg) + post_event(Device, event_msg) else: post_monitor_msg(Device.devicename, event_msg) return is_useable_flag @@ -537,7 +549,7 @@ def _get_devdata_useable_status(Device, data_source): else: event_msg+=(f"Age-{secs_to_time_str(loc_age_secs)} " f"(> {secs_to_time_str(Device.FromZone_BeingUpdated.interval_secs)})") - post_event(Device.devicename, event_msg) + post_event(Device, event_msg) gps_accuracy_ok = RawData.is_gps_good gps_accuracy = round(RawData.gps_accuracy) diff --git a/custom_components/icloud3/support/mobapp_data_handler.py b/custom_components/icloud3/support/mobapp_data_handler.py index 7cb976a..731d5e2 100644 --- a/custom_components/icloud3/support/mobapp_data_handler.py +++ b/custom_components/icloud3/support/mobapp_data_handler.py @@ -2,7 +2,7 @@ from ..global_variables import GlobalVariables as Gb from ..const import (DEVICE_TRACKER, NOTIFY, - CRLF_DOT, EVLOG_ALERT, RARROW, + CRLF_DOT, EVLOG_ALERT, RARROW, LT, NOT_SET, NOT_HOME, RARROW, NUMERIC, HIGH_INTEGER, HHMMSS_ZERO, ENTER_ZONE, EXIT_ZONE, MOBAPP_TRIGGERS_EXIT, @@ -16,10 +16,11 @@ log_debug_msg, log_exception, log_error_msg, log_rawdata, _trace, _traceha, ) from ..helpers.time_util import (secs_to_time, secs_since, format_time_age, format_age, format_age_hrs, ) -from ..helpers.dist_util import (format_dist_km, format_dist_m, ) +from ..helpers.dist_util import (format_dist_km, format_dist_m, m_to_um, ) from ..helpers import entity_io from ..support import mobapp_interface from ..support import stationary_zone as statzone +from ..support import zone_handler @@ -125,8 +126,73 @@ def check_mobapp_state_trigger_change(Device): Device.mobapp_data_trigger_secs = mobapp_data_trigger_secs Device.mobapp_data_trigger_time = mobapp_data_trigger_time - # If enter/exit zone, save zone and enter/exit time - if (Device.mobapp_data_trigger == EXIT_ZONE and Device.is_inzone): + + + # --------------------------------------------- + # If Exit Zone and was not in a zone + + # When exiting the zone, the current enter time should be after the current exit time. + # If it is still before the current exit time, that means there was never an enter trigger + # and we do not know what zone we are really in. Get the closest zone to our current + # location and use that zone if the distance is less than 300m. + # if (Device.mobapp_data_trigger == EXIT_ZONE + # and Device.isnotin_zone): + # and Device.mobapp_zone_enter_secs < Device.mobapp_zone_exit_secs): + + # Device.got_exit_trigger_flag = True + # Device.mobapp_zone_exit_secs = mobapp_data_secs + # Device.mobapp_zone_exit_time = mobapp_data_state_time + + # # Set Enter Zone fields so this Exit Zone test will not be processed again + # Device.mobapp_zone_enter_secs = mobapp_data_secs + # Device.mobapp_zone_enter_time = mobapp_data_state_time + # Device.mobapp_zone_enter_zone = Zone.zone + # Device.mobapp_zone_enter_dist_m = zone_dist_m + + # # Get closest zone + # Zone, zone, zone_dname, zone_dist_m = \ + # zone_handler.closest_zone( Device.mobapp_data_latitude, + # Device.mobapp_data_longitude) + + # event_msg =(f"Exit Zone trigger without Enter Zone trigger > " + # f"Zone-{zone_dname}, " + # f"Dist-{m_to_um(zone_dist_m)}, ") + # if Zone and zone_dist_m < 300: + # if Zone.passive: + # Device.mobapp_data_trigger += " (Passive)" + # Device.mobapp_data_reject_reason = "Exited Passive Zone" + # event_msg += f"Rejected, Passive Zone" + # Device.mobapp_zone_exit_zone = zone + # Device.mobapp_zone_exit_dist_m = zone_dist_m + # else: + # Device.mobapp_data_trigger += f" (Unknown)" + # Device.mobapp_zone_exit_zone = "unknown" + # Device.mobapp_zone_exit_dist_m = 0 + # Device.mobapp_data_reject_reason = "Exited Unknown Zone" + # event_msg += "Rejected, Unknown or too far away" + # post_event(Device, event_msg) + + # --------------------------------------------- + # Entering a zone and this state time > last zone enter time + # We are entering a new zone from not_home or going from one zone to another + if (Device.mobapp_data_trigger == ENTER_ZONE + and Device.isin_zone_mobapp_state + and mobapp_data_secs >= Device.mobapp_zone_enter_secs): + Device.mobapp_zone_enter_secs = mobapp_data_secs + Device.mobapp_zone_enter_time = mobapp_data_state_time + Device.mobapp_zone_enter_zone = mobapp_data_state + if mobapp_data_state in Gb.HAZones_by_zone: + Device.mobapp_zone_enter_dist_m = \ + Gb.Zones_by_zone[mobapp_data_state].distance_m( + Device.mobapp_data_latitude, Device.mobapp_data_longitude) + else: + Device.mobapp_zone_enter_dist_m = 0 + + # --------------------------------------------- + # Exiting a zone when we are in a zone + elif (Device.mobapp_data_trigger == EXIT_ZONE + and Device.isin_zone): + Device.got_exit_trigger_flag = True Device.mobapp_zone_exit_secs = mobapp_data_secs Device.mobapp_zone_exit_time = mobapp_data_state_time @@ -134,7 +200,7 @@ def check_mobapp_state_trigger_change(Device): Device.mobapp_zone_exit_zone = Device.passthru_zone elif (Device.mobapp_zone_enter_secs >= Device.loc_data_secs - or Device.is_in_statzone): + or Device.isin_statzone): Device.mobapp_zone_exit_zone = Device.loc_data_zone elif is_zone(Device.sensors[ZONE]): @@ -150,26 +216,15 @@ def check_mobapp_state_trigger_change(Device): Gb.Zones_by_zone[Device.mobapp_zone_exit_zone].distance_m( Device.mobapp_data_latitude, Device.mobapp_data_longitude) else: - Device.mobapp_zone_exit_zone = '' - Device.mobapp_zone_exit_dist_m = -1 - - if (Device.mobapp_data_trigger == ENTER_ZONE - and Device.is_inzone_mobapp_state - and mobapp_data_secs >= Device.mobapp_zone_enter_secs): - Device.mobapp_zone_enter_secs = mobapp_data_secs - Device.mobapp_zone_enter_time = mobapp_data_state_time - Device.mobapp_zone_enter_zone = mobapp_data_state - if mobapp_data_state in Gb.HAZones_by_zone: - Device.mobapp_zone_enter_dist_m = \ - Gb.Zones_by_zone[mobapp_data_state].distance_m( - Device.mobapp_data_latitude, Device.mobapp_data_longitude) - else: - Device.mobapp_zone_enter_dist_m = -1 + Device.mobapp_zone_exit_zone = 'unknown' + Device.mobapp_zone_exit_dist_m = 0 + # --------------------------------------------- mobapp_msg =(f"MobApp Monitor > " f"State-{Device.mobapp_data_state}@{Device.mobapp_data_state_time} (^state_age), " f"Trigger-{mobapp_data_trigger}@{mobapp_data_trigger_time} (^trig_age), ") + # --------------------------------------------- if mobapp_data_state_not_set_flag: Device.mobapp_data_change_reason = \ Device.mobapp_data_trigger = f"Initial MobApp Locate@{mobapp_data_time}" @@ -183,6 +238,14 @@ def check_mobapp_state_trigger_change(Device): elif mobapp_data_change_flag is False: Device.mobapp_data_reject_reason = "Data has not changed" + # Exit a zone and not in a zone, nothing to do + elif (Device.mobapp_data_trigger == EXIT_ZONE + and Device.isnotin_zone): + if Device.mobapp_data_secs > Device.located_secs_plus_5: + Device.mobapp_data_trigger = "Verify Location (Exit Unknown Zone)" + else: + Device.mobapp_data_reject_reason = "Exit when not in zone" + # Exit trigger and the trigger changed from last poll overrules trigger change time elif Device.mobapp_data_trigger == EXIT_ZONE: if Device.mobapp_data_secs > Device.located_secs_plus_5: @@ -195,7 +258,7 @@ def check_mobapp_state_trigger_change(Device): # Enter trigger and the trigger changed from last poll overrules trigger change time elif (Device.mobapp_data_trigger == ENTER_ZONE): Device.mobapp_data_change_reason = f"{ENTER_ZONE}@{Device.mobapp_data_time} " - if Device.is_inzone_mobapp_state: + if Device.isin_zone_mobapp_state: Device.mobapp_data_change_reason +=(f"({zone_dname(Device.mobapp_zone_enter_zone)}/" f"{format_dist_m(Device.mobapp_zone_enter_dist_m)})") @@ -207,10 +270,11 @@ def check_mobapp_state_trigger_change(Device): Device.mobapp_data_reject_reason = (f"Poor GPS Accuracy-{Device.mobapp_data_gps_accuracy}m " f"#{Device.old_loc_cnt}") + # --------------------------------------------- # Discard StatZone entered if StatZone was created in the last 15-secs if (Device.mobapp_data_trigger == ENTER_ZONE and is_statzone(Device.mobapp_data_state) - and Device.is_in_statzone + and Device.isin_statzone and secs_since(Device.loc_data_secs <= 15)): Device.mobapp_data_reject_reason = "Enter into StatZone just created" @@ -219,9 +283,11 @@ def check_mobapp_state_trigger_change(Device): and Device.mobapp_data_state == Device.loc_data_zone): Device.mobapp_data_reject_reason = "Enter Zone and already in zone" + # --------------------------------------------- if Device.is_passthru_zone_delay_active: Device.mobapp_data_reject_reason = f"Passing thru zone, {Device.mobapp_data_trigger} discarded" + # --------------------------------------------- # If Enter or Exit, reasons already set, continue if (Device.mobapp_data_change_reason or Device.mobapp_data_reject_reason): @@ -235,7 +301,6 @@ def check_mobapp_state_trigger_change(Device): elif Device.mobapp_data_secs > Device.located_secs_plus_5: Device.mobapp_data_change_reason = (f"{Device.mobapp_data_trigger}@" f"{Device.mobapp_data_time}") - # f"{Device.mobapp_data_trigger_time}") # No update needed if no location changes elif (Device.mobapp_data_state == Device.loc_data_zone #Device.last_update_loc_zone @@ -265,11 +330,13 @@ def check_mobapp_state_trigger_change(Device): else: Device.mobapp_data_reject_reason = "Failed Update Tests" + # --------------------------------------------- # If data time is very old, change it to it's age if secs_since(Device.mobapp_data_secs) >= 10800: Device.mobapp_data_time = device_trkr_attrs[TIMESTAMP_TIME] = \ format_age_hrs(Device.mobapp_data_secs) + # --------------------------------------------- # Display MobApp Monitor info message if the state or trigger changed if (Gb.this_update_time.endswith('00:00') or mobapp_msg != Device.last_mobapp_msg): @@ -327,7 +394,7 @@ def _display_mobapp_msg(Device, mobapp_msg): def reset_statzone_on_enter_exit_trigger(Device): try: if Device.mobapp_data_trigger in MOBAPP_TRIGGERS_EXIT: - if Device.is_in_statzone: + if Device.isin_statzone: statzone.exit_statzone(Device) except Exception as err: @@ -505,7 +572,8 @@ def sync_mobapp_data_state_statzone(Device): if (is_statzone(mobapp_data_state) and is_statzone(Device.loc_data_zone) - and Device.mobapp_data_state == NOT_HOME): + and Device.isnotin_zone_mobapp_state): + #and Device.mobapp_data_state == NOT_HOME): Device.mobapp_data_state = mobapp_data_state Device.mobapp_data_state_secs = Gb.this_update_secs Device.mobapp_data_state_time = Gb.this_update_time diff --git a/custom_components/icloud3/support/mobapp_interface.py b/custom_components/icloud3/support/mobapp_interface.py index d64e9a2..083dce3 100644 --- a/custom_components/icloud3/support/mobapp_interface.py +++ b/custom_components/icloud3/support/mobapp_interface.py @@ -208,7 +208,7 @@ def send_message_to_device(Device, service_data): if service_data.get('message') != "request_location_update": evlog_msg = (f"{EVLOG_NOTICE}Sending Message to Device > " f"Message-{service_data.get('message')}") - post_event(Device.devicename, evlog_msg) + post_event(Device, evlog_msg) Gb.hass.services.call("notify", Device.mobapp[NOTIFY], service_data) diff --git a/custom_components/icloud3/support/pyicloud_ic3.py b/custom_components/icloud3/support/pyicloud_ic3.py index 468ee01..0f5b7c0 100644 --- a/custom_components/icloud3/support/pyicloud_ic3.py +++ b/custom_components/icloud3/support/pyicloud_ic3.py @@ -1143,6 +1143,9 @@ def create_FamilySharing_object(self): try: if self.FamilySharing is not None: return + if Gb.PyiCloud and Gb.PyiCloud.FamilySharing is not None: + self.PyiCloud = Gb.PyiCloud + return self.FamilySharing = PyiCloud_FamilySharing(self, self._get_webservice_url("findme"), diff --git a/custom_components/icloud3/support/pyicloud_ic3_interface.py b/custom_components/icloud3/support/pyicloud_ic3_interface.py index 2478607..34e5ccf 100644 --- a/custom_components/icloud3/support/pyicloud_ic3_interface.py +++ b/custom_components/icloud3/support/pyicloud_ic3_interface.py @@ -272,14 +272,14 @@ def check_all_devices_online_status(): Device.offline_secs = Gb.this_update_secs event_msg = ( f"Device Offline and not available > " f"OfflineSince-{secs_to_time_age_str(Device.offline_secs)}") - post_event(Device.devicename, event_msg) + post_event(Device, event_msg) elif Device.is_pending: if Device.pending_secs == 0: Device.pending_secs = Gb.this_update_secs event_msg = ( f"Device status is Pending/Unknown > " f"PendingSince-{secs_to_time_age_str(Device.pending_secs)}") - post_event(Device.devicename, event_msg) + post_event(Device, event_msg) if any_device_online_flag == False: event_msg = ( f"All Devices are offline or have a pending status. " diff --git a/custom_components/icloud3/support/restore_state.py b/custom_components/icloud3/support/restore_state.py index db7eba4..54c50b4 100644 --- a/custom_components/icloud3/support/restore_state.py +++ b/custom_components/icloud3/support/restore_state.py @@ -4,8 +4,8 @@ from ..const import (RESTORE_STATE_FILE, DISTANCE_TO_OTHER_DEVICES, DISTANCE_TO_OTHER_DEVICES_DATETIME, HHMMSS_ZERO, AWAY, AWAY_FROM, NOT_SET, NOT_HOME, STATIONARY, STATIONARY_FNAME, - ZONE, ZONE_DISPLAY_AS, ZONE_FNAME, ZONE_NAME, ZONE_INFO, - LAST_ZONE, LAST_ZONE_DISPLAY_AS, LAST_ZONE_FNAME, LAST_ZONE_NAME, + ZONE, ZONE_DNAME, ZONE_FNAME, ZONE_NAME, ZONE_INFO, + LAST_ZONE, LAST_ZONE_DNAME, LAST_ZONE_FNAME, LAST_ZONE_NAME, DIR_OF_TRAVEL, ) from ..helpers.common import (instr, ) @@ -150,11 +150,11 @@ def _reset_statzone_values_to_away(sensors): statzone_fname = Gb.statzone_fname.replace('#', '') _reset_sensor_value(sensors, ZONE, "ic3_stationary_", NOT_HOME) - _reset_sensor_value(sensors, ZONE_DISPLAY_AS, statzone_fname, AWAY) + _reset_sensor_value(sensors, ZONE_DNAME, statzone_fname, AWAY) _reset_sensor_value(sensors, ZONE_FNAME, statzone_fname, AWAY) _reset_sensor_value(sensors, ZONE_NAME, "Ic3Stationary", AWAY) _reset_sensor_value(sensors, LAST_ZONE, "ic3_stationary_", NOT_SET) - _reset_sensor_value(sensors, LAST_ZONE_DISPLAY_AS, statzone_fname, NOT_SET) + _reset_sensor_value(sensors, LAST_ZONE_DNAME, statzone_fname, NOT_SET) _reset_sensor_value(sensors, LAST_ZONE_FNAME, statzone_fname, NOT_SET) _reset_sensor_value(sensors, LAST_ZONE_NAME, "Ic3Stationary", NOT_SET) _reset_sensor_value(sensors, DIR_OF_TRAVEL, f"@{statzone_fname}", AWAY) diff --git a/custom_components/icloud3/support/service_handler.py b/custom_components/icloud3/support/service_handler.py index bcf547d..b268804 100644 --- a/custom_components/icloud3/support/service_handler.py +++ b/custom_components/icloud3/support/service_handler.py @@ -473,7 +473,7 @@ def _handle_action_device_location_mobapp(Device): Request Mobile App location from the EvLog > Actions ''' if Device.is_data_source_MOBAPP is False: - return _handle_action_device_locate(Device, 'mobapp') + return _handle_action_device_locate(Device, 'mobapp') Device.display_info_msg('Updating Location') @@ -491,18 +491,18 @@ def _handle_action_device_locate(Device, action_option): _handle_action_device_location_mobapp(Device) return else: - post_event(Device.devicename, + post_event(Device, "Mobile App Location Tracking is not available") if (Gb.primary_data_source_ICLOUD is False or (Device.device_id_famshr is None and Device.device_id_fmf is None) or Device.is_data_source_ICLOUD is False): - post_event(Device.devicename, + post_event(Device, "iCloud Location Tracking is not available") return elif Device.is_offline: - post_event(Device.devicename, + post_event(Device, "The device is offline, iCloud Location Tracking is not available") return @@ -518,7 +518,7 @@ def _handle_action_device_locate(Device, action_option): Device.reset_tracking_fields(interval_secs) det_interval.update_all_device_fm_zone_sensors_interval(Device, interval_secs) Device.icloud_update_reason = f"Location Requested@{time_now()}" - post_event(Device.devicename, f"Location will be updated at {Device.next_update_time}") + post_event(Device, f"Location will be updated at {Device.next_update_time}") Device.write_ha_sensors_state([NEXT_UPDATE, INTERVAL]) #-------------------------------------------------------------------- diff --git a/custom_components/icloud3/support/start_ic3.py b/custom_components/icloud3/support/start_ic3.py index 0431ec8..22d4959 100644 --- a/custom_components/icloud3/support/start_ic3.py +++ b/custom_components/icloud3/support/start_ic3.py @@ -9,7 +9,7 @@ EVLOG_ALERT, EVLOG_IC3_STARTING, EVLOG_NOTICE, EVLOG_IC3_STAGE_HDR, EVENT_RECDS_MAX_CNT_BASE, EVENT_RECDS_MAX_CNT_ZONE, CRLF, CRLF_DOT, CRLF_CHK, CRLF_SP3_DOT, CRLF_SP5_DOT, CRLF_HDOT, - CRLF_SP3_STAR, CRLF_INDENT, CRLF_X, + CRLF_SP3_STAR, CRLF_INDENT, CRLF_X, CRLF_TAB, DOT, CRLF_SP8_DOT, CRLF_RED_X, RED_X, CRLF_STAR, YELLOW_ALERT, UNKNOWN, RARROW, NBSP4, NBSP6, CIRCLE_STAR, INFO_SEPARATOR, DASH_20, CHECK_MARK, ICLOUD, FMF, FAMSHR, APPLE_SPECIAL_ICLOUD_SERVER_COUNTRY_CODE, @@ -62,6 +62,7 @@ from ..support import mobapp_interface from ..support import mobapp_data_handler from ..support import service_handler +from ..support import zone_handler from ..support import stationary_zone as statzone from ..support.waze import Waze from ..support.waze_history import WazeRouteHistory as WazeHist @@ -75,7 +76,7 @@ open_ic3_log_file, close_ic3_log_file, format_filename, internal_error_msg2, _trace, _traceha, more_info, ) -from ..helpers.dist_util import (format_dist_km, ) +from ..helpers.dist_util import (format_dist_km, m_to_um, ) from ..helpers.time_util import (time_now_secs, secs_to_time_str, secs_to_time_age_str, secs_to_age_str, ) import os @@ -549,7 +550,7 @@ def check_mobile_app_integration(ha_started_check=None): if len(Gb.mobile_app_device_fnames) == 0: Gb.conf_data_source_MOBAPP = False - # Cycle thru conf_devices since the Gb.Devic + # Cycle thru conf_devices since the Gb.Device mobile_app_error_msg = '' for conf_device in Gb.conf_devices: if conf_device[CONF_MOBILE_APP_DEVICE] == 'None': @@ -561,11 +562,11 @@ def check_mobile_app_integration(ha_started_check=None): f"{RARROW}Assigned to {Device.fname_devicename}") if mobile_app_error_msg: - post_event( f"{EVLOG_ALERT}MOBILE APP INTEGRATION ERROR > Mobile App devices have been " - f"configured but the Mobile App Integration has not been installed or an " - f"Mobile App device is not available. " - f"The Mobile App will not be used as a data source for that device." - f"{mobile_app_error_msg}") + # post_event( f"{EVLOG_ALERT}MOBILE APP INTEGRATION ERROR > Mobile App devices have been " + # f"configured but the Mobile App Integration has not been installed or an " + # f"Mobile App device is not available. " + # f"The Mobile App will not be used as a data source for that device." + # f"{mobile_app_error_msg}") return False except Exception as err: @@ -689,7 +690,7 @@ def set_zone_display_as(): return zone_msg = '' - Gb.zone_display_as = NON_ZONE_ITEM_LIST.copy() + Gb.zones_dname = NON_ZONE_ITEM_LIST.copy() # Update any regular zones with any fname/display_as changes for zone, Zone in Gb.HAZones_by_zone.items(): @@ -700,7 +701,7 @@ def set_zone_display_as(): if Zone.radius_m > 1: if Zone.passive: - crlf_dot_x = CRLF_STAR + crlf_dot_x = CRLF_X passive_msg = ', Passive Zone' else: crlf_dot_x = CRLF_DOT @@ -713,7 +714,7 @@ def set_zone_display_as(): # StatZone.initialize_updatable_items() # StatZone.write_ha_zone_state(StatZone.attrs) - crlf_dot_x = CRLF_STAR if StatZone.passive else CRLF_DOT + crlf_dot_x = CRLF_X if StatZone.passive else CRLF_DOT zone_msg +=(f"{crlf_dot_x}{StatZone.zone}, " f"{StatZone.dname} (r{StatZone.radius_m}m)") @@ -994,8 +995,8 @@ def create_Zones_object(): try: if Gb.initial_icloud3_loading_flag: - event.async_track_state_added_domain(Gb.hass, 'zone', _async_add_zone_entity_id) - event.async_track_state_removed_domain(Gb.hass, 'zone', _async_remove_zone_entity_id) + event.async_track_state_added_domain(Gb.hass, 'zone', zone_handler.ha_added_zone_entity_id) + event.async_track_state_removed_domain(Gb.hass, 'zone', zone_handler.ha_removed_zone_entity_id) if Gb.initial_icloud3_loading_flag is False: Gb.hass.services.call(ZONE, "reload") except: @@ -1008,7 +1009,7 @@ def create_Zones_object(): Gb.Zones_by_zone = {} Gb.HAZones = [] Gb.HAZones_by_zone = {} - Gb.zone_display_as = NON_ZONE_ITEM_LIST.copy() + Gb.zones_dname = NON_ZONE_ITEM_LIST.copy() # PSEUDO ZONES - Create zones for Away, Unknown, None, etc that do not really exist # These zones/states. Radius=0 is used to bypass normal zone processing. @@ -1046,14 +1047,15 @@ def create_Zones_object(): Zone = iCloud3_Zone(zone) if Zone.radius_m > 0: + r_ft = f"/{m_to_um(Zone.radius_m)}" if Gb.um_MI else "" if Zone.passive: - crlf_dot_x = CRLF_STAR + crlf_dot_x = CRLF_X passive_msg = ', Passive Zone' else: crlf_dot_x = CRLF_DOT passive_msg = '' zone_msg +=(f"{crlf_dot_x}{Zone.zone}, " - f"{Zone.dname} (r{Zone.radius_m}m){passive_msg}") + f"{Zone.dname} (r{Zone.radius_m}m{r_ft}){passive_msg}") if zone == HOME: Gb.HomeZone = Zone @@ -1067,11 +1069,12 @@ def create_Zones_object(): Gb.HAZones = list_add(Gb.HAZones, Zone) Gb.Zones_by_zone[zone] = Zone Gb.HAZones_by_zone[zone] = Zone - Gb.zone_display_as[zone] = Zone.dname + Gb.zones_dname[zone] = Zone.dname - crlf_dot_x = CRLF_STAR if Zone.passive else CRLF_DOT + crlf_dot_x = CRLF_X if Zone.passive else CRLF_DOT + r_ft = f"/{m_to_um(Zone.radius_m)}" if Gb.um_MI else "" zone_msg +=(f"{crlf_dot_x}{Zone.zone}, " - f"{Zone.dname} (r{Zone.radius_m}m)") + f"{Zone.dname} (r{Zone.radius_m}m{r_ft})") log_msg = f"Set up Zones > zone, Display ({Gb.display_zone_format})" post_event(f"{log_msg}{zone_msg}") @@ -1108,66 +1111,6 @@ def create_Zones_object(): Gb.debug_log['Gb.Zones'] = Gb.Zones -#------------------------------------------------------------------------------ -@callback -def _async_add_zone_entity_id(event: EventType[event.EventStateChangedData]) -> None: - """Add zone entity ID.""" - - zone_entity_id = event.data['entity_id'] - zone = zone_entity_id.replace('zone.', '') - ha_zone_attrs = entity_io.ha_zone_attrs(zone_entity_id) - - try: - if ha_zone_attrs and LATITUDE in ha_zone_attrs: - Zone = iCloud3_Zone(zone) - - if isnot_statzone(zone): - post_event( f"HA Zone Added > Zone-{Zone.dname}/{Zone.zone} " - f"(r{Zone.radius_m}m)") - - except Exception as err: - log_exception(err) - pass - -#------------------------------------------------------------------------------ -@callback -def _async_remove_zone_entity_id(event: EventType[event.EventStateChangedData]) -> None: - """Remove zone entity ID.""" - try: - zone_entity_id = event.data['entity_id'] - zone = zone_entity_id.replace('zone.', '') - if zone not in Gb.HAZones_by_zone: - return - - Zone = Gb.HAZones_by_zone[zone] - - Zone.status = -1 - Gb.HAZones_by_zone_deleted[zone] = Zone - if isnot_statzone(zone): - if zone in Gb.zone_display_as: del Gb.zone_display_as[zone] - if Zone.fname in Gb.zone_display_as: del Gb.zone_display_as[Zone.fname] - if Zone.name in Gb.zone_display_as: del Gb.zone_display_as[Zone.name] - if Zone.title in Gb.zone_display_as: del Gb.zone_display_as[Zone.title] - - Gb.Zones = list_del(Gb.Zones, Zone) - if zone in Gb.Zones_by_zone: del Gb.Zones_by_zone[zone] - Gb.HAZones = list_del(Gb.HAZones, Zone) - if zone in Gb.HAZones_by_zone: del Gb.HAZones_by_zone[zone] - - for Device in Gb. Devices: - Device.remove_zone_from_settings(zone) - - post_event( f"HA Zone Deleted > Zone-{Zone.dname}/{zone} ({Zone.radius_m}m") - - except Exception as err: - log_exception(err) - Gb.restart_icloud3_request_flag = True - post_event( f"Zone Deleted Error > Zone-{Zone.dname}," - f"An error was encountered deleting the zone, " - f"iCloud3 will be restarted") - return - - #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # # ICLOUD3 STARTUP MODULES -- STAGE 1 @@ -1471,9 +1414,9 @@ def _check_conf_famshr_devices_not_set_up(_FamShr): Gb.debug_log['_.devices_not_set_up'] = devices_not_set_up - devices_not_set_up_str = list_to_str(devices_not_set_up, CRLF_STAR) + devices_not_set_up_str = list_to_str(devices_not_set_up, CRLF_X) post_startup_alert( f"FamShr Device Config Error > " - f"Device Not found{devices_not_set_up_str.replace(CRLF_STAR, CRLF_HDOT)}") + f"Device Not found{devices_not_set_up_str.replace(CRLF_X, CRLF_HDOT)}") log_msg = ( f"{EVLOG_ALERT}FAMSHR DEVICES ERROR > Your Apple iCloud Account Family Sharing List did " f"not return any information for some of configured devices. FamShr will not be used " f"to track these devices." @@ -1497,7 +1440,7 @@ def _display_devices_verification_status(PyiCloud, _FamShr): sorted_device_id_by_famshr_fname = OrderedDict(sorted(_FamShr.device_id_by_famshr_fname.items())) for famshr_fname, device_id in sorted_device_id_by_famshr_fname.items(): - _RawData = PyiCloud.RawData_by_device_id_famshr.get(device_id, None) + _RawData = PyiCloud.RawData_by_device_id_famshr.get(device_id, None) try: raw_model, model, model_display_name = \ @@ -1512,28 +1455,27 @@ def _display_devices_verification_status(PyiCloud, _FamShr): conf_device = _verify_conf_device(famshr_fname, device_id, _FamShr) - devicename = conf_device.get(CONF_IC3_DEVICENAME, 'Not Tracked') - Device = Gb.Devices_by_devicename.get(devicename) + devicename = conf_device.get(CONF_IC3_DEVICENAME) exception_msg = '' - if conf_device.get(CONF_TRACKING_MODE, False) == INACTIVE_DEVICE: + if devicename is None: + exception_msg = 'Not Assigned to an iCloud3 Device' + + elif conf_device.get(CONF_TRACKING_MODE, False) == INACTIVE_DEVICE: exception_msg += 'INACTIVE, ' elif _RawData is None: + Device = Gb.Devices_by_devicename.get(devicename) if Device: Gb.reinitialize_icloud_devices_flag = (Gb.conf_famshr_device_cnt > 0) - exception_msg += 'Not Tracked, ' + exception_msg += 'Not Assigned to an iCloud3 Device, ' - if instr(famshr_fname, '*') or instr(exception_msg, "INACTIVE"): - famshr_fname = famshr_fname.replace('*', '') - crlf_mark = CRLF_STAR - else: - crlf_mark = CRLF_DOT + famshr_fname = famshr_fname.replace('*', '') if exception_msg: - event_msg += ( f"{crlf_mark}" - f"{famshr_fname}{RARROW}{exception_msg}" - f"{model_display_name} ({raw_model})") + event_msg += ( f"{CRLF_X}" + f"{famshr_fname}, {model_display_name} ({raw_model}) >" + f"{CRLF_SP8_DOT}{exception_msg}") continue # If no location info in pyiCloud data but tracked device is matched, refresh the @@ -1556,40 +1498,38 @@ def _display_devices_verification_status(PyiCloud, _FamShr): f"{model_display_name} ({raw_model})") log_rawdata(log_title, {'data': _RawData.device_data}) - device_type = '' - if devicename in Gb.Devices_by_devicename: - Device = Gb.Devices_by_devicename[devicename] - device_type = Device.device_type - Device.device_id_famshr = device_id - - # rc9 Set verify status to a valid device_id exists instead of always True - # This will pick up devices in the configuration file that no longer exist - Device.verified_flag = device_id in PyiCloud.RawData_by_device_id - - # link paired devices (iPhone <--> Watch) - Device.paired_with_id = _RawData.device_data['prsId'] - if Device.paired_with_id is not None: - if Device.paired_with_id in Gb.PairedDevices_by_paired_with_id: - Gb.PairedDevices_by_paired_with_id[Device.paired_with_id].append(Device) - else: - Gb.PairedDevices_by_paired_with_id[Device.paired_with_id] = [Device] - - Gb.Devices_by_icloud_device_id[device_id] = Device - Gb.famshr_device_verified_cnt += 1 - - crlf_mark = CRLF_CHK - - elif (instr(exception_msg, "INACTIVE") - or instr(exception_msg, "TRACKING DISABLED") - or instr(exception_msg, "NO LOCATION")): - crlf_mark = CRLF_STAR - - else: - crlf_mark = CRLF_DOT + # if devicename not in Gb.Devices_by_devicename: + Device = Gb.Devices_by_devicename.get(devicename) + if Device is None: + if exception_msg == '': exception_msg = ', Unknown Device or Other Device setup error' + event_msg += ( f"{CRLF_X}{famshr_fname}, {model_display_name} ({raw_model}) >" + f"{CRLF_SP8_DOT}{devicename}" + f"{exception_msg}") + continue - event_msg += ( f"{crlf_mark}" - f"{famshr_fname}{RARROW}{devicename}, " - f"{model_display_name} ({raw_model})" + # Device = Gb.Devices_by_devicename[devicename] + Device.device_id_famshr = device_id + + # rc9 Set verify status to a valid device_id exists instead of always True + # This will pick up devices in the configuration file that no longer exist + Device.verified_flag = device_id in PyiCloud.RawData_by_device_id + + # link paired devices (iPhone <--> Watch) + Device.paired_with_id = _RawData.device_data['prsId'] + if Device.paired_with_id is not None: + if Device.paired_with_id in Gb.PairedDevices_by_paired_with_id: + Gb.PairedDevices_by_paired_with_id[Device.paired_with_id].append(Device) + else: + Gb.PairedDevices_by_paired_with_id[Device.paired_with_id] = [Device] + + Gb.Devices_by_icloud_device_id[device_id] = Device + Gb.famshr_device_verified_cnt += 1 + #ᗒ> + + event_msg += ( f"{CRLF_CHK}" + f"{famshr_fname}, {model_display_name} ({raw_model}) >" + f"{CRLF_SP8_DOT}{devicename}, {Device.fname} " + f"{Device.tracking_mode_fname}" f"{exception_msg}") post_event(event_msg) @@ -1737,8 +1677,7 @@ def _check_duplicate_device_names(PyiCloud, _FamShr): conf_device[CONF_FAMSHR_DEVICE_ID] = _FamShr.device_id_by_famshr_fname[famshr_fname_last_located] update_conf_file_flag = True Device = Gb.Devices_by_devicename[conf_device[CONF_IC3_DEVICENAME]] - if instr(Device.evlog_fname_alert_char, YELLOW_ALERT) is False: - Device.evlog_fname_alert_char += YELLOW_ALERT + Device.set_fname_alert(YELLOW_ALERT) except Exception as err: event_msg =( f"Error resolving similar device names, " @@ -1857,7 +1796,7 @@ def setup_tracked_devices_for_fmf(PyiCloud=None): elif device[CONF_TRACKING_MODE] == INACTIVE_DEVICE: exception_msg = 'INACTIVE' - crlf_mark = CRLF_STAR + crlf_mark = CRLF_X if exception_msg: exception_event_msg += (f"{crlf_mark}{fmf_email}{RARROW}{exception_msg}") @@ -1892,7 +1831,7 @@ def setup_tracked_devices_for_fmf(PyiCloud=None): f"{DEVICE_TYPE_FNAME.get(device_type, device_type)}" f"{exception_msg}") else: - event_msg += ( f"{CRLF_STAR}" + event_msg += ( f"{CRLF_X}" f"{fmf_email}{RARROW}{devicename}, " f"{DEVICE_TYPE_FNAME.get(device_type, device_type)}" f"{exception_msg}") @@ -2112,11 +2051,13 @@ def setup_tracked_devices_for_mobapp(): verified_mobapp_fnames = [] tracked_msg = f"Mobile App Devices > {Gb.conf_mobapp_device_cnt} of {len(Gb.conf_devices)} iCloud3 Devices Configured" + + Gb.devicenames_x_mobapp_devicename = {} for devicename, Device in Gb.Devices_by_devicename.items(): broadcast_info_msg(f"Set up Mobile App Devices > {devicename}") - matched_mobapp_devices = [] conf_mobapp_device = Device.mobapp[DEVICE_TRACKER].replace(DEVICE_TRACKER_DOT, '') + Gb.devicenames_x_mobapp_devicename[devicename] = None # Set mobapp devicename to icloud devicename if nothing is specified. Set to not monitored # if no icloud famshr name @@ -2130,27 +2071,36 @@ def setup_tracked_devices_for_mobapp(): mobapp_id_by_mobapp_devicename, conf_mobapp_device) if _mobapp_devicename is None: - if instr(Device.evlog_fname_alert_char, YELLOW_ALERT) is False: - Device.evlog_fname_alert_char += YELLOW_ALERT - mobapp_error_search_msg += f"{CRLF_STAR}{conf_mobapp_device}_??? > Assigned to {Device.fname_devicename}" + Device.set_fname_alert(YELLOW_ALERT) + mobapp_error_search_msg += (f"{CRLF_X}{conf_mobapp_device}_??? > " + f"Assigned to {Device.fname_devicename}") continue mobapp_devicename = _mobapp_devicename + Gb.devicenames_x_mobapp_devicename[devicename] = mobapp_devicename + Gb.devicenames_x_mobapp_devicename[mobapp_devicename] = devicename else: if conf_mobapp_device in mobapp_id_by_mobapp_devicename: mobapp_devicename = conf_mobapp_device + Gb.devicenames_x_mobapp_devicename[devicename] = mobapp_devicename + Gb.devicenames_x_mobapp_devicename[mobapp_devicename] = devicename else: - if instr(Device.evlog_fname_alert_char, YELLOW_ALERT) is False: - Device.evlog_fname_alert_char += YELLOW_ALERT - mobapp_error_not_found_msg += f"{CRLF_STAR}{conf_mobapp_device} > Assigned to {Device.fname_devicename}" + Device.set_fname_alert(YELLOW_ALERT) + mobapp_error_not_found_msg += ( f"{CRLF_X}{conf_mobapp_device} > " + f"Assigned to {Device.fname_devicename}") continue + for devicename, Device in Gb.Devices_by_devicename.items(): + mobapp_devicename = Gb.devicenames_x_mobapp_devicename[devicename] + if mobapp_devicename is None: continue + # device_tracker entity is disabled if instr(mobapp_id_by_mobapp_devicename[mobapp_devicename], 'DISABLED'): - if instr(Device.evlog_fname_alert_char, YELLOW_ALERT) is False: - Device.evlog_fname_alert_char += YELLOW_ALERT - mobapp_error_disabled_msg += f"{CRLF_DOT}{mobapp_devicename} > Assigned to-{Device.fname_devicename}" + Device.set_fname_alert(YELLOW_ALERT) + Device.mobapp_device_unavailable_flag = True + mobapp_error_disabled_msg += ( f"{CRLF_DOT}{mobapp_devicename} > " + f"Assigned to-{Device.fname_devicename}") continue # Build errors message, can still use the Mobile App for zone changes but sensors are not monitored @@ -2158,9 +2108,9 @@ def setup_tracked_devices_for_mobapp(): or instr(last_updt_trig_by_mobapp_devicename.get(mobapp_devicename, ''), 'DISABLED') or battery_level_sensors_by_mobapp_devicename.get(mobapp_devicename, '') == '' or instr(battery_level_sensors_by_mobapp_devicename.get(mobapp_devicename, ''), 'DISABLED')): - if instr(Device.evlog_fname_alert_char, YELLOW_ALERT) is False: - Device.evlog_fname_alert_char += YELLOW_ALERT - mobapp_error_mobile_app_msg += f"{CRLF_DOT}{mobapp_devicename} > Assigned to {Device.fname_devicename}" + Device.set_fname_alert(YELLOW_ALERT) + mobapp_error_mobile_app_msg += (f"{CRLF_DOT}{mobapp_devicename} > " + f"Assigned to {Device.fname_devicename}") try: mobapp_fname = device_info_by_mobapp_devicename[mobapp_devicename].rsplit('(')[0] @@ -2210,8 +2160,10 @@ def setup_tracked_devices_for_mobapp(): f"sensor.{battery_level_sensors_by_mobapp_devicename.get(mobapp_devicename, '')}" Device.mobapp[BATTERY_STATUS] = Device.sensors['mobapp_sensor-battery_status'] = \ f"sensor.{battery_state_sensors_by_mobapp_devicename.get(mobapp_devicename, '')}" - tracked_msg += (f"{CRLF_CHK}{mobapp_fname} ({mobapp_devicename}){RARROW}{devicename} " - f"{CRLF_INDENT}({Device.raw_model})") + + tracked_msg += (f"{CRLF_CHK}{mobapp_fname}, {mobapp_devicename} ({Device.raw_model}) >" + f"{CRLF_SP8_DOT}{devicename}, {Device.fname}" + f"{Device.tracking_mode_fname}") # Remove the mobapp device from the list since we know it is tracked if mobapp_devicename in unmatched_mobapp_devices: @@ -2222,28 +2174,34 @@ def setup_tracked_devices_for_mobapp(): # Devices in the list were not matched with an iCloud3 device or are disabled for mobapp_devicename, mobapp_id in unmatched_mobapp_devices.items(): + devicename = Gb.devicenames_x_mobapp_devicename.get(mobapp_devicename, 'unknown') + Device = Gb.Devices_by_devicename.get(devicename) + try: - mobapp_fname = device_info_by_mobapp_devicename[mobapp_devicename].rsplit('(')[0] + mobapp_dev_info = device_info_by_mobapp_devicename[mobapp_devicename] + fname_dev_type = mobapp_dev_info.rsplit('(') + mobapp_fname = fname_dev_type[0] + mobapp_dev_type = f"({fname_dev_type[1]}" except: - mobapp_fname = f"{mobapp_devicename.replace('_', ' ').title()}(?)" - - if mobapp_id_by_mobapp_devicename[mobapp_devicename].startswith('DISABLED'): - tracked_msg += f"{CRLF_STAR}{mobapp_fname} ({mobapp_devicename}){RARROW}DISABLED" - if mobapp_devicename in Gb.Devices_by_mobapp_devicename: - Device = Gb.Devices_by_mobapp_devicename[mobapp_devicename] + mobapp_info = f"{mobapp_devicename.replace('_', ' ').title()}(?)" + mobapp_fname = mobapp_info + mobapp_dev_type = '' - tracked_msg += f", UsedBy-{Device.fname_device}" - tracked_msg += f"{CRLF_INDENT}{device_info_by_mobapp_devicename[mobapp_devicename]}" + duplicate_msg = ' (DUPLICATE NAME)' if mobapp_fname in verified_mobapp_fnames else '' + crlf_sym = CRLF_X + if instr(mobapp_id_by_mobapp_devicename[mobapp_devicename], 'DISABLED'): + device_msg = "DISABLED IN MOBILE APP INTEGRATION" + crlf_sym = CRLF_RED_X + elif Device: + device_msg = "Not Monitored" else: - if mobapp_fname in verified_mobapp_fnames: - crlf_symb = CRLF_STAR - duplicate_msg = ' (DUPLICATE NAME)' - else: - crlf_symb = CRLF_DOT - duplicate_msg = '' + device_msg = "Not Assigned to an iCloud3 Device" - tracked_msg += (f"{crlf_symb}{mobapp_fname} ({mobapp_devicename}){RARROW}Not Monitored, " - f"{CRLF_INDENT}{device_info_by_mobapp_devicename[mobapp_devicename]}{duplicate_msg}") + tracked_msg += (f"{crlf_sym}{mobapp_fname}, {mobapp_devicename} {mobapp_dev_type} >") + if Device: + Device.set_fname_alert(YELLOW_ALERT) + tracked_msg += (f"{CRLF_SP8_DOT}{Device.devicename}, {Device.fname}") + tracked_msg += f"{CRLF_SP8_DOT}{device_msg}{duplicate_msg}" post_event(tracked_msg) _display_any_mobapp_errors( mobapp_error_mobile_app_msg, @@ -2251,16 +2209,6 @@ def setup_tracked_devices_for_mobapp(): mobapp_error_disabled_msg, mobapp_error_not_found_msg) - # if (verify_mobile_app_integration_installed() is False - # and Gb.conf_mobapp_device_cnt > 0): - # Gb.conf_data_source_MOBAPP = False - - # post_startup_alert( f"Mobile App Integration is not installed. Mobile App Tracking " - # f"Method is not available") - - # post_event(f"{EVLOG_ALERT}Mobile App devices have been configured but the Mobile App " - # f"Integration has not been installed. The Mobile App will not be used as a " - # f"data source; location data and zone enter/exit triggers will not be monitored") return #-------------------------------------------------------------------- @@ -2293,7 +2241,7 @@ def _search_for_mobapp_device(devicename, Device, mobapp_id_by_mobapp_devicename alert_msg =(f"{EVLOG_ALERT}DUPLICATE MOBAPP DEVICES FOUND > More than one Device Tracker Entity " f"was found during the scan of the HA Device Registry." - f"{CRLF_STAR}AssignedTo-{Device.fname_devicename}" + f"{CRLF_X}AssignedTo-{Device.fname_devicename}" f"{CRLF}{more_info('mobapp_error_multiple_devices_on_scan')}" f"{CRLF}{'-'*75}" f"{CRLF}Count-{len(matched_mobapp_devices)}, " @@ -2545,7 +2493,7 @@ def setup_trackable_devices(): if 'none' not in Device.log_zones: log_zones_fname = [zone_dname(zone) for zone in Device.log_zones] log_zones = list_to_str(log_zones_fname) - log_zones = f"{log_zones.replace(', Name-', f'{RARROW}(')}.cvs)" + log_zones = f"{log_zones.replace(', Name-', f'{RARROW}(')}.csv)" event_msg += f"{CRLF_HDOT}Log Zone Activity: {log_zones}" if Device.track_from_base_zone != HOME: @@ -2612,7 +2560,7 @@ def display_inactive_devices(): return event_msg = f"Inactive/Untracked Devices > " - event_msg+= list_to_str(inactive_devices, separator=CRLF_STAR) + event_msg+= list_to_str(inactive_devices, separator=CRLF_X) post_event(event_msg) if len(inactive_devices) == len(Gb.conf_devices): diff --git a/custom_components/icloud3/support/start_ic3_control.py b/custom_components/icloud3/support/start_ic3_control.py index e15ad1f..1ab3a22 100644 --- a/custom_components/icloud3/support/start_ic3_control.py +++ b/custom_components/icloud3/support/start_ic3_control.py @@ -368,7 +368,7 @@ def stage_7_initial_locate(): else: continue - post_event(Device.devicename, 'Trigger > Initial Locate') + post_event(Device, 'Trigger > Initial Locate') Device.update_sensors_flag = True diff --git a/custom_components/icloud3/support/stationary_zone.py b/custom_components/icloud3/support/stationary_zone.py index 2def2b1..caf54ed 100644 --- a/custom_components/icloud3/support/stationary_zone.py +++ b/custom_components/icloud3/support/stationary_zone.py @@ -8,8 +8,10 @@ # #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> from ..global_variables import GlobalVariables as Gb -from ..const import (ZONE, LATITUDE, LONGITUDE, STATZONE_RADIUS_1M, HIGH_INTEGER, - ENTER_ZONE, EXIT_ZONE, NEXT_UPDATE, INTERVAL, RARROW, ) +from ..const import (ZONE, LATITUDE, LONGITUDE, GPS, + STATZONE_RADIUS_1M, HIGH_INTEGER, + ENTER_ZONE, EXIT_ZONE, NEXT_UPDATE, INTERVAL, + NOT_SET, RARROW, ) from ..zone import iCloud3_StationaryZone from ..support import determine_interval as det_interval from ..support import mobapp_interface @@ -17,10 +19,65 @@ from ..helpers.messaging import (post_event, post_error_msg, post_monitor_msg, log_debug_msg, log_exception, log_rawdata, _trace, _traceha, ) from ..helpers.time_util import (secs_to_time, datetime_now, format_time_age, secs_since, ) -from ..helpers.dist_util import (format_dist_m, ) -from ..helpers import entity_io +from ..helpers.dist_util import (format_dist_m, calc_distance_km, ) +# from ..helpers import entity_io +#-------------------------------------------------------------------- +def create_StationaryZones_object(): + ''' + Create a new Stationary Zone + ''' + + statzone_id = str(len(Gb.StatZones) + 1) + StatZone = iCloud3_StationaryZone(statzone_id) + event_msg = (f"ADDED StatZone > {StatZone.fname} ({StatZone.zone})") + post_monitor_msg(event_msg) + + Gb.StatZones.append(StatZone) + Gb.StatZones_by_zone[StatZone.zone] = StatZone + + Gb.Zones.append(StatZone) + Gb.Zones_by_zone[StatZone.zone] = StatZone + Gb.HAZones.append(StatZone) + Gb.HAZones_by_zone[StatZone.zone] = StatZone + Gb.state_to_zone[StatZone.zone] = StatZone.zone + + return StatZone + +#-------------------------------------------------------------------- +def move_into_statzone_if_timer_reached(Device): + ''' + Check the Device's Stationary Zone expired timer and distance moved: + Update the Device's Stat Zone distance moved + 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 + ''' + if Gb.is_statzone_used is False: + return False + + calc_dist_last_poll_moved_km = calc_distance_km(Device.sensors[GPS], Device.loc_data_gps) + Device.update_distance_moved(calc_dist_last_poll_moved_km) + + # See if moved less than the stationary zone movement limit + # If updating via the Mobile App and the current state is stationary, + # make sure it is kept in the stationary zone + if Device.is_statzone_timer_reached is False or Device.is_location_old_or_gps_poor: + return False + + if Device.is_statzone_move_limit_exceeded: + Device.statzone_reset_timer + + # Monitored devices can move into a tracked zone but can not create on for itself + elif Device.is_monitored: #beta 4/13b16 + pass + + elif (Device.isnotin_statzone + or (is_statzone(Device.mobapp_data_state) and Device.loc_data_zone == NOT_SET)): + move_device_into_statzone(Device) + + return True + #-------------------------------------------------------------------- def move_device_into_statzone(Device): ''' @@ -94,28 +151,6 @@ def move_device_into_statzone(Device): return True -#-------------------------------------------------------------------- -def create_StationaryZones_object(): - ''' - Create a new Stationary Zone - ''' - - statzone_id = str(len(Gb.StatZones) + 1) - StatZone = iCloud3_StationaryZone(statzone_id) - event_msg = (f"ADDED StatZone > {StatZone.fname} ({StatZone.zone})") - post_monitor_msg(event_msg) - - Gb.StatZones.append(StatZone) - Gb.StatZones_by_zone[StatZone.zone] = StatZone - - Gb.Zones.append(StatZone) - Gb.Zones_by_zone[StatZone.zone] = StatZone - Gb.HAZones.append(StatZone) - Gb.HAZones_by_zone[StatZone.zone] = StatZone - Gb.state_to_zone[StatZone.zone] = StatZone.zone - - return StatZone - #-------------------------------------------------------------------- def exit_all_statzones(): ''' @@ -153,7 +188,7 @@ def exit_statzone(Device): event_msg =(f"Will Remove Stationary Zone > {StatZone.dname}, " f"LastUsedBy-{Device.fname}") - post_event(Device.devicename, event_msg) + post_event(Device, event_msg) #-------------------------------------------------------------------- def kill_and_recreate_unuseable_statzone(Device): @@ -205,12 +240,6 @@ def move_statzone_to_device_location(Device, latitude=None, longitude=None): if _is_too_close_to_another_zone(Device): return - # StatZone.attrs[LATITUDE] = StatZone.latitude = latitude - # StatZone.attrs[LONGITUDE] = StatZone.longitude = longitude - - # StatZone.radius_m = Gb.statzone_radius_m - # StatZone.passive = False - _clear_statzone_timer_distance(Device) StatZone.attrs[LATITUDE] = latitude @@ -232,11 +261,8 @@ def remove_statzone(StatZone, Device=None): if StatZone is None or StatZone.passive: return - # StatZone.radius_m = STATZONE_RADIUS_1M - # StatZone.passive = True StatZone.write_ha_zone_state(StatZone.passive_attrs) - #StatZone.remove_ha_zone() if StatZone not in Gb.StatZones_to_delete: Gb.StatZones_to_delete.append(StatZone) @@ -244,7 +270,7 @@ def remove_statzone(StatZone, Device=None): _clear_statzone_timer_distance(Device) event_msg =(f"Exited Stationary Zone > {StatZone.dname}, " f"DevicesRemaining-{devices_in_statzone_count(StatZone)}") - post_event(Device.devicename, event_msg) + post_event(Device, event_msg) #-------------------------------------------------------------------- def ha_statzones(): @@ -284,8 +310,6 @@ def _trigger_monitored_device_update(StatZone, Device, action): continue if event_msg: - # v3.0.rc7.1 Change Global force_update to the actual device needing it - # Gb.icloud_force_update_flag = True _Device.icloud_force_update_flag = True det_interval.update_all_device_fm_zone_sensors_interval(_Device, 5) _Device.icloud_update_reason = event_msg @@ -326,7 +350,7 @@ def _is_too_close_to_another_zone(Device): log_msg = ( f"{Device.devicename} > StatZone not created, too close to " f"{CloseZone.dname} " f"({format_dist_m(CloseZone.distance_m(Device.loc_data_latitude, Device.loc_data_longitude))})") - post_event(Device.devicename, log_msg) + post_event(Device, log_msg) log_debug_msg(log_msg) return True diff --git a/custom_components/icloud3/support/waze.py b/custom_components/icloud3/support/waze.py index 7244be4..8ba3106 100644 --- a/custom_components/icloud3/support/waze.py +++ b/custom_components/icloud3/support/waze.py @@ -15,7 +15,7 @@ from ..helpers.common import (instr, format_gps, ) from ..helpers.messaging import (post_event, post_internal_error, log_info_msg, _trace, _traceha, ) from ..helpers.time_util import (time_now_secs, datetime_now, secs_since, secs_to_time_str, mins_to_time_str, ) -from ..helpers.dist_util import (mi_to_km, format_dist_km, ) +from ..helpers.dist_util import (km_to_um, ) import traceback import time @@ -152,7 +152,7 @@ def get_route_time_distance(self, Device, FromZone, check_hist_db=True): if waze_status == WAZE_NO_DATA: event_msg = (f"Waze Route Error > Problem connecting to Waze Servers. " f"Distance will be calculated, Travel Time not available") - post_event(Device.devicename, event_msg) + post_event(Device, event_msg) return (WAZE_NO_DATA, 0, 0, 0) @@ -214,11 +214,13 @@ def get_route_time_distance(self, Device, FromZone, check_hist_db=True): if waze_source_msg == "": # event_msg += ( f"TravTime-{self.waze_mins_to_time_str(route_time)}, " event_msg += ( f"TravTime-{secs_to_time_str(route_time * 60)}, " - f"Dist-{format_dist_km(route_dist_km)}, " - f"Moved-{format_dist_km(dist_moved_km)}" + f"Dist-{km_to_um(route_dist_km)}, " + #f"Dist-{format_dist_km(route_dist_km)}, " + f"Moved-{km_to_um(dist_moved_km)}" + #f"Moved-{format_dist_km(dist_moved_km)}" #f"CalcMoved-{format_dist_km(Device.loc_data_dist_moved_km)}, " f"{wazehist_save_msg}") - post_event(Device.devicename, event_msg) + post_event(Device, event_msg) FromZone.waze_results = (WAZE_USED, route_time, route_dist_km, dist_moved_km) diff --git a/custom_components/icloud3/support/zone_handler.py b/custom_components/icloud3/support/zone_handler.py new file mode 100644 index 0000000..a76a0a4 --- /dev/null +++ b/custom_components/icloud3/support/zone_handler.py @@ -0,0 +1,532 @@ +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +# +# This module handles zone processing for the the icloud3_main module that +# is used to: +# - determine if a device is in a zone +# - select the zone and assigning it to a device +# - display all zone information in the Event Log +# - utilities for determining if a device can use a zone +# - requesting famshr updates for devices not using the mobile app +# +#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + +import os +import homeassistant.util.dt as dt_util +from homeassistant.helpers.typing import EventType +from homeassistant.helpers import event +from homeassistant.core import callback + + +from ..global_variables import GlobalVariables as Gb +from ..const import (HOME, NOT_HOME, NOT_SET, HIGH_INTEGER, RARROW, + GPS, HOME_DISTANCE, ENTER_ZONE, EXIT_ZONE, ZONE, LATITUDE, ) + +from ..zone import iCloud3_Zone +from ..support import stationary_zone as statzone +from ..support import determine_interval as det_interval +from ..helpers import entity_io +from ..helpers.common import (instr, is_zone, is_statzone, isnot_statzone, isnot_zone, zone_dname, + list_to_str, list_add, list_del,) +from ..helpers.messaging import (post_event, post_error_msg, post_monitor_msg, + log_info_msg, log_exception, + _trace, _traceha, ) +from ..helpers.time_util import (time_now_secs, secs_to_time, secs_to, secs_since, time_now, + datetime_now, secs_to_datetime, ) +from ..helpers.dist_util import (calc_distance_km, format_dist_km, format_dist_m, + km_to_um, m_to_um, ) + +# zone_data constants - Used in the select_zone function +ZD_DIST_M = 0 +ZD_ZONE = 1 +ZD_NAME = 2 +ZD_RADIUS = 3 +ZD_DNAME = 4 +ZD_CNT = 5 + + + +#------------------------------------------------------------------------------ +# +# DETERMINE THE ZONE THE DEVICE IS CURRENTLY IN +# +#------------------------------------------------------------------------------ +def update_current_zone(Device, display_zone_msg=True): + + ''' + Get current zone of the device based on the location + + Parameters: + selected_zone_results - The zone may have already been selected. If so, this list + is the results from a previous _select_zone + display_zone_msg - True if the msg should be posted to the Event Log + + Returns: + Zone Zone object + zone zone name or not_home if not in a zone + + NOTE: This is the same code as (active_zone/async_active_zone) in zone.py + but inserted here to use zone table loaded at startup rather than + calling hass on all polls + ''' + + # Zone selected may have been done when determing if the device just entered a zone + # during the passthru check. If so, use it and then reset it + if Device.selected_zone_results == []: + ZoneSelected, zone_selected, zone_selected_dist_m, zones_distance_list = \ + select_zone(Device) + else: + ZoneSelected, zone_selected, zone_selected_dist_m, zones_distance_list = \ + Device.selected_zone_results + Device.selected_zone_results = [] + + if zone_selected == 'unknown': + return ZoneSelected, zone_selected + + if ZoneSelected is None: + ZoneSelected = Gb.Zones_by_zone[NOT_HOME] + zone_selected = NOT_HOME + zone_selected_dist_m = 0 + + # In a zone but if not in a track from zone and was in a Stationary Zone, + # reset the stationary zone + elif Device.isin_statzone and isnot_statzone(zone_selected): + statzone.exit_statzone(Device) + + + # Get distance between zone selected and current zone to see if they overlap. + # If so, keep the current zone + if (zone_selected != NOT_HOME + and is_overlapping_zone(Device.loc_data_zone, zone_selected)): + zone_selected = Device.loc_data_zone + ZoneSelected = Gb.Zones_by_zone[Device.loc_data_zone] + + # The zone changed + elif Device.loc_data_zone != zone_selected: + # See if any device without the mobapp was in this zone. If so, request a + # location update since it was running on the inzone timer instead of + # exit triggers from the Mobile App + if (Gb.mobapp_monitor_any_devices_false_flag + and zone_selected == NOT_HOME + and Device.loc_data_zone != NOT_HOME): + request_update_devices_no_mobapp_same_zone_on_exit(Device) + + Device.loc_data_zone = zone_selected + Device.zone_change_secs = time_now_secs() + Device.zone_change_datetime = datetime_now() + + # The zone changed, update the enter/exit zone times if the + # Device does not use the Mobile App + if zone_selected == NOT_HOME: + if (Device.mobapp_monitor_flag is False + or Device.mobapp_zone_exit_secs == 0): + Device.mobapp_zone_exit_secs = time_now_secs() + Device.mobapp_zone_exit_time = time_now() + + else: + if (Device.mobapp_monitor_flag is False + or Device.mobapp_zone_enter_secs == 0): + Device.mobapp_zone_enter_secs = time_now_secs() + Device.mobapp_zone_enter_time = time_now() + + if display_zone_msg: + post_zone_selected_msg(Device, ZoneSelected, zone_selected, + zone_selected_dist_m, zones_distance_list) + + return ZoneSelected, zone_selected + +#-------------------------------------------------------------------- +def select_zone(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 + gps_accuracy_adj = int(Device.loc_data_gps_accuracy / 2) + + # [distance from zone, Zone, zone_name, redius, display_as] + zone_data_selected = [HIGH_INTEGER, None, '', HIGH_INTEGER, '', 1] + + # 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 + zones_msg = f"Zone > Unknown, GPS-{Device.loc_data_fgps}" + post_event(Device, zones_msg) + return ZoneSelected, zone_selected, 0, [] + + # Verify that the statzone was not left without an exit trigger. If so, move this device out of it. + if (Device.isin_statzone + and Device.StatZone.distance_m(latitude, longitude) > Device.StatZone.radius_m): + statzone.exit_statzone(Device) + + zones_data = [[Zone.distance_m(latitude, longitude), Zone, Zone.zone, + Zone.radius_m, Zone.dname] + for Zone in Gb.HAZones + if (Zone.passive is False)] + + # Do not select a new zone for the Device if it just left a zone. Set to Away and next_update will be soon + # if Device.wasin_zone is False or secs_since(Device.mobapp_zone_exit_secs) >= Gb.exit_zone_interval_secs/2: + # 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] + gps_accuracy_adj] + + for zone_data in inzone_zones: + if zone_data[ZD_RADIUS] <= zone_data_selected[ZD_RADIUS]: + zone_data_selected = zone_data + + ZoneSelected = zone_data_selected[ZD_ZONE] + zone_selected = zone_data_selected[ZD_NAME] + zone_selected_dist_m = zone_data_selected[ZD_DIST_M] + + # Selected a statzone + if zone_selected in Gb.StatZones_by_zone: + Device.StatZone = Gb.StatZones_by_zone[zone_selected] + + # In a zone and the mobapp enter zone info was not set, set it now + if (zone_selected != Device.mobapp_zone_enter_zone + and is_zone(zone_selected) and isnot_zone(Device.mobapp_zone_enter_zone)): + Device.mobapp_zone_enter_secs = Gb.this_update_secs + Device.mobapp_zone_enter_time = Gb.this_update_time + Device.mobapp_zone_enter_zone = zone_selected + + # Build an item for each zone (dist-from-zone|zone_name|display_name-##km) + zones_distance_list = \ + [(f"{int(zone_data[ZD_DIST_M]):08}|{zone_data[ZD_NAME]}|{zone_data[ZD_DIST_M]}") + for zone_data in zones_data if zone_data[ZD_NAME] != zone_selected] + + return ZoneSelected, zone_selected, zone_selected_dist_m, zones_distance_list + +#-------------------------------------------------------------------- +def post_zone_selected_msg(Device, ZoneSelected, zone_selected, + zone_selected_dist_m, zones_distance_list): + + device_zones = [_Device.loc_data_zone for _Device in Gb.Devices] + zones_cnt_by_zone = {_zone:device_zones.count(_zone) for _zone in set(device_zones)} + + # Format the Zone Selected Msg (ZoneName (#)) + zone_selected_msg = zone_dname(zone_selected) + if zone_selected in zones_cnt_by_zone: + zone_selected_msg += f"({zones_cnt_by_zone[zone_selected]})" + if ZoneSelected.radius_m > 0: + zone_selected_msg += f"-{m_to_um(zone_selected_dist_m)}" + + # Format distance msg + zones_dist_msg = '' + zones_displayed = [zone_selected] + zones_distance_list.sort() + for zone_distance_list in zones_distance_list: + zdl_items = zone_distance_list.split('|') + _zone = zdl_items[1] + _zone_dist = float(zdl_items[2]) + + zones_dist_msg += ( f"{zone_dname(_zone)}" + f"-{m_to_um(_zone_dist)}") + # zones_dist_msg += f"-r{int(Gb.Zones_by_zone[_zone].radius_m)}m" + zones_dist_msg += ", " + + gps_accuracy_msg = '' + if zone_selected_dist_m > ZoneSelected.radius_m: + gps_accuracy_msg = (f"AccuracyAdjustment-" + f"{int(Device.loc_data_gps_accuracy / 2)}m, ") + + # Format distance and count msg + zones_cnt_msg = '' + for _zone, cnt in zones_cnt_by_zone.items(): + if zone_dname(_zone) in zones_dist_msg: + zones_dist_msg = zones_dist_msg.replace( + zone_dname(_zone), f"{zone_dname(_zone)}({cnt})") + elif _zone != zone_selected: + zones_dist_msg += f"{zone_dname(_zone)}({cnt}), " + zones_cnt_msg += f"{zone_dname(_zone)}({cnt}), " + + zones_dist_msg = zones_dist_msg.replace('──', 'NotSet') + zones_cnt_msg = zones_cnt_msg.replace('──', 'NotSet') + + if is_zone(zone_selected) and isnot_statzone(zone_selected): + post_monitor_msg(Device.devicename, f"Zone Distance > {zones_dist_msg}") + zones_dist_msg = '' + else: + zones_cnt_msg = '' + + zones_msg =(f"Zone > " + f"{zone_selected_msg} > " + f"{zones_dist_msg}" + f"{zones_cnt_msg}" + f"{gps_accuracy_msg}" + f"GPS-{Device.loc_data_fgps}") + + if zone_selected == Device.log_zone: + zones_msg += ' (Logged)' + + post_event(Device, zones_msg) + + if (zones_cnt_msg + and Device.loc_data_zone != Device.sensors[ZONE] + and NOT_SET not in zones_cnt_by_zone): + for _Device in Gb.Devices: + if Device is not _Device: + event_msg = f"Zone-Device Counts > {zones_cnt_msg}" + post_event(_Device.devicename, event_msg) + +#-------------------------------------------------------------------- +def closest_zone(latitude, longitude): + ''' + Get the closest zone to this location + + Return: + - Zone, Zone entity, Zone display name, distance (m) + ''' + try: + zones_data = [[Zone.distance_m(latitude, longitude), Zone.zone] + for Zone in Gb.HAZones + if Zone.radius_m > 1] + zones_data.sort() + zone_dist_m, zone = zones_data[0] + Zone = Gb.Zones_by_zone.get(zone) + + return Zone, zone, Zone.dname, zone_dist_m + + except Exception as err: + log_exception(err) + return None, 'unknown', 'Unknown', 0 + +#-------------------------------------------------------------------- +def is_overlapping_zone(zone1, zone2): + ''' + zone1 and zone2 overlap if their distance between centers is less than 2m + ''' + try: + if zone1 == zone2: + return True + + if zone1 == "": zone1 = HOME + Zone1 = Gb.Zones_by_zone[zone1] + Zone2 = Gb.Zones_by_zone[zone2] + + zone_dist = Zone1.distance(Zone2.latitude, Zone2.longitude) + + return (zone_dist <= 2) + + except: + return False + +#-------------------------------------------------------------------- +def is_outside_zone_no_exit(Device, zone, trigger, latitude, longitude): + ''' + If the device is outside of the zone and less than the zone radius + gps_acuracy_threshold + and no Exit Trigger was received, it has probably wandered due to + GPS errors. If so, discard the poll and try again later + + Updates: Set the Device.outside_no_exit_trigger_flag + Increase the old_location_poor_gps count when this innitially occurs + Return: Reason message + ''' + if Device.mobapp_monitor_flag is False: + return '' + + trigger = Device.trigger if trigger == '' else trigger + if (instr(trigger, ENTER_ZONE) + or Device.sensor_zone == NOT_SET + or zone not in Gb.HAZones_by_zone + or Device.icloud_initial_locate_done is False): + Device.outside_no_exit_trigger_flag = False + return '' + + Zone = Gb.Zones_by_zone[zone] + dist_fm_zone_m = Zone.distance_m(latitude, longitude) + zone_radius_m = Zone.radius_m + zone_radius_accuracy_m = zone_radius_m + Gb.gps_accuracy_threshold + + info_msg = '' + if (dist_fm_zone_m > zone_radius_m + and Device.got_exit_trigger_flag is False + and Zone.is_statzone is False): + if (dist_fm_zone_m < zone_radius_accuracy_m + and Device.outside_no_exit_trigger_flag == False): + Device.outside_no_exit_trigger_flag = True + Device.old_loc_cnt += 1 + + info_msg = ("Outside of Zone without MobApp `Exit Zone` Trigger, " + f"Keeping in Zone-{Zone.dname} > ") + else: + Device.got_exit_trigger_flag = True + info_msg = ("Outside of Zone without MobApp `Exit Zone` Trigger " + f"but outside threshold, Exiting Zone-{Zone.dname} > ") + + info_msg += (f"Distance-{format_dist_m(dist_fm_zone_m)}, " + f"KeepInZoneThreshold-{format_dist_m(zone_radius_m)} " + f"to {format_dist_m(zone_radius_accuracy_m)}, " + f"Located-{Device.loc_data_time_age}") + + if Device.got_exit_trigger_flag: + Device.outside_no_exit_trigger_flag = False + + return info_msg + +#------------------------------------------------------------------------------ +def log_zone_enter_exit_activity(Device): + ''' + An entry can be written to the 'zone-log-[year]-[device-[zone].csv' file. + This file shows when a device entered & exited a zone, the time the device was in + the zone, the distance to Home, etc. It can be imported into a spreadsheet and used + at year end for expense calculations. + ''' + # Uncomment the following for testing + # if Gb.this_update_time.endswith('0:00') or Gb.this_update_time.endswith('5:00'): + # Device.mobapp_zone_exit_secs = time_now_secs() + # Device.mobapp_zone_exit_time = time_now() + # Device.last_zone = HOME + # pass + # elif 'none' in Device.log_zones: + + if ('none' in Device.log_zones + or Device.log_zone == Device.loc_data_zone + or (Device.log_zone == '' and Device.loc_data_zone not in Device.log_zones)): + return + + if Device.log_zone == '': + Device.log_zone = Device.loc_data_zone + Device.log_zone_enter_secs = Gb.this_update_secs + event_msg = f"Log Zone Activity > Logging Started-{zone_dname(Device.log_zone)}" + post_event(Device, event_msg) + return + + # Must be in the zone for at least 4-minutes + inzone_secs = secs_since(Device.log_zone_enter_secs) + inzone_hrs = inzone_secs/3600 + if inzone_secs < 240: return + + filename = (f"zone-log-{dt_util.now().strftime('%Y')}-" + f"{Device.log_zones_filename}.csv") + + with open(filename, 'a', encoding='utf8') as f: + if os.path.getsize(filename) == 0: + recd = "Date,Zone Enter Time,Zone Exit Time,Time (Mins),Time (Hrs),Distance (Home),Zone,Device\n" + f.write(recd) + + recd = (f"{datetime_now()[:10]}," + f"{secs_to_datetime(Device.log_zone_enter_secs)}," + f"{secs_to_datetime(Gb.this_update_secs)}," + f"{inzone_secs/60:.0f}," + f"{inzone_hrs:.2f}," + f"{Device.sensors[HOME_DISTANCE]:.2f}," + f"{Device.log_zone}," + f"{Device.devicename}" + "\n") + f.write(recd) + event_msg = f"Log Zone Activity > Logging Ended-{zone_dname(Device.log_zone)}" + post_event(Device, event_msg) + + if Device.loc_data_zone in Device.log_zones: + Device.log_zone = Device.loc_data_zone + Device.log_zone_enter_secs = Gb.this_update_secs + else: + Device.log_zone = '' + Device.log_zone_enter_secs = 0 + +#------------------------------------------------------------------------------ +def request_update_devices_no_mobapp_same_zone_on_exit(Device): + ''' + The Device is exiting a zone. Check all other Devices that were in the same + zone that do not have the mobapp installed and set the next update time to + 5-seconds to see if that device also exited instead of waiting for the other + devices inZone interval time to be reached. + + Check the next update time to make sure it has not already been updated when + the device without the Mobile App is with several devices that left the zone. + ''' + devices_to_update = [_Device + for _Device in Gb.Devices_by_devicename_tracked.values() + if (Device is not _Device + and _Device.is_data_source_MOBAPP is False + and _Device.loc_data_zone == Device.loc_data_zone + and secs_to(_Device.FromZone_Home.next_update_secs) > 60)] + + if devices_to_update == []: + return + + for _Device in devices_to_update: + _Device.icloud_force_update_flag = True + _Device.trigger = 'Check Zone Exit' + _Device.check_zone_exit_secs = time_now_secs() + det_interval.update_all_device_fm_zone_sensors_interval(_Device, 15) + event_msg = f"Trigger > Check Zone Exit, GeneratedBy-{Device.fname}" + post_event(_Device.devicename, event_msg) + + +#------------------------------------------------------------------------------ +@callback +#def _async_add_zone_entity_id(event: EventType[event.EventStateChangedData]) -> None: +def ha_added_zone_entity_id(event: EventType[event.EventStateChangedData]) -> None: + """Add zone entity ID.""" + + zone_entity_id = event.data['entity_id'] + zone = zone_entity_id.replace('zone.', '') + ha_zone_attrs = entity_io.ha_zone_attrs(zone_entity_id) + + try: + if ha_zone_attrs and LATITUDE in ha_zone_attrs: + Zone = iCloud3_Zone(zone) + + if isnot_statzone(zone): + post_event( f"HA Zone Added > Zone-{Zone.dname}/{Zone.zone} " + f"(r{Zone.radius_m}m)") + + except Exception as err: + log_exception(err) + pass + +#------------------------------------------------------------------------------ +@callback +#def _async_remove_zone_entity_id(event: EventType[event.EventStateChangedData]) -> None: +def ha_removed_zone_entity_id(event: EventType[event.EventStateChangedData]) -> None: + """Remove zone entity ID.""" + try: + zone_entity_id = event.data['entity_id'] + zone = zone_entity_id.replace('zone.', '') + + if (zone == HOME + or zone not in Gb.HAZones_by_zone + or Gb.start_icloud3_inprocess_flag + or Gb.restart_icloud3_request_flag): + return + + Zone = Gb.HAZones_by_zone[zone] + + Zone.status = -1 + Gb.HAZones_by_zone_deleted[zone] = Zone + Gb.Zones = list_del(Gb.Zones, Zone) + Gb.HAZones = list_del(Gb.HAZones, Zone) + if zone in Gb.Zones_by_zone: del Gb.Zones_by_zone[zone] + if zone in Gb.HAZones_by_zone: del Gb.HAZones_by_zone[zone] + + # if isnot_statzone(zone): + # if zone in Gb.zones_dname: del Gb.zones_dname[zone] + # if Zone.fname in Gb.zones_dname: del Gb.zones_dname[Zone.fname] + # if Zone.name in Gb.zones_dname: del Gb.zones_dname[Zone.name] + # if Zone.title in Gb.zones_dname: del Gb.zones_dname[Zone.title] + + for Device in Gb. Devices: + Device.remove_zone_from_settings(zone) + + post_event( f"HA Zone Deleted > Zone-{Zone.dname}/{zone}") + + except Exception as err: + log_exception(err) + Gb.restart_icloud3_request_flag = True + post_event( f"Zone Deleted Error > Zone-{Zone.dname}," + f"An error was encountered deleting the zone, " + f"iCloud3 will be restarted") + return diff --git a/custom_components/icloud3/zone.py b/custom_components/icloud3/zone.py index e036639..235bf6d 100644 --- a/custom_components/icloud3/zone.py +++ b/custom_components/icloud3/zone.py @@ -20,7 +20,7 @@ ZONE, TITLE, FNAME, NAME, ID, FRIENDLY_NAME, ICON, LATITUDE, LONGITUDE, RADIUS, PASSIVE, STATZONE_RADIUS_1M, ZONE, NON_ZONE_ITEM_LIST, ) -from .support import mobapp_interface +# from .support import mobapp_interface from .helpers import entity_io from .helpers.common import (instr, is_statzone, format_gps, zone_dname, list_add, list_del, ) @@ -28,7 +28,7 @@ log_exception, log_rawdata,_trace, _traceha, ) from .helpers.time_util import (time_now_secs, datetime_now, secs_to_time, secs_since, secs_to_datetime, format_time_age, ) -from .helpers.dist_util import (calc_distance_m, calc_distance_km, format_dist_km, format_dist_m, ) +from .helpers.dist_util import (calc_distance_m, calc_distance_km, ) MDI_NAME_LETTERS = {'circle-outline': '', 'box-outline': '', 'circle': '', 'box': ''} @@ -178,7 +178,7 @@ def initialize_zone_name(self, zone_data=None): self.dname = self.fname = self.ha_fname elif zone_data: self.dname = self.fname = zone_data.get(FRIENDLY_NAME, self.zone.title()) - elif self.zone in Gb.zone_display_as: + elif self.zone in Gb.zones_dname: self.dname = self.fname = zone_dname(self.zone) else: self.dname = self.fname = self.zone.title() @@ -199,15 +199,15 @@ def setup_zone_display_name(self): self.dname = self.title else: self.dname = self.fname - elif self.zone in Gb.zone_display_as: + elif self.zone in Gb.zones_dname: return self.names = [self.zone, self.dname] - Gb.zone_display_as[self.zone] = self.dname - Gb.zone_display_as[self.fname] = self.dname - Gb.zone_display_as[self.name] = self.dname - Gb.zone_display_as[self.title] = self.dname + Gb.zones_dname[self.zone] = self.dname + Gb.zones_dname[self.fname] = self.dname + Gb.zones_dname[self.name] = self.dname + Gb.zones_dname[self.title] = self.dname self.sensor_prefix = '' if self.zone == HOME else self.dname @@ -289,7 +289,7 @@ def __init__(self, statzone_id): self.removed_from_ha_secs = HIGH_INTEGER self.fname = f"StatZon{self.statzone_id}" - self.fname_id = self.dname = Gb.zone_display_as[self.zone] = self.fname + self.fname_id = self.dname = Gb.zones_dname[self.zone] = self.fname self.initialize_statzone_name() self.initialize_zone_attrs() @@ -310,7 +310,7 @@ def __init__(self, statzone_id): def initialize_statzone_name(self): if Gb.statzone_fname == '': Gb.statzone_fname = 'StatZon#' self.fname = Gb.statzone_fname.replace('#', self.statzone_id) - self.fname_id = self.dname = Gb.zone_display_as[self.zone] = self.fname + self.fname_id = self.dname = Gb.zones_dname[self.zone] = self.fname if instr(Gb.statzone_fname, '#') is False: self.fname_id = f"{self.fname} (..._{self.statzone_id})" diff --git a/info.md b/info.md index 8e04594..7cc6d03 100644 --- a/info.md +++ b/info.md @@ -1,35 +1,84 @@ -## [iCloud3 v3 iDevice Tracker](https://github.com/gcobb321/icloud3_v3) +# iCloud3 v3 -iCloud3 is an advanced iDevice tracker that reports location and other information from your Apple iCloud account and the HA Companion App that can be used for presence detection and zone automation activities. +------ -Some of the features of iCloud3: +### Release Candidate - v3.0.rc10.2 (2/6/2024) -- **Track devices from several sources** - Family members in your iCloud Account Family Sharing list and devices that have installed the HA Companion App (iOS App) are tracked. -- **Actively track a device** - The device will request it's location on a regular interval. -- **Passively monitor a device** - The device does not request it's location but is tracked when another tracked device requests theirs. -- **Waze Route Service** - The travel time and route distance to a tracked zone (Home) is provided by Waze. +------ + +[![CurrentVersion](https://img.shields.io/badge/Current_Version-v3.0-blue.svg)](https://github.com/gcobb321/icloud3_v3) [![Type](https://img.shields.io/badge/Type-Custom_Component-orange.svg)](https://github.com/gcobb321/icloud3_v3) [![HACS](https://img.shields.io/badge/HACS-Custom_Repository-orange.svg)](https://github.com/gcobb321/icloud3_v3) + +[![ProjectStage](https://img.shields.io/badge/Project_Stage-Release_Candidate_10-forestgreen.svg)](https://github/gcobb321/icloud3_v3) [![Released](https://img.shields.io/badge/Released-February,_2024-forestgreen.svg)](https://github.com/gcobb321/icloud3_v3) + + + +iCloud3 is a device tracker custom component that tracks your iPhones, iPads and Apple Watches. iDevices in the Family Sharing List and the HA Mobile App Integration are trackable. The iDevice requests location data from from Apple's iCloud Location Services and monitors various triggers sent from the Home Assistant Mobile App to Home Assistant. Sensors are updated with the device's location, distance from zones, travel time to zones, etc. + +Although AirPods and AirTags are in the iCloud Family Sharing list, they can not be tracked. They do not have the internal components to provide location data using cell towers and gps location information to Apple. + +### iCloud3 v3 Highlights + +Although Home Assistant has it's own official iCloud component, iCloud3 goes far beyond it's capabilities. The important highlights include: + +- **HA Integration** - iCloud3 is a Home Assistant custom integration that is set up and configured from the *HA Settings > Devices & Services > Integrations* screen. +- **Configuration Settings** - Configuration parameters are updated online using various screens and take effect immediately without restarting HA. +- **Track iPhones, iPads and Apple Watches** - Track or monitor your iDevices. +- **Location data sources** - Location data comes from the iCloud Account and the HA Companion App (Mobile App). +- **Actively track a device** - The device will request it's location on a regular interval based on its distance from Home or another zone. +- **Passively monitor a device** - The device does not request it's location. It is updated when another tracked device requests theirs. +- **Waze Route Service** - The travel time and distance to Home or another tracked zone is provided by Waze. +- **Waze Route Service History Database** - The travel time and distance data from Waze is saved to a local database and reused when the device is in a previous location. +- **Track from multiple zones** - Tracking results (location, travel time, distance, arrival time, etc.) are reported from the Home zone or another zone (office, second home, parents, etc.). +- **Primary Home Zone** - Set another zone as the primary zone for the device and report tracking results based on that location. This is useful when you have two homes, on a vacation at another location, triggering automations at your parents house with true devices, etc. +- **Improved GPS accuracy** - GPS wandering errors leading to incorrect zone exits are eliminated. +- **Monitors Mobile App activity** - Looks for location and trigger changes every 5-seconds. +- **Enter Zone delay** - Delay processing Zone Enter triggers in case you are just driving through it. - **Stationary Zone** - A dynamic *Stationary Zone* is created when the device has not moved for a while (doctors office, store, friend's house). This helps conserve battery life. -- **Sensors and more sensors** - Many sensors are created and updated with distance, travel time, polling data, battery status, zone attributes, etc. The sensors that are created is customizable. -- **Nearby Devices** - The location of all devices is monitored and the distance between devices is determined. Information from devices close to each other is shared. -- **Event Log** - The current status and event history of every tracked and monitored device is displayed on the iCloud3 Event Log custom Lovelace card. It shows information about devices that can be tracked, errors and alerts, nearby devices, tracking results, debug information and location request results. +- **Nearby devices** - The distance to other devices is displayed and used to determine tracking results. +- **Zone monitoring** - The number of devices in each zone is displayed when a device is updated. +- **Local Time Zone** - Event times are normally displayed using the time zone your HA server is in. If, hoowever, you are away from home and in another time zone can, the Event times can be displayed for the time zone you are in. +- **Zone Activity Log** - A log can be kept for each time you are in a zone. This log file (.csv format) can be imported into a spreadsheet program and used for expense reporting, travel history, device location monitoring, etc. +- **Sensors and more sensors** - Many sensors are created and updated with distance, travel time, polling data, battery status, zone attributes, etc. Select only the ones you want to use. +- **Battery status** - Updates the battery level and status (charging/not charging) from iCloud data during a tracking event and from the Mobile App every 5-seconds. +- **Distance Sensor Attributes** - Shows the distance to the center and edge of the Home zone, distance to other zones and distance to other devices. +- **Event Log** - The current status and event history of every tracked and monitored device is displayed on the iCloud3 Event Log custom Lovelace card. Information about device configuration, errors and alerts, nearby devices, tracking results, debug information and location request results is displayed. +- **Updating and Restarting** - iCloud3 can be restarted without restarting Home Assistant. +- **Restore state values on restart** - The current device_tracker and sensor entity states are restored on a restart. The attributes are not restored but are reset on the first tracking Event. +- **Device_tracker and sensor entities** - iCloud3 devices and sensors are Home Assistant entities that are added, deleted and changed on the *Update iCloud3 Devices* and *Sensors* configuration screens. +- **Zone Exits for devices not using the Mobile App** - Devices that do not or can not (Apple Watch) use the Mobile App respond to a zone exit when it detects another nearby device has left a zone. +- **Extensive Documentation** - The iCloud3 User Guide explains the three main components, hot to get started, how to migrate from v2, how to install the integration, each of the screens and special features, the service calls that can request updates, locate iPhones and send notification alerts, examples of how to automate opening your garage door when you arrive home, etc. +- **And More** - Review the following documentation to see if it will help you track and monitor the locations of your iPhones, iPads and Apple Watches. + +### Tracking Information Screen with Event Log + +The screens below are an example of how the many tracking sensors can be displayed. The screen on the left shows the current tracking formation for Gary while the Event Log on the right shows a history of important tracking events. + +![evlimg](https://gcobb321.github.io/icloud3_v3_docs/images/track-evlog-gary-tfz-lillian-home.png) + +### iCloud3 Documentation -### Installing iCloud3 +- Introduces the many features and components of iCloud3 +- Describes how to migration from v2.4.7 to v3.0 +- Provides step-by-step to install and configure iCloud3, it's components and it's supporting components (iCloud Account and the Mobile App) +- Highlights the configuration screens and parameters +- Provides example screens, automations and scripts +- The User Guide is [here](https://gcobb321.github.io/icloud3_v3_docs/#/) -iCloud3 is still in beta and has not been added to the HACS base integration list. It needs to be added to HACS as a custom repository and then downloaded. +### Installing or Upgrading to iCloud3 v3 -1. Open HACS. +- iCloud3 v3 is now available on the iCloud3 HACS base as a prerelease version. -2. Select **Integrations**, then select the 3-dots (**︙**) in the upper-right corner, then select **Custom Repositories**. +### Important Links -3. Type **gcobb321/icloud3_v3** in the Repository field, then select **Integration** in the Category dropdown list, then select **Add**. +- **iCloud3 v3 User Guide** -The User Guide is quite extensive and can be found [here](https://gcobb321.github.io/icloud3_v3_docs/#/) +- **iCloud3 v3 GitHub Repository (Prerelease Version)** - The primary GitHub Repository is [here](https://github.com/gcobb321/icloud3) +- **iCloud3 v3 Development GitHub Repository** - The Development Repository is used for beta version changes that have not been released yet is [here](https://github.com/gcobb321_v3) +- **Installing as a New Installation** - iCloud3 v3 is available in HACS as a prerelease version. Installation instructions are [here](https://gcobb321.github.io/icloud3_v3_docs/#/chapters/3.2-installing-and-configuring) +- **Migrating from v2.4.x** - This includes installing iCloud3 v3, migrating your current configuration and reviewing it to insure it was migrated correctly. Instructions are [here](https://gcobb321.github.io/icloud3_v3_docs/#/chapters/3.1-migrating-v2-to-v3) -4. Select **Integrations** again, then select **+ Explore & Download Repositories**. -5. Select *iCloud3 v3 iDevice Tracker*, then select **Download**. -### Useful Links -* [Brief Overview](https://github.com/gcobb321/icloud3_v3/blob/master/README.md) -* [Extensive Documentation](https://gcobb321.github.io/icloud3_v3/#/) -* [GitHub Repository](https://github.com/gcobb321/icloud3_v3) +----- +*Gary Cobb, aka GeeksterGary*