2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.io.hueemulation.internal.rest;
16 import java.util.stream.Collectors;
18 import javax.ws.rs.GET;
19 import javax.ws.rs.Path;
20 import javax.ws.rs.Produces;
21 import javax.ws.rs.core.Context;
22 import javax.ws.rs.core.Response;
23 import javax.ws.rs.core.UriInfo;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.jupnp.UpnpService;
28 import org.jupnp.controlpoint.ControlPoint;
29 import org.jupnp.model.meta.Device;
30 import org.jupnp.model.meta.DeviceDetails;
31 import org.jupnp.model.meta.LocalDevice;
32 import org.jupnp.model.meta.RemoteDevice;
33 import org.jupnp.registry.Registry;
34 import org.jupnp.registry.RegistryListener;
35 import org.openhab.io.hueemulation.internal.ConfigStore;
36 import org.openhab.io.hueemulation.internal.HueEmulationService;
37 import org.openhab.io.hueemulation.internal.upnp.UpnpServer;
38 import org.osgi.service.component.annotations.Component;
39 import org.osgi.service.component.annotations.Reference;
40 import org.osgi.service.component.annotations.ReferenceCardinality;
41 import org.osgi.service.component.annotations.ReferencePolicyOption;
42 import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
43 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
44 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
49 * This class is used by the status REST API for troubleshoot purposes.
51 * The UPNP announcement is tested, the /description.xml reachability is checked,
52 * and some statistics are gathered.
54 * @author David Graeff - Initial contribution
56 @Component(immediate = false, service = StatusResource.class)
58 @JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + HueEmulationService.REST_APP_NAME + ")")
61 public class StatusResource implements RegistryListener {
62 @Reference(cardinality = ReferenceCardinality.OPTIONAL, policyOption = ReferencePolicyOption.GREEDY)
63 protected @Nullable UpnpServer discovery;
65 protected @NonNullByDefault({}) ConfigStore cs;
67 protected @NonNullByDefault({}) UpnpService upnpService;
69 private enum upnpStatus {
70 service_not_registered,
71 service_registered_but_no_UPnP_traffic_yet,
72 upnp_announcement_thread_not_running,
73 any_device_but_not_this_one_found,
77 private upnpStatus selfTestUpnpFound = upnpStatus.service_not_registered;
79 private final Logger logger = LoggerFactory.getLogger(StatusResource.class);
82 * This will register to the {@link UpnpService} registry to get notified of new UPNP devices and check all existing
83 * devices if they match our UPNP announcement.
85 public void startUpnpSelfTest() {
86 Registry registry = upnpService.getRegistry();
87 if (registry == null) {
88 logger.warn("upnp service registry is null!");
92 selfTestUpnpFound = upnpStatus.service_registered_but_no_UPnP_traffic_yet;
94 for (RemoteDevice device : registry.getRemoteDevices()) {
95 remoteDeviceAdded(registry, device);
97 for (LocalDevice device : registry.getLocalDevices()) {
98 localDeviceAdded(registry, device);
101 registry.addListener(this);
103 ControlPoint controlPoint = upnpService.getControlPoint();
104 if (controlPoint != null) {
105 controlPoint.search();
111 @Produces("text/plain")
112 public Response link(@Context UriInfo uri) {
113 cs.setLinkbutton(!cs.ds.config.linkbutton, true, uri.getQueryParameters().containsKey("v1"));
114 URI newuri = uri.getBaseUri().resolve("/api/status");
115 return Response.seeOther(newuri).build();
118 private static String toYesNo(boolean b) {
119 return b ? "yes" : "no";
122 private static String TD(String s) {
123 return "<td>" + s + "</td>";
126 private static String TR(String s) {
127 return "<tr>" + s + "</tr>";
132 @Produces("text/html")
133 public String getStatus() {
134 UpnpServer localDiscovery = discovery;
135 if (localDiscovery == null) { // Optional service wiring
136 return "UPnP Server service not started!";
139 String format = "<html><body><h1>Self test</h1>" + //
140 "<p>To access any links you need be in pairing mode!</p>" + //
141 "<p>Pairing mode: %s (%s) <a href='%s/api/status/link'>Enable</a> | <a href='%s/api/status/link?V1=true'>Enable with bridge V1 emulation</a></p>"
143 "%d published lights (see <a href='%s/api/testuser/lights'>%s/api/testuser/lights</a>)<br>" + //
144 "%d published sensors (see <a href='%s/api/testuser/sensors'>%s/api/testuser/sensors</a>)<br>" + //
145 "<h2>UPnP discovery test</h2>" + //
147 "<table style='border:1px solid black'><tr><td>serial no</td><td>name</td></tr>%s</table>" + //
148 "<h2>Reachability test</h2>" + //
149 "<table style='border:1px solid black'><tr><td>URL</td><td>Responds?</td><td>Ours?</td></tr>%s</table>"
151 "<h2>Users</h2><ul>%s</ul></body></html>";
153 String users = cs.ds.config.whitelist.entrySet().stream().map(user -> "<li>" + user.getKey() + " <b>"
154 + user.getValue().name + "</b> <small>" + user.getValue().lastUseDate + "</small>")
155 .collect(Collectors.joining("\n"));
157 String url = "http://" + cs.ds.config.ipaddress + ":" + localDiscovery.getDefaultport();
159 String reachable = localDiscovery.selfTests().stream()
160 .map(entry -> TR(TD(entry.address) + TD(toYesNo(entry.reachable)) + TD(toYesNo(entry.isOurs))))
161 .collect(Collectors.joining("\n"));
163 Registry registry = upnpService.getRegistry();
165 if (registry != null) {
166 upnps = registry.getRemoteDevices().stream().map(device -> getDetails(device))
167 .map(details -> TR(TD(details.getSerialNumber()) + TD(details.getFriendlyName())))
168 .collect(Collectors.joining("\n"));
170 upnps = TR(TD("service not available") + TD(""));
173 if (!localDiscovery.upnpAnnouncementThreadRunning()) {
174 selfTestUpnpFound = upnpStatus.upnp_announcement_thread_not_running;
177 return String.format(format, cs.ds.config.linkbutton ? "On" : "Off",
178 cs.getConfig().temporarilyEmulateV1bridge ? "V1" : "V2", url, url, //
179 cs.ds.lights.size(), url, url, cs.ds.sensors.size(), url, url, //
180 selfTestUpnpFound.name().replace('_', ' '), //
181 upnps, reachable, users);
184 @NonNullByDefault({})
186 public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) {
189 @NonNullByDefault({})
191 public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice device, Exception ex) {
194 @NonNullByDefault({})
196 public void remoteDeviceAdded(Registry registry, RemoteDevice device) {
197 if (selfTestUpnpFound == upnpStatus.success) {
200 checkForDevice(getDetails(device));
203 private static DeviceDetails getDetails(@Nullable Device<?, ?, ?> device) {
204 if (device != null) {
205 DeviceDetails details = device.getDetails();
206 if (details != null) {
210 return new DeviceDetails(null, "", null, null, "", null, null, null, null, null);
213 private void checkForDevice(DeviceDetails details) {
214 selfTestUpnpFound = upnpStatus.any_device_but_not_this_one_found;
216 if (cs.ds.config.bridgeid.equals(details.getSerialNumber())) {
217 selfTestUpnpFound = upnpStatus.success;
219 } catch (Exception e) { // We really don't want the service to fail on any exception
220 logger.warn("upnp service: adding services failed: {}", details.getFriendlyName(), e);
224 @NonNullByDefault({})
226 public void remoteDeviceUpdated(Registry registry, RemoteDevice device) {
229 @NonNullByDefault({})
231 public void remoteDeviceRemoved(Registry registry, RemoteDevice device) {
232 UpnpServer localDiscovery = discovery;
233 if (selfTestUpnpFound != upnpStatus.success || localDiscovery == null
234 || localDiscovery.upnpAnnouncementThreadRunning()) {
237 DeviceDetails details = getDetails(device);
238 String serialNo = details.getSerialNumber();
239 if (cs.ds.config.bridgeid.equals(serialNo)) {
240 selfTestUpnpFound = upnpStatus.any_device_but_not_this_one_found;
244 @NonNullByDefault({})
246 public void localDeviceAdded(Registry registry, LocalDevice device) {
247 UpnpServer localDiscovery = discovery;
248 if (selfTestUpnpFound == upnpStatus.success || localDiscovery == null
249 || localDiscovery.upnpAnnouncementThreadRunning()) {
252 checkForDevice(getDetails(device));
255 @NonNullByDefault({})
257 public void localDeviceRemoved(Registry registry, LocalDevice device) {
260 @NonNullByDefault({})
262 public void beforeShutdown(Registry registry) {
266 public void afterShutdown() {
267 selfTestUpnpFound = upnpStatus.service_not_registered;