Skip to content

Fix wirelesstag KeyError when a tag disappears from a reload - #172600

Merged
Markus98 merged 8 commits into
home-assistant:devfrom
kohai-ut:wirelesstag-fix-update-keyerror
Oct 15, 2026
Merged

Markus98 merged 8 commits into
home-assistant:devfrom
kohai-ut:wirelesstag-fix-update-keyerror

Conversation

@kohai-ut

@kohai-ut kohai-ut commented May 30, 2026 •

Copy link
Copy Markdown
Contributor

Breaking change

Proposed change

WirelessTagBaseSensor.update() (the polling path) looked up the reloaded tags
with updated_tags[self._uuid]. If a reload no longer contains the entity's tag
(for example the tag was removed from the account), this raises KeyError.
The following is None guard only handles a present-but-None value, not a
missing key.

Use updated_tags.get(self._uuid) so a missing tag no longer raises. Instead of
logging an error on every poll, the entity is marked unavailable and the message
is logged at debug level, since a tag being removed from the account is not an
error.

The push callbacks in sensor.py and binary_sensor.py now share an
_apply_tag helper with the polling path, so the entity becomes available again
as soon as the tag shows up through either a poll or a push notification.

Found while reviewing the integration; no separate issue was filed.

Type of change

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

Additional information

  • This PR fixes or closes issue: fixes #
  • This PR is related to issue:
  • Link to documentation pull request:
  • Link to developer documentation pull request:
  • Link to frontend pull request:

Checklist

  • I understand the code I am submitting and can explain how it works.
  • 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.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

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

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

WirelessTagBaseSensor.update() looked up the reloaded tags with
updated_tags[self._uuid], which raises KeyError if the tag is no longer
returned (the following `is None` guard only handles a present None value).
Use .get() so a missing tag takes the existing graceful error path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings May 30, 2026 01:29
@home-assistant

Copy link
Copy Markdown
Contributor

Hey there @sergeymaysak, mind taking a look at this pull request as it has been labeled with an integration (wirelesstag) you are listed as a code owner for? Thanks!

Code owner commands

Code owners of wirelesstag can trigger bot actions by commenting:

  • @home-assistant close Closes the pull request.
  • @home-assistant mark-draft Mark the pull request as draft.
  • @home-assistant ready-for-review Remove the draft status from the pull request.
  • @home-assistant rename Awesome new title Renames the pull request.
  • @home-assistant reopen Reopen the pull request.
  • @home-assistant unassign wirelesstag Removes the current integration label and assignees on the pull request, add the integration domain after the command.
  • @home-assistant update-branch Update the pull request branch with the base branch.
  • @home-assistant add-label needs-more-information Add a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) to the pull request.
  • @home-assistant remove-label needs-more-information Remove a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) on the pull request.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds regression coverage and hardens Wireless Sensor Tags entity updates so missing tags on reload don’t raise errors.

Changes:

  • Avoid KeyError during entity update() when a tag disappears from load_tags().
  • Add a new sensor test ensuring the missing-tag path logs and leaves the entity intact.
  • Assign CODEOWNERS for the new WirelessTag test directory.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
tests/components/wirelesstag/test_sensor.py Adds regression test for missing tag on reload/update.
tests/components/wirelesstag/init.py Initializes the WirelessTag tests package.
homeassistant/components/wirelesstag/entity.py Switches dict access to .get() to prevent KeyError and log gracefully.
CODEOWNERS Adds ownership for WirelessTag tests directory.

Comment on lines +49 to +76
async def test_poll_handles_tag_missing_from_reload(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test a poll where the tag is no longer returned is handled gracefully.

If a reload no longer contains the entity's tag, the update must log and
return instead of raising a KeyError.
"""
tag = _mock_tag()
with patch("homeassistant.components.wirelesstag.WirelessTags") as mock_api_class:
mock_api = mock_api_class.return_value
mock_api.load_tags.return_value = {tag.uuid: tag}

assert await async_setup_component(hass, "wirelesstag", CONFIG)
await hass.async_block_till_done()
assert await async_setup_component(hass, "sensor", CONFIG)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID) is not None

# The tag is no longer returned by a reload.
mock_api.load_tags.return_value = {}
await async_update_entity(hass, ENTITY_ID)
await hass.async_block_till_done()

# The entity survives and the graceful error path is taken (no KeyError).
assert hass.states.get(ENTITY_ID) is not None
assert "Unable to update tag" in caplog.text

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call — cleared caplog right before the update and switched to asserting on an ERROR-level record, so it only passes for the missing-tag path. Done in 3e73c26.

return tag


async def test_poll_handles_tag_missing_from_reload(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed — renamed to test_update_handles_tag_missing_from_reload and updated the docstring to reflect the explicit update. Done in 3e73c26.

Address review feedback: rename the test to reflect that it triggers an
explicit update (not a scheduled poll), and clear caplog before the update
and assert on an ERROR-level record so the assertion only passes for the
missing-tag path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

updated_tags = self._api.load_tags()
if (updated_tag := updated_tags[self._uuid]) is None:
if (updated_tag := updated_tags.get(self._uuid)) is None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This bugfix is reasonable, however the below log might be over-logging at error. Consider lowering the level, or setting unavailable and clearing state if actually gone.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — the error was firing on every poll, and worse, the entity kept serving its last value while the tag was gone.

Went with the second option in bd61e8d: update() now records the missing tag, available returns False for it, and the log drops to debug since the condition is visible to the user as unavailable. The state is cleared, and the entity recovers if the tag comes back. The test asserts the unavailable/recover transitions instead of scraping the log.

The polling path logged at error level every scan interval when the tag
was missing from a reload, while the entity kept serving the last known
value. Track the missing tag, report it through availability, and drop
the log to debug now that the condition is visible to the user.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 27, 2026 16:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

# poll. Reported through availability rather than a log message
# repeated on every poll.
_LOGGER.debug('Unable to update tag: "%s"', self.name)
self._tag_missing = True

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct, and thanks — that was a real hole in the flag I just added. A push proves the tag exists, but neither callback cleared _tag_missing, so the entity stayed unavailable until the next successful poll.

Fixed in 4d4a80d by routing all three paths through one helper on the base entity rather than repeating the reset:

def _apply_tag(self, tag: SensorTag) -> None:
    """Adopt a freshly received tag and refresh the state from it."""
    self._tag_missing = False
    self._tag = tag
    self._state = self.updated_state_value()

update(), WirelessTagSensor._update_tag_info_callback and WirelessTagBinarySensor._on_binary_event_callback all call it, so a future platform cannot forget the reset. The test is now parametrized over both recovery paths (poll and push); I confirmed the push case fails against the previous commit and passes with this one.

The push callbacks replaced the tag without resetting the flag, so an
entity marked missing by one poll stayed unavailable until the next
successful poll even though the push proved the tag was back. Funnel both
callbacks and the poll path through a single helper that adopts the tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 27, 2026 16:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

) -> None:
"""Make the tag available again through the polling path."""
mock_api.load_tags.return_value = {tag.uuid: tag}
await async_update_entity(hass, ENTITY_ID)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we shouldn't call async_update_entity directly. Instead use the mock to fetch a registered callback method or skip time using the freezer to trigger a "natural" refresh

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 417ddb4 — replaced with freezer.tick(SCAN_INTERVAL) + async_fire_time_changed.

Comment on lines +64 to +66
async_dispatcher_send(
hass, SIGNAL_TAG_UPDATE.format(tag.tag_id, tag.tag_manager_mac), tag
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

don't use the dispatcher directly

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 417ddb4 — the test now calls the push callback the integration registered via mock_api.start_monitoring.call_args, from an executor job as the library does.

@home-assistant
home-assistant Bot marked this pull request as draft July 28, 2026 10:45
@home-assistant

Copy link
Copy Markdown
Contributor

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

Replace the direct async_update_entity call with a frozen-time tick over
the platform scan interval, and drive push recovery through the callback
the integration registered with the library mock instead of firing the
dispatcher signal by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 28, 2026 14:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

@kohai-ut
kohai-ut marked this pull request as ready for review July 28, 2026 14:36
@home-assistant
home-assistant Bot requested a review from joostlek July 28, 2026 14:36
@kohai-ut

Copy link
Copy Markdown
Contributor Author

@joostlek both points from your review are addressed in 417ddb4:

  • async_update_entity is gone — the poll path now ticks the freezer by SCAN_INTERVAL and fires async_fire_time_changed.
  • The dispatcher is gone — the push path fetches the callback the integration registered via mock_api.start_monitoring.call_args and calls it from an executor job, the way the library does.

The test is parametrized over both recovery paths (poll and push). CI is green. Ready for another look whenever you have a moment — thanks!

Resolve an add/add conflict in tests/components/wirelesstag/test_sensor.py:
the wirelesstag test module also arrived on dev via home-assistant#172590, home-assistant#172592 and
home-assistant#172598. Keep both suites, taking the shared _mock_tag helper and CONFIG
from dev since they cover temperature and humidity, and point the
missing-tag test at the existing entity id constant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 26, 2026 13:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment on lines +76 to +79
# The tag is gone from the account, so there is nothing left to
# poll. Reported through availability rather than a log message
# repeated on every poll.
_LOGGER.debug('Unable to update tag: "%s"', self.name)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair point — the debug line does fire on every poll, so that sentence described the change from error to debug rather than the code under it.

Replaced in 1faac3c with a single line stating the constraint that actually matters, which is why this is debug and not a warning:

# A tag can be removed from the account, so this is not an error.

The comment described the move from error to debug rather than the code
in front of it, and claimed the condition was reported through
availability instead of a per-poll log message while the debug line still
fires on every poll.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 26, 2026 13:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

tests/components/wirelesstag/test_sensor.py:158

  • Remove this comment because it only restates that the following mock response omits the tag.
        # The tag is no longer returned by a reload.

Copilot AI balanced review requested due to automatic review settings October 15, 2026 09:13

@Markus98 Markus98 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me! Thanks @kohai-ut!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@Markus98
Markus98 merged commit d32b97e into home-assistant:dev Oct 15, 2026
32 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants