2 * Copyright (c) 2010-2020 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.gardena.internal;
15 import java.util.Collection;
16 import java.util.Date;
17 import java.util.HashMap;
18 import java.util.HashSet;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.ScheduledExecutorService;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25 import java.util.stream.Collectors;
27 import org.apache.commons.lang.ObjectUtils;
28 import org.apache.commons.lang.StringUtils;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.eclipse.jetty.client.HttpResponseException;
31 import org.eclipse.jetty.client.api.ContentResponse;
32 import org.eclipse.jetty.client.api.Request;
33 import org.eclipse.jetty.client.util.StringContentProvider;
34 import org.eclipse.jetty.http.HttpHeader;
35 import org.eclipse.jetty.http.HttpMethod;
36 import org.eclipse.jetty.util.ssl.SslContextFactory;
37 import org.openhab.binding.gardena.internal.config.GardenaConfig;
38 import org.openhab.binding.gardena.internal.config.GardenaConfigWrapper;
39 import org.openhab.binding.gardena.internal.exception.GardenaDeviceNotFoundException;
40 import org.openhab.binding.gardena.internal.exception.GardenaException;
41 import org.openhab.binding.gardena.internal.exception.GardenaUnauthorizedException;
42 import org.openhab.binding.gardena.internal.model.Ability;
43 import org.openhab.binding.gardena.internal.model.Device;
44 import org.openhab.binding.gardena.internal.model.Devices;
45 import org.openhab.binding.gardena.internal.model.Errors;
46 import org.openhab.binding.gardena.internal.model.Location;
47 import org.openhab.binding.gardena.internal.model.Locations;
48 import org.openhab.binding.gardena.internal.model.NoResult;
49 import org.openhab.binding.gardena.internal.model.Property;
50 import org.openhab.binding.gardena.internal.model.PropertyValue;
51 import org.openhab.binding.gardena.internal.model.Session;
52 import org.openhab.binding.gardena.internal.model.SessionWrapper;
53 import org.openhab.binding.gardena.internal.model.Setting;
54 import org.openhab.binding.gardena.internal.model.command.Command;
55 import org.openhab.binding.gardena.internal.model.command.MowerParkUntilFurtherNoticeCommand;
56 import org.openhab.binding.gardena.internal.model.command.MowerParkUntilNextTimerCommand;
57 import org.openhab.binding.gardena.internal.model.command.MowerStartOverrideTimerCommand;
58 import org.openhab.binding.gardena.internal.model.command.MowerStartResumeScheduleCommand;
59 import org.openhab.binding.gardena.internal.model.command.SensorMeasureAmbientTemperatureCommand;
60 import org.openhab.binding.gardena.internal.model.command.SensorMeasureLightCommand;
61 import org.openhab.binding.gardena.internal.model.command.SensorMeasureSoilHumidityCommand;
62 import org.openhab.binding.gardena.internal.model.command.SensorMeasureSoilTemperatureCommand;
63 import org.openhab.binding.gardena.internal.model.command.SettingCommand;
64 import org.openhab.binding.gardena.internal.model.command.SettingCommandWrapper;
65 import org.openhab.binding.gardena.internal.model.command.WateringCancelOverrideCommand;
66 import org.openhab.binding.gardena.internal.model.command.WateringManualOverrideCommand;
67 import org.openhab.binding.gardena.internal.model.deser.DateDeserializer;
68 import org.openhab.binding.gardena.internal.model.deser.PropertyValueDeserializer;
69 import org.openhab.binding.gardena.internal.model.property.BaseProperty;
70 import org.openhab.binding.gardena.internal.model.property.IrrigationControlWateringProperty;
71 import org.openhab.binding.gardena.internal.model.property.PropertyWrapper;
72 import org.openhab.binding.gardena.internal.model.property.StringProperty;
73 import org.slf4j.Logger;
74 import org.slf4j.LoggerFactory;
76 import com.google.gson.Gson;
77 import com.google.gson.GsonBuilder;
80 * {@link GardenaSmart} implementation to access Gardena Smart Home.
82 * @author Gerhard Riegler - Initial contribution
84 public class GardenaSmartImpl implements GardenaSmart {
85 private final Logger logger = LoggerFactory.getLogger(GardenaSmartImpl.class);
87 public static final String DEVICE_CATEGORY_PUMP = "electronic_pressure_pump";
88 private static final String ABILITY_MOWER = "mower";
89 private static final String ABILITY_OUTLET = "outlet";
90 private static final String ABILITY_HUMIDITY = "humidity";
91 private static final String ABILITY_LIGHT = "light";
92 private static final String ABILITY_AMBIENT_TEMPERATURE = "ambient_temperature";
93 private static final String ABILITY_SOIL_TEMPERATURE = "soil_temperature";
94 private static final String ABILITY_POWER = "power";
95 private static final String ABILITY_WATERING = "watering";
96 private static final String ABILITY_MANUAL_WATERING = "manual_watering";
98 private static final String PROPERTY_BUTTON_MANUAL_OVERRIDE_TIME = "button_manual_override_time";
99 private static final String PROPERTY_POWER_TIMER = "power_timer";
100 private static final String PROPERTY_WATERING_TIMER = "watering_timer_";
101 private static final String PROPERTY_MANUAL_WATERING_TIMER = "manual_watering_timer";
103 private static final String DEVICE_CATEGORY_MOWER = "mower";
104 private static final String DEVICE_CATEGORY_GATEWAY = "gateway";
106 private static final String DEFAULT_MOWER_DURATION = "180";
108 private static final String URL = "https://smart.gardena.com";
109 private static final String URL_LOGIN = URL + "/v1/auth/token";
110 private static final String URL_LOCATIONS = URL + "/v1/locations/?user_id=";
111 private static final String URL_DEVICES = URL + "/v1/devices/?locationId=";
112 private static final String URL_COMMAND = URL + "/v1/devices/%s/abilities/%s/command?locationId=%s";
113 private static final String URL_PROPERTY = URL + "/v1/devices/%s/abilities/%s/properties/%s?locationId=%s";
114 private static final String URL_SETTING = URL + "/v1/devices/%s/settings/%s?locationId=%s";
116 private Gson gson = new GsonBuilder().registerTypeAdapter(Date.class, new DateDeserializer())
117 .registerTypeAdapter(PropertyValue.class, new PropertyValueDeserializer()).create();
118 private HttpClient httpClient;
120 private String mowerDuration = DEFAULT_MOWER_DURATION;
121 private Session session;
122 private GardenaConfig config;
125 private ScheduledExecutorService scheduler;
126 private ScheduledFuture<?> refreshThreadFuture;
127 private RefreshDevicesThread refreshDevicesThread = new RefreshDevicesThread();
129 private GardenaSmartEventListener eventListener;
131 private Map<String, Device> allDevicesById = new HashMap<>();
132 private Set<Location> allLocations = new HashSet<>();
135 public void init(String id, GardenaConfig config, GardenaSmartEventListener eventListener,
136 ScheduledExecutorService scheduler) throws GardenaException {
138 this.config = config;
139 this.eventListener = eventListener;
140 this.scheduler = scheduler;
142 if (!config.isValid()) {
143 throw new GardenaException("Invalid config, no email or password specified");
146 httpClient = new HttpClient(new SslContextFactory(true));
147 httpClient.setConnectTimeout(config.getConnectionTimeout() * 1000L);
151 } catch (Exception ex) {
152 throw new GardenaException(ex.getMessage(), ex);
159 public void dispose() {
160 stopRefreshThread(true);
161 if (httpClient != null) {
164 } catch (Exception e) {
167 httpClient.destroy();
169 allLocations.clear();
170 allDevicesById.clear();
174 * Schedules the device refresh thread.
176 private void startRefreshThread() {
177 refreshThreadFuture = scheduler.scheduleWithFixedDelay(refreshDevicesThread, 6, config.getRefresh(),
182 * Stops the device refresh thread.
184 private void stopRefreshThread(boolean force) {
185 if (refreshThreadFuture != null) {
186 refreshThreadFuture.cancel(force);
191 public String getId() {
196 public Set<Location> getLocations() {
201 public Device getDevice(String deviceId) throws GardenaException {
202 Device device = allDevicesById.get(deviceId);
203 if (device == null) {
204 throw new GardenaDeviceNotFoundException(
205 String.format("Device with id '%s' not found on gateway '%s'", deviceId, id));
211 public void loadAllDevices() throws GardenaException {
212 stopRefreshThread(false);
214 allLocations.clear();
215 allDevicesById.clear();
218 Locations locations = executeRequest(HttpMethod.GET,
219 URL_LOCATIONS + session.getSessionAttributes().getUserId(), null, Locations.class);
221 for (Location location : locations.getLocations()) {
222 allLocations.add(location);
223 Devices devices = loadDevices(location);
224 for (Device device : devices.getDevices()) {
225 if (DEVICE_CATEGORY_GATEWAY.equals(device.getCategory())) {
226 location.getDeviceIds().remove(device.getId());
228 allDevicesById.put(device.getId(), device);
233 startRefreshThread();
238 * Loads all devices for the location, adds virtual properties for commands.
240 private Devices loadDevices(Location location) throws GardenaException {
241 Devices devices = executeRequest(HttpMethod.GET, URL_DEVICES + location.getId(), null, Devices.class);
242 for (Device device : devices.getDevices()) {
243 device.setLocation(location);
244 for (Ability ability : device.getAbilities()) {
245 ability.setDevice(device);
246 for (Property property : ability.getProperties()) {
247 property.setAbility(ability);
249 if (device.getCategory().equals(DEVICE_CATEGORY_PUMP)) {
250 if (property.getName().equals(PROPERTY_MANUAL_WATERING_TIMER)) {
251 Integer duration = getIntegerValue(property.getValueAsString());
252 if (duration == null) {
255 property.setValue(new PropertyValue(String.valueOf(duration / 60)));
260 for (Setting setting : device.getSettings()) {
261 setting.setDevice(device);
264 if (DEVICE_CATEGORY_MOWER.equals(device.getCategory())) {
265 Ability mower = device.getAbility(ABILITY_MOWER);
266 mower.addProperty(new Property(GardenaSmartCommandName.PARK_UNTIL_NEXT_TIMER, "false"));
267 mower.addProperty(new Property(GardenaSmartCommandName.PARK_UNTIL_FURTHER_NOTICE, "false"));
268 mower.addProperty(new Property(GardenaSmartCommandName.START_RESUME_SCHEDULE, "false"));
269 mower.addProperty(new Property(GardenaSmartCommandName.START_OVERRIDE_TIMER, "false"));
271 mower.addProperty(new Property(GardenaSmartCommandName.DURATION_PROPERTY, mowerDuration));
278 public void sendCommand(Device device, GardenaSmartCommandName commandName, Object value) throws GardenaException {
279 Ability ability = null;
280 Command command = null;
282 switch (commandName) {
283 case PARK_UNTIL_NEXT_TIMER:
284 ability = device.getAbility(ABILITY_MOWER);
285 command = new MowerParkUntilNextTimerCommand();
287 case PARK_UNTIL_FURTHER_NOTICE:
288 ability = device.getAbility(ABILITY_MOWER);
289 command = new MowerParkUntilFurtherNoticeCommand();
291 case START_RESUME_SCHEDULE:
292 ability = device.getAbility(ABILITY_MOWER);
293 command = new MowerStartResumeScheduleCommand();
295 case START_OVERRIDE_TIMER:
296 ability = device.getAbility(ABILITY_MOWER);
297 command = new MowerStartOverrideTimerCommand(mowerDuration);
299 case DURATION_PROPERTY:
301 throw new GardenaException("Command '" + commandName + "' requires a value");
303 mowerDuration = ObjectUtils.toString(value);
305 case MEASURE_AMBIENT_TEMPERATURE:
306 ability = device.getAbility(ABILITY_AMBIENT_TEMPERATURE);
307 command = new SensorMeasureAmbientTemperatureCommand();
310 ability = device.getAbility(ABILITY_LIGHT);
311 command = new SensorMeasureLightCommand();
313 case MEASURE_SOIL_HUMIDITY:
314 ability = device.getAbility(ABILITY_HUMIDITY);
315 command = new SensorMeasureSoilHumidityCommand();
317 case MEASURE_SOIL_TEMPERATURE:
318 ability = device.getAbility(ABILITY_SOIL_TEMPERATURE);
319 command = new SensorMeasureSoilTemperatureCommand();
321 case OUTLET_MANUAL_OVERRIDE_TIME:
323 throw new GardenaException("Command '" + commandName + "' requires a value");
325 StringProperty prop = new StringProperty(PROPERTY_BUTTON_MANUAL_OVERRIDE_TIME,
326 ObjectUtils.toString(value));
328 executeSetProperty(device, ABILITY_OUTLET, PROPERTY_BUTTON_MANUAL_OVERRIDE_TIME, prop);
331 ability = device.getAbility(ABILITY_OUTLET);
332 if (value != null && value == Boolean.TRUE) {
333 String wateringDuration = device.getAbility(ABILITY_OUTLET)
334 .getProperty(PROPERTY_BUTTON_MANUAL_OVERRIDE_TIME).getValueAsString();
335 command = new WateringManualOverrideCommand(wateringDuration);
337 command = new WateringCancelOverrideCommand();
342 throw new GardenaException("Command '" + commandName + "' requires a value");
344 prop = new StringProperty(PROPERTY_POWER_TIMER, ObjectUtils.toString(value));
345 executeSetProperty(device, ABILITY_POWER, PROPERTY_POWER_TIMER, prop);
347 case WATERING_TIMER_VALVE_1:
348 case WATERING_TIMER_VALVE_2:
349 case WATERING_TIMER_VALVE_3:
350 case WATERING_TIMER_VALVE_4:
351 case WATERING_TIMER_VALVE_5:
352 case WATERING_TIMER_VALVE_6:
354 throw new GardenaException("Command '" + commandName + "' requires a value");
355 } else if (!(value instanceof Integer)) {
356 throw new GardenaException("Watering duration value '" + value + "' not a number");
358 int valveId = Integer.parseInt(StringUtils.right(commandName.toString(), 1));
359 String wateringTimerProperty = PROPERTY_WATERING_TIMER + valveId;
360 IrrigationControlWateringProperty irrigationProp = new IrrigationControlWateringProperty(
361 wateringTimerProperty, (Integer) value, valveId);
362 executeSetProperty(device, ABILITY_WATERING, wateringTimerProperty, irrigationProp);
364 case PUMP_MANUAL_WATERING_TIMER:
365 Integer duration = getIntegerValue(value);
366 if (duration == null) {
367 throw new GardenaException("Command '" + commandName + "' requires a number value");
369 prop = new StringProperty(PROPERTY_MANUAL_WATERING_TIMER, String.valueOf(duration * 60));
371 executeSetProperty(device, ABILITY_MANUAL_WATERING, PROPERTY_MANUAL_WATERING_TIMER, prop);
374 throw new GardenaException("Unknown command " + commandName);
377 if (command != null) {
378 stopRefreshThread(false);
379 executeRequest(HttpMethod.POST, getCommandUrl(device, ability), command, NoResult.class);
380 startRefreshThread();
384 private Integer getIntegerValue(Object value) {
386 return Integer.valueOf(ObjectUtils.toString(value));
387 } catch (NumberFormatException ex) {
393 * Sends the new property value for the ability.
395 private void executeSetProperty(Device device, String ability, String property, BaseProperty value)
396 throws GardenaException {
397 String propertyUrl = String.format(URL_PROPERTY, device.getId(), ability, property,
398 device.getLocation().getId());
399 stopRefreshThread(false);
400 executeRequest(HttpMethod.PUT, propertyUrl, new PropertyWrapper(value), NoResult.class);
401 device.getAbility(ability).getProperty(property).setValue(new PropertyValue(value.getValue()));
402 startRefreshThread();
406 public void sendSetting(Setting setting, Object value) throws GardenaException {
407 SettingCommand settingCommand = new SettingCommand(setting.getName());
408 settingCommand.setDeviceId(setting.getDevice().getId());
409 settingCommand.setValue(value);
411 stopRefreshThread(false);
412 executeRequest(HttpMethod.PUT, getSettingUrl(setting), new SettingCommandWrapper(settingCommand),
414 startRefreshThread();
418 * Returns the command url.
420 private String getCommandUrl(Device device, Ability ability) throws GardenaException {
421 return String.format(URL_COMMAND, device.getId(), ability.getName(), device.getLocation().getId());
425 * Returns the settings url.
427 private String getSettingUrl(Setting setting) {
428 Device device = setting.getDevice();
429 return String.format(URL_SETTING, device.getId(), setting.getId(), device.getLocation().getId());
433 * Communicates with Gardena Smart Home and parses the result.
435 private synchronized <T> T executeRequest(HttpMethod method, String url, Object contentObject, Class<T> result)
436 throws GardenaException {
438 if (logger.isTraceEnabled()) {
439 logger.trace("{} request: {}", method, url);
440 if (contentObject != null) {
441 logger.trace("{} data : {}", method, gson.toJson(contentObject));
445 Request request = httpClient.newRequest(url).method(method)
446 .timeout(config.getConnectionTimeout(), TimeUnit.SECONDS)
447 .idleTimeout(config.getConnectionTimeout(), TimeUnit.SECONDS)
448 .header(HttpHeader.CONTENT_TYPE, "application/json").header(HttpHeader.ACCEPT, "application/json")
449 .header(HttpHeader.ACCEPT_ENCODING, "gzip");
451 if (contentObject != null) {
452 StringContentProvider content = new StringContentProvider(gson.toJson(contentObject));
453 request.content(content);
456 if (!result.equals(SessionWrapper.class)) {
458 request.header("authorization", "Bearer " + session.getToken());
459 request.header("authorization-provider", session.getSessionAttributes().getProvider());
462 ContentResponse contentResponse = request.send();
463 int status = contentResponse.getStatus();
464 if (logger.isTraceEnabled()) {
465 logger.trace("Status : {}", status);
466 logger.trace("Response: {}", contentResponse.getContentAsString());
470 throw new GardenaException(
471 gson.fromJson(contentResponse.getContentAsString(), Errors.class).toString());
472 } else if (status != 200 && status != 204 && status != 201) {
473 throw new GardenaException(String.format("Error %s %s", status, contentResponse.getReason()));
476 if (result == NoResult.class) {
480 return gson.fromJson(contentResponse.getContentAsString(), result);
481 } catch (ExecutionException ex) {
482 Throwable cause = ex.getCause();
483 if (cause instanceof HttpResponseException) {
484 HttpResponseException responseException = (HttpResponseException) ex.getCause();
485 int status = responseException.getResponse().getStatus();
487 throw new GardenaUnauthorizedException(ex.getCause());
490 throw new GardenaException(ex.getMessage(), ex);
491 } catch (Exception ex) {
492 throw new GardenaException(ex.getMessage(), ex);
497 * Verifies the Gardena Smart Home session and reconnects if necessary.
499 private void verifySession() throws GardenaException {
501 || session.getCreated() + (config.getSessionTimeout() * 60000) <= System.currentTimeMillis()) {
502 logger.trace("(Re)logging in to Gardena Smart Home");
503 session = executeRequest(HttpMethod.POST, URL_LOGIN, new GardenaConfigWrapper(config), SessionWrapper.class)
509 * Thread which refreshes the data from Gardena Smart Home.
511 private class RefreshDevicesThread implements Runnable {
512 private boolean connectionLost = false;
517 logger.debug("Refreshing gardena device data");
518 final Map<String, Device> newDevicesById = new HashMap<>();
520 for (Location location : allLocations) {
521 Devices devices = loadDevices(location);
522 for (Device device : devices.getDevices()) {
523 if (DEVICE_CATEGORY_GATEWAY.equals(device.getCategory())) {
524 location.getDeviceIds().remove(device.getId());
526 newDevicesById.put(device.getId(), device);
531 if (connectionLost) {
532 connectionLost = false;
533 logger.info("Connection resumed to Gardena Smart Home with id '{}'", id);
534 eventListener.onConnectionResumed();
537 // determine deleted devices
538 Collection<Device> deletedDevices = allDevicesById.values().stream()
539 .filter(d -> !newDevicesById.values().contains(d)).collect(Collectors.toSet());
541 // determine new devices
542 Collection<Device> newDevices = newDevicesById.values().stream()
543 .filter(d -> !allDevicesById.values().contains(d)).collect(Collectors.toSet());
545 // determine updated devices
546 Collection<Device> updatedDevices = allDevicesById.values().stream().distinct()
547 .filter(newDevicesById.values()::contains).collect(Collectors.toSet());
549 allDevicesById = newDevicesById;
551 for (Device deletedDevice : deletedDevices) {
552 eventListener.onDeviceDeleted(deletedDevice);
555 for (Device newDevice : newDevices) {
556 eventListener.onNewDevice(newDevice);
559 for (Device updatedDevice : updatedDevices) {
560 eventListener.onDeviceUpdated(updatedDevice);
563 } catch (GardenaException ex) {
564 if (!connectionLost) {
565 connectionLost = true;
566 logger.warn("Connection lost to Gardena Smart Home with id '{}'", id);
567 logger.trace("{}", ex.getMessage(), ex);
568 eventListener.onConnectionLost();