Skip to content

fix: parental-control switch left stale on rule removal (cache_time race + missing else-branch)#1255

Open
paul43210 wants to merge 1 commit into
Vaskivskyi:devfrom
paul43210:fix/pc-rule-cache-and-stale-switch
Open

fix: parental-control switch left stale on rule removal (cache_time race + missing else-branch)#1255
paul43210 wants to merge 1 commit into
Vaskivskyi:devfrom
paul43210:fix/pc-rule-cache-and-stale-switch

Conversation

@paul43210

Copy link
Copy Markdown

Summary

Two related bugs cause switch.<client>_block_internet entities to remain state=on after their underlying parental-control rule is removed (via the device_internet_access state=remove service, the router admin UI, the ASUS app, or 64-rule cap eviction). The switch becomes a zombie that misreports the device as blocked indefinitely, until the user manually reloads the integration.

Observed against ZenWiFi BT8 firmware 3.0.0.6.102_58407 with a Node-RED-driven workflow that frequently issues device_internet_access calls. Reproduced multiple times in a single debugging session; confirmed fully resolved after the patch with 100+ block/unblock cycles producing zero zombies.

Root cause

Bug 1 — bridge.py:_get_data_parental_control doesn't honour write-then-read freshness

async def _get_data_parental_control(self) -> dict[str, Any]:
    """Get parental control data from the device."""
    return await self._get_data(
        AsusData.PARENTAL_CONTROL,
        self._process_data_parental_control,
    )   # force defaults to False; library returns cached data within cache_time

CONF_DEFAULT_CACHE_TIME = 5 seconds. When async_service_device_internet_access in router.py calls update_pc_rules() immediately after a successful async_pc_rule(REMOVE), the library returns its still-cached pre-write rule list. self._pc_rules is then refreshed from stale data — the just-removed MAC is still present. The subsequent platform unload+reload (also in the service handler) iterates this stale pc_rules and re-creates ClientInternetSwitch for a rule that no longer exists on the router. is_on continues to return True based on the cached _rule.type=BLOCK.

Bug 2 — switch.py:ClientInternetSwitch.async_on_demand_update has no else-branch

@callback
def async_on_demand_update(self) -> None:
    """Update the state."""
    if self._rule.mac in self._router.pc_rules:
        self._rule = self._router.pc_rules[self._rule.mac]
        self.async_write_ha_state()
    # ^ falls through silently when MAC has been removed from pc_rules

Once update_pc_rules eventually catches up (next poll cycle, cache expired), signal_pc_rules_update fires and async_on_demand_update runs for every existing switch. For the just-removed MAC, the if test fails and the function silently returns. The cached self._rule is never invalidated, is_on keeps returning True. Subsequent polls continue to no-op for as long as the entity exists — only an integration reload clears it.

Why both fixes

Either fix in isolation would address the immediate symptom for service-triggered removes (since the service handler already does a platform unload+reload). Both fixes are needed because:

  • Bug 1 ensures the post-write platform reload sees correct state.
  • Bug 2 covers out-of-band removals (router UI, ASUS app, 64-rule cap eviction, recovery after a temporary router API hiccup) that don't trigger the service handler's platform reload.

Reproduction

  1. From an HA automation or Node-RED flow, call asusrouter.device_internet_access with state=block for a fresh MAC. Confirm the rule appears in the router admin UI and switch.<client>_block_internet becomes state=on.
  2. Within ≤5 seconds of the next asusrouter poll cycle, call the same service with state=remove.
  3. Observe: router admin UI shows the rule is gone, the target device regains internet. But switch.<client>_block_internet remains state=on and is_on=True.
  4. Reload the asusrouter integration → switch entity transitions to unavailable or disappears, confirming the cached state was the issue.

Verification after patch

Same reproduction; the switch correctly drops to unavailable immediately after the remove, and the entity is cleanly removed by the platform reload. Tested with 100+ block/unblock cycles in a single session: zero zombie switches.

Test plan

  • Existing test suite passes
  • Manual: block a device, observe BT8 rule + switch.on
  • Manual: unblock immediately, observe BT8 rule removed AND switch transitions to unavailable/removed (not stuck on)
  • Manual: from router admin UI, delete a rule out-of-band; observe HA switch transitions to unavailable on the next poll

🤖 Generated with Claude Code

Two related bugs cause `switch.<client>_block_internet` entities to remain
`state=on` after their underlying parental-control rule is removed:

1. `_get_data_parental_control` defaults to `force=False`, so the post-write
   refresh in `async_service_device_internet_access` receives cached pre-write
   data when called within `cache_time=5s` of the previous poll. The
   subsequent platform unload+reload then iterates the stale `pc_rules` and
   re-creates the switch for a rule that no longer exists on the router.

2. `ClientInternetSwitch.async_on_demand_update` has no else-branch. Once
   `pc_rules` catches up and the MAC is gone, the switch's cached `_rule`
   (with `type=BLOCK`) is never invalidated. `is_on` keeps returning True
   for as long as the entity exists.

Fixes:
- Thread `force: bool = False` through `_get_data_parental_control` and
  `update_pc_rules`; pass `force=True` from the service handler so the
  post-write fetch bypasses the cache.
- Add an else-branch to `async_on_demand_update` that marks the entity
  unavailable when its MAC is gone from `pc_rules`. Covers out-of-band
  removals too (router UI, ASUS app, 64-rule cap eviction).

Verified against ZenWiFi BT8 firmware 3.0.0.6.102_58407 with 100+
block/unblock cycles: zero zombie switches.
@paul43210

Copy link
Copy Markdown
Author

Heads-up before review: in the interest of being honest, this PR was largely AI-drafted (Claude, Anthropic) — the code transform, tests, and prose are AI-generated. The bug investigation and validation against my hardware (ZenWiFi BT8 mesh on FW 3.0.0.6.102_58407) are mine. Mentioning this proactively because a similar question came up on Vaskivskyi/asusrouter#812 in the underlying library repo, and it's only fair you know what you're looking at upfront. 😅

Happy to elaborate on the diagnosis or the change, and equally happy to take rework feedback or close if it's not a fit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Incoming

Development

Successfully merging this pull request may close these issues.

1 participant