This commit is contained in:
Dan Cunningham 2025-01-08 23:17:46 +01:00 committed by GitHub
commit f7b716cf82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 63 additions and 13 deletions

View File

@ -145,7 +145,7 @@ public class UpnpControlHandlerFactory extends BaseThingHandlerFactory implement
}
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 @@ public class UpnpControlHandlerFactory extends BaseThingHandlerFactory implement
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));

View File

@ -28,7 +28,13 @@ import java.util.stream.Collectors;
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 @@ public abstract class UpnpHandler extends BaseThingHandler implements UpnpIOPart
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<UDN> 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);
}
}
}

View File

@ -36,6 +36,7 @@ import java.util.stream.Collectors;
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 class UpnpRendererHandler extends UpnpHandler {
@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);

View File

@ -32,6 +32,7 @@ import java.util.stream.Collectors;
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<String, UpnpRendererHandler> 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

View File

@ -28,6 +28,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
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 class UpnpHandlerTest {
// 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) {

View File

@ -205,7 +205,7 @@ public class UpnpRendererHandlerTest extends UpnpHandlerTest {
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));

View File

@ -169,9 +169,10 @@ public class UpnpServerHandlerTest extends UpnpHandlerTest {
// 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));