added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -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>

View File

@@ -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()));
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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());
}
}
}

View File

@@ -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()));
}
}
}
}

View File

@@ -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());
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<>();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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()));
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 &gt; 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 &gt; My Applications</a> menu
<ul>
<li>In "First Reply Url" field insert the public callback URL "https://&lt;your openHAB host&gt;:&lt;your openHAB port&gt;/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://&lt;your openHAB host&gt;:&lt;your openHAB port&gt;/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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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