2 * Copyright (c) 2010-2021 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.binding.hdpowerview.internal;
15 import java.time.Instant;
17 import javax.ws.rs.ProcessingException;
18 import javax.ws.rs.client.Client;
19 import javax.ws.rs.client.Entity;
20 import javax.ws.rs.client.Invocation;
21 import javax.ws.rs.client.WebTarget;
22 import javax.ws.rs.core.MediaType;
23 import javax.ws.rs.core.Response;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
28 import org.openhab.binding.hdpowerview.internal.api.requests.ShadeMove;
29 import org.openhab.binding.hdpowerview.internal.api.requests.ShadeStop;
30 import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
31 import org.openhab.binding.hdpowerview.internal.api.responses.Shade;
32 import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
36 import com.google.gson.Gson;
37 import com.google.gson.JsonParseException;
40 * JAX-RS targets for communicating with an HD PowerView hub
42 * @author Andy Lintner - Initial contribution
43 * @author Andrew Fiddian-Green - Added support for secondary rail positions
46 public class HDPowerViewWebTargets {
48 private static final String PUT = "PUT";
49 private static final String GET = "GET";
50 private static final String SCENE_ID = "sceneId";
51 private static final String ID = "id";
52 private static final String REFRESH = "refresh";
53 private static final String CONN_HDR = "Connection";
54 private static final String CONN_VAL = "close"; // versus "keep-alive"
56 private final Logger logger = LoggerFactory.getLogger(HDPowerViewWebTargets.class);
59 * the hub returns a 423 error (resource locked) daily just after midnight;
60 * which means it is temporarily undergoing maintenance; so we use "soft"
61 * exception handling during the five minute maintenance period after a 423
64 private final int maintenancePeriod = 300;
65 private Instant maintenanceScheduledEnd = Instant.now().minusSeconds(2 * maintenancePeriod);
67 private WebTarget base;
68 private WebTarget shades;
69 private WebTarget shade;
70 private WebTarget sceneActivate;
71 private WebTarget scenes;
73 private final Gson gson = new Gson();
76 * Initialize the web targets
78 * @param client the Javax RS client (the binding)
79 * @param ipAddress the IP address of the server (the hub)
81 public HDPowerViewWebTargets(Client client, String ipAddress) {
82 base = client.target("http://" + ipAddress + "/api");
83 shades = base.path("shades/");
84 shade = base.path("shades/{id}");
85 sceneActivate = base.path("scenes");
86 scenes = base.path("scenes/");
90 * Fetches a JSON package that describes all shades in the hub, and wraps it in
91 * a Shades class instance
93 * @return Shades class instance
94 * @throws JsonParseException if there is a JSON parsing error
95 * @throws ProcessingException if there is any processing error
96 * @throws HubMaintenanceException if the hub is down for maintenance
98 public @Nullable Shades getShades() throws JsonParseException, ProcessingException, HubMaintenanceException {
99 String json = invoke(shades.request().header(CONN_HDR, CONN_VAL).buildGet(), shades, null);
100 return gson.fromJson(json, Shades.class);
104 * Instructs the hub to move a specific shade
106 * @param shadeId id of the shade to be moved
107 * @param position instance of ShadePosition containing the new position
108 * @throws ProcessingException if there is any processing error
109 * @throws HubMaintenanceException if the hub is down for maintenance
111 public void moveShade(int shadeId, ShadePosition position) throws ProcessingException, HubMaintenanceException {
112 WebTarget target = shade.resolveTemplate(ID, shadeId);
113 String json = gson.toJson(new ShadeMove(shadeId, position));
114 invoke(target.request().header(CONN_HDR, CONN_VAL)
115 .buildPut(Entity.entity(json, MediaType.APPLICATION_JSON_TYPE)), target, json);
120 * Fetches a JSON package that describes all scenes in the hub, and wraps it in
121 * a Scenes class instance
123 * @return Scenes class instance
124 * @throws JsonParseException if there is a JSON parsing error
125 * @throws ProcessingException if there is any processing error
126 * @throws HubMaintenanceException if the hub is down for maintenance
128 public @Nullable Scenes getScenes() throws JsonParseException, ProcessingException, HubMaintenanceException {
129 String json = invoke(scenes.request().header(CONN_HDR, CONN_VAL).buildGet(), scenes, null);
130 return gson.fromJson(json, Scenes.class);
134 * Instructs the hub to execute a specific scene
136 * @param sceneId id of the scene to be executed
137 * @throws ProcessingException if there is any processing error
138 * @throws HubMaintenanceException if the hub is down for maintenance
140 public void activateScene(int sceneId) throws ProcessingException, HubMaintenanceException {
141 WebTarget target = sceneActivate.queryParam(SCENE_ID, sceneId);
142 invoke(target.request().header(CONN_HDR, CONN_VAL).buildGet(), target, null);
145 private synchronized String invoke(Invocation invocation, WebTarget target, @Nullable String jsonCommand)
146 throws ProcessingException, HubMaintenanceException {
147 if (logger.isTraceEnabled()) {
148 logger.trace("API command {} {}", jsonCommand == null ? GET : PUT, target.getUri());
149 if (jsonCommand != null) {
150 logger.trace("JSON command = {}", jsonCommand);
155 response = invocation.invoke();
156 } catch (ProcessingException e) {
157 if (Instant.now().isBefore(maintenanceScheduledEnd)) {
158 // throw "softer" exception during maintenance window
159 logger.debug("Hub still undergoing maintenance");
160 throw new HubMaintenanceException("Hub still undergoing maintenance");
164 int statusCode = response.getStatus();
165 if (statusCode == 423) {
166 // set end of maintenance window, and throw a "softer" exception
167 maintenanceScheduledEnd = Instant.now().plusSeconds(maintenancePeriod);
168 logger.debug("Hub undergoing maintenance");
169 if (response.hasEntity()) {
170 response.readEntity(String.class);
173 throw new HubMaintenanceException("Hub undergoing maintenance");
175 if (statusCode != 200) {
176 logger.warn("Hub returned HTTP error '{}'", statusCode);
177 if (response.hasEntity()) {
178 response.readEntity(String.class);
181 throw new ProcessingException(String.format("HTTP %d error", statusCode));
183 if (!response.hasEntity()) {
184 logger.warn("Hub returned no content");
186 throw new ProcessingException("Missing response entity");
188 String jsonResponse = response.readEntity(String.class);
189 if (logger.isTraceEnabled()) {
190 logger.trace("JSON response = {}", jsonResponse);
196 * Fetches a JSON package that describes a specific shade in the hub, and wraps it
197 * in a Shade class instance
199 * @param shadeId id of the shade to be fetched
200 * @return Shade class instance
201 * @throws ProcessingException if there is any processing error
202 * @throws HubMaintenanceException if the hub is down for maintenance
204 public @Nullable Shade getShade(int shadeId) throws ProcessingException, HubMaintenanceException {
205 WebTarget target = shade.resolveTemplate(ID, shadeId);
206 String json = invoke(target.request().header(CONN_HDR, CONN_VAL).buildGet(), target, null);
207 return gson.fromJson(json, Shade.class);
211 * Instructs the hub to do a hard refresh (discovery on the hubs RF network) on
212 * a specific shade; fetches a JSON package that describes that shade, and wraps
213 * it in a Shade class instance
215 * @param shadeId id of the shade to be refreshed
216 * @return Shade class instance
217 * @throws ProcessingException if there is any processing error
218 * @throws HubMaintenanceException if the hub is down for maintenance
220 public @Nullable Shade refreshShade(int shadeId) throws ProcessingException, HubMaintenanceException {
221 WebTarget target = shade.resolveTemplate(ID, shadeId).queryParam(REFRESH, true);
222 String json = invoke(target.request().header(CONN_HDR, CONN_VAL).buildGet(), target, null);
223 return gson.fromJson(json, Shade.class);
227 * Tells the hub to stop movement of a specific shade
229 * @param shadeId id of the shade to be stopped
230 * @throws ProcessingException if there is any processing error
231 * @throws HubMaintenanceException if the hub is down for maintenance
233 public void stopShade(int shadeId) throws ProcessingException, HubMaintenanceException {
234 WebTarget target = shade.resolveTemplate(ID, shadeId);
235 String json = gson.toJson(new ShadeStop(shadeId));
236 invoke(target.request().header(CONN_HDR, CONN_VAL)
237 .buildPut(Entity.entity(json, MediaType.APPLICATION_JSON_TYPE)), target, json);