diff --git a/bundles/org.openhab.binding.upnpcontrol/src/main/java/org/openhab/binding/upnpcontrol/internal/UpnpControlHandlerFactory.java b/bundles/org.openhab.binding.upnpcontrol/src/main/java/org/openhab/binding/upnpcontrol/internal/UpnpControlHandlerFactory.java index dc020a30c97a6..4083a5fe570a6 100644 --- a/bundles/org.openhab.binding.upnpcontrol/src/main/java/org/openhab/binding/upnpcontrol/internal/UpnpControlHandlerFactory.java +++ b/bundles/org.openhab.binding.upnpcontrol/src/main/java/org/openhab/binding/upnpcontrol/internal/UpnpControlHandlerFactory.java @@ -145,7 +145,7 @@ public void unregisterHandler(Thing thing) { } private UpnpServerHandler addServer(Thing thing) { - UpnpServerHandler handler = new UpnpServerHandler(thing, upnpIOService, upnpRenderers, + UpnpServerHandler handler = new UpnpServerHandler(thing, upnpIOService, upnpService, upnpRenderers, upnpStateDescriptionProvider, upnpCommandDescriptionProvider, configuration); String key = thing.getUID().toString(); upnpServers.put(key, handler); @@ -162,8 +162,8 @@ private UpnpServerHandler addServer(Thing thing) { private UpnpRendererHandler addRenderer(Thing thing) { callbackUrl = createCallbackUrl(); - UpnpRendererHandler handler = new UpnpRendererHandler(thing, upnpIOService, this, upnpStateDescriptionProvider, - upnpCommandDescriptionProvider, configuration); + UpnpRendererHandler handler = new UpnpRendererHandler(thing, upnpIOService, upnpService, this, + upnpStateDescriptionProvider, upnpCommandDescriptionProvider, configuration); String key = thing.getUID().toString(); upnpRenderers.put(key, handler); upnpServers.forEach((thingId, value) -> value.addRendererOption(key)); diff --git a/bundles/org.openhab.binding.upnpcontrol/src/main/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpHandler.java b/bundles/org.openhab.binding.upnpcontrol/src/main/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpHandler.java index 1c16a74dd3f7d..0b5953b5cd200 100644 --- a/bundles/org.openhab.binding.upnpcontrol/src/main/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpHandler.java +++ b/bundles/org.openhab.binding.upnpcontrol/src/main/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpHandler.java @@ -28,7 +28,13 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.jupnp.UpnpService; +import org.jupnp.model.message.discovery.OutgoingSearchRequest; +import org.jupnp.model.message.header.UDNHeader; +import org.jupnp.model.message.header.UpnpHeader; import org.jupnp.model.meta.RemoteDevice; +import org.jupnp.model.types.UDN; +import org.jupnp.transport.RouterException; import org.openhab.binding.upnpcontrol.internal.UpnpChannelName; import org.openhab.binding.upnpcontrol.internal.UpnpDynamicCommandDescriptionProvider; import org.openhab.binding.upnpcontrol.internal.UpnpDynamicStateDescriptionProvider; @@ -77,6 +83,7 @@ public abstract class UpnpHandler extends BaseThingHandler implements UpnpIOPart static final Pattern PROTOCOL_PATTERN = Pattern.compile("(?:.*):(?:.*):(.*):(?:.*)"); protected UpnpIOService upnpIOService; + protected UpnpService upnpService; protected volatile @Nullable RemoteDevice device; @@ -117,12 +124,14 @@ public abstract class UpnpHandler extends BaseThingHandler implements UpnpIOPart protected UpnpDynamicStateDescriptionProvider upnpStateDescriptionProvider; protected UpnpDynamicCommandDescriptionProvider upnpCommandDescriptionProvider; - public UpnpHandler(Thing thing, UpnpIOService upnpIOService, UpnpControlBindingConfiguration configuration, + public UpnpHandler(Thing thing, UpnpIOService upnpIOService, UpnpService upnpService, + UpnpControlBindingConfiguration configuration, UpnpDynamicStateDescriptionProvider upnpStateDescriptionProvider, UpnpDynamicCommandDescriptionProvider upnpCommandDescriptionProvider) { super(thing); this.upnpIOService = upnpIOService; + this.upnpService = upnpService; this.bindingConfig = configuration; @@ -652,4 +661,21 @@ private void cancelSubscriptionRefreshJob() { protected @Nullable RemoteDevice getDevice() { return device; } + + /** + * Send a device search request to the UPnP remote device. + * + * Some devices, such as LinkPlay based systems (WiiM, Arylic, etc.) loose their registrations over time. Sending a + * periodic search request will help keep the device registered. + */ + protected void sendDeviceSearchRequest() { + try { + UpnpHeader searchTarget = new UDNHeader(new UDN(getUDN())); + OutgoingSearchRequest searchRequest = new OutgoingSearchRequest(searchTarget, 5); + upnpService.getRouter().send(searchRequest); + logger.debug("M-SEARCH query sent for device UDN: {}", searchTarget.getValue()); + } catch (RouterException e) { + logger.debug("Failed to send M-SEARCH", e); + } + } } diff --git a/bundles/org.openhab.binding.upnpcontrol/src/main/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpRendererHandler.java b/bundles/org.openhab.binding.upnpcontrol/src/main/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpRendererHandler.java index 6f0165ddb10a8..6b8060b97f777 100644 --- a/bundles/org.openhab.binding.upnpcontrol/src/main/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpRendererHandler.java +++ b/bundles/org.openhab.binding.upnpcontrol/src/main/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpRendererHandler.java @@ -36,6 +36,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.jupnp.UpnpService; import org.jupnp.model.meta.RemoteDevice; import org.openhab.binding.upnpcontrol.internal.UpnpChannelName; import org.openhab.binding.upnpcontrol.internal.UpnpDynamicCommandDescriptionProvider; @@ -160,11 +161,12 @@ public class UpnpRendererHandler extends UpnpHandler { private volatile @Nullable ScheduledFuture trackPositionRefresh; private volatile int posAtNotificationStart = 0; - public UpnpRendererHandler(Thing thing, UpnpIOService upnpIOService, UpnpAudioSinkReg audioSinkReg, - UpnpDynamicStateDescriptionProvider upnpStateDescriptionProvider, + public UpnpRendererHandler(Thing thing, UpnpIOService upnpIOService, UpnpService upnpService, + UpnpAudioSinkReg audioSinkReg, UpnpDynamicStateDescriptionProvider upnpStateDescriptionProvider, UpnpDynamicCommandDescriptionProvider upnpCommandDescriptionProvider, UpnpControlBindingConfiguration configuration) { - super(thing, upnpIOService, configuration, upnpStateDescriptionProvider, upnpCommandDescriptionProvider); + super(thing, upnpIOService, upnpService, configuration, upnpStateDescriptionProvider, + upnpCommandDescriptionProvider); serviceSubscriptions.add(AV_TRANSPORT); serviceSubscriptions.add(RENDERING_CONTROL); @@ -218,6 +220,8 @@ public void dispose() { @Override protected void initJob() { synchronized (jobLock) { + sendDeviceSearchRequest(); + if (!upnpIOService.isRegistered(this)) { String msg = String.format("@text/offline.device-not-registered [ \"%s\" ]", getUDN()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg); diff --git a/bundles/org.openhab.binding.upnpcontrol/src/main/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpServerHandler.java b/bundles/org.openhab.binding.upnpcontrol/src/main/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpServerHandler.java index 343184cdf5ab8..e63b1582f3269 100644 --- a/bundles/org.openhab.binding.upnpcontrol/src/main/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpServerHandler.java +++ b/bundles/org.openhab.binding.upnpcontrol/src/main/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpServerHandler.java @@ -32,6 +32,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.jupnp.UpnpService; import org.openhab.binding.upnpcontrol.internal.UpnpDynamicCommandDescriptionProvider; import org.openhab.binding.upnpcontrol.internal.UpnpDynamicStateDescriptionProvider; import org.openhab.binding.upnpcontrol.internal.config.UpnpControlBindingConfiguration; @@ -100,12 +101,13 @@ public class UpnpServerHandler extends UpnpHandler { protected @NonNullByDefault({}) UpnpControlServerConfiguration config; - public UpnpServerHandler(Thing thing, UpnpIOService upnpIOService, + public UpnpServerHandler(Thing thing, UpnpIOService upnpIOService, UpnpService upnpService, ConcurrentMap upnpRenderers, UpnpDynamicStateDescriptionProvider upnpStateDescriptionProvider, UpnpDynamicCommandDescriptionProvider upnpCommandDescriptionProvider, UpnpControlBindingConfiguration configuration) { - super(thing, upnpIOService, configuration, upnpStateDescriptionProvider, upnpCommandDescriptionProvider); + super(thing, upnpIOService, upnpService, configuration, upnpStateDescriptionProvider, + upnpCommandDescriptionProvider); this.upnpRenderers = upnpRenderers; // put root as highest level in parent map diff --git a/bundles/org.openhab.binding.upnpcontrol/src/test/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpHandlerTest.java b/bundles/org.openhab.binding.upnpcontrol/src/test/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpHandlerTest.java index cfd2aae97f56a..b1ae7a4e9573b 100644 --- a/bundles/org.openhab.binding.upnpcontrol/src/test/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpHandlerTest.java +++ b/bundles/org.openhab.binding.upnpcontrol/src/test/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpHandlerTest.java @@ -28,6 +28,10 @@ import org.eclipse.jdt.annotation.Nullable; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; +import org.jupnp.UpnpService; +import org.jupnp.model.message.discovery.OutgoingSearchRequest; +import org.jupnp.transport.Router; +import org.jupnp.transport.RouterException; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; @@ -67,6 +71,9 @@ public class UpnpHandlerTest { @Mock protected @Nullable UpnpIOService upnpIOService; + @Mock + protected @Nullable UpnpService upnpService; + @Mock protected @Nullable UpnpDynamicStateDescriptionProvider upnpStateDescriptionProvider; @@ -115,6 +122,16 @@ public void setUp() { // stub config for initialize when(config.as(UpnpControlConfiguration.class)).thenReturn(new UpnpControlConfiguration()); + + upnpService = mock(UpnpService.class); + Router router = mock(Router.class); + when(upnpService.getRouter()).thenReturn(router); + try { + doNothing().when(router).send(any(OutgoingSearchRequest.class)); + } catch (RouterException e) { + // This will never happen in the test since doNothing doesn't trigger behavior + throw new RuntimeException("Unexpected exception in test setup", e); + } } protected void initHandler(UpnpHandler handler) { diff --git a/bundles/org.openhab.binding.upnpcontrol/src/test/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpRendererHandlerTest.java b/bundles/org.openhab.binding.upnpcontrol/src/test/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpRendererHandlerTest.java index 893bc560a9c64..05875d61ecfb7 100644 --- a/bundles/org.openhab.binding.upnpcontrol/src/test/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpRendererHandlerTest.java +++ b/bundles/org.openhab.binding.upnpcontrol/src/test/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpRendererHandlerTest.java @@ -205,7 +205,7 @@ public void setUp() { upnpEntryQueue = new UpnpEntryQueue(entries, "54321"); handler = spy(new UpnpRendererHandler(requireNonNull(thing), requireNonNull(upnpIOService), - requireNonNull(audioSinkReg), requireNonNull(upnpStateDescriptionProvider), + requireNonNull(upnpService), requireNonNull(audioSinkReg), requireNonNull(upnpStateDescriptionProvider), requireNonNull(upnpCommandDescriptionProvider), configuration)); initHandler(requireNonNull(handler)); diff --git a/bundles/org.openhab.binding.upnpcontrol/src/test/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpServerHandlerTest.java b/bundles/org.openhab.binding.upnpcontrol/src/test/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpServerHandlerTest.java index ae4873a1da50f..bef9af436e18e 100644 --- a/bundles/org.openhab.binding.upnpcontrol/src/test/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpServerHandlerTest.java +++ b/bundles/org.openhab.binding.upnpcontrol/src/test/java/org/openhab/binding/upnpcontrol/internal/handler/UpnpServerHandlerTest.java @@ -169,9 +169,10 @@ public void setUp() { // stub config for initialize when(config.as(UpnpControlServerConfiguration.class)).thenReturn(new UpnpControlServerConfiguration()); - handler = spy(new UpnpServerHandler(requireNonNull(thing), requireNonNull(upnpIOService), - requireNonNull(upnpRenderers), requireNonNull(upnpStateDescriptionProvider), - requireNonNull(upnpCommandDescriptionProvider), configuration)); + handler = spy( + new UpnpServerHandler(requireNonNull(thing), requireNonNull(upnpIOService), requireNonNull(upnpService), + requireNonNull(upnpRenderers), requireNonNull(upnpStateDescriptionProvider), + requireNonNull(upnpCommandDescriptionProvider), configuration)); initHandler(requireNonNull(handler));