<artifactId>org.openhab.binding.lifx</artifactId>
<version>${project.version}</version>
</dependency>
- <!-- linky binding suppressed from the distribution until it is fixed
- <dependency>
+ <dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.linky</artifactId>
<version>${project.version}</version>
- </dependency>
- -->
+ </dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.linuxinput</artifactId>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.linky-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+ <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+ <feature name="openhab-binding-linky" description="Linky Binding" version="${project.version}">
+ <feature>openhab-runtime-base</feature>
+ <bundle dependency="true">mvn:org.jsoup/jsoup/1.8.3</bundle>
+ <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.linky/${project.version}</bundle>
+ </feature>
+</features>
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<features name="org.openhab.binding.linky-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
- <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
-
- <feature name="openhab-binding-linky" description="Linky Binding" version="${project.version}">
- <feature>openhab-runtime-base</feature>
- <bundle dependency="true">mvn:org.jsoup/jsoup/1.8.3</bundle>
- <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.linky/${project.version}</bundle>
- </feature>
-</features>
*/
package org.openhab.binding.linky.internal;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
/**
* The {@link LinkyConfiguration} is the class used to match the
* thing configuration.
*
* @author Gaël L'hopital - Initial contribution
*/
+@NonNullByDefault
public class LinkyConfiguration {
public static final String INTERNAL_AUTH_ID = "internalAuthId";
- public String username;
- public String password;
- public String internalAuthId;
+ public String username = "";
+ public String password = "";
+ public String internalAuthId = "";
+
+ public boolean seemsValid() {
+ return !username.isBlank() && !password.isBlank() && !internalAuthId.isBlank();
+ }
}
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.linky")
public class LinkyHandlerFactory extends BaseThingHandlerFactory {
+ private static final DateTimeFormatter LINKY_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSX");
+
private final Logger logger = LoggerFactory.getLogger(LinkyHandlerFactory.class);
- private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSX");
private final LocaleProvider localeProvider;
private final Gson gson;
private final HttpClient httpClient;
this.localeProvider = localeProvider;
this.gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class,
(JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
- .parse(json.getAsJsonPrimitive().getAsString(), formatter))
+ .parse(json.getAsJsonPrimitive().getAsString(), LINKY_FORMATTER))
.create();
this.httpClient = httpClientFactory.createHttpClient(LinkyBindingConstants.BINDING_ID);
}
private final Logger logger = LoggerFactory.getLogger(EnedisHttpApi.class);
private final Gson gson;
private final HttpClient httpClient;
- private final LinkyConfiguration config;
private boolean connected = false;
+ private final CookieStore cookieStore;
+ private final LinkyConfiguration config;
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 {
- addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId);
-
logger.debug("Starting login process for user : {}", config.username);
try {
logger.debug(
"Step 3 : auth1 - retrieve the template, thanks to cookie internalAuthId, user is already set");
- result = httpClient.POST(url).send();
+ result = httpClient.POST(url).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());
}
AuthData authData = gson.fromJson(result.getContentAsString(), AuthData.class);
- if (authData.callbacks.size() < 2 || authData.callbacks.get(0).input.size() == 0
- || authData.callbacks.get(1).input.size() == 0 || !config.username
+ 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())) {
throw new LinkyException("Authentication error, the authentication_cookie is probably wrong");
}
authData.callbacks.get(1).input.get(0).value = config.password;
- url = "https://mon-compte.enedis.fr/auth/json/authenticate?realm=/enedis&spEntityID=SP-ODW-PROD&goto=/auth/SSOPOST/metaAlias/enedis/providerIDP?ReqID%"
+ 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")
+ .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());
}
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);
}
}
- public String getLocation(ContentResponse response) {
+ private String getLocation(ContentResponse response) {
return response.getHeaders().get(HttpHeader.LOCATION);
}
- public void disconnect() throws LinkyException {
+ private void disconnect() throws LinkyException {
if (connected) {
logger.debug("Logout process");
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));
- CookieStore cookieStore = httpClient.getCookieStore();
cookieStore.removeAll();
connected = false;
} catch (InterruptedException | ExecutionException | TimeoutException e) {
}
private void addCookie(String key, String value) {
- CookieStore cookieStore = httpClient.getCookieStore();
HttpCookie cookie = new HttpCookie(key, value);
cookie.setDomain(".enedis.fr");
cookie.setPath("/");
}
try {
PrmInfo[] prms = gson.fromJson(data, PrmInfo[].class);
+ if (prms == null || prms.length < 1) {
+ throw new LinkyException("Invalid prms data received");
+ }
return prms[0];
} catch (JsonSyntaxException e) {
logger.debug("invalid JSON response not matching PrmInfo[].class: {}", data);
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);
return Optional.ofNullable(cachedValue);
}
- /**
- * Puts a new value into the cache.
- *
- * @param value the new value
- */
- public final synchronized void putValue(@Nullable V value) {
- this.value = value;
- expiresAt = calcNextExpiresAt();
- }
-
- /**
- * Invalidates the value in the cache.
- */
- public final synchronized void invalidateValue() {
- value = null;
- expiresAt = calcAlreadyExpired();
- }
-
/**
* Refreshes and returns the value in the cache.
*
*/
package org.openhab.binding.linky.internal.dto;
-import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
public @Nullable Object value;
public @Nullable String valueAsString() {
- if (value instanceof String) {
- return (String) value;
- }
- return null;
+ return (value instanceof String) ? (String) value : null;
}
}
public @Nullable String type;
- public List<NameValuePair> output = new ArrayList<>();
- public List<NameValuePair> input = new ArrayList<>();
+ public List<NameValuePair> output = List.of();
+ public List<NameValuePair> input = List.of();
}
public @Nullable String authId;
public @Nullable String template;
public @Nullable String stage;
public @Nullable String header;
- public List<AuthDataCallBack> callbacks = new ArrayList<>();
+ public List<AuthDataCallBack> callbacks = List.of();
}
@NonNullByDefault
public class LinkyHandler extends BaseThingHandler {
- private final Logger logger = LoggerFactory.getLogger(LinkyHandler.class);
-
private static final int REFRESH_FIRST_HOUR_OF_DAY = 1;
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;
updateStatus(ThingStatus.UNKNOWN);
LinkyConfiguration config = getConfigAs(LinkyConfiguration.class);
- enedisApi = new EnedisHttpApi(config, gson, httpClient);
-
- scheduler.submit(() -> {
- try {
- EnedisHttpApi api = this.enedisApi;
- if (api != null) {
+ if (config.seemsValid()) {
+ enedisApi = new EnedisHttpApi(config, gson, httpClient);
+ scheduler.submit(() -> {
+ try {
+ EnedisHttpApi api = this.enedisApi;
api.initialize();
updateStatus(ThingStatus.ONLINE);
refreshJob = scheduler.scheduleWithFixedDelay(this::updateData,
ChronoUnit.MINUTES.between(now, nextDayFirstTimeUpdate) % REFRESH_INTERVAL_IN_MIN + 1,
REFRESH_INTERVAL_IN_MIN, TimeUnit.MINUTES);
- } else {
- throw new LinkyException("Enedis Api is not initialized");
+ } catch (LinkyException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
- } catch (LinkyException e) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
- }
- });
+ });
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "Username, password and authId are mandatory");
+ }
}
/**
return consumption;
}
- public void checkData(Consumption consumption) throws LinkyException {
+ private void checkData(Consumption consumption) throws LinkyException {
if (consumption.aggregats.days.periodes.size() == 0) {
throw new LinkyException("invalid consumptions data: no day period");
}
<module>org.openhab.binding.lgtvserial</module>
<module>org.openhab.binding.lgwebos</module>
<module>org.openhab.binding.lifx</module>
- <!-- linky binding suppressed from the distribution until it is fixed
- <module>org.openhab.binding.linky</module>
- -->
+ <module>org.openhab.binding.linky</module>
<module>org.openhab.binding.linuxinput</module>
<module>org.openhab.binding.lirc</module>
<module>org.openhab.binding.logreader</module>