mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-02-05 20:04:23 +01:00
update linky binding to use new dataconnect api
Signed-off-by: Laurent ARNAL <laurent@clae.net>
This commit is contained in:
parent
9544767641
commit
43585f32ed
@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
<feature name="openhab-binding-linky" description="Linky Binding" version="${project.version}">
|
<feature name="openhab-binding-linky" description="Linky Binding" version="${project.version}">
|
||||||
<feature>openhab-runtime-base</feature>
|
<feature>openhab-runtime-base</feature>
|
||||||
|
<feature>openhab-core-auth-oauth2client</feature>
|
||||||
<bundle dependency="true">mvn:org.jsoup/jsoup/1.14.3</bundle>
|
<bundle dependency="true">mvn:org.jsoup/jsoup/1.14.3</bundle>
|
||||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.linky/${project.version}</bundle>
|
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.linky/${project.version}</bundle>
|
||||||
|
|
||||||
</feature>
|
</feature>
|
||||||
</features>
|
</features>
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 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.linky.internal;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Gaël L'hopital - Initial contribution *
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public interface LinkyAccountHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns true if the Spotify Bridge is authorized.
|
||||||
|
*/
|
||||||
|
boolean isAuthorized();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls Smartthings Api to obtain refresh and access tokens and persist data with Thing.
|
||||||
|
*
|
||||||
|
* @param redirectUrl The redirect url Smartthings calls back to
|
||||||
|
* @param reqCode The unique code passed by Smartthings to obtain the refresh and access tokens
|
||||||
|
* @return returns the name of the Smartthings user that is authorized
|
||||||
|
*/
|
||||||
|
String authorize(String redirectUrl, String reqCode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the Url to use to call Smartthings to authorize the application.
|
||||||
|
*
|
||||||
|
* @param redirectUri The uri Smartthings will redirect back to
|
||||||
|
* @return the formatted url that should be used to call Smartthings Web Api with
|
||||||
|
*/
|
||||||
|
String formatAuthorizationUrl(String redirectUri);
|
||||||
|
}
|
@ -0,0 +1,202 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 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.linky.internal;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Hashtable;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution *
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
|
*/
|
||||||
|
@Component(service = LinkyAuthService.class, configurationPid = "binding.internal.authService")
|
||||||
|
@NonNullByDefault
|
||||||
|
public class LinkyAuthService {
|
||||||
|
|
||||||
|
private static final String TEMPLATE_PATH = "templates/";
|
||||||
|
private static final String TEMPLATE_INDEX = TEMPLATE_PATH + "index.html";
|
||||||
|
private static final String ERROR_UKNOWN_BRIDGE = "Returned 'state' by doesn't match any Bridges. Has the bridge been removed?";
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(LinkyAuthService.class);
|
||||||
|
|
||||||
|
// private final List<SpotifyAccountHandler> handlers = new ArrayList<>();
|
||||||
|
|
||||||
|
private @NonNullByDefault({}) HttpService httpService;
|
||||||
|
private @NonNullByDefault({}) BundleContext bundleContext;
|
||||||
|
private @Nullable LinkyAccountHandler accountHandler;
|
||||||
|
|
||||||
|
@Activate
|
||||||
|
protected void activate(ComponentContext componentContext, Map<String, Object> properties) {
|
||||||
|
try {
|
||||||
|
bundleContext = componentContext.getBundleContext();
|
||||||
|
httpService.registerServlet(LinkyBindingConstants.LINKY_ALIAS, createServlet(), new Hashtable<>(),
|
||||||
|
httpService.createDefaultHttpContext());
|
||||||
|
httpService.registerResources(LinkyBindingConstants.LINKY_ALIAS + LinkyBindingConstants.LINKY_IMG_ALIAS,
|
||||||
|
"web", null);
|
||||||
|
} catch (NamespaceException | ServletException | IOException e) {
|
||||||
|
logger.warn("Error during spotify servlet startup", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deactivate
|
||||||
|
protected void deactivate(ComponentContext componentContext) {
|
||||||
|
httpService.unregister(LinkyBindingConstants.LINKY_ALIAS);
|
||||||
|
httpService.unregister(LinkyBindingConstants.LINKY_ALIAS + LinkyBindingConstants.LINKY_IMG_ALIAS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link SpotifyAuthServlet}.
|
||||||
|
*
|
||||||
|
* @return the newly created servlet
|
||||||
|
* @throws IOException thrown when an HTML template could not be read
|
||||||
|
*/
|
||||||
|
private HttpServlet createServlet() throws IOException {
|
||||||
|
return new LinkyAuthServlet(this, readTemplate(TEMPLATE_INDEX));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a template from file and returns the content as String.
|
||||||
|
*
|
||||||
|
* @param templateName name of the template file to read
|
||||||
|
* @return The content of the template file
|
||||||
|
* @throws IOException thrown when an HTML template could not be read
|
||||||
|
*/
|
||||||
|
private String readTemplate(String templateName) throws IOException {
|
||||||
|
final URL index = bundleContext.getBundle().getEntry(templateName);
|
||||||
|
|
||||||
|
if (index == null) {
|
||||||
|
throw new FileNotFoundException(
|
||||||
|
String.format("Cannot find '{}' - failed to initialize Linky servlet", templateName));
|
||||||
|
} else {
|
||||||
|
try (InputStream inputStream = index.openStream()) {
|
||||||
|
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call with Spotify redirect uri returned State and Code values to get the refresh and access tokens and persist
|
||||||
|
* these values
|
||||||
|
*
|
||||||
|
* @param servletBaseURL the servlet base, which will be the Spotify redirect url
|
||||||
|
* @param state The Spotify returned state value
|
||||||
|
* @param code The Spotify returned code value
|
||||||
|
* @return returns the name of the Spotify user that is authorized
|
||||||
|
*/
|
||||||
|
public String authorize(String servletBaseURL, String state, String code) {
|
||||||
|
LinkyAccountHandler accountHandler = getLinkyAccountHandler();
|
||||||
|
if (accountHandler == null) {
|
||||||
|
logger.debug(
|
||||||
|
"Linky redirected with state '{}' but no matching bridge was found. Possible bridge has been removed.",
|
||||||
|
state);
|
||||||
|
throw new RuntimeException(ERROR_UKNOWN_BRIDGE);
|
||||||
|
} else {
|
||||||
|
return accountHandler.authorize(servletBaseURL, code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param listener Adds the given handler
|
||||||
|
*/
|
||||||
|
public void setLinkyAccountHandler(LinkyAccountHandler accountHandler) {
|
||||||
|
this.accountHandler = accountHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param handler Removes the given handler
|
||||||
|
*/
|
||||||
|
public void unsetLinkyAccountHandler(LinkyAccountHandler accountHandler) {
|
||||||
|
this.accountHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param listener Adds the given handler
|
||||||
|
*/
|
||||||
|
public @Nullable LinkyAccountHandler getLinkyAccountHandler() {
|
||||||
|
return this.accountHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param listener Adds the given handler
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
* public void addSpotifyAccountHandler(SpotifyAccountHandler listener) {
|
||||||
|
* if (!handlers.contains(listener)) {
|
||||||
|
* handlers.add(listener);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param handler Removes the given handler
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
* public void removeSpotifyAccountHandler(SpotifyAccountHandler handler) {
|
||||||
|
* handlers.remove(handler);
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Returns all {@link SpotifyAccountHandler}s.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
* public List<SpotifyAccountHandler> getSpotifyAccountHandlers() {
|
||||||
|
* return handlers;
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the {@link SpotifyAccountHandler} that matches the given thing UID.
|
||||||
|
*
|
||||||
|
* @param thingUID UID of the thing to match the handler with
|
||||||
|
* @return the {@link SpotifyAccountHandler} matching the thing UID or null
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
* private @Nullable SpotifyAccountHandler getSpotifyAuthListener(String thingUID) {
|
||||||
|
* final Optional<SpotifyAccountHandler> maybeListener = handlers.stream().filter(l -> l.equalsThingUID(thingUID))
|
||||||
|
* .findFirst();
|
||||||
|
* return maybeListener.isPresent() ? maybeListener.get() : null;
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Reference
|
||||||
|
protected void setHttpService(HttpService httpService) {
|
||||||
|
this.httpService = httpService;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void unsetHttpService(HttpService httpService) {
|
||||||
|
this.httpService = null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 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.linky.internal;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
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.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The xxx manages the authorization with the Linky Web API. The servlet implements the
|
||||||
|
* Authorization Code flow and saves the resulting refreshToken with the bridge.
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution *
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class LinkyAuthServlet extends HttpServlet {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -4719613645562518231L;
|
||||||
|
|
||||||
|
private static final Pattern MESSAGE_KEY_PATTERN = Pattern.compile("\\$\\{([^\\}]+)\\}");
|
||||||
|
|
||||||
|
// Keys present in the index.html
|
||||||
|
private static final String KEY_BRIDGE_URI = "bridge.uri";
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(LinkyAuthServlet.class);
|
||||||
|
private final LinkyAuthService linkyAuthService;
|
||||||
|
private final String indexTemplate;
|
||||||
|
|
||||||
|
public LinkyAuthServlet(LinkyAuthService linkyAuthService, String indexTemplate) {
|
||||||
|
this.linkyAuthService = linkyAuthService;
|
||||||
|
this.indexTemplate = indexTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
logger.debug("Linky auth callback servlet received GET request {}.", req.getRequestURI());
|
||||||
|
final Map<String, String> replaceMap = new HashMap<>();
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* final String servletBaseURL = req.getRequestURL().toString();
|
||||||
|
* final String queryString = req.getQueryString();
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* String servletBaseURLSecure = servletBaseURL.replace("http://", "https://").replace("8080", "8443");
|
||||||
|
* handleSmartthingsRedirect(replaceMap, servletBaseURLSecure, queryString);
|
||||||
|
* resp.setContentType(CONTENT_TYPE);
|
||||||
|
* LinkyAccountHandler accountHandler = linkyAuthService.getLinkyAccountHandler();
|
||||||
|
*/
|
||||||
|
String uri = "https://mon-compte-particulier.enedis.fr/dataconnect/v1/oauth2/authorize?client_id=e551937c-5250-48bc-b4a6-2323af68db92&duration=P36M&response_type=code";
|
||||||
|
|
||||||
|
// replaceMap.put(KEY_REDIRECT_URI, servletBaseURLSecure);
|
||||||
|
replaceMap.put(KEY_BRIDGE_URI, uri);
|
||||||
|
resp.getWriter().append(replaceKeysFromMap(indexTemplate, replaceMap));
|
||||||
|
resp.getWriter().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces all keys from the map found in the template with values from the map. If the key is not found the key
|
||||||
|
* will be kept in the template.
|
||||||
|
*
|
||||||
|
* @param template template to replace keys with values
|
||||||
|
* @param map map with key value pairs to replace in the template
|
||||||
|
* @return a template with keys replaced
|
||||||
|
*/
|
||||||
|
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.debug("Error occurred during template filling, cause ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.appendTail(sb);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,9 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.linky.internal;
|
package org.openhab.binding.linky.internal;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.openhab.core.thing.ThingTypeUID;
|
import org.openhab.core.thing.ThingTypeUID;
|
||||||
|
|
||||||
@ -20,6 +23,7 @@ import org.openhab.core.thing.ThingTypeUID;
|
|||||||
* used across the whole binding.
|
* used across the whole binding.
|
||||||
*
|
*
|
||||||
* @author Gaël L'hopital - Initial contribution
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API *
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class LinkyBindingConstants {
|
public class LinkyBindingConstants {
|
||||||
@ -32,7 +36,7 @@ public class LinkyBindingConstants {
|
|||||||
// Thing properties
|
// Thing properties
|
||||||
public static final String PUISSANCE = "puissance";
|
public static final String PUISSANCE = "puissance";
|
||||||
public static final String PRM_ID = "prmId";
|
public static final String PRM_ID = "prmId";
|
||||||
public static final String USER_ID = "av2_interne_id";
|
public static final String USER_ID = "customerId";
|
||||||
|
|
||||||
// List of all Channel id's
|
// List of all Channel id's
|
||||||
public static final String YESTERDAY = "daily#yesterday";
|
public static final String YESTERDAY = "daily#yesterday";
|
||||||
@ -44,4 +48,21 @@ public class LinkyBindingConstants {
|
|||||||
public static final String LAST_MONTH = "monthly#lastMonth";
|
public static final String LAST_MONTH = "monthly#lastMonth";
|
||||||
public static final String THIS_YEAR = "yearly#thisYear";
|
public static final String THIS_YEAR = "yearly#thisYear";
|
||||||
public static final String LAST_YEAR = "yearly#lastYear";
|
public static final String LAST_YEAR = "yearly#lastYear";
|
||||||
|
|
||||||
|
// Authorization related Servlet and resources aliases.
|
||||||
|
public static final String LINKY_ALIAS = "/connectlinky";
|
||||||
|
public static final String LINKY_IMG_ALIAS = "/img";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smartthings scopes needed by this binding to work.
|
||||||
|
*/
|
||||||
|
public static final String LINKY_SCOPES = Stream.of("r:devices:*", "w:devices:*", "x:devices:*", "r:hubs:*",
|
||||||
|
"r:locations:*", "w:locations:*", "x:locations:*", "r:scenes:*", "x:scenes:*", "r:rules:*", "w:rules:*",
|
||||||
|
"r:installedapps", "w:installedapps").collect(Collectors.joining(" "));
|
||||||
|
|
||||||
|
// List of Spotify services related urls, information
|
||||||
|
public static final String LINKY_ACCOUNT_URL = "https://api.smartthings.com/oauth";
|
||||||
|
public static final String LINKY_AUTHORIZE_URL = LINKY_ACCOUNT_URL + "/authorize";
|
||||||
|
public static final String LINKY_API_TOKEN_URL = LINKY_ACCOUNT_URL + "/token";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -19,15 +19,14 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
|||||||
* thing configuration.
|
* thing configuration.
|
||||||
*
|
*
|
||||||
* @author Gaël L'hopital - Initial contribution
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class LinkyConfiguration {
|
public class LinkyConfiguration {
|
||||||
public static final String INTERNAL_AUTH_ID = "internalAuthId";
|
public String token = "";
|
||||||
public String username = "";
|
public String prmId = "";
|
||||||
public String password = "";
|
|
||||||
public String internalAuthId = "";
|
|
||||||
|
|
||||||
public boolean seemsValid() {
|
public boolean seemsValid() {
|
||||||
return !username.isBlank() && !password.isBlank() && !internalAuthId.isBlank();
|
return !token.isBlank() && !prmId.isBlank();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.linky.internal;
|
package org.openhab.binding.linky.internal;
|
||||||
|
|
||||||
import static org.openhab.binding.linky.internal.LinkyBindingConstants.THING_TYPE_LINKY;
|
|
||||||
|
|
||||||
import java.security.KeyManagementException;
|
import java.security.KeyManagementException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
@ -27,6 +25,7 @@ import org.eclipse.jdt.annotation.Nullable;
|
|||||||
import org.eclipse.jetty.client.HttpClient;
|
import org.eclipse.jetty.client.HttpClient;
|
||||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||||
import org.openhab.binding.linky.internal.handler.LinkyHandler;
|
import org.openhab.binding.linky.internal.handler.LinkyHandler;
|
||||||
|
import org.openhab.core.auth.client.oauth2.OAuthFactory;
|
||||||
import org.openhab.core.i18n.LocaleProvider;
|
import org.openhab.core.i18n.LocaleProvider;
|
||||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||||
import org.openhab.core.io.net.http.TrustAllTrustManager;
|
import org.openhab.core.io.net.http.TrustAllTrustManager;
|
||||||
@ -50,10 +49,11 @@ import com.google.gson.JsonDeserializer;
|
|||||||
* The {@link LinkyHandlerFactory} is responsible for creating things handlers.
|
* The {@link LinkyHandlerFactory} is responsible for creating things handlers.
|
||||||
*
|
*
|
||||||
* @author Gaël L'hopital - Initial contribution
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.linky")
|
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.linky")
|
||||||
public class LinkyHandlerFactory extends BaseThingHandlerFactory {
|
public class LinkyHandlerFactory extends BaseThingHandlerFactory implements LinkyAccountHandler {
|
||||||
private static final DateTimeFormatter LINKY_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSX");
|
private static final DateTimeFormatter LINKY_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSX");
|
||||||
private static final int REQUEST_BUFFER_SIZE = 8000;
|
private static final int REQUEST_BUFFER_SIZE = 8000;
|
||||||
private static final int RESPONSE_BUFFER_SIZE = 200000;
|
private static final int RESPONSE_BUFFER_SIZE = 200000;
|
||||||
@ -65,10 +65,13 @@ public class LinkyHandlerFactory extends BaseThingHandlerFactory {
|
|||||||
.create();
|
.create();
|
||||||
private final LocaleProvider localeProvider;
|
private final LocaleProvider localeProvider;
|
||||||
private final HttpClient httpClient;
|
private final HttpClient httpClient;
|
||||||
|
private final OAuthFactory oAuthFactory;
|
||||||
|
private final LinkyAuthService authService;
|
||||||
|
|
||||||
@Activate
|
@Activate
|
||||||
public LinkyHandlerFactory(final @Reference LocaleProvider localeProvider,
|
public LinkyHandlerFactory(final @Reference LocaleProvider localeProvider,
|
||||||
final @Reference HttpClientFactory httpClientFactory) {
|
final @Reference HttpClientFactory httpClientFactory, final @Reference LinkyAuthService authService,
|
||||||
|
final @Reference OAuthFactory oAuthFactory) {
|
||||||
this.localeProvider = localeProvider;
|
this.localeProvider = localeProvider;
|
||||||
SslContextFactory sslContextFactory = new SslContextFactory.Client();
|
SslContextFactory sslContextFactory = new SslContextFactory.Client();
|
||||||
try {
|
try {
|
||||||
@ -85,6 +88,8 @@ public class LinkyHandlerFactory extends BaseThingHandlerFactory {
|
|||||||
httpClient.setFollowRedirects(false);
|
httpClient.setFollowRedirects(false);
|
||||||
httpClient.setRequestBufferSize(REQUEST_BUFFER_SIZE);
|
httpClient.setRequestBufferSize(REQUEST_BUFFER_SIZE);
|
||||||
httpClient.setResponseBufferSize(RESPONSE_BUFFER_SIZE);
|
httpClient.setResponseBufferSize(RESPONSE_BUFFER_SIZE);
|
||||||
|
this.oAuthFactory = oAuthFactory;
|
||||||
|
this.authService = authService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -109,12 +114,32 @@ public class LinkyHandlerFactory extends BaseThingHandlerFactory {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||||
return THING_TYPE_LINKY.equals(thingTypeUID);
|
return LinkyBindingConstants.THING_TYPE_LINKY.equals(thingTypeUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||||
return supportsThingType(thing.getThingTypeUID()) ? new LinkyHandler(thing, localeProvider, gson, httpClient)
|
|
||||||
|
authService.setLinkyAccountHandler(this);
|
||||||
|
|
||||||
|
return supportsThingType(thing.getThingTypeUID())
|
||||||
|
? new LinkyHandler(thing, localeProvider, gson, httpClient, oAuthFactory)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAuthorized() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String authorize(String redirectUrl, String reqCode) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String formatAuthorizationUrl(String redirectUri) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.linky.internal.api;
|
package org.openhab.binding.linky.internal.api;
|
||||||
|
|
||||||
import java.net.HttpCookie;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -22,8 +20,6 @@ import java.util.Objects;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
|
||||||
@ -42,13 +38,17 @@ import org.jsoup.nodes.Document;
|
|||||||
import org.jsoup.nodes.Element;
|
import org.jsoup.nodes.Element;
|
||||||
import org.openhab.binding.linky.internal.LinkyConfiguration;
|
import org.openhab.binding.linky.internal.LinkyConfiguration;
|
||||||
import org.openhab.binding.linky.internal.LinkyException;
|
import org.openhab.binding.linky.internal.LinkyException;
|
||||||
import org.openhab.binding.linky.internal.dto.AuthData;
|
import org.openhab.binding.linky.internal.dto.AddressInfo;
|
||||||
import org.openhab.binding.linky.internal.dto.AuthResult;
|
|
||||||
import org.openhab.binding.linky.internal.dto.ConsumptionReport;
|
import org.openhab.binding.linky.internal.dto.ConsumptionReport;
|
||||||
import org.openhab.binding.linky.internal.dto.ConsumptionReport.Consumption;
|
import org.openhab.binding.linky.internal.dto.ConsumptionReport.Consumption;
|
||||||
import org.openhab.binding.linky.internal.dto.PrmDetail;
|
import org.openhab.binding.linky.internal.dto.ContactInfo;
|
||||||
|
import org.openhab.binding.linky.internal.dto.Customer;
|
||||||
|
import org.openhab.binding.linky.internal.dto.CustomerIdResponse;
|
||||||
|
import org.openhab.binding.linky.internal.dto.CustomerReponse;
|
||||||
|
import org.openhab.binding.linky.internal.dto.IdentityInfo;
|
||||||
|
import org.openhab.binding.linky.internal.dto.MeterResponse;
|
||||||
import org.openhab.binding.linky.internal.dto.PrmInfo;
|
import org.openhab.binding.linky.internal.dto.PrmInfo;
|
||||||
import org.openhab.binding.linky.internal.dto.UserInfo;
|
import org.openhab.binding.linky.internal.dto.UsagePoint;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -59,23 +59,18 @@ import com.google.gson.JsonSyntaxException;
|
|||||||
* {@link EnedisHttpApi} wraps the Enedis Webservice.
|
* {@link EnedisHttpApi} wraps the Enedis Webservice.
|
||||||
*
|
*
|
||||||
* @author Gaël L'hopital - Initial contribution
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class EnedisHttpApi {
|
public class EnedisHttpApi {
|
||||||
private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
private static final String ENEDIS_DOMAIN = ".enedis.fr";
|
|
||||||
private static final String URL_APPS_LINCS = "https://alex.microapplications" + ENEDIS_DOMAIN;
|
private static final String BASE_URL = "https://www.myelectricaldata.fr/";
|
||||||
private static final String URL_MON_COMPTE = "https://mon-compte" + ENEDIS_DOMAIN;
|
private static final String CONTRACT_URL = BASE_URL + "contracts";
|
||||||
private static final String URL_COMPTE_PART = URL_MON_COMPTE.replace("compte", "compte-particulier");
|
private static final String IDENTITY_URL = BASE_URL + "identity";
|
||||||
private static final String URL_ENEDIS_AUTHENTICATE = URL_APPS_LINCS + "/authenticate?target=" + URL_COMPTE_PART;
|
private static final String CONTACT_URL = BASE_URL + "contact";
|
||||||
private static final String USER_INFO_CONTRACT_URL = URL_APPS_LINCS + "/mon-compte-client/api/private/v1/userinfos";
|
private static final String ADDRESS_URL = BASE_URL + "addresses";
|
||||||
private static final String USER_INFO_URL = URL_APPS_LINCS + "/userinfos";
|
private static final String MEASURE_URL = BASE_URL + "%s/%s/start/%s/end/%s/cache";
|
||||||
private static final String PRM_INFO_BASE_URL = URL_APPS_LINCS + "/mes-mesures/api/private/v1/personnes/";
|
|
||||||
private static final String PRM_INFO_URL = URL_APPS_LINCS + "/mes-prms-part/api/private/v2/personnes/%s/prms";
|
|
||||||
private static final String MEASURE_URL = PRM_INFO_BASE_URL
|
|
||||||
+ "%s/prms/%s/donnees-%s?dateDebut=%s&dateFin=%s&mesuretypecode=CONS";
|
|
||||||
private static final URI COOKIE_URI = URI.create(URL_COMPTE_PART);
|
|
||||||
private static final Pattern REQ_PATTERN = Pattern.compile("ReqID%(.*?)%26");
|
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(EnedisHttpApi.class);
|
private final Logger logger = LoggerFactory.getLogger(EnedisHttpApi.class);
|
||||||
private final Gson gson;
|
private final Gson gson;
|
||||||
@ -91,108 +86,6 @@ public class EnedisHttpApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void initialize() throws LinkyException {
|
public void initialize() throws LinkyException {
|
||||||
logger.debug("Starting login process for user: {}", config.username);
|
|
||||||
|
|
||||||
try {
|
|
||||||
addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId);
|
|
||||||
logger.debug("Step 1: getting authentification");
|
|
||||||
String data = getContent(URL_ENEDIS_AUTHENTICATE);
|
|
||||||
|
|
||||||
logger.debug("Reception request SAML");
|
|
||||||
Document htmlDocument = Jsoup.parse(data);
|
|
||||||
Element el = htmlDocument.select("form").first();
|
|
||||||
Element samlInput = el.select("input[name=SAMLRequest]").first();
|
|
||||||
|
|
||||||
logger.debug("Step 2: send SSO SAMLRequest");
|
|
||||||
ContentResponse result = httpClient.POST(el.attr("action"))
|
|
||||||
.content(getFormContent("SAMLRequest", samlInput.attr("value"))).send();
|
|
||||||
if (result.getStatus() != HttpStatus.FOUND_302) {
|
|
||||||
throw new LinkyException("Connection failed step 2");
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug("Get the location and the ReqID");
|
|
||||||
Matcher m = REQ_PATTERN.matcher(getLocation(result));
|
|
||||||
if (!m.find()) {
|
|
||||||
throw new LinkyException("Unable to locate ReqId in header");
|
|
||||||
}
|
|
||||||
|
|
||||||
String reqId = m.group(1);
|
|
||||||
String authenticateUrl = URL_MON_COMPTE
|
|
||||||
+ "/auth/json/authenticate?realm=/enedis&forward=true&spEntityID=SP-ODW-PROD&goto=/auth/SSOPOST/metaAlias/enedis/providerIDP?ReqID%"
|
|
||||||
+ reqId + "%26index%3Dnull%26acsURL%3D" + URL_APPS_LINCS
|
|
||||||
+ "/saml/SSO%26spEntityID%3DSP-ODW-PROD%26binding%3Durn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST&AMAuthCookie=";
|
|
||||||
|
|
||||||
logger.debug("Step 3: auth1 - retrieve the template, thanks to cookie internalAuthId user is already set");
|
|
||||||
result = httpClient.POST(authenticateUrl).header("X-NoSession", "true").header("X-Password", "anonymous")
|
|
||||||
.header("X-Requested-With", "XMLHttpRequest").header("X-Username", "anonymous").send();
|
|
||||||
if (result.getStatus() != HttpStatus.OK_200) {
|
|
||||||
throw new LinkyException("Connection failed step 3 - auth1: %s", result.getContentAsString());
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthData authData = gson.fromJson(result.getContentAsString(), AuthData.class);
|
|
||||||
if (authData == null || authData.callbacks.size() < 2 || authData.callbacks.get(0).input.isEmpty()
|
|
||||||
|| authData.callbacks.get(1).input.isEmpty() || !config.username
|
|
||||||
.equals(Objects.requireNonNull(authData.callbacks.get(0).input.get(0)).valueAsString())) {
|
|
||||||
logger.debug("auth1 - invalid template for auth data: {}", result.getContentAsString());
|
|
||||||
throw new LinkyException("Authentication error, the authentication_cookie is probably wrong");
|
|
||||||
}
|
|
||||||
|
|
||||||
authData.callbacks.get(1).input.get(0).value = config.password;
|
|
||||||
logger.debug("Step 4: auth2 - send the auth data");
|
|
||||||
result = httpClient.POST(authenticateUrl).header(HttpHeader.CONTENT_TYPE, MediaType.APPLICATION_JSON)
|
|
||||||
.header("X-NoSession", "true").header("X-Password", "anonymous")
|
|
||||||
.header("X-Requested-With", "XMLHttpRequest").header("X-Username", "anonymous")
|
|
||||||
.content(new StringContentProvider(gson.toJson(authData))).send();
|
|
||||||
if (result.getStatus() != HttpStatus.OK_200) {
|
|
||||||
throw new LinkyException("Connection failed step 3 - auth2: %s", result.getContentAsString());
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthResult authResult = gson.fromJson(result.getContentAsString(), AuthResult.class);
|
|
||||||
if (authResult == null) {
|
|
||||||
throw new LinkyException("Invalid authentication result data");
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug("Add the tokenId cookie");
|
|
||||||
addCookie("enedisExt", authResult.tokenId);
|
|
||||||
|
|
||||||
logger.debug("Step 5: retrieve the SAMLresponse");
|
|
||||||
data = getContent(URL_MON_COMPTE + "/" + authResult.successUrl);
|
|
||||||
htmlDocument = Jsoup.parse(data);
|
|
||||||
el = htmlDocument.select("form").first();
|
|
||||||
samlInput = el.select("input[name=SAMLResponse]").first();
|
|
||||||
|
|
||||||
logger.debug("Step 6: post the SAMLresponse to finish the authentication");
|
|
||||||
result = httpClient.POST(el.attr("action")).content(getFormContent("SAMLResponse", samlInput.attr("value")))
|
|
||||||
.send();
|
|
||||||
if (result.getStatus() != HttpStatus.FOUND_302) {
|
|
||||||
throw new LinkyException("Connection failed step 6");
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug("Step 7: retrieve cookieKey");
|
|
||||||
result = httpClient.GET(USER_INFO_CONTRACT_URL);
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
HashMap<String, String> hashRes = gson.fromJson(result.getContentAsString(), HashMap.class);
|
|
||||||
|
|
||||||
String cookieKey;
|
|
||||||
if (hashRes != null && hashRes.containsKey("cnAlex")) {
|
|
||||||
cookieKey = "personne_for_" + hashRes.get("cnAlex");
|
|
||||||
} else {
|
|
||||||
throw new LinkyException("Connection failed step 7, missing cookieKey");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<HttpCookie> lCookie = httpClient.getCookieStore().getCookies();
|
|
||||||
Optional<HttpCookie> cookie = lCookie.stream().filter(it -> it.getName().contains(cookieKey)).findFirst();
|
|
||||||
|
|
||||||
String cookieVal = cookie.map(HttpCookie::getValue)
|
|
||||||
.orElseThrow(() -> new LinkyException("Connection failed step 7, missing cookieVal"));
|
|
||||||
|
|
||||||
addCookie(cookieKey, cookieVal);
|
|
||||||
|
|
||||||
connected = true;
|
|
||||||
} catch (InterruptedException | TimeoutException | ExecutionException | JsonSyntaxException e) {
|
|
||||||
throw new LinkyException(e, "Error opening connection with Enedis webservice");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getLocation(ContentResponse response) {
|
private String getLocation(ContentResponse response) {
|
||||||
@ -203,14 +96,6 @@ public class EnedisHttpApi {
|
|||||||
if (connected) {
|
if (connected) {
|
||||||
logger.debug("Logout process");
|
logger.debug("Logout process");
|
||||||
connected = false;
|
connected = false;
|
||||||
try { // Three times in a row to get disconnected
|
|
||||||
String location = getLocation(httpClient.GET(URL_APPS_LINCS + "/logout"));
|
|
||||||
location = getLocation(httpClient.GET(location));
|
|
||||||
getLocation(httpClient.GET(location));
|
|
||||||
httpClient.getCookieStore().removeAll();
|
|
||||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
|
||||||
throw new LinkyException(e, "Error while disconnecting from Enedis webservice");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,27 +107,15 @@ public class EnedisHttpApi {
|
|||||||
disconnect();
|
disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addCookie(String key, String value) {
|
private String getData(String url) throws LinkyException {
|
||||||
HttpCookie cookie = new HttpCookie(key, value);
|
|
||||||
cookie.setDomain(ENEDIS_DOMAIN);
|
|
||||||
cookie.setPath("/");
|
|
||||||
httpClient.getCookieStore().add(COOKIE_URI, cookie);
|
|
||||||
}
|
|
||||||
|
|
||||||
private FormContentProvider getFormContent(String fieldName, String fieldValue) {
|
|
||||||
Fields fields = new Fields();
|
|
||||||
fields.put(fieldName, fieldValue);
|
|
||||||
return new FormContentProvider(fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getContent(String url) throws LinkyException {
|
|
||||||
try {
|
try {
|
||||||
Request request = httpClient.newRequest(url)
|
Request request = httpClient.newRequest(url);
|
||||||
.agent("Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0");
|
|
||||||
request = request.method(HttpMethod.GET);
|
request = request.method(HttpMethod.GET);
|
||||||
|
request = request.header("Authorization", config.token);
|
||||||
|
|
||||||
ContentResponse result = request.send();
|
ContentResponse result = request.send();
|
||||||
if (result.getStatus() != HttpStatus.OK_200) {
|
if (result.getStatus() != 200) {
|
||||||
throw new LinkyException("Error requesting '%s': %s", url, result.getContentAsString());
|
throw new LinkyException("Error requesting '%s' : %s", url, result.getContentAsString());
|
||||||
}
|
}
|
||||||
String content = result.getContentAsString();
|
String content = result.getContentAsString();
|
||||||
logger.trace("getContent returned {}", content);
|
logger.trace("getContent returned {}", content);
|
||||||
@ -252,54 +125,134 @@ public class EnedisHttpApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> T getData(String url, Class<T> clazz) throws LinkyException {
|
public PrmInfo getPrmInfo() throws LinkyException {
|
||||||
|
PrmInfo result = new PrmInfo();
|
||||||
|
Customer customer = getCustomer();
|
||||||
|
UsagePoint usagePoint = customer.usagePoints[0];
|
||||||
|
|
||||||
|
result.contractInfo = usagePoint.contracts;
|
||||||
|
result.usagePointInfo = usagePoint.usagePoint;
|
||||||
|
result.identityInfo = getIdentity();
|
||||||
|
result.addressInfo = getAddress();
|
||||||
|
result.contactInfo = getContact();
|
||||||
|
|
||||||
|
result.prmId = result.usagePointInfo.usagePointId;
|
||||||
|
result.customerId = customer.customerId;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Customer getCustomer() throws LinkyException {
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
initialize();
|
initialize();
|
||||||
}
|
}
|
||||||
String data = getContent(url);
|
String data = getData(String.format("%s/%s/cache", CONTRACT_URL, config.prmId));
|
||||||
if (data.isEmpty()) {
|
if (data.isEmpty()) {
|
||||||
throw new LinkyException("Requesting '%s' returned an empty response", url);
|
throw new LinkyException("Requesting '%s' returned an empty response", CONTRACT_URL);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return Objects.requireNonNull(gson.fromJson(data, clazz));
|
CustomerReponse cResponse = gson.fromJson(data, CustomerReponse.class);
|
||||||
|
if (cResponse == null) {
|
||||||
|
throw new LinkyException("Invalid customer data received");
|
||||||
|
}
|
||||||
|
return cResponse.customer;
|
||||||
} catch (JsonSyntaxException e) {
|
} catch (JsonSyntaxException e) {
|
||||||
logger.debug("Invalid JSON response not matching {}: {}", clazz.getName(), data);
|
logger.debug("invalid JSON response not matching PrmInfo[].class: {}", data);
|
||||||
throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", url);
|
throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", CONTRACT_URL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public PrmInfo getPrmInfo(String internId) throws LinkyException {
|
public AddressInfo getAddress() throws LinkyException {
|
||||||
String url = PRM_INFO_URL.formatted(internId);
|
if (!connected) {
|
||||||
PrmInfo[] prms = getData(url, PrmInfo[].class);
|
initialize();
|
||||||
if (prms.length < 1) {
|
}
|
||||||
throw new LinkyException("Invalid prms data received");
|
String data = getData(String.format("%s/%s/cache", ADDRESS_URL, "21454992660003"));
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
throw new LinkyException("Requesting '%s' returned an empty response", ADDRESS_URL);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
CustomerReponse cResponse = gson.fromJson(data, CustomerReponse.class);
|
||||||
|
if (cResponse == null) {
|
||||||
|
throw new LinkyException("Invalid customer data received");
|
||||||
|
}
|
||||||
|
return cResponse.customer.usagePoints[0].usagePoint.usagePointAddresses;
|
||||||
|
} catch (JsonSyntaxException e) {
|
||||||
|
logger.debug("invalid JSON response not matching PrmInfo[].class: {}", data);
|
||||||
|
throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", ADDRESS_URL);
|
||||||
}
|
}
|
||||||
return prms[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public PrmDetail getPrmDetails(String internId, String prmId) throws LinkyException {
|
public IdentityInfo getIdentity() throws LinkyException {
|
||||||
String url = PRM_INFO_URL.formatted(internId) + "/" + prmId
|
if (!connected) {
|
||||||
+ "?embed=SITALI&embed=SITCOM&embed=SITCON&embed=SYNCON";
|
initialize();
|
||||||
return getData(url, PrmDetail.class);
|
}
|
||||||
|
String data = getData(String.format("%s/%s/cache", IDENTITY_URL, "21454992660003"));
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
throw new LinkyException("Requesting '%s' returned an empty response", IDENTITY_URL);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
CustomerIdResponse iResponse = gson.fromJson(data, CustomerIdResponse.class);
|
||||||
|
if (iResponse == null) {
|
||||||
|
throw new LinkyException("Invalid customer data received");
|
||||||
|
}
|
||||||
|
return iResponse.identity.naturalPerson;
|
||||||
|
} catch (JsonSyntaxException e) {
|
||||||
|
logger.debug("invalid JSON response not matching PrmInfo[].class: {}", data);
|
||||||
|
throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", IDENTITY_URL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserInfo getUserInfo() throws LinkyException {
|
public ContactInfo getContact() throws LinkyException {
|
||||||
return getData(USER_INFO_URL, UserInfo.class);
|
if (!connected) {
|
||||||
|
initialize();
|
||||||
|
}
|
||||||
|
String data = getData(String.format("%s/%s/cache", CONTACT_URL, "21454992660003"));
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
throw new LinkyException("Requesting '%s' returned an empty response", CONTACT_URL);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
CustomerIdResponse cResponse = gson.fromJson(data, CustomerIdResponse.class);
|
||||||
|
if (cResponse == null) {
|
||||||
|
throw new LinkyException("Invalid customer data received");
|
||||||
|
}
|
||||||
|
return cResponse.contactData;
|
||||||
|
} catch (JsonSyntaxException e) {
|
||||||
|
logger.debug("invalid JSON response not matching PrmInfo[].class: {}", data);
|
||||||
|
throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", CONTACT_URL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Consumption getMeasures(String userId, String prmId, LocalDate from, LocalDate to, String request)
|
private Consumption getMeasures(String userId, String prmId, LocalDate from, LocalDate to, String request)
|
||||||
throws LinkyException {
|
throws LinkyException {
|
||||||
String url = String.format(MEASURE_URL, userId, prmId, request, from.format(API_DATE_FORMAT),
|
String dtStart = from.format(API_DATE_FORMAT);
|
||||||
to.format(API_DATE_FORMAT));
|
String dtEnd = to.format(API_DATE_FORMAT);
|
||||||
ConsumptionReport report = getData(url, ConsumptionReport.class);
|
|
||||||
return report.firstLevel.consumptions;
|
String url = String.format(MEASURE_URL, request, prmId, dtStart, dtEnd);
|
||||||
|
if (!connected) {
|
||||||
|
initialize();
|
||||||
|
}
|
||||||
|
String data = getData(url);
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
throw new LinkyException("Requesting '%s' returned an empty response", url);
|
||||||
|
}
|
||||||
|
logger.trace("getData returned {}", data);
|
||||||
|
try {
|
||||||
|
MeterResponse meterResponse = gson.fromJson(data, MeterResponse.class);
|
||||||
|
if (meterResponse == null) {
|
||||||
|
throw new LinkyException("No report data received");
|
||||||
|
}
|
||||||
|
return new ConsumptionReport().new Consumption();
|
||||||
|
} catch (JsonSyntaxException e) {
|
||||||
|
logger.debug("invalid JSON response not matching ConsumptionReport.class: {}", data);
|
||||||
|
throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Consumption getEnergyData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
|
public Consumption getEnergyData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
|
||||||
return getMeasures(userId, prmId, from, to, "energie");
|
return getMeasures(userId, prmId, from, to, "daily_consumption");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Consumption getPowerData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
|
public Consumption getPowerData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
|
||||||
return getMeasures(userId, prmId, from, to, "pmax");
|
return getMeasures(userId, prmId, from, to, "daily_consumption_max_power");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.linky.internal.dto;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.jaas.spi.UserInfo;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link UserInfo} holds informations about energy delivery point
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class AddressInfo {
|
||||||
|
public String street;
|
||||||
|
public String locality;
|
||||||
|
|
||||||
|
@SerializedName("postalCode")
|
||||||
|
public String postal_code;
|
||||||
|
|
||||||
|
@SerializedName("insee_code")
|
||||||
|
public String inseeCode;
|
||||||
|
|
||||||
|
public String city;
|
||||||
|
public String country;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.linky.internal.dto;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.jaas.spi.UserInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link UserInfo} holds informations about energy delivery point
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class ContactInfo {
|
||||||
|
public String phone;
|
||||||
|
public String email;
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.linky.internal.dto;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.jaas.spi.UserInfo;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link UserInfo} holds informations about energy delivery point
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class Contracts {
|
||||||
|
public String segment;
|
||||||
|
|
||||||
|
@SerializedName("subscribed_power")
|
||||||
|
public String subscribedPower;
|
||||||
|
|
||||||
|
@SerializedName("last_activation_date")
|
||||||
|
public String lastActivationDate;
|
||||||
|
|
||||||
|
@SerializedName("distribution_tariff")
|
||||||
|
public String distributionTariff;
|
||||||
|
|
||||||
|
@SerializedName("offpeak_hours")
|
||||||
|
public String offpeakHours;
|
||||||
|
|
||||||
|
@SerializedName("contract_status")
|
||||||
|
public String contractStatus;
|
||||||
|
|
||||||
|
@SerializedName("last_distribution_tariff_change_date")
|
||||||
|
public String lastDistributionTariffChangeDate;
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.linky.internal.dto;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.jaas.spi.UserInfo;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link UserInfo} holds informations about energy delivery point
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class Customer {
|
||||||
|
@SerializedName("customer_id")
|
||||||
|
public String customerId;
|
||||||
|
|
||||||
|
@SerializedName("usage_points")
|
||||||
|
public UsagePoint[] usagePoints;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.linky.internal.dto;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.jaas.spi.UserInfo;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link UserInfo} holds informations about energy delivery point
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class CustomerIdResponse {
|
||||||
|
@SerializedName("customer_id")
|
||||||
|
public String customerId;
|
||||||
|
|
||||||
|
public IdentityDetails identity;
|
||||||
|
|
||||||
|
@SerializedName("contact_data")
|
||||||
|
public ContactInfo contactData;
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.linky.internal.dto;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.jaas.spi.UserInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link UserInfo} holds informations about energy delivery point
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class CustomerReponse {
|
||||||
|
public Customer customer;
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.linky.internal.dto;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.jaas.spi.UserInfo;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link UserInfo} holds informations about energy delivery point
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class IdentityDetails {
|
||||||
|
@SerializedName("natural_person")
|
||||||
|
public IdentityInfo naturalPerson;
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.linky.internal.dto;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.jaas.spi.UserInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link UserInfo} holds informations about energy delivery point
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class IdentityInfo {
|
||||||
|
public String title;
|
||||||
|
public String firstname;
|
||||||
|
public String lastname;
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.linky.internal.dto;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.jaas.spi.UserInfo;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link UserInfo} holds informations about energy delivery point
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class IntervalReading {
|
||||||
|
@SerializedName("value")
|
||||||
|
public String value;
|
||||||
|
|
||||||
|
@SerializedName("date")
|
||||||
|
public String date;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.linky.internal.dto;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.jaas.spi.UserInfo;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link UserInfo} holds informations about energy delivery point
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class MeterReading {
|
||||||
|
@SerializedName("usage_point_id")
|
||||||
|
public String usagePointId;
|
||||||
|
|
||||||
|
@SerializedName("start")
|
||||||
|
public String startDate;
|
||||||
|
|
||||||
|
@SerializedName("end")
|
||||||
|
public String endDate;
|
||||||
|
|
||||||
|
@SerializedName("quality")
|
||||||
|
public String quality;
|
||||||
|
|
||||||
|
@SerializedName("reading_type")
|
||||||
|
public ReadingType readingType;
|
||||||
|
|
||||||
|
@SerializedName("interval_reading")
|
||||||
|
public IntervalReading[] intervalReading;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.linky.internal.dto;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.jaas.spi.UserInfo;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link UserInfo} holds informations about energy delivery point
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class MeterResponse {
|
||||||
|
@SerializedName("meter_reading")
|
||||||
|
public MeterReading meterReading;
|
||||||
|
|
||||||
|
}
|
@ -12,12 +12,22 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.linky.internal.dto;
|
package org.openhab.binding.linky.internal.dto;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.jaas.spi.UserInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link UserInfo} holds ids of existing Prms
|
* The {@link UserInfo} holds ids of existing Prms
|
||||||
*
|
*
|
||||||
* @author Gaël L'hopital - Initial contribution
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class PrmInfo {
|
public class PrmInfo {
|
||||||
public String idPrm;
|
public String prmId;
|
||||||
|
public String customerId;
|
||||||
|
|
||||||
|
public Contracts contractInfo;
|
||||||
|
public UsagePointDetails usagePointInfo;
|
||||||
|
public ContactInfo contactInfo;
|
||||||
|
public AddressInfo addressInfo;
|
||||||
|
public IdentityInfo identityInfo;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.linky.internal.dto;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.jaas.spi.UserInfo;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link UserInfo} holds informations about energy delivery point
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class ReadingType {
|
||||||
|
@SerializedName("measurement_kind")
|
||||||
|
public String measurementKind;
|
||||||
|
|
||||||
|
@SerializedName("measuring_period")
|
||||||
|
public String measuringPeriod;
|
||||||
|
|
||||||
|
@SerializedName("unit")
|
||||||
|
public String unit;
|
||||||
|
|
||||||
|
@SerializedName("aggregate")
|
||||||
|
public String aggregate;
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.linky.internal.dto;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.jaas.spi.UserInfo;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link UserInfo} holds informations about energy delivery point
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class UsagePoint {
|
||||||
|
@SerializedName("usage_point")
|
||||||
|
public UsagePointDetails usagePoint;
|
||||||
|
|
||||||
|
public Contracts contracts;
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.linky.internal.dto;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.jaas.spi.UserInfo;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link UserInfo} holds informations about energy delivery point
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class UsagePointDetails {
|
||||||
|
@SerializedName("usage_point_id")
|
||||||
|
public String usagePointId;
|
||||||
|
|
||||||
|
@SerializedName("usage_point_status")
|
||||||
|
public String usagePointStatus;
|
||||||
|
|
||||||
|
@SerializedName("meter_type")
|
||||||
|
public String meterType;
|
||||||
|
|
||||||
|
@SerializedName("usage_point_addresses")
|
||||||
|
public AddressInfo usagePointAddresses;
|
||||||
|
}
|
@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
import org.eclipse.jetty.client.HttpClient;
|
||||||
|
import org.openhab.binding.linky.internal.LinkyBindingConstants;
|
||||||
import org.openhab.binding.linky.internal.LinkyConfiguration;
|
import org.openhab.binding.linky.internal.LinkyConfiguration;
|
||||||
import org.openhab.binding.linky.internal.LinkyException;
|
import org.openhab.binding.linky.internal.LinkyException;
|
||||||
import org.openhab.binding.linky.internal.api.EnedisHttpApi;
|
import org.openhab.binding.linky.internal.api.EnedisHttpApi;
|
||||||
@ -37,7 +38,8 @@ import org.openhab.binding.linky.internal.dto.ConsumptionReport.Aggregate;
|
|||||||
import org.openhab.binding.linky.internal.dto.ConsumptionReport.Consumption;
|
import org.openhab.binding.linky.internal.dto.ConsumptionReport.Consumption;
|
||||||
import org.openhab.binding.linky.internal.dto.PrmDetail;
|
import org.openhab.binding.linky.internal.dto.PrmDetail;
|
||||||
import org.openhab.binding.linky.internal.dto.PrmInfo;
|
import org.openhab.binding.linky.internal.dto.PrmInfo;
|
||||||
import org.openhab.binding.linky.internal.dto.UserInfo;
|
import org.openhab.core.auth.client.oauth2.OAuthClientService;
|
||||||
|
import org.openhab.core.auth.client.oauth2.OAuthFactory;
|
||||||
import org.openhab.core.i18n.LocaleProvider;
|
import org.openhab.core.i18n.LocaleProvider;
|
||||||
import org.openhab.core.library.types.DateTimeType;
|
import org.openhab.core.library.types.DateTimeType;
|
||||||
import org.openhab.core.library.types.QuantityType;
|
import org.openhab.core.library.types.QuantityType;
|
||||||
@ -61,6 +63,7 @@ import com.google.gson.Gson;
|
|||||||
* sent to one of the channels.
|
* sent to one of the channels.
|
||||||
*
|
*
|
||||||
* @author Gaël L'hopital - Initial contribution
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
@ -81,6 +84,8 @@ public class LinkyHandler extends BaseThingHandler {
|
|||||||
private @Nullable ScheduledFuture<?> refreshJob;
|
private @Nullable ScheduledFuture<?> refreshJob;
|
||||||
private @Nullable EnedisHttpApi enedisApi;
|
private @Nullable EnedisHttpApi enedisApi;
|
||||||
|
|
||||||
|
private final OAuthFactory oAuthFactory;
|
||||||
|
|
||||||
private @NonNullByDefault({}) String prmId;
|
private @NonNullByDefault({}) String prmId;
|
||||||
private @NonNullByDefault({}) String userId;
|
private @NonNullByDefault({}) String userId;
|
||||||
|
|
||||||
@ -90,7 +95,10 @@ public class LinkyHandler extends BaseThingHandler {
|
|||||||
ALL
|
ALL
|
||||||
}
|
}
|
||||||
|
|
||||||
public LinkyHandler(Thing thing, LocaleProvider localeProvider, Gson gson, HttpClient httpClient) {
|
private @Nullable OAuthClientService oAuthService;
|
||||||
|
|
||||||
|
public LinkyHandler(Thing thing, LocaleProvider localeProvider, Gson gson, HttpClient httpClient,
|
||||||
|
OAuthFactory oAuthFactory) {
|
||||||
super(thing);
|
super(thing);
|
||||||
this.gson = gson;
|
this.gson = gson;
|
||||||
this.httpClient = httpClient;
|
this.httpClient = httpClient;
|
||||||
@ -107,6 +115,8 @@ public class LinkyHandler extends BaseThingHandler {
|
|||||||
return consumption;
|
return consumption;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.oAuthFactory = oAuthFactory;
|
||||||
|
|
||||||
this.cachedPowerData = new ExpiringDayCache<>("power cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
|
this.cachedPowerData = new ExpiringDayCache<>("power cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
|
||||||
// We request data for yesterday and the day before yesterday, even if the data for the day before yesterday
|
// We request data for yesterday and the day before yesterday, even if the data for the day before yesterday
|
||||||
// is not needed by the binding. This is only a workaround to an API bug that will return
|
// is not needed by the binding. This is only a workaround to an API bug that will return
|
||||||
@ -152,6 +162,11 @@ public class LinkyHandler extends BaseThingHandler {
|
|||||||
|
|
||||||
LinkyConfiguration config = getConfigAs(LinkyConfiguration.class);
|
LinkyConfiguration config = getConfigAs(LinkyConfiguration.class);
|
||||||
if (config.seemsValid()) {
|
if (config.seemsValid()) {
|
||||||
|
|
||||||
|
OAuthClientService oAuthService = oAuthFactory.createOAuthClientService(thing.getUID().getAsString(),
|
||||||
|
LinkyBindingConstants.LINKY_API_TOKEN_URL, LinkyBindingConstants.LINKY_AUTHORIZE_URL, "clientId",
|
||||||
|
"clientSecret", LinkyBindingConstants.LINKY_SCOPES, true);
|
||||||
|
|
||||||
enedisApi = new EnedisHttpApi(config, gson, httpClient);
|
enedisApi = new EnedisHttpApi(config, gson, httpClient);
|
||||||
scheduler.submit(() -> {
|
scheduler.submit(() -> {
|
||||||
try {
|
try {
|
||||||
@ -159,15 +174,9 @@ public class LinkyHandler extends BaseThingHandler {
|
|||||||
api.initialize();
|
api.initialize();
|
||||||
updateStatus(ThingStatus.ONLINE);
|
updateStatus(ThingStatus.ONLINE);
|
||||||
|
|
||||||
if (thing.getProperties().isEmpty()) {
|
PrmInfo prmInfo = api.getPrmInfo();
|
||||||
UserInfo userInfo = api.getUserInfo();
|
updateProperties(Map.of(USER_ID, prmInfo.customerId, PUISSANCE,
|
||||||
PrmInfo prmInfo = api.getPrmInfo(userInfo.userProperties.internId);
|
prmInfo.contractInfo.subscribedPower, PRM_ID, prmInfo.prmId));
|
||||||
PrmDetail details = api.getPrmDetails(userInfo.userProperties.internId, prmInfo.idPrm);
|
|
||||||
updateProperties(Map.of(USER_ID, userInfo.userProperties.internId, PUISSANCE,
|
|
||||||
details.situationContractuelleDtos[0].structureTarifaire().puissanceSouscrite().valeur()
|
|
||||||
+ " kVA",
|
|
||||||
PRM_ID, prmInfo.idPrm));
|
|
||||||
}
|
|
||||||
|
|
||||||
prmId = thing.getProperties().get(PRM_ID);
|
prmId = thing.getProperties().get(PRM_ID);
|
||||||
userId = thing.getProperties().get(USER_ID);
|
userId = thing.getProperties().get(USER_ID);
|
||||||
@ -544,4 +553,5 @@ public class LinkyHandler extends BaseThingHandler {
|
|||||||
aggregate.datas.get(index));
|
aggregate.datas.get(index));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,19 +20,13 @@
|
|||||||
</channel-groups>
|
</channel-groups>
|
||||||
|
|
||||||
<config-description>
|
<config-description>
|
||||||
<parameter name="username" type="text" required="true">
|
<parameter name="prmId" type="text" required="true">
|
||||||
<label>Username</label>
|
<label>PrmId</label>
|
||||||
<context>email</context>
|
<description>Your PrmId</description>
|
||||||
<description>Your Enedis Username</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="password" type="text" required="true">
|
<parameter name="token" type="text" required="true">
|
||||||
<label>Password</label>
|
<label>Token</label>
|
||||||
<context>password</context>
|
<description>Your Enedis token</description>
|
||||||
<description>Your Enedis Password</description>
|
|
||||||
</parameter>
|
|
||||||
<parameter name="internalAuthId" type="text" required="true">
|
|
||||||
<label>Auth ID</label>
|
|
||||||
<description>Authentication ID delivered after the captcha (see documentation).</description>
|
|
||||||
</parameter>
|
</parameter>
|
||||||
</config-description>
|
</config-description>
|
||||||
</thing-type>
|
</thing-type>
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
<!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 Smartthings</title>
|
||||||
|
<link rel="icon" href="img/favicon.ico" type="image/vnd.microsoft.icon">
|
||||||
|
<link>
|
||||||
|
<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>
|
||||||
|
<svg class="logo" xmlns="http://www.w3.org/2000/svg" height="60px" version="1.1" viewBox="0 0 530 180">
|
||||||
|
<path style="stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1;" d="m 195,90 l 15,10 c 0,0 -5,-5 0,-20 z m 10,3 l 120,0 l 0,-6 l -120,0 z m 130,-3 l -15,-10 c 0,0 5,5 0,20 z" />
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<g transform="matrix(1.0671711,0,0,1.0802403,+350,+3.638968)">
|
||||||
|
<svg fill="#15BFFF" width="150px" height="150px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>SmartThings icon</title>
|
||||||
|
<path d="M11.51 0C8.338 0 5.034.537 2.694 2.694.5 5.174 0 8.464 0 11.525v.942c0 3.165.537 6.499 2.694 8.84C5.188 23.513 8.494 24 11.569 24h.854c3.18 0 6.528-.53 8.883-2.694C23.514 18.811 24 15.5 24 12.423v-.853c0-3.18-.53-6.528-2.694-8.876C18.826.494 15.544 0 12.482 0zM12 3.505c1.244 0 2.256.99 2.256 2.206 0 1.065-.685 1.976-1.715 2.181v1.59c1.48.214 2.528 1.43 2.528 2.934 0 1.654-1.377 3-3.07 3-1.692 0-3.068-1.346-3.068-3 0-.17.017-.335.045-.497l-1.536-.488a2.258 2.258 0 01-1.962 1.12c-.237 0-.471-.037-.698-.11-1.183-.375-1.833-1.622-1.449-2.78a2.246 2.246 0 012.146-1.524c.237 0 .471.036.698.108a2.23 2.23 0 011.313 1.098c.204.391.282.823.232 1.249l1.535.488c.44-.86 1.378-1.453 2.384-1.599V7.892c-1.029-.205-1.896-1.116-1.896-2.181 0-1.217 1.012-2.206 2.257-2.206zm0 .882c-.747 0-1.354.594-1.354 1.324 0 .73.607 1.324 1.354 1.324.746 0 1.354-.594 1.354-1.324 0-.73-.608-1.324-1.354-1.324zm6.522 3.75c.98 0 1.843.613 2.146 1.525.186.56.138 1.158-.135 1.683-.274.526-.74.915-1.314 1.096-.227.073-.461.11-.698.11a2.258 2.258 0 01-1.962-1.12l-.634.201-.278-.838.632-.202a2.21 2.21 0 011.546-2.347c.226-.072.46-.108.697-.108zM5.476 9.02c-.588 0-1.105.368-1.287.915-.23.694.159 1.442.869 1.668.136.043.277.065.419.065.588 0 1.105-.368 1.287-.915a1.29 1.29 0 00-.081-1.01 1.338 1.338 0 00-.788-.658 1.377 1.377 0 00-.42-.065zm13.045 0c-.142 0-.282.021-.419.065a1.32 1.32 0 00-.869 1.668c.182.547.7.915 1.287.915.142 0 .283-.022.42-.065.344-.11.623-.343.787-.659.165-.315.193-.673.082-1.009a1.348 1.348 0 00-1.288-.915zM12 10.474c-1.095 0-1.986.871-1.986 1.942 0 1.07.89 1.941 1.986 1.941 1.094 0 1.985-.87 1.985-1.94 0-1.072-.89-1.943-1.985-1.943zm-2.706 4.831l.73.519-.39.526c.709.757.801 1.925.16 2.787-.423.57-1.106.91-1.827.91-.478 0-.937-.147-1.325-.422a2.177 2.177 0 01-.499-3.082 2.28 2.28 0 012.76-.71zm5.41 0l.392.528a2.285 2.285 0 012.76.71 2.178 2.178 0 01-.499 3.082 2.275 2.275 0 01-1.325.421 2.28 2.28 0 01-1.827-.91 2.172 2.172 0 01.16-2.785l-.39-.527zm-6.734 1.21c-.433 0-.843.205-1.097.547-.44.59-.304 1.42.3 1.849a1.37 1.37 0 001.891-.293c.44-.59.305-1.42-.3-1.85a1.364 1.364 0 00-.794-.252zm8.059 0c-.287 0-.561.088-.795.254a1.307 1.307 0 00-.299 1.849 1.371 1.371 0 001.891.293 1.307 1.307 0 00.3-1.85 1.37 1.37 0 00-1.097-.545Z"/>
|
||||||
|
</svg>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.0671711,0,0,1.0802403,-242.07004,-3.638968)">
|
||||||
|
<path d="m 235.55996,122.32139 65.99684,-65.245439 9.61127,-9.494429 9.61126,9.494429 48.71786,48.127399 -0.0713,0.24287 -0.96104,2.79606 -1.09061,2.7291 -1.22617,2.66469 -1.34976,2.59982 -1.22704,2.10395 -52.40328,-51.76861 c -22.86589,22.605906 -45.73092,45.21138 -68.59595,67.81602 -2.64028,-3.84168 -5.09265,-7.79865 -7.01216,-12.06586 z" style="clip-rule:evenodd;fill:#e64a19;fill-rule:evenodd;stroke-width:0.87332809" />
|
||||||
|
<path d="m 311.16893,3.3686968 c 46.45255,0 84.33469,37.4250352 84.33469,83.3154142 0,45.887409 -37.88214,83.314139 -84.33469,83.314139 -25.37318,0 -48.18501,-11.17665 -63.66633,-28.79057 l 2.98008,-2.95501 2.16319,-2.13785 2.16749,-2.14377 2.16835,-2.14209 0.0884,-0.0865 c 13.00665,15.21415 32.43854,24.9082 54.09884,24.9082 39.00964,0 70.82522,-31.42855 70.82522,-69.966579 0,-38.540154 -31.81558,-69.969554 -70.82522,-69.969554 -39.01137,0 -70.82694,31.4294 -70.82694,69.969554 0,5.189918 0.58434,10.24717 1.67968,15.121749 l -4.69923,4.65206 -6.27207,6.2012 c -2.73168,-8.18222 -4.217,-16.909931 -4.217,-25.97501 0,-45.89038 37.88214,-83.3154145 84.33556,-83.3154145 z" style="clip-rule:evenodd;fill:#474747;fill-rule:evenodd;stroke-width:0.87332809" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<h3>Authorize openHAB binding for Smartthings</h3>
|
||||||
|
<p>On this page you can authorize the openHAB Smartthings biding to access your Smartthings account.</p>
|
||||||
|
<p>
|
||||||
|
The redirect URI to use with Smartthings for this openHAB installation is
|
||||||
|
<a href="${redirectUri}">${redirectUri}</a>
|
||||||
|
</p>
|
||||||
|
${error} ${authorizedUser}
|
||||||
|
|
||||||
|
<div class="block${bridge.authorized}">
|
||||||
|
Connect to SmartThings:
|
||||||
|
<p>
|
||||||
|
<div class="button">
|
||||||
|
<a href=${bridge.uri}>Authorize Bridge
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user