@NonNullByDefault
public class EnedisHttpApi {
private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
- private static final String URL_APPS_LINCS = "https://apps.lincs.enedis.fr";
- private static final String URL_MON_COMPTE = "https://mon-compte.enedis.fr";
- private static final String URL_ENEDIS_AUTHENTICATE = URL_APPS_LINCS
- + "/authenticate?target=https://mon-compte-particulier.enedis.fr/suivi-de-mesure/";
- private static final String URL_COOKIE = "https://mon-compte-particulier.enedis.fr";
+ private static final String ENEDIS_DOMAIN = ".enedis.fr";
+ private static final String URL_APPS_LINCS = "https://apps.lincs" + 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_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 URI COOKIE_URI = URI.create(URL_COMPTE_PART);
+ private static final Pattern REQ_PATTERN = Pattern.compile("ReqID%(.*?)%26");
private final Logger logger = LoggerFactory.getLogger(EnedisHttpApi.class);
private final Gson gson;
private final HttpClient httpClient;
- private boolean connected = false;
private final CookieStore cookieStore;
private final LinkyConfiguration config;
+ private boolean connected = false;
+
public EnedisHttpApi(LinkyConfiguration config, Gson gson, HttpClient httpClient) {
this.gson = gson;
this.httpClient = httpClient;
this.config = config;
this.cookieStore = httpClient.getCookieStore();
- addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId);
}
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 = getData(URL_ENEDIS_AUTHENTICATE);
}
logger.debug("Get the location and the ReqID");
- Pattern p = Pattern.compile("ReqID%(.*?)%26");
- Matcher m = p.matcher(getLocation(result));
+ 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 url = URL_MON_COMPTE
+ 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%3Dhttps://apps.lincs.enedis.fr/saml/SSO%26spEntityID%3DSP-ODW-PROD%26binding%3Durn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST&AMAuthCookie=";
+ + 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(url).header("X-NoSession", "true").header("X-Password", "anonymous")
+ 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 : " + result.getContentAsString());
+ throw new LinkyException("Connection failed step 3 - auth1 : %s", result.getContentAsString());
}
AuthData authData = gson.fromJson(result.getContentAsString(), AuthData.class);
}
authData.callbacks.get(1).input.get(0).value = config.password;
- url = URL_MON_COMPTE
- + "/auth/json/authenticate?realm=/enedis&spEntityID=SP-ODW-PROD&goto=/auth/SSOPOST/metaAlias/enedis/providerIDP?ReqID%"
- + reqId
- + "%26index%3Dnull%26acsURL%3Dhttps://apps.lincs.enedis.fr/saml/SSO%26spEntityID%3DSP-ODW-PROD%26binding%3Durn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST&AMAuthCookie=";
-
- logger.debug("Step 3 : auth2 - send the auth data");
- result = httpClient.POST(url).header(HttpHeader.CONTENT_TYPE, "application/json")
+ 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 : " + result.getContentAsString());
+ throw new LinkyException("Connection failed step 3 - auth2 : %s", result.getContentAsString());
}
AuthResult authResult = gson.fromJson(result.getContentAsString(), AuthResult.class);
logger.debug("Add the tokenId cookie");
addCookie("enedisExt", authResult.tokenId);
- logger.debug("Step 4 : retrieve the SAMLresponse");
+ logger.debug("Step 5 : retrieve the SAMLresponse");
data = getData(URL_MON_COMPTE + "/" + authResult.successUrl);
htmlDocument = Jsoup.parse(data);
el = htmlDocument.select("form").first();
samlInput = el.select("input[name=SAMLResponse]").first();
- logger.debug("Step 5 : 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")))
.send();
if (result.getStatus() != 302) {
}
connected = true;
} catch (InterruptedException | TimeoutException | ExecutionException | JsonSyntaxException e) {
- throw new LinkyException("Error opening connection with Enedis webservice", e);
+ throw new LinkyException(e, "Error opening connection with Enedis webservice");
}
}
private void disconnect() throws LinkyException {
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));
- location = getLocation(httpClient.GET(location));
+ getLocation(httpClient.GET(location));
cookieStore.removeAll();
- connected = false;
} catch (InterruptedException | ExecutionException | TimeoutException e) {
- throw new LinkyException("Error while disconnecting from Enedis webservice", e);
+ throw new LinkyException(e, "Error while disconnecting from Enedis webservice");
}
}
}
private void addCookie(String key, String value) {
HttpCookie cookie = new HttpCookie(key, value);
- cookie.setDomain(".enedis.fr");
+ cookie.setDomain(ENEDIS_DOMAIN);
cookie.setPath("/");
- cookieStore.add(URI.create(URL_COOKIE), cookie);
+ cookieStore.add(COOKIE_URI, cookie);
}
private FormContentProvider getFormContent(String fieldName, String fieldValue) {
try {
ContentResponse result = httpClient.GET(url);
if (result.getStatus() != 200) {
- throw new LinkyException(String.format("Error requesting '%s' : %s", url, result.getContentAsString()));
+ throw new LinkyException("Error requesting '%s' : %s", url, result.getContentAsString());
}
return result.getContentAsString();
} catch (InterruptedException | ExecutionException | TimeoutException e) {
- throw new LinkyException(String.format("Error getting url : '%s'", url), e);
+ throw new LinkyException(e, "Error getting url : '%s'", url);
}
}
if (!connected) {
initialize();
}
- final String prm_info_url = URL_APPS_LINCS + "/mes-mesures/api/private/v1/personnes/null/prms";
- String data = getData(prm_info_url);
+ String data = getData(PRM_INFO_URL);
if (data.isEmpty()) {
- throw new LinkyException(String.format("Requesting '%s' returned an empty response", prm_info_url));
+ throw new LinkyException("Requesting '%s' returned an empty response", PRM_INFO_URL);
}
try {
PrmInfo[] prms = gson.fromJson(data, PrmInfo[].class);
return prms[0];
} catch (JsonSyntaxException e) {
logger.debug("invalid JSON response not matching PrmInfo[].class: {}", data);
- throw new LinkyException(String.format("Requesting '%s' returned an invalid JSON response : %s",
- prm_info_url, e.getMessage()), e);
+ throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", PRM_INFO_URL);
}
}
if (!connected) {
initialize();
}
- final String user_info_url = URL_APPS_LINCS + "/userinfos";
- String data = getData(user_info_url);
+ String data = getData(USER_INFO_URL);
if (data.isEmpty()) {
- throw new LinkyException(String.format("Requesting '%s' returned an empty response", user_info_url));
+ 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(String.format("Requesting '%s' returned an invalid JSON response : %s",
- user_info_url, e.getMessage()), e);
+ 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)
throws LinkyException {
- final String measure_url = URL_APPS_LINCS
- + "/mes-mesures/api/private/v1/personnes/%s/prms/%s/donnees-%s?dateDebut=%s&dateFin=%s&mesuretypecode=CONS";
- 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));
+ if (!connected) {
+ initialize();
+ }
String data = getData(url);
if (data.isEmpty()) {
- throw new LinkyException(String.format("Requesting '%s' returned an empty response", url));
+ throw new LinkyException("Requesting '%s' returned an empty response", url);
}
logger.trace("getData returned {}", data);
try {
return report.firstLevel.consumptions;
} catch (JsonSyntaxException e) {
logger.debug("invalid JSON response not matching ConsumptionReport.class: {}", data);
- throw new LinkyException(
- String.format("Requesting '%s' returned an invalid JSON response : %s", url, e.getMessage()), e);
+ 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 {
- if (!connected) {
- initialize();
- }
return getMeasures(userId, prmId, from, to, "energie");
}
public Consumption getPowerData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
- if (!connected) {
- initialize();
- }
return getMeasures(userId, prmId, from, to, "pmax");
}
}
import java.time.temporal.ChronoUnit;
import java.time.temporal.WeekFields;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
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.PrmInfo;
-import org.openhab.binding.linky.internal.dto.UserInfo;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.QuantityType;
private static final int REFRESH_INTERVAL_IN_MIN = 120;
private final Logger logger = LoggerFactory.getLogger(LinkyHandler.class);
-
private final HttpClient httpClient;
private final Gson gson;
private final WeekFields weekFields;
- private @Nullable ScheduledFuture<?> refreshJob;
- private @Nullable EnedisHttpApi enedisApi;
-
private final ExpiringDayCache<Consumption> cachedDailyData;
private final ExpiringDayCache<Consumption> cachedPowerData;
private final ExpiringDayCache<Consumption> cachedMonthlyData;
private final ExpiringDayCache<Consumption> cachedYearlyData;
+ private @Nullable ScheduledFuture<?> refreshJob;
+ private @Nullable EnedisHttpApi enedisApi;
+
private @NonNullByDefault({}) String prmId;
private @NonNullByDefault({}) String userId;
});
this.cachedPowerData = new ExpiringDayCache<>("power cache", REFRESH_FIRST_HOUR_OF_DAY, () -> {
- LocalDate to = LocalDate.now().plusDays(1);
- LocalDate from = to.minusDays(2);
+ LocalDate to = LocalDate.now();
+ LocalDate from = to.minusDays(1);
Consumption consumption = getPowerData(from, to);
if (consumption != null) {
logData(consumption.aggregats.days, "Day (peak)", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME,
updateStatus(ThingStatus.ONLINE);
if (thing.getProperties().isEmpty()) {
- Map<String, String> properties = new HashMap<>();
PrmInfo prmInfo = api.getPrmInfo();
- UserInfo userInfo = api.getUserInfo();
- properties.put(USER_ID, userInfo.userProperties.internId);
- properties.put(PUISSANCE, prmInfo.puissanceSouscrite + " kVA");
- properties.put(PRM_ID, prmInfo.prmId);
- updateProperties(properties);
+ updateProperties(Map.of(USER_ID, api.getUserInfo().userProperties.internId, PUISSANCE,
+ prmInfo.puissanceSouscrite + " kVA", PRM_ID, prmInfo.prmId));
}
prmId = thing.getProperties().get(PRM_ID);
private void checkData(Consumption consumption) throws LinkyException {
if (consumption.aggregats.days.periodes.size() == 0) {
- throw new LinkyException("invalid consumptions data: no day period");
+ throw new LinkyException("Invalid consumptions data: no day period");
}
if (consumption.aggregats.days.periodes.size() != consumption.aggregats.days.datas.size()) {
- throw new LinkyException("invalid consumptions data: not one data for each day period");
+ throw new LinkyException("Invalid consumptions data: not any data for each day period");
}
if (consumption.aggregats.weeks.periodes.size() == 0) {
- throw new LinkyException("invalid consumptions data: no week period");
+ throw new LinkyException("Invalid consumptions data: no week period");
}
if (consumption.aggregats.weeks.periodes.size() != consumption.aggregats.weeks.datas.size()) {
- throw new LinkyException("invalid consumptions data: not one data for each week period");
+ throw new LinkyException("Invalid consumptions data: not any data for each week period");
}
if (consumption.aggregats.months.periodes.size() == 0) {
- throw new LinkyException("invalid consumptions data: no month period");
+ throw new LinkyException("Invalid consumptions data: no month period");
}
if (consumption.aggregats.months.periodes.size() != consumption.aggregats.months.datas.size()) {
- throw new LinkyException("invalid consumptions data: not one data for each month period");
+ throw new LinkyException("Invalid consumptions data: not any data for each month period");
}
if (consumption.aggregats.years.periodes.size() == 0) {
- throw new LinkyException("invalid consumptions data: no year period");
+ throw new LinkyException("Invalid consumptions data: no year period");
}
if (consumption.aggregats.years.periodes.size() != consumption.aggregats.years.datas.size()) {
- throw new LinkyException("invalid consumptions data: not one data for each year period");
+ throw new LinkyException("Invalid consumptions data: not any data for each year period");
}
}