Skip to content

Add background monitoring thread to Find My iPhone service with device property enhancements#175

Merged
timlaing merged 4 commits intomainfrom
bugfix/apple-email
Jan 10, 2026
Merged

Add background monitoring thread to Find My iPhone service with device property enhancements#175
timlaing merged 4 commits intomainfrom
bugfix/apple-email

Conversation

@timlaing
Copy link
Copy Markdown
Owner

@timlaing timlaing commented Jan 10, 2026

Proposed change

Bugfix for regular apple emails.

This has been resolved using a background thread as a keep-alive in the same why this iCloud UI works.
additional changes have added extra checks for capabilities like location, erase, sound, before issuing these commands.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New service (thank you!)
  • New feature (which adds functionality to an existing service)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests
  • Documentation or code sample

Example of code:

Additional information

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

  • Documentation added/updated to README

…vice properties

- Introduced a background thread to periodically refresh device location data.
- Updated `_refresh_client_with_reauth` to accept a `locate` parameter.
- Added properties to `AppleDevice` for feature availability checks.
- Refactored tests to cover new functionality and ensure proper behavior.
@timlaing timlaing self-assigned this Jan 10, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 10, 2026

📝 Walkthrough

Walkthrough

Adds a background monitor thread and related refresh/reauth logic to the Find My iPhone service, extends AppleDevice with capability and metadata properties, updates device access to auto-refresh when manager is not alive, removes one VS Code extension from devcontainer, and adds test fixtures and extensive tests for the new monitoring and refresh behaviour.

Changes

Cohort / File(s) Summary
DevContainer Configuration
\.devcontainer/devcontainer.json``
Removed the visualstudioexptteam.vscodeintellicode extension from the extensions array.
Find My iPhone service
\pyicloud/services/findmyiphone.py``
Added _monitor_thread background loop; introduced _refresh_client_with_reauth(retry: bool = False, locate: bool = True), public refresh(locate: bool = False), is_alive property, and monitor thread management. Extended _refresh_client payload with locate/serverContext and updated family-device refresh flow. Expanded AppleDevice API with metadata and capability properties (name, model, model_name, device_type, lost_mode_available, messaging_available, sound_available, erase_available, location_available), updated string representations, and added capability gating and extended display_message parameters. Device accessors now trigger manager refresh when not alive.
Tests — fixtures
\tests/conftest.py``
Mocked PyiCloudService._setup_cookie_directory in pyicloud_service fixture and added an autouse session-scoped mock_thread fixture to patch threading.Thread to avoid spawning real threads during tests.
Tests — findmyiphone
\tests/services/test_findmyiphone.py``
Added tests covering _monitor_thread behaviour (interval invocation, locate propagation, exception resilience), re-auth/refresh flows, is_alive interactions, device access paths (__getitem__, __iter__, __getattr__), and capability-gated actions (play_sound, display_message, lost_device).
Tests — CLI
\tests/test_cmdline.py``
Widened test data type for device content to accept features list and added a LOC feature entry in test_list_devices_option_short_list.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Manager as FindMyiPhoneServiceManager
    participant Monitor as _monitor_thread
    participant Refresh as _refresh_client_with_reauth
    participant Devices

    Client->>Manager: instantiate / access devices
    Manager->>Refresh: initial _refresh_client_with_reauth(locate=True)
    Manager->>Monitor: start daemon thread (interval, locate)

    loop every interval
        Monitor->>Refresh: call refresh(locate=...)
        Refresh->>Devices: fetch device list (+ locate/context)
        Devices-->>Refresh: return device data
        Refresh->>Manager: update cached devices
    end

    Client->>Manager: access device (getitem/iter/attr)
    alt Manager not alive
        Manager->>Refresh: trigger _refresh_client_with_reauth(locate=?)
        Refresh->>Devices: fetch latest state
        Refresh-->>Manager: updated cache
    else Manager alive
        Manager-->>Client: return cached devices
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Feature/erase-device #104 — Overlaps changes to pyicloud/services/findmyiphone.py, particularly around AppleDevice capability properties and manager methods.
  • Improve-test-coverage #71 — Related to AppleDevice attribute-access and manager/device access patterns; touches similar code paths.
  • Bugfix/fetch-devices-2 #135 — Related to refresh logic and locate-flag/polling behaviour in FindMyiPhone refresh flow.

Poem

🐰 A soft thread hums through the night,
Polling devices, keeping state right.
With beeps and messages and locate in sight,
I hop and refresh — everything’s bright! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the main changes: adding a background monitoring thread to Find My iPhone service and enhancing device properties.
Linked Issues check ✅ Passed The changes implement background monitoring and re-authentication logic that addresses the root cause of repeated sign-in notifications by preventing unnecessary authentication cycles [#154].
Out of Scope Changes check ✅ Passed All changes are directly scoped to addressing issue #154: monitoring thread implementation, device property enhancements, re-authentication logic, and comprehensive test coverage.
Docstring Coverage ✅ Passed Docstring coverage is 98.11% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed The pull request description clearly relates to the changeset, describing a bugfix for Apple emails using a background thread keep-alive mechanism and capability checks.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bugfix/apple-email

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @pyicloud/services/findmyiphone.py:
- Around line 480-487: The location_available property mixes self.data (which
may trigger a refresh) and self._content (which is a direct, potentially stale
attribute); change the checks to use self.data consistently (e.g., replace uses
of self._content["location"] and "location" in self._content with lookups
against self.data) so the availability check always uses refreshed data,
referencing the property name location_available to locate the code to update.
🧹 Nitpick comments (5)
tests/conftest.py (1)

215-219: Session-scoped autouse fixture may have broad unintended effects.

This fixture applies to all tests in the session, which could interfere with tests that legitimately require threading. Additionally, without explicit configuration, mock_thread_class.return_value.is_alive.return_value will default to a MagicMock (truthy), which may not match the expected behaviour in production code that checks is_alive().

Consider:

  1. Using scope="function" or applying only to specific test modules/classes.
  2. Configuring default return values if specific behaviour is expected.
🔧 Suggested improvement
 @pytest.fixture(autouse=True, scope="session")
 def mock_thread():
     """Mock threading.Thread to prevent actual thread creation during tests."""
     with patch("threading.Thread") as mock_thread_class:
+        # Configure sensible defaults
+        mock_thread_class.return_value.is_alive.return_value = False
         yield mock_thread_class
tests/services/test_findmyiphone.py (1)

687-816: Test name may be misleading; consider refactoring for clarity.

The test is named test_refresh_client_with_reauth_with_loading_no_complete, but member1 eventually transitions to "deviceFetchStatus": "DONE" at lines 788-795. This makes the test name potentially confusing.

Additionally, the extensive inline mock data is quite verbose and duplicative. Consider extracting the member data into helper functions or constants to improve readability.

pyicloud/services/findmyiphone.py (3)

125-136: Consider making the refresh interval configurable.

The monitor thread interval is hardcoded to 1.0 second (line 130). This could be quite aggressive for API rate limits, especially if multiple devices or services are active. Consider:

  1. Making this configurable via a class/instance attribute or constructor parameter.
  2. Documenting the rate limit implications.
🔧 Suggested improvement
+_DEFAULT_MONITOR_INTERVAL: float = 1.0

 class FindMyiPhoneServiceManager(BaseService):
     # ...
     def __init__(
         self,
         service_root: str,
         token_endpoint: str,
         session: PyiCloudSession,
         params: dict[str, Any],
         with_family=False,
+        monitor_interval: float = _DEFAULT_MONITOR_INTERVAL,
     ) -> None:
         # ...
+        self._monitor_interval: float = monitor_interval
         # ...

     def _refresh_client_with_reauth(self, ...):
         # ...
         if not self.is_alive:
             self._monitor = threading.Thread(
                 target=_monitor_thread,
                 kwargs={
                     "func": self._refresh_client,
-                    "interval": 1.0,
+                    "interval": self._monitor_interval,
                     "stop_event": self.stop_event,
                 },
                 daemon=True,
             )

230-234: Consider using locate=False for __len__.

The __len__ method currently triggers a full location refresh (locate=True) when the monitor is not alive. If the caller only wants to check the device count (e.g., if len(manager) > 0), triggering location updates is unexpected and potentially costly. Consider using locate=False for this accessor.

🔧 Suggested improvement
     def __len__(self) -> int:
         """Returns the number of devices."""
         if not self.is_alive:
-            self._refresh_client_with_reauth(locate=True)
+            self._refresh_client_with_reauth(locate=False)
         return len(self._devices)

70-71: Consider adding a public stop() method for graceful cleanup.

While the monitor thread is daemonized and will be killed on process exit, library consumers may want to explicitly stop the background refresh (e.g., in long-running applications or when switching accounts). Currently, they would need to access stop_event directly. A public stop() method would provide a cleaner API.

🔧 Suggested addition
def stop(self) -> None:
    """Stop the background monitor thread."""
    self.stop_event.set()
    if self._monitor is not None:
        self._monitor.join(timeout=2.0)
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cfb15f8 and 9a7bf5c.

📒 Files selected for processing (5)
  • .devcontainer/devcontainer.json
  • pyicloud/services/findmyiphone.py
  • tests/conftest.py
  • tests/services/test_findmyiphone.py
  • tests/test_cmdline.py
💤 Files with no reviewable changes (1)
  • .devcontainer/devcontainer.json
🧰 Additional context used
🧬 Code graph analysis (2)
tests/services/test_findmyiphone.py (1)
pyicloud/services/findmyiphone.py (7)
  • AppleDevice (252-495)
  • _monitor_thread (25-35)
  • location (287-291)
  • data (415-420)
  • play_sound (312-325)
  • display_message (327-354)
  • lost_device (356-383)
pyicloud/services/findmyiphone.py (1)
pyicloud/exceptions.py (2)
  • PyiCloudAuthRequiredException (89-95)
  • PyiCloudServiceUnavailable (16-17)
🔇 Additional comments (21)
tests/test_cmdline.py (1)

383-383: The test assertions are correct and complete for short list mode.

The implementation of _list_devices_option() (pyicloud/cmdline.py, lines 450–474) deliberately excludes the features field when in short list mode (command_line.list = True). The short list outputs only essential device information: Name, Display Name, Location, Battery Level, Battery Status, Device Class, and Device Model.

The test data includes the features field to ensure the device object is well-formed, but the test assertions correctly do not verify it in short list output because it is not printed. This is intentional design—long list mode (command_line.longlist = True) prints all keys, whereas short list mode provides a filtered view.

No changes to the test assertions are required.

tests/conftest.py (1)

118-125: LGTM!

The addition of the _setup_cookie_directory mock alongside the existing authenticate mock is appropriate for preventing filesystem access during test initialisation. The return value /tmp/pyicloud/cookies is a reasonable placeholder.

tests/services/test_findmyiphone.py (9)

5-21: LGTM!

The updated imports appropriately support the new test coverage for the monitor thread functionality and device property testing.


111-116: LGTM!

Good test coverage for the location_available property guard on the location accessor.


133-150: LGTM!

Good test coverage for the lazy refresh behaviour when accessing the data property with a stopped monitor thread.


152-168: LGTM!

Good test coverage for the __getitem__ method's conditional refresh logic based on is_alive state.


170-186: LGTM!

Good test coverage for __getattr__ behaviour, including the AttributeError for non-existent attributes and conditional refresh.


195-276: LGTM!

Comprehensive test coverage for feature-gated device actions (play_sound, display_message, lost_device). The tests correctly verify that PyiCloudServiceUnavailable is raised when features are disabled and that the correct payloads are sent when enabled.


279-341: LGTM!

Good test coverage for FindMyiPhoneServiceManager methods including __getitem__, __iter__, and the is_alive integration for conditional refresh behaviour.


832-934: LGTM!

Comprehensive test coverage for the _monitor_thread function, including interval-based invocation, locate parameter propagation, exception handling, and multiple interval calls. The datetime mocking approach is well-structured.


526-546: LGTM!

Good test coverage for the erase_available feature guard on erase_device.

pyicloud/services/findmyiphone.py (10)

25-35: LGTM! Background thread implementation is sound.

The monitor thread implementation correctly uses stop_event.wait(timeout=interval) for interruptible sleeping and catches all exceptions to prevent thread crashes. The timing logic is appropriate for periodic refresh.


202-204: LGTM!

Clean public API wrapper. The default locate=False is sensible for public use, distinguishing it from the internal method's default of locate=True.


236-239: LGTM!

Correct implementation for checking if the background monitor thread is running.


286-291: LGTM!

The location_available guard prevents accessing invalid location data. The is False pattern is consistent with similar checks elsewhere in this file.


312-325: LGTM!

Good feature availability guard for play_sound.


327-354: LGTM!

Good addition of vibrate and strobe parameters, and appropriate feature availability guard for messaging.


356-383: LGTM!

Good feature availability guard for lost_device.


396-412: LGTM!

Good feature availability guard for erase_device.


414-438: LGTM!

Consistent lazy refresh pattern across data, __getitem__, and __getattr__ accessors. Using refresh() (which defaults to locate=False) is appropriate for attribute access.


489-495: LGTM!

Clean string representations using the new model_name and name properties.

Comment thread pyicloud/services/findmyiphone.py
@sonarqubecloud
Copy link
Copy Markdown

@timlaing timlaing changed the title Bugfix/apple email Add background monitoring thread to Find My iPhone service with device property enhancements Jan 10, 2026
@timlaing timlaing marked this pull request as ready for review January 10, 2026 19:24
Copilot AI review requested due to automatic review settings January 10, 2026 19:24
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pyicloud/services/findmyiphone.py (1)

45-73: Add resource cleanup mechanism for the background thread.

The background monitor thread is started during initialisation (via line 73 → _refresh_client_with_reauth → _start_monitor_thread) but there's no public method to stop it. Whilst the thread is marked as daemon (line 142), this approach can lead to resource leaks or ungraceful shutdowns.

Consider adding a public close() or stop() method that sets self.stop_event and joins the thread, or implement the context manager protocol (__enter__/__exit__).

🤖 Fix all issues with AI agents
In @pyicloud/services/findmyiphone.py:
- Around line 132-145: The monitor thread interval is hardcoded to 1.0s in
_start_monitor_thread which causes excessive API calls; add a configurable
interval parameter to the class constructor (e.g., interval: float = 300.0) and
store it (self._monitor_interval), update _start_monitor_thread to use
self._monitor_interval instead of 1.0, keep type hints (float) and default to a
much larger value like 300.0 (5 minutes), and ensure existing uses of
_refresh_client, stop_event and self._monitor remain unchanged.
- Around line 25-36: The timing logic in _monitor_thread currently waits for
interval then compares next_event causing roughly a double delay; remove the
next_event calculation and the if next_event check so the loop simply does while
not stop_event.wait(timeout=interval): try: func(locate) except Exception as
exc: _LOGGER.debug(...) — i.e., call func(locate) immediately after each wait to
enforce a fixed polling interval, and delete or stop using the next_event
variable.
🧹 Nitpick comments (3)
pyicloud/services/findmyiphone.py (3)

215-243: Consider using locate=False for read-only fallback refreshes.

Lines 217-218, 235-236, and 241-242 all trigger _refresh_client_with_reauth(locate=True) when the monitor isn't alive. These auto-refresh calls serve as fallbacks when the background thread has stopped, which is sensible. However, using locate=True for simple data access operations (getting, iterating, or counting devices) may be unnecessarily expensive.

Consider using locate=False for these fallback refreshes, reserving location updates for explicit calls or the background monitor.

♻️ Proposed change
     def __getitem__(self, key: str | int) -> "AppleDevice":
         """Gets a device by name or index."""
         if not self.is_alive:
-            self._refresh_client_with_reauth(locate=True)
+            self._refresh_client_with_reauth(locate=False)
 
         if isinstance(key, int):
             key = self._devices_names[key]
         return self._devices[key]
 
     # ... (similar changes for __iter__ and __len__)
 
     def __iter__(self) -> Iterator["AppleDevice"]:
         """Iterates over the devices."""
 
         if not self.is_alive:
-            self._refresh_client_with_reauth(locate=True)
+            self._refresh_client_with_reauth(locate=False)
         return iter(self._devices.values())
 
     def __len__(self) -> int:
         """Returns the number of devices."""
         if not self.is_alive:
-            self._refresh_client_with_reauth(locate=True)
+            self._refresh_client_with_reauth(locate=False)
         return len(self._devices)

245-248: Document or reconsider the behaviour when the monitor thread stops.

If the monitor thread terminates unexpectedly (e.g., due to repeated exceptions), is_alive will return False, which triggers fallback refreshes on every device access (lines 217, 235, 241). However, nothing automatically restarts the monitor thread. This means:

  1. If the monitor dies, it stays dead
  2. Every subsequent device access will trigger a full refresh

Consider either:

  • Documenting this behaviour clearly, or
  • Auto-restarting the monitor thread in the fallback refresh paths if it has stopped

449-496: Consider accessing _content directly in capability properties.

Lines 449-496 define several capability properties (e.g., name, lost_mode_available, messaging_available) that all access self.data, which triggers the is_alive check and potential refresh on lines 426-427. Whilst the check is efficient when the monitor is alive, capability properties are typically static metadata that don't require real-time updates.

For better performance and to avoid unnecessary liveness checks when accessing device capabilities, consider using self._content directly in these readonly property getters.

♻️ Proposed refactor
     @property
     def name(self) -> str:
         """Gets the device name."""
-        return self.data.get("name", "")
+        return self._content.get("name", "")
 
     @property
     def model(self) -> str:
         """Gets the device model."""
-        return self.data.get("deviceModel", "")
+        return self._content.get("deviceModel", "")
 
     # ... (similar changes for other capability properties)
 
     @property
     def lost_mode_available(self) -> bool:
         """Indicates if lost mode is available for the device."""
-        return self.data.get("lostModeCapable", False)
+        return self._content.get("lostModeCapable", False)

Note: Keep the is_alive check in the data property for dynamic properties like location.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a7bf5c and 0781d75.

📒 Files selected for processing (1)
  • pyicloud/services/findmyiphone.py
🧰 Additional context used
🧬 Code graph analysis (1)
pyicloud/services/findmyiphone.py (2)
pyicloud/exceptions.py (2)
  • PyiCloudAuthRequiredException (89-95)
  • PyiCloudServiceUnavailable (16-17)
pyicloud/services/base.py (1)
  • session (20-22)

Comment thread pyicloud/services/findmyiphone.py
Comment thread pyicloud/services/findmyiphone.py
Comment thread pyicloud/services/findmyiphone.py
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 10, 2026

✅ Actions performed

Comments resolved and changes approved.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a background monitoring thread to the Find My iPhone service to maintain an active connection with the iCloud API, addressing issues with regular Apple email accounts. The implementation includes capability checks for device actions and adds several new device properties.

Changes:

  • Added a background monitoring thread that periodically refreshes device data to keep the session alive
  • Implemented device capability checking before executing actions (sound, message, location, erase)
  • Added new device properties (name, model, model_name, device_type, and availability checks for various features)

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
pyicloud/services/findmyiphone.py Main implementation of background monitor thread, device initialization refactoring, and new device properties with capability checks
tests/services/test_findmyiphone.py Comprehensive test coverage for monitor thread functionality, capability checks, and property access with refresh logic
tests/test_cmdline.py Updated type annotations for device content to include features field
tests/conftest.py Added session-scoped fixture to mock threading.Thread and cookie directory setup
.devcontainer/devcontainer.json Removed deprecated VS Code extension

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

)
self.stop_event.clear()
self._monitor.start()

Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FindMyiPhoneServiceManager starts a daemon background thread but doesn't provide a cleanup method to properly stop the thread when the service is no longer needed. While daemon threads will terminate when the main program exits, it's better practice to provide an explicit cleanup method (e.g., close() or shutdown()) that calls self.stop_event.set() to gracefully stop the monitor thread. This is especially important for long-running applications or testing scenarios where the service may be created and destroyed multiple times.

Suggested change
def shutdown(self) -> None:
"""Stops the monitor thread for the FindMyiPhoneServiceManager."""
# Signal the monitor thread to stop.
self.stop_event.set()
# If the monitor thread was started and is still running, wait for it
# to finish. Using getattr avoids errors if _monitor is unset.
monitor = getattr(self, "_monitor", None)
if monitor is not None and monitor.is_alive():
monitor.join(timeout=5.0)

Copilot uses AI. Check for mistakes.
strobe=False,
) -> None:
"""Send a request to the device to play a sound.
"""Send a request to the device to a display a message.
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring contains a grammatical error: "Send a request to the device to a display a message" should be "Send a request to the device to display a message" (remove "a" before "display").

Suggested change
"""Send a request to the device to a display a message.
"""Send a request to the device to display a message.

Copilot uses AI. Check for mistakes.
Comment on lines 356 to +360
"userText": True,
"text": message,
"sound": sounds,
"vibrate": vibrate,
"strobe": strobe,
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The order of dictionary keys in the display_message method's data payload doesn't match the order expected in the test. The implementation has the order: device, subject, userText, text, sound, vibrate, strobe. However, the test expects: device, subject, sound, vibrate, strobe, userText, text. While dictionary key order shouldn't matter for functionality in Python 3.7+, it's better practice to keep the test and implementation in sync to avoid confusion. Consider updating either the implementation or the test to maintain consistent ordering.

Suggested change
"userText": True,
"text": message,
"sound": sounds,
"vibrate": vibrate,
"strobe": strobe,
"sound": sounds,
"vibrate": vibrate,
"strobe": strobe,
"userText": True,
"text": message,

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +35
next_event: datetime = datetime.now() + timedelta(seconds=interval)
while not stop_event.wait(timeout=interval):
if next_event < datetime.now():
try:
func(locate)
except Exception as exc:
_LOGGER.debug("FindMyiPhone monitor thread error: %s", exc)
next_event = datetime.now() + timedelta(seconds=interval)
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The monitor thread implementation has a timing logic issue. The stop_event.wait(timeout=interval) already waits for the interval duration, but then the code checks if next_event < datetime.now() which may not trigger as expected. The wait() method will return immediately if the event is set, or after interval seconds. However, the next_event is only updated after a successful function call, which means if the function is never called, next_event remains in the past indefinitely.

A simpler and more correct implementation would be to either:

  1. Remove the next_event logic entirely and rely on the wait(timeout=interval), or
  2. Use wait(timeout=0.1) or similar short timeout and check the next_event timing properly
Suggested change
next_event: datetime = datetime.now() + timedelta(seconds=interval)
while not stop_event.wait(timeout=interval):
if next_event < datetime.now():
try:
func(locate)
except Exception as exc:
_LOGGER.debug("FindMyiPhone monitor thread error: %s", exc)
next_event = datetime.now() + timedelta(seconds=interval)
while not stop_event.wait(timeout=interval):
try:
func(locate)
except Exception as exc:
_LOGGER.debug("FindMyiPhone monitor thread error: %s", exc)

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +98
def _refresh_client_with_reauth(
self, retry: bool = False, locate: bool = True
) -> None:
"""
Refreshes the FindMyiPhoneService endpoint with re-authentication.
This ensures that the location data is up-to-date.
"""
# Refresh the client (own devices first)
self.stop_event.set()
try:
self._refresh_client(locate=True)
self._refresh_client(locate=locate)
except PyiCloudAuthRequiredException:
if retry is True:
raise

_LOGGER.debug("Re-authenticating session")
self._server_ctx = None
self.session.service.authenticate(force_refresh=True)
self.refresh_client_with_reauth(retry=True)
self._refresh_client_with_reauth(retry=True, locate=locate)
return

# Initialize devices (including family devices if enabled)
self._initialize_devices(locate=locate)
self._start_monitor_thread()
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _refresh_client_with_reauth method calls self.stop_event.set() at the beginning, which will stop any running monitor thread. However, if this method is called during normal operation (e.g., when is_alive returns False), it will unnecessarily stop and restart the monitor thread. This could lead to unnecessary thread churn. Consider checking if the thread is already alive before stopping it, or only stopping it when actually needed (e.g., during re-authentication).

Copilot uses AI. Check for mistakes.
Comment on lines 295 to 300
@property
def location(self) -> Optional[dict[str, Any]]:
"""Updates the device location."""
self._manager.refresh_client_with_reauth()
if self.location_available is False:
return None
return self._content["location"]
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The location property no longer calls self._manager.refresh_client_with_reauth() before returning the location data. While this may be intentional (to rely on the background monitor thread), it means that if the monitor thread is not running or has failed, the location data could be stale. The old implementation ensured fresh data on every access. Consider documenting this behavior change or adding a staleness check.

Copilot uses AI. Check for mistakes.
"""Returns status information for device.

This returns only a subset of possible properties.
"""
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The status() method no longer calls self._manager.refresh_client_with_reauth() before returning device status. This is a breaking change in behavior - the old implementation guaranteed fresh data, while the new implementation relies on the background monitor thread. If the monitor thread has stopped or failed, the status data could be stale. This should be documented, or the refresh call should be restored to maintain the original API contract.

Suggested change
"""
"""
# Ensure we return fresh data by refreshing the client before
# reading from the cached device content. This restores the
# original API contract that status() always reflects the
# latest server state, independent of the monitor thread.
self._manager._refresh_client_with_reauth(locate=False)

Copilot uses AI. Check for mistakes.
Comment on lines +457 to +495
return self.data.get("deviceModel", "")

@property
def model_name(self) -> str:
"""Gets the device model name."""
return self.data.get("deviceDisplayName", "")

@property
def device_type(self) -> str:
"""Gets the device type."""
return self.data.get("deviceClass", "")

@property
def lost_mode_available(self) -> bool:
"""Indicates if lost mode is available for the device."""
return self.data.get("lostModeCapable", False)

@property
def messaging_available(self) -> bool:
"""Indicates if messaging is available for the device."""
return self.data.get("features", {}).get("MSG", False)

@property
def sound_available(self) -> bool:
"""Indicates if sound is available for the device."""
return self.data.get("features", {}).get("SND", False)

@property
def erase_available(self) -> bool:
"""Indicates if erase is available for the device."""
return self.data.get("features", {}).get("WIP", False)

@property
def location_available(self) -> bool:
"""Indicates if location is available for the device."""
return (
self.data.get("features", {}).get("LOC", False)
and "location" in self._content
and self._content["location"] is not None
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new property methods (name, model, model_name, device_type, lost_mode_available, messaging_available, sound_available, erase_available, location_available) all call self.data, which triggers a refresh check via self._manager.is_alive and potentially calls self._manager.refresh(). This creates a potential infinite recursion or circular dependency issue because location_available property calls self.data which may refresh, and the refresh logic could potentially trigger property access again. Additionally, this means that checking simple availability properties like sound_available could trigger a full refresh if the monitor thread isn't alive, which seems excessive. Consider accessing self._content directly in these property methods instead of going through self.data.

Suggested change
return self.data.get("deviceModel", "")
@property
def model_name(self) -> str:
"""Gets the device model name."""
return self.data.get("deviceDisplayName", "")
@property
def device_type(self) -> str:
"""Gets the device type."""
return self.data.get("deviceClass", "")
@property
def lost_mode_available(self) -> bool:
"""Indicates if lost mode is available for the device."""
return self.data.get("lostModeCapable", False)
@property
def messaging_available(self) -> bool:
"""Indicates if messaging is available for the device."""
return self.data.get("features", {}).get("MSG", False)
@property
def sound_available(self) -> bool:
"""Indicates if sound is available for the device."""
return self.data.get("features", {}).get("SND", False)
@property
def erase_available(self) -> bool:
"""Indicates if erase is available for the device."""
return self.data.get("features", {}).get("WIP", False)
@property
def location_available(self) -> bool:
"""Indicates if location is available for the device."""
return (
self.data.get("features", {}).get("LOC", False)
and "location" in self._content
and self._content["location"] is not None
content = self._content or {}
return content.get("deviceModel", "")
@property
def model_name(self) -> str:
"""Gets the device model name."""
content = self._content or {}
return content.get("deviceDisplayName", "")
@property
def device_type(self) -> str:
"""Gets the device type."""
content = self._content or {}
return content.get("deviceClass", "")
@property
def lost_mode_available(self) -> bool:
"""Indicates if lost mode is available for the device."""
content = self._content or {}
return content.get("lostModeCapable", False)
@property
def messaging_available(self) -> bool:
"""Indicates if messaging is available for the device."""
content = self._content or {}
features = content.get("features") or {}
return features.get("MSG", False)
@property
def sound_available(self) -> bool:
"""Indicates if sound is available for the device."""
content = self._content or {}
features = content.get("features") or {}
return features.get("SND", False)
@property
def erase_available(self) -> bool:
"""Indicates if erase is available for the device."""
content = self._content or {}
features = content.get("features") or {}
return features.get("WIP", False)
@property
def location_available(self) -> bool:
"""Indicates if location is available for the device."""
content = self._content or {}
features = content.get("features") or {}
return (
features.get("LOC", False)
and "location" in content
and content["location"] is not None

Copilot uses AI. Check for mistakes.
Comment thread tests/test_cmdline.py
Comment on lines +383 to +393
content: dict[str, str | list[dict[str, bool]]] = {
"name": "Test Device",
"deviceDisplayName": "Test Display",
"location": "Test Location",
"batteryLevel": "100%",
"batteryStatus": "Charging",
"deviceClass": "Phone",
"deviceModel": "iPhone",
"features": [
{"LOC": True},
],
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type annotation for the content dictionary is incorrect. The features field should be typed as dict[str, bool] rather than list[dict[str, bool]]. Looking at the actual device data structure used throughout the codebase (e.g., in const_findmyiphone.py), features is always a dictionary mapping feature codes to boolean values, not a list of dictionaries.

Copilot uses AI. Check for mistakes.
raise PyiCloudNoDevicesException()

_LOGGER.debug("Number of devices found: %d", len(self._devices))
_LOGGER.info("Number of devices found: %d", len(self._devices))
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logging level for "Number of devices found" was changed from debug to info. This could be noisy for users as this message will now appear in regular logs. Consider whether this is intentional or if debug level is more appropriate for this internal operation detail.

Suggested change
_LOGGER.info("Number of devices found: %d", len(self._devices))
_LOGGER.debug("Number of devices found: %d", len(self._devices))

Copilot uses AI. Check for mistakes.
@timlaing timlaing merged commit 740a4f1 into main Jan 10, 2026
26 checks passed
@timlaing timlaing deleted the bugfix/apple-email branch January 10, 2026 19:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Frequent "Your Apple Account was used to sign in to iCloud via web browser"

2 participants