]> git.basschouten.com Git - openhab-addons.git/blob
2449abd6f7d792c3225bdb304f5e42a77c0352cc
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.io.hueemulation.internal.rest;
14
15 import java.net.URI;
16 import java.util.stream.Collectors;
17
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;
24
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;
47
48 /**
49  * This class is used by the status REST API for troubleshoot purposes.
50  * <p>
51  * The UPNP announcement is tested, the /description.xml reachability is checked,
52  * and some statistics are gathered.
53  *
54  * @author David Graeff - Initial contribution
55  */
56 @Component(immediate = false, service = StatusResource.class)
57 @JaxrsResource
58 @JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + HueEmulationService.REST_APP_NAME + ")")
59 @NonNullByDefault
60 @Path("")
61 public class StatusResource implements RegistryListener {
62     @Reference(cardinality = ReferenceCardinality.OPTIONAL, policyOption = ReferencePolicyOption.GREEDY)
63     protected @Nullable UpnpServer discovery;
64     @Reference
65     protected @NonNullByDefault({}) ConfigStore cs;
66     @Reference
67     protected @NonNullByDefault({}) UpnpService upnpService;
68
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,
74         success
75     }
76
77     private upnpStatus selfTestUpnpFound = upnpStatus.service_not_registered;
78
79     private final Logger logger = LoggerFactory.getLogger(StatusResource.class);
80
81     /**
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.
84      */
85     public void startUpnpSelfTest() {
86         Registry registry = upnpService.getRegistry();
87         if (registry == null) {
88             logger.warn("upnp service registry is null!");
89             return;
90         }
91
92         selfTestUpnpFound = upnpStatus.service_registered_but_no_UPnP_traffic_yet;
93
94         for (RemoteDevice device : registry.getRemoteDevices()) {
95             remoteDeviceAdded(registry, device);
96         }
97         for (LocalDevice device : registry.getLocalDevices()) {
98             localDeviceAdded(registry, device);
99         }
100
101         registry.addListener(this);
102
103         ControlPoint controlPoint = upnpService.getControlPoint();
104         if (controlPoint != null) {
105             controlPoint.search();
106         }
107     }
108
109     @GET
110     @Path("status/link")
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();
116     }
117
118     private static String toYesNo(boolean b) {
119         return b ? "yes" : "no";
120     }
121
122     private static String TD(String s) {
123         return "<td>" + s + "</td>";
124     }
125
126     private static String TR(String s) {
127         return "<tr>" + s + "</tr>";
128     }
129
130     @GET
131     @Path("status")
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!";
137         }
138
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>"
142                 + //
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>" + //
146                 "<p>%s</p>" + //
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>"
150                 + //
151                 "<h2>Users</h2><ul>%s</ul></body></html>";
152
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"));
156
157         String url = "http://" + cs.ds.config.ipaddress + ":" + localDiscovery.getDefaultport();
158
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"));
162
163         Registry registry = upnpService.getRegistry();
164         String upnps;
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"));
169         } else {
170             upnps = TR(TD("service not available") + TD(""));
171         }
172
173         if (!localDiscovery.upnpAnnouncementThreadRunning()) {
174             selfTestUpnpFound = upnpStatus.upnp_announcement_thread_not_running;
175         }
176
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);
182     }
183
184     @NonNullByDefault({})
185     @Override
186     public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) {
187     }
188
189     @NonNullByDefault({})
190     @Override
191     public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice device, Exception ex) {
192     }
193
194     @NonNullByDefault({})
195     @Override
196     public void remoteDeviceAdded(Registry registry, RemoteDevice device) {
197         if (selfTestUpnpFound == upnpStatus.success) {
198             return;
199         }
200         checkForDevice(getDetails(device));
201     }
202
203     private static DeviceDetails getDetails(@Nullable Device<?, ?, ?> device) {
204         if (device != null) {
205             DeviceDetails details = device.getDetails();
206             if (details != null) {
207                 return details;
208             }
209         }
210         return new DeviceDetails(null, "", null, null, "", null, null, null, null, null);
211     }
212
213     private void checkForDevice(DeviceDetails details) {
214         selfTestUpnpFound = upnpStatus.any_device_but_not_this_one_found;
215         try {
216             if (cs.ds.config.bridgeid.equals(details.getSerialNumber())) {
217                 selfTestUpnpFound = upnpStatus.success;
218             }
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);
221         }
222     }
223
224     @NonNullByDefault({})
225     @Override
226     public void remoteDeviceUpdated(Registry registry, RemoteDevice device) {
227     }
228
229     @NonNullByDefault({})
230     @Override
231     public void remoteDeviceRemoved(Registry registry, RemoteDevice device) {
232         UpnpServer localDiscovery = discovery;
233         if (selfTestUpnpFound != upnpStatus.success || localDiscovery == null
234                 || localDiscovery.upnpAnnouncementThreadRunning()) {
235             return;
236         }
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;
241         }
242     }
243
244     @NonNullByDefault({})
245     @Override
246     public void localDeviceAdded(Registry registry, LocalDevice device) {
247         UpnpServer localDiscovery = discovery;
248         if (selfTestUpnpFound == upnpStatus.success || localDiscovery == null
249                 || localDiscovery.upnpAnnouncementThreadRunning()) {
250             return;
251         }
252         checkForDevice(getDetails(device));
253     }
254
255     @NonNullByDefault({})
256     @Override
257     public void localDeviceRemoved(Registry registry, LocalDevice device) {
258     }
259
260     @NonNullByDefault({})
261     @Override
262     public void beforeShutdown(Registry registry) {
263     }
264
265     @Override
266     public void afterShutdown() {
267         selfTestUpnpFound = upnpStatus.service_not_registered;
268     }
269 }