diff --git a/bundles/org.openhab.binding.linky/src/main/feature/feature.xml b/bundles/org.openhab.binding.linky/src/main/feature/feature.xml index 41dad925440..7e83306ff37 100644 --- a/bundles/org.openhab.binding.linky/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.linky/src/main/feature/feature.xml @@ -4,7 +4,9 @@ openhab-runtime-base + openhab-core-auth-oauth2client mvn:org.jsoup/jsoup/1.14.3 mvn:org.openhab.addons.bundles/org.openhab.binding.linky/${project.version} + diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyAccountHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyAccountHandler.java new file mode 100644 index 00000000000..1cebc3e7b5e --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyAccountHandler.java @@ -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); +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyAuthService.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyAuthService.java new file mode 100644 index 00000000000..9f6bed55693 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyAuthService.java @@ -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 handlers = new ArrayList<>(); + + private @NonNullByDefault({}) HttpService httpService; + private @NonNullByDefault({}) BundleContext bundleContext; + private @Nullable LinkyAccountHandler accountHandler; + + @Activate + protected void activate(ComponentContext componentContext, Map 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 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 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; + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyAuthServlet.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyAuthServlet.java new file mode 100644 index 00000000000..f659de06d36 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyAuthServlet.java @@ -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 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 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(); + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyBindingConstants.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyBindingConstants.java index 9ceacb91fb1..119926c61e3 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyBindingConstants.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyBindingConstants.java @@ -12,6 +12,9 @@ */ package org.openhab.binding.linky.internal; +import java.util.stream.Collectors; +import java.util.stream.Stream; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.thing.ThingTypeUID; @@ -20,6 +23,7 @@ import org.openhab.core.thing.ThingTypeUID; * used across the whole binding. * * @author Gaël L'hopital - Initial contribution + * @author Laurent Arnal - Rewrite addon to use official dataconect API * */ @NonNullByDefault public class LinkyBindingConstants { @@ -32,7 +36,7 @@ public class LinkyBindingConstants { // Thing properties public static final String PUISSANCE = "puissance"; 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 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 THIS_YEAR = "yearly#thisYear"; 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"; + } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyConfiguration.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyConfiguration.java index 3c75363fd83..6bfa0f6403b 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyConfiguration.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyConfiguration.java @@ -19,15 +19,14 @@ import org.eclipse.jdt.annotation.NonNullByDefault; * thing configuration. * * @author Gaël L'hopital - Initial contribution + * @author Laurent Arnal - Rewrite addon to use official dataconect API */ @NonNullByDefault public class LinkyConfiguration { - public static final String INTERNAL_AUTH_ID = "internalAuthId"; - public String username = ""; - public String password = ""; - public String internalAuthId = ""; + public String token = ""; + public String prmId = ""; public boolean seemsValid() { - return !username.isBlank() && !password.isBlank() && !internalAuthId.isBlank(); + return !token.isBlank() && !prmId.isBlank(); } } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java index 315ed268c12..91882a07146 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java @@ -12,8 +12,6 @@ */ package org.openhab.binding.linky.internal; -import static org.openhab.binding.linky.internal.LinkyBindingConstants.THING_TYPE_LINKY; - import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.time.ZonedDateTime; @@ -27,6 +25,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.util.ssl.SslContextFactory; 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.io.net.http.HttpClientFactory; 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. * * @author Gaël L'hopital - Initial contribution + * @author Laurent Arnal - Rewrite addon to use official dataconect API */ @NonNullByDefault @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 int REQUEST_BUFFER_SIZE = 8000; private static final int RESPONSE_BUFFER_SIZE = 200000; @@ -65,10 +65,13 @@ public class LinkyHandlerFactory extends BaseThingHandlerFactory { .create(); private final LocaleProvider localeProvider; private final HttpClient httpClient; + private final OAuthFactory oAuthFactory; + private final LinkyAuthService authService; @Activate 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; SslContextFactory sslContextFactory = new SslContextFactory.Client(); try { @@ -85,6 +88,8 @@ public class LinkyHandlerFactory extends BaseThingHandlerFactory { httpClient.setFollowRedirects(false); httpClient.setRequestBufferSize(REQUEST_BUFFER_SIZE); httpClient.setResponseBufferSize(RESPONSE_BUFFER_SIZE); + this.oAuthFactory = oAuthFactory; + this.authService = authService; } @Override @@ -109,12 +114,32 @@ public class LinkyHandlerFactory extends BaseThingHandlerFactory { @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { - return THING_TYPE_LINKY.equals(thingTypeUID); + return LinkyBindingConstants.THING_TYPE_LINKY.equals(thingTypeUID); } @Override 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; } + + @Override + public boolean isAuthorized() { + return true; + } + + @Override + public String authorize(String redirectUrl, String reqCode) { + return ""; + } + + @Override + public String formatAuthorizationUrl(String redirectUri) { + return ""; + } + } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java index 2c2a755d3ba..fbe2404d647 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java @@ -12,8 +12,6 @@ */ package org.openhab.binding.linky.internal.api; -import java.net.HttpCookie; -import java.net.URI; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.HashMap; @@ -22,8 +20,6 @@ import java.util.Objects; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import javax.ws.rs.core.MediaType; @@ -42,13 +38,17 @@ import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.openhab.binding.linky.internal.LinkyConfiguration; import org.openhab.binding.linky.internal.LinkyException; -import org.openhab.binding.linky.internal.dto.AuthData; -import org.openhab.binding.linky.internal.dto.AuthResult; +import org.openhab.binding.linky.internal.dto.AddressInfo; import org.openhab.binding.linky.internal.dto.ConsumptionReport; 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.UserInfo; +import org.openhab.binding.linky.internal.dto.UsagePoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,23 +59,18 @@ import com.google.gson.JsonSyntaxException; * {@link EnedisHttpApi} wraps the Enedis Webservice. * * @author Gaël L'hopital - Initial contribution + * @author Laurent Arnal - Rewrite addon to use official dataconect API */ @NonNullByDefault public class EnedisHttpApi { - private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy"); - private static final String ENEDIS_DOMAIN = ".enedis.fr"; - private static final String URL_APPS_LINCS = "https://alex.microapplications" + ENEDIS_DOMAIN; - private static final String URL_MON_COMPTE = "https://mon-compte" + ENEDIS_DOMAIN; - private static final String URL_COMPTE_PART = URL_MON_COMPTE.replace("compte", "compte-particulier"); - private static final String URL_ENEDIS_AUTHENTICATE = URL_APPS_LINCS + "/authenticate?target=" + URL_COMPTE_PART; - private static final String USER_INFO_CONTRACT_URL = URL_APPS_LINCS + "/mon-compte-client/api/private/v1/userinfos"; - private static final String USER_INFO_URL = URL_APPS_LINCS + "/userinfos"; - 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 static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + private static final String BASE_URL = "https://www.myelectricaldata.fr/"; + private static final String CONTRACT_URL = BASE_URL + "contracts"; + private static final String IDENTITY_URL = BASE_URL + "identity"; + private static final String CONTACT_URL = BASE_URL + "contact"; + private static final String ADDRESS_URL = BASE_URL + "addresses"; + private static final String MEASURE_URL = BASE_URL + "%s/%s/start/%s/end/%s/cache"; private final Logger logger = LoggerFactory.getLogger(EnedisHttpApi.class); private final Gson gson; @@ -91,108 +86,6 @@ public class EnedisHttpApi { } 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 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 lCookie = httpClient.getCookieStore().getCookies(); - Optional 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) { @@ -203,14 +96,6 @@ public class EnedisHttpApi { if (connected) { logger.debug("Logout process"); 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(); } - private void addCookie(String key, String value) { - 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 { + private String getData(String url) throws LinkyException { try { - 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 = httpClient.newRequest(url); request = request.method(HttpMethod.GET); + request = request.header("Authorization", config.token); + ContentResponse result = request.send(); - if (result.getStatus() != HttpStatus.OK_200) { - throw new LinkyException("Error requesting '%s': %s", url, result.getContentAsString()); + if (result.getStatus() != 200) { + throw new LinkyException("Error requesting '%s' : %s", url, result.getContentAsString()); } String content = result.getContentAsString(); logger.trace("getContent returned {}", content); @@ -252,54 +125,134 @@ public class EnedisHttpApi { } } - private T getData(String url, Class 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) { initialize(); } - String data = getContent(url); + String data = getData(String.format("%s/%s/cache", CONTRACT_URL, config.prmId)); 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 { - 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) { - logger.debug("Invalid JSON response not matching {}: {}", clazz.getName(), data); - throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", url); + logger.debug("invalid JSON response not matching PrmInfo[].class: {}", data); + throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", CONTRACT_URL); } } - public PrmInfo getPrmInfo(String internId) throws LinkyException { - String url = PRM_INFO_URL.formatted(internId); - PrmInfo[] prms = getData(url, PrmInfo[].class); - if (prms.length < 1) { - throw new LinkyException("Invalid prms data received"); + public AddressInfo getAddress() throws LinkyException { + if (!connected) { + initialize(); + } + 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 { - String url = PRM_INFO_URL.formatted(internId) + "/" + prmId - + "?embed=SITALI&embed=SITCOM&embed=SITCON&embed=SYNCON"; - return getData(url, PrmDetail.class); + public IdentityInfo getIdentity() throws LinkyException { + if (!connected) { + initialize(); + } + 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 { - return getData(USER_INFO_URL, UserInfo.class); + public ContactInfo getContact() throws LinkyException { + 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) throws LinkyException { - String url = String.format(MEASURE_URL, userId, prmId, request, from.format(API_DATE_FORMAT), - to.format(API_DATE_FORMAT)); - ConsumptionReport report = getData(url, ConsumptionReport.class); - return report.firstLevel.consumptions; + String dtStart = from.format(API_DATE_FORMAT); + String dtEnd = to.format(API_DATE_FORMAT); + + 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 { - 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 { - return getMeasures(userId, prmId, from, to, "pmax"); + return getMeasures(userId, prmId, from, to, "daily_consumption_max_power"); } } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/AddressInfo.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/AddressInfo.java new file mode 100644 index 00000000000..bb7170b1649 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/AddressInfo.java @@ -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; + +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ContactInfo.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ContactInfo.java new file mode 100644 index 00000000000..94c40c43253 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ContactInfo.java @@ -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; +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Contracts.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Contracts.java new file mode 100644 index 00000000000..c9bf59b0022 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Contracts.java @@ -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; +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Customer.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Customer.java new file mode 100644 index 00000000000..de2caae124a --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Customer.java @@ -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; + +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/CustomerIdResponse.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/CustomerIdResponse.java new file mode 100644 index 00000000000..f0e46550263 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/CustomerIdResponse.java @@ -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; +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/CustomerReponse.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/CustomerReponse.java new file mode 100644 index 00000000000..1215134483b --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/CustomerReponse.java @@ -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; +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IdentityDetails.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IdentityDetails.java new file mode 100644 index 00000000000..15a07b6a275 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IdentityDetails.java @@ -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; +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IdentityInfo.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IdentityInfo.java new file mode 100644 index 00000000000..8923636061c --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IdentityInfo.java @@ -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; +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java new file mode 100644 index 00000000000..23bf0d4018c --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java @@ -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; + +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java new file mode 100644 index 00000000000..8d32d1b722d --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java @@ -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; + +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterResponse.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterResponse.java new file mode 100644 index 00000000000..ff307fd9fee --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterResponse.java @@ -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; + +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/PrmInfo.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/PrmInfo.java index b03f577de79..9318ac3f322 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/PrmInfo.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/PrmInfo.java @@ -12,12 +12,22 @@ */ package org.openhab.binding.linky.internal.dto; +import org.eclipse.jetty.jaas.spi.UserInfo; + /** * The {@link UserInfo} holds ids of existing Prms * * @author Gaël L'hopital - Initial contribution + * @author Laurent Arnal - Rewrite addon to use official dataconect API */ 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; } diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ReadingType.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ReadingType.java new file mode 100644 index 00000000000..454f04fe295 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ReadingType.java @@ -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; +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/UsagePoint.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/UsagePoint.java new file mode 100644 index 00000000000..aa8d6bbf359 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/UsagePoint.java @@ -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; +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/UsagePointDetails.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/UsagePointDetails.java new file mode 100644 index 00000000000..2b2eeb81111 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/UsagePointDetails.java @@ -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; +} diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java index d38809b29a9..f83636f0de6 100644 --- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java +++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java @@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; 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.LinkyException; 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.PrmDetail; 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.library.types.DateTimeType; import org.openhab.core.library.types.QuantityType; @@ -61,6 +63,7 @@ import com.google.gson.Gson; * sent to one of the channels. * * @author Gaël L'hopital - Initial contribution + * @author Laurent Arnal - Rewrite addon to use official dataconect API */ @NonNullByDefault @@ -81,6 +84,8 @@ public class LinkyHandler extends BaseThingHandler { private @Nullable ScheduledFuture refreshJob; private @Nullable EnedisHttpApi enedisApi; + private final OAuthFactory oAuthFactory; + private @NonNullByDefault({}) String prmId; private @NonNullByDefault({}) String userId; @@ -90,7 +95,10 @@ public class LinkyHandler extends BaseThingHandler { 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); this.gson = gson; this.httpClient = httpClient; @@ -107,6 +115,8 @@ public class LinkyHandler extends BaseThingHandler { return consumption; }); + this.oAuthFactory = oAuthFactory; + 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 // 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); 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); scheduler.submit(() -> { try { @@ -159,15 +174,9 @@ public class LinkyHandler extends BaseThingHandler { api.initialize(); updateStatus(ThingStatus.ONLINE); - if (thing.getProperties().isEmpty()) { - UserInfo userInfo = api.getUserInfo(); - PrmInfo prmInfo = api.getPrmInfo(userInfo.userProperties.internId); - 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)); - } + PrmInfo prmInfo = api.getPrmInfo(); + updateProperties(Map.of(USER_ID, prmInfo.customerId, PUISSANCE, + prmInfo.contractInfo.subscribedPower, PRM_ID, prmInfo.prmId)); prmId = thing.getProperties().get(PRM_ID); userId = thing.getProperties().get(USER_ID); @@ -544,4 +553,5 @@ public class LinkyHandler extends BaseThingHandler { aggregate.datas.get(index)); } } + } diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml index fbcdbb509e3..778b374ed24 100644 --- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml @@ -20,19 +20,13 @@ - - - email - Your Enedis Username - - - - password - Your Enedis Password - - - - Authentication ID delivered after the captcha (see documentation). + + + Your PrmId + + + + Your Enedis token diff --git a/bundles/org.openhab.binding.linky/src/main/resources/templates/index.html b/bundles/org.openhab.binding.linky/src/main/resources/templates/index.html new file mode 100644 index 00000000000..b5e0ce91ca5 --- /dev/null +++ b/bundles/org.openhab.binding.linky/src/main/resources/templates/index.html @@ -0,0 +1,89 @@ + + + + + +${pageRefresh} +Authorize openHAB binding for Smartthings + + + + + + +

Authorize openHAB binding for Smartthings

+

On this page you can authorize the openHAB Smartthings biding to access your Smartthings account.

+

+ The redirect URI to use with Smartthings for this openHAB installation is + ${redirectUri} +

+ ${error} ${authorizedUser} + +
+ Connect to SmartThings: +

+

+

+
+ + + \ No newline at end of file