mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-02-10 14:26:59 +01:00
add WebBrigeHandler
Signed-off-by: Laurent ARNAL <laurent@clae.net>
This commit is contained in:
parent
1241ad3ef4
commit
8a7e4bd3a7
@ -0,0 +1,258 @@
|
|||||||
|
/**
|
||||||
|
* 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.handler;
|
||||||
|
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jetty.client.api.ContentResponse;
|
||||||
|
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||||
|
import org.eclipse.jetty.http.HttpHeader;
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
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.api.EnedisHttpApi;
|
||||||
|
import org.openhab.binding.linky.internal.dto.AuthData;
|
||||||
|
import org.openhab.binding.linky.internal.dto.AuthResult;
|
||||||
|
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
|
||||||
|
import org.openhab.core.auth.client.oauth2.OAuthFactory;
|
||||||
|
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||||
|
import org.openhab.core.thing.Bridge;
|
||||||
|
import org.openhab.core.thing.ThingRegistry;
|
||||||
|
import org.osgi.service.component.ComponentContext;
|
||||||
|
import org.osgi.service.component.annotations.Reference;
|
||||||
|
import org.osgi.service.http.HttpService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonSyntaxException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link EnedisBridgeHandler} is the base handler to access enedis data.
|
||||||
|
*
|
||||||
|
* @author Laurent Arnal - Initial contribution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class EnedisWebBridgeHandler extends ApiBridgeHandler {
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(EnedisWebBridgeHandler.class);
|
||||||
|
|
||||||
|
private static final String BASE_URL = "https://ext.prod-sandbox.api.enedis.fr/";
|
||||||
|
|
||||||
|
private static final String CONTRACT_URL = BASE_URL + "customers_upc/v5/usage_points/contracts?usage_point_id=%s";
|
||||||
|
private static final String IDENTITY_URL = BASE_URL + "customers_i/v5/identity?usage_point_id=%s";
|
||||||
|
private static final String CONTACT_URL = BASE_URL + "customers_cd/v5/contact_data?usage_point_id=%s";
|
||||||
|
private static final String ADDRESS_URL = BASE_URL + "customers_upa/v5/usage_points/addresses?usage_point_id=%s";
|
||||||
|
private static final String MEASURE_DAILY_CONSUMPTION_URL = BASE_URL
|
||||||
|
+ "metering_data_dc/v5/daily_consumption?usage_point_id=%s&start=%s&end=%s";
|
||||||
|
private static final String MEASURE_MAX_POWER_URL = BASE_URL
|
||||||
|
+ "metering_data_dcmp/v5/daily_consumption_max_power?usage_point_id=%s&start=%s&end=%s";
|
||||||
|
|
||||||
|
private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
|
||||||
|
private static final String URL_APPS_LINCS = "https://alex.microapplications" + EnedisHttpApi.ENEDIS_DOMAIN;
|
||||||
|
private static final String URL_ENEDIS_AUTHENTICATE = URL_APPS_LINCS + "/authenticate?target="
|
||||||
|
+ EnedisHttpApi.URL_COMPTE_PART;
|
||||||
|
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 = PRM_INFO_BASE_URL + "null/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 Pattern REQ_PATTERN = Pattern.compile("ReqID%(.*?)%26");
|
||||||
|
|
||||||
|
private boolean connected = false;
|
||||||
|
|
||||||
|
public EnedisWebBridgeHandler(Bridge bridge, final @Reference HttpClientFactory httpClientFactory,
|
||||||
|
final @Reference OAuthFactory oAuthFactory, final @Reference HttpService httpService,
|
||||||
|
final @Reference ThingRegistry thingRegistry, ComponentContext componentContext, Gson gson) {
|
||||||
|
super(bridge, httpClientFactory, oAuthFactory, httpService, thingRegistry, componentContext, gson);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() {
|
||||||
|
super.initialize();
|
||||||
|
|
||||||
|
try {
|
||||||
|
doAuth();
|
||||||
|
} catch (LinkyException ex) {
|
||||||
|
logger.debug("Exception: {}", ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientId() {
|
||||||
|
return config.clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientSecret() {
|
||||||
|
return config.clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
logger.debug("Shutting down Netatmo API bridge handler.");
|
||||||
|
try {
|
||||||
|
enedisApi.disconnect();
|
||||||
|
} catch (LinkyException ex) {
|
||||||
|
logger.debug("Exception: {}", ex.getMessage());
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getToken(LinkyHandler handler) throws LinkyException {
|
||||||
|
|
||||||
|
AccessTokenResponse accesToken = getAccessTokenResponse();
|
||||||
|
if (accesToken == null) {
|
||||||
|
accesToken = getAccessTokenByClientCredentials();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accesToken == null) {
|
||||||
|
throw new LinkyException("no token");
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Bearer " + accesToken.getAccessToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContactUrl() {
|
||||||
|
return CONTACT_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContractUrl() {
|
||||||
|
return CONTRACT_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIdentityUrl() {
|
||||||
|
return IDENTITY_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAddressUrl() {
|
||||||
|
return ADDRESS_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDailyConsumptionUrl() {
|
||||||
|
return MEASURE_DAILY_CONSUMPTION_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMaxPowerUrl() {
|
||||||
|
return MEASURE_MAX_POWER_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTempoUrl() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void doAuth() throws LinkyException {
|
||||||
|
logger.debug("Starting login process for user : {}", config.username);
|
||||||
|
|
||||||
|
try {
|
||||||
|
enedisApi.addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId);
|
||||||
|
logger.debug("Step 1 : getting authentification");
|
||||||
|
String data = enedisApi.getData(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(enedisApi.getFormContent("SAMLRequest", samlInput.attr("value"))).send();
|
||||||
|
if (result.getStatus() != 302) {
|
||||||
|
throw new LinkyException("Connection failed step 2");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Get the location and the ReqID");
|
||||||
|
Matcher m = REQ_PATTERN.matcher(enedisApi.getLocation(result));
|
||||||
|
if (!m.find()) {
|
||||||
|
throw new LinkyException("Unable to locate ReqId in header");
|
||||||
|
}
|
||||||
|
|
||||||
|
String reqId = m.group(1);
|
||||||
|
String authenticateUrl = EnedisHttpApi.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() != 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, "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() != 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");
|
||||||
|
enedisApi.addCookie("enedisExt", authResult.tokenId);
|
||||||
|
|
||||||
|
logger.debug("Step 5 : retrieve the SAMLresponse");
|
||||||
|
data = enedisApi.getData(EnedisHttpApi.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(enedisApi.getFormContent("SAMLResponse", samlInput.attr("value"))).send();
|
||||||
|
if (result.getStatus() != 302) {
|
||||||
|
throw new LinkyException("Connection failed step 6");
|
||||||
|
}
|
||||||
|
connected = true;
|
||||||
|
} catch (InterruptedException | TimeoutException | ExecutionException | JsonSyntaxException e) {
|
||||||
|
throw new LinkyException(e, "Error opening connection with Enedis webservice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,6 +25,34 @@
|
|||||||
|
|
||||||
</bridge-type>
|
</bridge-type>
|
||||||
|
|
||||||
|
<bridge-type id="EnedisWebBridge">
|
||||||
|
<label>EnedisWebBridge</label>
|
||||||
|
<description>
|
||||||
|
Provides your energy consumption data.
|
||||||
|
In order to receive the data, you must activate your account at
|
||||||
|
https://espace-client-particuliers.enedis.fr/web/espace-particuliers/compteur-linky.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<config-description>
|
||||||
|
<parameter name="username" type="text" required="true">
|
||||||
|
<label>Username</label>
|
||||||
|
<context>email</context>
|
||||||
|
<description>Your Enedis Username</description>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="password" type="text" required="true">
|
||||||
|
<label>Password</label>
|
||||||
|
<context>password</context>
|
||||||
|
<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>
|
||||||
|
</config-description>
|
||||||
|
|
||||||
|
</bridge-type>
|
||||||
|
|
||||||
|
|
||||||
<bridge-type id="MyElectricalDataBridge">
|
<bridge-type id="MyElectricalDataBridge">
|
||||||
<label>MyElectricalDataBridge</label>
|
<label>MyElectricalDataBridge</label>
|
||||||
<description>
|
<description>
|
||||||
@ -41,6 +69,7 @@
|
|||||||
|
|
||||||
<supported-bridge-type-refs>
|
<supported-bridge-type-refs>
|
||||||
<bridge-type-ref id="EnedisBridge"/>
|
<bridge-type-ref id="EnedisBridge"/>
|
||||||
|
<bridge-type-ref id="EnedisWebBridge"/>
|
||||||
<bridge-type-ref id="MyElectricalDataBridge"/>
|
<bridge-type-ref id="MyElectricalDataBridge"/>
|
||||||
</supported-bridge-type-refs>
|
</supported-bridge-type-refs>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user