added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.bticinosmarther-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-bticinosmarther" description="BTicino Smarther Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bticinosmarther/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@code SmartherBindingConstants} class defines the common constants used across the whole binding.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmartherBindingConstants {
|
||||
|
||||
private static final String BINDING_ID = "bticinosmarther";
|
||||
|
||||
// Date and time formats used by the binding
|
||||
public static final String DTF_DATE = "dd/MM/yyyy";
|
||||
public static final String DTF_DATETIME = "yyyy-MM-dd'T'HH:mm:ss";
|
||||
public static final String DTF_DATETIME_EXT = "yyyy-MM-dd'T'HH:mm:ssXXX";
|
||||
public static final String DTF_TODAY = "'Today at' HH:mm";
|
||||
public static final String DTF_TOMORROW = "'Tomorrow at' HH:mm";
|
||||
public static final String DTF_DAY_HHMM = "dd/MM/yyyy 'at' HH:mm";
|
||||
|
||||
// Generic constants
|
||||
public static final String HTTPS_SCHEMA = "https";
|
||||
public static final String NAME_SEPARATOR = ", ";
|
||||
public static final String UNAVAILABLE = "N/A";
|
||||
public static final String DEFAULT_PROGRAM = "Default";
|
||||
|
||||
// List of BTicino/Legrand API gateway related urls, information
|
||||
public static final String SMARTHER_ACCOUNT_URL = "https://partners-login.eliotbylegrand.com";
|
||||
public static final String SMARTHER_AUTHORIZE_URL = SMARTHER_ACCOUNT_URL + "/authorize";
|
||||
public static final String SMARTHER_API_TOKEN_URL = SMARTHER_ACCOUNT_URL + "/token";
|
||||
public static final String SMARTHER_API_SCOPES = Stream.of("comfort.read", "comfort.write")
|
||||
.collect(Collectors.joining(" "));
|
||||
public static final String SMARTHER_API_URL = "https://api.developer.legrand.com/smarther/v2.0";
|
||||
|
||||
// Servlets and resources aliases
|
||||
public static final String AUTH_SERVLET_ALIAS = "/" + BINDING_ID + "/connectsmarther";
|
||||
public static final String NOTIFY_SERVLET_ALIAS = "/" + BINDING_ID + "/notifysmarther";
|
||||
public static final String IMG_SERVLET_ALIAS = "/img";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
|
||||
public static final ThingTypeUID THING_TYPE_MODULE = new ThingTypeUID(BINDING_ID, "module");
|
||||
|
||||
// List of all common properties
|
||||
public static final String PROPERTY_STATUS_REFRESH_PERIOD = "statusRefreshPeriod";
|
||||
|
||||
// List of all bridge properties
|
||||
public static final String PROPERTY_SUBSCRIPTION_KEY = "subscriptionKey";
|
||||
public static final String PROPERTY_CLIENT_ID = "clientId";
|
||||
public static final String PROPERTY_CLIENT_SECRET = "clientSecret";
|
||||
public static final String PROPERTY_NOTIFICATION_URL = "notificationUrl";
|
||||
public static final String PROPERTY_NOTIFICATIONS = "notifications";
|
||||
|
||||
// List of all module properties
|
||||
public static final String PROPERTY_PLANT_ID = "plantId";
|
||||
public static final String PROPERTY_MODULE_ID = "moduleId";
|
||||
public static final String PROPERTY_MODULE_NAME = "moduleName";
|
||||
public static final String PROPERTY_DEVICE_TYPE = "deviceType";
|
||||
|
||||
// List of all bridge Status Channel ids
|
||||
public static final String CHANNEL_STATUS_API_CALLS_HANDLED = "status#apiCallsHandled";
|
||||
public static final String CHANNEL_STATUS_NOTIFS_RECEIVED = "status#notifsReceived";
|
||||
public static final String CHANNEL_STATUS_NOTIFS_REJECTED = "status#notifsRejected";
|
||||
// List of all bridge Config Channel ids
|
||||
public static final String CHANNEL_CONFIG_FETCH_LOCATIONS = "config#fetchLocations";
|
||||
|
||||
// List of all module Measures Channel ids
|
||||
public static final String CHANNEL_MEASURES_TEMPERATURE = "measures#temperature";
|
||||
public static final String CHANNEL_MEASURES_HUMIDITY = "measures#humidity";
|
||||
// List of all module Status Channel ids
|
||||
public static final String CHANNEL_STATUS_STATE = "status#state";
|
||||
public static final String CHANNEL_STATUS_FUNCTION = "status#function";
|
||||
public static final String CHANNEL_STATUS_MODE = "status#mode";
|
||||
public static final String CHANNEL_STATUS_TEMPERATURE = "status#temperature";
|
||||
public static final String CHANNEL_STATUS_PROGRAM = "status#program";
|
||||
public static final String CHANNEL_STATUS_ENDTIME = "status#endTime";
|
||||
public static final String CHANNEL_STATUS_TEMP_FORMAT = "status#temperatureFormat";
|
||||
// List of all module Settings Channel ids
|
||||
public static final String CHANNEL_SETTINGS_MODE = "settings#mode";
|
||||
public static final String CHANNEL_SETTINGS_TEMPERATURE = "settings#temperature";
|
||||
public static final String CHANNEL_SETTINGS_PROGRAM = "settings#program";
|
||||
public static final String CHANNEL_SETTINGS_BOOSTTIME = "settings#boostTime";
|
||||
public static final String CHANNEL_SETTINGS_ENDDATE = "settings#endDate";
|
||||
public static final String CHANNEL_SETTINGS_ENDHOUR = "settings#endHour";
|
||||
public static final String CHANNEL_SETTINGS_ENDMINUTE = "settings#endMinute";
|
||||
public static final String CHANNEL_SETTINGS_POWER = "settings#power";
|
||||
// List of all module Config Channel ids
|
||||
public static final String CHANNEL_CONFIG_FETCH_PROGRAMS = "config#fetchPrograms";
|
||||
|
||||
// List of all adressable things
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
|
||||
.unmodifiableSet(Stream.of(THING_TYPE_BRIDGE, THING_TYPE_MODULE).collect(Collectors.toSet()));
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.account;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Location;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Module;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.ModuleStatus;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Plant;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Program;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Subscription;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException;
|
||||
import org.openhab.binding.bticinosmarther.internal.model.ModuleSettings;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
|
||||
/**
|
||||
* The {@code SmartherAccountHandler} interface is used to decouple the Smarther account handler implementation from
|
||||
* other Bridge code.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface SmartherAccountHandler extends ThingHandler {
|
||||
|
||||
/**
|
||||
* Returns the {@link ThingUID} associated with this Smarther account handler.
|
||||
*
|
||||
* @return the thing UID associated with this Smarther account handler
|
||||
*/
|
||||
ThingUID getUID();
|
||||
|
||||
/**
|
||||
* Returns the label of the Smarther Bridge associated with this Smarther account handler.
|
||||
*
|
||||
* @return a string containing the bridge label associated with the account handler
|
||||
*/
|
||||
String getLabel();
|
||||
|
||||
/**
|
||||
* Returns the available locations associated with this Smarther account handler.
|
||||
*
|
||||
* @return the list of available locations, or an empty {@link List} in case of no locations found
|
||||
*/
|
||||
List<Location> getLocations();
|
||||
|
||||
/**
|
||||
* Checks whether the given location is managed by this Smarther account handler
|
||||
*
|
||||
* @param plantId
|
||||
* the identifier of the location to search for
|
||||
*
|
||||
* @return {@code true} if the given location is found, {@code false} otherwise
|
||||
*/
|
||||
boolean hasLocation(String plantId);
|
||||
|
||||
/**
|
||||
* Returns the plants registered under the Smarther account the bridge has been configured with.
|
||||
*
|
||||
* @return the list of registered plants, or an empty {@link List} in case of no plants found
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the Smarther API
|
||||
*/
|
||||
List<Plant> getPlants() throws SmartherGatewayException;
|
||||
|
||||
/**
|
||||
* Returns the subscriptions registered to the C2C Webhook, where modules status notifications are currently sent
|
||||
* for all the plants.
|
||||
*
|
||||
* @return the list of registered subscriptions, or an empty {@link List} in case of no subscriptions found
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the Smarther API
|
||||
*/
|
||||
List<Subscription> getSubscriptions() throws SmartherGatewayException;
|
||||
|
||||
/**
|
||||
* Subscribes a plant to the C2C Webhook to start receiving modules status notifications.
|
||||
*
|
||||
* @param plantId
|
||||
* the identifier of the plant to be subscribed
|
||||
* @param notificationUrl
|
||||
* the url notifications will have to be sent to for the given plant
|
||||
*
|
||||
* @return the identifier this subscription has been registered under
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the Smarther API
|
||||
*/
|
||||
String subscribePlant(String plantId, String notificationUrl) throws SmartherGatewayException;
|
||||
|
||||
/**
|
||||
* Unsubscribes a plant from the C2C Webhook to stop receiving modules status notifications.
|
||||
*
|
||||
* @param plantId
|
||||
* the identifier of the plant to be unsubscribed
|
||||
* @param subscriptionId
|
||||
* the identifier of the subscription to be removed for the given plant
|
||||
*
|
||||
* @return {@code true} if the plant is successfully unsubscribed, {@code false} otherwise
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the Smarther API
|
||||
*/
|
||||
void unsubscribePlant(String plantId, String subscriptionId) throws SmartherGatewayException;
|
||||
|
||||
/**
|
||||
* Returns the chronothermostat modules registered at the given location.
|
||||
*
|
||||
* @param location
|
||||
* the identifier of the location
|
||||
*
|
||||
* @return the list of registered modules, or an empty {@link List} if the location contains no module or in case of
|
||||
* communication issues with the Smarther API
|
||||
*/
|
||||
List<Module> getLocationModules(Location location);
|
||||
|
||||
/**
|
||||
* Returns the current status of a given chronothermostat module.
|
||||
*
|
||||
* @param plantId
|
||||
* the identifier of the plant
|
||||
* @param moduleId
|
||||
* the identifier of the chronothermostat module inside the plant
|
||||
*
|
||||
* @return the current status of the chronothermostat module
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the Smarther API
|
||||
*/
|
||||
ModuleStatus getModuleStatus(String plantId, String moduleId) throws SmartherGatewayException;
|
||||
|
||||
/**
|
||||
* Sends new settings to be applied to a given chronothermostat module.
|
||||
*
|
||||
* @param settings
|
||||
* the module settings to be applied
|
||||
*
|
||||
* @return {@code true} if the settings have been successfully applied, {@code false} otherwise
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the Smarther API
|
||||
*/
|
||||
boolean setModuleStatus(ModuleSettings moduleSettings) throws SmartherGatewayException;
|
||||
|
||||
/**
|
||||
* Returns the automatic mode programs registered for the given chronothermostat module.
|
||||
*
|
||||
* @param plantId
|
||||
* the identifier of the plant
|
||||
* @param moduleId
|
||||
* the identifier of the chronothermostat module inside the plant
|
||||
*
|
||||
* @return the list of registered programs, or an empty {@link List} in case of no programs found
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the Smarther API
|
||||
*/
|
||||
List<Program> getModulePrograms(String plantId, String moduleId) throws SmartherGatewayException;
|
||||
|
||||
/**
|
||||
* Checks whether the Smarther Bridge associated with this Smarther account handler is authorized by Smarther API.
|
||||
*
|
||||
* @return {@code true} if the Bridge is authorized, {@code false} otherwise
|
||||
*/
|
||||
boolean isAuthorized();
|
||||
|
||||
/**
|
||||
* Checks whether the Smarther Bridge thing is online.
|
||||
*
|
||||
* @return {@code true} if the Bridge is online, {@code false} otherwise
|
||||
*/
|
||||
boolean isOnline();
|
||||
|
||||
/**
|
||||
* Performs the authorization procedure with Legrand/Bticino portal.
|
||||
* In case of success, the returned refresh/access tokens and the notification url are stored in the Bridge.
|
||||
*
|
||||
* @param redirectUrl
|
||||
* the redirect url BTicino/Legrand portal calls back to
|
||||
* @param reqCode
|
||||
* the unique code passed by BTicino/Legrand portal to obtain the refresh and access tokens
|
||||
* @param notificationUrl
|
||||
* the endpoint C2C Webhook service will send module status notifications to, once authorized
|
||||
*
|
||||
* @return a string containing the name of the BTicino/Legrand portal user that is authorized
|
||||
*/
|
||||
String authorize(String redirectUrl, String reqCode, String notificationUrl) throws SmartherGatewayException;
|
||||
|
||||
/**
|
||||
* Compares this Smarther account handler instance to a given Thing UID.
|
||||
*
|
||||
* @param thingUID
|
||||
* the Thing UID the account handler is compared to
|
||||
*
|
||||
* @return {@code true} if the two instances match, {@code false} otherwise
|
||||
*/
|
||||
boolean equalsThingUID(String thingUID);
|
||||
|
||||
/**
|
||||
* Formats the url used to call the Smarther API in order to authorize the Smarther Bridge associated with this
|
||||
* Smarther account handler.
|
||||
*
|
||||
* @param redirectUri
|
||||
* the uri BTicino/Legrand portal redirects back to
|
||||
*
|
||||
* @return a string containing the formatted url, or the empty string ("") in case of issue
|
||||
*/
|
||||
String formatAuthorizationUrl(String redirectUri);
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.account;
|
||||
|
||||
import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.util.ConcurrentHashSet;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Notification;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Sender;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException;
|
||||
import org.openhab.binding.bticinosmarther.internal.util.StringUtil;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.osgi.service.component.ComponentContext;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.http.HttpService;
|
||||
import org.osgi.service.http.NamespaceException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@code SmartherAccountService} class manages the servlets and bind authorization servlet to Bridges.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@Component(service = SmartherAccountService.class, immediate = true, configurationPid = "binding.bticinosmarther.accountService")
|
||||
@NonNullByDefault
|
||||
public class SmartherAccountService {
|
||||
|
||||
private static final String TEMPLATE_PATH = "templates/";
|
||||
private static final String IMAGE_PATH = "web";
|
||||
private static final String TEMPLATE_APPLICATION = TEMPLATE_PATH + "application.html";
|
||||
private static final String TEMPLATE_INDEX = TEMPLATE_PATH + "index.html";
|
||||
private static final String ERROR_UKNOWN_BRIDGE = "Returned 'state' doesn't match any Bridges. Has the bridge been removed?";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SmartherAccountService.class);
|
||||
|
||||
private final Set<SmartherAccountHandler> handlers = new ConcurrentHashSet<>();
|
||||
|
||||
private @Nullable HttpService httpService;
|
||||
private @Nullable BundleContext bundleContext;
|
||||
|
||||
@Activate
|
||||
protected void activate(ComponentContext componentContext, Map<String, Object> properties) {
|
||||
try {
|
||||
this.bundleContext = componentContext.getBundleContext();
|
||||
|
||||
final HttpService localHttpService = this.httpService;
|
||||
if (localHttpService != null) {
|
||||
// Register the authorization servlet
|
||||
localHttpService.registerServlet(AUTH_SERVLET_ALIAS, createAuthorizationServlet(), new Hashtable<>(),
|
||||
localHttpService.createDefaultHttpContext());
|
||||
localHttpService.registerResources(AUTH_SERVLET_ALIAS + IMG_SERVLET_ALIAS, IMAGE_PATH, null);
|
||||
|
||||
// Register the notification servlet
|
||||
localHttpService.registerServlet(NOTIFY_SERVLET_ALIAS, createNotificationServlet(), new Hashtable<>(),
|
||||
localHttpService.createDefaultHttpContext());
|
||||
}
|
||||
} catch (NamespaceException | ServletException | IOException e) {
|
||||
logger.warn("Error during Smarther servlet startup", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
protected void deactivate(ComponentContext componentContext) {
|
||||
final HttpService localHttpService = this.httpService;
|
||||
if (localHttpService != null) {
|
||||
// Unregister the authorization servlet
|
||||
localHttpService.unregister(AUTH_SERVLET_ALIAS);
|
||||
localHttpService.unregister(AUTH_SERVLET_ALIAS + IMG_SERVLET_ALIAS);
|
||||
|
||||
// Unregister the notification servlet
|
||||
localHttpService.unregister(NOTIFY_SERVLET_ALIAS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherAuthorizationServlet}.
|
||||
*
|
||||
* @return the newly created servlet
|
||||
*
|
||||
* @throws {@link IOException}
|
||||
* in case of issues reading one of the internal html templates
|
||||
*/
|
||||
private HttpServlet createAuthorizationServlet() throws IOException {
|
||||
return new SmartherAuthorizationServlet(this, readTemplate(TEMPLATE_INDEX), readTemplate(TEMPLATE_APPLICATION));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherNotificationServlet}.
|
||||
*
|
||||
* @return the newly created servlet
|
||||
*/
|
||||
private HttpServlet createNotificationServlet() {
|
||||
return new SmartherNotificationServlet(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a template from file and returns its content as string.
|
||||
*
|
||||
* @param templateName
|
||||
* the name of the template file to read
|
||||
*
|
||||
* @return a string representing the content of the template file
|
||||
*
|
||||
* @throws {@link IOException}
|
||||
* in case of issues reading the template from file
|
||||
*/
|
||||
private String readTemplate(String templateName) throws IOException {
|
||||
final BundleContext localBundleContext = this.bundleContext;
|
||||
if (localBundleContext != null) {
|
||||
final URL index = localBundleContext.getBundle().getEntry(templateName);
|
||||
|
||||
if (index == null) {
|
||||
throw new FileNotFoundException(String
|
||||
.format("Cannot find template '%s' - failed to initialize Smarther servlet", templateName));
|
||||
} else {
|
||||
try (InputStream input = index.openStream()) {
|
||||
return StringUtil.streamToString(input);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new IOException("Cannot get template, bundle context is null");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the received Smarther API authorization response to the proper Smarther account handler.
|
||||
* Part of the Legrand/Bticino OAuth2 authorization process.
|
||||
*
|
||||
* @param servletBaseURL
|
||||
* the authorization servlet url needed to derive the notification endpoint url
|
||||
* @param state
|
||||
* the authorization state needed to match the correct Smarther account handler to authorize
|
||||
* @param code The BTicino/Legrand API returned code value
|
||||
* the authorization code to authorize with the account handler
|
||||
*
|
||||
* @return a string containing the name of the authorized BTicino/Legrand portal user
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the Smarther API or no account handler found
|
||||
*/
|
||||
public String dispatchAuthorization(String servletBaseURL, String state, String code)
|
||||
throws SmartherGatewayException {
|
||||
// Searches the SmartherAccountHandler instance that matches the given state
|
||||
final SmartherAccountHandler accountHandler = getAccountHandlerByUID(state);
|
||||
if (accountHandler != null) {
|
||||
// Generates the notification URL from servletBaseURL
|
||||
final String notificationUrl = servletBaseURL.replace(AUTH_SERVLET_ALIAS, NOTIFY_SERVLET_ALIAS);
|
||||
|
||||
logger.debug("API authorization: calling authorize on {}", accountHandler.getUID());
|
||||
|
||||
// Passes the authorization to the handler
|
||||
return accountHandler.authorize(servletBaseURL, code, notificationUrl);
|
||||
} else {
|
||||
logger.trace("API authorization: request redirected with state '{}'", state);
|
||||
logger.warn("API authorization: no matching bridge was found. Possible bridge has been removed.");
|
||||
throw new SmartherGatewayException(ERROR_UKNOWN_BRIDGE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the received C2C Webhook notification to the proper Smarther notification handler.
|
||||
*
|
||||
* @param notification
|
||||
* the received notification to handle
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the Smarther API or no notification handler found
|
||||
*/
|
||||
public void dispatchNotification(Notification notification) throws SmartherGatewayException {
|
||||
final Sender sender = notification.getSender();
|
||||
if (sender != null) {
|
||||
// Searches the SmartherAccountHandler instance that matches the given location
|
||||
final SmartherAccountHandler accountHandler = getAccountHandlerByLocation(sender.getPlant().getId());
|
||||
if (accountHandler == null) {
|
||||
logger.warn("C2C notification [{}]: no matching bridge was found. Possible bridge has been removed.",
|
||||
notification.getId());
|
||||
throw new SmartherGatewayException(ERROR_UKNOWN_BRIDGE);
|
||||
} else if (accountHandler.isOnline()) {
|
||||
final SmartherNotificationHandler notificationHandler = (SmartherNotificationHandler) accountHandler;
|
||||
|
||||
if (notificationHandler.useNotifications()) {
|
||||
// Passes the notification to the handler
|
||||
notificationHandler.handleNotification(notification);
|
||||
} else {
|
||||
logger.debug(
|
||||
"C2C notification [{}]: notification discarded as bridge does not handle notifications.",
|
||||
notification.getId());
|
||||
}
|
||||
} else {
|
||||
logger.debug("C2C notification [{}]: notification discarded as bridge is offline.",
|
||||
notification.getId());
|
||||
}
|
||||
} else {
|
||||
logger.debug("C2C notification [{}]: notification discarded as payload is invalid.", notification.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link SmartherAccountHandler} handler to the set of account service handlers.
|
||||
*
|
||||
* @param handler
|
||||
* the handler to add to the handlers set
|
||||
*/
|
||||
public void addSmartherAccountHandler(SmartherAccountHandler handler) {
|
||||
handlers.add(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a {@link SmartherAccountHandler} handler from the set of account service handlers.
|
||||
*
|
||||
* @param handler
|
||||
* the handler to remove from the handlers set
|
||||
*/
|
||||
public void removeSmartherAccountHandler(SmartherAccountHandler handler) {
|
||||
handlers.remove(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the {@link SmartherAccountHandler} account service handlers.
|
||||
*
|
||||
* @return a set containing all the account service handlers
|
||||
*/
|
||||
public Set<SmartherAccountHandler> getSmartherAccountHandlers() {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches the {@link SmartherAccountHandler} handler that matches the given Thing UID.
|
||||
*
|
||||
* @param thingUID
|
||||
* the UID of the Thing to match the handler with
|
||||
*
|
||||
* @return the handler matching the given Thing UID, or {@code null} if none matches
|
||||
*/
|
||||
private @Nullable SmartherAccountHandler getAccountHandlerByUID(String thingUID) {
|
||||
final Optional<SmartherAccountHandler> maybeHandler = handlers.stream().filter(l -> l.equalsThingUID(thingUID))
|
||||
.findFirst();
|
||||
return (maybeHandler.isPresent()) ? maybeHandler.get() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches the {@link SmartherAccountHandler} handler that matches the given location plant.
|
||||
*
|
||||
* @param plantId
|
||||
* the identifier of the plant to match the handler with
|
||||
*
|
||||
* @return the handler matching the given location plant, or {@code null} if none matches
|
||||
*/
|
||||
private @Nullable SmartherAccountHandler getAccountHandlerByLocation(String plantId) {
|
||||
final Optional<SmartherAccountHandler> maybeHandler = handlers.stream().filter(l -> l.hasLocation(plantId))
|
||||
.findFirst();
|
||||
return (maybeHandler.isPresent()) ? maybeHandler.get() : null;
|
||||
}
|
||||
|
||||
@Reference
|
||||
protected void setHttpService(HttpService httpService) {
|
||||
this.httpService = httpService;
|
||||
}
|
||||
|
||||
protected void unsetHttpService(HttpService httpService) {
|
||||
this.httpService = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.account;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.util.MultiMap;
|
||||
import org.eclipse.jetty.util.UrlEncoded;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Location;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException;
|
||||
import org.openhab.binding.bticinosmarther.internal.util.StringUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@code SmartherAuthorizationServlet} class acts as the registered endpoint for the user to automatically manage
|
||||
* the BTicino/Legrand API authorization process.
|
||||
* The servlet follows the OAuth2 Authorization Code flow, saving the resulting refreshToken within the Smarther Bridge.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmartherAuthorizationServlet extends HttpServlet {
|
||||
|
||||
private static final long serialVersionUID = 5199173744807168342L;
|
||||
|
||||
private static final String CONTENT_TYPE = "text/html;charset=UTF-8";
|
||||
private static final String X_FORWARDED_PROTO = "X-Forwarded-Proto";
|
||||
|
||||
// Http request parameters
|
||||
private static final String PARAM_CODE = "code";
|
||||
private static final String PARAM_STATE = "state";
|
||||
private static final String PARAM_ERROR = "error";
|
||||
|
||||
// Simple HTML templates for inserting messages.
|
||||
private static final String HTML_EMPTY_APPLICATIONS = "<p class='block'>Manually add a Smarther Bridge to authorize it here<p>";
|
||||
private static final String HTML_BRIDGE_AUTHORIZED = "<p class='block authorized'>Bridge authorized for Client Id %s</p>";
|
||||
private static final String HTML_ERROR = "<p class='block error'>Call to Smarther API gateway failed with error: %s</p>";
|
||||
|
||||
private static final Pattern MESSAGE_KEY_PATTERN = Pattern.compile("\\$\\{([^\\}]+)\\}");
|
||||
|
||||
// Keys present in the index.html
|
||||
private static final String KEY_PAGE_REFRESH = "pageRefresh";
|
||||
private static final String HTML_META_REFRESH_CONTENT = "<meta http-equiv='refresh' content='10; url=%s'>";
|
||||
private static final String KEY_AUTHORIZED_BRIDGE = "authorizedBridge";
|
||||
private static final String KEY_ERROR = "error";
|
||||
private static final String KEY_APPLICATIONS = "applications";
|
||||
private static final String KEY_REDIRECT_URI = "redirectUri";
|
||||
// Keys present in the application.html
|
||||
private static final String APPLICATION_ID = "application.id";
|
||||
private static final String APPLICATION_NAME = "application.name";
|
||||
private static final String APPLICATION_LOCATIONS = "application.locations";
|
||||
private static final String APPLICATION_AUTHORIZED_CLASS = "application.authorized";
|
||||
private static final String APPLICATION_AUTHORIZE = "application.authorize";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SmartherAuthorizationServlet.class);
|
||||
|
||||
private final SmartherAccountService accountService;
|
||||
private final String indexTemplate;
|
||||
private final String applicationTemplate;
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherAuthorizationServlet} associated to the given {@link SmartherAccountService} service
|
||||
* and with the given html index/application templates.
|
||||
*
|
||||
* @param accountService
|
||||
* the account service to associate to the servlet
|
||||
* @param indexTemplate
|
||||
* the html template to use as index page for the user
|
||||
* @param applicationTemplate
|
||||
* the html template to use as application page for the user
|
||||
*/
|
||||
public SmartherAuthorizationServlet(SmartherAccountService accountService, String indexTemplate,
|
||||
String applicationTemplate) {
|
||||
this.accountService = accountService;
|
||||
this.indexTemplate = indexTemplate;
|
||||
this.applicationTemplate = applicationTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response)
|
||||
throws ServletException, IOException {
|
||||
if (request != null && response != null) {
|
||||
final String servletBaseURL = extractServletBaseURL(request);
|
||||
logger.debug("Authorization callback servlet received GET request {}", servletBaseURL);
|
||||
|
||||
// Handle the received data
|
||||
final Map<String, String> replaceMap = new HashMap<>();
|
||||
handleSmartherRedirect(replaceMap, servletBaseURL, request.getQueryString());
|
||||
|
||||
// Build a http 200 (Success) response for the caller
|
||||
response.setContentType(CONTENT_TYPE);
|
||||
response.setStatus(HttpStatus.OK_200);
|
||||
replaceMap.put(KEY_REDIRECT_URI, servletBaseURL);
|
||||
replaceMap.put(KEY_APPLICATIONS, formatApplications(applicationTemplate, servletBaseURL));
|
||||
response.getWriter().append(replaceKeysFromMap(indexTemplate, replaceMap));
|
||||
response.getWriter().close();
|
||||
} else if (response != null) {
|
||||
// Build a http 400 (Bad Request) error response for the caller
|
||||
response.setContentType(CONTENT_TYPE);
|
||||
response.setStatus(HttpStatus.BAD_REQUEST_400);
|
||||
response.getWriter().close();
|
||||
} else {
|
||||
throw new ServletException("Authorization callback with null request/response");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the servlet base url from the received http request, handling eventual reverse proxy.
|
||||
*
|
||||
* @param request
|
||||
* the received http request
|
||||
*
|
||||
* @return a string containing the servlet base url
|
||||
*/
|
||||
private String extractServletBaseURL(HttpServletRequest request) {
|
||||
final StringBuffer requestURL = request.getRequestURL();
|
||||
|
||||
// Try to infer the real protocol from request headers
|
||||
final String realProtocol = StringUtil.defaultIfBlank(request.getHeader(X_FORWARDED_PROTO),
|
||||
request.getScheme());
|
||||
|
||||
return requestURL.replace(0, requestURL.indexOf(":"), realProtocol).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a call from BTicino/Legrand API gateway to the redirect_uri, dispatching the authorization flow to the
|
||||
* proper authorization handler.
|
||||
* If the user was authorized, this is passed on to the handler; in case of an error, this is shown to the user.
|
||||
* Based on all these different outcomes the html response is generated to inform the user.
|
||||
*
|
||||
* @param replaceMap
|
||||
* a map with key string values to use in the html templates
|
||||
* @param servletBaseURL
|
||||
* the servlet base url to compose the correct API gateway redirect_uri
|
||||
* @param queryString
|
||||
* the querystring part of the received request, may be {@code null}
|
||||
*/
|
||||
private void handleSmartherRedirect(Map<String, String> replaceMap, String servletBaseURL,
|
||||
@Nullable String queryString) {
|
||||
replaceMap.put(KEY_AUTHORIZED_BRIDGE, "");
|
||||
replaceMap.put(KEY_ERROR, "");
|
||||
replaceMap.put(KEY_PAGE_REFRESH, "");
|
||||
|
||||
if (queryString != null) {
|
||||
final MultiMap<String> params = new MultiMap<>();
|
||||
UrlEncoded.decodeTo(queryString, params, StandardCharsets.UTF_8.name());
|
||||
final String reqCode = params.getString(PARAM_CODE);
|
||||
final String reqState = params.getString(PARAM_STATE);
|
||||
final String reqError = params.getString(PARAM_ERROR);
|
||||
|
||||
replaceMap.put(KEY_PAGE_REFRESH,
|
||||
params.isEmpty() ? "" : String.format(HTML_META_REFRESH_CONTENT, servletBaseURL));
|
||||
if (!StringUtil.isBlank(reqError)) {
|
||||
logger.debug("Authorization redirected with an error: {}", reqError);
|
||||
replaceMap.put(KEY_ERROR, String.format(HTML_ERROR, reqError));
|
||||
} else if (!StringUtil.isBlank(reqState)) {
|
||||
try {
|
||||
logger.trace("Received from authorization - state:[{}] code:[{}]", reqState, reqCode);
|
||||
replaceMap.put(KEY_AUTHORIZED_BRIDGE, String.format(HTML_BRIDGE_AUTHORIZED,
|
||||
accountService.dispatchAuthorization(servletBaseURL, reqState, reqCode)));
|
||||
} catch (SmartherGatewayException e) {
|
||||
logger.debug("Exception during authorizaton: ", e);
|
||||
replaceMap.put(KEY_ERROR, String.format(HTML_ERROR, e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an html formatted text representing all the available Smarther Bridge applications.
|
||||
*
|
||||
* @param applicationTemplate
|
||||
* the html template to format the application with
|
||||
* @param servletBaseURL
|
||||
* the redirect_uri to link to the authorization button as authorization url
|
||||
*
|
||||
* @return a string containing the html formatted text
|
||||
*/
|
||||
private String formatApplications(String applicationTemplate, String servletBaseURL) {
|
||||
final Set<SmartherAccountHandler> applications = accountService.getSmartherAccountHandlers();
|
||||
|
||||
return applications.isEmpty() ? HTML_EMPTY_APPLICATIONS
|
||||
: applications.stream().map(p -> formatApplication(applicationTemplate, p, servletBaseURL))
|
||||
.collect(Collectors.joining());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an html formatted text representing a given Smarther Bridge application.
|
||||
*
|
||||
* @param applicationTemplate
|
||||
* the html template to format the application with
|
||||
* @param handler
|
||||
* the Smarther application handler to use
|
||||
* @param servletBaseURL
|
||||
* the redirect_uri to link to the authorization button as authorization url
|
||||
*
|
||||
* @return a string containing the html formatted text
|
||||
*/
|
||||
private String formatApplication(String applicationTemplate, SmartherAccountHandler handler,
|
||||
String servletBaseURL) {
|
||||
final Map<String, String> map = new HashMap<>();
|
||||
|
||||
map.put(APPLICATION_ID, handler.getUID().getAsString());
|
||||
map.put(APPLICATION_NAME, handler.getLabel());
|
||||
|
||||
if (handler.isAuthorized()) {
|
||||
final String availableLocations = Location.toNameString(handler.getLocations());
|
||||
map.put(APPLICATION_AUTHORIZED_CLASS, " authorized");
|
||||
map.put(APPLICATION_LOCATIONS, String.format(" (Available locations: %s)", availableLocations));
|
||||
} else {
|
||||
map.put(APPLICATION_AUTHORIZED_CLASS, "");
|
||||
map.put(APPLICATION_LOCATIONS, "");
|
||||
}
|
||||
|
||||
map.put(APPLICATION_AUTHORIZE, handler.formatAuthorizationUrl(servletBaseURL));
|
||||
return replaceKeysFromMap(applicationTemplate, map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all keys found in the template with the values matched from the map.
|
||||
* If a key is not found in the map, it is kept unchanged in the template.
|
||||
*
|
||||
* @param template
|
||||
* the template to replace keys on
|
||||
* @param map
|
||||
* the map containing the key/value pairs to replace in the template
|
||||
*
|
||||
* @return a string containing the resulting template after the replace process
|
||||
*/
|
||||
private String replaceKeysFromMap(String template, Map<String, String> map) {
|
||||
final Matcher m = MESSAGE_KEY_PATTERN.matcher(template);
|
||||
final StringBuffer sb = new StringBuffer();
|
||||
|
||||
while (m.find()) {
|
||||
try {
|
||||
final String key = m.group(1);
|
||||
m.appendReplacement(sb, Matcher.quoteReplacement(map.getOrDefault(key, "${" + key + '}')));
|
||||
} catch (RuntimeException e) {
|
||||
logger.warn("Error occurred during template filling, cause ", e);
|
||||
}
|
||||
}
|
||||
m.appendTail(sb);
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.account;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Notification;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
|
||||
/**
|
||||
* The {@code SmartherNotificationHandler} interface is used to decouple the Smarther notification handler
|
||||
* implementation from other Bridge code.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface SmartherNotificationHandler extends ThingHandler {
|
||||
|
||||
/**
|
||||
* Tells whether the Smarther Bridge associated with this handler supports notifications.
|
||||
*
|
||||
* @return {@code true} if the Bridge supports notifications, {@code false} otherwise
|
||||
*/
|
||||
boolean useNotifications();
|
||||
|
||||
/**
|
||||
* Calls the Smarther API to register a new notification endpoint to the C2C Webhook service.
|
||||
*
|
||||
* @param plantId
|
||||
* the identifier of the plant the notification endpoint belongs to
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the Smarther API
|
||||
*/
|
||||
void registerNotification(String plantId) throws SmartherGatewayException;
|
||||
|
||||
/**
|
||||
* Handles a new notifications received from the C2C Webhook notification service.
|
||||
*
|
||||
* @param notification
|
||||
* the received notification
|
||||
*/
|
||||
void handleNotification(Notification notification);
|
||||
|
||||
/**
|
||||
* Calls the Smarther API to unregister a notification endpoint already registered to the C2C Webhook service.
|
||||
*
|
||||
* @param plantId
|
||||
* the identifier of the plant the notification endpoint belongs to
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the Smarther API
|
||||
*/
|
||||
void unregisterNotification(String plantId) throws SmartherGatewayException;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.account;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Notification;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException;
|
||||
import org.openhab.binding.bticinosmarther.internal.util.ModelUtil;
|
||||
import org.openhab.binding.bticinosmarther.internal.util.StringUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
/**
|
||||
* The {@code SmartherNotificationServlet} class acts as the registered endpoint to receive module status notifications
|
||||
* from the Legrand/Bticino C2C Webhook notification service.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmartherNotificationServlet extends HttpServlet {
|
||||
|
||||
private static final long serialVersionUID = -2474355132186048438L;
|
||||
|
||||
private static final String CONTENT_TYPE = "application/json;charset=UTF-8";
|
||||
private static final String OK_RESULT_MSG = "{\"result\":0}";
|
||||
private static final String KO_RESULT_MSG = "{\"result\":1}";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SmartherNotificationServlet.class);
|
||||
|
||||
private final SmartherAccountService accountService;
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherNotificationServlet} associated to the given {@link SmartherAccountService} service.
|
||||
*
|
||||
* @param accountService
|
||||
* the account service to associate to the servlet
|
||||
*/
|
||||
public SmartherNotificationServlet(SmartherAccountService accountService) {
|
||||
this.accountService = accountService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response)
|
||||
throws ServletException, IOException {
|
||||
if (request != null && response != null) {
|
||||
logger.debug("Notification callback servlet received POST request {}", request.getRequestURI());
|
||||
|
||||
// Handle the received data
|
||||
final String requestBody = StringUtil.readerToString(request.getReader());
|
||||
final String responseBody = dispatchNotifications(requestBody);
|
||||
|
||||
// Build a http 200 (Success) response for the caller
|
||||
response.setContentType(CONTENT_TYPE);
|
||||
response.setStatus(HttpStatus.OK_200);
|
||||
response.getWriter().append(responseBody);
|
||||
response.getWriter().close();
|
||||
} else if (response != null) {
|
||||
// Build a http 400 (Bad Request) error response for the caller
|
||||
response.setContentType(CONTENT_TYPE);
|
||||
response.setStatus(HttpStatus.BAD_REQUEST_400);
|
||||
response.getWriter().close();
|
||||
} else {
|
||||
throw new ServletException("Notification callback with null request/response");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches all the notifications contained in the received payload to the proper notification handlers.
|
||||
* The response to the notification service is generated based on the different outcomes.
|
||||
*
|
||||
* @param payload
|
||||
* the received servlet payload to process, may be {@code null}
|
||||
*
|
||||
* @return a string containing the response to the notification service
|
||||
*/
|
||||
private String dispatchNotifications(@Nullable String payload) {
|
||||
try {
|
||||
logger.trace("C2C listener received payload: {}", payload);
|
||||
if (!StringUtil.isBlank(payload)) {
|
||||
List<Notification> notifications = ModelUtil.gsonInstance().fromJson(payload,
|
||||
new TypeToken<List<Notification>>() {
|
||||
}.getType());
|
||||
|
||||
if (notifications != null) {
|
||||
notifications.forEach(n -> handleSmartherNotification(n));
|
||||
}
|
||||
}
|
||||
return OK_RESULT_MSG;
|
||||
} catch (JsonSyntaxException e) {
|
||||
logger.warn("C2C payload parsing error: {} ", e.getMessage());
|
||||
return KO_RESULT_MSG;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a single notification contained in the received payload to the proper notification handler.
|
||||
*
|
||||
* @param notification
|
||||
* the notification to dispatch
|
||||
*/
|
||||
private void handleSmartherNotification(Notification notification) {
|
||||
try {
|
||||
this.accountService.dispatchNotification(notification);
|
||||
} catch (SmartherGatewayException e) {
|
||||
logger.warn("C2C notification {}: not applied: {}", notification.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api;
|
||||
|
||||
import static org.eclipse.jetty.http.HttpMethod.*;
|
||||
import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
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.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Chronothermostat;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.MeasureUnit;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Module;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.ModuleStatus;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Plant;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Plants;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Program;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Subscription;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Topology;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherAuthorizationException;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherTokenExpiredException;
|
||||
import org.openhab.binding.bticinosmarther.internal.model.ModuleSettings;
|
||||
import org.openhab.binding.bticinosmarther.internal.util.ModelUtil;
|
||||
import org.openhab.binding.bticinosmarther.internal.util.StringUtil;
|
||||
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
|
||||
import org.openhab.core.auth.client.oauth2.OAuthClientService;
|
||||
import org.openhab.core.auth.client.oauth2.OAuthException;
|
||||
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
/**
|
||||
* The {@code SmartherApi} class is used to communicate with the BTicino/Legrand API gateway.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmartherApi {
|
||||
|
||||
private static final String CONTENT_TYPE = "application/json";
|
||||
private static final String BEARER = "Bearer ";
|
||||
|
||||
// API gateway request headers
|
||||
private static final String HEADER_ACCEPT = "Accept";
|
||||
// API gateway request attributes
|
||||
private static final String ATTR_FUNCTION = "function";
|
||||
private static final String ATTR_MODE = "mode";
|
||||
private static final String ATTR_PROGRAMS = "programs";
|
||||
private static final String ATTR_NUMBER = "number";
|
||||
private static final String ATTR_SETPOINT = "setPoint";
|
||||
private static final String ATTR_VALUE = "value";
|
||||
private static final String ATTR_UNIT = "unit";
|
||||
private static final String ATTR_ACTIVATION_TIME = "activationTime";
|
||||
private static final String ATTR_ENDPOINT_URL = "EndPointUrl";
|
||||
// API gateway operation paths
|
||||
private static final String PATH_PLANTS = "/plants";
|
||||
private static final String PATH_TOPOLOGY = PATH_PLANTS + "/%s/topology";
|
||||
private static final String PATH_MODULE = "/chronothermostat/thermoregulation/addressLocation/plants/%s/modules/parameter/id/value/%s";
|
||||
private static final String PATH_PROGRAMS = "/programlist";
|
||||
private static final String PATH_SUBSCRIPTIONS = "/subscription";
|
||||
private static final String PATH_SUBSCRIBE = PATH_PLANTS + "/%s/subscription";
|
||||
private static final String PATH_UNSUBSCRIBE = PATH_SUBSCRIBE + "/%s";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SmartherApi.class);
|
||||
|
||||
private final OAuthClientService oAuthClientService;
|
||||
private final String oAuthSubscriptionKey;
|
||||
private final SmartherApiConnector connector;
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherApi} to the API gateway with the specified OAuth2 attributes (subscription key and
|
||||
* client service), scheduler service and http client.
|
||||
*
|
||||
* @param clientService
|
||||
* the OAuth2 authorization client service to be used
|
||||
* @param subscriptionKey
|
||||
* the OAuth2 subscription key to be used with the given client service
|
||||
* @param scheduler
|
||||
* the scheduler to be used to reschedule calls when rate limit exceeded or call not succeeded
|
||||
* @param httpClient
|
||||
* the http client to be used to make http calls to the API gateway
|
||||
*/
|
||||
public SmartherApi(final OAuthClientService clientService, final String subscriptionKey,
|
||||
final ScheduledExecutorService scheduler, final HttpClient httpClient) {
|
||||
this.oAuthClientService = clientService;
|
||||
this.oAuthSubscriptionKey = subscriptionKey;
|
||||
this.connector = new SmartherApiConnector(scheduler, httpClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plants registered under the Smarther account the bridge has been configured with.
|
||||
*
|
||||
* @return the list of registered plants, or an empty {@link List} in case of no plants found
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the API gateway
|
||||
*/
|
||||
public List<Plant> getPlants() throws SmartherGatewayException {
|
||||
try {
|
||||
final ContentResponse response = requestBasic(GET, PATH_PLANTS);
|
||||
if (response.getStatus() == HttpStatus.NO_CONTENT_204) {
|
||||
return new ArrayList<>();
|
||||
} else {
|
||||
return ModelUtil.gsonInstance().fromJson(response.getContentAsString(), Plants.class).getPlants();
|
||||
}
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new SmartherGatewayException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the chronothermostat modules registered in the given plant.
|
||||
*
|
||||
* @param plantId
|
||||
* the identifier of the plant
|
||||
*
|
||||
* @return the list of registered modules, or an empty {@link List} in case the plant contains no module
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the API gateway
|
||||
*/
|
||||
public List<Module> getPlantModules(String plantId) throws SmartherGatewayException {
|
||||
try {
|
||||
final ContentResponse response = requestBasic(GET, String.format(PATH_TOPOLOGY, plantId));
|
||||
final Topology topology = ModelUtil.gsonInstance().fromJson(response.getContentAsString(), Topology.class);
|
||||
return topology.getModules();
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new SmartherGatewayException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current status of a given chronothermostat module.
|
||||
*
|
||||
* @param plantId
|
||||
* the identifier of the plant
|
||||
* @param moduleId
|
||||
* the identifier of the chronothermostat module inside the plant
|
||||
*
|
||||
* @return the current status of the chronothermostat module
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the API gateway
|
||||
*/
|
||||
public ModuleStatus getModuleStatus(String plantId, String moduleId) throws SmartherGatewayException {
|
||||
try {
|
||||
final ContentResponse response = requestModule(GET, plantId, moduleId, null);
|
||||
return ModelUtil.gsonInstance().fromJson(response.getContentAsString(), ModuleStatus.class);
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new SmartherGatewayException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends new settings to be applied to a given chronothermostat module.
|
||||
*
|
||||
* @param settings
|
||||
* the module settings to be applied
|
||||
*
|
||||
* @return {@code true} if the settings have been successfully applied, {@code false} otherwise
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the API gateway
|
||||
*/
|
||||
public boolean setModuleStatus(ModuleSettings settings) throws SmartherGatewayException {
|
||||
// Prepare request payload
|
||||
Map<String, Object> rootMap = new IdentityHashMap<>();
|
||||
rootMap.put(ATTR_FUNCTION, settings.getFunction().getValue());
|
||||
rootMap.put(ATTR_MODE, settings.getMode().getValue());
|
||||
switch (settings.getMode()) {
|
||||
case AUTOMATIC:
|
||||
// {"function":"heating","mode":"automatic","programs":[{"number":0}]}
|
||||
Map<String, Integer> programMap = new IdentityHashMap<String, Integer>();
|
||||
programMap.put(ATTR_NUMBER, Integer.valueOf(settings.getProgram()));
|
||||
List<Map<String, Integer>> programsList = new ArrayList<>();
|
||||
programsList.add(programMap);
|
||||
rootMap.put(ATTR_PROGRAMS, programsList);
|
||||
break;
|
||||
case MANUAL:
|
||||
// {"function":"heating","mode":"manual","setPoint":{"value":0.0,"unit":"C"},"activationTime":"X"}
|
||||
QuantityType<Temperature> newTemperature = settings.getSetPointTemperature(SIUnits.CELSIUS);
|
||||
if (newTemperature == null) {
|
||||
throw new SmartherGatewayException("Invalid temperature unit transformation");
|
||||
}
|
||||
Map<String, Object> setPointMap = new IdentityHashMap<String, Object>();
|
||||
setPointMap.put(ATTR_VALUE, newTemperature.doubleValue());
|
||||
setPointMap.put(ATTR_UNIT, MeasureUnit.CELSIUS.getValue());
|
||||
rootMap.put(ATTR_SETPOINT, setPointMap);
|
||||
rootMap.put(ATTR_ACTIVATION_TIME, settings.getActivationTime());
|
||||
break;
|
||||
case BOOST:
|
||||
// {"function":"heating","mode":"boost","activationTime":"X"}
|
||||
rootMap.put(ATTR_ACTIVATION_TIME, settings.getActivationTime());
|
||||
break;
|
||||
case OFF:
|
||||
// {"function":"heating","mode":"off"}
|
||||
break;
|
||||
case PROTECTION:
|
||||
// {"function":"heating","mode":"protection"}
|
||||
break;
|
||||
}
|
||||
final String jsonPayload = ModelUtil.gsonInstance().toJson(rootMap);
|
||||
|
||||
// Send request to server
|
||||
final ContentResponse response = requestModule(POST, settings.getPlantId(), settings.getModuleId(),
|
||||
jsonPayload);
|
||||
return (response.getStatus() == HttpStatus.OK_200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the automatic mode programs registered for the given chronothermostat module.
|
||||
*
|
||||
* @param plantId
|
||||
* the identifier of the plant
|
||||
* @param moduleId
|
||||
* the identifier of the chronothermostat module inside the plant
|
||||
*
|
||||
* @return the list of registered programs, or an empty {@link List} in case of no programs found
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the API gateway
|
||||
*/
|
||||
public List<Program> getModulePrograms(String plantId, String moduleId) throws SmartherGatewayException {
|
||||
try {
|
||||
final ContentResponse response = requestModule(GET, plantId, moduleId, PATH_PROGRAMS, null);
|
||||
final ModuleStatus moduleStatus = ModelUtil.gsonInstance().fromJson(response.getContentAsString(),
|
||||
ModuleStatus.class);
|
||||
|
||||
final Chronothermostat chronothermostat = moduleStatus.toChronothermostat();
|
||||
return (chronothermostat != null) ? chronothermostat.getPrograms() : Collections.emptyList();
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new SmartherGatewayException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subscriptions registered to the C2C Webhook, where modules status notifications are currently sent
|
||||
* for all the plants.
|
||||
*
|
||||
* @return the list of registered subscriptions, or an empty {@link List} in case of no subscriptions found
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the API gateway
|
||||
*/
|
||||
public List<Subscription> getSubscriptions() throws SmartherGatewayException {
|
||||
try {
|
||||
final ContentResponse response = requestBasic(GET, PATH_SUBSCRIPTIONS);
|
||||
if (response.getStatus() == HttpStatus.NO_CONTENT_204) {
|
||||
return new ArrayList<>();
|
||||
} else {
|
||||
return ModelUtil.gsonInstance().fromJson(response.getContentAsString(),
|
||||
new TypeToken<List<Subscription>>() {
|
||||
}.getType());
|
||||
}
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new SmartherGatewayException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes a plant to the C2C Webhook to start receiving modules status notifications.
|
||||
*
|
||||
* @param plantId
|
||||
* the identifier of the plant to be subscribed
|
||||
* @param notificationUrl
|
||||
* the url notifications will have to be sent to for the given plant
|
||||
*
|
||||
* @return the identifier this subscription has been registered under
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the API gateway
|
||||
*/
|
||||
public String subscribePlant(String plantId, String notificationUrl) throws SmartherGatewayException {
|
||||
try {
|
||||
// Prepare request payload
|
||||
Map<String, Object> rootMap = new IdentityHashMap<String, Object>();
|
||||
rootMap.put(ATTR_ENDPOINT_URL, notificationUrl);
|
||||
final String jsonPayload = ModelUtil.gsonInstance().toJson(rootMap);
|
||||
// Send request to server
|
||||
final ContentResponse response = requestBasic(POST, String.format(PATH_SUBSCRIBE, plantId), jsonPayload);
|
||||
// Handle response payload
|
||||
final Subscription subscription = ModelUtil.gsonInstance().fromJson(response.getContentAsString(),
|
||||
Subscription.class);
|
||||
return subscription.getSubscriptionId();
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new SmartherGatewayException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes a plant from the C2C Webhook to stop receiving modules status notifications.
|
||||
*
|
||||
* @param plantId
|
||||
* the identifier of the plant to be unsubscribed
|
||||
* @param subscriptionId
|
||||
* the identifier of the subscription to be removed for the given plant
|
||||
*
|
||||
* @return {@code true} if the plant is successfully unsubscribed, {@code false} otherwise
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the API gateway
|
||||
*/
|
||||
public boolean unsubscribePlant(String plantId, String subscriptionId) throws SmartherGatewayException {
|
||||
final ContentResponse response = requestBasic(DELETE, String.format(PATH_UNSUBSCRIBE, plantId, subscriptionId));
|
||||
return (response.getStatus() == HttpStatus.OK_200);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
//
|
||||
// Internal API call handling methods
|
||||
//
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Calls the API gateway with the given http method, request url and actual data.
|
||||
*
|
||||
* @param method
|
||||
* the http method to make the call with
|
||||
* @param url
|
||||
* the API operation url to call
|
||||
* @param requestData
|
||||
* the actual data to send in the request body, may be {@code null}
|
||||
*
|
||||
* @return the response received from the API gateway
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the API gateway
|
||||
*/
|
||||
private ContentResponse requestBasic(HttpMethod method, String url, @Nullable String requestData)
|
||||
throws SmartherGatewayException {
|
||||
return request(method, SMARTHER_API_URL + url, requestData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the API gateway with the given http method and request url.
|
||||
*
|
||||
* @param method
|
||||
* the http method to make the call with
|
||||
* @param url
|
||||
* the API operation url to call
|
||||
*
|
||||
* @return the response received from the API gateway
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the API gateway
|
||||
*/
|
||||
private ContentResponse requestBasic(HttpMethod method, String url) throws SmartherGatewayException {
|
||||
return requestBasic(method, url, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the API gateway with the given http method, plant id, module id, request path and actual data.
|
||||
*
|
||||
* @param method
|
||||
* the http method to make the call with
|
||||
* @param plantId
|
||||
* the identifier of the plant to use
|
||||
* @param moduleId
|
||||
* the identifier of the module to use
|
||||
* @param path
|
||||
* the API operation relative path to call, may be {@code null}
|
||||
* @param requestData
|
||||
* the actual data to send in the request body, may be {@code null}
|
||||
*
|
||||
* @return the response received from the API gateway
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the API gateway
|
||||
*/
|
||||
private ContentResponse requestModule(HttpMethod method, String plantId, String moduleId, @Nullable String path,
|
||||
@Nullable String requestData) throws SmartherGatewayException {
|
||||
final String url = String.format(PATH_MODULE, plantId, moduleId) + StringUtil.defaultString(path);
|
||||
return requestBasic(method, url, requestData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the API gateway with the given http method, plant id, module id and actual data.
|
||||
*
|
||||
* @param method
|
||||
* the http method to make the call with
|
||||
* @param plantId
|
||||
* the identifier of the plant to use
|
||||
* @param moduleId
|
||||
* the identifier of the module to use
|
||||
* @param requestData
|
||||
* the actual data to send in the request body, may be {@code null}
|
||||
*
|
||||
* @return the response received from the API gateway
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the API gateway
|
||||
*/
|
||||
private ContentResponse requestModule(HttpMethod method, String plantId, String moduleId,
|
||||
@Nullable String requestData) throws SmartherGatewayException {
|
||||
return requestModule(method, plantId, moduleId, null, requestData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the API gateway with the given http method, request url and actual data.
|
||||
*
|
||||
* @param method
|
||||
* the http method to make the call with
|
||||
* @param url
|
||||
* the API operation url to call
|
||||
* @param requestData
|
||||
* the actual data to send in the request body, may be {@code null}
|
||||
*
|
||||
* @return the response received from the API gateway
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the API gateway
|
||||
*/
|
||||
private ContentResponse request(HttpMethod method, String url, @Nullable String requestData)
|
||||
throws SmartherGatewayException {
|
||||
logger.debug("Request: ({}) {} - {}", method, url, StringUtil.defaultString(requestData));
|
||||
Function<HttpClient, Request> call = httpClient -> httpClient.newRequest(url).method(method)
|
||||
.header(HEADER_ACCEPT, CONTENT_TYPE)
|
||||
.content(new StringContentProvider(StringUtil.defaultString(requestData)), CONTENT_TYPE);
|
||||
|
||||
try {
|
||||
final AccessTokenResponse accessTokenResponse = oAuthClientService.getAccessTokenResponse();
|
||||
final String accessToken = (accessTokenResponse == null) ? null : accessTokenResponse.getAccessToken();
|
||||
|
||||
if (accessToken == null || accessToken.isEmpty()) {
|
||||
throw new SmartherAuthorizationException(String
|
||||
.format("No gateway accesstoken. Did you authorize smarther via %s ?", AUTH_SERVLET_ALIAS));
|
||||
} else {
|
||||
return requestWithRetry(call, accessToken);
|
||||
}
|
||||
} catch (SmartherGatewayException e) {
|
||||
throw e;
|
||||
} catch (OAuthException | OAuthResponseException e) {
|
||||
throw new SmartherAuthorizationException(e.getMessage(), e);
|
||||
} catch (IOException e) {
|
||||
throw new SmartherGatewayException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages a generic call to the API gateway using the given authorization access token.
|
||||
* Retries the call if the access token is expired (refreshing it on behalf of further calls).
|
||||
*
|
||||
* @param call
|
||||
* the http call to make
|
||||
* @param accessToken
|
||||
* the authorization access token to use
|
||||
*
|
||||
* @return the response received from the API gateway
|
||||
*
|
||||
* @throws {@link OAuthException}
|
||||
* in case of issues during the OAuth process
|
||||
* @throws {@link OAuthResponseException}
|
||||
* in case of response issues during the OAuth process
|
||||
* @throws {@link IOException}
|
||||
* in case of I/O issues of some sort
|
||||
*/
|
||||
private ContentResponse requestWithRetry(final Function<HttpClient, Request> call, final String accessToken)
|
||||
throws OAuthException, OAuthResponseException, IOException {
|
||||
try {
|
||||
return this.connector.request(call, this.oAuthSubscriptionKey, BEARER + accessToken);
|
||||
} catch (SmartherTokenExpiredException e) {
|
||||
// Retry with new access token
|
||||
try {
|
||||
return this.connector.request(call, this.oAuthSubscriptionKey,
|
||||
BEARER + this.oAuthClientService.refreshToken().getAccessToken());
|
||||
} catch (SmartherTokenExpiredException ex) {
|
||||
// This should never happen in normal conditions
|
||||
throw new SmartherAuthorizationException(String.format("Cannot refresh token: %s", ex.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api;
|
||||
|
||||
import static org.eclipse.jetty.http.HttpStatus.*;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherAuthorizationException;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherInvalidResponseException;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherSubscriptionAlreadyExistsException;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherTokenExpiredException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* The {@code SmartherApiConnector} class is used to perform the actual call to the API gateway.
|
||||
* It handles the returned http status codes and the error codes eventually returned by the API gateway itself.
|
||||
*
|
||||
* Response mappings:
|
||||
* <ul>
|
||||
* <li>Plants : 200, 204, 400, 401, 404, 408, 469, 470, 500</li>
|
||||
* <li>Topology : 200, 400, 401, 404, 408, 469, 470, 500</li>
|
||||
* <li>Measures : 200, 400, 401, 404, 408, 469, 470, 500</li>
|
||||
* <li>ProgramList : 200, 400, 401, 404, 408, 469, 470, 500</li>
|
||||
* <li>Get Status : 200, 400, 401, 404, 408, 469, 470, 500</li>
|
||||
* <li>Set Status : 200, 400, 401, 404, 408, 430, 469, 470, 486, 500</li>
|
||||
* <li>Get Subscriptions : 200, 204, 400, 401, 404, 500</li>
|
||||
* <li>Subscribe : 201, 400, 401, 404, 409, 500</li>
|
||||
* <li>Delete Subscription : 200, 400, 401, 404, 500</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmartherApiConnector {
|
||||
|
||||
private static final String RETRY_AFTER_HEADER = "Retry-After";
|
||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
private static final String SUBSCRIPTION_HEADER = "Ocp-Apim-Subscription-Key";
|
||||
|
||||
private static final String ERROR_CODE = "statusCode";
|
||||
private static final String ERROR_MESSAGE = "message";
|
||||
private static final String TOKEN_EXPIRED = "expired";
|
||||
private static final String AUTHORIZATION_ERROR = "error_description";
|
||||
|
||||
private static final int HTTP_CLIENT_TIMEOUT_SECONDS = 10;
|
||||
private static final int HTTP_CLIENT_RETRY_COUNT = 5;
|
||||
|
||||
// Set Chronothermostat Status > Wrong input parameters
|
||||
private static final int WRONG_INPUT_PARAMS_430 = 430;
|
||||
// Official application password expired: password used in the Thermostat official app is expired.
|
||||
private static final int APP_PASSWORD_EXPIRED_469 = 469;
|
||||
// Official application terms and conditions expired: terms and conditions for Thermostat official app are expired.
|
||||
private static final int APP_TERMS_EXPIRED_470 = 470;
|
||||
// Set Chronothermostat Status > Busy visual user interface
|
||||
private static final int BUSY_VISUAL_UI_486 = 486;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SmartherApiConnector.class);
|
||||
|
||||
private final JsonParser parser = new JsonParser();
|
||||
private final HttpClient httpClient;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherApiConnector} to the API gateway with the specified scheduler and http client.
|
||||
*
|
||||
* @param scheduler
|
||||
* the scheduler to be used to reschedule calls when rate limit exceeded or call not succeeded
|
||||
* @param httpClient
|
||||
* the http client to be used to make http calls to the API gateway
|
||||
*/
|
||||
public SmartherApiConnector(ScheduledExecutorService scheduler, HttpClient httpClient) {
|
||||
this.scheduler = scheduler;
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a call to the API gateway and returns the raw response.
|
||||
*
|
||||
* @param requester
|
||||
* the function to construct the request, using the http client that is passed as argument to the
|
||||
* function itself
|
||||
* @param subscription
|
||||
* the subscription string to be used in the call {@code Subscription} header
|
||||
* @param authorization
|
||||
* the authorization string to be used in the call {@code Authorization} header
|
||||
*
|
||||
* @return the raw response returned by the API gateway
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* if the call failed due to an issue with the API gateway
|
||||
*/
|
||||
public ContentResponse request(Function<HttpClient, Request> requester, String subscription, String authorization)
|
||||
throws SmartherGatewayException {
|
||||
final Caller caller = new Caller(requester, subscription, authorization);
|
||||
|
||||
try {
|
||||
return caller.call().get();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new SmartherGatewayException("Thread interrupted");
|
||||
} catch (ExecutionException e) {
|
||||
final Throwable cause = e.getCause();
|
||||
|
||||
if (cause instanceof SmartherGatewayException) {
|
||||
throw (SmartherGatewayException) cause;
|
||||
} else {
|
||||
throw new SmartherGatewayException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@code Caller} class represents the handler to make calls to the API gateway.
|
||||
* In case of rate limiting or not finished jobs, it will retry a number of times in a specified timeframe then
|
||||
* gives up with an exception.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
private class Caller {
|
||||
private final Function<HttpClient, Request> requester;
|
||||
private final String subscription;
|
||||
private final String authorization;
|
||||
|
||||
private final CompletableFuture<ContentResponse> future = new CompletableFuture<>();
|
||||
private int delaySeconds;
|
||||
private int attempts;
|
||||
|
||||
/**
|
||||
* Constructs a {@code Caller} to the API gateway with the specified requester, subscription and authorization.
|
||||
*
|
||||
* @param requester
|
||||
* the function to construct the request, using the http client that is passed as argument to the
|
||||
* function itself
|
||||
* @param subscription
|
||||
* the subscription string to be used in the call {@code Subscription} header
|
||||
* @param authorization
|
||||
* the authorization string to be used in the call {@code Authorization} header
|
||||
*/
|
||||
public Caller(Function<HttpClient, Request> requester, String subscription, String authorization) {
|
||||
this.requester = requester;
|
||||
this.subscription = subscription;
|
||||
this.authorization = authorization;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the request as a {@link CompletableFuture}, setting its state once finished.
|
||||
* The original caller should call the {@code get} method on the Future to wait for the call to finish.
|
||||
* The first attempt is not scheduled so, if the first call succeeds, the {@code get} method directly returns
|
||||
* the value. This method is rescheduled in case the call is to be retried.
|
||||
*
|
||||
* @return the {@link CompletableFuture} holding the call
|
||||
*/
|
||||
public CompletableFuture<ContentResponse> call() {
|
||||
attempts++;
|
||||
try {
|
||||
final boolean success = processResponse(requester.apply(httpClient)
|
||||
.header(SUBSCRIPTION_HEADER, subscription).header(AUTHORIZATION_HEADER, authorization)
|
||||
.timeout(HTTP_CLIENT_TIMEOUT_SECONDS, TimeUnit.SECONDS).send());
|
||||
|
||||
if (!success) {
|
||||
if (attempts < HTTP_CLIENT_RETRY_COUNT) {
|
||||
logger.debug("API Gateway call attempt: {}", attempts);
|
||||
|
||||
scheduler.schedule(this::call, delaySeconds, TimeUnit.SECONDS);
|
||||
} else {
|
||||
logger.debug("Giving up on accessing API Gateway. Check network connectivity!");
|
||||
future.completeExceptionally(new SmartherGatewayException(
|
||||
String.format("Could not reach the API Gateway after %s retries.", attempts)));
|
||||
}
|
||||
}
|
||||
} catch (ExecutionException e) {
|
||||
future.completeExceptionally(e.getCause());
|
||||
} catch (SmartherGatewayException e) {
|
||||
future.completeExceptionally(e);
|
||||
} catch (RuntimeException | TimeoutException e) {
|
||||
future.completeExceptionally(e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the response from the API gateway call and handles the http status codes.
|
||||
*
|
||||
* @param response
|
||||
* the response content returned by the API gateway
|
||||
*
|
||||
* @return {@code true} if the call was successful, {@code false} if the call failed in a way that can be
|
||||
* retried
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* if the call failed due to an irrecoverable issue and cannot be retried (user should be informed)
|
||||
*/
|
||||
private boolean processResponse(ContentResponse response) throws SmartherGatewayException {
|
||||
boolean success = false;
|
||||
|
||||
logger.debug("Response Code: {}", response.getStatus());
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Response Data: {}", response.getContentAsString());
|
||||
}
|
||||
switch (response.getStatus()) {
|
||||
case OK_200:
|
||||
case CREATED_201:
|
||||
case NO_CONTENT_204:
|
||||
case NOT_MODIFIED_304:
|
||||
future.complete(response);
|
||||
success = true;
|
||||
break;
|
||||
|
||||
case ACCEPTED_202:
|
||||
logger.debug(
|
||||
"API Gateway returned error status 202 (the request has been accepted for processing, but the processing has not been completed)");
|
||||
future.complete(response);
|
||||
success = true;
|
||||
break;
|
||||
|
||||
case FORBIDDEN_403:
|
||||
// Process for authorization error, and logging.
|
||||
processErrorState(response);
|
||||
future.complete(response);
|
||||
success = true;
|
||||
break;
|
||||
|
||||
case BAD_REQUEST_400:
|
||||
case NOT_FOUND_404:
|
||||
case REQUEST_TIMEOUT_408:
|
||||
case WRONG_INPUT_PARAMS_430:
|
||||
case APP_PASSWORD_EXPIRED_469:
|
||||
case APP_TERMS_EXPIRED_470:
|
||||
case BUSY_VISUAL_UI_486:
|
||||
case INTERNAL_SERVER_ERROR_500:
|
||||
throw new SmartherGatewayException(processErrorState(response));
|
||||
|
||||
case UNAUTHORIZED_401:
|
||||
throw new SmartherAuthorizationException(processErrorState(response));
|
||||
|
||||
case CONFLICT_409:
|
||||
// Subscribe to C2C notifications > Subscription already exists.
|
||||
throw new SmartherSubscriptionAlreadyExistsException(processErrorState(response));
|
||||
|
||||
case TOO_MANY_REQUESTS_429:
|
||||
// Response Code 429 means requests rate limits exceeded.
|
||||
final String retryAfter = response.getHeaders().get(RETRY_AFTER_HEADER);
|
||||
logger.debug(
|
||||
"API Gateway returned error status 429 (rate limit exceeded - retry after {} seconds, decrease polling interval of bridge, going to sleep...)",
|
||||
retryAfter);
|
||||
delaySeconds = Integer.parseInt(retryAfter);
|
||||
break;
|
||||
|
||||
case BAD_GATEWAY_502:
|
||||
case SERVICE_UNAVAILABLE_503:
|
||||
default:
|
||||
throw new SmartherGatewayException(String.format("API Gateway returned error status %s (%s)",
|
||||
response.getStatus(), HttpStatus.getMessage(response.getStatus())));
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the responded content if the status code indicated an error.
|
||||
*
|
||||
* @param response
|
||||
* the response content returned by the API gateway
|
||||
*
|
||||
* @return the error message extracted from the response content
|
||||
*
|
||||
* @throws {@link SmartherTokenExpiredException}
|
||||
* if the authorization access token used to communicate with the API gateway has expired
|
||||
* @throws {@link SmartherAuthorizationException}
|
||||
* if a generic authorization issue with the API gateway has occurred
|
||||
* @throws {@link SmartherInvalidResponseException}
|
||||
* if the response received from the API gateway cannot be parsed
|
||||
*/
|
||||
private String processErrorState(ContentResponse response)
|
||||
throws SmartherTokenExpiredException, SmartherAuthorizationException, SmartherInvalidResponseException {
|
||||
try {
|
||||
final JsonElement element = parser.parse(response.getContentAsString());
|
||||
|
||||
if (element.isJsonObject()) {
|
||||
final JsonObject object = element.getAsJsonObject();
|
||||
if (object.has(ERROR_CODE) && object.has(ERROR_MESSAGE)) {
|
||||
final String message = object.get(ERROR_MESSAGE).getAsString();
|
||||
|
||||
// Bad request can be anything, from authorization problems to plant or module problems.
|
||||
// Therefore authorization type errors are filtered and handled differently.
|
||||
logger.debug("Bad request: {}", message);
|
||||
if (message.contains(TOKEN_EXPIRED)) {
|
||||
throw new SmartherTokenExpiredException(message);
|
||||
} else {
|
||||
return message;
|
||||
}
|
||||
} else if (object.has(AUTHORIZATION_ERROR)) {
|
||||
final String errorDescription = object.get(AUTHORIZATION_ERROR).getAsString();
|
||||
throw new SmartherAuthorizationException(errorDescription);
|
||||
}
|
||||
}
|
||||
logger.debug("Unknown response: {}", response);
|
||||
return "Unknown response";
|
||||
} catch (JsonSyntaxException e) {
|
||||
logger.warn("Response was not json: ", e);
|
||||
throw new SmartherInvalidResponseException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.dto;
|
||||
|
||||
import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.LoadState;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.MeasureUnit;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherIllegalPropertyValueException;
|
||||
import org.openhab.binding.bticinosmarther.internal.util.DateUtil;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@code Chronothermostat} class defines the dto for Smarther API chronothermostat object.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
public class Chronothermostat {
|
||||
|
||||
private static final String TIME_FOREVER = "Forever";
|
||||
|
||||
private String function;
|
||||
private String mode;
|
||||
@SerializedName("setPoint")
|
||||
private Measure setPointTemperature;
|
||||
private List<Program> programs;
|
||||
@SerializedName("temperatureFormat")
|
||||
private String temperatureFormat;
|
||||
@SerializedName("loadState")
|
||||
private String loadState;
|
||||
@SerializedName("activationTime")
|
||||
private String activationTime;
|
||||
private String time;
|
||||
private Sensor thermometer;
|
||||
private Sensor hygrometer;
|
||||
private boolean online;
|
||||
private Sender sender;
|
||||
|
||||
/**
|
||||
* Returns the operational function of this chronothermostat module.
|
||||
*
|
||||
* @return a string containing the module operational function
|
||||
*/
|
||||
public String getFunction() {
|
||||
return function;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the operational mode of this chronothermostat module.
|
||||
*
|
||||
* @return a string containing the module operational mode
|
||||
*/
|
||||
public String getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the operational setpoint temperature of this chronothermostat module.
|
||||
*
|
||||
* @return a {@link Measure} object representing the module operational setpoint temperature
|
||||
*/
|
||||
public Measure getSetPointTemperature() {
|
||||
return setPointTemperature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of programs registered on this chronothermostat module.
|
||||
*
|
||||
* @return the list of registered programs, or an empty list in case of no programs available
|
||||
*/
|
||||
public List<Program> getPrograms() {
|
||||
return (programs != null) ? programs : Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the operational temperature format of this chronothermostat module.
|
||||
*
|
||||
* @return a string containing the module operational temperature format
|
||||
*/
|
||||
public String getTemperatureFormat() {
|
||||
return temperatureFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the operational temperature format of this chronothermostat module.
|
||||
*
|
||||
* @return a {@link MeasureUnit} object representing the module operational temperature format
|
||||
*
|
||||
* @throws {@link SmartherIllegalPropertyValueException}
|
||||
* if the measure internal raw unit cannot be mapped to any valid measure unit
|
||||
*/
|
||||
public MeasureUnit getTemperatureFormatUnit() throws SmartherIllegalPropertyValueException {
|
||||
return MeasureUnit.fromValue(temperatureFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the operational load state of this chronothermostat module.
|
||||
*
|
||||
* @return a string containing the module operational load state
|
||||
*/
|
||||
public String getLoadState() {
|
||||
return loadState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the load state of this chronothermostat module is "active" (i.e. module is turned on).
|
||||
*
|
||||
* @return {@code true} if the load state is active, {@code false} otherwise
|
||||
*
|
||||
* @throws {@link SmartherIllegalPropertyValueException}
|
||||
* if the load state internal raw value cannot be mapped to any valid load state enum value
|
||||
*/
|
||||
public boolean isActive() throws SmartherIllegalPropertyValueException {
|
||||
return LoadState.fromValue(loadState).isActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the operational activation time of this chronothermostat module.
|
||||
*
|
||||
* @return a string containing the module operational activation time
|
||||
*/
|
||||
public String getActivationTime() {
|
||||
return activationTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a label for the operational activation time of this chronothermostat module.
|
||||
*
|
||||
* @return a string containing the module operational activation time label, or {@code null} if the activation time
|
||||
* cannot be parsed to a valid date/time
|
||||
*/
|
||||
public @Nullable String getActivationTimeLabel() {
|
||||
String timeLabel = TIME_FOREVER;
|
||||
if (activationTime != null) {
|
||||
try {
|
||||
final ZonedDateTime dateActivationTime = DateUtil.parseZonedTime(activationTime, DTF_DATETIME_EXT);
|
||||
final ZonedDateTime dateTomorrow = DateUtil.getZonedStartOfDay(1, dateActivationTime.getZone());
|
||||
|
||||
if (dateActivationTime.isBefore(dateTomorrow)) {
|
||||
timeLabel = DateUtil.format(dateActivationTime, DTF_TODAY);
|
||||
} else if (dateActivationTime.isBefore(dateTomorrow.plusDays(1))) {
|
||||
timeLabel = DateUtil.format(dateActivationTime, DTF_TOMORROW);
|
||||
} else {
|
||||
timeLabel = DateUtil.format(dateActivationTime, DTF_DAY_HHMM);
|
||||
}
|
||||
} catch (DateTimeParseException e) {
|
||||
timeLabel = null;
|
||||
}
|
||||
}
|
||||
return timeLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current time (clock) of this chronothermostat module.
|
||||
*
|
||||
* @return a string containing the module current time
|
||||
*/
|
||||
public String getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the thermometer sensor of this chronothermostat module.
|
||||
*
|
||||
* @return the thermometer sensor of this module
|
||||
*/
|
||||
public Sensor getThermometer() {
|
||||
return thermometer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hygrometer sensor of this chronothermostat module.
|
||||
*
|
||||
* @return the hygrometer sensor of this module
|
||||
*/
|
||||
public Sensor getHygrometer() {
|
||||
return hygrometer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether this module is online.
|
||||
*
|
||||
* @return {@code true} if the module is online, {@code false} otherwise
|
||||
*/
|
||||
public boolean isOnline() {
|
||||
return online;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sender associated with this chronothermostat module.
|
||||
*
|
||||
* @return a {@link Sender} object representing the sender associated with this module, or {@code null} in case of
|
||||
* no sender information available
|
||||
*/
|
||||
public @Nullable Sender getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the operational program of this chronothermostat module.
|
||||
*
|
||||
* @return a {@link Program} object representing the module operational program, or {@code null} in case of no
|
||||
* program currently set for this module
|
||||
*/
|
||||
public @Nullable Program getProgram() {
|
||||
return (programs != null && !programs.isEmpty()) ? programs.get(0) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"function=%s, mode=%s, setPointTemperature=[%s], programs=%s, temperatureFormat=%s, loadState=%s, time=%s, activationTime=%s, thermometer=[%s], hygrometer=[%s], online=%s, sender=[%s]",
|
||||
function, mode, setPointTemperature, programs, temperatureFormat, loadState, time, activationTime,
|
||||
thermometer, hygrometer, online, sender);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.dto;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherIllegalPropertyValueException;
|
||||
import org.openhab.core.library.unit.ImperialUnits;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
|
||||
/**
|
||||
* The {@code Enums} class represents a container for enums related to Smarther API.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Enums {
|
||||
|
||||
/**
|
||||
* The {@code Function} enum maps the values of chronothermostat operation function.
|
||||
*/
|
||||
public enum Function implements TypeWithStringProperty {
|
||||
HEATING("HEATING"),
|
||||
COOLING("COOLING");
|
||||
|
||||
private final String value;
|
||||
|
||||
Function(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code Function} enum value from the given raw value.
|
||||
*
|
||||
* @param value
|
||||
* the raw value to get an enum value from
|
||||
*
|
||||
* @return the enum value representing the given raw value
|
||||
*
|
||||
* @throws {@link SmartherIllegalPropertyValueException}
|
||||
* if the raw value cannot be mapped to any valid enum value
|
||||
*/
|
||||
public static Function fromValue(String value) throws SmartherIllegalPropertyValueException {
|
||||
return lookup(Function.class, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@code Mode} enum maps the values of chronothermostat operation mode.
|
||||
*/
|
||||
public enum Mode implements TypeWithStringProperty {
|
||||
AUTOMATIC("AUTOMATIC"),
|
||||
MANUAL("MANUAL"),
|
||||
BOOST("BOOST"),
|
||||
OFF("OFF"),
|
||||
PROTECTION("PROTECTION");
|
||||
|
||||
private final String value;
|
||||
|
||||
Mode(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code Mode} enum value from the given raw value.
|
||||
*
|
||||
* @param value
|
||||
* the raw value to get an enum value from
|
||||
*
|
||||
* @return the enum value representing the given raw value
|
||||
*
|
||||
* @throws {@link SmartherIllegalPropertyValueException}
|
||||
* if the raw value cannot be mapped to any valid enum value
|
||||
*/
|
||||
public static Mode fromValue(String value) throws SmartherIllegalPropertyValueException {
|
||||
return lookup(Mode.class, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@code LoadState} enum maps the values of chronothermostat operation load state.
|
||||
*/
|
||||
public enum LoadState implements TypeWithStringProperty {
|
||||
ACTIVE("ACTIVE"),
|
||||
INACTIVE("INACTIVE");
|
||||
|
||||
private final String value;
|
||||
|
||||
LoadState(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the load state value is "active".
|
||||
*
|
||||
* @return {@code true} if the load state value is "active", {@code false} otherwise
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return ACTIVE.getValue().equals(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code LoadState} enum value from the given raw value.
|
||||
*
|
||||
* @param value
|
||||
* the raw value to get an enum value from
|
||||
*
|
||||
* @return the enum value representing the given raw value
|
||||
*
|
||||
* @throws {@link SmartherIllegalPropertyValueException}
|
||||
* if the raw value cannot be mapped to any valid enum value
|
||||
*/
|
||||
public static LoadState fromValue(String value) throws SmartherIllegalPropertyValueException {
|
||||
return lookup(LoadState.class, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@code MeasureUnit} enum maps the values of managed measure unit.
|
||||
*/
|
||||
public enum MeasureUnit implements TypeWithStringProperty {
|
||||
CELSIUS("C"),
|
||||
FAHRENHEIT("F"),
|
||||
PERCENTAGE("%"),
|
||||
DIMENSIONLESS("");
|
||||
|
||||
private final String value;
|
||||
|
||||
MeasureUnit(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code MeasureUnit} enum value for the given measure {@link Unit}.
|
||||
*
|
||||
* @param unit
|
||||
* the measure unit to get an enum value for
|
||||
*
|
||||
* @return the enum value representing the given measure unit
|
||||
*/
|
||||
public static MeasureUnit fromUnit(Unit<?> unit) {
|
||||
if (unit == SIUnits.CELSIUS) {
|
||||
return CELSIUS;
|
||||
} else if (unit == ImperialUnits.FAHRENHEIT) {
|
||||
return FAHRENHEIT;
|
||||
} else if (unit == SmartHomeUnits.PERCENT) {
|
||||
return PERCENTAGE;
|
||||
} else {
|
||||
return DIMENSIONLESS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code MeasureUnit} enum value from the given raw value.
|
||||
*
|
||||
* @param value
|
||||
* the raw value to get an enum value from
|
||||
*
|
||||
* @return the enum value representing the given raw value
|
||||
*
|
||||
* @throws {@link SmartherIllegalPropertyValueException}
|
||||
* if the raw value cannot be mapped to any valid enum value
|
||||
*/
|
||||
public static MeasureUnit fromValue(String value) throws SmartherIllegalPropertyValueException {
|
||||
return lookup(MeasureUnit.class, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@code BoostTime} enum maps the time values of chronothermostat boost mode.
|
||||
*/
|
||||
public enum BoostTime implements TypeWithIntProperty {
|
||||
MINUTES_30(30),
|
||||
MINUTES_60(60),
|
||||
MINUTES_90(90);
|
||||
|
||||
private final int value;
|
||||
|
||||
BoostTime(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code BoostTime} enum value from the given raw value.
|
||||
*
|
||||
* @param value
|
||||
* the raw value to get an enum value from
|
||||
*
|
||||
* @return the enum value representing the given raw value
|
||||
*
|
||||
* @throws {@link SmartherIllegalPropertyValueException}
|
||||
* if the raw value cannot be mapped to any valid enum value
|
||||
*/
|
||||
public static BoostTime fromValue(int value) throws SmartherIllegalPropertyValueException {
|
||||
return lookup(BoostTime.class, value);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// UTILITY INTERFACES AND METHODS
|
||||
// ------------------------------
|
||||
|
||||
interface TypeWithIntProperty {
|
||||
int getValue();
|
||||
}
|
||||
|
||||
public static <E extends Enum<E> & TypeWithIntProperty> E lookup(Class<E> en, int value)
|
||||
throws SmartherIllegalPropertyValueException {
|
||||
for (E constant : en.getEnumConstants()) {
|
||||
if (constant.getValue() == value) {
|
||||
return constant;
|
||||
}
|
||||
}
|
||||
throw new SmartherIllegalPropertyValueException(en.getSimpleName(), String.valueOf(value));
|
||||
}
|
||||
|
||||
interface TypeWithStringProperty {
|
||||
String getValue();
|
||||
}
|
||||
|
||||
public static <E extends Enum<E> & TypeWithStringProperty> E lookup(Class<E> en, String value)
|
||||
throws SmartherIllegalPropertyValueException {
|
||||
for (E constant : en.getEnumConstants()) {
|
||||
if (constant.getValue().equals(value)) {
|
||||
return constant;
|
||||
}
|
||||
}
|
||||
throw new SmartherIllegalPropertyValueException(en.getSimpleName(), value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.dto;
|
||||
|
||||
import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.NAME_SEPARATOR;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bticinosmarther.internal.util.StringUtil;
|
||||
|
||||
/**
|
||||
* The {@code Location} class defines the dto for Smarther API location object.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Location {
|
||||
|
||||
private String plantId;
|
||||
private String name;
|
||||
private @Nullable String subscriptionId;
|
||||
private @Nullable String endpointUrl;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code Location} with the given plant and subscription.
|
||||
*
|
||||
* @param plant
|
||||
* the location plant to use
|
||||
* @param subscription
|
||||
* the notification subscription endpoint to use, may be {@code null}
|
||||
*/
|
||||
private Location(Plant plant, @Nullable Subscription subscription) {
|
||||
super();
|
||||
this.plantId = plant.getId();
|
||||
this.name = plant.getName();
|
||||
if (subscription != null) {
|
||||
this.subscriptionId = subscription.getSubscriptionId();
|
||||
this.endpointUrl = subscription.getEndpointUrl();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@code Location} with the given plant and subscription.
|
||||
*
|
||||
* @param plant
|
||||
* the location plant to use
|
||||
* @param subscription
|
||||
* the notification subscription endpoint to use, may be {@code null}
|
||||
*
|
||||
* @return the newly created Location object
|
||||
*/
|
||||
public static Location fromPlant(Plant plant, @Nullable Subscription subscription) {
|
||||
return new Location(plant, subscription);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@code Location} with the given plant and no subscription.
|
||||
*
|
||||
* @param plant
|
||||
* the location plant to use
|
||||
*
|
||||
* @return the newly created Location object
|
||||
*/
|
||||
public static Location fromPlant(Plant plant) {
|
||||
return new Location(plant, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@code Location} with the given plant and optional subscription.
|
||||
*
|
||||
* @param plant
|
||||
* the location plant to use
|
||||
* @param subscription
|
||||
* the optional notification subscription endpoint to use, may contain no subscription
|
||||
*
|
||||
* @return the newly created Location object
|
||||
*/
|
||||
public static Location fromPlant(Plant plant, Optional<Subscription> subscription) {
|
||||
return (subscription.isPresent()) ? new Location(plant, subscription.get()) : new Location(plant, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plant identifier associated with this location.
|
||||
*
|
||||
* @return a string containing the plant identifier
|
||||
*/
|
||||
public String getPlantId() {
|
||||
return plantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plant name associated with this location.
|
||||
*
|
||||
* @return a string containing the plant name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the location has an associated subscription.
|
||||
*
|
||||
* @return {@code true} if the location has a subscription, {@code false} otherwise
|
||||
*/
|
||||
public boolean hasSubscription() {
|
||||
return !StringUtil.isBlank(subscriptionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the notification subscription details for the location.
|
||||
*
|
||||
* @param subscriptionId
|
||||
* the subscription identifier to use
|
||||
* @param endpointUrl
|
||||
* the notification endpoint to use
|
||||
*/
|
||||
public void setSubscription(String subscriptionId, String endpointUrl) {
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.endpointUrl = endpointUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsets the notification subscription details for the location.
|
||||
* I.e. resets all of its details to {@code null}.
|
||||
*/
|
||||
public void unsetSubscription() {
|
||||
this.subscriptionId = null;
|
||||
this.endpointUrl = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the notification subscription identifier for this location.
|
||||
*
|
||||
* @return a string containing the subscription identifier, may be {@code null}
|
||||
*/
|
||||
public @Nullable String getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the notification endpoint for this location.
|
||||
*
|
||||
* @return a string containing the notification endpoint, may be {@code null}
|
||||
*/
|
||||
public @Nullable String getEndpointUrl() {
|
||||
return endpointUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of {@link Location} objects into a string containing the location names, comma separated.
|
||||
*
|
||||
* @param locations
|
||||
* the list of location objects to be converted, may be {@code null}
|
||||
*
|
||||
* @return a string containing the comma separated location names, or {@code null} if the list is {@code null} or
|
||||
* empty.
|
||||
*/
|
||||
public static @Nullable String toNameString(@Nullable List<Location> locations) {
|
||||
if (locations == null || locations.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return locations.stream().map(a -> String.valueOf(a.getName())).collect(Collectors.joining(NAME_SEPARATOR));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("plantId=%s, name=%s, subscriptionId=%s, endpointUrl=%s", plantId, name, subscriptionId,
|
||||
endpointUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.dto;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.measure.quantity.Dimensionless;
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.MeasureUnit;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherIllegalPropertyValueException;
|
||||
import org.openhab.binding.bticinosmarther.internal.util.StringUtil;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.ImperialUnits;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import tec.uom.se.unit.Units;
|
||||
|
||||
/**
|
||||
* The {@code Measure} class defines the dto for Smarther API measure object.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
public class Measure {
|
||||
|
||||
@SerializedName("timeStamp")
|
||||
private String timestamp;
|
||||
private String value;
|
||||
private String unit;
|
||||
|
||||
public String getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of this measure.
|
||||
*
|
||||
* @return a string containing the measure value
|
||||
*/
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the measure unit of this measure.
|
||||
*
|
||||
* @return a string containing the measure unit
|
||||
*/
|
||||
public String getUnit() {
|
||||
return unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the measure unit of this measure.
|
||||
*
|
||||
* @return a {@link MeasureUnit} object representing the measure unit
|
||||
*
|
||||
* @throws {@link SmartherIllegalPropertyValueException}
|
||||
* if the measure internal raw unit cannot be mapped to any valid measure unit
|
||||
*/
|
||||
public MeasureUnit getMeasureUnit() throws SmartherIllegalPropertyValueException {
|
||||
return MeasureUnit.fromValue(unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value and measure unit of this measure as a combined {@link State} object.
|
||||
*
|
||||
* @return the value and measure unit
|
||||
*
|
||||
* @throws {@link SmartherIllegalPropertyValueException}
|
||||
* if the measure internal raw unit cannot be mapped to any valid measure unit
|
||||
*/
|
||||
public State toState() throws SmartherIllegalPropertyValueException {
|
||||
State state = UnDefType.UNDEF;
|
||||
final Optional<Double> optValue = (StringUtil.isBlank(value)) ? Optional.empty()
|
||||
: Optional.of(Double.parseDouble(value));
|
||||
|
||||
switch (MeasureUnit.fromValue(unit)) {
|
||||
case CELSIUS:
|
||||
state = optValue.<State> map(t -> new QuantityType<Temperature>(new DecimalType(t), SIUnits.CELSIUS))
|
||||
.orElse(UnDefType.UNDEF);
|
||||
break;
|
||||
case FAHRENHEIT:
|
||||
state = optValue
|
||||
.<State> map(t -> new QuantityType<Temperature>(new DecimalType(t), ImperialUnits.FAHRENHEIT))
|
||||
.orElse(UnDefType.UNDEF);
|
||||
break;
|
||||
case PERCENTAGE:
|
||||
state = optValue.<State> map(t -> new QuantityType<Dimensionless>(new DecimalType(t), Units.PERCENT))
|
||||
.orElse(UnDefType.UNDEF);
|
||||
break;
|
||||
case DIMENSIONLESS:
|
||||
state = optValue.<State> map(t -> new DecimalType(t)).orElse(UnDefType.UNDEF);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return (StringUtil.isBlank(timestamp)) ? String.format("value=%s, unit=%s", value, unit)
|
||||
: String.format("value=%s, unit=%s, timestamp=%s", value, unit, timestamp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.dto;
|
||||
|
||||
import org.openhab.binding.bticinosmarther.internal.util.StringUtil;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@code Module} class defines the dto for Smarther API chronothermostat module object.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
public class Module {
|
||||
|
||||
@SerializedName("device")
|
||||
private String deviceType;
|
||||
private String id;
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Returns the device type of the chronothermostat module.
|
||||
*
|
||||
* @return a string containing the module device type
|
||||
*/
|
||||
public String getDeviceType() {
|
||||
return StringUtil.capitalizeAll(deviceType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier of the chronothermostat module.
|
||||
*
|
||||
* @return a string containing the module identifier
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the chronothermostat module reference label (i.e. the module "name").
|
||||
*
|
||||
* @return a string containing the module reference label
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("id=%s, name=%s, type=%s", id, name, deviceType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.dto;
|
||||
|
||||
/**
|
||||
* The {@code ModuleRef} class defines the dto for Smarther API chronothermostat module reference object.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
public class ModuleRef {
|
||||
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* Returns the identifier of the chronothermostat module.
|
||||
*
|
||||
* @return a string containing the module identifier
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("id=%s", id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@code ModuleStatus} class defines the dto for Smarther API module status object.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
public class ModuleStatus {
|
||||
|
||||
private List<Chronothermostat> chronothermostats;
|
||||
|
||||
/**
|
||||
* Returns the chronothermostat details of this module status.
|
||||
*
|
||||
* @return the chronothermostat details
|
||||
*/
|
||||
public List<Chronothermostat> getChronothermostats() {
|
||||
return chronothermostats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first chronothermostat item contained in this module status.
|
||||
*
|
||||
* @return the first chronothermostat item, or {@code null} in case of no item found
|
||||
*/
|
||||
public @Nullable Chronothermostat toChronothermostat() {
|
||||
return (!chronothermostats.isEmpty() && chronothermostats.get(0) != null) ? chronothermostats.get(0) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("chronothermostats=[%s]", chronothermostats);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.dto;
|
||||
|
||||
import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.NAME_SEPARATOR;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@code Modules} class defines the dto for Smarther API list of modules.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
public class Modules {
|
||||
|
||||
private List<Module> modules;
|
||||
|
||||
/**
|
||||
* Returns the list of modules contained in this object.
|
||||
*
|
||||
* @return the list of modules
|
||||
*/
|
||||
public @Nullable List<Module> getModules() {
|
||||
return modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of {@link Module} objects into a string containing the module names, comma separated.
|
||||
*
|
||||
* @param modules
|
||||
* the list of module objects to be converted, may be {@code null}
|
||||
*
|
||||
* @return a string containing the comma separated module names, or {@code null} if the list is {@code null} or
|
||||
* empty.
|
||||
*/
|
||||
public static @Nullable String toNameString(@Nullable List<Module> modules) {
|
||||
if (modules == null || modules.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return modules.stream().map(a -> String.valueOf(a.getName())).collect(Collectors.joining(NAME_SEPARATOR));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.dto;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@code Notification} class defines the dto for Smarther API notification object.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
public class Notification {
|
||||
|
||||
private String id;
|
||||
@SerializedName("eventType")
|
||||
private String eventType;
|
||||
private String subject;
|
||||
@SerializedName("eventTime")
|
||||
private String eventTime;
|
||||
private ModuleStatus data;
|
||||
|
||||
/**
|
||||
* Returns the identifier of this notification.
|
||||
*
|
||||
* @return a string containing the notification identifier
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the event type of this notification.
|
||||
*
|
||||
* @return a string containing the notification event type
|
||||
*/
|
||||
public String getEventType() {
|
||||
return eventType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subject of this notification.
|
||||
*
|
||||
* @return a string containing the notification subject
|
||||
*/
|
||||
public String getSubject() {
|
||||
return subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the event time of this notification.
|
||||
*
|
||||
* @return a string containing the notification event time
|
||||
*/
|
||||
public String getEventTime() {
|
||||
return eventTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the module status data (i.e. the payload) of this notification.
|
||||
*
|
||||
* @return the module status data, or {@code null} in case of no data found
|
||||
*/
|
||||
public @Nullable ModuleStatus getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the chronothermostat details of this notification.
|
||||
*
|
||||
* @return the chronothermostat details, or {@code null} in case of no data found
|
||||
*/
|
||||
public @Nullable Chronothermostat getChronothermostat() {
|
||||
if (data != null) {
|
||||
return data.toChronothermostat();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sender details of this notification.
|
||||
*
|
||||
* @return the sender details, or {@code null} in case of no data found
|
||||
*/
|
||||
public @Nullable Sender getSender() {
|
||||
if (data != null) {
|
||||
final Chronothermostat chronothermostat = data.toChronothermostat();
|
||||
if (chronothermostat != null) {
|
||||
return chronothermostat.getSender();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("id=%s, eventType=%s, subject=%s, eventTime=%s, data=[%s]", id, eventType, subject,
|
||||
eventTime, data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@code Plant} class defines the dto for Smarther API plant object.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
public class Plant {
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
private List<Module> modules;
|
||||
|
||||
/**
|
||||
* Returns the identifier of the plant.
|
||||
*
|
||||
* @return a string containing the plant identifier
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plant reference label (i.e. the plant "name").
|
||||
*
|
||||
* @return a string containing the plant reference label
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of chronothermostat modules of the plant.
|
||||
*
|
||||
* @return the list of chronothermostat modules of the plant, or {@code null} in case the plant has no modules
|
||||
*/
|
||||
public @Nullable List<Module> getModules() {
|
||||
return modules;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("id=%s, name=%s", id, name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.dto;
|
||||
|
||||
/**
|
||||
* The {@code PlantRef} class defines the dto for Smarther API plant reference object.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
public class PlantRef {
|
||||
|
||||
private String id;
|
||||
private ModuleRef module;
|
||||
|
||||
/**
|
||||
* Returns the identifier of the plant.
|
||||
*
|
||||
* @return a string containing the plant identifier
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the chronothermostat reference inside the plant.
|
||||
*
|
||||
* @return a {@link ModuleRef} object representing the chronothermostat module reference
|
||||
*/
|
||||
public ModuleRef getModule() {
|
||||
return module;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("id=%s, module=[%s]", id, module);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The {@code Plants} class defines the dto for Smarther API list of plants.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
public class Plants {
|
||||
|
||||
private List<Plant> plants;
|
||||
|
||||
/**
|
||||
* Returns the list of plants contained in this object.
|
||||
*
|
||||
* @return the list of plants
|
||||
*/
|
||||
public List<Plant> getPlants() {
|
||||
return plants;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.dto;
|
||||
|
||||
import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.DEFAULT_PROGRAM;
|
||||
|
||||
/**
|
||||
* The {@code Program} class defines the dto for Smarther API program object.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
public class Program {
|
||||
|
||||
private int number;
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Returns the program number.
|
||||
*
|
||||
* @return the program number
|
||||
*/
|
||||
public int getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the program reference label (i.e. the program "name").
|
||||
*
|
||||
* @return a string containing the program reference label
|
||||
*/
|
||||
public String getName() {
|
||||
return (number == 0) ? DEFAULT_PROGRAM : name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("number=%d, name=%s", number, name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@code Sender} class defines the dto for Smarther API sender object.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
public class Sender {
|
||||
|
||||
@SerializedName("addressType")
|
||||
private String addressType;
|
||||
private String system;
|
||||
private PlantRef plant;
|
||||
|
||||
/**
|
||||
* Returns the sender address type.
|
||||
*
|
||||
* @return a string containing the sender address type
|
||||
*/
|
||||
public String getAddressType() {
|
||||
return addressType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sender system.
|
||||
*
|
||||
* @return a string containing the sender system
|
||||
*/
|
||||
public String getSystem() {
|
||||
return system;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sender plant reference.
|
||||
*
|
||||
* @return a {@link PlantRef} object representing the sender plant reference
|
||||
*/
|
||||
public PlantRef getPlant() {
|
||||
return plant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("addressType=%s, system=%s, plant=[%s]", addressType, system, plant);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherIllegalPropertyValueException;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* The {@code Sensor} class defines the dto for Smarther API sensor object.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
public class Sensor {
|
||||
|
||||
private List<Measure> measures;
|
||||
|
||||
/**
|
||||
* Returns the list of measures this sensor takes.
|
||||
*
|
||||
* @return the measures this sensor takes, may be {@code null}
|
||||
*/
|
||||
public @Nullable List<Measure> getMeasures() {
|
||||
return measures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the measure taken by this sensor at the given index.
|
||||
*
|
||||
* @param index
|
||||
* the index to get the measure for
|
||||
*
|
||||
* @return the requested measure, or {@code null} in case of no measure found at given index
|
||||
*/
|
||||
public @Nullable Measure getMeasure(int index) {
|
||||
return (measures != null && measures.size() > index) ? measures.get(index) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the overall state of the sensor.
|
||||
*
|
||||
* @return a {@link State} object representing the overall state of the sensor
|
||||
*
|
||||
* @throws {@link SmartherIllegalPropertyValueException}
|
||||
* if the sensor internal raw state cannot be mapped to any valid value
|
||||
*/
|
||||
public State toState() throws SmartherIllegalPropertyValueException {
|
||||
final Measure measure = getMeasure(0);
|
||||
return (measure != null) ? measure.toState() : UnDefType.UNDEF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("measures=%s", measures);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@code Subscription} class defines the dto for Smarther API notification subscription object.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
public class Subscription {
|
||||
|
||||
@SerializedName("plantId")
|
||||
private String plantId;
|
||||
@SerializedName("subscriptionId")
|
||||
private String subscriptionId;
|
||||
@SerializedName("EndPointUrl")
|
||||
private String endpointUrl;
|
||||
|
||||
/**
|
||||
* Returns the identifier of the plant this subscription relates to.
|
||||
*
|
||||
* @return a string containing the plant identifier
|
||||
*/
|
||||
public String getPlantId() {
|
||||
return plantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the notification subscription identifier.
|
||||
*
|
||||
* @return a string containing the subscription identifier
|
||||
*/
|
||||
public String getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the notification endpoint url this subscription maps to.
|
||||
*
|
||||
* @return a string containing the notification endpoint url
|
||||
*/
|
||||
public String getEndpointUrl() {
|
||||
return endpointUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("plantId=%s, id=%s, endpoint=%s", plantId, subscriptionId, endpointUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@code Topology} class defines the dto for Smarther API topology object.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
public class Topology {
|
||||
|
||||
private Plant plant;
|
||||
|
||||
/**
|
||||
* Returns a {@link Plant} object representing the plant contained in this topology.
|
||||
*
|
||||
* @return the plant contained in this topology, or {@code null} if the topology has no plant
|
||||
*/
|
||||
public @Nullable Plant getPlant() {
|
||||
return plant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of chronothermostat modules contained in this topology.
|
||||
*
|
||||
* @return the list of chronothermostat modules contained in this topology, or an empty list in case the topology
|
||||
* has no modules
|
||||
*/
|
||||
public List<Module> getModules() {
|
||||
return (plant != null) ? plant.getModules() : new ArrayList<>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.exception;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Signals that a generic OAuth2 authorization issue with API gateway has occurred.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmartherAuthorizationException extends SmartherGatewayException {
|
||||
|
||||
private static final long serialVersionUID = 2608406239134276285L;
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherAuthorizationException} with the specified detail message.
|
||||
*
|
||||
* @param message
|
||||
* the error message returned from the API gateway
|
||||
*/
|
||||
public SmartherAuthorizationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherAuthorizationException} with the specified detail message and cause.
|
||||
*
|
||||
* @param message
|
||||
* the error message returned from the API gateway
|
||||
* @param cause
|
||||
* the cause (a null value is permitted, and indicates that the cause is nonexistent or unknown)
|
||||
*/
|
||||
public SmartherAuthorizationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.exception;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Signals that a generic communication issue with API gateway has occurred.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmartherGatewayException extends IOException {
|
||||
|
||||
private static final long serialVersionUID = -3614645621941830547L;
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherGatewayException} with the specified detail message.
|
||||
*
|
||||
* @param message
|
||||
* the error message returned from the API gateway
|
||||
*/
|
||||
public SmartherGatewayException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherGatewayException} with the specified detail message and cause.
|
||||
*
|
||||
* @param message
|
||||
* the error message returned from the API gateway
|
||||
* @param cause
|
||||
* the cause (a null value is permitted, and indicates that the cause is nonexistent or unknown)
|
||||
*/
|
||||
public SmartherGatewayException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherGatewayException} with the specified cause and a detail message of
|
||||
* {@code (cause==null ? null : cause.toString())} (which typically contains the class and detail message of
|
||||
* {@code cause}).
|
||||
* This constructor is useful for API gateway exceptions that are little more than wrappers for other throwables.
|
||||
*
|
||||
* @param cause
|
||||
* the cause (a null value is permitted, and indicates that the cause is nonexistent or unknown)
|
||||
*/
|
||||
public SmartherGatewayException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.exception;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Signals that an "invalid property value" issue has occurred when deailng with enumerated type chronothermostat
|
||||
* properties.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmartherIllegalPropertyValueException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -2549779559688846805L;
|
||||
|
||||
private static final String MSG_FORMAT = "'%s' = '%s'";
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherIllegalPropertyValueException} with the specified detail message.
|
||||
*
|
||||
* @param message
|
||||
* the error message returned from the API gateway
|
||||
*/
|
||||
public SmartherIllegalPropertyValueException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherIllegalPropertyValueException} with the specified property name and invalid value
|
||||
* returned by the API gateway.
|
||||
*
|
||||
* @param propertyName
|
||||
* the property name that caused the issue
|
||||
* @param invalidValue
|
||||
* the invalid value returned by the API gateway for {@code PropertyName}
|
||||
*/
|
||||
public SmartherIllegalPropertyValueException(String propertyName, String invalidValue) {
|
||||
super(String.format(MSG_FORMAT, propertyName, invalidValue));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.exception;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Signals that an "invalid response" messaging issue with API gateway has occurred.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmartherInvalidResponseException extends SmartherGatewayException {
|
||||
|
||||
private static final long serialVersionUID = 3166922285185480855L;
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherInvalidResponseException} with the specified detail message.
|
||||
*
|
||||
* @param message
|
||||
* the error message returned from the API gateway
|
||||
*/
|
||||
public SmartherInvalidResponseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.exception;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Signals that a generic C2C Webhook notification issue with API gateway has occurred.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmartherNotificationException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = -634107708647244174L;
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherNotificationException} with the specified detail message.
|
||||
*
|
||||
* @param message
|
||||
* the error message returned from the API gateway
|
||||
*/
|
||||
public SmartherNotificationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherNotificationException} with the specified detail message and cause.
|
||||
*
|
||||
* @param message
|
||||
* the error message returned from the API gateway
|
||||
* @param cause
|
||||
* the cause (a null value is permitted, and indicates that the cause is nonexistent or unknown)
|
||||
*/
|
||||
public SmartherNotificationException(String message, Throwable exception) {
|
||||
super(message, exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.exception;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Signals that a "subscription for given plant already exists" C2C Webhook issue with API gateway has occurred.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmartherSubscriptionAlreadyExistsException extends SmartherNotificationException {
|
||||
|
||||
private static final long serialVersionUID = 5185321219105493105L;
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherSubscriptionAlreadyExistsException} with the specified detail message.
|
||||
*
|
||||
* @param message
|
||||
* the error message returned from the API gateway
|
||||
*/
|
||||
public SmartherSubscriptionAlreadyExistsException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.api.exception;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Signals that an "access token expired" OAuth2 authorization issue with API gateway has occurred.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmartherTokenExpiredException extends SmartherAuthorizationException {
|
||||
|
||||
private static final long serialVersionUID = 6967072975936269922L;
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherTokenExpiredException} with the specified detail message.
|
||||
*
|
||||
* @param message
|
||||
* the error message returned from the API gateway
|
||||
*/
|
||||
public SmartherTokenExpiredException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The {@code SmartherBridgeConfiguration} class defines the internal configuration of a {@code SmartherBridgeHandler}
|
||||
* instance.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
public class SmartherBridgeConfiguration {
|
||||
|
||||
private String subscriptionKey;
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private boolean useNotifications;
|
||||
private int statusRefreshPeriod;
|
||||
private String notificationUrl;
|
||||
private List<String> notifications;
|
||||
|
||||
/**
|
||||
* Returns the Legrand/Bticino product subscription key.
|
||||
*
|
||||
* @return a string containing the subscription key
|
||||
*/
|
||||
public String getSubscriptionKey() {
|
||||
return subscriptionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Legrand/Bticino product subscription key.
|
||||
*
|
||||
* @param subscriptionKey
|
||||
* the new product subscription key
|
||||
*/
|
||||
public void setSubscriptionKey(String subscriptionKey) {
|
||||
this.subscriptionKey = subscriptionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Legrand/Bticino user account client identifier.
|
||||
*
|
||||
* @return a string containing the client identifier
|
||||
*/
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Legrand/Bticino user account client identifier.
|
||||
*
|
||||
* @param clientId
|
||||
* the new client identifier
|
||||
*/
|
||||
public void setClientId(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Legrand/Bticino user account client secret.
|
||||
*
|
||||
* @return a string containing the client secret
|
||||
*/
|
||||
public String getClientSecret() {
|
||||
return clientSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Legrand/Bticino user account client secret.
|
||||
*
|
||||
* @param clientSecret
|
||||
* the new client secret
|
||||
*/
|
||||
public void setClientSecret(String clientSecret) {
|
||||
this.clientSecret = clientSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the Bridge subscribes to receive modules status notifications.
|
||||
*
|
||||
* @return {@code true} if the notifications are turned on, {@code false} otherwise
|
||||
*/
|
||||
public boolean isUseNotifications() {
|
||||
return useNotifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the Bridge subscribes to receive modules status notifications.
|
||||
*
|
||||
* @param useNotifications
|
||||
* {@code true} if the notifications are turned on, {@code false} otherwise
|
||||
*/
|
||||
public void setUseNotifications(boolean useNotifications) {
|
||||
this.useNotifications = useNotifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Bridge status refresh period (in minutes).
|
||||
*
|
||||
* @return the Bridge status refresh period
|
||||
*/
|
||||
public int getStatusRefreshPeriod() {
|
||||
return statusRefreshPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Bridge status refresh period (in minutes).
|
||||
*
|
||||
* @param statusRefreshPeriod
|
||||
* the new Bridge status refresh period
|
||||
*/
|
||||
public void setStatusRefreshPeriod(int statusRefreshPeriod) {
|
||||
this.statusRefreshPeriod = statusRefreshPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the notification url for this Bridge.
|
||||
*
|
||||
* @return a string containing the notification url
|
||||
*/
|
||||
public String getNotificationUrl() {
|
||||
return notificationUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the notification url for this Bridge.
|
||||
*
|
||||
* @param notificationUrl
|
||||
* the new notification url
|
||||
*/
|
||||
public void setNotificationUrl(String notificationUrl) {
|
||||
this.notificationUrl = notificationUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a notification identifier to the Bridge notifications list.
|
||||
*
|
||||
* @param notificationId
|
||||
* the notification identifier to add
|
||||
*
|
||||
* @return the new Bridge notifications list
|
||||
*/
|
||||
public List<String> addNotification(String notificationId) {
|
||||
if (notifications == null) {
|
||||
notifications = new ArrayList<>();
|
||||
}
|
||||
if (!notifications.contains(notificationId)) {
|
||||
notifications.add(notificationId);
|
||||
}
|
||||
return notifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a notification identifier from the Bridge notifications list.
|
||||
*
|
||||
* @param notificationId
|
||||
* the notification identifier to remove
|
||||
*
|
||||
* @return the new Bridge notifications list
|
||||
*/
|
||||
public List<String> removeNotification(String notificationId) {
|
||||
if (notifications != null) {
|
||||
notifications.remove(notificationId);
|
||||
}
|
||||
return notifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current Bridge notifications list.
|
||||
*
|
||||
* @return the current Bridge notifications list
|
||||
*/
|
||||
public List<String> getNotifications() {
|
||||
return (notifications != null) ? notifications : Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new Bridge notifications list.
|
||||
*
|
||||
* @param notifications
|
||||
* the new notifications list to set
|
||||
*/
|
||||
public void setNotifications(List<String> notifications) {
|
||||
this.notifications = notifications;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.config;
|
||||
|
||||
/**
|
||||
* The {@code SmartherModuleConfiguration} class defines the internal configuration of a {@code SmartherModuleHandler}
|
||||
* instance.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
public class SmartherModuleConfiguration {
|
||||
|
||||
private String plantId;
|
||||
private String moduleId;
|
||||
private boolean settingsAutoupdate;
|
||||
private int programsRefreshPeriod;
|
||||
private int numberOfEndDays;
|
||||
private int statusRefreshPeriod;
|
||||
|
||||
/**
|
||||
* Returns the location plant identifier.
|
||||
*
|
||||
* @return a string containing the plant identifier
|
||||
*/
|
||||
public String getPlantId() {
|
||||
return plantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the location plant identifier.
|
||||
*
|
||||
* @param plantId
|
||||
* the new plant identifier
|
||||
*/
|
||||
public void setPlantId(String plantId) {
|
||||
this.plantId = plantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the chronothermostat module identifier.
|
||||
*
|
||||
* @return a string containing the module identifier
|
||||
*/
|
||||
public String getModuleId() {
|
||||
return moduleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the chronothermostat module identifier.
|
||||
*
|
||||
* @param moduleId
|
||||
* the new module identifier
|
||||
*/
|
||||
public void setModuleId(String moduleId) {
|
||||
this.moduleId = moduleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the Module settings are updated with its status.
|
||||
*
|
||||
* @return {@code true} if the settings are updated whenever the module status is updated, {@code false} if the
|
||||
* settings are updated only upon module initialization
|
||||
*/
|
||||
public boolean isSettingsAutoupdate() {
|
||||
return settingsAutoupdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the Module settings are updated with its status.
|
||||
*
|
||||
* @param settingsAutoupdate
|
||||
* {@code true} if the settings are updated whenever the module status is updated, {@code false} if the
|
||||
* settings are updated only upon module initialization
|
||||
*/
|
||||
public void setSettingsAutoupdate(boolean settingsAutoupdate) {
|
||||
this.settingsAutoupdate = settingsAutoupdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the automatic mode programs refresh period (in hours).
|
||||
*
|
||||
* @return the automatic mode programs refresh period
|
||||
*/
|
||||
public int getProgramsRefreshPeriod() {
|
||||
return programsRefreshPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the automatic mode programs refresh period (in hours).
|
||||
*
|
||||
* @param programsRefreshPeriod
|
||||
* the new automatic mode programs refresh period
|
||||
*/
|
||||
public void setProgramsRefreshPeriod(int programsRefreshPeriod) {
|
||||
this.programsRefreshPeriod = programsRefreshPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of end days to be displayed in manual mode.
|
||||
*
|
||||
* @return the number of end days to be displayed
|
||||
*/
|
||||
public int getNumberOfEndDays() {
|
||||
return numberOfEndDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of end days to be displayed in manual mode.
|
||||
*
|
||||
* @param numberOfEndDays
|
||||
* the new number of end days to be displayed
|
||||
*/
|
||||
public void setNumberOfEndDays(int numberOfEndDays) {
|
||||
this.numberOfEndDays = numberOfEndDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Module status refresh period (in minutes).
|
||||
*
|
||||
* @return the Module status refresh period
|
||||
*/
|
||||
public int getStatusRefreshPeriod() {
|
||||
return statusRefreshPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Module status refresh period (in minutes).
|
||||
*
|
||||
* @param statusRefreshPeriod
|
||||
* the new Module status refresh period
|
||||
*/
|
||||
public void setStatusRefreshPeriod(int statusRefreshPeriod) {
|
||||
this.statusRefreshPeriod = statusRefreshPeriod;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bticinosmarther.internal.account.SmartherAccountHandler;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Location;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Module;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@code SmartherModuleDiscoveryService} queries the Smarther API gateway to discover available Chronothermostat
|
||||
* modules inside existing plants registered under the configured Bridges.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmartherModuleDiscoveryService extends AbstractDiscoveryService
|
||||
implements DiscoveryService, ThingHandlerService {
|
||||
|
||||
// Only modules can be discovered. A bridge must be manually added.
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_MODULE);
|
||||
|
||||
private static final int DISCOVERY_TIME_SECONDS = 30;
|
||||
|
||||
private static final String ID_SEPARATOR = "-";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SmartherModuleDiscoveryService.class);
|
||||
|
||||
private @Nullable SmartherAccountHandler bridgeHandler;
|
||||
private @Nullable ThingUID bridgeUID;
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherModuleDiscoveryService}.
|
||||
*/
|
||||
public SmartherModuleDiscoveryService() {
|
||||
super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_TIME_SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypes() {
|
||||
return SUPPORTED_THING_TYPES_UIDS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activate() {
|
||||
logger.debug("Bridge[{}] Activating chronothermostat discovery service", this.bridgeUID);
|
||||
Map<String, @Nullable Object> properties = new HashMap<>();
|
||||
properties.put(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY, Boolean.TRUE);
|
||||
super.activate(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
logger.debug("Bridge[{}] Deactivating chronothermostat discovery service", this.bridgeUID);
|
||||
removeOlderResults(new Date().getTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThingHandler(@Nullable ThingHandler handler) {
|
||||
if (handler instanceof SmartherAccountHandler) {
|
||||
final SmartherAccountHandler localBridgeHandler = (SmartherAccountHandler) handler;
|
||||
this.bridgeHandler = localBridgeHandler;
|
||||
this.bridgeUID = localBridgeHandler.getUID();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return this.bridgeHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
logger.debug("Bridge[{}] Performing background discovery scan for chronothermostats", this.bridgeUID);
|
||||
discoverChronothermostats();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
logger.debug("Bridge[{}] Starting discovery scan for chronothermostats", this.bridgeUID);
|
||||
discoverChronothermostats();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void abortScan() {
|
||||
super.abortScan();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void stopScan() {
|
||||
super.stopScan();
|
||||
removeOlderResults(getTimestampOfLastScan());
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers Chronothermostat devices for the given bridge handler.
|
||||
*/
|
||||
private synchronized void discoverChronothermostats() {
|
||||
final SmartherAccountHandler localBridgeHandler = this.bridgeHandler;
|
||||
if (localBridgeHandler != null) {
|
||||
// If the bridge is not online no other thing devices can be found, so no reason to scan at this moment
|
||||
if (localBridgeHandler.isOnline()) {
|
||||
localBridgeHandler.getLocations()
|
||||
.forEach(l -> localBridgeHandler.getLocationModules(l).forEach(m -> addDiscoveredDevice(l, m)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Chronothermostat module Thing based on the remotely discovered location and module.
|
||||
*
|
||||
* @param location
|
||||
* the location containing the discovered module
|
||||
* @param module
|
||||
* the discovered module
|
||||
*/
|
||||
private void addDiscoveredDevice(Location location, Module module) {
|
||||
ThingUID localBridgeUID = this.bridgeUID;
|
||||
if (localBridgeUID != null) {
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put(PROPERTY_PLANT_ID, location.getPlantId());
|
||||
properties.put(PROPERTY_MODULE_ID, module.getId());
|
||||
properties.put(PROPERTY_MODULE_NAME, module.getName());
|
||||
properties.put(PROPERTY_DEVICE_TYPE, module.getDeviceType());
|
||||
|
||||
ThingUID thingUID = new ThingUID(THING_TYPE_MODULE, localBridgeUID, getThingIdFromModule(module));
|
||||
|
||||
final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withBridge(localBridgeUID)
|
||||
.withProperties(properties).withRepresentationProperty(PROPERTY_MODULE_ID)
|
||||
.withLabel(module.getName()).build();
|
||||
thingDiscovered(discoveryResult);
|
||||
logger.debug("Bridge[{}] Chronothermostat with id '{}' and name '{}' added to Inbox with UID '{}'",
|
||||
localBridgeUID, module.getId(), module.getName(), thingUID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the Thing identifier based on the Chronothermostat module identifier.
|
||||
*
|
||||
* @param module
|
||||
* the Chronothermostat module to use
|
||||
*
|
||||
* @return a string containing the generated Thing identifier
|
||||
*/
|
||||
private String getThingIdFromModule(Module module) {
|
||||
final String moduleId = module.getId();
|
||||
return moduleId.substring(0, moduleId.indexOf(ID_SEPARATOR));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.factory;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants;
|
||||
import org.openhab.binding.bticinosmarther.internal.account.SmartherAccountService;
|
||||
import org.openhab.binding.bticinosmarther.internal.handler.SmartherBridgeHandler;
|
||||
import org.openhab.binding.bticinosmarther.internal.handler.SmartherDynamicStateDescriptionProvider;
|
||||
import org.openhab.binding.bticinosmarther.internal.handler.SmartherModuleHandler;
|
||||
import org.openhab.core.auth.client.oauth2.OAuthFactory;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.scheduler.CronScheduler;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@code SmartherHandlerFactory} class is responsible for creating things and thing handlers.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.bticinosmarther")
|
||||
@NonNullByDefault
|
||||
public class SmartherHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SmartherHandlerFactory.class);
|
||||
|
||||
private final OAuthFactory oAuthFactory;
|
||||
private final SmartherAccountService authService;
|
||||
private final HttpClient httpClient;
|
||||
private final CronScheduler cronScheduler;
|
||||
private final SmartherDynamicStateDescriptionProvider dynamicStateDescriptionProvider;
|
||||
|
||||
@Activate
|
||||
public SmartherHandlerFactory(@Reference OAuthFactory oAuthFactory, @Reference SmartherAccountService authService,
|
||||
@Reference HttpClientFactory httpClientFactory, @Reference CronScheduler cronScheduler,
|
||||
@Reference SmartherDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
|
||||
this.oAuthFactory = oAuthFactory;
|
||||
this.authService = authService;
|
||||
this.httpClient = httpClientFactory.getCommonHttpClient();
|
||||
this.cronScheduler = cronScheduler;
|
||||
this.dynamicStateDescriptionProvider = dynamicStateDescriptionProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SmartherBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (SmartherBindingConstants.THING_TYPE_BRIDGE.equals(thingTypeUID)) {
|
||||
final SmartherBridgeHandler handler = new SmartherBridgeHandler((Bridge) thing, oAuthFactory, httpClient);
|
||||
this.authService.addSmartherAccountHandler(handler);
|
||||
return handler;
|
||||
} else if (SmartherBindingConstants.THING_TYPE_MODULE.equals(thingTypeUID)) {
|
||||
return new SmartherModuleHandler(thing, cronScheduler, dynamicStateDescriptionProvider);
|
||||
} else {
|
||||
logger.debug("Unsupported thing {}", thing.getThingTypeUID());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void removeHandler(ThingHandler thingHandler) {
|
||||
if (thingHandler instanceof SmartherBridgeHandler) {
|
||||
authService.removeSmartherAccountHandler((SmartherBridgeHandler) thingHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,765 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.handler;
|
||||
|
||||
import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.bticinosmarther.internal.account.SmartherAccountHandler;
|
||||
import org.openhab.binding.bticinosmarther.internal.account.SmartherNotificationHandler;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.SmartherApi;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Location;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Module;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.ModuleStatus;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Notification;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Plant;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Program;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Sender;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Subscription;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherAuthorizationException;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException;
|
||||
import org.openhab.binding.bticinosmarther.internal.config.SmartherBridgeConfiguration;
|
||||
import org.openhab.binding.bticinosmarther.internal.discovery.SmartherModuleDiscoveryService;
|
||||
import org.openhab.binding.bticinosmarther.internal.model.BridgeStatus;
|
||||
import org.openhab.binding.bticinosmarther.internal.model.ModuleSettings;
|
||||
import org.openhab.binding.bticinosmarther.internal.util.StringUtil;
|
||||
import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
|
||||
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
|
||||
import org.openhab.core.auth.client.oauth2.OAuthClientService;
|
||||
import org.openhab.core.auth.client.oauth2.OAuthException;
|
||||
import org.openhab.core.auth.client.oauth2.OAuthFactory;
|
||||
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
|
||||
import org.openhab.core.cache.ExpiringCache;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@code SmartherBridgeHandler} class is responsible of the handling of a Smarther Bridge thing.
|
||||
* The Smarther Bridge is used to manage a set of Smarther Chronothermostat Modules registered under the same
|
||||
* Legrand/Bticino account credentials.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmartherBridgeHandler extends BaseBridgeHandler
|
||||
implements SmartherAccountHandler, SmartherNotificationHandler, AccessTokenRefreshListener {
|
||||
|
||||
private static final long POLL_INITIAL_DELAY = 5;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SmartherBridgeHandler.class);
|
||||
|
||||
private final OAuthFactory oAuthFactory;
|
||||
private final HttpClient httpClient;
|
||||
|
||||
// Bridge configuration
|
||||
private SmartherBridgeConfiguration config;
|
||||
|
||||
// Field members assigned in initialize method
|
||||
private @Nullable Future<?> pollFuture;
|
||||
private @Nullable OAuthClientService oAuthService;
|
||||
private @Nullable SmartherApi smartherApi;
|
||||
private @Nullable ExpiringCache<List<Location>> locationCache;
|
||||
private @Nullable BridgeStatus bridgeStatus;
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherBridgeHandler} for the given Bridge thing, authorization factory and http client.
|
||||
*
|
||||
* @param bridge
|
||||
* the {@link Bridge} thing to be used
|
||||
* @param oAuthFactory
|
||||
* the OAuth2 authorization factory to be used
|
||||
* @param httpClient
|
||||
* the http client to be used
|
||||
*/
|
||||
public SmartherBridgeHandler(Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient) {
|
||||
super(bridge);
|
||||
this.oAuthFactory = oAuthFactory;
|
||||
this.httpClient = httpClient;
|
||||
this.config = new SmartherBridgeConfiguration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Collections.singleton(SmartherModuleDiscoveryService.class);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
//
|
||||
// Bridge thing lifecycle management methods
|
||||
//
|
||||
// ===========================================================================
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Bridge[{}] Initialize handler", thing.getUID());
|
||||
|
||||
this.config = getConfigAs(SmartherBridgeConfiguration.class);
|
||||
if (StringUtil.isBlank(config.getSubscriptionKey())) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"The 'Subscription Key' property is not set or empty. If you have an older thing please recreate it.");
|
||||
return;
|
||||
}
|
||||
if (StringUtil.isBlank(config.getClientId())) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"The 'Client Id' property is not set or empty. If you have an older thing please recreate it.");
|
||||
return;
|
||||
}
|
||||
if (StringUtil.isBlank(config.getClientSecret())) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"The 'Client Secret' property is not set or empty. If you have an older thing please recreate it.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize OAuth2 authentication support
|
||||
final OAuthClientService localOAuthService = oAuthFactory.createOAuthClientService(thing.getUID().getAsString(),
|
||||
SMARTHER_API_TOKEN_URL, SMARTHER_AUTHORIZE_URL, config.getClientId(), config.getClientSecret(),
|
||||
SMARTHER_API_SCOPES, false);
|
||||
localOAuthService.addAccessTokenRefreshListener(SmartherBridgeHandler.this);
|
||||
this.oAuthService = localOAuthService;
|
||||
|
||||
// Initialize Smarther Api
|
||||
final SmartherApi localSmartherApi = new SmartherApi(localOAuthService, config.getSubscriptionKey(), scheduler,
|
||||
httpClient);
|
||||
this.smartherApi = localSmartherApi;
|
||||
|
||||
// Initialize locations (plant Ids) local cache
|
||||
final ExpiringCache<List<Location>> localLocationCache = new ExpiringCache<>(
|
||||
Duration.ofMinutes(config.getStatusRefreshPeriod()), this::locationCacheAction);
|
||||
this.locationCache = localLocationCache;
|
||||
|
||||
// Initialize bridge local status
|
||||
final BridgeStatus localBridgeStatus = new BridgeStatus();
|
||||
this.bridgeStatus = localBridgeStatus;
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
|
||||
schedulePoll();
|
||||
|
||||
logger.debug("Bridge[{}] Finished initializing!", thing.getUID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_CONFIG_FETCH_LOCATIONS:
|
||||
if (command instanceof OnOffType) {
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
logger.debug(
|
||||
"Bridge[{}] Manually triggered channel to remotely fetch the updated client locations list",
|
||||
thing.getUID());
|
||||
expireCache();
|
||||
getLocations();
|
||||
updateChannelState(CHANNEL_CONFIG_FETCH_LOCATIONS, OnOffType.OFF);
|
||||
}
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (command instanceof RefreshType) {
|
||||
// Avoid logging wrong command when refresh command is sent
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Bridge[{}] Received command {} of wrong type {} on channel {}", thing.getUID(), command,
|
||||
command.getClass().getTypeName(), channelUID.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRemoval() {
|
||||
super.handleRemoval();
|
||||
stopPoll(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("Bridge[{}] Dispose handler", thing.getUID());
|
||||
final OAuthClientService localOAuthService = this.oAuthService;
|
||||
if (localOAuthService != null) {
|
||||
localOAuthService.removeAccessTokenRefreshListener(this);
|
||||
}
|
||||
this.oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
|
||||
stopPoll(true);
|
||||
logger.debug("Bridge[{}] Finished disposing!", thing.getUID());
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
//
|
||||
// Bridge data cache management methods
|
||||
//
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Returns the available locations to be cached for this Bridge.
|
||||
*
|
||||
* @return the available locations to be cached for this Bridge, or {@code null} if the list of available locations
|
||||
* cannot be retrieved
|
||||
*/
|
||||
private @Nullable List<Location> locationCacheAction() {
|
||||
try {
|
||||
// Retrieve the plants list from the API Gateway
|
||||
final List<Plant> plants = getPlants();
|
||||
|
||||
List<Location> locations;
|
||||
if (config.isUseNotifications()) {
|
||||
// Retrieve the subscriptions list from the API Gateway
|
||||
final List<Subscription> subscriptions = getSubscriptions();
|
||||
|
||||
// Enrich the notifications list with externally registered subscriptions
|
||||
updateNotifications(subscriptions);
|
||||
|
||||
// Get the notifications list from bridge config
|
||||
final List<String> notifications = config.getNotifications();
|
||||
|
||||
locations = plants.stream().map(p -> Location.fromPlant(p, subscriptions.stream()
|
||||
.filter(s -> s.getPlantId().equals(p.getId()) && notifications.contains(s.getSubscriptionId()))
|
||||
.findFirst())).collect(Collectors.toList());
|
||||
} else {
|
||||
locations = plants.stream().map(p -> Location.fromPlant(p)).collect(Collectors.toList());
|
||||
}
|
||||
logger.debug("Bridge[{}] Available locations: {}", thing.getUID(), locations);
|
||||
|
||||
return locations;
|
||||
|
||||
} catch (SmartherGatewayException e) {
|
||||
logger.warn("Bridge[{}] Cannot retrieve available locations: {}", thing.getUID(), e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates this Bridge local notifications list with externally registered subscriptions.
|
||||
*
|
||||
* @param subscriptions
|
||||
* the externally registered subscriptions to be added to the local notifications list
|
||||
*/
|
||||
private void updateNotifications(List<Subscription> subscriptions) {
|
||||
// Get the notifications list from bridge config
|
||||
List<String> notifications = config.getNotifications();
|
||||
|
||||
for (Subscription s : subscriptions) {
|
||||
if (s.getEndpointUrl().equalsIgnoreCase(config.getNotificationUrl())
|
||||
&& !notifications.contains(s.getSubscriptionId())) {
|
||||
// Add the external subscription to notifications list
|
||||
notifications = config.addNotification(s.getSubscriptionId());
|
||||
|
||||
// Save the updated notifications list back to bridge config
|
||||
Configuration configuration = editConfiguration();
|
||||
configuration.put(PROPERTY_NOTIFICATIONS, notifications);
|
||||
updateConfiguration(configuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all the cache to "expired" for this Bridge.
|
||||
*/
|
||||
private void expireCache() {
|
||||
logger.debug("Bridge[{}] Invalidating location cache", thing.getUID());
|
||||
final ExpiringCache<List<Location>> localLocationCache = this.locationCache;
|
||||
if (localLocationCache != null) {
|
||||
localLocationCache.invalidateValue();
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
//
|
||||
// Bridge status polling mechanism methods
|
||||
//
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Starts a new scheduler to periodically poll and update this Bridge status.
|
||||
*/
|
||||
private void schedulePoll() {
|
||||
stopPoll(false);
|
||||
|
||||
// Schedule poll to start after POLL_INITIAL_DELAY sec and run periodically based on status refresh period
|
||||
final Future<?> localPollFuture = scheduler.scheduleWithFixedDelay(this::poll, POLL_INITIAL_DELAY,
|
||||
config.getStatusRefreshPeriod() * 60, TimeUnit.SECONDS);
|
||||
this.pollFuture = localPollFuture;
|
||||
|
||||
logger.debug("Bridge[{}] Scheduled poll for {} sec out, then every {} min", thing.getUID(), POLL_INITIAL_DELAY,
|
||||
config.getStatusRefreshPeriod());
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all running poll schedulers.
|
||||
*
|
||||
* @param mayInterruptIfRunning
|
||||
* {@code true} if the thread executing this task should be interrupted, {@code false} if the in-progress
|
||||
* tasks are allowed to complete
|
||||
*/
|
||||
private synchronized void stopPoll(boolean mayInterruptIfRunning) {
|
||||
final Future<?> localPollFuture = this.pollFuture;
|
||||
if (localPollFuture != null) {
|
||||
if (!localPollFuture.isCancelled()) {
|
||||
localPollFuture.cancel(mayInterruptIfRunning);
|
||||
}
|
||||
this.pollFuture = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls to update this Bridge status, calling the Smarther API to refresh its plants list.
|
||||
*
|
||||
* @return {@code true} if the method completes without errors, {@code false} otherwise
|
||||
*/
|
||||
private synchronized boolean poll() {
|
||||
try {
|
||||
onAccessTokenResponse(getAccessTokenResponse());
|
||||
|
||||
expireCache();
|
||||
getLocations();
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
return true;
|
||||
} catch (SmartherAuthorizationException e) {
|
||||
logger.warn("Bridge[{}] Authorization error during polling: {}", thing.getUID(), e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
|
||||
} catch (RuntimeException e) {
|
||||
// All other exceptions apart from Authorization and Gateway issues
|
||||
logger.warn("Bridge[{}] Unexpected error during polling, please report if this keeps occurring: ",
|
||||
thing.getUID(), e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
|
||||
}
|
||||
schedulePoll();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccessTokenResponse(@Nullable AccessTokenResponse tokenResponse) {
|
||||
logger.trace("Bridge[{}] Got access token: {}", thing.getUID(),
|
||||
(tokenResponse != null) ? tokenResponse.getAccessToken() : "none");
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
//
|
||||
// Bridge convenience methods
|
||||
//
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Convenience method to get this Bridge configuration.
|
||||
*
|
||||
* @return a {@link SmartherBridgeConfiguration} object containing the Bridge configuration
|
||||
*/
|
||||
public SmartherBridgeConfiguration getSmartherBridgeConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to get the access token from Smarther API authorization layer.
|
||||
*
|
||||
* @return the autorization access token, may be {@code null}
|
||||
*
|
||||
* @throws {@link SmartherAuthorizationException}
|
||||
* in case of authorization issues with the Smarther API
|
||||
*/
|
||||
private @Nullable AccessTokenResponse getAccessTokenResponse() throws SmartherAuthorizationException {
|
||||
try {
|
||||
final OAuthClientService localOAuthService = this.oAuthService;
|
||||
if (localOAuthService != null) {
|
||||
return localOAuthService.getAccessTokenResponse();
|
||||
}
|
||||
return null;
|
||||
} catch (OAuthException | IOException | OAuthResponseException | RuntimeException e) {
|
||||
throw new SmartherAuthorizationException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to update the given Channel state "only" if the Channel is linked.
|
||||
*
|
||||
* @param channelId
|
||||
* the identifier of the Channel to be updated
|
||||
* @param state
|
||||
* the new state to be applied to the given Channel
|
||||
*/
|
||||
private void updateChannelState(String channelId, State state) {
|
||||
final Channel channel = thing.getChannel(channelId);
|
||||
|
||||
if (channel != null && isLinked(channel.getUID())) {
|
||||
updateState(channel.getUID(), state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to update the Smarther API calls counter for this Bridge.
|
||||
*/
|
||||
private void updateApiCallsCounter() {
|
||||
final BridgeStatus localBridgeStatus = this.bridgeStatus;
|
||||
if (localBridgeStatus != null) {
|
||||
updateChannelState(CHANNEL_STATUS_API_CALLS_HANDLED,
|
||||
new DecimalType(localBridgeStatus.incrementApiCallsHandled()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to check and get the Smarther API instance for this Bridge.
|
||||
*
|
||||
* @return the Smarther API instance
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case the Smarther API instance is {@code null}
|
||||
*/
|
||||
private SmartherApi getSmartherApi() throws SmartherGatewayException {
|
||||
final SmartherApi localSmartherApi = this.smartherApi;
|
||||
if (localSmartherApi == null) {
|
||||
throw new SmartherGatewayException("Smarther API instance is null");
|
||||
}
|
||||
return localSmartherApi;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
//
|
||||
// Implementation of the SmartherAccountHandler interface
|
||||
//
|
||||
// ===========================================================================
|
||||
|
||||
@Override
|
||||
public ThingUID getUID() {
|
||||
return thing.getUID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return StringUtil.defaultString(thing.getLabel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Location> getLocations() {
|
||||
final ExpiringCache<List<Location>> localLocationCache = this.locationCache;
|
||||
final List<Location> locations = (localLocationCache != null) ? localLocationCache.getValue() : null;
|
||||
return (locations != null) ? locations : Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLocation(String plantId) {
|
||||
final ExpiringCache<List<Location>> localLocationCache = this.locationCache;
|
||||
final List<Location> locations = (localLocationCache != null) ? localLocationCache.getValue() : null;
|
||||
return (locations != null) ? locations.stream().anyMatch(l -> l.getPlantId().equals(plantId)) : false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Plant> getPlants() throws SmartherGatewayException {
|
||||
updateApiCallsCounter();
|
||||
return getSmartherApi().getPlants();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Subscription> getSubscriptions() throws SmartherGatewayException {
|
||||
updateApiCallsCounter();
|
||||
return getSmartherApi().getSubscriptions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String subscribePlant(String plantId, String notificationUrl) throws SmartherGatewayException {
|
||||
updateApiCallsCounter();
|
||||
return getSmartherApi().subscribePlant(plantId, notificationUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribePlant(String plantId, String subscriptionId) throws SmartherGatewayException {
|
||||
updateApiCallsCounter();
|
||||
getSmartherApi().unsubscribePlant(plantId, subscriptionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Module> getLocationModules(Location location) {
|
||||
try {
|
||||
updateApiCallsCounter();
|
||||
return getSmartherApi().getPlantModules(location.getPlantId());
|
||||
} catch (SmartherGatewayException e) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModuleStatus getModuleStatus(String plantId, String moduleId) throws SmartherGatewayException {
|
||||
updateApiCallsCounter();
|
||||
return getSmartherApi().getModuleStatus(plantId, moduleId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setModuleStatus(ModuleSettings moduleSettings) throws SmartherGatewayException {
|
||||
updateApiCallsCounter();
|
||||
return getSmartherApi().setModuleStatus(moduleSettings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Program> getModulePrograms(String plantId, String moduleId) throws SmartherGatewayException {
|
||||
updateApiCallsCounter();
|
||||
return getSmartherApi().getModulePrograms(plantId, moduleId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAuthorized() {
|
||||
try {
|
||||
final AccessTokenResponse tokenResponse = getAccessTokenResponse();
|
||||
onAccessTokenResponse(tokenResponse);
|
||||
|
||||
return (tokenResponse != null && tokenResponse.getAccessToken() != null
|
||||
&& tokenResponse.getRefreshToken() != null);
|
||||
} catch (SmartherAuthorizationException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnline() {
|
||||
return (thing.getStatus() == ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String authorize(String redirectUrl, String reqCode, String notificationUrl)
|
||||
throws SmartherGatewayException {
|
||||
try {
|
||||
logger.debug("Bridge[{}] Call API gateway to get access token. RedirectUri: {}", thing.getUID(),
|
||||
redirectUrl);
|
||||
|
||||
final OAuthClientService localOAuthService = this.oAuthService;
|
||||
if (localOAuthService == null) {
|
||||
throw new SmartherAuthorizationException("Authorization service is null");
|
||||
}
|
||||
|
||||
// OAuth2 call to get access token from received authorization code
|
||||
localOAuthService.getAccessTokenResponseByAuthorizationCode(reqCode, redirectUrl);
|
||||
|
||||
// Store the notification URL in bridge configuration
|
||||
Configuration configuration = editConfiguration();
|
||||
configuration.put(PROPERTY_NOTIFICATION_URL, notificationUrl);
|
||||
updateConfiguration(configuration);
|
||||
config.setNotificationUrl(notificationUrl);
|
||||
logger.debug("Bridge[{}] Store notification URL: {}", thing.getUID(), notificationUrl);
|
||||
|
||||
// Reschedule the polling thread
|
||||
schedulePoll();
|
||||
|
||||
return config.getClientId();
|
||||
} catch (OAuthResponseException e) {
|
||||
throw new SmartherAuthorizationException(e.toString(), e);
|
||||
} catch (OAuthException | IOException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
|
||||
throw new SmartherGatewayException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equalsThingUID(String thingUID) {
|
||||
return thing.getUID().getAsString().equals(thingUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String formatAuthorizationUrl(String redirectUri) {
|
||||
try {
|
||||
final OAuthClientService localOAuthService = this.oAuthService;
|
||||
if (localOAuthService != null) {
|
||||
return localOAuthService.getAuthorizationUrl(redirectUri, null, thing.getUID().getAsString());
|
||||
}
|
||||
} catch (OAuthException e) {
|
||||
logger.warn("Bridge[{}] Error constructing AuthorizationUrl: {}", thing.getUID(), e.getMessage());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
//
|
||||
// Implementation of the SmartherNotificationHandler interface
|
||||
//
|
||||
// ===========================================================================
|
||||
|
||||
@Override
|
||||
public boolean useNotifications() {
|
||||
return config.isUseNotifications();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void registerNotification(String plantId) throws SmartherGatewayException {
|
||||
if (!config.isUseNotifications()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ExpiringCache<List<Location>> localLocationCache = this.locationCache;
|
||||
if (localLocationCache != null) {
|
||||
List<Location> locations = localLocationCache.getValue();
|
||||
if (locations != null) {
|
||||
final Optional<Location> maybeLocation = locations.stream().filter(l -> l.getPlantId().equals(plantId))
|
||||
.findFirst();
|
||||
if (maybeLocation.isPresent()) {
|
||||
Location location = maybeLocation.get();
|
||||
if (!location.hasSubscription()) {
|
||||
// Validate notification Url (must be non-null and https)
|
||||
final String notificationUrl = config.getNotificationUrl();
|
||||
if (isValidNotificationUrl(notificationUrl)) {
|
||||
// Call gateway to register plant subscription
|
||||
String subscriptionId = subscribePlant(plantId, config.getNotificationUrl());
|
||||
logger.debug("Bridge[{}] Notification registered: [plantId={}, subscriptionId={}]",
|
||||
thing.getUID(), plantId, subscriptionId);
|
||||
|
||||
// Add the new subscription to notifications list
|
||||
List<String> notifications = config.addNotification(subscriptionId);
|
||||
|
||||
// Save the updated notifications list back to bridge config
|
||||
Configuration configuration = editConfiguration();
|
||||
configuration.put(PROPERTY_NOTIFICATIONS, notifications);
|
||||
updateConfiguration(configuration);
|
||||
|
||||
// Update the local locationCache with the added data
|
||||
locations.stream().forEach(l -> {
|
||||
if (l.getPlantId().equals(plantId)) {
|
||||
l.setSubscription(subscriptionId, config.getNotificationUrl());
|
||||
}
|
||||
});
|
||||
localLocationCache.putValue(locations);
|
||||
} else {
|
||||
logger.warn(
|
||||
"Bridge[{}] Invalid notification Url [{}]: must be non-null, public https address",
|
||||
thing.getUID(), notificationUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNotification(Notification notification) {
|
||||
final Sender sender = notification.getSender();
|
||||
if (sender != null) {
|
||||
final BridgeStatus localBridgeStatus = this.bridgeStatus;
|
||||
if (localBridgeStatus != null) {
|
||||
logger.debug("Bridge[{}] Notification received: [id={}]", thing.getUID(), notification.getId());
|
||||
updateChannelState(CHANNEL_STATUS_NOTIFS_RECEIVED,
|
||||
new DecimalType(localBridgeStatus.incrementNotificationsReceived()));
|
||||
|
||||
final String plantId = sender.getPlant().getId();
|
||||
final String moduleId = sender.getPlant().getModule().getId();
|
||||
Optional<SmartherModuleHandler> maybeModuleHandler = getThing().getThings().stream()
|
||||
.map(t -> (SmartherModuleHandler) t.getHandler()).filter(h -> h.isLinkedTo(plantId, moduleId))
|
||||
.findFirst();
|
||||
|
||||
if (config.isUseNotifications() && maybeModuleHandler.isPresent()) {
|
||||
maybeModuleHandler.get().handleNotification(notification);
|
||||
} else {
|
||||
logger.debug("Bridge[{}] Notification rejected: no module handler available", thing.getUID());
|
||||
updateChannelState(CHANNEL_STATUS_NOTIFS_REJECTED,
|
||||
new DecimalType(localBridgeStatus.incrementNotificationsRejected()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unregisterNotification(String plantId) throws SmartherGatewayException {
|
||||
if (!config.isUseNotifications()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ExpiringCache<List<Location>> localLocationCache = this.locationCache;
|
||||
if (localLocationCache != null) {
|
||||
List<Location> locations = localLocationCache.getValue();
|
||||
|
||||
final long remainingModules = getThing().getThings().stream()
|
||||
.map(t -> (SmartherModuleHandler) t.getHandler()).filter(h -> h.getPlantId().equals(plantId))
|
||||
.count();
|
||||
|
||||
if (locations != null && remainingModules == 0) {
|
||||
final Optional<Location> maybeLocation = locations.stream().filter(l -> l.getPlantId().equals(plantId))
|
||||
.findFirst();
|
||||
if (maybeLocation.isPresent()) {
|
||||
Location location = maybeLocation.get();
|
||||
final String subscriptionId = location.getSubscriptionId();
|
||||
if (location.hasSubscription() && (subscriptionId != null)) {
|
||||
// Call gateway to unregister plant subscription
|
||||
unsubscribePlant(plantId, subscriptionId);
|
||||
logger.debug("Bridge[{}] Notification unregistered: [plantId={}, subscriptionId={}]",
|
||||
thing.getUID(), plantId, subscriptionId);
|
||||
|
||||
// Remove the subscription from notifications list
|
||||
List<String> notifications = config.removeNotification(subscriptionId);
|
||||
|
||||
// Save the updated notifications list back to bridge config
|
||||
Configuration configuration = editConfiguration();
|
||||
configuration.put(PROPERTY_NOTIFICATIONS, notifications);
|
||||
updateConfiguration(configuration);
|
||||
|
||||
// Update the local locationCache with the removed data
|
||||
locations.stream().forEach(l -> {
|
||||
if (l.getPlantId().equals(plantId)) {
|
||||
l.unsetSubscription();
|
||||
}
|
||||
});
|
||||
localLocationCache.putValue(locations);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the passed string is a formally valid Notification Url (non-null, public https address).
|
||||
*
|
||||
* @param str
|
||||
* the string to check
|
||||
*
|
||||
* @return {@code true} if the given string is a formally valid Notification Url, {@code false} otherwise
|
||||
*/
|
||||
private boolean isValidNotificationUrl(@Nullable String str) {
|
||||
try {
|
||||
if (str != null) {
|
||||
URI maybeValidNotificationUrl = new URI(str);
|
||||
if (HTTPS_SCHEMA.equals(maybeValidNotificationUrl.getScheme())) {
|
||||
InetAddress address = InetAddress.getByName(maybeValidNotificationUrl.getHost());
|
||||
if (!address.isLoopbackAddress() && !address.isSiteLocalAddress()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (URISyntaxException | UnknownHostException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.handler;
|
||||
|
||||
import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.DTF_DATE;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Program;
|
||||
import org.openhab.binding.bticinosmarther.internal.util.DateUtil;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
|
||||
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
|
||||
import org.openhab.core.types.StateOption;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* Dynamically create the users list of programs and setting dates.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@Component(service = { DynamicStateDescriptionProvider.class, SmartherDynamicStateDescriptionProvider.class })
|
||||
@NonNullByDefault
|
||||
public class SmartherDynamicStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
|
||||
|
||||
private static final String LABEL_FOREVER = "Forever";
|
||||
private static final String LABEL_TODAY = "Today";
|
||||
private static final String LABEL_TOMORROW = "Tomorrow";
|
||||
|
||||
public void setEndDates(ChannelUID channelUID, int maxEndDays) {
|
||||
List<StateOption> endDates = new ArrayList<>();
|
||||
|
||||
endDates.add(new StateOption("", LABEL_FOREVER));
|
||||
|
||||
final LocalDateTime today = LocalDate.now().atStartOfDay();
|
||||
|
||||
endDates.add(new StateOption(DateUtil.format(today, DTF_DATE), LABEL_TODAY));
|
||||
if (maxEndDays > 1) {
|
||||
endDates.add(new StateOption(DateUtil.format(today.plusDays(1), DTF_DATE), LABEL_TOMORROW));
|
||||
for (int i = 2; i < maxEndDays; i++) {
|
||||
final String newDate = DateUtil.format(today.plusDays(i), DTF_DATE);
|
||||
endDates.add(new StateOption(newDate, newDate));
|
||||
}
|
||||
}
|
||||
|
||||
setStateOptions(channelUID, endDates);
|
||||
}
|
||||
|
||||
public void setPrograms(ChannelUID channelUID, @Nullable List<Program> programs) {
|
||||
if (programs != null) {
|
||||
setStateOptions(channelUID,
|
||||
programs.stream()
|
||||
.map(program -> new StateOption(String.valueOf(program.getNumber()), program.getName()))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,734 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.handler;
|
||||
|
||||
import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Chronothermostat;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.BoostTime;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.Mode;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.ModuleStatus;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Notification;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Program;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherIllegalPropertyValueException;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherSubscriptionAlreadyExistsException;
|
||||
import org.openhab.binding.bticinosmarther.internal.config.SmartherModuleConfiguration;
|
||||
import org.openhab.binding.bticinosmarther.internal.model.ModuleSettings;
|
||||
import org.openhab.binding.bticinosmarther.internal.util.StringUtil;
|
||||
import org.openhab.core.cache.ExpiringCache;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.scheduler.CronScheduler;
|
||||
import org.openhab.core.scheduler.ScheduledCompletableFuture;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingStatusInfo;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@code SmartherModuleHandler} class is responsible of a single Smarther Chronothermostat, handling the commands
|
||||
* that are sent to one of its channels.
|
||||
* Each Smarther Chronothermostat communicates with the Smarther API via its assigned {@code SmartherBridgeHandler}.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmartherModuleHandler extends BaseThingHandler {
|
||||
|
||||
private static final String DAILY_MIDNIGHT = "1 0 0 * * ? *";
|
||||
private static final long POLL_INITIAL_DELAY = 5;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SmartherModuleHandler.class);
|
||||
|
||||
private final CronScheduler cronScheduler;
|
||||
private final SmartherDynamicStateDescriptionProvider dynamicStateDescriptionProvider;
|
||||
private final ChannelUID programChannelUID;
|
||||
private final ChannelUID endDateChannelUID;
|
||||
|
||||
// Module configuration
|
||||
private SmartherModuleConfiguration config;
|
||||
|
||||
// Field members assigned in initialize method
|
||||
private @Nullable ScheduledCompletableFuture<Void> jobFuture;
|
||||
private @Nullable Future<?> pollFuture;
|
||||
private @Nullable SmartherBridgeHandler bridgeHandler;
|
||||
private @Nullable ExpiringCache<List<Program>> programCache;
|
||||
private @Nullable ModuleSettings moduleSettings;
|
||||
|
||||
// Chronothermostat local status
|
||||
private @Nullable Chronothermostat chronothermostat;
|
||||
|
||||
/**
|
||||
* Constructs a {@code SmartherModuleHandler} for the given thing, scheduler and dynamic state description provider.
|
||||
*
|
||||
* @param thing
|
||||
* the {@link Thing} thing to be used
|
||||
* @param scheduler
|
||||
* the {@link CronScheduler} periodic job scheduler to be used
|
||||
* @param provider
|
||||
* the {@link SmartherDynamicStateDescriptionProvider} dynamic state description provider to be used
|
||||
*/
|
||||
public SmartherModuleHandler(Thing thing, CronScheduler scheduler,
|
||||
SmartherDynamicStateDescriptionProvider provider) {
|
||||
super(thing);
|
||||
this.cronScheduler = scheduler;
|
||||
this.dynamicStateDescriptionProvider = provider;
|
||||
this.programChannelUID = new ChannelUID(thing.getUID(), CHANNEL_SETTINGS_PROGRAM);
|
||||
this.endDateChannelUID = new ChannelUID(thing.getUID(), CHANNEL_SETTINGS_ENDDATE);
|
||||
this.config = new SmartherModuleConfiguration();
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
//
|
||||
// Chronothermostat thing lifecycle management methods
|
||||
//
|
||||
// ===========================================================================
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Module[{}] Initialize handler", thing.getUID());
|
||||
|
||||
final Bridge localBridge = getBridge();
|
||||
if (localBridge == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
|
||||
return;
|
||||
}
|
||||
|
||||
final SmartherBridgeHandler localBridgeHandler = (SmartherBridgeHandler) localBridge.getHandler();
|
||||
this.bridgeHandler = localBridgeHandler;
|
||||
if (localBridgeHandler == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
|
||||
"Missing configuration from the Smarther Bridge (UID:%s). Fix configuration or report if this problem remains.",
|
||||
localBridge.getBridgeUID()));
|
||||
return;
|
||||
}
|
||||
|
||||
this.config = getConfigAs(SmartherModuleConfiguration.class);
|
||||
if (StringUtil.isBlank(config.getPlantId())) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"The 'Plant Id' property is not set or empty. If you have an older thing please recreate it.");
|
||||
return;
|
||||
}
|
||||
if (StringUtil.isBlank(config.getModuleId())) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"The 'Module Id' property is not set or empty. If you have an older thing please recreate it.");
|
||||
return;
|
||||
}
|
||||
if (config.getProgramsRefreshPeriod() <= 0) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"The 'Programs Refresh Period' must be > 0. If you have an older thing please recreate it.");
|
||||
return;
|
||||
}
|
||||
if (config.getStatusRefreshPeriod() <= 0) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"The 'Module Status Refresh Period' must be > 0. If you have an older thing please recreate it.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize automatic mode programs local cache
|
||||
final ExpiringCache<List<Program>> localProgramCache = new ExpiringCache<>(
|
||||
Duration.ofHours(config.getProgramsRefreshPeriod()), this::programCacheAction);
|
||||
this.programCache = localProgramCache;
|
||||
|
||||
// Initialize module local settings
|
||||
final ModuleSettings localModuleSettings = new ModuleSettings(config.getPlantId(), config.getModuleId());
|
||||
this.moduleSettings = localModuleSettings;
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
|
||||
scheduleJob();
|
||||
schedulePoll();
|
||||
|
||||
logger.debug("Module[{}] Finished initializing!", thing.getUID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
try {
|
||||
handleCommandInternal(channelUID, command);
|
||||
updateModuleStatus();
|
||||
} catch (SmartherIllegalPropertyValueException e) {
|
||||
logger.warn("Module[{}] Received command {} with illegal value {} on channel {}", thing.getUID(), command,
|
||||
e.getMessage(), channelUID.getId());
|
||||
} catch (SmartherGatewayException e) {
|
||||
// catch exceptions and handle it in your binding
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the command sent to a given Channel of this Chronothermostat.
|
||||
*
|
||||
* @param channelUID
|
||||
* the identifier of the Channel
|
||||
* @param command
|
||||
* the command sent to the given Channel
|
||||
*
|
||||
* @throws {@link SmartherIllegalPropertyValueException}
|
||||
* if the command contains an illegal value that cannot be mapped to any valid enum value
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case of communication issues with the Smarther API
|
||||
*/
|
||||
private void handleCommandInternal(ChannelUID channelUID, Command command)
|
||||
throws SmartherIllegalPropertyValueException, SmartherGatewayException {
|
||||
final ModuleSettings localModuleSettings = this.moduleSettings;
|
||||
if (localModuleSettings == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_SETTINGS_MODE:
|
||||
if (command instanceof StringType) {
|
||||
localModuleSettings.setMode(Mode.fromValue(command.toString()));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case CHANNEL_SETTINGS_TEMPERATURE:
|
||||
if (changeTemperature(command, localModuleSettings)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case CHANNEL_SETTINGS_PROGRAM:
|
||||
if (command instanceof DecimalType) {
|
||||
localModuleSettings.setProgram(((DecimalType) command).intValue());
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case CHANNEL_SETTINGS_BOOSTTIME:
|
||||
if (command instanceof DecimalType) {
|
||||
localModuleSettings.setBoostTime(BoostTime.fromValue(((DecimalType) command).intValue()));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case CHANNEL_SETTINGS_ENDDATE:
|
||||
if (command instanceof StringType) {
|
||||
localModuleSettings.setEndDate(command.toString());
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case CHANNEL_SETTINGS_ENDHOUR:
|
||||
if (changeTimeHour(command, localModuleSettings)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case CHANNEL_SETTINGS_ENDMINUTE:
|
||||
if (changeTimeMinute(command, localModuleSettings)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case CHANNEL_SETTINGS_POWER:
|
||||
if (command instanceof OnOffType) {
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
// Apply module settings to the remote module
|
||||
if (getBridgeHandler().setModuleStatus(localModuleSettings)) {
|
||||
// Change applied, update module status
|
||||
logger.debug("Module[{}] New settings applied!", thing.getUID());
|
||||
}
|
||||
updateChannelState(CHANNEL_SETTINGS_POWER, OnOffType.OFF);
|
||||
}
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case CHANNEL_CONFIG_FETCH_PROGRAMS:
|
||||
if (command instanceof OnOffType) {
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
logger.debug(
|
||||
"Module[{}] Manually triggered channel to remotely fetch the updated programs list",
|
||||
thing.getUID());
|
||||
expireCache();
|
||||
refreshProgramsList();
|
||||
updateChannelState(CHANNEL_CONFIG_FETCH_PROGRAMS, OnOffType.OFF);
|
||||
}
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (command instanceof RefreshType) {
|
||||
// Avoid logging wrong command when refresh command is sent
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Module[{}] Received command {} of wrong type {} on channel {}", thing.getUID(), command,
|
||||
command.getClass().getTypeName(), channelUID.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the "temperature" in module settings, based on the received Command.
|
||||
* The new value is checked against the temperature limits allowed by the device.
|
||||
*
|
||||
* @param command
|
||||
* the command received on temperature Channel
|
||||
*
|
||||
* @return {@code true} if the change succeeded, {@code false} otherwise
|
||||
*/
|
||||
private boolean changeTemperature(Command command, final ModuleSettings settings) {
|
||||
if (!(command instanceof QuantityType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QuantityType<?> quantity = (QuantityType<?>) command;
|
||||
QuantityType<?> newMeasure = quantity.toUnit(SIUnits.CELSIUS);
|
||||
|
||||
// Check remote device temperature limits
|
||||
if (newMeasure != null && newMeasure.doubleValue() >= 7.1 && newMeasure.doubleValue() <= 40.0) {
|
||||
// Only tenth degree increments are allowed
|
||||
double newTemperature = Math.round(newMeasure.doubleValue() * 10) / 10.0;
|
||||
|
||||
settings.setSetPointTemperature(QuantityType.valueOf(newTemperature, SIUnits.CELSIUS));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the "end hour" for manual mode in module settings, based on the received Command.
|
||||
* The new value is checked against the 24-hours clock allowed range.
|
||||
*
|
||||
* @param command
|
||||
* the command received on end hour Channel
|
||||
*
|
||||
* @return {@code true} if the change succeeded, {@code false} otherwise
|
||||
*/
|
||||
private boolean changeTimeHour(Command command, final ModuleSettings settings) {
|
||||
if (command instanceof DecimalType) {
|
||||
int endHour = ((DecimalType) command).intValue();
|
||||
if (endHour >= 0 && endHour <= 23) {
|
||||
settings.setEndHour(endHour);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the "end minute" for manual mode in module settings, based on the received Command.
|
||||
* The new value is modified to match a 15 min step increment.
|
||||
*
|
||||
* @param command
|
||||
* the command received on end minute Channel
|
||||
*
|
||||
* @return {@code true} if the change succeeded, {@code false} otherwise
|
||||
*/
|
||||
private boolean changeTimeMinute(Command command, final ModuleSettings settings) {
|
||||
if (command instanceof DecimalType) {
|
||||
int endMinute = ((DecimalType) command).intValue();
|
||||
if (endMinute >= 0 && endMinute <= 59) {
|
||||
// Only 15 min increments are allowed
|
||||
endMinute = Math.round(endMinute / 15) * 15;
|
||||
settings.setEndMinute(endMinute);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the notification dispatched to this Chronothermostat from the reference Smarther Bridge.
|
||||
*
|
||||
* @param notification
|
||||
* the notification to handle
|
||||
*/
|
||||
public void handleNotification(Notification notification) {
|
||||
try {
|
||||
final Chronothermostat notificationChrono = notification.getChronothermostat();
|
||||
if (notificationChrono != null) {
|
||||
this.chronothermostat = notificationChrono;
|
||||
if (config.isSettingsAutoupdate()) {
|
||||
final ModuleSettings localModuleSettings = this.moduleSettings;
|
||||
if (localModuleSettings != null) {
|
||||
localModuleSettings.updateFromChronothermostat(notificationChrono);
|
||||
}
|
||||
}
|
||||
logger.debug("Module[{}] Handle notification: [{}]", thing.getUID(), this.chronothermostat);
|
||||
updateModuleStatus();
|
||||
}
|
||||
} catch (SmartherIllegalPropertyValueException e) {
|
||||
logger.warn("Module[{}] Notification has illegal value: [{}]", thing.getUID(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
|
||||
if (bridgeStatusInfo.getStatus() != ThingStatus.ONLINE) {
|
||||
// Put module offline when the parent bridge goes offline
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Smarther Bridge Offline");
|
||||
logger.debug("Module[{}] Bridge switched {}", thing.getUID(), bridgeStatusInfo.getStatus());
|
||||
} else {
|
||||
// Update the module status when the parent bridge return online
|
||||
logger.debug("Module[{}] Bridge is back ONLINE", thing.getUID());
|
||||
// Restart polling to collect module data
|
||||
schedulePoll();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRemoval() {
|
||||
super.handleRemoval();
|
||||
stopPoll(true);
|
||||
stopJob(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("Module[{}] Dispose handler", thing.getUID());
|
||||
stopPoll(true);
|
||||
stopJob(true);
|
||||
try {
|
||||
getBridgeHandler().unregisterNotification(config.getPlantId());
|
||||
} catch (SmartherGatewayException e) {
|
||||
logger.warn("Module[{}] API Gateway error during disposing: {}", thing.getUID(), e.getMessage());
|
||||
}
|
||||
logger.debug("Module[{}] Finished disposing!", thing.getUID());
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
//
|
||||
// Chronothermostat data cache management methods
|
||||
//
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Returns the available automatic mode programs to be cached for this Chronothermostat.
|
||||
*
|
||||
* @return the available programs to be cached for this Chronothermostat, or {@code null} if the list of available
|
||||
* programs cannot be retrieved
|
||||
*/
|
||||
private @Nullable List<Program> programCacheAction() {
|
||||
try {
|
||||
final List<Program> programs = getBridgeHandler().getModulePrograms(config.getPlantId(),
|
||||
config.getModuleId());
|
||||
logger.debug("Module[{}] Available programs: {}", thing.getUID(), programs);
|
||||
|
||||
return programs;
|
||||
|
||||
} catch (SmartherGatewayException e) {
|
||||
logger.warn("Module[{}] Cannot retrieve available programs: {}", thing.getUID(), e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all the cache to "expired" for this Chronothermostat.
|
||||
*/
|
||||
private void expireCache() {
|
||||
logger.debug("Module[{}] Invalidating program cache", thing.getUID());
|
||||
final ExpiringCache<List<Program>> localProgramCache = this.programCache;
|
||||
if (localProgramCache != null) {
|
||||
localProgramCache.invalidateValue();
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
//
|
||||
// Chronothermostat job scheduler methods
|
||||
//
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Starts a new cron scheduler to execute the internal recurring jobs.
|
||||
*/
|
||||
private synchronized void scheduleJob() {
|
||||
stopJob(false);
|
||||
|
||||
// Schedule daily job to start daily, at midnight
|
||||
final ScheduledCompletableFuture<Void> localJobFuture = cronScheduler.schedule(this::dailyJob, DAILY_MIDNIGHT);
|
||||
this.jobFuture = localJobFuture;
|
||||
|
||||
logger.debug("Module[{}] Scheduled recurring job {} to start at midnight", thing.getUID(),
|
||||
Integer.toHexString(localJobFuture.hashCode()));
|
||||
|
||||
// Execute daily job immediately at startup
|
||||
this.dailyJob();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all running jobs.
|
||||
*
|
||||
* @param mayInterruptIfRunning
|
||||
* {@code true} if the thread executing this task should be interrupted, {@code false} if the in-progress
|
||||
* tasks are allowed to complete
|
||||
*/
|
||||
private synchronized void stopJob(boolean mayInterruptIfRunning) {
|
||||
final ScheduledCompletableFuture<Void> localJobFuture = this.jobFuture;
|
||||
if (localJobFuture != null) {
|
||||
if (!localJobFuture.isCancelled()) {
|
||||
localJobFuture.cancel(mayInterruptIfRunning);
|
||||
}
|
||||
this.jobFuture = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to be executed by the daily job: refresh the end dates list for "manual" mode.
|
||||
*/
|
||||
private void dailyJob() {
|
||||
logger.debug("Module[{}] Daily job, refreshing the end dates list for \"manual\" mode", thing.getUID());
|
||||
// Refresh the end dates list for "manual" mode
|
||||
dynamicStateDescriptionProvider.setEndDates(endDateChannelUID, config.getNumberOfEndDays());
|
||||
// If expired, update EndDate in module settings
|
||||
final ModuleSettings localModuleSettings = this.moduleSettings;
|
||||
if (localModuleSettings != null && localModuleSettings.isEndDateExpired()) {
|
||||
localModuleSettings.refreshEndDate();
|
||||
updateChannelState(CHANNEL_SETTINGS_ENDDATE, new StringType(localModuleSettings.getEndDate()));
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
//
|
||||
// Chronothermostat status polling mechanism methods
|
||||
//
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Starts a new scheduler to periodically poll and update this Chronothermostat status.
|
||||
*/
|
||||
private void schedulePoll() {
|
||||
stopPoll(false);
|
||||
|
||||
// Schedule poll to start after POLL_INITIAL_DELAY sec and run periodically based on status refresh period
|
||||
final Future<?> localPollFuture = scheduler.scheduleWithFixedDelay(this::poll, POLL_INITIAL_DELAY,
|
||||
config.getStatusRefreshPeriod() * 60, TimeUnit.SECONDS);
|
||||
this.pollFuture = localPollFuture;
|
||||
|
||||
logger.debug("Module[{}] Scheduled poll for {} sec out, then every {} min", thing.getUID(), POLL_INITIAL_DELAY,
|
||||
config.getStatusRefreshPeriod());
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all running poll schedulers.
|
||||
*
|
||||
* @param mayInterruptIfRunning
|
||||
* {@code true} if the thread executing this task should be interrupted, {@code false} if the in-progress
|
||||
* tasks are allowed to complete
|
||||
*/
|
||||
private synchronized void stopPoll(boolean mayInterruptIfRunning) {
|
||||
final Future<?> localPollFuture = this.pollFuture;
|
||||
if (localPollFuture != null) {
|
||||
if (!localPollFuture.isCancelled()) {
|
||||
localPollFuture.cancel(mayInterruptIfRunning);
|
||||
}
|
||||
this.pollFuture = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls to update this Chronothermostat status.
|
||||
*
|
||||
* @return {@code true} if the method completes without errors, {@code false} otherwise
|
||||
*/
|
||||
private synchronized boolean poll() {
|
||||
try {
|
||||
final Bridge bridge = getBridge();
|
||||
if (bridge != null) {
|
||||
final ThingStatusInfo bridgeStatusInfo = bridge.getStatusInfo();
|
||||
if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
|
||||
ModuleStatus moduleStatus = getBridgeHandler().getModuleStatus(config.getPlantId(),
|
||||
config.getModuleId());
|
||||
|
||||
final Chronothermostat statusChrono = moduleStatus.toChronothermostat();
|
||||
if (statusChrono != null) {
|
||||
if ((this.chronothermostat == null) || config.isSettingsAutoupdate()) {
|
||||
final ModuleSettings localModuleSettings = this.moduleSettings;
|
||||
if (localModuleSettings != null) {
|
||||
localModuleSettings.updateFromChronothermostat(statusChrono);
|
||||
}
|
||||
}
|
||||
this.chronothermostat = statusChrono;
|
||||
logger.debug("Module[{}] Status: [{}]", thing.getUID(), this.chronothermostat);
|
||||
} else {
|
||||
throw new SmartherGatewayException("No chronothermostat data found");
|
||||
}
|
||||
|
||||
// Refresh the programs list for "automatic" mode
|
||||
refreshProgramsList();
|
||||
|
||||
updateModuleStatus();
|
||||
|
||||
getBridgeHandler().registerNotification(config.getPlantId());
|
||||
|
||||
// Everything is ok > set the Thing state to Online
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
return true;
|
||||
} else if (thing.getStatus() != ThingStatus.OFFLINE) {
|
||||
logger.debug("Module[{}] Switched {} as Bridge is not online", thing.getUID(),
|
||||
bridgeStatusInfo.getStatus());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Smarther Bridge Offline");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (SmartherIllegalPropertyValueException e) {
|
||||
logger.debug("Module[{}] Illegal property value error during polling: {}", thing.getUID(), e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
|
||||
} catch (SmartherSubscriptionAlreadyExistsException e) {
|
||||
logger.debug("Module[{}] Subscription error during polling: {}", thing.getUID(), e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
|
||||
} catch (SmartherGatewayException e) {
|
||||
logger.warn("Module[{}] API Gateway error during polling: {}", thing.getUID(), e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
} catch (RuntimeException e) {
|
||||
// All other exceptions apart from Subscription and Gateway issues
|
||||
logger.warn("Module[{}] Unexpected error during polling, please report if this keeps occurring: ",
|
||||
thing.getUID(), e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
|
||||
}
|
||||
schedulePoll();
|
||||
return false;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
//
|
||||
// Chronothermostat convenience methods
|
||||
//
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Convenience method to check and get the Smarther Bridge handler instance for this Module.
|
||||
*
|
||||
* @return the Smarther Bridge handler instance
|
||||
*
|
||||
* @throws {@link SmartherGatewayException}
|
||||
* in case the Smarther Bridge handler instance is {@code null}
|
||||
*/
|
||||
private SmartherBridgeHandler getBridgeHandler() throws SmartherGatewayException {
|
||||
final SmartherBridgeHandler localBridgeHandler = this.bridgeHandler;
|
||||
if (localBridgeHandler == null) {
|
||||
throw new SmartherGatewayException("Smarther Bridge handler instance is null");
|
||||
}
|
||||
return localBridgeHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this Chronothermostat plant identifier
|
||||
*
|
||||
* @return a string containing the plant identifier
|
||||
*/
|
||||
public String getPlantId() {
|
||||
return config.getPlantId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this Chronothermostat module identifier
|
||||
*
|
||||
* @return a string containing the module identifier
|
||||
*/
|
||||
public String getModuleId() {
|
||||
return config.getModuleId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this Chronothermostat matches with the given plant and module identifiers.
|
||||
*
|
||||
* @param plantId
|
||||
* the plant identifier to match to
|
||||
* @param moduleId
|
||||
* the module identifier to match to
|
||||
*
|
||||
* @return {@code true} if the Chronothermostat matches the given plant and module identifiers, {@code false}
|
||||
* otherwise
|
||||
*/
|
||||
public boolean isLinkedTo(String plantId, String moduleId) {
|
||||
return (config.getPlantId().equals(plantId) && config.getModuleId().equals(moduleId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to refresh the module programs list from cache.
|
||||
*/
|
||||
private void refreshProgramsList() {
|
||||
final ExpiringCache<List<Program>> localProgramCache = this.programCache;
|
||||
if (localProgramCache != null) {
|
||||
final List<Program> programs = localProgramCache.getValue();
|
||||
if (programs != null) {
|
||||
dynamicStateDescriptionProvider.setPrograms(programChannelUID, programs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to update the given Channel state "only" if the Channel is linked.
|
||||
*
|
||||
* @param channelId
|
||||
* the identifier of the Channel to be updated
|
||||
* @param state
|
||||
* the new state to be applied to the given Channel
|
||||
*/
|
||||
private void updateChannelState(String channelId, State state) {
|
||||
final Channel channel = thing.getChannel(channelId);
|
||||
|
||||
if (channel != null && isLinked(channel.getUID())) {
|
||||
updateState(channel.getUID(), state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to update the whole status of the Chronothermostat associated to this handler.
|
||||
* Channels are updated based on the local {@code chronothermostat} and {@code moduleSettings} objects.
|
||||
*
|
||||
* @throws {@link SmartherIllegalPropertyValueException}
|
||||
* if at least one of the module properties cannot be mapped to any valid enum value
|
||||
*/
|
||||
private void updateModuleStatus() throws SmartherIllegalPropertyValueException {
|
||||
final Chronothermostat localChrono = this.chronothermostat;
|
||||
if (localChrono != null) {
|
||||
// Update the Measures channels
|
||||
updateChannelState(CHANNEL_MEASURES_TEMPERATURE, localChrono.getThermometer().toState());
|
||||
updateChannelState(CHANNEL_MEASURES_HUMIDITY, localChrono.getHygrometer().toState());
|
||||
// Update the Status channels
|
||||
updateChannelState(CHANNEL_STATUS_STATE, (localChrono.isActive() ? OnOffType.ON : OnOffType.OFF));
|
||||
updateChannelState(CHANNEL_STATUS_FUNCTION,
|
||||
new StringType(StringUtil.capitalize(localChrono.getFunction().toLowerCase())));
|
||||
updateChannelState(CHANNEL_STATUS_MODE,
|
||||
new StringType(StringUtil.capitalize(localChrono.getMode().toLowerCase())));
|
||||
updateChannelState(CHANNEL_STATUS_TEMPERATURE, localChrono.getSetPointTemperature().toState());
|
||||
updateChannelState(CHANNEL_STATUS_ENDTIME, new StringType(localChrono.getActivationTimeLabel()));
|
||||
updateChannelState(CHANNEL_STATUS_TEMP_FORMAT, new StringType(localChrono.getTemperatureFormat()));
|
||||
final Program localProgram = localChrono.getProgram();
|
||||
if (localProgram != null) {
|
||||
updateChannelState(CHANNEL_STATUS_PROGRAM, new StringType(String.valueOf(localProgram.getNumber())));
|
||||
}
|
||||
}
|
||||
|
||||
final ModuleSettings localSettings = this.moduleSettings;
|
||||
if (localSettings != null) {
|
||||
// Update the Settings channels
|
||||
updateChannelState(CHANNEL_SETTINGS_MODE, new StringType(localSettings.getMode().getValue()));
|
||||
updateChannelState(CHANNEL_SETTINGS_TEMPERATURE, localSettings.getSetPointTemperature());
|
||||
updateChannelState(CHANNEL_SETTINGS_PROGRAM, new DecimalType(localSettings.getProgram()));
|
||||
updateChannelState(CHANNEL_SETTINGS_BOOSTTIME, new DecimalType(localSettings.getBoostTime().getValue()));
|
||||
updateChannelState(CHANNEL_SETTINGS_ENDDATE, new StringType(localSettings.getEndDate()));
|
||||
updateChannelState(CHANNEL_SETTINGS_ENDHOUR, new DecimalType(localSettings.getEndHour()));
|
||||
updateChannelState(CHANNEL_SETTINGS_ENDMINUTE, new DecimalType(localSettings.getEndMinute()));
|
||||
updateChannelState(CHANNEL_SETTINGS_POWER, OnOffType.OFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.model;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@code BridgeStatus} class defines the internal status of a Smarther Bridge.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class BridgeStatus {
|
||||
|
||||
private long apiCallsHandled;
|
||||
private long notificationsReceived;
|
||||
private long notificationsRejected;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code BridgeStatus}.
|
||||
*/
|
||||
public BridgeStatus() {
|
||||
this.apiCallsHandled = 0;
|
||||
this.notificationsReceived = 0;
|
||||
this.notificationsRejected = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of API gateway calls made by the bridge.
|
||||
*
|
||||
* @return the total number of API calls made.
|
||||
*/
|
||||
public long getApiCallsHandled() {
|
||||
return apiCallsHandled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the total number of API gateway calls made by the bridge.
|
||||
*
|
||||
* @return the total number of API calls made, after the increment.
|
||||
*/
|
||||
public long incrementApiCallsHandled() {
|
||||
return ++apiCallsHandled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the total number of API gateway calls made by the bridge.
|
||||
*
|
||||
* @param totalNumber
|
||||
* the total number of API calls to be set as made
|
||||
*/
|
||||
public void setApiCallsHandled(long totalNumber) {
|
||||
this.apiCallsHandled = totalNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of module status notifications received by the bridge.
|
||||
*
|
||||
* @return the total number of received notifications.
|
||||
*/
|
||||
public long getNotificationsReceived() {
|
||||
return notificationsReceived;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the total number of module status notifications received by the bridge.
|
||||
*
|
||||
* @return the total number of received notification, after the increment.
|
||||
*/
|
||||
public long incrementNotificationsReceived() {
|
||||
return ++notificationsReceived;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the total number of module status notifications received by the bridge.
|
||||
*
|
||||
* @param totalNumber
|
||||
* the total number of notifications to be set as received
|
||||
*/
|
||||
public void setNotificationsReceived(long totalNumber) {
|
||||
this.notificationsReceived = totalNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of module status notifications rejected by the bridge.
|
||||
*
|
||||
* @return the total number of rejected notifications.
|
||||
*/
|
||||
public long getNotificationsRejected() {
|
||||
return notificationsRejected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the total number of module status notifications rejected by the bridge.
|
||||
*
|
||||
* @return the total number of rejected notification, after the increment.
|
||||
*/
|
||||
public long incrementNotificationsRejected() {
|
||||
return ++notificationsRejected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the total number of module status notifications rejected by the bridge.
|
||||
*
|
||||
* @param totalNumber
|
||||
* the total number of notifications to be set as rejected
|
||||
*/
|
||||
public void setNotificationsRejected(long totalNumber) {
|
||||
this.notificationsRejected = totalNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("apiCallsHandled=%s, notifsReceived=%s, notifsRejected=%s", apiCallsHandled,
|
||||
notificationsReceived, notificationsRejected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.model;
|
||||
|
||||
import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import javax.measure.Unit;
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Chronothermostat;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.BoostTime;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.Function;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.dto.Enums.Mode;
|
||||
import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherIllegalPropertyValueException;
|
||||
import org.openhab.binding.bticinosmarther.internal.util.DateUtil;
|
||||
import org.openhab.binding.bticinosmarther.internal.util.StringUtil;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
|
||||
/**
|
||||
* The {@code ModuleSettings} class defines the operational settings of a Smarther Chronothermostat.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ModuleSettings {
|
||||
|
||||
private transient String plantId;
|
||||
private transient String moduleId;
|
||||
private Function function;
|
||||
private Mode mode;
|
||||
private QuantityType<Temperature> setPointTemperature;
|
||||
private int program;
|
||||
private BoostTime boostTime;
|
||||
private @Nullable String endDate;
|
||||
private int endHour;
|
||||
private int endMinute;
|
||||
|
||||
/**
|
||||
* Constructs a {@code ModuleSettings} with the specified plant and module identifiers.
|
||||
*
|
||||
* @param plantId
|
||||
* the identifier of the plant
|
||||
* @param moduleId
|
||||
* the identifier of the chronothermostat module inside the plant
|
||||
*/
|
||||
public ModuleSettings(String plantId, String moduleId) {
|
||||
this.plantId = plantId;
|
||||
this.moduleId = moduleId;
|
||||
this.function = Function.HEATING;
|
||||
this.mode = Mode.AUTOMATIC;
|
||||
this.setPointTemperature = QuantityType.valueOf(7.0, SIUnits.CELSIUS);
|
||||
this.program = 0;
|
||||
this.boostTime = BoostTime.MINUTES_30;
|
||||
this.endDate = null;
|
||||
this.endHour = 0;
|
||||
this.endMinute = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates this module settings from a {@link Chronothermostat} dto object.
|
||||
*
|
||||
* @param chronothermostat
|
||||
* the chronothermostat dto to get data from
|
||||
*
|
||||
* @throws {@link SmartherIllegalPropertyValueException}
|
||||
* if at least one of the module properties cannot be mapped to any valid enum value
|
||||
*/
|
||||
public void updateFromChronothermostat(Chronothermostat chronothermostat)
|
||||
throws SmartherIllegalPropertyValueException {
|
||||
this.function = Function.fromValue(chronothermostat.getFunction());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plant identifier.
|
||||
*
|
||||
* @return a string containing the plant identifier.
|
||||
*/
|
||||
public String getPlantId() {
|
||||
return plantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the module identifier.
|
||||
*
|
||||
* @return a string containing the module identifier.
|
||||
*/
|
||||
public String getModuleId() {
|
||||
return moduleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the module operational function.
|
||||
*
|
||||
* @return a {@link Function} enum representing the module operational function
|
||||
*/
|
||||
public Function getFunction() {
|
||||
return function;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the module operational mode.
|
||||
*
|
||||
* @return a {@link Mode} enum representing the module operational mode
|
||||
*/
|
||||
public Mode getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the module operational mode.
|
||||
*
|
||||
* @param mode
|
||||
* a {@link Mode} enum representing the module operational mode to set
|
||||
*/
|
||||
public void setMode(Mode mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the module operational setpoint temperature for "manual" mode.
|
||||
*
|
||||
* @return a {@link QuantityType<Temperature>} object representing the module operational setpoint temperature
|
||||
*/
|
||||
public QuantityType<Temperature> getSetPointTemperature() {
|
||||
return setPointTemperature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the module operational setpoint temperature for "manual" mode, using a target unit.
|
||||
*
|
||||
* @param targetUnit
|
||||
* the {@link Unit} unit to convert the setpoint temperature to
|
||||
*
|
||||
* @return a {@link QuantityType<Temperature>} object representing the module operational setpoint temperature
|
||||
*/
|
||||
public @Nullable QuantityType<Temperature> getSetPointTemperature(Unit<?> targetUnit) {
|
||||
return setPointTemperature.toUnit(targetUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the module operational setpoint temperature for "manual" mode.
|
||||
*
|
||||
* @param setPointTemperature
|
||||
* a {@link QuantityType<Temperature>} object representing the setpoint temperature to set
|
||||
*/
|
||||
public void setSetPointTemperature(QuantityType<Temperature> setPointTemperature) {
|
||||
this.setPointTemperature = setPointTemperature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the module operational program for "automatic" mode.
|
||||
*
|
||||
* @return the module operational program for automatic mode
|
||||
*/
|
||||
public int getProgram() {
|
||||
return program;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the module operational program for "automatic" mode.
|
||||
*
|
||||
* @param program
|
||||
* the module operational program to set
|
||||
*/
|
||||
public void setProgram(int program) {
|
||||
this.program = program;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the module operational boost time for "boost" mode.
|
||||
*
|
||||
* @return a {@link BoostTime} enum representing the module operational boost time
|
||||
*/
|
||||
public BoostTime getBoostTime() {
|
||||
return boostTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the module operational boost time for "boost" mode.
|
||||
*
|
||||
* @param boostTime
|
||||
* a {@link BoostTime} enum representing the module operational boost time to set
|
||||
*/
|
||||
public void setBoostTime(BoostTime boostTime) {
|
||||
this.boostTime = boostTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the module operational end date for "manual" mode.
|
||||
*
|
||||
* @return a string containing the module operational end date, may be {@code null}
|
||||
*/
|
||||
public @Nullable String getEndDate() {
|
||||
return endDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the module operational end date for "manual" mode has expired.
|
||||
*
|
||||
* @return {@code true} if the end date has expired, {@code false} otherwise
|
||||
*/
|
||||
public boolean isEndDateExpired() {
|
||||
if (endDate != null) {
|
||||
final LocalDateTime dtEndDate = DateUtil.parseDate(endDate, DTF_DATE).atStartOfDay();
|
||||
final LocalDateTime dtToday = LocalDate.now().atStartOfDay();
|
||||
|
||||
return (dtEndDate.isBefore(dtToday));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the module operational end date for "manual" mode, setting it to current local date.
|
||||
*/
|
||||
public void refreshEndDate() {
|
||||
if (endDate != null) {
|
||||
this.endDate = DateUtil.format(LocalDateTime.now(), DTF_DATE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the module operational end date for "manual" mode.
|
||||
*
|
||||
* @param endDate
|
||||
* the module operational end date to set
|
||||
*/
|
||||
public void setEndDate(String endDate) {
|
||||
this.endDate = StringUtil.stripToNull(endDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the module operational end hour for "manual" mode.
|
||||
*
|
||||
* @return the module operational end hour
|
||||
*/
|
||||
public int getEndHour() {
|
||||
return endHour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the module operational end hour for "manual" mode.
|
||||
*
|
||||
* @param endHour
|
||||
* the module operational end hour to set
|
||||
*/
|
||||
public void setEndHour(int endHour) {
|
||||
this.endHour = endHour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the module operational end minute for "manual" mode.
|
||||
*
|
||||
* @return the module operational end minute
|
||||
*/
|
||||
public int getEndMinute() {
|
||||
return endMinute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the module operational end minute for "manual" mode.
|
||||
*
|
||||
* @param endMinute
|
||||
* the module operational end minute to set
|
||||
*/
|
||||
public void setEndMinute(int endMinute) {
|
||||
this.endMinute = endMinute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the date and time (format YYYY-MM-DDThh:mm:ss) to which this module settings will be maintained.
|
||||
* For boost mode a range is returned, as duration is limited to 30, 60 or 90 minutes, indicating starting (current)
|
||||
* and final date and time.
|
||||
*
|
||||
* @return a string containing the module settings activation time, or and empty ("") string if the module operation
|
||||
* mode doesn't allow for an activation time
|
||||
*/
|
||||
public String getActivationTime() {
|
||||
if (mode.equals(Mode.MANUAL) && (endDate != null)) {
|
||||
LocalDateTime d = DateUtil.parseDate(endDate, DTF_DATE).atTime(endHour, endMinute);
|
||||
return DateUtil.format(d, DTF_DATETIME);
|
||||
} else if (mode.equals(Mode.BOOST)) {
|
||||
LocalDateTime d1 = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES);
|
||||
LocalDateTime d2 = d1.plusMinutes(boostTime.getValue());
|
||||
return DateUtil.formatRange(d1, d2, DTF_DATETIME);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"plantId=%s, moduleId=%s, mode=%s, setPointTemperature=%s, program=%s, boostTime=%s, endDate=%s, endHour=%s, endMinute=%s",
|
||||
plantId, moduleId, mode, setPointTemperature, program, boostTime, endDate, endHour, endMinute);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.util;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@code DateUtil} class defines common date utility functions used across the whole binding.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public final class DateUtil {
|
||||
|
||||
private static final String RANGE_FORMAT = "%s/%s";
|
||||
|
||||
/**
|
||||
* Parses a local date contained in the given string, using the given pattern.
|
||||
*
|
||||
* @param str
|
||||
* the string to be parsed (can be {@code null})
|
||||
* @param pattern
|
||||
* the pattern to be used to parse the given string
|
||||
*
|
||||
* @return a {@link LocalDate} object containing the parsed date
|
||||
*
|
||||
* @throws {@link DateTimeParseException}
|
||||
* if the string cannot be parsed to a local date
|
||||
*/
|
||||
public static LocalDate parseDate(@Nullable String str, String pattern) {
|
||||
DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern);
|
||||
return LocalDate.parse(str, dtf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a local date and time contained in the given string, using the given pattern.
|
||||
*
|
||||
* @param str
|
||||
* the string to be parsed (can be {@code null})
|
||||
* @param pattern
|
||||
* the pattern to be used to parse the given string
|
||||
*
|
||||
* @return a {@link LocalDateTime} object containing the parsed date and time
|
||||
*
|
||||
* @throws {@link DateTimeParseException}
|
||||
* if the string cannot be parsed to a local date and time
|
||||
*/
|
||||
public static LocalDateTime parseLocalTime(@Nullable String str, String pattern) {
|
||||
DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern);
|
||||
return LocalDateTime.parse(str, dtf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a date and time with timezone contained in the given string, using the given pattern.
|
||||
*
|
||||
* @param str
|
||||
* the string to be parsed (can be {@code null})
|
||||
* @param pattern
|
||||
* the pattern to be used to parse the given string
|
||||
*
|
||||
* @return a {@link ZonedDateTime} object containing the parsed date and time with timezone
|
||||
*
|
||||
* @throws {@link DateTimeParseException}
|
||||
* if the string cannot be parsed to a date and time with timezone
|
||||
*/
|
||||
public static ZonedDateTime parseZonedTime(@Nullable String str, String pattern) {
|
||||
DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern);
|
||||
return ZonedDateTime.parse(str, dtf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a date at given days after today and at start of day in the given timezone.
|
||||
*
|
||||
* @param days
|
||||
* the number of days to be added ({@code 0} means today)
|
||||
* @param zoneId
|
||||
* the identifier of the timezone to be applied
|
||||
*
|
||||
* @return a {@link ZonedDateTime} object containing the date and time with timezone
|
||||
*/
|
||||
public static ZonedDateTime getZonedStartOfDay(int days, ZoneId zoneId) {
|
||||
return LocalDate.now().plusDays(days).atStartOfDay(zoneId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representing the given local date and time object, using the given format pattern.
|
||||
*
|
||||
* @param date
|
||||
* the local date and time object to be formatted
|
||||
* @param pattern
|
||||
* the format pattern to be applied
|
||||
*
|
||||
* @return a string representing the local date and time object
|
||||
*
|
||||
* @throws {@link DateTimeException}
|
||||
* if an error occurs during printing
|
||||
*/
|
||||
public static String format(LocalDateTime date, String pattern) {
|
||||
DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern);
|
||||
return date.format(dtf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representing the given date and time with timezone object, using the given format pattern.
|
||||
*
|
||||
* @param date
|
||||
* the date and time with timezone object to be formatted
|
||||
* @param pattern
|
||||
* the format pattern to be applied
|
||||
*
|
||||
* @return a string representing the date and time with timezone object
|
||||
*
|
||||
* @throws {@link DateTimeException}
|
||||
* if an error occurs during printing
|
||||
*/
|
||||
public static String format(ZonedDateTime date, String pattern) {
|
||||
DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern);
|
||||
return date.format(dtf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representing the range between two local date and time objects, using the given format pattern.
|
||||
* The range itself is returned as {@code <date1>/<date2>}.
|
||||
*
|
||||
* @param date1
|
||||
* the first local date and time object in range
|
||||
* @param date2
|
||||
* the second local date and time object in range
|
||||
* @param pattern
|
||||
* the format pattern to be applied
|
||||
*
|
||||
* @return a string representing the range between the two local date and time objects
|
||||
*
|
||||
* @throws {@link DateTimeException}
|
||||
* if an error occurs during printing
|
||||
*/
|
||||
public static String formatRange(LocalDateTime date1, LocalDateTime date2, String pattern) {
|
||||
DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern);
|
||||
return String.format(RANGE_FORMAT, date1.format(dtf), date2.format(dtf));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representing the range between two date and time with timezone objects, using the given format
|
||||
* pattern.
|
||||
* The range itself is returned as {@code <date1>/<date2>}.
|
||||
*
|
||||
* @param date1
|
||||
* the first date and time with timezone object in range
|
||||
* @param date2
|
||||
* the second date and time with timezone object in range
|
||||
* @param pattern
|
||||
* the format pattern to be applied
|
||||
*
|
||||
* @return a string representing the range between the two date and time with timezone objects
|
||||
*
|
||||
* @throws {@link DateTimeException}
|
||||
* if an error occurs during printing
|
||||
*/
|
||||
public static String formatRange(ZonedDateTime date1, ZonedDateTime date2, String pattern) {
|
||||
DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern);
|
||||
return String.format(RANGE_FORMAT, date1.format(dtf), date2.format(dtf));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.util;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.FieldNamingPolicy;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
/**
|
||||
* The {@code ModelUtil} utility class to get the {@code Gson} instance to parse the Smarther API data with.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public final class ModelUtil {
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
|
||||
|
||||
private ModelUtil() {
|
||||
// Util class
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code Gson} instance to parse the Smarther API data with.
|
||||
*
|
||||
* @return the {@code Gson} instance
|
||||
*/
|
||||
public static Gson gsonInstance() {
|
||||
return GSON;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.bticinosmarther.internal.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@code StringUtil} class defines common string utility functions used across the whole binding.
|
||||
*
|
||||
* @author Fabio Possieri - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public final class StringUtil {
|
||||
|
||||
private static final int EOF = -1;
|
||||
private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
|
||||
|
||||
/**
|
||||
* Checks if a string is whitespace, empty ("") or {@code null}.
|
||||
*
|
||||
* @param str
|
||||
* the string to check, may be {@code null}
|
||||
*
|
||||
* @return {@code true} if the string is {@code null}, empty or whitespace
|
||||
*/
|
||||
public static boolean isBlank(@Nullable String str) {
|
||||
return (str == null || str.trim().isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns either the passed in string or, if the string is {@code null}, an empty string ("").
|
||||
*
|
||||
* @param str
|
||||
* the string to check, may be {@code null}
|
||||
*
|
||||
* @return the passed in string, or the empty string if it was {@code null}
|
||||
*
|
||||
*/
|
||||
public static final String defaultString(@Nullable String str) {
|
||||
return (str == null) ? "" : str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns either the passed in string or, if the string is whitespace, empty ("") or {@code null}, a default value.
|
||||
*
|
||||
* @param str
|
||||
* the string to check, may be {@code null}
|
||||
* @param defaultStr
|
||||
* the default string to return
|
||||
*
|
||||
* @return the passed in string, or the default one
|
||||
*/
|
||||
public static String defaultIfBlank(String str, String defaultStr) {
|
||||
return StringUtil.isBlank(str) ? defaultStr : str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips whitespace from the start and end of a string returning {@code null} if the string is empty ("") after the
|
||||
* strip.
|
||||
*
|
||||
* @param str
|
||||
* the string to be stripped, may be {@code null}
|
||||
*
|
||||
* @return the stripped string, {@code null} if whitespace, empty or {@code null} input string
|
||||
*/
|
||||
public static @Nullable String stripToNull(@Nullable String str) {
|
||||
if (str == null) {
|
||||
return null;
|
||||
}
|
||||
String s = str.trim();
|
||||
return (s.isEmpty()) ? null : s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalizes a string changing the first letter to title case as per {@link Character#toTitleCase(char)}. No other
|
||||
* letters are changed.
|
||||
*
|
||||
* @param str
|
||||
* the string to capitalize, may be {@code null}
|
||||
*
|
||||
* @return the capitalized string, {@code null} if {@code null} input string
|
||||
*/
|
||||
public static @Nullable String capitalize(@Nullable String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return str;
|
||||
}
|
||||
return str.substring(0, 1).toUpperCase() + str.substring(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all the whitespace separated words in a string into capitalized words, that is each word is made up of a
|
||||
* titlecase character and then a series of lowercase characters.
|
||||
*
|
||||
* @param str
|
||||
* the string to capitalize, may be {@code null}
|
||||
*
|
||||
* @return the capitalized string, {@code null} if {@code null} input string
|
||||
*/
|
||||
public static @Nullable String capitalizeAll(@Nullable String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return str;
|
||||
}
|
||||
// Java 8 version
|
||||
return Arrays.stream(str.split("\\s+")).map(t -> t.substring(0, 1).toUpperCase() + t.substring(1).toLowerCase())
|
||||
.collect(Collectors.joining(" "));
|
||||
// Ready for Java 9+
|
||||
// return Pattern.compile("\\b(.)(.*?)\\b").matcher(str)
|
||||
// .replaceAll(match -> match.group(1).toUpperCase() + match.group(2).toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of an {@link InputStream} stream as a string using the default character encoding of the
|
||||
* platform. This method buffers the input internally, so there is no need to use a {@code BufferedInputStream}.
|
||||
*
|
||||
* @param input
|
||||
* the {@code InputStream} to read from
|
||||
*
|
||||
* @return the string read from stream
|
||||
*
|
||||
* @throws {@link IOException}
|
||||
* if an I/O error occurs
|
||||
*/
|
||||
public static String streamToString(InputStream input) throws IOException {
|
||||
InputStreamReader reader = new InputStreamReader(input);
|
||||
|
||||
final StringWriter writer = new StringWriter();
|
||||
char[] buffer = new char[DEFAULT_BUFFER_SIZE];
|
||||
|
||||
int n = 0;
|
||||
while ((n = reader.read(buffer)) != EOF) {
|
||||
writer.write(buffer, 0, n);
|
||||
}
|
||||
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of a {@link Reader} stream as a string using the default character encoding of the platform.
|
||||
* This method doesn't buffer the input internally, so eventually {@code BufferedReder} needs to be used externally.
|
||||
*
|
||||
* @param reader
|
||||
* the {@code Reader} to read from
|
||||
*
|
||||
* @return the string read from stream
|
||||
*
|
||||
* @throws {@link IOException}
|
||||
* if an I/O error occurs
|
||||
*/
|
||||
public static String readerToString(Reader reader) throws IOException {
|
||||
final StringWriter writer = new StringWriter();
|
||||
|
||||
int c;
|
||||
while ((c = reader.read()) != EOF) {
|
||||
writer.write(c);
|
||||
}
|
||||
|
||||
return writer.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="bticinosmarther" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>BTicino Smarther Binding</name>
|
||||
<description>This is the binding for BTicino Smarther chronothermostat units</description>
|
||||
<author>Fabio Possieri</author>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,148 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<!-- Config for BTicino Smarther Bridge -->
|
||||
<config-description uri="bridge-type:smarther:bridge">
|
||||
|
||||
<!-- Parameter groups -->
|
||||
<parameter-group name="subscription">
|
||||
<label>Product Subscription</label>
|
||||
<description>Details of the Smarther product subscription connected to the BTicino/Legrand development account.</description>
|
||||
</parameter-group>
|
||||
|
||||
<parameter-group name="application">
|
||||
<label>Application Details</label>
|
||||
<description>Details of the Smarther application registered on the BTicino/Legrand development portal.</description>
|
||||
</parameter-group>
|
||||
|
||||
<parameter-group name="advancedset">
|
||||
<label>Advanced Settings</label>
|
||||
<description>Advanced settings of this bridge.</description>
|
||||
</parameter-group>
|
||||
|
||||
<!-- Parameters -->
|
||||
<parameter name="subscriptionKey" groupName="subscription" type="text" pattern="[0-9a-f]{32}">
|
||||
<label>Subscription Key</label>
|
||||
<description>This is the Subscription Key provided by BTicino/Legrand when you subscribe to Smarther - v2.0 product.
|
||||
Go to https://developer.legrand.com/tutorials/getting-started/</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
|
||||
<parameter name="clientId" groupName="application" type="text"
|
||||
pattern="[0-9a-f]{8}[-]([0-9a-f]{4}[-]){3}[0-9a-f]{12}">
|
||||
<label>Client ID</label>
|
||||
<description>This is the Client ID provided by BTicino/Legrand when you add a new Application to your developer
|
||||
account. Go to https://developer.legrand.com/tutorials/create-an-application/</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
|
||||
<parameter name="clientSecret" groupName="application" type="text">
|
||||
<label>Client Secret</label>
|
||||
<description>This is the Client Secret provided by BTicino/Legrand when you add a new Application to your developer
|
||||
account.</description>
|
||||
<required>true</required>
|
||||
<context>password</context>
|
||||
</parameter>
|
||||
|
||||
<parameter name="useNotifications" groupName="advancedset" type="boolean">
|
||||
<label>Use Notifications</label>
|
||||
<description>ON = the bridge subscribes each of its locations to receive C2C notifications upon changes on each of
|
||||
its modules' status or sensors data - temperature, humidity (requires a public https endpoint has been set as "First
|
||||
Reply Url" when registering the Application on Legrand's development portal); OFF = for each module connected to
|
||||
this bridge, status+sensors data are requested to Smarther API gateway on a periodical basis and whenever new
|
||||
settings are applied (period can be changed via module's "Status Refresh Period" parameter).</description>
|
||||
<required>false</required>
|
||||
<advanced>true</advanced>
|
||||
<default>true</default>
|
||||
</parameter>
|
||||
|
||||
<parameter name="statusRefreshPeriod" groupName="advancedset" type="integer" min="1" unit="min">
|
||||
<label>Bridge Status Refresh Period (minutes)</label>
|
||||
<description>This is the frequency the Smarther API gateway is called to update bridge status. There are limits to
|
||||
the number of requests that can be sent to the Smarther API gateway. The more often you poll, the faster locations
|
||||
are updated - at the risk of running out of your request quota.</description>
|
||||
<required>false</required>
|
||||
<advanced>true</advanced>
|
||||
<unitLabel>Minutes</unitLabel>
|
||||
<default>1440</default>
|
||||
</parameter>
|
||||
|
||||
</config-description>
|
||||
|
||||
<!-- Config for BTicino Smarther Module -->
|
||||
<config-description uri="thing-type:smarther:module">
|
||||
|
||||
<!-- Parameter groups -->
|
||||
<parameter-group name="topology">
|
||||
<label>Module Topology</label>
|
||||
<description>Reference to uniquely identify the module towards the BTicino/Legrand API gateway.</description>
|
||||
</parameter-group>
|
||||
|
||||
<parameter-group name="advancedset">
|
||||
<label>Advanced Settings</label>
|
||||
<description>Advanced settings of this module.</description>
|
||||
</parameter-group>
|
||||
|
||||
<!-- Parameters -->
|
||||
<parameter name="plantId" groupName="topology" type="text"
|
||||
pattern="[0-9a-f]{8}[-]([0-9a-f]{4}[-]){3}[0-9a-f]{12}">
|
||||
<label>Location Plant Id</label>
|
||||
<description>This is the Plant Id of the location the Chronothermostat module is installed in, provided by Smarther
|
||||
API.</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
|
||||
<parameter name="moduleId" groupName="topology" type="text"
|
||||
pattern="[0-9a-f]{8}[-]([0-9a-f]{4}[-]){3}[0-9a-f]{12}">
|
||||
<label>Chronothermostat Module Id</label>
|
||||
<description>This is the Module Id of the Chronothermostat module, provided by Smarther API.</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
|
||||
<parameter name="settingsAutoupdate" groupName="advancedset" type="boolean">
|
||||
<label>Module Settings Auto-Update</label>
|
||||
<description>ON = the module settings are automatically updated according to the module status whenever it changes
|
||||
(e.g. polling, notification, etc.). OFF = the module settings are aligned to the module status only upon module
|
||||
initialization.</description>
|
||||
<required>false</required>
|
||||
<advanced>true</advanced>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
|
||||
<parameter name="programsRefreshPeriod" groupName="advancedset" type="integer" min="1" unit="h">
|
||||
<label>Programs Refresh Period (hours)</label>
|
||||
<description>This is the frequency the Smarther API gateway is called to refresh Programs list used in "automatic"
|
||||
mode. There are limits to the number of requests that can be sent to the Smarther API gateway. The more often you
|
||||
poll, the faster locations are updated - at the risk of running out of your request quota.</description>
|
||||
<required>false</required>
|
||||
<advanced>true</advanced>
|
||||
<unitLabel>Hours</unitLabel>
|
||||
<default>12</default>
|
||||
</parameter>
|
||||
|
||||
<parameter name="numberOfEndDays" groupName="advancedset" type="integer" min="1" max="9">
|
||||
<label>Number Of Days For End Date</label>
|
||||
<description>This is the number of days to be displayed in module settings, as options list for "End Date" field in
|
||||
"manual" mode (e.g. 1 = only "Today" is displayed, 5 = "Today" + "Tomorrow" + following 3 days are displayed).</description>
|
||||
<required>false</required>
|
||||
<advanced>true</advanced>
|
||||
<default>5</default>
|
||||
</parameter>
|
||||
|
||||
<parameter name="statusRefreshPeriod" groupName="advancedset" type="integer" min="1" unit="min">
|
||||
<label>Module Status Refresh Period (minutes)</label>
|
||||
<description>This is the frequency the Smarther API gateway is called to update module status and sensors data. There
|
||||
are limits to the number of requests that can be sent to the Smarther API gateway. The more often you poll, the
|
||||
faster locations are updated - at the risk of running out of your request quota.</description>
|
||||
<required>false</required>
|
||||
<advanced>true</advanced>
|
||||
<unitLabel>Minutes</unitLabel>
|
||||
<default>60</default>
|
||||
</parameter>
|
||||
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
||||
@@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="bticinosmarther"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<!-- BTicino/Legrand Smarther API Bridge -->
|
||||
<bridge-type id="bridge">
|
||||
<label>BTicino Smarther Bridge</label>
|
||||
<description>
|
||||
<![CDATA[
|
||||
This bridge represents the gateway to Smarther API in the context of one specific BTicino/Legrand developer account.<br/>
|
||||
If you want to control your devices in the context of different accounts you have to register a bridge for each account.<br/>
|
||||
<br/>
|
||||
<b>How-To configure the bridge:</b><br/>
|
||||
<ul>
|
||||
<li>Sign up for a new developer account on <a href="https://developer.legrand.com/login" target="_blank">Works with Legrand website</a></li>
|
||||
<li>Subscribe to "Starter Kit for Legrand APIs" from <a href="https://portal.developer.legrand.com/products/starter-kit" target="_blank">API > Subscriptions</a> menu
|
||||
<ul>
|
||||
<li>This will generate your primary and secondary "Subscription Key"</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Register a new application from <a href="https://partners-mysettings.developer.legrand.com/Application/Index" target="_blank">User > My Applications</a> menu
|
||||
<ul>
|
||||
<li>In "First Reply Url" field insert the public callback URL "https://<your openHAB host>:<your openHAB port>/smarther/connectsmarther"</li>
|
||||
<li>Tick the checkbox near "comfort.read" and "comfort.write" scopes</li>
|
||||
</ul>
|
||||
You should receive an email from Legrand, usually within 1-2 days max, containing your application's "Client ID" and "Client Secret".
|
||||
</li>
|
||||
</ul>
|
||||
<b>How-To authorize the bridge:</b><br/>
|
||||
<ul>
|
||||
<li>Create and configure a bridge Thing first, using above Subscription Key + Client ID + Client Secret, then</li>
|
||||
<li>Open in your browser the public URL "https://<your openHAB host>:<your openHAB port>/smarther/connectsmarther", and</li>
|
||||
<li>Follow the steps reported therein to authorize the bridge</li>
|
||||
</ul>
|
||||
]]>
|
||||
</description>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group id="status" typeId="bridge-status"/>
|
||||
<channel-group id="config" typeId="bridge-config"/>
|
||||
</channel-groups>
|
||||
|
||||
<properties>
|
||||
<property name="vendor">BTicino</property>
|
||||
</properties>
|
||||
|
||||
<representation-property>subscriptionKey</representation-property>
|
||||
|
||||
<config-description-ref uri="bridge-type:smarther:bridge"/>
|
||||
</bridge-type>
|
||||
|
||||
<!-- Channel groups -->
|
||||
<channel-group-type id="bridge-status">
|
||||
<label>Status</label>
|
||||
<description>Current operational status of the bridge</description>
|
||||
<channels>
|
||||
<channel id="apiCallsHandled" typeId="status-apicallshandled"/>
|
||||
<channel id="notifsReceived" typeId="status-notifsreceived"/>
|
||||
<channel id="notifsRejected" typeId="status-notifsrejected"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="bridge-config">
|
||||
<label>Configuration</label>
|
||||
<description>Convenience configuration channels for the bridge</description>
|
||||
<channels>
|
||||
<channel id="fetchLocations" typeId="config-fetchlocations"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<!-- Channel types -->
|
||||
<channel-type id="status-apicallshandled">
|
||||
<item-type>Number</item-type>
|
||||
<label>API Calls Handled</label>
|
||||
<description>Total number of API calls handled by the bridge</description>
|
||||
<state readOnly="true" min="0" pattern="%d"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="status-notifsreceived">
|
||||
<item-type>Number</item-type>
|
||||
<label>Notifications Received</label>
|
||||
<description>Total number of C2C notifications received by the bridge</description>
|
||||
<state readOnly="true" min="0" pattern="%d"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="status-notifsrejected">
|
||||
<item-type>Number</item-type>
|
||||
<label>Notifications Rejected</label>
|
||||
<description>Total number of C2C notifications rejected by the bridge</description>
|
||||
<state readOnly="true" min="0" pattern="%d"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="config-fetchlocations" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Fetch Locations List</label>
|
||||
<description>This is a convenience switch to trigger a call to the Smarther API gateway, to manually fetch the updated
|
||||
client locations list.</description>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,222 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="bticinosmarther"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<!-- BTicino Smarther Module Thing -->
|
||||
<thing-type id="module">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="bridge"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>BTicino Smarther Chronothermostat</label>
|
||||
<description>This thing represents a BTicino Smarther chronothermostat module.</description>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group id="measures" typeId="module-measures"/>
|
||||
<channel-group id="status" typeId="module-status"/>
|
||||
<channel-group id="settings" typeId="module-settings"/>
|
||||
<channel-group id="config" typeId="module-config"/>
|
||||
</channel-groups>
|
||||
|
||||
<properties>
|
||||
<property name="vendor">BTicino</property>
|
||||
<property name="modelId">X8000</property>
|
||||
</properties>
|
||||
|
||||
<representation-property>moduleId</representation-property>
|
||||
|
||||
<config-description-ref uri="thing-type:smarther:module"/>
|
||||
</thing-type>
|
||||
|
||||
<!-- Channel groups -->
|
||||
<channel-group-type id="module-measures">
|
||||
<label>Measures</label>
|
||||
<description>Measures taken from the module on-board sensors</description>
|
||||
<channels>
|
||||
<channel id="temperature" typeId="measures-temperature"/>
|
||||
<channel id="humidity" typeId="measures-humidity"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="module-status">
|
||||
<label>Status</label>
|
||||
<description>Current operational status of the module</description>
|
||||
<channels>
|
||||
<channel id="state" typeId="status-state"/>
|
||||
<channel id="function" typeId="status-function"/>
|
||||
<channel id="mode" typeId="status-mode"/>
|
||||
<channel id="temperature" typeId="status-temperature"/>
|
||||
<channel id="program" typeId="status-program"/>
|
||||
<channel id="endTime" typeId="status-endtime"/>
|
||||
<channel id="temperatureFormat" typeId="status-temperatureformat"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="module-settings">
|
||||
<label>Settings</label>
|
||||
<description>New operational settings to be applied to the module</description>
|
||||
<channels>
|
||||
<channel id="mode" typeId="settings-mode"/>
|
||||
<channel id="temperature" typeId="settings-temperature"/>
|
||||
<channel id="program" typeId="settings-program"/>
|
||||
<channel id="boostTime" typeId="settings-boosttime"/>
|
||||
<channel id="endDate" typeId="settings-enddate"/>
|
||||
<channel id="endHour" typeId="settings-endhour"/>
|
||||
<channel id="endMinute" typeId="settings-endminute"/>
|
||||
<channel id="power" typeId="settings-power"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="module-config">
|
||||
<label>Configuration</label>
|
||||
<description>Convenience configuration channels for the module</description>
|
||||
<channels>
|
||||
<channel id="fetchPrograms" typeId="config-fetchprograms"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<!-- Channel types -->
|
||||
<channel-type id="measures-temperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Temperature</label>
|
||||
<description>Indoor temperature as measured by the sensor</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%" step="0.1"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="measures-humidity">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Humidity</label>
|
||||
<description>Indoor humidity as measured by the sensor</description>
|
||||
<category>Humidity</category>
|
||||
<state readOnly="true" min="0" max="100" pattern="%.1f %unit%" step="0.1"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="status-state">
|
||||
<item-type>Switch</item-type>
|
||||
<label>State</label>
|
||||
<description>Current operational state of the module</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="status-function" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Function</label>
|
||||
<description>Current operational function set on the module</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="status-mode">
|
||||
<item-type>String</item-type>
|
||||
<label>Mode</label>
|
||||
<description>Current operational mode set on the module</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="status-temperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Temperature</label>
|
||||
<description>Current operational target temperature set on the module</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%" step="0.1"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="status-program">
|
||||
<item-type>String</item-type>
|
||||
<label>Program</label>
|
||||
<description>Current operational program set on the module</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="status-endtime">
|
||||
<item-type>String</item-type>
|
||||
<label>End Time</label>
|
||||
<description>Current operational end time set on the module</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="status-temperatureformat" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Temperature Format</label>
|
||||
<description>Current operational temperature format of the module</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="settings-mode">
|
||||
<item-type>String</item-type>
|
||||
<label>Mode</label>
|
||||
<description>New operational mode to be set on the module</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="AUTOMATIC">Automatic</option>
|
||||
<option value="MANUAL">Manual</option>
|
||||
<option value="BOOST">Boost</option>
|
||||
<option value="OFF">Off</option>
|
||||
<option value="PROTECTION">Protection</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="settings-temperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Temperature</label>
|
||||
<description>New operational set-point temperature to be set on the module (valid only for Mode = "Manual")</description>
|
||||
<category>Temperature</category>
|
||||
<state pattern="%.1f %unit%" min="7.1" max="104" step="0.1"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="settings-program">
|
||||
<item-type>Number</item-type>
|
||||
<label>Program</label>
|
||||
<description>New operational program to be set on the module (valid only for Mode = "Automatic")</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="settings-boosttime">
|
||||
<item-type>Number</item-type>
|
||||
<label>Boost Time</label>
|
||||
<description>New operational boost time to be set on the module (valid only for Mode = "Boost")</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="30">30 min</option>
|
||||
<option value="60">60 min</option>
|
||||
<option value="90">90 min</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="settings-enddate">
|
||||
<item-type>String</item-type>
|
||||
<label>End Date</label>
|
||||
<description>New operational end date to be set on the module (valid only for Mode = "Manual")</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="settings-endhour">
|
||||
<item-type>Number</item-type>
|
||||
<label>End Hour</label>
|
||||
<description>New operational end hour to be set on the module (valid only for Mode = "Manual")</description>
|
||||
<state pattern="%02d" min="0" max="23" step="1"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="settings-endminute">
|
||||
<item-type>Number</item-type>
|
||||
<label>End Minute</label>
|
||||
<description>New operational end minute to be set on the module (valid only for Mode = "Manual")</description>
|
||||
<state pattern="%02d" min="0" max="59" step="15"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="settings-power">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Power</label>
|
||||
<description>Power on, send new operational settings to the module</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="config-fetchprograms" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Fetch Programs List</label>
|
||||
<description>This is a convenience switch to trigger a call to the Smarther API gateway, to manually fetch the updated
|
||||
module programs list.</description>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="block${application.authorized}" id="${application.id}">
|
||||
Connect to Smarther API gateway: <i>${application.name} ${application.locations}</i>
|
||||
<div class="button"><p><a href=${application.authorize}>Authorize Bridge</a></p></div>
|
||||
</div>
|
||||
@@ -0,0 +1,95 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
${pageRefresh}
|
||||
<title>Authorize openHAB binding for BTicino Smarther Chronothermostats</title>
|
||||
<link rel="icon" href="connectsmarther/img/favicon.ico" type="image/vnd.microsoft.icon" />
|
||||
<style>
|
||||
html {
|
||||
font-family: "Roboto", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.block {
|
||||
border: 1px solid #bbb;
|
||||
background-color: white;
|
||||
margin: 10px 0;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #FFC0C0;
|
||||
border: 1px solid darkred;
|
||||
color: darkred
|
||||
}
|
||||
|
||||
.authorized {
|
||||
border: 1px solid #90EE90;
|
||||
background-color: #E0FFE0;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.button a {
|
||||
background: #1ED760;
|
||||
border-radius: 500px;
|
||||
color: white;
|
||||
padding: 10px 20px 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
border-width: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="width: 1024px; margin: auto; border: 2px solid gray; padding: 10px;">
|
||||
<svg class="logo" xmlns="http://www.w3.org/2000/svg" width="290px" height="70px" style="vector-effect: non-scaling-stroke;" preserveAspectRatio="xMidYMid meet">
|
||||
<g>
|
||||
<path fill="#444444" d="m29.129353,5.716719c-6.284499,1.347164 -11.247366,4.122323 -15.697762,8.702682c-4.288564,4.445642 -6.93183,9.457094 -8.010714,15.249901c-0.485498,2.559612 -0.566414,8.433249 -0.161833,10.588712c0.431554,2.155463 0.863107,3.825947 1.051912,4.041493c0.107888,0.107773 0.997968,-0.619696 2.022908,-1.643541l1.834103,-1.832144l-0.296693,-1.912974c-0.404582,-2.613499 -0.377609,-4.930622 0.107888,-7.70578c1.995935,-11.423954 10.923701,-19.83026 22.440788,-21.177424c8.280435,-0.969958 17.2082,2.748215 22.386844,9.295434c4.88195,6.14307 6.716053,13.525531 5.205615,20.907992c-1.941991,9.484037 -9.170514,17.189818 -18.475889,19.6686c-3.236652,0.862185 -8.415295,0.996902 -11.651948,0.269433c-4.74709,-1.023845 -8.819877,-3.206252 -12.407166,-6.574162l-1.861075,-1.778257l-1.699242,1.697427l-1.67227,1.670484l1.699243,1.616597c4.288564,4.09538 9.332347,6.789709 15.15832,8.10993c2.643266,0.592752 9.332347,0.592752 11.921669,0c12.272306,-2.775159 21.119155,-11.693387 23.735449,-23.952583c0.620358,-2.963762 0.566414,-8.594909 -0.134861,-11.80116c-0.701275,-3.179308 -1.132828,-4.337869 -2.643266,-7.436347c-3.803066,-7.732724 -11.112505,-13.579417 -19.77055,-15.761824c-3.371513,-0.835242 -9.7639,-0.969958 -13.081469,-0.24249z"/>
|
||||
<path d="m160.888066,19.727229c0,13.552474 0.026972,14.064396 0.539442,15.088241c0.296693,0.592752 1.051912,1.401051 1.67227,1.8052l1.105856,0.754412l5.90689,0.08083l5.90689,0.08083l-0.080916,-3.314024l-0.080916,-3.314024l-2.157768,-0.134716l-2.157768,-0.134716l0,-5.253941l0,-5.253941l2.238684,-0.08083l2.211712,-0.08083l0,-3.206251l0,-3.233195l-2.292629,0l-2.292629,0l0,-3.906777l0,-3.906777l-5.25956,0l-5.25956,0l0,14.01051z"/>
|
||||
<path fill="#f27500" d="m132.837081,21.747976l0,15.761824l10.330315,0c5.664141,0 10.761868,-0.107773 11.328282,-0.24249c1.483466,-0.350263 2.832071,-1.347164 3.506373,-2.586556c0.566414,-1.023845 0.593386,-1.454938 0.674303,-8.621852c0.107888,-8.594909 -0.026972,-9.376264 -1.591354,-10.912032c-1.483466,-1.454938 -2.427489,-1.616597 -8.577128,-1.616597l-5.421392,0l0,-3.77206l0,-3.77206l-5.124699,0l-5.124699,0l0,15.761824zm15.589874,3.69123l-0.080916,5.200055l-2.616294,0.08083l-2.643266,0.08083l0,-5.280884l0,-5.253941l2.69721,0l2.69721,0l-0.053944,5.173111z"/>
|
||||
<path d="m177.341047,25.520036l0,11.989763l5.25956,0l5.25956,0l0,-11.989763l0,-11.989763l-5.25956,0l-5.25956,0l0,11.989763z"/>
|
||||
<path d="m192.850005,13.907479c-1.1598,0.484979 -2.508405,2.12852 -2.751154,3.314024c-0.107888,0.565809 -0.215777,4.472586 -0.215777,8.702682c0,7.463291 0.026972,7.70578 0.620358,8.810455c0.323665,0.646639 1.105856,1.481881 1.726214,1.88603l1.132828,0.754412l9.305375,0.08083c9.251431,0.08083 9.332347,0.053887 10.546091,-0.538866c0.701275,-0.377206 1.53741,-1.104675 2.022908,-1.778257c0.809163,-1.158561 0.809163,-1.212448 0.809163,-4.715075l0,-3.556514l-5.124699,0l-5.124699,0l-0.080916,0.673582c-0.026972,0.377206 -0.134861,1.185505 -0.215777,1.8052l-0.134861,1.158561l-2.427489,0l-2.400517,0l0,-4.984508l0,-4.984508l2.400517,0l2.400517,0l0.161833,0.835242c0.080916,0.458036 0.161833,1.266335 0.161833,1.751314l0,0.916072l5.286532,0l5.286532,0l-0.107888,-3.448741c-0.107888,-3.69123 -0.350637,-4.445643 -1.753187,-5.765864c-0.404582,-0.404149 -1.105856,-0.862185 -1.510438,-0.996902c-1.186772,-0.458036 -18.961387,-0.377206 -20.013299,0.08083z"/>
|
||||
<path d="m218.068919,25.520036l0,11.989763l5.25956,0l5.25956,0l0,-11.989763l0,-11.989763l-5.25956,0l-5.25956,0l0,11.989763z"/>
|
||||
<path d="m230.476086,25.520036l0,11.989763l5.25956,0l5.25956,0l0.053944,-8.433249l0.080916,-8.406306l2.508405,-0.08083l2.481433,-0.08083l0,8.514079l0,8.487136l5.25956,0l5.25956,0l0,-10.103733c0,-9.564867 -0.026972,-10.184563 -0.539442,-11.181465c-0.296693,-0.565809 -0.970996,-1.374108 -1.53741,-1.8052l-0.970996,-0.754412l-11.571031,-0.08083l-11.544059,-0.08083l0,12.016707z"/>
|
||||
<path d="m261.628862,13.907479c-0.970996,0.404149 -2.481433,2.074633 -2.69721,2.936818c-0.080916,0.323319 -0.134861,4.418699 -0.080916,9.106831l0.080916,8.514079l0.782191,0.996902c1.510438,1.96686 1.834103,2.04769 12.029557,2.04769c8.199519,0 9.143542,-0.053887 10.114538,-0.511922c1.348605,-0.592752 2.454461,-1.859087 2.832071,-3.206251c0.350637,-1.320221 0.350637,-15.222958 -0.026972,-16.570122c-0.350637,-1.266335 -1.699242,-2.855989 -2.859043,-3.314024c-1.240717,-0.538866 -18.988359,-0.511922 -20.175131,0zm12.542027,11.612557l0,4.984508l-2.427489,0l-2.427489,0l0,-4.984508l0,-4.984508l2.427489,0l2.427489,0l0,4.984508z"/>
|
||||
<path fill="#e23d18" d="m21.604137,34.788527l-13.459078,13.444701l0.350637,0.835242c0.188805,0.458036 0.701275,1.401051 1.132828,2.101576l0.809163,1.239391l12.164417,-12.151423c6.716053,-6.708879 12.32625,-12.178366 12.461111,-12.178366c0.134861,0 4.423425,4.149266 9.494179,9.214605c6.014778,6.008353 9.332347,9.133775 9.494179,8.972115c0.161833,-0.16166 0.620358,-1.131618 1.051912,-2.182406l0.809163,-1.912973l-10.438203,-10.427053l-10.411231,-10.400109l-13.459078,13.444701z"/>
|
||||
<path d="m82.480169,33.441363c-4.477369,1.88603 -8.415295,3.556514 -8.738961,3.69123c-0.51247,0.24249 0.809163,0.862185 8.415295,4.068437l9.008682,3.77206l0.080916,-2.371009l0.080916,-2.397953l8.334379,0l8.361351,0l0,2.290179c0,1.670484 0.080916,2.290179 0.350637,2.290179c0.323665,0 17.397005,-7.139971 17.639754,-7.355518c0.107888,-0.134716 -17.316089,-7.463291 -17.72067,-7.463291c-0.161833,0 -0.269721,0.943015 -0.269721,2.290179l0,2.290179l-8.361351,0l-8.361351,0l0,-2.290179c0,-1.670484 -0.080916,-2.290179 -0.323665,-2.263236c-0.188805,0 -4.018843,1.562711 -8.496212,3.448741z"/>
|
||||
<path fill="#BBBBBB" d="m138.555166,40.177185c-3.101792,1.077732 -4.666173,3.206251 -4.666173,6.412503c0,2.182406 0.350637,3.098478 1.753187,4.310926c1.078884,0.969958 2.508405,1.481881 6.230555,2.236293c4.908922,0.996902 6.338444,2.04769 6.338444,4.715075c0,3.287081 -2.616294,4.903678 -7.498244,4.661189c-2.400517,-0.107773 -4.854978,-0.943015 -6.068723,-2.020747c-0.458526,-0.431093 -0.51247,-0.404149 -1.186772,0.24249c-0.377609,0.404149 -0.620358,0.889129 -0.539442,1.077732c0.053944,0.215546 0.863107,0.754412 1.726214,1.239391c4.585257,2.424896 11.16645,2.101576 13.890632,-0.673582c1.348605,-1.374108 1.834103,-2.667386 1.834103,-4.795905c0,-3.664287 -1.753187,-5.119225 -7.821909,-6.466389c-5.340476,-1.212448 -6.473304,-2.020747 -6.473304,-4.526472c0,-1.832144 0.701275,-3.125421 2.103824,-3.906777c2.427489,-1.347164 5.852946,-1.266335 8.819877,0.215546l1.618326,0.808299l0.620358,-0.700525c0.431554,-0.511922 0.539442,-0.808299 0.350637,-1.023845c-0.431554,-0.511922 -3.047847,-1.72437 -4.36948,-2.020747c-1.888047,-0.458036 -5.070755,-0.350263 -6.662109,0.215546z"/>
|
||||
<path fill="#BBBBBB" d="m233.443017,52.193891l0,12.259196l1.078884,0l1.078884,0l0,-6.14307c0,-5.415601 0.080916,-6.250843 0.51247,-7.247745c1.402549,-3.044592 6.823941,-3.044592 8.442268,0c0.566414,1.050788 0.620358,1.535767 0.701275,7.247745l0.107888,6.14307l1.078884,0l1.078884,0l-0.107888,-6.547219c-0.107888,-7.355518 -0.269721,-8.136873 -2.18474,-9.807357c-2.292629,-1.993803 -6.392388,-1.751314 -8.738961,0.511922l-0.890079,0.862185l0,-4.768962l0,-4.768962l-1.078884,0l-1.078884,0l0,12.259196z"/>
|
||||
<path fill="#BBBBBB" d="m222.114735,44.380338l0,2.559612l-0.944024,0c-0.890079,0 -0.944024,0.053887 -0.944024,0.943015c0,0.889129 0.053944,0.943015 0.944024,0.943015l0.944024,0l0.026972,6.250843c0,4.122323 0.107888,6.601106 0.323665,7.220801c0.485498,1.427994 1.807131,2.155463 3.85701,2.155463l1.726214,0l0,-0.916072c0,-0.889129 -0.026972,-0.916072 -1.375577,-1.023845c-2.292629,-0.215546 -2.265656,-0.134716 -2.346573,-7.355518l-0.080916,-6.331673l1.888047,0l1.915019,0l0,-0.943015l0,-0.943015l-1.888047,0l-1.888047,0l0,-2.559612l0,-2.559612l-1.078884,0l-1.078884,0l0,2.559612z"/>
|
||||
<path fill="#BBBBBB" d="m162.10181,46.913007c-1.1598,0.296376 -2.022908,0.781355 -3.020875,1.72437l-0.890079,0.835242l0,-1.266335l0,-1.266335l-1.078884,0l-1.078884,0l0,8.756569l0,8.756569l1.078884,0l1.078884,0l0,-6.14307c0,-5.846694 0.026972,-6.196956 0.620358,-7.328574c0.728247,-1.454938 1.968963,-2.155463 3.830038,-2.155463c1.888047,0 3.101792,0.673582 3.85701,2.155463c0.566414,1.131618 0.593386,1.508824 0.593386,7.328574l0,6.14307l1.051912,0l1.051912,0l0.107888,-6.14307c0.080916,-5.442544 0.161833,-6.2239 0.620358,-7.166915c0.782191,-1.562711 1.726214,-2.12852 3.72215,-2.263236c1.402549,-0.08083 1.807131,0 2.616294,0.538866c1.780159,1.185505 1.888047,1.751314 1.888047,8.756569l0,6.277786l1.240717,0l1.240717,0l-0.134861,-6.412503c-0.134861,-7.220801 -0.269721,-7.921327 -1.941991,-9.645697c-2.211712,-2.290179 -5.718085,-2.290179 -8.118602,0.026943c-0.620358,0.592752 -1.186772,1.266335 -1.267689,1.481881c-0.134861,0.323319 -0.242749,0.323319 -0.51247,-0.134716c-1.618326,-2.505726 -3.883983,-3.475684 -6.55422,-2.855989z"/>
|
||||
<path fill="#BBBBBB" d="m191.177735,46.93995c-1.699242,0.350263 -3.85701,1.562711 -3.85701,2.155463c0,0.107773 0.296693,0.404149 0.674303,0.646639c0.620358,0.404149 0.701275,0.377206 1.645298,-0.16166c1.456493,-0.862185 5.016811,-0.996902 6.230555,-0.24249c1.213745,0.754412 1.699242,1.643541 1.888047,3.367911l0.161833,1.508824l-3.506373,0c-5.070755,0 -7.012746,0.754412 -7.875853,3.044592c-0.431554,1.104675 -0.350637,3.583457 0.080916,4.472586c0.64733,1.239391 2.157768,2.371009 3.668206,2.721272c1.807131,0.431093 4.801034,0.16166 6.203583,-0.538866c1.294661,-0.673582 1.348605,-0.673582 1.348605,0c0,0.458036 0.188805,0.538866 1.078884,0.538866l1.078884,0l0,-6.439446c0,-5.73892 -0.053944,-6.574162 -0.51247,-7.624951c-0.64733,-1.508824 -1.456493,-2.317123 -2.886015,-2.936818c-1.348605,-0.619696 -3.85701,-0.835242 -5.421392,-0.511922zm6.662109,11.774217c0,1.320221 -0.107888,2.505726 -0.215777,2.694329c-0.431554,0.700525 -2.211712,1.158561 -4.396452,1.158561c-3.425457,0 -4.828006,-0.916072 -4.828006,-3.179308c0,-2.559612 0.809163,-2.990705 5.745057,-3.017648l3.695178,0l0,2.344066z"/>
|
||||
<path fill="#BBBBBB" d="m212.108085,47.074666c-0.674303,0.215546 -1.807131,0.889129 -2.481433,1.508824l-1.267689,1.104675l0,-1.374108l0,-1.374108l-1.078884,0l-1.078884,0l0,8.756569l0,8.756569l1.051912,0l1.078884,0l0.080916,-6.439446c0.080916,-7.086085 0.107888,-7.113028 1.915019,-8.487136c1.02494,-0.754412 3.749122,-0.916072 5.178643,-0.296376l1.078884,0.431093l0.593386,-0.754412c0.782191,-0.969958 0.782191,-1.050788 -0.215777,-1.589654c-1.186772,-0.592752 -3.371513,-0.727469 -4.854978,-0.24249z"/>
|
||||
<path fill="#BBBBBB" d="m257.771852,47.155496c-3.398485,1.212448 -4.639201,3.502627 -4.639201,8.487136c0,3.448741 0.215777,4.418699 1.321633,6.170013c1.1598,1.832144 3.236652,2.775159 6.095695,2.775159c2.346573,-0.026943 4.423425,-0.700525 5.502309,-1.859087l0.566414,-0.592752l-0.620358,-0.592752l-0.593386,-0.565809l-1.591354,0.781355c-1.240717,0.619696 -1.941991,0.808299 -3.20968,0.808299c-3.101792,0 -5.016811,-1.643541 -5.448364,-4.661189l-0.188805,-1.266335l6.257527,0l6.257527,0l-0.161833,-2.101576c-0.350637,-3.825947 -1.429521,-5.927523 -3.72215,-7.086085c-1.53741,-0.781355 -4.072787,-0.916072 -5.825974,-0.296376zm5.205615,2.344066c1.321633,0.754412 1.968963,1.939917 2.211712,3.906777l0.161833,1.347164l-5.178643,0l-5.178643,0l0.161833,-1.293278c0.431554,-2.936818 2.346573,-4.634246 5.232588,-4.634246c1.078884,0 1.780159,0.188603 2.589322,0.673582z"/>
|
||||
<path fill="#BBBBBB" d="m278.729174,47.074666c-0.674303,0.215546 -1.726214,0.862185 -2.346573,1.427994l-1.132828,1.050788l0,-1.320221l0,-1.293278l-1.213745,0l-1.213745,0l0,8.756569l0,8.756569l1.213745,0l1.213745,0l0,-6.439446c0,-6.2239 0.026972,-6.439446 0.620358,-7.328574c0.323665,-0.511922 1.02494,-1.131618 1.53741,-1.374108c1.294661,-0.673582 3.910955,-0.646639 5.016811,0.026943l0.863107,0.511922l0.566414,-0.754412c0.755219,-1.050788 0.728247,-1.266335 -0.269721,-1.778257c-1.186772,-0.592752 -3.371513,-0.727469 -4.854978,-0.24249z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<h3>Authorize openHAB binding for BTicino Smarther Chronothermostats</h3>
|
||||
<p>On this page you can authorize your openHAB Smarther Bridge configured with the Subscription Key, Client Id and Client Secret of the Smarther Application on your developer account.</p>
|
||||
<p>You have to login to your BTicino/Legrand developer account, in order to authorize this binding to access your account and connected devices.</p>
|
||||
<p>To use this binding the following requirements apply:</p>
|
||||
<ul>
|
||||
<li>A BTicino/Legrand developer account.
|
||||
<li>Subscribe to Smarther - v2.0 product, to obtain the Subscription Key
|
||||
<li>Register an Application on your developer account.
|
||||
</ul>
|
||||
<p>
|
||||
The redirect URI to use when registering an Applicaton for this openHAB installation is
|
||||
<a href="${redirectUri}">${redirectUri}</a>
|
||||
</p>
|
||||
${error} ${authorizedBridge} ${applications}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Reference in New Issue
Block a user