review consent / authorisation flow to be compatible with both MyElectricalData and Enedis

Signed-off-by: Laurent ARNAL <laurent@clae.net>
This commit is contained in:
Laurent ARNAL 2024-06-04 15:51:56 +02:00
parent fe75abe979
commit d1920f21db
8 changed files with 161 additions and 68 deletions

View File

@ -69,6 +69,7 @@ public class LinkyAuthServlet extends HttpServlet {
public LinkyAuthServlet(ApiBridgeHandler apiBridgeHandler, String indexTemplate) {
this.indexTemplate = indexTemplate;
this.apiBridgeHandler = apiBridgeHandler;
}
@Override

View File

@ -106,18 +106,20 @@ public class LinkyBindingConstants {
// "r:locations:*", "w:locations:*", "x:locations:*", "r:scenes:*", "x:scenes:*", "r:rules:*", "w:rules:*",
// "r:installedapps", "w:installedapps"
// List of Linky services related urls, information
public static final String LINKY_MYELECTRICALDATA_ACCOUNT_URL = "https://www.myelectricaldata.fr/";
public static final String LINKY_MYELECTRICALDATA_AUTHORIZE_URL = LINKY_MYELECTRICALDATA_ACCOUNT_URL
+ "v1/oauth2/authorize";
public static final String LINKY_MYELECTRICALDATA_API_TOKEN_URL = LINKY_MYELECTRICALDATA_ACCOUNT_URL + "token";
public static final String ENEDIS_ACCOUNT_URL_PROD = "https://mon-compte-particulier.enedis.fr/";
public static final String ENEDIS_AUTHORIZE_URL_PROD = ENEDIS_ACCOUNT_URL_PROD + "dataconnect/v1/oauth2/authorize";
public static final String ENEDIS_API_TOKEN_URL_PROD = ENEDIS_ACCOUNT_URL_PROD + "oauth2/v3/token";
public static final String ENEDIS_ACCOUNT_URL_PREPROD = "https://ext.prod-sandbox.api.enedis.fr/";
public static final String ENEDIS_AUTHORIZE_URL_PREPROD = ENEDIS_ACCOUNT_URL_PREPROD
public static final String ENEDIS_AUTHORIZE_URL_PREPROD = ENEDIS_ACCOUNT_URL_PROD
+ "dataconnect/v1/oauth2/authorize";
public static final String ENEDIS_API_TOKEN_URL_PREPROD = ENEDIS_ACCOUNT_URL_PREPROD + "oauth2/v3/token";
// List of Linky services related urls, information
public static final String LINKY_MYELECTRICALDATA_ACCOUNT_URL = "https://www.myelectricaldata.fr/";
// public static final String LINKY_MYELECTRICALDATA_AUTHORIZE_URL = LINKY_MYELECTRICALDATA_ACCOUNT_URL
// + "v1/oauth2/authorize?usage_points_id=21454992660003&user_type=aa&person_id=-1";
public static final String LINKY_MYELECTRICALDATA_AUTHORIZE_URL = ENEDIS_AUTHORIZE_URL_PROD;
public static final String LINKY_MYELECTRICALDATA_API_TOKEN_URL = LINKY_MYELECTRICALDATA_ACCOUNT_URL
+ "v1/oauth2/authorize?client_id=%s&response_type=code&redirect_uri=na&user_type=na&state=na&person_id=-1&usage_points_id=%s";
}

View File

@ -43,6 +43,7 @@ import org.openhab.binding.linky.internal.dto.TempoResponse;
import org.openhab.binding.linky.internal.dto.UsagePoint;
import org.openhab.binding.linky.internal.dto.UsagePointDetails;
import org.openhab.binding.linky.internal.handler.ApiBridgeHandler;
import org.openhab.binding.linky.internal.handler.LinkyHandler;
import org.openhab.binding.linky.internal.handler.MyElectricalDataBridgeHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -92,8 +93,12 @@ public class EnedisHttpApi {
disconnect();
}
private String getData(String url) throws LinkyException {
return getData(apiBridgeHandler, url, httpClient, apiBridgeHandler.getToken());
public String getData(LinkyHandler handler, String url) throws LinkyException {
return getData(apiBridgeHandler, url, httpClient, apiBridgeHandler.getToken(handler));
}
public String getData(String url) throws LinkyException {
return getData(apiBridgeHandler, url, httpClient, "");
}
private static String getData(ApiBridgeHandler apiBridgeHandler, String url, HttpClient httpClient, String token)
@ -131,7 +136,7 @@ public class EnedisHttpApi {
}
}
public PrmInfo getPrmInfo(String prmId) throws LinkyException {
public PrmInfo getPrmInfo(LinkyHandler handler, String prmId) throws LinkyException {
PrmInfo result = new PrmInfo();
if (apiBridgeHandler instanceof MyElectricalDataBridgeHandler) {
@ -170,14 +175,14 @@ public class EnedisHttpApi {
result.customerId = "xxxxxxxxxx";
} else {
Customer customer = getCustomer(prmId);
Customer customer = getCustomer(handler, prmId);
UsagePoint usagePoint = customer.usagePoints[0];
result.contractInfo = usagePoint.contracts;
result.usagePointInfo = usagePoint.usagePoint;
result.identityInfo = getIdentity(prmId);
result.addressInfo = getAddress(prmId);
result.contactInfo = getContact(prmId);
result.identityInfo = getIdentity(handler, prmId);
result.addressInfo = getAddress(handler, prmId);
result.contactInfo = getContact(handler, prmId);
result.prmId = result.usagePointInfo.usagePointId;
result.customerId = customer.customerId;
@ -190,12 +195,12 @@ public class EnedisHttpApi {
return apiUrl.formatted(prmId);
}
public Customer getCustomer(String prmId) throws LinkyException {
public Customer getCustomer(LinkyHandler handler, String prmId) throws LinkyException {
if (!connected) {
initialize();
}
String contractUrl = apiBridgeHandler.getContractUrl();
String data = getData(formatUrl(contractUrl, prmId));
String data = getData(handler, formatUrl(contractUrl, prmId));
if (data.isEmpty()) {
throw new LinkyException("Requesting '%s' returned an empty response", contractUrl);
}
@ -211,12 +216,12 @@ public class EnedisHttpApi {
}
}
public AddressInfo getAddress(String prmId) throws LinkyException {
public AddressInfo getAddress(LinkyHandler handler, String prmId) throws LinkyException {
if (!connected) {
initialize();
}
String addressUrl = apiBridgeHandler.getAddressUrl();
String data = getData(formatUrl(addressUrl, prmId));
String data = getData(handler, formatUrl(addressUrl, prmId));
if (data.isEmpty()) {
throw new LinkyException("Requesting '%s' returned an empty response", addressUrl);
}
@ -232,12 +237,12 @@ public class EnedisHttpApi {
}
}
public IdentityInfo getIdentity(String prmId) throws LinkyException {
public IdentityInfo getIdentity(LinkyHandler handler, String prmId) throws LinkyException {
if (!connected) {
initialize();
}
String identityUrl = apiBridgeHandler.getIdentityUrl();
String data = getData(formatUrl(identityUrl, prmId));
String data = getData(handler, formatUrl(identityUrl, prmId));
if (data.isEmpty()) {
throw new LinkyException("Requesting '%s' returned an empty response", identityUrl);
}
@ -253,12 +258,12 @@ public class EnedisHttpApi {
}
}
public ContactInfo getContact(String prmId) throws LinkyException {
public ContactInfo getContact(LinkyHandler handler, String prmId) throws LinkyException {
if (!connected) {
initialize();
}
String contactUrl = apiBridgeHandler.getContactUrl();
String data = getData(formatUrl(contactUrl, prmId));
String data = getData(handler, formatUrl(contactUrl, prmId));
if (data.isEmpty()) {
throw new LinkyException("Requesting '%s' returned an empty response", contactUrl);
@ -275,7 +280,8 @@ public class EnedisHttpApi {
}
}
private MeterReading getMeasures(String apiUrl, String prmId, LocalDate from, LocalDate to) throws LinkyException {
private MeterReading getMeasures(LinkyHandler handler, String apiUrl, String prmId, LocalDate from, LocalDate to)
throws LinkyException {
String dtStart = from.format(API_DATE_FORMAT);
String dtEnd = to.format(API_DATE_FORMAT);
@ -283,7 +289,7 @@ public class EnedisHttpApi {
if (!connected) {
initialize();
}
String data = getData(url);
String data = getData(handler, url);
if (data.isEmpty()) {
throw new LinkyException("Requesting '%s' returned an empty response", url);
}
@ -300,20 +306,22 @@ public class EnedisHttpApi {
}
}
public MeterReading getEnergyData(String prmId, LocalDate from, LocalDate to) throws LinkyException {
return getMeasures(apiBridgeHandler.getDailyConsumptionUrl(), prmId, from, to);
public MeterReading getEnergyData(LinkyHandler handler, String prmId, LocalDate from, LocalDate to)
throws LinkyException {
return getMeasures(handler, apiBridgeHandler.getDailyConsumptionUrl(), prmId, from, to);
}
public MeterReading getPowerData(String prmId, LocalDate from, LocalDate to) throws LinkyException {
return getMeasures(apiBridgeHandler.getMaxPowerUrl(), prmId, from, to);
public MeterReading getPowerData(LinkyHandler handler, String prmId, LocalDate from, LocalDate to)
throws LinkyException {
return getMeasures(handler, apiBridgeHandler.getMaxPowerUrl(), prmId, from, to);
}
public String getTempoData() throws LinkyException {
public String getTempoData(LinkyHandler handler) throws LinkyException {
String url = String.format(apiBridgeHandler.getTempoUrl(), "2024-01-01", "2024-01-31");
if (!connected) {
initialize();
}
String data = getData(url);
String data = getData(handler, url);
if (data.isEmpty()) {
throw new LinkyException("Requesting '%s' returned an empty response", url);
}
@ -334,5 +342,4 @@ public class EnedisHttpApi {
// return data;
}
}

View File

@ -82,7 +82,7 @@ public abstract class ApiBridgeHandler extends BaseBridgeHandler {
private @Nullable EnedisHttpApi enedisApi;
private final Gson gson;
private OAuthClientService oAuthService;
private final ThingRegistry thingRegistry;
protected final ThingRegistry thingRegistry;
private static @Nullable HttpServlet servlet;
protected @Nullable LinkyConfiguration config;
@ -125,9 +125,19 @@ public abstract class ApiBridgeHandler extends BaseBridgeHandler {
config = getConfigAs(LinkyConfiguration.class);
this.oAuthService = oAuthFactory.createOAuthClientService(LinkyBindingConstants.BINDING_ID,
LinkyBindingConstants.ENEDIS_API_TOKEN_URL_PREPROD, LinkyBindingConstants.ENEDIS_AUTHORIZE_URL_PREPROD,
config.clientId, config.clientSecret, LinkyBindingConstants.LINKY_SCOPES, true);
String tokenUrl = "";
String authorizeUrl = "";
if (this instanceof MyElectricalDataBridgeHandler) {
tokenUrl = LinkyBindingConstants.LINKY_MYELECTRICALDATA_API_TOKEN_URL;
authorizeUrl = LinkyBindingConstants.LINKY_MYELECTRICALDATA_AUTHORIZE_URL;
} else if (this instanceof EnedisBridgeHandler) {
tokenUrl = LinkyBindingConstants.ENEDIS_API_TOKEN_URL_PREPROD;
authorizeUrl = LinkyBindingConstants.ENEDIS_AUTHORIZE_URL_PREPROD;
}
this.oAuthService = oAuthFactory.createOAuthClientService(LinkyBindingConstants.BINDING_ID, tokenUrl,
authorizeUrl, config.clientId, config.clientSecret, LinkyBindingConstants.LINKY_SCOPES, true);
registerServlet();
@ -138,6 +148,10 @@ public abstract class ApiBridgeHandler extends BaseBridgeHandler {
return enedisApi;
}
public abstract String getClientId();
public abstract String getClientSecret();
@Override
public void initialize() {
logger.debug("Initializing Netatmo API bridge handler.");
@ -260,21 +274,9 @@ public abstract class ApiBridgeHandler extends BaseBridgeHandler {
&& accessTokenResponse.getRefreshToken() != null;
}
public String getToken() throws LinkyException {
public abstract 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();
}
private @Nullable AccessTokenResponse getAccessTokenByClientCredentials() {
protected @Nullable AccessTokenResponse getAccessTokenByClientCredentials() {
try {
return oAuthService.getAccessTokenByClientCredentials(LinkyBindingConstants.LINKY_SCOPES);
} catch (OAuthException | IOException | OAuthResponseException | RuntimeException e) {
@ -283,7 +285,7 @@ public abstract class ApiBridgeHandler extends BaseBridgeHandler {
}
}
private @Nullable AccessTokenResponse getAccessTokenResponse() {
protected @Nullable AccessTokenResponse getAccessTokenResponse() {
try {
return oAuthService.getAccessTokenResponse();
} catch (OAuthException | IOException | OAuthResponseException | RuntimeException e) {
@ -293,8 +295,6 @@ public abstract class ApiBridgeHandler extends BaseBridgeHandler {
}
public String formatAuthorizationUrl(String redirectUri) {
// Will work only in case of direct oAuth2 authentification to enedis
// this is not the case in v1 as we go trough MyElectricalData
try {
String uri = this.oAuthService.getAuthorizationUrl(redirectUri, LinkyBindingConstants.LINKY_SCOPES,
LinkyBindingConstants.BINDING_ID);
@ -337,5 +337,4 @@ public abstract class ApiBridgeHandler extends BaseBridgeHandler {
public abstract String getMaxPowerUrl();
public abstract String getTempoUrl();
}

View File

@ -13,6 +13,8 @@
package org.openhab.binding.linky.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.linky.internal.LinkyException;
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;
@ -47,10 +49,6 @@ public class EnedisBridgeHandler extends ApiBridgeHandler {
private static final String TEMPO_URL = BASE_URL + "rte/tempo/%s/%s";
// private static final String TOKEN_URL = BASE_URL
// +
// "v1/oauth2/authorize?client_id=%s&response_type=code&redirect_uri=na&user_type=na&state=na&person_id=-1&usage_points_id=%s";
public EnedisBridgeHandler(Bridge bridge, final @Reference HttpClientFactory httpClientFactory,
final @Reference OAuthFactory oAuthFactory, final @Reference HttpService httpService,
final @Reference ThingRegistry thingRegistry, ComponentContext componentContext, Gson gson) {
@ -62,6 +60,16 @@ public class EnedisBridgeHandler extends ApiBridgeHandler {
super.initialize();
}
@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.");
@ -69,6 +77,21 @@ public class EnedisBridgeHandler extends ApiBridgeHandler {
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;

View File

@ -161,7 +161,7 @@ public class LinkyHandler extends BaseThingHandler {
LinkyConfiguration config = this.config;
if (api != null && config != null) {
PrmInfo prmInfo = api.getPrmInfo(config.prmId);
PrmInfo prmInfo = api.getPrmInfo(this, config.prmId);
updateProperties(Map.of(USER_ID, prmInfo.customerId, PUISSANCE,
prmInfo.contractInfo.subscribedPower, PRM_ID, prmInfo.prmId));
@ -198,7 +198,7 @@ public class LinkyHandler extends BaseThingHandler {
if (api != null && config != null) {
try {
PrmInfo info = api.getPrmInfo(config.prmId);
PrmInfo info = api.getPrmInfo(this, config.prmId);
String title = info.identityInfo.title;
String firstName = info.identityInfo.firstname;
String lastName = info.identityInfo.lastname;
@ -466,7 +466,7 @@ public class LinkyHandler extends BaseThingHandler {
EnedisHttpApi api = this.enedisApi;
if (api != null) {
try {
MeterReading meterReading = api.getEnergyData(config.prmId, from, to);
MeterReading meterReading = api.getEnergyData(this, config.prmId, from, to);
updateStatus(ThingStatus.ONLINE);
return meterReading;
} catch (LinkyException e) {
@ -484,7 +484,7 @@ public class LinkyHandler extends BaseThingHandler {
EnedisHttpApi api = this.enedisApi;
if (api != null) {
try {
MeterReading meterReading = api.getPowerData(config.prmId, from, to);
MeterReading meterReading = api.getPowerData(this, config.prmId, from, to);
updateStatus(ThingStatus.ONLINE);
return meterReading;
} catch (LinkyException e) {

View File

@ -12,11 +12,18 @@
*/
package org.openhab.binding.linky.internal.handler;
import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
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;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Reference;
@ -46,6 +53,8 @@ public class MyElectricalDataBridgeHandler extends ApiBridgeHandler {
private static final String TEMPO_URL = BASE_URL + "rte/tempo/%s/%s";
// https://www.myelectricaldata.fr/v1/oauth2/authorize?response_type=code&client_id=&state=linky&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fconnectlinky&scope=am_application_scope+default&user_type=aa&person_id=-1&usage_points_id=aa
public MyElectricalDataBridgeHandler(Bridge bridge, final @Reference HttpClientFactory httpClientFactory,
final @Reference OAuthFactory oAuthFactory, final @Reference HttpService httpService,
final @Reference ThingRegistry thingRegistry, ComponentContext componentContext, Gson gson) {
@ -57,6 +66,54 @@ public class MyElectricalDataBridgeHandler extends ApiBridgeHandler {
super.initialize();
}
@Override
public String getClientId() {
return "e551937c-5250-48bc-b4a6-2323af68db92";
}
@Override
public String getClientSecret() {
return "";
}
@Override
public String formatAuthorizationUrl(String redirectUri) {
return super.formatAuthorizationUrl("");
}
@Override
public String authorize(String redirectUri, String reqState, String reqCode) throws LinkyException {
String url = String.format(LinkyBindingConstants.LINKY_MYELECTRICALDATA_API_TOKEN_URL, getClientId(), reqCode);
EnedisHttpApi enedisApi = getEnedisApi();
if (enedisApi == null) {
return "";
}
String token = enedisApi.getData(url);
logger.debug("token: {}", token);
Collection<Thing> col = this.thingRegistry.getAll();
for (Thing thing : col) {
if (LinkyBindingConstants.THING_TYPE_LINKY.equals(thing.getThingTypeUID())) {
Configuration config = thing.getConfiguration();
String prmId = (String) config.get("prmId");
if (!prmId.equals(reqCode)) {
continue;
}
config.put("token", token);
LinkyHandler handler = (LinkyHandler) thing.getHandler();
if (handler != null) {
handler.saveConfiguration(config);
}
}
}
return token;
}
@Override
public void dispose() {
logger.debug("Shutting down Netatmo API bridge handler.");
@ -65,7 +122,11 @@ public class MyElectricalDataBridgeHandler extends ApiBridgeHandler {
}
@Override
public String getToken() throws LinkyException {
public String getToken(LinkyHandler handler) throws LinkyException {
LinkyConfiguration config = handler.getLinkyConfig();
if (config == null) {
return "";
}
return config.token;
}

View File

@ -26,7 +26,7 @@
</bridge-type>
<bridge-type id="MyElectricalDataBridge">
<label>EnedisBridge</label>
<label>MyElectricalDataBridge</label>
<description>
Provides your energy consumption data.
In order to receive the data, you must activate your account at
@ -34,11 +34,6 @@
</description>
<config-description>
<parameter name="token" type="text" required="false">
<label>Token</label>
<description>Your Enedis token (can be left empty, use the connection page to automatically fill it
http://youopenhab/connectlinky)</description>
</parameter>
</config-description>
</bridge-type>
@ -74,6 +69,11 @@
<label>PrmId</label>
<description>Your PrmId</description>
</parameter>
<parameter name="token" type="text" required="false">
<label>Token</label>
<description>Your Enedis token (can be left empty, use the connection page to automatically fill it
http://youopenhab/connectlinky)</description>
</parameter>
</config-description>