Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
139 commits
Select commit Hold shift + click to select a range
edc1a4f
fix: use separate descriptor pool for protobuf files to avoid duplica…
claude Jan 24, 2026
c57691a
Merge pull request #985 from jleinenbach/claude/fix-upgrade-bug-ugK8s
jleinenbach Jan 24, 2026
73bd71b
fix: add missing 'accuracy_sanitized_count' stat to coordinator initi…
claude Jan 26, 2026
ddac6e4
Merge pull request #986 from jleinenbach/claude/fix-tracker-location-…
jleinenbach Jan 26, 2026
be2bb6c
fix: improve InvalidTag error handling in decrypt_locations.py
claude Jan 26, 2026
ca06c07
Merge pull request #987 from jleinenbach/claude/protobuf-cleanup-revi…
jleinenbach Jan 26, 2026
b111751
feat: add stale_threshold_enabled toggle to disable staleness checking
claude Jan 28, 2026
dda8e29
fix: increase default stale threshold from 30 min to 2 hours
claude Jan 28, 2026
53b7d57
fix: align translation key order with English reference
claude Jan 28, 2026
a7dae5f
fix: properly close unawaited coroutines in eid_resolver.py
claude Jan 28, 2026
92142bb
Merge pull request #988 from jleinenbach/claude/investigate-settings-…
jleinenbach Jan 28, 2026
48d0156
test: add regression tests for protobuf descriptor pool namespace con…
claude Jan 29, 2026
96dab8b
fix: remove redundant vendored Any_pb2 – use official google.protobuf…
claude Jan 29, 2026
51a8bdf
fix: prefer official googleapis-common-protos for google.rpc.Status
claude Jan 29, 2026
94241b5
test: add 100% coverage tests for standalone CLI main.py
claude Jan 29, 2026
37a75f7
docs: document protobuf namespace rules and pool architecture in AGEN…
claude Jan 29, 2026
7ad64d0
fix: use description_placeholders for URLs in translation strings
claude Jan 29, 2026
1b469b5
style: fix ruff import sorting and add explicit check=False
claude Jan 29, 2026
58f7811
fix: resolve mypy strict errors in nova_request.py and local protobuf…
claude Jan 29, 2026
70f27bf
Merge pull request #989 from jleinenbach/claude/check-protobuf-issue-…
jleinenbach Jan 29, 2026
a0760be
fix: correct off-by-one in retry attempt count display
claude Jan 29, 2026
6cb0a2f
Merge pull request #990 from jleinenbach/claude/fix-retry-logic-bug-a…
jleinenbach Jan 29, 2026
4868e89
docs: verify and correct README.md against actual codebase
claude Jan 29, 2026
a5825ad
Merge pull request #992 from jleinenbach/claude/verify-update-readme-…
jleinenbach Jan 29, 2026
2e210ad
security: add comprehensive security review report
claude Jan 29, 2026
0c2798a
fix: security review fixes and FMDN-aware re-assessment
claude Jan 29, 2026
06f0c33
fix: use atomic decrypt_and_verify, remove SECURITY_REVIEW, update tests
claude Jan 29, 2026
5d958bb
Merge pull request #993 from jleinenbach/claude/security-review-jWbjN
jleinenbach Jan 29, 2026
e51e556
fix: lower protobuf/selenium version floors to support HA 2025.8+
claude Jan 29, 2026
9de86db
Merge pull request #994 from jleinenbach/claude/check-ha-version-comp…
jleinenbach Jan 29, 2026
17c930f
fix: deduplicate Nova API error messages and adjust retry log levels
claude Jan 29, 2026
548f75f
Merge pull request #995 from jleinenbach/claude/fix-nova-api-logging-…
jleinenbach Jan 29, 2026
154544b
fix: relax pycares constraint from >=4.4.0,<5 to >=4.4.0
claude Jan 29, 2026
921cacb
chore: regenerate poetry.lock after pycares constraint change
claude Jan 29, 2026
57959ab
Merge pull request #996 from jleinenbach/claude/relax-pycares-constra…
jleinenbach Jan 29, 2026
8bb4f36
fix: sync all language files with en.json as canonical reference
claude Jan 29, 2026
9879c8a
Merge pull request #997 from jleinenbach/claude/sync-language-files-T…
jleinenbach Jan 29, 2026
503f5c2
fix: Python 3.13 and HA 2026.x compatibility bugs
claude Jan 30, 2026
f8197f6
fix: remaining deprecations and code quality issues
claude Jan 30, 2026
3a50461
fix: suppress RuntimeWarning for unawaited coroutines in test_main.py
claude Jan 30, 2026
bd37cef
Merge pull request #998 from jleinenbach/claude/test-ha-python-3.13-F…
jleinenbach Jan 30, 2026
980d3f9
fix: batch UploadPrecomputedPublicKeyIds requests to max 40 devices
claude Jan 30, 2026
97c788c
style: fix import sorting (ruff I001)
claude Jan 30, 2026
45ec191
Merge pull request #999 from jleinenbach/claude/review-upstream-issue…
jleinenbach Jan 30, 2026
faf51a5
docs: add geo-consistency warning to auth repair issue and troublesho…
claude Jan 30, 2026
f86b136
Merge pull request #1000 from jleinenbach/claude/review-upstream-issu…
jleinenbach Jan 30, 2026
4eaeb13
feat: add FMDN_FLAGS_PROBE diagnostic logging for hashed flags byte
claude Jan 30, 2026
32640e4
fix: make FMDN_FLAGS_PROBE always log on first match per device
claude Jan 30, 2026
2962457
fix: remove _legacy_payload_start padding heuristic from EID extraction
claude Jan 30, 2026
7f46b26
fix: compute flags XOR mask for all EID windows and variants
claude Jan 30, 2026
5142980
feat: add BLE battery sensor from FMDN hashed-flags decode
claude Jan 30, 2026
67387c1
fix: sync ble_battery translation to all 9 languages, 100% test coverage
claude Jan 30, 2026
1173b00
feat: add docstrings, translate all languages, increase test coverage
claude Jan 30, 2026
a656e05
fix: demote FMDN_FLAGS_PROBE logging from INFO to DEBUG
claude Jan 30, 2026
4f865c5
Merge pull request #1001 from jleinenbach/claude/review-upstream-issu…
jleinenbach Jan 30, 2026
a14c6a6
fix: use canonical_id as battery state storage key (device ID mismatch)
claude Jan 31, 2026
6bda888
docs: BLE battery sensor architecture & lessons learned
claude Jan 31, 2026
eaf2c8c
Merge pull request #1002 from jleinenbach/claude/review-upstream-issu…
jleinenbach Jan 31, 2026
7643fcd
fix: battery_percentage → battery_pct typo in sensor creation log
claude Jan 31, 2026
4cd0ff9
style: replace percent format with f-string (UP031)
claude Jan 31, 2026
ecb6578
Merge pull request #1003 from jleinenbach/claude/review-upstream-issu…
jleinenbach Jan 31, 2026
9ce4809
fix: decrypt() required 20-byte identity_key but callers pass 32-byte…
claude Jan 31, 2026
91b2f54
Merge pull request #1004 from jleinenbach/claude/review-upstream-issu…
jleinenbach Jan 31, 2026
d4d242e
fix: use _attr_* pattern for tracker entity to restore attributes (HA…
claude Jan 31, 2026
8fc3758
Merge pull request #1005 from jleinenbach/claude/fix-tracker-attribut…
jleinenbach Jan 31, 2026
ce6c4f1
docs: Play Sound architecture and BLE ring implementation plan
claude Jan 31, 2026
0f47739
docs: correct Play Sound architecture after upstream + Google research
claude Jan 31, 2026
b5f12b4
feat: response logging, BLE scan info capture, and mik-laj analysis
claude Jan 31, 2026
aade887
docs: fix data_len values and mark completed phases in Play Sound docs
claude Jan 31, 2026
9600dcd
docs: add DULT non-owner sound protocol analysis from AirGuard deep dive
claude Jan 31, 2026
b089828
feat: add HA-Bluetooth FMDN advertisement scanner for BLE MAC collection
claude Jan 31, 2026
a8d923a
fix: battery_raw=3 maps to None (not 0%) and Bermuda event guard
claude Jan 31, 2026
f94e13f
style: fix ruff I001 import sorting in __init__.py unload path
claude Jan 31, 2026
87ddb17
feat: add UWT-Mode binary sensor entity per device
claude Feb 1, 2026
905015f
Merge pull request #1006 from jleinenbach/claude/review-priority-bugs…
jleinenbach Feb 1, 2026
09dd681
refactor: remove uwt_mode attribute from BLE battery sensor
claude Feb 1, 2026
e4ec649
Merge pull request #1007 from jleinenbach/claude/review-priority-bugs…
jleinenbach Feb 1, 2026
40d9e70
fix: add proactive owner key version mismatch check before decryption
claude Feb 1, 2026
28b229f
Merge pull request #1008 from jleinenbach/claude/google-findmy-integr…
jleinenbach Feb 1, 2026
c608ca2
refactor: deduplicate diagnostics components and fix privacy leak in …
claude Feb 1, 2026
13e4226
Merge pull request #1009 from jleinenbach/claude/review-diagnostics-c…
jleinenbach Feb 1, 2026
ca7e03a
fix: harden NovaApi cache isolation, ephemeral session warning, and r…
claude Feb 1, 2026
3db8800
Merge pull request #1010 from jleinenbach/claude/review-novaapi-modul…
jleinenbach Feb 1, 2026
145daf6
refactor: modernize __init__.py toward HA 2026.2 standards
claude Feb 1, 2026
d105b57
fix: update repairs test to accept extracted constant name
claude Feb 1, 2026
88cb8b9
Merge pull request #1011 from jleinenbach/claude/review-init-standard…
jleinenbach Feb 1, 2026
163a5eb
perf: add cooperative yielding to EID cache refresh loop
claude Feb 1, 2026
dfd43f6
style: apply ruff format to 12 files flagged by CI
claude Feb 1, 2026
0959fa7
Merge pull request #1012 from jleinenbach/claude/review-eid-resolver-…
jleinenbach Feb 1, 2026
93f2212
fix: implement proper subentry handler registration for HA 2026.x com…
claude Feb 3, 2026
b14b7a8
test: update tests to verify subentry handler registration
claude Feb 3, 2026
d783e1f
chore: update poetry.lock for HA 2026.1.1 compatibility
claude Feb 3, 2026
a3612eb
docs: add lessons learned for subentry handler registration and poetr…
claude Feb 3, 2026
bdc3b23
Merge pull request #1013 from jleinenbach/claude/fix-invalid-handler-…
jleinenbach Feb 3, 2026
1fb0ecd
fix: hide subentry UI buttons by returning empty dict from async_get_…
claude Feb 3, 2026
0b0aed8
Merge pull request #1014 from jleinenbach/claude/fix-invalid-handler-…
jleinenbach Feb 3, 2026
7392ad3
feat: enable stale threshold by default with FMDN-spec-compliant values
claude Feb 3, 2026
d534916
Merge pull request #1015 from jleinenbach/claude/enable-stale-thresho…
jleinenbach Feb 3, 2026
745e23c
feat: add Last Location entity and remove stale threshold toggle
claude Feb 3, 2026
08bb4f6
fix: remove DEFAULT_STALE_THRESHOLD_ENABLED from __all__ and apply ru…
claude Feb 3, 2026
4541fd7
fix: update tests for Last Location entity and add icon mapping
claude Feb 3, 2026
ad19fef
fix: remove trailing commas from icons.json for valid JSON
claude Feb 3, 2026
0bf3beb
fix: simplify CONFIG_SCHEMA to satisfy hassfest validation
claude Feb 3, 2026
d93dea4
fix: remove redundant root-level title from translation files
claude Feb 3, 2026
18e03f9
fix: add fallback for CONFIG_SCHEMA on older HA versions
claude Feb 3, 2026
739dcf9
fix: update tests for Last Location entity and translation title removal
claude Feb 3, 2026
7651c6a
Merge pull request #1016 from jleinenbach/claude/enable-stale-thresho…
jleinenbach Feb 3, 2026
3edebc3
fix: correct contributor_mode default in all translation files
claude Feb 3, 2026
8c378d4
fix: adopt HA entity naming best practice for device_tracker entities
claude Feb 4, 2026
43d2736
docs: add entity naming best practice to AGENTS.md
claude Feb 4, 2026
439ac8a
fix: update tests for Last Location entity and translation title removal
claude Feb 4, 2026
25280ac
Merge pull request #1017 from jleinenbach/claude/sync-config-defaults…
jleinenbach Feb 4, 2026
a81f8b6
fix: enable translation-based naming for Last Location entity
claude Feb 4, 2026
8b3e431
docs: document critical _attr_name = None vs not set difference
claude Feb 4, 2026
90c3075
Merge pull request #1018 from jleinenbach/claude/sync-config-defaults…
jleinenbach Feb 4, 2026
605a734
fix: translate contributor_mode options and update stale_threshold de…
claude Feb 4, 2026
93c38eb
Merge pull request #1019 from jleinenbach/claude/fix-translations-Mh9Vs
jleinenbach Feb 4, 2026
90bf146
fix: resolve all mypy --strict errors for Python 3.14 + mypy 1.19.1
claude Feb 6, 2026
71ff7e9
docs: document _MixinBase typing pattern and restore python_version 3.13
claude Feb 6, 2026
49ec84e
fix: resolve ruff E402/I001/PLC0414 and GoogleHomeFilter forward-refe…
claude Feb 6, 2026
50a726e
Merge pull request #1020 from jleinenbach/claude/fix-mypy-strict-type…
jleinenbach Feb 6, 2026
585bbaa
fix: replace hardcoded 50m significance threshold with accuracy-adapt…
claude Feb 7, 2026
1d2f56e
Merge pull request #1021 from jleinenbach/claude/review-upstream-issu…
jleinenbach Feb 7, 2026
30606ec
test: add import smoke test for all 123 modules
claude Feb 7, 2026
b3ea04d
Merge pull request #1022 from jleinenbach/claude/review-upstream-issu…
jleinenbach Feb 7, 2026
5518ca5
fix: guard DataUpdateCoordinator stubs in _MixinBase with TYPE_CHECKING
claude Feb 7, 2026
74c1b9e
Merge pull request #1023 from jleinenbach/claude/fix-ble-battery-sens…
jleinenbach Feb 7, 2026
2092521
fix: add base64 padding to FCM crypto_key and salt decoding
claude Feb 13, 2026
2634f71
Merge pull request #1024 from jleinenbach/claude/check-firebase-auth-…
jleinenbach Feb 13, 2026
38b6185
feat: make external URL optional, fall back to internal URL for map view
claude Feb 13, 2026
be7d9e3
fix: detect internal URL by comparison instead of checking external_u…
claude Feb 13, 2026
55c7d74
fix: update test assertion to match renamed warning message
claude Feb 13, 2026
5043bb4
Merge pull request #1025 from jleinenbach/claude/optional-external-ur…
jleinenbach Feb 13, 2026
ab61a9f
fix: parse FCM crypto-key and encryption headers by parameter name
claude Feb 14, 2026
da409eb
Merge pull request #1026 from jleinenbach/claude/debug-integration-er…
jleinenbach Feb 14, 2026
cf4ba96
Regenerate _pb2.py files with protobuf 6.31.1 and add round-trip tests
claude Feb 15, 2026
f27a5b0
fix: align protobuf floor with generated runtime version and fix test…
claude Feb 15, 2026
211bc00
chore: regenerate poetry.lock after protobuf version bump
claude Feb 15, 2026
65c311e
Merge pull request #1027 from jleinenbach/claude/check-google-travel-…
jleinenbach Feb 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,36 @@ Prefer the executable name when it is available; fall back to the module form wh
* **Synchronization points:** Keep `custom_components/googlefindmy/manifest.json`, `custom_components/googlefindmy/requirements.txt`, `pyproject.toml`, and `custom_components/googlefindmy/requirements-dev.txt` aligned. When bumping versions, check whether other files (for example, `hacs.json` or helpers under `script/`) must change as well.
* **Upgrade workflow:** With internet access, perform dependency maintenance via `pip install`, `pip-compile`, `pip-audit`, `poetry update` (if relevant), and `python -m pip list --outdated`. Afterwards rerun tests/linters and document the outcomes.
* **Change notes:** Record adjusted minimum versions or dropped legacy releases in the PR description and, when needed, in `CHANGELOG.md` or `README.md`.

### Poetry lock file management

**Critical:** After ANY change to `pyproject.toml`, regenerate `poetry.lock` with `poetry lock` before committing. CI will fail with "pyproject.toml changed significantly since poetry.lock was last generated" if the content-hash doesn't match.

**Correct workflow:**
```bash
# 1. Edit pyproject.toml (e.g., change dependency version)
# 2. Regenerate lock file
poetry lock

# 3. Verify lock is in sync
poetry check

# 4. Commit BOTH files together
git add pyproject.toml poetry.lock
git commit -m "chore: update dependency X to version Y"
```

**Common mistakes to avoid:**
- Committing `pyproject.toml` without regenerating `poetry.lock`
- Running `poetry install` without first running `poetry lock` after `pyproject.toml` changes
- Using `--no-update` flag when dependencies need updating

**CI failure pattern:**
```
pyproject.toml changed significantly since poetry.lock was last generated.
Run `poetry lock` to fix the lock file.
```

* **Manifest compatibility (Jan 2025):** The shared CI still ships a `script.hassfest` build that rejects the `homeassistant` manifest key. Until upstream relaxes the schema for custom integrations, do **not** add `"homeassistant": "<version>"` to `custom_components/googlefindmy/manifest.json` or `hacs.json`. Track the minimum supported Home Assistant core release in documentation/tests instead.

## Maintenance mode
Expand Down Expand Up @@ -728,6 +758,16 @@ artifacts remain exempt when explicitly flagged by repo configuration).
* Repairs/Diagnostics: provide both; redact aggressively.
* Storage: use `helpers.storage.Store` for tokens/state; throttle writes (batch/merge).
* System health: prefer the `SystemHealthRegistration` helper (`homeassistant.components.system_health.SystemHealthRegistration`) when available and keep the legacy component import only as a guarded fallback.
* **Entity naming** (HA Best Practice, ref: [Adopting a new way to name entities](https://developers.home-assistant.io/blog/2022/07/10/entity_naming/)):
- Always set `_attr_has_entity_name = True` on entity classes.
- **Primary entity** (represents the device itself): set `_attr_name = None` so it inherits only the device name (e.g., "Galaxy S25 Ultra").
- **Secondary entities** (additional features): use `translation_key` with a `name` in translations; HA auto-composes the friendly name as "Device Name + Translation" (e.g., "Galaxy S25 Ultra Last location").
- **Translation files**: for the primary entity's `translation_key`, **omit** the `"name"` key entirely (presence of `"name"` would append a suffix); for secondary entities, **include** the `"name"` key with the suffix text.
- Never set `_attr_name` dynamically at runtime (e.g., in coordinator update callbacks) when using `has_entity_name=True`—the device registry is the single source of truth for the device name.
- **CRITICAL: `_attr_name = None` vs. attribute not set** — These behave differently with `has_entity_name=True`:
- `_attr_name = None` (explicitly set) → entity inherits **only** the device name, no suffix
- `_attr_name` **not set** (attribute doesn't exist) → name comes from `translation_key`
- If a parent class sets `_attr_name = None` in `__init__()` and a child class needs the translation-based name, the child must **delete** the attribute after `super().__init__()`: `del self._attr_name`

### 11.8 Release & operations

Expand Down
94 changes: 55 additions & 39 deletions README.md

Large diffs are not rendered by default.

25 changes: 1 addition & 24 deletions custom_components/googlefindmy/Auth/aas_token_retrieval.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
from .gpsoauth_loader import (
gpsoauth as _gpsoauth_proxy,
)
from .token_cache import TokenCache, async_get_all_cached_values
from .token_cache import TokenCache
from .username_provider import username_string

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -366,29 +366,6 @@ async def _generate_aas_token(*, cache: TokenCache) -> str: # noqa: PLR0912, PL
)
break

# Fallback 3: Try global cache for ADM tokens if entry cache had none (validation scenario)
if not oauth_token and cache:
try:
all_cached_global = await async_get_all_cached_values()
for key, value in all_cached_global.items():
if (
isinstance(key, str)
and key.startswith("adm_token_")
and isinstance(value, str)
and value
):
oauth_token = value
extracted_username = key.replace("adm_token_", "", 1)
if extracted_username and "@" in extracted_username:
username = extracted_username
_LOGGER.info(
"Using existing ADM token from global cache for OAuth exchange.",
extra={"user": _mask_email_for_logs(username)},
)
break
except Exception: # noqa: BLE001
pass

if not oauth_token:
raise ValueError(
"No OAuth token available; please configure the integration with a valid token."
Expand Down
6 changes: 3 additions & 3 deletions custom_components/googlefindmy/Auth/fcm_receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,9 @@ def _on_credentials_updated(self, creds: Any) -> None:
try:
if self._cache is not None:
if creds is None:
self._cache._data.pop("fcm_credentials", None)
self._cache.sync_pop("fcm_credentials")
else:
self._cache._data["fcm_credentials"] = creds
self._cache.sync_set("fcm_credentials", creds)
else:
set_cached_value("fcm_credentials", creds)
self._creds = creds
Expand Down Expand Up @@ -203,5 +203,5 @@ def _read_cached_credentials(self) -> Any:
"""Return credentials from the selected cache without raising."""

if self._cache is not None:
return self._cache._data.get("fcm_credentials")
return self._cache.sync_get("fcm_credentials")
return get_cached_value("fcm_credentials")
54 changes: 45 additions & 9 deletions custom_components/googlefindmy/Auth/fcm_receiver_ha.py
Original file line number Diff line number Diff line change
Expand Up @@ -985,7 +985,10 @@ def register_coordinator(self, coordinator: Any) -> None:

pending_creds = self._pending_creds.pop(entry.entry_id, None)
if pending_creds is not None:
asyncio.create_task(cache.set("fcm_credentials", pending_creds))
self._dispatch_to_hass_loop(
cache.set("fcm_credentials", pending_creds),
label=f"set_pending_creds_{entry.entry_id}",
)

pending_tokens = self._pending_routing_tokens.pop(entry.entry_id, set())

Expand All @@ -1007,21 +1010,30 @@ async def _flush_tokens() -> None:
err,
)

asyncio.create_task(_flush_tokens())
self._dispatch_to_hass_loop(
_flush_tokens(),
label=f"flush_pending_tokens_{entry.entry_id}",
)

# Mirror any known credentials to this entry cache
try:
creds = self.creds.get(entry.entry_id)
if creds and cache is not None:
asyncio.create_task(cache.set("fcm_credentials", creds))
self._dispatch_to_hass_loop(
cache.set("fcm_credentials", creds),
label=f"mirror_creds_{entry.entry_id}",
)
except Exception as err:
_LOGGER.debug("Entry-scoped credentials persistence skipped: %s", err)

# Update routing with any token we already have
token = self.get_fcm_token(entry.entry_id)
if token:
self._update_token_routing(token, {entry.entry_id})
asyncio.create_task(self._persist_routing_token(entry.entry_id, token))
self._dispatch_to_hass_loop(
self._persist_routing_token(entry.entry_id, token),
label=f"persist_routing_token_{entry.entry_id}",
)

# Load persisted routing tokens for this entry and map them as well
if cache is not None:
Expand All @@ -1040,10 +1052,16 @@ async def _load_tokens() -> None:
err,
)

asyncio.create_task(_load_tokens())
self._dispatch_to_hass_loop(
_load_tokens(),
label=f"load_persisted_tokens_{entry.entry_id}",
)

# Start supervisor for this entry
asyncio.create_task(self._start_supervisor_for_entry(entry.entry_id, cache))
self._dispatch_to_hass_loop(
self._start_supervisor_for_entry(entry.entry_id, cache),
label=f"start_supervisor_{entry.entry_id}",
)

def unregister_coordinator(self, coordinator: Any) -> None:
"""Unregister a coordinator (sync; safe for async_on_unload)."""
Expand Down Expand Up @@ -1129,6 +1147,18 @@ async def _handle_notification_async(
await self._run_callback_async(cb, canonic_id, hex_string)
return

# Log FCM pushes that have no registered callback (e.g. sound
# confirmations, device status updates). This fires only in
# response to a user-initiated action (Play Sound button etc.)
# so it does not create log spam during normal operation.
_LOGGER.debug(
"FCM push for %s has no registered callback "
"(may be action confirmation): payload_len=%d, hex_prefix=%s",
canonic_id[:8],
len(hex_string),
hex_string[:120] if hex_string else "(empty)",
)

tracked = [
c for c in target_coordinators if self._is_tracked(c, canonic_id)
]
Expand Down Expand Up @@ -1668,12 +1698,18 @@ def _on_credentials_updated_for_entry(self, entry_id: str, creds: Any) -> None:
token = self.get_fcm_token(entry_id)
if token:
self._update_token_routing(token, {entry_id})
asyncio.create_task(self._persist_routing_token(entry_id, token))
self._dispatch_to_hass_loop(
self._persist_routing_token(entry_id, token),
label=f"persist_routing_token_{entry_id}",
)
self._clear_fatal_error_for_entry(
entry_id, reason="Credentials updated for entry"
)

asyncio.create_task(self._async_save_credentials_for_entry(entry_id))
self._dispatch_to_hass_loop(
self._async_save_credentials_for_entry(entry_id),
label=f"save_credentials_{entry_id}",
)
_LOGGER.info("[entry=%s] FCM credentials updated", entry_id)

async def _async_save_credentials_for_entry(self, entry_id: str) -> None:
Expand Down Expand Up @@ -1755,7 +1791,7 @@ async def async_stop(self, timeout: float = 5.0) -> None:
eid,
timeout,
)
except (ConnectionError, TimeoutError) as err:
except ConnectionError as err:
_LOGGER.debug("[entry=%s] FCM client stop network error: %s", eid, err)
except Exception as err: # noqa: BLE001
_LOGGER.debug(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
from typing import TYPE_CHECKING, Any, cast

from aiohttp import ClientSession
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_der_private_key

import http_ece
Expand Down Expand Up @@ -435,8 +434,8 @@ def _decrypt_raw_data(
salt_str: str,
raw_data: bytes,
) -> bytes:
crypto_key = urlsafe_b64decode(crypto_key_str.encode("ascii"))
salt = urlsafe_b64decode(salt_str.encode("ascii"))
crypto_key = urlsafe_b64decode(crypto_key_str.encode("ascii") + b"=" * (-len(crypto_key_str) % 4))
salt = urlsafe_b64decode(salt_str.encode("ascii") + b"=" * (-len(salt_str) % 4))

keys_section = credentials.get("keys")
if not isinstance(keys_section, Mapping):
Expand All @@ -447,11 +446,9 @@ def _decrypt_raw_data(
if not (isinstance(private_value, str) and isinstance(secret_value, str)):
raise ValueError("Invalid key values in credential payload")

der_data = urlsafe_b64decode(private_value.encode("ascii") + b"========")
secret = urlsafe_b64decode(secret_value.encode("ascii") + b"========")
privkey = load_der_private_key(
der_data, password=None, backend=default_backend()
)
der_data = urlsafe_b64decode(private_value.encode("ascii") + b"=" * (-len(private_value) % 4))
secret = urlsafe_b64decode(secret_value.encode("ascii") + b"=" * (-len(secret_value) % 4))
privkey = load_der_private_key(der_data, password=None)
decrypted = http_decrypt(
raw_data,
salt=salt,
Expand All @@ -473,6 +470,20 @@ def _app_data_by_key(
return ""
raise RuntimeError(f"couldn't find in app_data {key}")

@staticmethod
def _extract_header_param(header: str, param: str) -> str:
"""Extract a named parameter from a semicolon-separated header value.

FCM headers like crypto-key and encryption use the format
``key=value;key2=value2``. Blindly slicing off a fixed prefix
breaks when extra parameters (e.g. ``p256ecdsa=...``) are present.
"""
for part in header.split(";"):
key, _, value = part.strip().partition("=")
if key == param:
return value
raise ValueError(f"Parameter '{param}' not found in header: {header}")

def _handle_data_message(
self,
msg: DataMessageStanza,
Expand All @@ -490,8 +501,12 @@ def _handle_data_message(
):
# The deleted_messages message does not contain data.
return
crypto_key = self._app_data_by_key(msg, "crypto-key")[3:] # strip dh=
salt = self._app_data_by_key(msg, "encryption")[5:] # strip salt=
crypto_key = self._extract_header_param(
self._app_data_by_key(msg, "crypto-key"), "dh"
)
salt = self._extract_header_param(
self._app_data_by_key(msg, "encryption"), "salt"
)
subtype = self._app_data_by_key(msg, "subtype")
if TYPE_CHECKING:
assert self.credentials
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading