org.openhab.binding.gardena.internal.GardenaSmartImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.binding.gardena.internal.GardenaSmartImpl.java

Source

/**
 * Copyright (c) 2010-2017 by the respective copyright holders.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.openhab.binding.gardena.internal;

import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ObjectUtils;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpResponseException;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.gardena.internal.config.GardenaConfig;
import org.openhab.binding.gardena.internal.config.GardenaConfigWrapper;
import org.openhab.binding.gardena.internal.exception.GardenaDeviceNotFoundException;
import org.openhab.binding.gardena.internal.exception.GardenaException;
import org.openhab.binding.gardena.internal.exception.GardenaUnauthorizedException;
import org.openhab.binding.gardena.internal.model.Ability;
import org.openhab.binding.gardena.internal.model.Device;
import org.openhab.binding.gardena.internal.model.Devices;
import org.openhab.binding.gardena.internal.model.Errors;
import org.openhab.binding.gardena.internal.model.Location;
import org.openhab.binding.gardena.internal.model.Locations;
import org.openhab.binding.gardena.internal.model.NoResult;
import org.openhab.binding.gardena.internal.model.Property;
import org.openhab.binding.gardena.internal.model.Session;
import org.openhab.binding.gardena.internal.model.SessionWrapper;
import org.openhab.binding.gardena.internal.model.command.Command;
import org.openhab.binding.gardena.internal.model.command.MowerParkUntilFurtherNoticeCommand;
import org.openhab.binding.gardena.internal.model.command.MowerParkUntilNextTimerCommand;
import org.openhab.binding.gardena.internal.model.command.MowerStartOverrideTimerCommand;
import org.openhab.binding.gardena.internal.model.command.MowerStartResumeScheduleCommand;
import org.openhab.binding.gardena.internal.model.command.SensorMeasureAmbientTemperatureCommand;
import org.openhab.binding.gardena.internal.model.command.SensorMeasureLightCommand;
import org.openhab.binding.gardena.internal.model.command.SensorMeasureSoilHumidityCommand;
import org.openhab.binding.gardena.internal.model.command.SensorMeasureSoilTemperatureCommand;
import org.openhab.binding.gardena.internal.model.command.WateringCancelOverrideCommand;
import org.openhab.binding.gardena.internal.model.command.WateringManualOverrideCommand;
import org.openhab.binding.gardena.internal.model.deser.DateDeserializer;
import org.openhab.binding.gardena.internal.model.property.SimpleProperties;
import org.openhab.binding.gardena.internal.model.property.SimplePropertiesWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

/**
 * {@link GardenaSmart} implementation to access Gardena Smart Home.
 *
 * @author Gerhard Riegler - Initial contribution
 */
public class GardenaSmartImpl implements GardenaSmart {
    private final Logger logger = LoggerFactory.getLogger(GardenaSmartImpl.class);

    private static final String ABILITY_MOWER = "mower";
    private static final String ABILITY_OUTLET = "outlet";
    private static final String ABILITY_HUMIDITY = "humidity";
    private static final String ABILITY_LIGHT = "light";
    private static final String ABILITY_AMBIENT_TEMPERATURE = "ambient_temperature";
    private static final String ABILITY_SOIL_TEMPERATURE = "soil_temperature";

    private static final String PROPERTY_BUTTON_MANUAL_OVERRIDE_TIME = "button_manual_override_time";

    private static final String DEVICE_CATEGORY_MOWER = "mower";
    private static final String DEVICE_CATEGORY_GATEWAY = "gateway";

    private static final String DEFAULT_MOWER_DURATION = "180";

    private static final String URL = "https://smart.gardena.com";
    private static final String URL_LOGIN = URL + "/sg-1/sessions";
    private static final String URL_LOCATIONS = URL + "/sg-1/locations/?user_id=";
    private static final String URL_DEVICES = URL + "/sg-1/devices/?locationId=";
    private static final String URL_COMMAND = URL + "/sg-1/devices/%s/abilities/%s/command?locationId=%s";
    private static final String URL_PROPERTY = URL + "/sg-1/devices/%s/abilities/%s/properties/%s?locationId=%s";

    private Gson gson = new GsonBuilder().registerTypeAdapter(Date.class, new DateDeserializer()).create();
    private HttpClient httpClient;

    private String mowerDuration = DEFAULT_MOWER_DURATION;
    private Session session;
    private GardenaConfig config;
    private String id;

    private ScheduledExecutorService scheduler;
    private ScheduledFuture<?> refreshThreadFuture;
    private RefreshDevicesThread refreshDevicesThread = new RefreshDevicesThread();

    private GardenaSmartEventListener eventListener;

    private Map<String, Device> allDevicesById = new HashMap<String, Device>();
    private Set<Location> allLocations = new HashSet<Location>();

    /**
     * {@inheritDoc}
     */
    @Override
    public void init(String id, GardenaConfig config, GardenaSmartEventListener eventListener,
            ScheduledExecutorService scheduler) throws GardenaException {
        this.id = id;
        this.config = config;
        this.eventListener = eventListener;
        this.scheduler = scheduler;

        if (!config.isValid()) {
            throw new GardenaException("Invalid config, no email or password specified");
        }

        httpClient = new HttpClient(new SslContextFactory(true));
        httpClient.setConnectTimeout(config.getConnectionTimeout() * 1000L);

        try {
            httpClient.start();
        } catch (Exception ex) {
            throw new GardenaException(ex.getMessage(), ex);
        }

        loadAllDevices();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void dispose() {
        stopRefreshThread();
        if (httpClient != null) {
            try {
                httpClient.stop();
            } catch (Exception e) {
                // ignore
            }
            httpClient.destroy();
        }
        allLocations.clear();
        allDevicesById.clear();
    }

    /**
     * Schedules the device refresh thread.
     */
    private void startRefreshThread() {
        refreshThreadFuture = scheduler.scheduleWithFixedDelay(refreshDevicesThread, config.getRefresh(),
                config.getRefresh(), TimeUnit.SECONDS);
    }

    /**
     * Stops the device refresh thread.
     */
    private void stopRefreshThread() {
        if (refreshThreadFuture != null) {
            refreshThreadFuture.cancel(true);
        }
    }

    /**
     * Schedules a intermediate device refresh.
     */
    private void scheduleIntermediateRefresh() {
        if (refreshThreadFuture != null) {
            if (refreshThreadFuture.getDelay(TimeUnit.SECONDS) > 5) {
                scheduler.schedule(refreshDevicesThread, 3, TimeUnit.SECONDS);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getId() {
        return id;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Set<Location> getLocations() {
        return allLocations;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Device getDevice(String deviceId) throws GardenaException {
        Device device = allDevicesById.get(deviceId);
        if (device == null) {
            throw new GardenaDeviceNotFoundException(
                    String.format("Device with id '%s' not found on gateway '%s'", deviceId, id));
        }
        return device;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void loadAllDevices() throws GardenaException {
        stopRefreshThread();
        try {
            allLocations.clear();
            allDevicesById.clear();

            verifySession();
            Locations locations = executeRequest(HttpMethod.GET, URL_LOCATIONS + session.getUserId(), null,
                    Locations.class);

            for (Location location : locations.getLocations()) {
                allLocations.add(location);
                Devices devices = loadDevices(location);
                for (Device device : devices.getDevices()) {
                    if (DEVICE_CATEGORY_GATEWAY.equals(device.getCategory())) {
                        location.getDeviceIds().remove(device.getId());
                    } else {
                        allDevicesById.put(device.getId(), device);
                    }
                }
            }
        } finally {
            startRefreshThread();
        }
    }

    /**
     * Loads all devices for the location, adds virtual properties for commands.
     */
    private Devices loadDevices(Location location) throws GardenaException {
        Devices devices = executeRequest(HttpMethod.GET, URL_DEVICES + location.getId(), null, Devices.class);
        for (Device device : devices.getDevices()) {
            device.setLocation(location);
            for (Ability ability : device.getAbilities()) {
                ability.setDevice(device);
                for (Property property : ability.getProperties()) {
                    property.setAbility(ability);
                }
            }

            if (DEVICE_CATEGORY_MOWER.equals(device.getCategory())) {
                Ability mower = device.getAbility(ABILITY_MOWER);
                mower.addProperty(new Property(GardenaSmartCommandName.PARK_UNTIL_NEXT_TIMER, "false"));
                mower.addProperty(new Property(GardenaSmartCommandName.PARK_UNTIL_FURTHER_NOTICE, "false"));
                mower.addProperty(new Property(GardenaSmartCommandName.START_RESUME_SCHEDULE, "false"));
                mower.addProperty(new Property(GardenaSmartCommandName.START_OVERRIDE_TIMER, "false"));

                mower.addProperty(new Property(GardenaSmartCommandName.DURATION_PROPERTY, mowerDuration));
            }
        }
        return devices;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void sendCommand(Device device, GardenaSmartCommandName commandName, Object value)
            throws GardenaException {
        Ability ability = null;
        Command command = null;

        switch (commandName) {
        case PARK_UNTIL_NEXT_TIMER:
            ability = device.getAbility(ABILITY_MOWER);
            command = new MowerParkUntilNextTimerCommand();
            break;
        case PARK_UNTIL_FURTHER_NOTICE:
            ability = device.getAbility(ABILITY_MOWER);
            command = new MowerParkUntilFurtherNoticeCommand();
            break;
        case START_RESUME_SCHEDULE:
            ability = device.getAbility(ABILITY_MOWER);
            command = new MowerStartResumeScheduleCommand();
            break;
        case START_OVERRIDE_TIMER:
            ability = device.getAbility(ABILITY_MOWER);
            command = new MowerStartOverrideTimerCommand(mowerDuration);
            break;
        case DURATION_PROPERTY:
            if (value == null) {
                throw new GardenaException("Command '" + commandName + "' requires a value");
            }
            mowerDuration = ObjectUtils.toString(value);
            return;
        case MEASURE_AMBIENT_TEMPERATURE:
            ability = device.getAbility(ABILITY_AMBIENT_TEMPERATURE);
            command = new SensorMeasureAmbientTemperatureCommand();
            break;
        case MEASURE_LIGHT:
            ability = device.getAbility(ABILITY_LIGHT);
            command = new SensorMeasureLightCommand();
            break;
        case MEASURE_SOIL_HUMIDITY:
            ability = device.getAbility(ABILITY_HUMIDITY);
            command = new SensorMeasureSoilHumidityCommand();
            break;
        case MEASURE_SOIL_TEMPERATURE:
            ability = device.getAbility(ABILITY_SOIL_TEMPERATURE);
            command = new SensorMeasureSoilTemperatureCommand();
            break;
        case OUTLET_MANUAL_OVERRIDE_TIME:
            if (value == null) {
                throw new GardenaException("Command '" + commandName + "' requires a value");
            }
            SimpleProperties prop = new SimpleProperties(PROPERTY_BUTTON_MANUAL_OVERRIDE_TIME,
                    ObjectUtils.toString(value));
            String propertyUrl = String.format(URL_PROPERTY, device.getId(), ABILITY_OUTLET,
                    PROPERTY_BUTTON_MANUAL_OVERRIDE_TIME, device.getLocation().getId());
            executeRequest(HttpMethod.PUT, propertyUrl, new SimplePropertiesWrapper(prop), NoResult.class);
            break;
        case OUTLET_VALVE:
            ability = device.getAbility(ABILITY_OUTLET);
            if (value != null && value == Boolean.TRUE) {
                String wateringDuration = device.getAbility(ABILITY_OUTLET)
                        .getProperty(PROPERTY_BUTTON_MANUAL_OVERRIDE_TIME).getValue();
                command = new WateringManualOverrideCommand(wateringDuration);
            } else {
                command = new WateringCancelOverrideCommand();
            }
            break;

        default:
            throw new GardenaException("Unknown command " + commandName);
        }

        if (command != null) {
            executeRequest(HttpMethod.POST, getCommandUrl(device, ability), command, NoResult.class);
        }
        scheduleIntermediateRefresh();
    }

    /**
     * Returns the command url.
     */
    private String getCommandUrl(Device device, Ability ability) throws GardenaException {
        return String.format(URL_COMMAND, device.getId(), ability.getName(), device.getLocation().getId());
    }

    /**
     * Communicates with Gardena Smart Home and parses the result.
     */
    private synchronized <T> T executeRequest(HttpMethod method, String url, Object contentObject, Class<T> result)
            throws GardenaException {
        try {
            if (logger.isTraceEnabled()) {
                logger.trace("{} request:  {}", method, url);
                if (contentObject != null) {
                    logger.trace("{} data   :  {}", method, gson.toJson(contentObject));
                }
            }

            Request request = httpClient.newRequest(url).method(method)
                    .timeout(config.getConnectionTimeout(), TimeUnit.SECONDS)
                    .header(HttpHeader.CONTENT_TYPE, "application/json")
                    .header(HttpHeader.ACCEPT, "application/json").header(HttpHeader.ACCEPT_ENCODING, "gzip");

            if (contentObject != null) {
                StringContentProvider content = new StringContentProvider(gson.toJson(contentObject));
                request.content(content);
            }

            if (!result.equals(SessionWrapper.class)) {
                verifySession();
                request.header("X-Session", session.getToken());
            }

            ContentResponse contentResponse = request.send();
            int status = contentResponse.getStatus();
            if (logger.isTraceEnabled()) {
                logger.trace("Status  : {}", status);
                logger.trace("Response: {}", contentResponse.getContentAsString());
            }

            if (status == 500) {
                throw new GardenaException(
                        gson.fromJson(contentResponse.getContentAsString(), Errors.class).toString());
            } else if (status != 200 && status != 204) {
                throw new GardenaException(String.format("Error %s %s", status, contentResponse.getReason()));
            }

            if (result == NoResult.class) {
                return null;
            }

            return gson.fromJson(contentResponse.getContentAsString(), result);
        } catch (ExecutionException ex) {
            Throwable cause = ex.getCause();
            if (cause instanceof HttpResponseException) {
                HttpResponseException responseException = (HttpResponseException) ex.getCause();
                int status = responseException.getResponse().getStatus();
                if (status == 401) {
                    throw new GardenaUnauthorizedException(ex.getCause());
                }
            }
            throw new GardenaException(ex.getMessage(), ex);
        } catch (Exception ex) {
            throw new GardenaException(ex.getMessage(), ex);
        }
    }

    /**
     * Verifies the Gardena Smart Home session and reconnects if necessary.
     */
    private void verifySession() throws GardenaException {
        if (session == null
                || session.getCreated() + (config.getSessionTimeout() * 60000) <= System.currentTimeMillis()) {
            logger.trace("(Re)logging in to Gardena Smart Home");
            session = executeRequest(HttpMethod.POST, URL_LOGIN, new GardenaConfigWrapper(config),
                    SessionWrapper.class).getSession();
        }
    }

    /**
     * Thread which refreshes the data from Gardena Smart Home.
     */
    private class RefreshDevicesThread implements Runnable {
        private boolean connectionLost = false;

        /**
         * {@inheritDoc}
         */
        @Override
        public void run() {
            try {
                logger.debug("Refreshing gardena device data");
                Map<String, Device> newDevicesById = new HashMap<String, Device>();

                for (Location location : allLocations) {
                    Devices devices = loadDevices(location);
                    for (Device device : devices.getDevices()) {
                        if (DEVICE_CATEGORY_GATEWAY.equals(device.getCategory())) {
                            location.getDeviceIds().remove(device.getId());
                        } else {
                            newDevicesById.put(device.getId(), device);
                        }
                    }
                }

                if (connectionLost) {
                    connectionLost = false;
                    logger.info("Connection resumed to Gardena Smart Home with id '{}'", id);
                    eventListener.onConnectionResumed();
                }

                // determine deleted devices
                @SuppressWarnings("unchecked")
                Collection<Device> deletedDevices = CollectionUtils.subtract(allDevicesById.values(),
                        newDevicesById.values());

                // determine new devices
                @SuppressWarnings("unchecked")
                Collection<Device> newDevices = CollectionUtils.subtract(newDevicesById.values(),
                        allDevicesById.values());

                // determine updated devices
                @SuppressWarnings("unchecked")
                Collection<Device> updatedDevices = CollectionUtils.intersection(allDevicesById.values(),
                        newDevicesById.values());

                allDevicesById = newDevicesById;
                newDevicesById = null;

                for (Device deletedDevice : deletedDevices) {
                    eventListener.onDeviceDeleted(deletedDevice);
                }

                for (Device newDevice : newDevices) {
                    eventListener.onNewDevice(newDevice);
                }

                for (Device updatedDevice : updatedDevices) {
                    eventListener.onDeviceUpdated(updatedDevice);
                }

            } catch (GardenaException ex) {
                if (!connectionLost) {
                    connectionLost = true;
                    logger.warn("Connection lost to Gardena Smart Home with id '{}'", id);
                    logger.trace("{}", ex.getMessage(), ex);
                    eventListener.onConnectionLost();
                }
            }
        }
    }
}