mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[linky] Yet another website underlaying API modification (#17538)
* Yet another website underlaying API modification * Correction for current and previous week, month, year * Added unitHing * Switch peek power to kVA * Adding new cookie and user agent Signed-off-by: Gaël L'hopital <gael@lhopital.org>
This commit is contained in:
parent
f0d4a0a965
commit
7cab153ebf
@ -83,7 +83,7 @@ In case you are running openHAB inside Docker, the binding will work only if you
|
|||||||
### Thing
|
### Thing
|
||||||
|
|
||||||
```java
|
```java
|
||||||
Thing linky:linky:local "Compteur Linky" [ username="example@domaine.fr", password="******" ]
|
Thing linky:linky:local "Compteur Linky" [ username="example@domaine.fr", password="******", internalAuthId="******" ]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Items
|
### Items
|
||||||
|
@ -14,6 +14,10 @@
|
|||||||
|
|
||||||
<name>openHAB Add-ons :: Bundles :: Linky Binding</name>
|
<name>openHAB Add-ons :: Bundles :: Linky Binding</name>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<bnd.importpackage>javax.annotation.meta;resolution:=optional</bnd.importpackage>
|
||||||
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jsoup</groupId>
|
<groupId>org.jsoup</groupId>
|
||||||
|
@ -36,10 +36,10 @@ public class LinkyException extends Exception {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public LinkyException(String message, Object... params) {
|
public LinkyException(String message, Object... params) {
|
||||||
this(String.format(message, params));
|
this(message.formatted(params));
|
||||||
}
|
}
|
||||||
|
|
||||||
public LinkyException(Exception e, String message, Object... params) {
|
public LinkyException(Exception e, String message, Object... params) {
|
||||||
this(e, String.format(message, params));
|
this(e, message.formatted(params));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,7 @@ import com.google.gson.JsonDeserializer;
|
|||||||
public class LinkyHandlerFactory extends BaseThingHandlerFactory {
|
public class LinkyHandlerFactory extends BaseThingHandlerFactory {
|
||||||
private static final DateTimeFormatter LINKY_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSX");
|
private static final DateTimeFormatter LINKY_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSX");
|
||||||
private static final int REQUEST_BUFFER_SIZE = 8000;
|
private static final int REQUEST_BUFFER_SIZE = 8000;
|
||||||
|
private static final int RESPONSE_BUFFER_SIZE = 200000;
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(LinkyHandlerFactory.class);
|
private final Logger logger = LoggerFactory.getLogger(LinkyHandlerFactory.class);
|
||||||
private final Gson gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class,
|
private final Gson gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class,
|
||||||
@ -83,6 +84,7 @@ public class LinkyHandlerFactory extends BaseThingHandlerFactory {
|
|||||||
this.httpClient = httpClientFactory.createHttpClient(LinkyBindingConstants.BINDING_ID, sslContextFactory);
|
this.httpClient = httpClientFactory.createHttpClient(LinkyBindingConstants.BINDING_ID, sslContextFactory);
|
||||||
httpClient.setFollowRedirects(false);
|
httpClient.setFollowRedirects(false);
|
||||||
httpClient.setRequestBufferSize(REQUEST_BUFFER_SIZE);
|
httpClient.setRequestBufferSize(REQUEST_BUFFER_SIZE);
|
||||||
|
httpClient.setResponseBufferSize(RESPONSE_BUFFER_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -16,18 +16,26 @@ import java.net.HttpCookie;
|
|||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
import org.eclipse.jetty.client.HttpClient;
|
||||||
import org.eclipse.jetty.client.api.ContentResponse;
|
import org.eclipse.jetty.client.api.ContentResponse;
|
||||||
|
import org.eclipse.jetty.client.api.Request;
|
||||||
import org.eclipse.jetty.client.util.FormContentProvider;
|
import org.eclipse.jetty.client.util.FormContentProvider;
|
||||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||||
import org.eclipse.jetty.http.HttpHeader;
|
import org.eclipse.jetty.http.HttpHeader;
|
||||||
|
import org.eclipse.jetty.http.HttpMethod;
|
||||||
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
import org.eclipse.jetty.util.Fields;
|
import org.eclipse.jetty.util.Fields;
|
||||||
import org.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
import org.jsoup.nodes.Document;
|
import org.jsoup.nodes.Document;
|
||||||
@ -38,6 +46,7 @@ import org.openhab.binding.linky.internal.dto.AuthData;
|
|||||||
import org.openhab.binding.linky.internal.dto.AuthResult;
|
import org.openhab.binding.linky.internal.dto.AuthResult;
|
||||||
import org.openhab.binding.linky.internal.dto.ConsumptionReport;
|
import org.openhab.binding.linky.internal.dto.ConsumptionReport;
|
||||||
import org.openhab.binding.linky.internal.dto.ConsumptionReport.Consumption;
|
import org.openhab.binding.linky.internal.dto.ConsumptionReport.Consumption;
|
||||||
|
import org.openhab.binding.linky.internal.dto.PrmDetail;
|
||||||
import org.openhab.binding.linky.internal.dto.PrmInfo;
|
import org.openhab.binding.linky.internal.dto.PrmInfo;
|
||||||
import org.openhab.binding.linky.internal.dto.UserInfo;
|
import org.openhab.binding.linky.internal.dto.UserInfo;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@ -59,9 +68,10 @@ public class EnedisHttpApi {
|
|||||||
private static final String URL_MON_COMPTE = "https://mon-compte" + 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_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 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 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_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 PRM_INFO_URL = URL_APPS_LINCS + "/mes-prms/api/private/v2/personnes/%s/prms";
|
||||||
private static final String MEASURE_URL = PRM_INFO_BASE_URL
|
private static final String MEASURE_URL = PRM_INFO_BASE_URL
|
||||||
+ "%s/prms/%s/donnees-%s?dateDebut=%s&dateFin=%s&mesuretypecode=CONS";
|
+ "%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 URI COOKIE_URI = URI.create(URL_COMPTE_PART);
|
||||||
@ -81,22 +91,22 @@ public class EnedisHttpApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void initialize() throws LinkyException {
|
public void initialize() throws LinkyException {
|
||||||
logger.debug("Starting login process for user : {}", config.username);
|
logger.debug("Starting login process for user: {}", config.username);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId);
|
addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId);
|
||||||
logger.debug("Step 1 : getting authentification");
|
logger.debug("Step 1: getting authentification");
|
||||||
String data = getData(URL_ENEDIS_AUTHENTICATE);
|
String data = getContent(URL_ENEDIS_AUTHENTICATE);
|
||||||
|
|
||||||
logger.debug("Reception request SAML");
|
logger.debug("Reception request SAML");
|
||||||
Document htmlDocument = Jsoup.parse(data);
|
Document htmlDocument = Jsoup.parse(data);
|
||||||
Element el = htmlDocument.select("form").first();
|
Element el = htmlDocument.select("form").first();
|
||||||
Element samlInput = el.select("input[name=SAMLRequest]").first();
|
Element samlInput = el.select("input[name=SAMLRequest]").first();
|
||||||
|
|
||||||
logger.debug("Step 2 : send SSO SAMLRequest");
|
logger.debug("Step 2: send SSO SAMLRequest");
|
||||||
ContentResponse result = httpClient.POST(el.attr("action"))
|
ContentResponse result = httpClient.POST(el.attr("action"))
|
||||||
.content(getFormContent("SAMLRequest", samlInput.attr("value"))).send();
|
.content(getFormContent("SAMLRequest", samlInput.attr("value"))).send();
|
||||||
if (result.getStatus() != 302) {
|
if (result.getStatus() != HttpStatus.FOUND_302) {
|
||||||
throw new LinkyException("Connection failed step 2");
|
throw new LinkyException("Connection failed step 2");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,11 +122,11 @@ public class EnedisHttpApi {
|
|||||||
+ reqId + "%26index%3Dnull%26acsURL%3D" + URL_APPS_LINCS
|
+ 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=";
|
+ "/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");
|
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")
|
result = httpClient.POST(authenticateUrl).header("X-NoSession", "true").header("X-Password", "anonymous")
|
||||||
.header("X-Requested-With", "XMLHttpRequest").header("X-Username", "anonymous").send();
|
.header("X-Requested-With", "XMLHttpRequest").header("X-Username", "anonymous").send();
|
||||||
if (result.getStatus() != 200) {
|
if (result.getStatus() != HttpStatus.OK_200) {
|
||||||
throw new LinkyException("Connection failed step 3 - auth1 : %s", result.getContentAsString());
|
throw new LinkyException("Connection failed step 3 - auth1: %s", result.getContentAsString());
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthData authData = gson.fromJson(result.getContentAsString(), AuthData.class);
|
AuthData authData = gson.fromJson(result.getContentAsString(), AuthData.class);
|
||||||
@ -128,13 +138,13 @@ public class EnedisHttpApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
authData.callbacks.get(1).input.get(0).value = config.password;
|
authData.callbacks.get(1).input.get(0).value = config.password;
|
||||||
logger.debug("Step 4 : auth2 - send the auth data");
|
logger.debug("Step 4: auth2 - send the auth data");
|
||||||
result = httpClient.POST(authenticateUrl).header(HttpHeader.CONTENT_TYPE, "application/json")
|
result = httpClient.POST(authenticateUrl).header(HttpHeader.CONTENT_TYPE, MediaType.APPLICATION_JSON)
|
||||||
.header("X-NoSession", "true").header("X-Password", "anonymous")
|
.header("X-NoSession", "true").header("X-Password", "anonymous")
|
||||||
.header("X-Requested-With", "XMLHttpRequest").header("X-Username", "anonymous")
|
.header("X-Requested-With", "XMLHttpRequest").header("X-Username", "anonymous")
|
||||||
.content(new StringContentProvider(gson.toJson(authData))).send();
|
.content(new StringContentProvider(gson.toJson(authData))).send();
|
||||||
if (result.getStatus() != 200) {
|
if (result.getStatus() != HttpStatus.OK_200) {
|
||||||
throw new LinkyException("Connection failed step 3 - auth2 : %s", result.getContentAsString());
|
throw new LinkyException("Connection failed step 3 - auth2: %s", result.getContentAsString());
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthResult authResult = gson.fromJson(result.getContentAsString(), AuthResult.class);
|
AuthResult authResult = gson.fromJson(result.getContentAsString(), AuthResult.class);
|
||||||
@ -145,18 +155,40 @@ public class EnedisHttpApi {
|
|||||||
logger.debug("Add the tokenId cookie");
|
logger.debug("Add the tokenId cookie");
|
||||||
addCookie("enedisExt", authResult.tokenId);
|
addCookie("enedisExt", authResult.tokenId);
|
||||||
|
|
||||||
logger.debug("Step 5 : retrieve the SAMLresponse");
|
logger.debug("Step 5: retrieve the SAMLresponse");
|
||||||
data = getData(URL_MON_COMPTE + "/" + authResult.successUrl);
|
data = getContent(URL_MON_COMPTE + "/" + authResult.successUrl);
|
||||||
htmlDocument = Jsoup.parse(data);
|
htmlDocument = Jsoup.parse(data);
|
||||||
el = htmlDocument.select("form").first();
|
el = htmlDocument.select("form").first();
|
||||||
samlInput = el.select("input[name=SAMLResponse]").first();
|
samlInput = el.select("input[name=SAMLResponse]").first();
|
||||||
|
|
||||||
logger.debug("Step 6 : post the SAMLresponse to finish the authentication");
|
logger.debug("Step 6: post the SAMLresponse to finish the authentication");
|
||||||
result = httpClient.POST(el.attr("action")).content(getFormContent("SAMLResponse", samlInput.attr("value")))
|
result = httpClient.POST(el.attr("action")).content(getFormContent("SAMLResponse", samlInput.attr("value")))
|
||||||
.send();
|
.send();
|
||||||
if (result.getStatus() != 302) {
|
if (result.getStatus() != HttpStatus.FOUND_302) {
|
||||||
throw new LinkyException("Connection failed step 6");
|
throw new LinkyException("Connection failed step 6");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug("Step 7: retrieve cookieKey");
|
||||||
|
result = httpClient.GET(USER_INFO_CONTRACT_URL);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
HashMap<String, String> hashRes = gson.fromJson(result.getContentAsString(), HashMap.class);
|
||||||
|
|
||||||
|
String cookieKey;
|
||||||
|
if (hashRes != null && hashRes.containsKey("cnAlex")) {
|
||||||
|
cookieKey = "personne_for_" + hashRes.get("cnAlex");
|
||||||
|
} else {
|
||||||
|
throw new LinkyException("Connection failed step 7, missing cookieKey");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HttpCookie> lCookie = httpClient.getCookieStore().getCookies();
|
||||||
|
Optional<HttpCookie> cookie = lCookie.stream().filter(it -> it.getName().contains(cookieKey)).findFirst();
|
||||||
|
|
||||||
|
String cookieVal = cookie.map(HttpCookie::getValue)
|
||||||
|
.orElseThrow(() -> new LinkyException("Connection failed step 7, missing cookieVal"));
|
||||||
|
|
||||||
|
addCookie(cookieKey, cookieVal);
|
||||||
|
|
||||||
connected = true;
|
connected = true;
|
||||||
} catch (InterruptedException | TimeoutException | ExecutionException | JsonSyntaxException e) {
|
} catch (InterruptedException | TimeoutException | ExecutionException | JsonSyntaxException e) {
|
||||||
throw new LinkyException(e, "Error opening connection with Enedis webservice");
|
throw new LinkyException(e, "Error opening connection with Enedis webservice");
|
||||||
@ -203,76 +235,64 @@ public class EnedisHttpApi {
|
|||||||
return new FormContentProvider(fields);
|
return new FormContentProvider(fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getData(String url) throws LinkyException {
|
private String getContent(String url) throws LinkyException {
|
||||||
try {
|
try {
|
||||||
ContentResponse result = httpClient.GET(url);
|
Request request = httpClient.newRequest(url)
|
||||||
if (result.getStatus() != 200) {
|
.agent("Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0");
|
||||||
throw new LinkyException("Error requesting '%s' : %s", url, result.getContentAsString());
|
request = request.method(HttpMethod.GET);
|
||||||
|
ContentResponse result = request.send();
|
||||||
|
if (result.getStatus() != HttpStatus.OK_200) {
|
||||||
|
throw new LinkyException("Error requesting '%s': %s", url, result.getContentAsString());
|
||||||
}
|
}
|
||||||
return result.getContentAsString();
|
String content = result.getContentAsString();
|
||||||
|
logger.trace("getContent returned {}", content);
|
||||||
|
return content;
|
||||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||||
throw new LinkyException(e, "Error getting url : '%s'", url);
|
throw new LinkyException(e, "Error getting url: '%s'", url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public PrmInfo getPrmInfo() throws LinkyException {
|
private <T> T getData(String url, Class<T> clazz) throws LinkyException {
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
initialize();
|
initialize();
|
||||||
}
|
}
|
||||||
String data = getData(PRM_INFO_URL);
|
String data = getContent(url);
|
||||||
if (data.isEmpty()) {
|
if (data.isEmpty()) {
|
||||||
throw new LinkyException("Requesting '%s' returned an empty response", PRM_INFO_URL);
|
throw new LinkyException("Requesting '%s' returned an empty response", url);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
PrmInfo[] prms = gson.fromJson(data, PrmInfo[].class);
|
return Objects.requireNonNull(gson.fromJson(data, clazz));
|
||||||
if (prms == null || prms.length < 1) {
|
|
||||||
throw new LinkyException("Invalid prms data received");
|
|
||||||
}
|
|
||||||
return prms[0];
|
|
||||||
} catch (JsonSyntaxException e) {
|
} catch (JsonSyntaxException e) {
|
||||||
logger.debug("invalid JSON response not matching PrmInfo[].class: {}", data);
|
logger.debug("Invalid JSON response not matching {}: {}", clazz.getName(), data);
|
||||||
throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", PRM_INFO_URL);
|
throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", 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");
|
||||||
|
}
|
||||||
|
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 UserInfo getUserInfo() throws LinkyException {
|
public UserInfo getUserInfo() throws LinkyException {
|
||||||
if (!connected) {
|
return getData(USER_INFO_URL, UserInfo.class);
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
String data = getData(USER_INFO_URL);
|
|
||||||
if (data.isEmpty()) {
|
|
||||||
throw new LinkyException("Requesting '%s' returned an empty response", USER_INFO_URL);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return Objects.requireNonNull(gson.fromJson(data, UserInfo.class));
|
|
||||||
} catch (JsonSyntaxException e) {
|
|
||||||
logger.debug("invalid JSON response not matching UserInfo.class: {}", data);
|
|
||||||
throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", USER_INFO_URL);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Consumption getMeasures(String userId, String prmId, LocalDate from, LocalDate to, String request)
|
private Consumption getMeasures(String userId, String prmId, LocalDate from, LocalDate to, String request)
|
||||||
throws LinkyException {
|
throws LinkyException {
|
||||||
String url = String.format(MEASURE_URL, userId, prmId, request, from.format(API_DATE_FORMAT),
|
String url = String.format(MEASURE_URL, userId, prmId, request, from.format(API_DATE_FORMAT),
|
||||||
to.format(API_DATE_FORMAT));
|
to.format(API_DATE_FORMAT));
|
||||||
if (!connected) {
|
ConsumptionReport report = getData(url, ConsumptionReport.class);
|
||||||
initialize();
|
return report.firstLevel.consumptions;
|
||||||
}
|
|
||||||
String data = getData(url);
|
|
||||||
if (data.isEmpty()) {
|
|
||||||
throw new LinkyException("Requesting '%s' returned an empty response", url);
|
|
||||||
}
|
|
||||||
logger.trace("getData returned {}", data);
|
|
||||||
try {
|
|
||||||
ConsumptionReport report = gson.fromJson(data, ConsumptionReport.class);
|
|
||||||
if (report == null) {
|
|
||||||
throw new LinkyException("No report data received");
|
|
||||||
}
|
|
||||||
return report.firstLevel.consumptions;
|
|
||||||
} catch (JsonSyntaxException e) {
|
|
||||||
logger.debug("invalid JSON response not matching ConsumptionReport.class: {}", data);
|
|
||||||
throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", url);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Consumption getEnergyData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
|
public Consumption getEnergyData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* 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 java.util.ArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link PrmDetail} holds detailed informations about prm configuration
|
||||||
|
*
|
||||||
|
* @author Gaël L'hopital - Initial contribution
|
||||||
|
*/
|
||||||
|
public class PrmDetail {
|
||||||
|
public record Adresse(String ligne2, String ligne3, String ligne4, String ligne5, String ligne6) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DicEntry(String code, String libelle) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Measure(String unite, String valeur) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record AlimentationPrincipale(Object puissanceRaccordementInjection,
|
||||||
|
Measure puissanceRaccordementSoutirage) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Compteur(boolean accessibilite, boolean ticActivee, boolean ticStandard) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Contrat(DicEntry typeContrat, String referenceContrat) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Disjoncteur(DicEntry calibre) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DispositifComptage(DicEntry typeComptage) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record GrilleFournisseur(DicEntry calendrier, Object classeTemporelle) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record InformationsContractuelles(Contrat contrat, DicEntry etatContractuel, SiContractuel siContractuel) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SiContractuel(DicEntry application) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SituationAlimentationDto(AlimentationPrincipale alimentationPrincipale) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SituationComptageDto(ArrayList<Compteur> compteurs, Disjoncteur disjoncteur,
|
||||||
|
DispositifComptage dispositifComptage) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SituationContractuelleDto(InformationsContractuelles informationsContractuelles,
|
||||||
|
StructureTarifaire structureTarifaire, String fournisseur, DicEntry segment) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record StructureTarifaire(Measure puissanceSouscrite, GrilleFournisseur grilleFournisseur) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SyntheseContractuelleDto(DicEntry niveauOuvertureServices) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Adresse adresse;
|
||||||
|
public String segment;
|
||||||
|
public SyntheseContractuelleDto syntheseContractuelleDto;
|
||||||
|
public SituationContractuelleDto[] situationContractuelleDtos;
|
||||||
|
public SituationAlimentationDto situationAlimentationDto;
|
||||||
|
public SituationComptageDto situationComptageDto;
|
||||||
|
}
|
@ -13,35 +13,11 @@
|
|||||||
package org.openhab.binding.linky.internal.dto;
|
package org.openhab.binding.linky.internal.dto;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link UserInfo} holds informations about energy delivery point
|
* The {@link UserInfo} holds ids of existing Prms
|
||||||
*
|
*
|
||||||
* @author Gaël L'hopital - Initial contribution
|
* @author Gaël L'hopital - Initial contribution
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class PrmInfo {
|
public class PrmInfo {
|
||||||
public class Adresse {
|
public String idPrm;
|
||||||
public Object adresseLigneUn;
|
|
||||||
public String adresseLigneDeux;
|
|
||||||
public Object adresseLigneTrois;
|
|
||||||
public String adresseLigneQuatre;
|
|
||||||
public Object adresseLigneCinq;
|
|
||||||
public String adresseLigneSix;
|
|
||||||
public String adresseLigneSept;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String prmId;
|
|
||||||
public String dateFinRole;
|
|
||||||
public String segment;
|
|
||||||
public Adresse adresse;
|
|
||||||
public String typeCompteur;
|
|
||||||
public String niveauOuvertureServices;
|
|
||||||
public String communiquant;
|
|
||||||
public long dateSoutirage;
|
|
||||||
public String dateInjection;
|
|
||||||
public int departement;
|
|
||||||
public int puissanceSouscrite;
|
|
||||||
public String codeCalendrier;
|
|
||||||
public String codeTitulaire;
|
|
||||||
public boolean collecteActivee;
|
|
||||||
public boolean multiTitulaire;
|
|
||||||
}
|
}
|
||||||
|
@ -35,10 +35,13 @@ import org.openhab.binding.linky.internal.api.EnedisHttpApi;
|
|||||||
import org.openhab.binding.linky.internal.api.ExpiringDayCache;
|
import org.openhab.binding.linky.internal.api.ExpiringDayCache;
|
||||||
import org.openhab.binding.linky.internal.dto.ConsumptionReport.Aggregate;
|
import org.openhab.binding.linky.internal.dto.ConsumptionReport.Aggregate;
|
||||||
import org.openhab.binding.linky.internal.dto.ConsumptionReport.Consumption;
|
import org.openhab.binding.linky.internal.dto.ConsumptionReport.Consumption;
|
||||||
|
import org.openhab.binding.linky.internal.dto.PrmDetail;
|
||||||
import org.openhab.binding.linky.internal.dto.PrmInfo;
|
import org.openhab.binding.linky.internal.dto.PrmInfo;
|
||||||
|
import org.openhab.binding.linky.internal.dto.UserInfo;
|
||||||
import org.openhab.core.i18n.LocaleProvider;
|
import org.openhab.core.i18n.LocaleProvider;
|
||||||
import org.openhab.core.library.types.DateTimeType;
|
import org.openhab.core.library.types.DateTimeType;
|
||||||
import org.openhab.core.library.types.QuantityType;
|
import org.openhab.core.library.types.QuantityType;
|
||||||
|
import org.openhab.core.library.unit.MetricPrefix;
|
||||||
import org.openhab.core.library.unit.Units;
|
import org.openhab.core.library.unit.Units;
|
||||||
import org.openhab.core.thing.ChannelUID;
|
import org.openhab.core.thing.ChannelUID;
|
||||||
import org.openhab.core.thing.Thing;
|
import org.openhab.core.thing.Thing;
|
||||||
@ -157,9 +160,13 @@ public class LinkyHandler extends BaseThingHandler {
|
|||||||
updateStatus(ThingStatus.ONLINE);
|
updateStatus(ThingStatus.ONLINE);
|
||||||
|
|
||||||
if (thing.getProperties().isEmpty()) {
|
if (thing.getProperties().isEmpty()) {
|
||||||
PrmInfo prmInfo = api.getPrmInfo();
|
UserInfo userInfo = api.getUserInfo();
|
||||||
updateProperties(Map.of(USER_ID, api.getUserInfo().userProperties.internId, PUISSANCE,
|
PrmInfo prmInfo = api.getPrmInfo(userInfo.userProperties.internId);
|
||||||
prmInfo.puissanceSouscrite + " kVA", PRM_ID, prmInfo.prmId));
|
PrmDetail details = api.getPrmDetails(userInfo.userProperties.internId, prmInfo.idPrm);
|
||||||
|
updateProperties(Map.of(USER_ID, userInfo.userProperties.internId, PUISSANCE,
|
||||||
|
details.situationContractuelleDtos[0].structureTarifaire().puissanceSouscrite().valeur()
|
||||||
|
+ " kVA",
|
||||||
|
PRM_ID, prmInfo.idPrm));
|
||||||
}
|
}
|
||||||
|
|
||||||
prmId = thing.getProperties().get(PRM_ID);
|
prmId = thing.getProperties().get(PRM_ID);
|
||||||
@ -204,7 +211,7 @@ public class LinkyHandler extends BaseThingHandler {
|
|||||||
if (isLinked(PEAK_POWER) || isLinked(PEAK_TIMESTAMP)) {
|
if (isLinked(PEAK_POWER) || isLinked(PEAK_TIMESTAMP)) {
|
||||||
cachedPowerData.getValue().ifPresentOrElse(values -> {
|
cachedPowerData.getValue().ifPresentOrElse(values -> {
|
||||||
Aggregate days = values.aggregats.days;
|
Aggregate days = values.aggregats.days;
|
||||||
updateVAChannel(PEAK_POWER, days.datas.get(days.datas.size() - 1));
|
updatekVAChannel(PEAK_POWER, days.datas.get(days.datas.size() - 1));
|
||||||
updateState(PEAK_TIMESTAMP, new DateTimeType(days.periodes.get(days.datas.size() - 1).dateDebut));
|
updateState(PEAK_TIMESTAMP, new DateTimeType(days.periodes.get(days.datas.size() - 1).dateDebut));
|
||||||
}, () -> {
|
}, () -> {
|
||||||
updateKwhChannel(PEAK_POWER, Double.NaN);
|
updateKwhChannel(PEAK_POWER, Double.NaN);
|
||||||
@ -213,6 +220,19 @@ public class LinkyHandler extends BaseThingHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setCurrentAndPrevious(Aggregate periods, String currentChannel, String previousChannel) {
|
||||||
|
double currentValue = 0.0;
|
||||||
|
double previousValue = 0.0;
|
||||||
|
if (!periods.datas.isEmpty()) {
|
||||||
|
currentValue = periods.datas.get(periods.datas.size() - 1);
|
||||||
|
if (periods.datas.size() > 1) {
|
||||||
|
previousValue = periods.datas.get(periods.datas.size() - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateKwhChannel(currentChannel, currentValue);
|
||||||
|
updateKwhChannel(previousChannel, previousValue);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request new dayly/weekly data and updates channels
|
* Request new dayly/weekly data and updates channels
|
||||||
*/
|
*/
|
||||||
@ -221,17 +241,7 @@ public class LinkyHandler extends BaseThingHandler {
|
|||||||
cachedDailyData.getValue().ifPresentOrElse(values -> {
|
cachedDailyData.getValue().ifPresentOrElse(values -> {
|
||||||
Aggregate days = values.aggregats.days;
|
Aggregate days = values.aggregats.days;
|
||||||
updateKwhChannel(YESTERDAY, days.datas.get(days.datas.size() - 1));
|
updateKwhChannel(YESTERDAY, days.datas.get(days.datas.size() - 1));
|
||||||
int idxLast = days.periodes.get(days.periodes.size() - 1).dateDebut.get(weekFields.dayOfWeek()) == 7 ? 2
|
setCurrentAndPrevious(values.aggregats.weeks, THIS_WEEK, LAST_WEEK);
|
||||||
: 1;
|
|
||||||
Aggregate weeks = values.aggregats.weeks;
|
|
||||||
if (weeks.datas.size() > idxLast) {
|
|
||||||
updateKwhChannel(LAST_WEEK, weeks.datas.get(idxLast));
|
|
||||||
}
|
|
||||||
if (weeks.datas.size() > (idxLast + 1)) {
|
|
||||||
updateKwhChannel(THIS_WEEK, weeks.datas.get(idxLast + 1));
|
|
||||||
} else {
|
|
||||||
updateKwhChannel(THIS_WEEK, 0.0);
|
|
||||||
}
|
|
||||||
}, () -> {
|
}, () -> {
|
||||||
updateKwhChannel(YESTERDAY, Double.NaN);
|
updateKwhChannel(YESTERDAY, Double.NaN);
|
||||||
if (ZonedDateTime.now().get(weekFields.dayOfWeek()) == 1) {
|
if (ZonedDateTime.now().get(weekFields.dayOfWeek()) == 1) {
|
||||||
@ -249,22 +259,15 @@ public class LinkyHandler extends BaseThingHandler {
|
|||||||
*/
|
*/
|
||||||
private synchronized void updateMonthlyData() {
|
private synchronized void updateMonthlyData() {
|
||||||
if (isLinked(LAST_MONTH) || isLinked(THIS_MONTH)) {
|
if (isLinked(LAST_MONTH) || isLinked(THIS_MONTH)) {
|
||||||
cachedMonthlyData.getValue().ifPresentOrElse(values -> {
|
cachedMonthlyData.getValue().ifPresentOrElse(
|
||||||
Aggregate months = values.aggregats.months;
|
values -> setCurrentAndPrevious(values.aggregats.months, THIS_MONTH, LAST_MONTH), () -> {
|
||||||
updateKwhChannel(LAST_MONTH, months.datas.get(0));
|
if (ZonedDateTime.now().getDayOfMonth() == 1) {
|
||||||
if (months.datas.size() > 1) {
|
updateKwhChannel(THIS_MONTH, 0.0);
|
||||||
updateKwhChannel(THIS_MONTH, months.datas.get(1));
|
updateKwhChannel(LAST_MONTH, Double.NaN);
|
||||||
} else {
|
} else {
|
||||||
updateKwhChannel(THIS_MONTH, 0.0);
|
updateKwhChannel(THIS_MONTH, Double.NaN);
|
||||||
}
|
}
|
||||||
}, () -> {
|
});
|
||||||
if (ZonedDateTime.now().getDayOfMonth() == 1) {
|
|
||||||
updateKwhChannel(THIS_MONTH, 0.0);
|
|
||||||
updateKwhChannel(LAST_MONTH, Double.NaN);
|
|
||||||
} else {
|
|
||||||
updateKwhChannel(THIS_MONTH, Double.NaN);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,22 +276,15 @@ public class LinkyHandler extends BaseThingHandler {
|
|||||||
*/
|
*/
|
||||||
private synchronized void updateYearlyData() {
|
private synchronized void updateYearlyData() {
|
||||||
if (isLinked(LAST_YEAR) || isLinked(THIS_YEAR)) {
|
if (isLinked(LAST_YEAR) || isLinked(THIS_YEAR)) {
|
||||||
cachedYearlyData.getValue().ifPresentOrElse(values -> {
|
cachedYearlyData.getValue().ifPresentOrElse(
|
||||||
Aggregate years = values.aggregats.years;
|
values -> setCurrentAndPrevious(values.aggregats.years, THIS_YEAR, LAST_YEAR), () -> {
|
||||||
updateKwhChannel(LAST_YEAR, years.datas.get(0));
|
if (ZonedDateTime.now().getDayOfYear() == 1) {
|
||||||
if (years.datas.size() > 1) {
|
updateKwhChannel(THIS_YEAR, 0.0);
|
||||||
updateKwhChannel(THIS_YEAR, years.datas.get(1));
|
updateKwhChannel(LAST_YEAR, Double.NaN);
|
||||||
} else {
|
} else {
|
||||||
updateKwhChannel(THIS_YEAR, 0.0);
|
updateKwhChannel(THIS_YEAR, Double.NaN);
|
||||||
}
|
}
|
||||||
}, () -> {
|
});
|
||||||
if (ZonedDateTime.now().getDayOfYear() == 1) {
|
|
||||||
updateKwhChannel(THIS_YEAR, 0.0);
|
|
||||||
updateKwhChannel(LAST_YEAR, Double.NaN);
|
|
||||||
} else {
|
|
||||||
updateKwhChannel(THIS_YEAR, Double.NaN);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,9 +294,10 @@ public class LinkyHandler extends BaseThingHandler {
|
|||||||
Double.isNaN(consumption) ? UnDefType.UNDEF : new QuantityType<>(consumption, Units.KILOWATT_HOUR));
|
Double.isNaN(consumption) ? UnDefType.UNDEF : new QuantityType<>(consumption, Units.KILOWATT_HOUR));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateVAChannel(String channelId, double power) {
|
private void updatekVAChannel(String channelId, double power) {
|
||||||
logger.debug("Update channel {} with {}", channelId, power);
|
logger.debug("Update channel {} with {}", channelId, power);
|
||||||
updateState(channelId, Double.isNaN(power) ? UnDefType.UNDEF : new QuantityType<>(power, Units.VOLT_AMPERE));
|
updateState(channelId, Double.isNaN(power) ? UnDefType.UNDEF
|
||||||
|
: new QuantityType<>(power, MetricPrefix.KILO(Units.VOLT_AMPERE)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -96,10 +96,10 @@
|
|||||||
</channel-type>
|
</channel-type>
|
||||||
|
|
||||||
<channel-type id="power">
|
<channel-type id="power">
|
||||||
<item-type>Number:Power</item-type>
|
<item-type unitHint="kVA">Number:Power</item-type>
|
||||||
<label>Yesterday Peak Power</label>
|
<label>Yesterday Peak Power</label>
|
||||||
<description>Maximum power usage yesterday</description>
|
<description>Maximum power usage yesterday</description>
|
||||||
<state readOnly="true" pattern="%.3f %unit%"/>
|
<state readOnly="true" pattern="%.2f %unit%"/>
|
||||||
</channel-type>
|
</channel-type>
|
||||||
|
|
||||||
<channel-type id="timestamp">
|
<channel-type id="timestamp">
|
||||||
|
Loading…
Reference in New Issue
Block a user