Skip to content

Keep UPnP event subscriptions alive when a device drops its SID - #22

Open
MarvinSchenkel wants to merge 1 commit into
Linkplay2020:mainfrom
MarvinSchenkel:fix/subscription-renewal-keyerror
Open

Keep UPnP event subscriptions alive when a device drops its SID#22
MarvinSchenkel wants to merge 1 commit into
Linkplay2020:mainfrom
MarvinSchenkel:fix/subscription-renewal-keyerror

Conversation

@MarvinSchenkel

@MarvinSchenkel MarvinSchenkel commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

A device can silently stop reporting state after a subscription renewal.

UpnpEventHandler.async_resubscribe() raises a plain KeyError when the handler no longer holds a SID for the service. It documents this itself (:raise KeyError: Supplied service_or_sid is not known.), but _renew_subscriptions() only catches UpnpError. The KeyError escapes the method before the _schedule_subscription_renewal() call on the line below it ever runs.

The renewal is a one-shot loop.call_later, so nothing re-arms it. That leaves the device with no event subscription and no renewal timer, while it still reports _available = True and _event_handler_started = True. Live state only reaches the consumer over NOTIFY, so the player freezes on whatever it last reported.

The same exception also skips the recovery. It propagates out of ensure_subscriptions() before reaching the if not ok: await async_init_services_and_subscribe() fallback that would have re-subscribed.

The caller is fire-and-forget, so it lands as an unretrieved task exception:

Task exception was never retrieved
  File ".../wiim/wiim_device.py", line 535, in _renew_subscriptions
    await self._event_handler.async_resubscribe(self.av_transport)
  File ".../async_upnp_client/event_handler.py", line 117, in _sid_and_service
    raise KeyError(f"Unknown UpnpService {service}")
KeyError: 'Unknown UpnpService <UpnpService(urn:upnp-org:serviceId:AVTransport, uuid:FF98F7F4-...)>'

We hit this 52 times across 13 separate events on three devices in about a day, mostly right after an ungroup.

Changes

  • Add _renew_service_subscription(). When sid_for_service() returns None the SID is gone, so it sends a full async_subscribe() instead of a resubscribe that can only fail.
  • Move _schedule_subscription_renewal() out of the three try blocks into the if success_all: branch, so a successful run arms the next renewal once rather than once per service.
  • Pass the renewal timeout explicitly instead of relying on the library default.
  • Add tests covering a dropped SID, the timer staying armed, and an offline device still failing the renewal.

I did consider just catching KeyError alongside UpnpError. That stops the crash, but the next renewal raises the same thing again, and falling into the failure branch stops the notify server and waits for availability polling to recover. Checking the SID first repairs the subscription in place and the device never stops eventing.

async_resubscribe raises a plain KeyError when the event handler no longer
holds a SID for the service. It is not a UpnpError, so it escaped the renewal
before the next renewal was scheduled, leaving the device without eventing and
skipping the re-init that ensure_subscriptions falls back to.

Subscribe from scratch when the SID is gone, and schedule the next renewal
once per successful run instead of once per service.
@MarvinSchenkel
MarvinSchenkel marked this pull request as ready for review August 21, 2026 09:37
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.

1 participant