This commit is contained in:
lo92fr 2025-01-09 11:09:17 +01:00 committed by GitHub
commit 10155a4115
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 5530 additions and 680 deletions

View File

@ -1,63 +1,341 @@
# Linky Binding
This binding uses the API provided by Enedis to retrieve your energy consumption data.
You need to create an Enedis account [here](https://espace-client-connexion.enedis.fr/auth/UI/Login?realm=particuliers) if you don't have one already.
You need to create an Enedis account [here](https://mon-compte-client.enedis.fr/) if you don't have one already.
Please ensure that you have accepted their conditions, and check that you can see graphs on the website.
Especially, check hourly view/graph. Enedis may ask for permission the first time to start collecting hourly data.
The binding will not provide these informations unless this step is ok.
This new binding version is able to use multiple bridge to access the consumption data.
You can use :
- The enedis-web bridge : this one will use the old Enedis API, base on the enedis web site to gather the data.
- The myelectricaldata bridge : this one will use the new Rest Enedis API. We will use the MyElectricalData proxy site to access the data.
- The enedis bridge : this one will also use the new Rest Enedis API, and will directly gather data from Enedis Site.
There is advantage and disadvantage for each method.
- Enedis-web bridge is the old way to go.
- MyelectricalData and enedis bridge both use new API format, less prone to change of the web site architecture.
- MyelectricalData bridge is handle by third party provider, but is stable.
- Enedis bridge use direct connection to Enedis, but currently required complex registration step with Enedis.
this limitation would certainly go away in near feature, that will make Enedis Bridge the preffered way to go.
## Supported Things
There is one supported thing : the `linky` thing is retrieving the consumption of your home from the [Linky electric meter](https://www.enedis.fr/linky-compteur-communicant).
You can have multiple linky thing in your setup if you have different house / linky linked to your account.
You can switch the thing from one bridge to another if you experience troubles with one bridge.
Data will be the quite the same ever bridge you use.
Only a few items from contract are not the same between web bridge and api bridge.
## Discovery
This binding does not provide discovery service.
This binding currently does not provide discovery service.
Perhaps will add auto linky discovery in a future version.
## Binding Configuration
The binding has no configuration options, all configuration is done at Thing level.
To retrieve data, Linky thing will be need to be linked to a LinkyBridge. LinkyBridge can be today select between enedis-web, myelectricaldata and enedis.
### enedis-web bridge
If you select enedis-web bridge, you will need :
- To create an Enedis account : https://mon-compte-client.enedis.fr/
- To fill the bridge with you information : username, password, and also InternalAuthId.
| Parameter | Description |
|----------------|--------------------------------|
| username | Your Enedis platform username. |
| password | Your Enedis platform password. |
| internalAuthId | The internal authID |
This version is now compatible with the new version of Enedis WEB API (deployed from june 2020).
To avoid the captcha login, it is necessary to log before on a classical browser (e.g Chrome, Firefox) and to retrieve the user cookies (internalAuthId).
Instructions given for Firefox :
1. Go to <https://mon-compte-client.enedis.fr/>.
1. Select "Particulier" in the drop down list and click on the "Connexion" button.
1. You'll be redirected to a page where you'll have to enter you Enedis account email address and check the "Je ne suis pas un robot" checkbox.
1. Clic on "Suivant".
1. In the login page, prefilled with your mail address, enter your Enedis account password and click on "Connexion à Espace Client Enedis".
1. You will be directed to your Enedis account environment. Get back to previous page in you browser.
1. Disconnect from your Enedis account
1. Repeat steps 1, 2. You should arrive directly on step 5, then open the developer tool window (F12) and select "Stockage" tab. In the "Cookies" entry, select "https://mon-compte-enedis.fr". You'll find an entry named "internalAuthId", copy this value in your openHAB configuration.
### myelectricaldata bridge
If you select MyElectricalData bridge, you will need :
- To create an Enedis account : https://mon-compte-client.enedis.fr/
Follow these steps to initialize the token. You can access the procedure from the connectlinky page available from your openhab: https://home.myopenhab.org/connectlinky/index.
- to select your provider
![connectlinky-index](doc/connectlinky-index.png)
<br/>
- To follow the two first step wizard, and click on the "access Enedis" button
![connectlinky-myelectricaldata-step1](doc/connectlinky-myelectricaldata-step1.png)<br/>
![connectlinky-myelectricaldata-step2](doc/connectlinky-myelectricaldata-step2.png)<br/>
- To login to your Enedis Account
![connectlinky-myelectricaldata-step2b](doc/connectlinky-myelectricaldata-step2b.png)<br/>
- To authorize data collection for your prmId. <br>
If you have multiple linky on your account like me, you will have to repeat the procedure for each linky.
Don't select the two linky in the same procedure, it will not work !
![connectlinky-myelectricaldata-step2c](doc/connectlinky-myelectricaldata-step2c.png)<br/>
- You will then be redirect to a confirmation page on MyElectricalData web site
![connectlinky-myelectricaldata-step2d](doc/connectlinky-myelectricaldata-step2d.png)<br/>
- Go back to your openhab with step3 : "connectlinky/myelectricaldata-step3", then select your prmId in combobox, and click "Retrieve Token"
![connectlinky-myelectricaldata-step3](doc/connectlinky-myelectricaldata-step3.png)<br/>
- Last, you will see this confirmation page if everything is everything is ok
![connectlinky-myelectricaldata-step3b](doc/connectlinky-myelectricaldata-step3b.png)<br/>
If you select enedis bridge, you will need :
- To create an Enedis account : https://mon-compte-client.enedis.fr/
Follow these steps to initialize the token. you can access the procedure from the connectlinky page available from your openhab: https://home.myopenhab.org/connectlinky/index.
- to select your provider
![enedis-index](doc/enedis-index.png)<br/>
- To follow the two first step wizard, and click on the "access Enedis" button
![connectlinky-enedis-step1](doc/connectlinky-enedis-step1.png)<br/>
![connectlinky-enedis-step2](doc/connectlinky-enedis-step2.png)<br/>
- To login to your Enedis Account
![connectlinky-enedis-step2b](doc/connectlinky-enedis-step2b.png)<br/>
- To authorize data collection for your prmId. <br>
If you have multiple linky on your account like me, you will have to repeat the procedure for each linky.
Don't select the two linky in the same procedure, it will not work !
![connectlinky-enedis-step2c](doc/connectlinky-enedis-step2c.png)<br/>
- Last, you will see this confirmation page if everything is everything is ok
![connectlinky-enedis-step3](doc/connectlinky-enedis-step3.png)<br/>
## Thing Configuration
The thing has the following configuration parameters:
| Parameter | Description |
|----------------|--------------------------------|
| username | Your Enedis platform username. |
| password | Your Enedis platform password. |
| internalAuthId | The internal authID |
| Parameter | Description |
|----------------|---------------------------------------------------------------------------------------------|
| prmId | The prmId link to the linky Handler. |
| token | Optional : need if a token necessary to access this Linky thing (use for MyElectricaldata) |
This version is now compatible with the new API of Enedis (deployed from june 2020).
To avoid the captcha login, it is necessary to log before on a classical browser (e.g Chrome, Firefox) and to retrieve the user cookies (internalAuthId).
Thing linky:linky:local "Compteur Linky" [ prmId="xxxx", token="yyyyyyyyyyyyyyyyyyyyyyy" ]
Instructions given for Firefox :
1. Go to <https://mon-compte-client.enedis.fr/>.
1. Select "Particulier" in the drop down list and click on the "Connexion" button.
1. You'll be redirected to a page where you'll have to enter you Enedis account email address and check the "Je ne suis pas un robot" checkbox.
1. Clic on "Suivant".
1. In the login page, prefilled with your mail address, enter your Enedis account password and click on "Connexion à Espace Client Enedis".
1. You will be directed to your Enedis account environment. Get back to previous page in you browser.
1. Disconnect from your Enedis account
1. Repeat steps 1, 2. You should arrive directly on step 5, then open the developer tool window (F12) and select "Stockage" tab. In the "Cookies" entry, select "https://mon-compte-enedis.fr". You'll find an entry named "internalAuthId", copy this value in your openHAB configuration.
## Channels
The information that is retrieved is available as these channels:
The information that is retrieved is available as many different groups.
| Channel ID | Item Type | Description |
|-------------------|---------------|------------------------------|
| daily#yesterday | Number:Energy | Yesterday energy usage |
| daily#power | Number:Power | Yesterday's peak power usage |
| daily#timestamp | DateTime | Timestamp of the power peak |
| weekly#thisWeek | Number:Energy | Current week energy usage |
| weekly#lastWeek | Number:Energy | Last week energy usage |
| monthly#thisMonth | Number:Energy | Current month energy usage |
| monthly#lastMonth | Number:Energy | Last month energy usage |
| yearly#thisYear | Number:Energy | Current year energy usage |
| yearly#lastYear | Number:Energy | Last year energy usage |
- The Main group will give information about the contract linked to this linky.
You will find the following channel:
| Channel ID | Item Type | Description |
|---------------------------------------------------|----------------|-----------------------------------------------|
| main#identitiy | String | The full name of the contract older |
| main#contractSubscribedPower | String | The subscribed max Power |
| main#contractLastActivationDate | String | |
| main#contractDistributionTariff | String | |
| main#contractOffpeakHours | String | The OffPeakHour link to your contract |
| main#contractLastDistributionTariffChangeDate | String | |
| main#contractSegment | String | |
| main#usagePointId | String | |
| main#usagePointStatus | String | |
| main#usagePointMeterType | String | |
| main#usagePointAddressCity | String | |
| main#usagePointAddressCountry | String | |
| main#usagePointAddressInseeCode | String | |
| main#usagePointAddressPostalCode | String | |
| main#usagePointAddressStreet | String | |
| main#contactMail | String | |
| main#contactPhone | String | |
- The tempo group will give information about the tempo day color link to a tempo contract
| Channel ID | Item Type | Description |
|---------------------------------------------------|----------------|----------------------------------------------------------------------------|
| tempo#tempoInfoToday | String | The tempo color for the current day |
| tempo#tempoInfoTomorrow | String | The tempo color for the tomorrow |
| tempo#tempoInfoTimeSeries | String | A timeseries channel that will expose full tempo information for one year |
Using the timeseries channel, you will be able to esealy create a calendar graph to show the tempo calendar.
You will need for this to enable a timeseries persistence framework.
Graph definitions will look like this
```java
config:
chartType: month
future: false
label: Tempo
period: M
sidebar: true
slots:
calendar:
- component: oh-calendar-axis
config:
cellSize: 10
dayLabel:
firstDay: 1
fontSize: 16
margin: 20
left: center
monthLabel:
color: "#c0c0ff"
fontSize: 30
margin: 20
orient: vertical
top: middle
yearLabel:
color: "#c0c0ff"
fontSize: 30
margin: 50
dataZoom:
- component: oh-chart-datazoom
config:
orient: horizontal
show: true
type: slider
grid: []
legend:
- component: oh-chart-legend
config:
show: false
series:
- component: oh-calendar-series
config:
aggregationFunction: average
calendarIndex: 0
coordinateSystem: calendar
item: Linky_Melody_Tempo
label:
formatter: =v=> JSON.stringify(v.data[0]).substring(1,11)
show: true
smartFormatter: false
name: Series 1
service: inmemory
type: heatmap
title:
- component: oh-chart-title
config:
show: true
text: Calendrier Tempo
toolbox:
- component: oh-chart-toolbox
config:
presetFeatures:
- saveAsImage
- restore
- dataView
- dataZoom
- magicType
show: true
tooltip:
- component: oh-chart-tooltip
config:
formatter: "{c}"
show: true
visualMap:
- component: oh-chart-visualmap
config:
bottom: 0
calculable: true
inRange:
color:
- "#0000ff"
- "#ffffff"
- "#ff0000"
left: center
max: 2
min: 0
orient: horizontal
presetPalette: ""
show: false
type: continuous
xAxis: []
yAxis: []
```
The resulting graph will look like this:
![TempoGraph](doc/TempoGraph.png)
| Channel ID | Item Type | Description |
|-----------------------|---------------|---------------------------------------|
| daily#yesterday | Number:Energy | Yesterday energy usage |
| daily#day-2 | Number:Energy | Day-2 energy usage |
| daily#day-3 | Number:Energy | Day-3 energy usage |
| daily#consumption | Number:Energy | timeseries for consumption |
| daily#power | Number:Power | Yesterday's peak power usage |
| daily#timestamp | DateTime | Timestamp of the power peak |
| daily#power-2 | Number:Power | Day-2's peak power usage |
| daily#timestamp-2 | DateTime | Timestamp Day-2's of the power peak |
| daily#power-3 | Number:Power | Day-3's peak power usage |
| daily#timestamp-3 | DateTime | Timestamp Day-3's of the power peak |
| daily#mawPower | Number:Power | timeseries for maxPower |
| Channel ID | Item Type | Description |
|-----------------------|---------------|------------------------------|
| weekly#thisWeek | Number:Energy | Current week energy usage |
| weekly#lastWeek | Number:Energy | Last week energy usage |
| weekly#week-2 | Number:Energy | Last week energy usage |
| weekly#consumption | Number:Energy | Last week energy usage |
| weekly#maxPower | Number:Energy | Last week energy usage |
| Channel ID | Item Type | Description |
|-----------------------|---------------|------------------------------|
| monthly#thisMonth | Number:Energy | Current month energy usage |
| monthly#lastMonth | Number:Energy | Last month energy usage |
| monthly#month-2 | Number:Energy | Last month energy usage |
| monthly#consumption | Number:Energy | Last month energy usage |
| monthly#maxPower | Number:Energy | Last month energy usage |
| Channel ID | Item Type | Description |
|-----------------------|---------------|------------------------------|
| yearly#thisYear | Number:Energy | Current year energy usage |
| yearly#lastYear | Number:Energy | Last year energy usage |
| yearly#year-2 | Number:Energy | year-2 energy usage |
| yearly#consumption | Number:Energy | Last year energy usage |
| yearly#maxPower | Number:Energy | Last year energy usage |
![TempoGraph](doc/GraphConso.png)
## Console Commands
@ -97,3 +375,4 @@ Number:Energy ConsoMoisDernier "Conso mois dernier [%.0f %unit%]" <energy> { cha
Number:Energy ConsoAnneeEnCours "Conso cette année [%.0f %unit%]" <energy> { channel="linky:linky:local:yearly#thisYear" }
Number:Energy ConsoAnneeDerniere "Conso année dernière [%.0f %unit%]" <energy> { channel="linky:linky:local:yearly#lastYear" }
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

@ -4,6 +4,7 @@
<feature name="openhab-binding-linky" description="Linky Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-core-auth-oauth2client</feature>
<bundle dependency="true">mvn:org.jsoup/jsoup/1.14.3</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.linky/${project.version}</bundle>
</feature>

View File

@ -0,0 +1,271 @@
/*
* Copyright (c) 2010-2025 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;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.UrlEncoded;
import org.openhab.binding.linky.internal.handler.ApiBridgeHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The LinkyAuthServlet manages the authorization with the Linky Web API. The servlet implements the
* Authorization Code flow and saves the resulting refreshToken with the bridge.
*
* @author Gaël L'hopital - Initial contribution
* @author Laurent Arnal - Rewrite addon to use official dataconect API
*/
@NonNullByDefault
public class LinkyAuthServlet extends HttpServlet {
private static final long serialVersionUID = -4719613645562518231L;
private static final Pattern MESSAGE_KEY_PATTERN = Pattern.compile("\\$\\{([^\\}]+)\\}");
private static final String CONTENT_TYPE = "text/html;charset=UTF-8";
private static final String HTML_USER_AUTHORIZED = "<p class='block authorized'>Addon authorized for %s.</p>";
private static final String HTML_ERROR = "<p class='block error'>Call to Enedis failed with error: %s</p>";
// Keys present in the index.html
private static final String KEY_AUTHORIZE_URI = "authorize.uri";
private static final String KEY_RETRIEVE_TOKEN_URI = "retrieveToken.uri";
private static final String KEY_REDIRECT_URI = "redirectUri";
private static final String KEY_CODE = "code.Value";
private static final String KEY_PRMID = "prmId.Value";
private static final String KEY_PRMID_OPTION = "prmId.Option";
private static final String KEY_AUTHORIZED_USER = "authorizedUser";
private static final String KEY_CB_DISPLAY_CONFIRMATION = "cb.displayConfirmation";
private static final String KEY_CB_DISPLAY_ERROR = "cb.displayError";
private static final String KEY_CB_DISPLAY_INSTRUCTION = "cb.displayInstruction";
private static final String KEY_ERROR = "error";
private static final String KEY_PAGE_REFRESH = "pageRefresh";
private static final String TEMPLATE_PATH = "templates/";
private final Logger logger = LoggerFactory.getLogger(LinkyAuthServlet.class);
private final String index;
private final String enedisStep1;
private final String enedisStep2;
private final String enedisStep3;
private final String myelectricaldataStep1;
private final String myelectricaldataStep2;
private final String myelectricaldataStep3;
private ApiBridgeHandler apiBridgeHandler;
public LinkyAuthServlet(ApiBridgeHandler apiBridgeHandler) throws LinkyException {
this.apiBridgeHandler = apiBridgeHandler;
try {
this.index = readTemplate("index.html");
this.enedisStep1 = readTemplate("enedis-step1.html");
this.enedisStep2 = readTemplate("enedis-step2.html");
this.enedisStep3 = readTemplate("enedis-step3-cb.html");
this.myelectricaldataStep1 = readTemplate("myelectricaldata-step1.html");
this.myelectricaldataStep2 = readTemplate("myelectricaldata-step2.html");
this.myelectricaldataStep3 = readTemplate("myelectricaldata-step3.html");
} catch (IOException e) {
throw new LinkyException("unable to initialize auth servlet", e);
}
}
/**
* Reads a template from file and returns the content as String.
*
* @param templateName name of the template file to read
* @return The content of the template file
* @throws IOException thrown when an HTML template could not be read
*/
private String readTemplate(String templateName) throws IOException {
final URL url = apiBridgeHandler.getBundleContext().getBundle().getEntry(TEMPLATE_PATH + templateName);
if (url == null) {
throw new FileNotFoundException(
String.format("Cannot find {}' - failed to initialize Linky servlet".formatted(templateName)));
} else {
try (InputStream inputStream = url.openStream()) {
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
}
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
logger.debug("Linky auth callback servlet received GET request {}.", req.getRequestURI());
final Map<String, String> replaceMap = new HashMap<>();
StringBuffer requestUrl = req.getRequestURL();
String servletBaseUrl = requestUrl != null ? requestUrl.toString() : "";
String template = "";
if (servletBaseUrl.contains("index")) {
template = index;
} else if (servletBaseUrl.contains("enedis-step1")) {
template = enedisStep1;
} else if (servletBaseUrl.contains("enedis-step2")) {
template = enedisStep2;
} else if (servletBaseUrl.contains("enedis-step3-cb")) {
template = enedisStep3;
} else if (servletBaseUrl.contains("myelectricaldata-step1")) {
template = myelectricaldataStep1;
} else if (servletBaseUrl.contains("myelectricaldata-step2")) {
template = myelectricaldataStep2;
} else if (servletBaseUrl.contains("myelectricaldata-step3")) {
template = myelectricaldataStep3;
} else if (servletBaseUrl.contains("enedis")) {
template = enedisStep1;
} else if (servletBaseUrl.contains("myelectricaldata")) {
template = myelectricaldataStep1;
} else {
template = index;
}
// for some unknown reason, getRequestURL return a malformed URL mixing http:// and port 443
if (servletBaseUrl.contains(":443")) {
servletBaseUrl = servletBaseUrl.replace("http://", "https://");
servletBaseUrl = servletBaseUrl.replace(":443", "");
}
try {
handleLinkyRedirect(replaceMap, servletBaseUrl, req.getQueryString());
resp.setContentType(CONTENT_TYPE);
StringBuffer optionBuffer = new StringBuffer();
List<String> prmIds = apiBridgeHandler.getAllPrmId();
for (String prmId : prmIds) {
optionBuffer.append("<option value=\"" + prmId + "\">" + prmId + "</option>");
}
final MultiMap<String> params = new MultiMap<>();
String queryString = req.getQueryString();
if (queryString != null) {
UrlEncoded.decodeTo(queryString, params, StandardCharsets.UTF_8.name());
}
final String usagePointId = params.getString("usage_point_id");
final String code = params.getString("code");
replaceMap.put(KEY_PRMID, usagePointId);
replaceMap.put(KEY_CODE, code);
replaceMap.put(KEY_PRMID_OPTION, optionBuffer.toString());
replaceMap.put(KEY_REDIRECT_URI, servletBaseUrl);
replaceMap.put(KEY_RETRIEVE_TOKEN_URI, servletBaseUrl + "?state=OK");
String authorizeUri = apiBridgeHandler.formatAuthorizationUrl("");
replaceMap.put(KEY_AUTHORIZE_URI, authorizeUri);
resp.getWriter().append(replaceKeysFromMap(template, replaceMap));
resp.getWriter().close();
} catch (LinkyException ex) {
resp.setContentType(CONTENT_TYPE);
replaceMap.put(KEY_ERROR, "Error during request handling : " + ex.getMessage());
resp.getWriter().append(replaceKeysFromMap(template, replaceMap));
resp.getWriter().close();
}
}
/**
* Handles a possible call from Enedis to the redirect_uri. If that is the case Linky will pass the authorization
* codes via the url and these are processed. In case of an error this is shown to the user. If the user was
* authorized this is passed on to the handler. Based on all these different outcomes the HTML is generated to
* inform the user.
*
* @param replaceMap a map with key String values that will be mapped in the HTML templates.
* @param servletBaseURL the servlet base, which should be used as the Linky redirect_uri value
* @param queryString the query part of the GET request this servlet is processing
*/
private void handleLinkyRedirect(Map<String, String> replaceMap, String servletBaseURL,
@Nullable String queryString) throws LinkyException {
replaceMap.put(KEY_AUTHORIZED_USER, "");
replaceMap.put(KEY_ERROR, "");
replaceMap.put(KEY_PAGE_REFRESH, "");
replaceMap.put(KEY_CB_DISPLAY_CONFIRMATION, "none");
replaceMap.put(KEY_CB_DISPLAY_ERROR, "none");
replaceMap.put(KEY_CB_DISPLAY_INSTRUCTION, "true");
if (queryString != null) {
final MultiMap<String> params = new MultiMap<>();
UrlEncoded.decodeTo(queryString, params, StandardCharsets.UTF_8.name());
final String reqCode = params.getString("code");
final String reqState = params.getString("state");
final String reqError = params.getString("error");
replaceMap.put(KEY_PAGE_REFRESH, "");
// params.isEmpty() ? "" : String.format(HTML_META_REFRESH_CONTENT, servletBaseURL)
if (!StringUtil.isBlank(reqError)) {
logger.debug("Linky redirected with an error: {}", reqError);
replaceMap.put(KEY_CB_DISPLAY_ERROR, "true");
replaceMap.put(KEY_CB_DISPLAY_CONFIRMATION, "none");
replaceMap.put(KEY_CB_DISPLAY_INSTRUCTION, "none");
replaceMap.put(KEY_ERROR, String.format(HTML_ERROR, reqError));
} else if (!StringUtil.isBlank(reqState)) {
replaceMap.put(KEY_CB_DISPLAY_ERROR, "none");
replaceMap.put(KEY_CB_DISPLAY_CONFIRMATION, "true");
replaceMap.put(KEY_CB_DISPLAY_INSTRUCTION, "none");
try {
replaceMap.put(KEY_AUTHORIZED_USER, String.format(HTML_USER_AUTHORIZED,
reqCode + " / " + apiBridgeHandler.authorize(servletBaseURL, reqState, reqCode)));
} catch (LinkyException e) {
logger.debug("Exception during authorizaton: ", e);
replaceMap.put(KEY_ERROR, String.format(HTML_ERROR, e.getMessage()));
}
}
}
}
/**
* Replaces all keys from the map found in the template with values from the map. If the key is not found the key
* will be kept in the template.
*
* @param template template to replace keys with values
* @param map map with key value pairs to replace in the template
* @return a template with keys replaced
*/
private String replaceKeysFromMap(String template, Map<String, String> map) {
final Matcher m = MESSAGE_KEY_PATTERN.matcher(template);
final StringBuffer sb = new StringBuffer();
while (m.find()) {
try {
final String key = m.group(1);
m.appendReplacement(sb, Matcher.quoteReplacement(map.getOrDefault(key, "${" + key + '}')));
} catch (RuntimeException e) {
logger.debug("Error occurred during template filling, cause ", e);
}
}
m.appendTail(sb);
return sb.toString();
}
}

View File

@ -12,6 +12,8 @@
*/
package org.openhab.binding.linky.internal;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
@ -20,6 +22,7 @@ import org.openhab.core.thing.ThingTypeUID;
* used across the whole binding.
*
* @author Gaël L'hopital - Initial contribution
* @author Laurent Arnal - Rewrite addon to use official dataconect API *
*/
@NonNullByDefault
public class LinkyBindingConstants {
@ -27,21 +30,96 @@ public class LinkyBindingConstants {
public static final String BINDING_ID = "linky";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_API_ENEDIS_BRIDGE = new ThingTypeUID(BINDING_ID, "enedis");
public static final ThingTypeUID THING_TYPE_API_WEB_ENEDIS_BRIDGE = new ThingTypeUID(BINDING_ID, "enedis-web");
public static final ThingTypeUID THING_TYPE_API_MYELECTRICALDATA_BRIDGE = new ThingTypeUID(BINDING_ID,
"my-electrical-data");
public static final ThingTypeUID THING_TYPE_LINKY = new ThingTypeUID(BINDING_ID, "linky");
public static final Set<ThingTypeUID> SUPPORTED_DEVICE_THING_TYPES_UIDS = Set.of(THING_TYPE_API_ENEDIS_BRIDGE,
THING_TYPE_API_WEB_ENEDIS_BRIDGE, THING_TYPE_API_MYELECTRICALDATA_BRIDGE, THING_TYPE_LINKY);
// Thing properties
// List of all Channel groups id's
public static final String PUISSANCE = "puissance";
public static final String PRM_ID = "prmId";
public static final String USER_ID = "av2_interne_id";
public static final String USER_ID = "customerId";
public static final String AV2_ID = "av2_interne_id";
public static final String DAILY_GROUP = "daily";
public static final String WEEKLY_GROUP = "weekly";
public static final String MONTHLY_GROUP = "monthly";
public static final String YEARLY_GROUP = "yearly";
public static final String MAIN_GROUP = "main";
public static final String TEMPO_GROUP = "tempo";
public static final String LOAD_CURVE_GROUP = "loadCurve";
// List of all Channel id's
public static final String YESTERDAY = "daily#yesterday";
public static final String PEAK_POWER = "daily#power";
public static final String PEAK_TIMESTAMP = "daily#timestamp";
public static final String THIS_WEEK = "weekly#thisWeek";
public static final String LAST_WEEK = "weekly#lastWeek";
public static final String THIS_MONTH = "monthly#thisMonth";
public static final String LAST_MONTH = "monthly#lastMonth";
public static final String THIS_YEAR = "yearly#thisYear";
public static final String LAST_YEAR = "yearly#lastYear";
public static final String CONSUMPTION_CHANNEL = "consumption";
public static final String MAX_POWER_CHANNEL = "maxPower";
public static final String POWER_CHANNEL = "power";
public static final String TIMESTAMP_CHANNEL = "power";
public static final String DAY_MINUS_1 = "yesterday";
public static final String DAY_MINUS_2 = "day-2";
public static final String DAY_MINUS_3 = "day-3";
public static final String PEAK_POWER_DAY_MINUS_1 = "power";
public static final String PEAK_POWER_TS_DAY_MINUS_1 = "timestamp";
public static final String PEAK_POWER_DAY_MINUS_2 = "power-2";
public static final String PEAK_POWER_TS_DAY_MINUS_2 = "timestamp-2";
public static final String PEAK_POWER_DAY_MINUS_3 = "power-3";
public static final String PEAK_POWER_TS_DAY_MINUS_3 = "timestamp-3";
public static final String WEEK_MINUS_0 = "thisWeek";
public static final String WEEK_MINUS_1 = "lastWeek";
public static final String WEEK_MINUS_2 = "week-2";
public static final String MONTH_MINUS_0 = "thisMonth";
public static final String MONTH_MINUS_1 = "lastMonth";
public static final String MONTH_MINUS_2 = "month-2";
public static final String YEAR_MINUS_0 = "thisYear";
public static final String YEAR_MINUS_1 = "lastYear";
public static final String YEAR_MINUS_2 = "year-2";
public static final String TEMPO_TODAY_INFO = "tempoInfoToday";
public static final String TEMPO_TOMORROW_INFO = "tempoInfoTomorrow";
public static final String TEMPO_TEMPO_INFO_TIME_SERIES = "tempoInfoTimeSeries";
public static final String MAIN_IDENTITY = "identity";
public static final String MAIN_CONTRACT_SUBSCRIBED_POWER = "contractSubscribedPower";
public static final String MAIN_CONTRACT_LAST_ACTIVATION_DATE = "contractLastActivationDate";
public static final String MAIN_CONTRACT_DISTRIBUTION_TARIFF = "contractDistributionTariff";
public static final String MAIN_CONTRACT_OFF_PEAK_HOURS = "contractOffpeakHours";
public static final String MAIN_CONTRACT_CONTRACT_STATUS = "contractStatus";
public static final String MAIN_CONTRACT_CONTRACT_TYPE = "contractType";
public static final String MAIN_CONTRACT_LAST_DISTRIBUTION_TARIFF_CHANGE_DATE = "contractLastDistributionTariffChangeDate";
public static final String MAIN_CONTRACT_SEGMENT = "contractSegment";
public static final String MAIN_USAGEPOINT_ID = "usagePointId";
public static final String MAIN_USAGEPOINT_STATUS = "usagePointStatus";
public static final String MAIN_USAGEPOINT_METER_TYPE = "usagePointMeterType";
public static final String MAIN_USAGEPOINT_METER_ADDRESS_CITY = "usagePointAddressCity";
public static final String MAIN_USAGEPOINT_METER_ADDRESS_COUNTRY = "usagePointAddressCountry";
public static final String MAIN_USAGEPOINT_METER_ADDRESS_INSEE_CODE = "usagePointAddressInseeCode";
public static final String MAIN_USAGEPOINT_METER_ADDRESS_POSTAL_CODE = "usagePointAddressPostalCode";
public static final String MAIN_USAGEPOINT_METER_ADDRESS_STREET = "usagePointAddressStreet";
public static final String MAIN_CONTACT_MAIL = "contactMail";
public static final String MAIN_CONTACT_PHONE = "contactPhone";
// Authorization related Servlet and resources aliases.
public static final String LINKY_ALIAS = "/connectlinky";
public static final String LINKY_IMG_ALIAS = "/img";
/**
* Smartthings scopes needed by this binding to work.
*/
public static final String LINKY_SCOPES = "am_application_scope default";
}

View File

@ -13,21 +13,31 @@
package org.openhab.binding.linky.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.config.core.Configuration;
/**
* The {@link LinkyConfiguration} is the class used to match the
* thing configuration.
*
* @author Gaël L'hopital - Initial contribution
* @author Laurent Arnal - Rewrite addon to use official dataconect API
*/
@NonNullByDefault
public class LinkyConfiguration {
public class LinkyConfiguration extends Configuration {
public static final String INTERNAL_AUTH_ID = "internalAuthId";
public String token = "";
public String timezone = "";
public String prmId = "";
public String clientId = "";
public String clientSecret = "";
public boolean isSandbox = false;
public String username = "";
public String password = "";
public String internalAuthId = "";
public boolean seemsValid() {
return !username.isBlank() && !password.isBlank() && !internalAuthId.isBlank();
return !prmId.isBlank();
}
}

View File

@ -12,25 +12,28 @@
*/
package org.openhab.binding.linky.internal;
import static org.openhab.binding.linky.internal.LinkyBindingConstants.THING_TYPE_LINKY;
import static java.time.temporal.ChronoField.*;
import static org.openhab.binding.linky.internal.LinkyBindingConstants.*;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import java.time.format.DateTimeFormatterBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.linky.internal.handler.EnedisBridgeHandler;
import org.openhab.binding.linky.internal.handler.EnedisWebBridgeHandler;
import org.openhab.binding.linky.internal.handler.LinkyHandler;
import org.openhab.binding.linky.internal.handler.MyElectricalDataBridgeHandler;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.io.net.http.TrustAllTrustManager;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
@ -39,8 +42,7 @@ import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.osgi.service.http.HttpService;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@ -50,71 +52,101 @@ import com.google.gson.JsonDeserializer;
* The {@link LinkyHandlerFactory} is responsible for creating things handlers.
*
* @author Gaël L'hopital - Initial contribution
* @author Laurent Arnal - Rewrite addon to use official dataconect API
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.linky")
@Component(immediate = true, 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 static final int REQUEST_BUFFER_SIZE = 8000;
private static final int RESPONSE_BUFFER_SIZE = 200000;
private static final DateTimeFormatter LINKY_LOCALDATE_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd");
private static final DateTimeFormatter LINKY_LOCALDATETIME_FORMATTER = new DateTimeFormatterBuilder()
.appendPattern("uuuu-MM-dd['T'][' ']HH:mm").optionalStart().appendLiteral(':')
.appendValue(SECOND_OF_MINUTE, 2).optionalStart().appendFraction(NANO_OF_SECOND, 0, 9, true).toFormatter();
private final Logger logger = LoggerFactory.getLogger(LinkyHandlerFactory.class);
private final Gson gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class,
(JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
.parse(json.getAsJsonPrimitive().getAsString(), LINKY_FORMATTER))
/*
* ;
*
* DateTimeFormatter formatter1 = new DateTimeFormatterBuilder()
* .appendPattern(DATE_TIME_FORMAT_PATTERN)
* // optional decimal point followed by 1 to 6 digits
* .optionalStart()
* .appendPattern(".")
* .appendFraction(ChronoField.MICRO_OF_SECOND, 1, 6, false)
* .optionalEnd()
* .toFormatter();
*/
private final HttpClientFactory httpClientFactory;
private final OAuthFactory oAuthFactory;
private final HttpService httpService;
private final ThingRegistry thingRegistry;
private final ComponentContext componentContext;
private final TimeZoneProvider timeZoneProvider;
private final Gson gson = new GsonBuilder()
.registerTypeAdapter(ZonedDateTime.class,
(JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
.parse(json.getAsJsonPrimitive().getAsString(), LINKY_FORMATTER))
.registerTypeAdapter(LocalDate.class,
(JsonDeserializer<LocalDate>) (json, type, jsonDeserializationContext) -> LocalDate
.parse(json.getAsJsonPrimitive().getAsString(), LINKY_LOCALDATE_FORMATTER))
.registerTypeAdapter(LocalDateTime.class,
(JsonDeserializer<LocalDateTime>) (json, type, jsonDeserializationContext) -> {
try {
return LocalDateTime.parse(json.getAsJsonPrimitive().getAsString(),
LINKY_LOCALDATETIME_FORMATTER);
} catch (Exception ex) {
return LocalDate.parse(json.getAsJsonPrimitive().getAsString(), LINKY_LOCALDATE_FORMATTER)
.atStartOfDay();
}
})
.create();
private final LocaleProvider localeProvider;
private final HttpClient httpClient;
@Activate
public LinkyHandlerFactory(final @Reference LocaleProvider localeProvider,
final @Reference HttpClientFactory httpClientFactory) {
final @Reference HttpClientFactory httpClientFactory, final @Reference OAuthFactory oAuthFactory,
final @Reference HttpService httpService, final @Reference ThingRegistry thingRegistry,
ComponentContext componentContext, final @Reference TimeZoneProvider timeZoneProvider) {
this.localeProvider = localeProvider;
SslContextFactory sslContextFactory = new SslContextFactory.Client();
try {
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, new TrustManager[] { TrustAllTrustManager.getInstance() }, null);
sslContextFactory.setSslContext(sslContext);
} catch (NoSuchAlgorithmException e) {
logger.warn("An exception occurred while requesting the SSL encryption algorithm : '{}'", e.getMessage(),
e);
} catch (KeyManagementException e) {
logger.warn("An exception occurred while initialising the SSL context : '{}'", e.getMessage(), e);
}
this.httpClient = httpClientFactory.createHttpClient(LinkyBindingConstants.BINDING_ID, sslContextFactory);
httpClient.setFollowRedirects(false);
httpClient.setRequestBufferSize(REQUEST_BUFFER_SIZE);
httpClient.setResponseBufferSize(RESPONSE_BUFFER_SIZE);
this.timeZoneProvider = timeZoneProvider;
this.httpClientFactory = httpClientFactory;
this.oAuthFactory = oAuthFactory;
this.httpService = httpService;
this.thingRegistry = thingRegistry;
this.componentContext = componentContext;
}
@Override
protected void activate(ComponentContext componentContext) {
super.activate(componentContext);
try {
httpClient.start();
} catch (Exception e) {
logger.warn("Unable to start Jetty HttpClient {}", e.getMessage());
}
}
@Override
protected void deactivate(ComponentContext componentContext) {
super.deactivate(componentContext);
try {
httpClient.stop();
} catch (Exception e) {
logger.warn("Unable to stop Jetty HttpClient {}", e.getMessage());
}
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return THING_TYPE_LINKY.equals(thingTypeUID);
return SUPPORTED_DEVICE_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
return supportsThingType(thing.getThingTypeUID()) ? new LinkyHandler(thing, localeProvider, gson, httpClient)
: null;
if (THING_TYPE_API_ENEDIS_BRIDGE.equals(thing.getThingTypeUID())) {
EnedisBridgeHandler handler = new EnedisBridgeHandler((Bridge) thing, this.httpClientFactory,
this.oAuthFactory, this.httpService, thingRegistry, componentContext, gson);
return handler;
} else if (THING_TYPE_API_WEB_ENEDIS_BRIDGE.equals(thing.getThingTypeUID())) {
EnedisWebBridgeHandler handler = new EnedisWebBridgeHandler((Bridge) thing, this.httpClientFactory,
this.oAuthFactory, this.httpService, thingRegistry, componentContext, gson);
return handler;
} else if (THING_TYPE_API_MYELECTRICALDATA_BRIDGE.equals(thing.getThingTypeUID())) {
MyElectricalDataBridgeHandler handler = new MyElectricalDataBridgeHandler((Bridge) thing,
this.httpClientFactory, this.oAuthFactory, this.httpService, thingRegistry, componentContext, gson);
return handler;
} else if (THING_TYPE_LINKY.equals(thing.getThingTypeUID())) {
LinkyHandler handler = new LinkyHandler(thing, localeProvider, timeZoneProvider);
return handler;
}
return null;
}
}

View File

@ -13,42 +13,40 @@
package org.openhab.binding.linky.internal.api;
import java.net.HttpCookie;
import java.net.URI;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ws.rs.core.MediaType;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
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.StringContentProvider;
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.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.dto.AuthData;
import org.openhab.binding.linky.internal.dto.AuthResult;
import org.openhab.binding.linky.internal.dto.ConsumptionReport;
import org.openhab.binding.linky.internal.dto.ConsumptionReport.Consumption;
import org.openhab.binding.linky.internal.dto.Contact;
import org.openhab.binding.linky.internal.dto.Contract;
import org.openhab.binding.linky.internal.dto.Identity;
import org.openhab.binding.linky.internal.dto.MeterReading;
import org.openhab.binding.linky.internal.dto.PrmDetail;
import org.openhab.binding.linky.internal.dto.PrmInfo;
import org.openhab.binding.linky.internal.dto.ResponseContact;
import org.openhab.binding.linky.internal.dto.ResponseContract;
import org.openhab.binding.linky.internal.dto.ResponseIdentity;
import org.openhab.binding.linky.internal.dto.ResponseMeter;
import org.openhab.binding.linky.internal.dto.ResponseTempo;
import org.openhab.binding.linky.internal.dto.UsagePoint;
import org.openhab.binding.linky.internal.dto.UserInfo;
import org.openhab.binding.linky.internal.handler.EnedisWebBridgeHandler;
import org.openhab.binding.linky.internal.handler.LinkyBridgeHandler;
import org.openhab.binding.linky.internal.handler.LinkyHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -59,191 +57,82 @@ import com.google.gson.JsonSyntaxException;
* {@link EnedisHttpApi} wraps the Enedis Webservice.
*
* @author Gaël L'hopital - Initial contribution
* @author Laurent Arnal - Rewrite addon to use official dataconect API
*/
@NonNullByDefault
public class EnedisHttpApi {
private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private static final String ENEDIS_DOMAIN = ".enedis.fr";
private static final String URL_APPS_LINCS = "https://alex.microapplications" + 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_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 PRM_INFO_BASE_URL = URL_APPS_LINCS + "/mes-mesures/api/private/v1/personnes/";
private static final String PRM_INFO_URL = URL_APPS_LINCS + "/mes-prms-part/api/private/v2/personnes/%s/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 final LinkyConfiguration config;
private final LinkyBridgeHandler linkyBridgeHandler;
private boolean connected = false;
public EnedisHttpApi(LinkyConfiguration config, Gson gson, HttpClient httpClient) {
public EnedisHttpApi(LinkyBridgeHandler linkyBridgeHandler, Gson gson, HttpClient httpClient) {
this.gson = gson;
this.httpClient = httpClient;
this.config = config;
this.linkyBridgeHandler = linkyBridgeHandler;
}
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 = getContent(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(getFormContent("SAMLRequest", samlInput.attr("value"))).send();
if (result.getStatus() != HttpStatus.FOUND_302) {
throw new LinkyException("Connection failed step 2");
}
logger.debug("Get the location and the ReqID");
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 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%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() != HttpStatus.OK_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, MediaType.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() != HttpStatus.OK_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");
addCookie("enedisExt", authResult.tokenId);
logger.debug("Step 5: retrieve the SAMLresponse");
data = getContent(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(getFormContent("SAMLResponse", samlInput.attr("value")))
.send();
if (result.getStatus() != HttpStatus.FOUND_302) {
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;
} catch (InterruptedException | TimeoutException | ExecutionException | JsonSyntaxException e) {
throw new LinkyException(e, "Error opening connection with Enedis webservice");
}
}
private String getLocation(ContentResponse response) {
return response.getHeaders().get(HttpHeader.LOCATION);
}
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));
getLocation(httpClient.GET(location));
httpClient.getCookieStore().removeAll();
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new LinkyException(e, "Error while disconnecting from Enedis webservice");
}
}
}
public boolean isConnected() {
return connected;
}
public void dispose() throws LinkyException {
disconnect();
}
private void addCookie(String key, String value) {
HttpCookie cookie = new HttpCookie(key, value);
cookie.setDomain(ENEDIS_DOMAIN);
cookie.setPath("/");
httpClient.getCookieStore().add(COOKIE_URI, cookie);
}
private FormContentProvider getFormContent(String fieldName, String fieldValue) {
public FormContentProvider getFormContent(String fieldName, String fieldValue) {
Fields fields = new Fields();
fields.put(fieldName, fieldValue);
return new FormContentProvider(fields);
}
private String getContent(String url) throws LinkyException {
public void addCookie(String key, String value) {
HttpCookie cookie = new HttpCookie(key, value);
cookie.setDomain(EnedisWebBridgeHandler.ENEDIS_DOMAIN);
cookie.setPath("/");
httpClient.getCookieStore().add(EnedisWebBridgeHandler.COOKIE_URI, cookie);
}
public String getLocation(ContentResponse response) {
return response.getHeaders().get(HttpHeader.LOCATION);
}
public String getContent(LinkyHandler handler, String url) throws LinkyException {
return getContent(logger, linkyBridgeHandler, url, httpClient, linkyBridgeHandler.getToken(handler));
}
public String getContent(String url) throws LinkyException {
return getContent(logger, linkyBridgeHandler, url, httpClient, "");
}
private static String getContent(Logger logger, LinkyBridgeHandler linkyBridgeHandler, String url,
HttpClient httpClient, String token) throws LinkyException {
try {
Request request = httpClient.newRequest(url)
.agent("Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0");
Request request = httpClient.newRequest(url);
request = request.agent("Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0");
request = request.method(HttpMethod.GET);
if (!token.isEmpty()) {
request = request.header("Authorization", "" + token);
request = request.header("Accept", "application/json");
}
ContentResponse result = request.send();
if (result.getStatus() != HttpStatus.OK_200) {
if (result.getStatus() == HttpStatus.TEMPORARY_REDIRECT_307
|| result.getStatus() == HttpStatus.MOVED_TEMPORARILY_302) {
String loc = result.getHeaders().get("Location");
String newUrl = linkyBridgeHandler.getBaseUrl() + loc.substring(1);
request = httpClient.newRequest(newUrl);
request = request.method(HttpMethod.GET);
result = request.send();
if (result.getStatus() == HttpStatus.TEMPORARY_REDIRECT_307
|| result.getStatus() == HttpStatus.MOVED_TEMPORARILY_302) {
loc = result.getHeaders().get("Location");
String[] urlParts = loc.split("/");
if (urlParts.length < 4) {
throw new LinkyException("malformed url : %s", loc);
}
return urlParts[3];
}
}
if (result.getStatus() != 200) {
throw new LinkyException("Error requesting '%s': %s", url, result.getContentAsString());
}
String content = result.getContentAsString();
logger.trace("getContent returned {}", content);
return content;
@ -252,54 +141,147 @@ public class EnedisHttpApi {
}
}
private <T> T getData(String url, Class<T> clazz) throws LinkyException {
if (!connected) {
initialize();
private <T> T getData(LinkyHandler handler, String url, Class<T> clazz) throws LinkyException {
if (!linkyBridgeHandler.isConnected()) {
linkyBridgeHandler.initialize();
}
String data = getContent(url);
if (data.isEmpty()) {
throw new LinkyException("Requesting '%s' returned an empty response", url);
}
try {
return Objects.requireNonNull(gson.fromJson(data, clazz));
} catch (JsonSyntaxException e) {
logger.debug("Invalid JSON response not matching {}: {}", clazz.getName(), data);
throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", url);
int numberRetry = 0;
LinkyException lastException = null;
logger.debug("getData begin {}: {}", clazz.getName(), url);
while (numberRetry < 3) {
try {
String data = getContent(handler, url);
if (!data.isEmpty()) {
try {
logger.debug("getData success {}: {}", clazz.getName(), url);
return Objects.requireNonNull(gson.fromJson(data, clazz));
} catch (JsonSyntaxException e) {
logger.debug("Invalid JSON response not matching {}: {}", clazz.getName(), data);
throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", url);
}
}
} catch (LinkyException ex) {
lastException = ex;
logger.debug("getData error {}: {} , retry{}", clazz.getName(), url, numberRetry);
// try to reinit connection, fail after 3 attemps
linkyBridgeHandler.connectionInit();
}
numberRetry++;
}
logger.debug("getData error {}: {} , maxRetry", clazz.getName(), url);
throw Objects.requireNonNull(lastException);
}
public PrmInfo getPrmInfo(String internId) throws LinkyException {
String url = PRM_INFO_URL.formatted(internId);
PrmInfo[] prms = getData(url, PrmInfo[].class);
public PrmInfo getPrmInfo(LinkyHandler handler, String internId, String prmId) throws LinkyException {
String prmInfoUrl = linkyBridgeHandler.getContractUrl().formatted(internId);
PrmInfo[] prms = getData(handler, prmInfoUrl, PrmInfo[].class);
if (prms.length < 1) {
throw new LinkyException("Invalid prms data received");
}
return prms[0];
Optional<PrmInfo> result = Arrays.stream(prms).filter(x -> x.idPrm.equals(prmId)).findFirst();
if (result.isPresent()) {
return result.get();
}
throw new LinkyException(("PRM with id : %s does not exist").formatted(prmId));
}
public PrmDetail getPrmDetails(String internId, String prmId) throws LinkyException {
String url = PRM_INFO_URL.formatted(internId) + "/" + prmId
public PrmDetail getPrmDetails(LinkyHandler handler, String internId, String prmId) throws LinkyException {
String prmInfoUrl = linkyBridgeHandler.getContractUrl();
String url = prmInfoUrl.formatted(internId) + "/" + prmId
+ "?embed=SITALI&embed=SITCOM&embed=SITCON&embed=SYNCON";
return getData(url, PrmDetail.class);
return getData(handler, url, PrmDetail.class);
}
public UserInfo getUserInfo() throws LinkyException {
return getData(USER_INFO_URL, UserInfo.class);
public UserInfo getUserInfo(LinkyHandler handler) throws LinkyException {
String userInfoUrl = linkyBridgeHandler.getContactUrl();
return getData(handler, userInfoUrl, UserInfo.class);
}
private Consumption getMeasures(String userId, String prmId, LocalDate from, LocalDate to, String request)
public String formatUrl(String apiUrl, String prmId) {
return apiUrl.formatted(prmId);
}
public Contract getContract(LinkyHandler handler, String prmId) throws LinkyException {
String contractUrl = linkyBridgeHandler.getContractUrl().formatted(prmId);
ResponseContract contractResponse = getData(handler, contractUrl, ResponseContract.class);
return contractResponse.customer.usagePoint[0].contracts;
}
public UsagePoint getUsagePoint(LinkyHandler handler, String prmId) throws LinkyException {
String addressUrl = linkyBridgeHandler.getAddressUrl().formatted(prmId);
ResponseContract contractResponse = getData(handler, addressUrl, ResponseContract.class);
return contractResponse.customer.usagePoint[0].usagePoint;
}
public Identity getIdentity(LinkyHandler handler, String prmId) throws LinkyException {
String identityUrl = linkyBridgeHandler.getIdentityUrl().formatted(prmId);
ResponseIdentity customerIdReponse = getData(handler, identityUrl, ResponseIdentity.class);
String name = customerIdReponse.identity.naturalPerson.lastname;
String[] nameParts = name.split(" ");
if (nameParts.length > 1) {
customerIdReponse.identity.naturalPerson.firstname = name.split(" ")[0];
customerIdReponse.identity.naturalPerson.lastname = name.split(" ")[1];
}
return customerIdReponse.identity.naturalPerson;
}
public Contact getContact(LinkyHandler handler, String prmId) throws LinkyException {
String contactUrl = linkyBridgeHandler.getContactUrl().formatted(prmId);
ResponseContact contactResponse = getData(handler, contactUrl, ResponseContact.class);
return contactResponse.contact;
}
private MeterReading getMeasures(LinkyHandler handler, String apiUrl, String mps, String prmId, LocalDate from,
LocalDate to) throws LinkyException {
String dtStart = from.format(linkyBridgeHandler.getApiDateFormat());
String dtEnd = to.format(linkyBridgeHandler.getApiDateFormat());
if (handler.supportNewApiFormat()) {
String url = String.format(apiUrl, prmId, dtStart, dtEnd);
ResponseMeter meterResponse = getData(handler, url, ResponseMeter.class);
return meterResponse.meterReading;
} else {
String url = String.format(apiUrl, mps, prmId, dtStart, dtEnd);
ConsumptionReport consomptionReport = getData(handler, url, ConsumptionReport.class);
return MeterReading.convertFromComsumptionReport(consomptionReport);
}
}
public MeterReading getEnergyData(LinkyHandler handler, String mps, String prmId, LocalDate from, LocalDate to)
throws LinkyException {
String url = String.format(MEASURE_URL, userId, prmId, request, from.format(API_DATE_FORMAT),
to.format(API_DATE_FORMAT));
ConsumptionReport report = getData(url, ConsumptionReport.class);
return report.firstLevel.consumptions;
return getMeasures(handler, linkyBridgeHandler.getDailyConsumptionUrl(), mps, prmId, from, to);
}
public Consumption getEnergyData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
return getMeasures(userId, prmId, from, to, "energie");
public MeterReading getLoadCurveData(LinkyHandler handler, String mps, String prmId, LocalDate from, LocalDate to)
throws LinkyException {
return getMeasures(handler, linkyBridgeHandler.getLoadCurveUrl(), mps, prmId, from, to);
}
public Consumption getPowerData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
return getMeasures(userId, prmId, from, to, "pmax");
public MeterReading getPowerData(LinkyHandler handler, String mps, String prmId, LocalDate from, LocalDate to)
throws LinkyException {
return getMeasures(handler, linkyBridgeHandler.getMaxPowerUrl(), mps, prmId, from, to);
}
public ResponseTempo getTempoData(LinkyHandler handler, LocalDate from, LocalDate to) throws LinkyException {
String dtStart = from.format(linkyBridgeHandler.getApiDateFormatYearsFirst());
String dtEnd = to.format(linkyBridgeHandler.getApiDateFormatYearsFirst());
String url = String.format(linkyBridgeHandler.getTempoUrl(), dtStart, dtEnd);
if (url.isEmpty()) {
return new ResponseTempo();
}
ResponseTempo responseTempo = getData(handler, url, ResponseTempo.class);
return responseTempo;
}
}

View File

@ -29,6 +29,11 @@ public class AuthData {
public @Nullable String name;
public @Nullable Object value;
public NameValuePair(String name, Object value) {
this.name = name;
this.value = value;
}
public @Nullable String valueAsString() {
return (value instanceof String stringValue) ? stringValue : null;
}

View File

@ -12,7 +12,8 @@
*/
package org.openhab.binding.linky.internal.dto;
import java.time.ZonedDateTime;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import com.google.gson.annotations.SerializedName;
@ -22,28 +23,32 @@ import com.google.gson.annotations.SerializedName;
* returned by API calls
*
* @author Gaël L'hopital - Initial contribution
* @author Laurent ARNAL - fix to handle new Dto format after enedis site modifications
*/
public class ConsumptionReport {
public class Period {
public String grandeurPhysiqueEnum;
public ZonedDateTime dateDebut;
public ZonedDateTime dateFin;
public class Data {
public LocalDateTime dateDebut;
public LocalDateTime dateFin;
public Double valeur;
}
public class Aggregate {
public List<String> labels;
public List<Period> periodes;
public List<Double> datas;
@SerializedName("donnees")
public List<Data> datas;
public String unite;
}
public class ChronoData {
@SerializedName("JOUR")
@SerializedName("heure")
public Aggregate heure;
@SerializedName("jour")
public Aggregate days;
@SerializedName("SEMAINE")
@SerializedName("semaine")
public Aggregate weeks;
@SerializedName("MOIS")
@SerializedName("mois")
public Aggregate months;
@SerializedName("ANNEE")
@SerializedName("annee")
public Aggregate years;
}
@ -51,14 +56,10 @@ public class ConsumptionReport {
public ChronoData aggregats;
public String grandeurMetier;
public String grandeurPhysique;
public String unite;
public LocalDate dateDebut;
public LocalDate dateFin;
}
public class FirstLevel {
@SerializedName("CONS")
public Consumption consumptions;
}
@SerializedName("1")
public FirstLevel firstLevel;
@SerializedName("cons")
public Consumption consumptions;
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2010-2025 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;
/**
* The {@link UserInfo} holds informations about energy delivery point
*
* @author Laurent Arnal - Initial contribution
*/
public class Contact {
public String phone;
public String email;
public static Contact convertFromUserInfo(UserInfo userInfo) {
Contact result = new Contact();
result.email = userInfo.userProperties.mail;
return result;
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) 2010-2025 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 com.google.gson.annotations.SerializedName;
/**
* The {@link UserInfo} holds informations about energy delivery point
*
* @author Laurent Arnal - Initial contribution
*/
public class Contract {
public String segment;
@SerializedName("subscribed_power")
public String subscribedPower;
@SerializedName("last_activation_date")
public String lastActivationDate;
@SerializedName("distribution_tariff")
public String distributionTariff;
@SerializedName("offpeak_hours")
public String offpeakHours;
@SerializedName("contract_status")
public String contractStatus;
@SerializedName("contract_type")
public String contractType;
@SerializedName("last_distribution_tariff_change_date")
public String lastDistributionTariffChangeDate;
public static Contract convertFromPrmDetail(PrmDetail prmDetail) {
Contract result = new Contract();
result.segment = prmDetail.segment;
result.subscribedPower = prmDetail.situationContractuelleDtos[0].structureTarifaire().puissanceSouscrite()
.valeur();
result.lastActivationDate = "";
result.distributionTariff = prmDetail.situationContractuelleDtos[0].structureTarifaire().grilleFournisseur()
.calendrier().libelle();
result.offpeakHours = "";
result.contractStatus = prmDetail.situationContractuelleDtos[0].informationsContractuelles().etatContractuel()
.code();
result.contractType = prmDetail.situationContractuelleDtos[0].informationsContractuelles().contrat()
.typeContrat().libelle();
result.lastDistributionTariffChangeDate = "";
return result;
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2010-2025 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;
/**
* The {@link UserInfo} holds informations about energy delivery point
*
* @author Gaël L'hopital - Initial contribution
* @author Laurent Arnal - Rewrite addon to use official dataconect API
*/
public class Identity {
public String title;
public String firstname;
public String lastname;
public static Identity convertFromUserInfo(UserInfo userInfo) {
Identity result = new Identity();
result.firstname = userInfo.userProperties.firstName;
result.lastname = userInfo.userProperties.name;
result.title = "";
return result;
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2010-2025 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.time.LocalDateTime;
/**
* The {@link UserInfo} holds informations about energy delivery point
*
* @author Gaël L'hopital - Initial contribution
* @author Laurent Arnal - Rewrite addon to use official dataconect API
*/
public class IntervalReading {
public double value;
public LocalDateTime date;
}

View File

@ -0,0 +1,75 @@
/*
* Copyright (c) 2010-2025 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 org.openhab.binding.linky.internal.dto.ConsumptionReport.Data;
import com.google.gson.annotations.SerializedName;
/**
* The {@link UserInfo} holds informations about energy delivery point
*
* @author Gaël L'hopital - Initial contribution
* @author Laurent Arnal - Rewrite addon to use official dataconect API
*/
public class MeterReading {
@SerializedName("usage_point_id")
public String usagePointId;
@SerializedName("start")
public String startDate;
@SerializedName("end")
public String endDate;
public String quality;
@SerializedName("reading_type")
public ReadingType readingType;
@SerializedName("interval_reading")
public IntervalReading[] baseValue;
public IntervalReading[] weekValue;
public IntervalReading[] monthValue;
public IntervalReading[] yearValue;
public static MeterReading convertFromComsumptionReport(ConsumptionReport comsumptionReport) {
MeterReading result = new MeterReading();
result.readingType = new ReadingType();
if (comsumptionReport.consumptions.aggregats != null) {
if (comsumptionReport.consumptions.aggregats.days != null) {
result.baseValue = fromAgregat(comsumptionReport.consumptions.aggregats.days);
} else if (comsumptionReport.consumptions.aggregats.heure != null) {
result.baseValue = fromAgregat(comsumptionReport.consumptions.aggregats.heure);
}
}
return result;
}
public static IntervalReading[] fromAgregat(ConsumptionReport.Aggregate agregat) {
int size = agregat.datas.size();
IntervalReading[] result = new IntervalReading[size];
for (int i = 0; i < size; i++) {
Data dataObj = agregat.datas.get(i);
result[i] = new IntervalReading();
result[i].value = Double.valueOf(dataObj.valeur);
result[i].date = dataObj.dateDebut;
}
return result;
}
}

View File

@ -21,6 +21,7 @@ import java.util.ArrayList;
*/
public class PrmDetail {
public record Adresse(String ligne2, String ligne3, String ligne4, String ligne5, String ligne6) {
}
public record DicEntry(String code, String libelle) {

View File

@ -16,6 +16,7 @@ package org.openhab.binding.linky.internal.dto;
* The {@link UserInfo} holds ids of existing Prms
*
* @author Gaël L'hopital - Initial contribution
* @author Laurent Arnal - Rewrite addon to use official dataconect API
*/
public class PrmInfo {

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2010-2025 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 com.google.gson.annotations.SerializedName;
/**
* The {@link UserInfo} holds informations about energy delivery point
*
* @author Gaël L'hopital - Initial contribution
* @author Laurent Arnal - Rewrite addon to use official dataconect API
*/
public class ReadingType {
@SerializedName("measurement_kind")
public String measurementKind;
@SerializedName("measuring_period")
public String measuringPeriod;
@SerializedName("unit")
public String unit;
@SerializedName("aggregate")
public String aggregate;
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2010-2025 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 com.google.gson.annotations.SerializedName;
/**
* The {@link UserInfo} holds informations about energy delivery point
*
* @author Laurent Arnal - Initial contribution - Rewrite addon to use official dataconect API
*/
public class ResponseContact {
@SerializedName("customer_id")
public String customerId;
@SerializedName("contact_data")
public Contact contact;
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2010-2025 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 com.google.gson.annotations.SerializedName;
/**
* The {@link ResponseContract} holds informations about the contract
*
* @author Gaël L'hopital - Initial contribution
* @author Laurent Arnal - Rewrite addon to use official dataconect API
*/
public class ResponseContract {
public Customer customer;
public class Customer {
@SerializedName("customer_id")
public String customerId;
@SerializedName("usage_points")
public UsagePoints[] usagePoint;
}
public class UsagePoints {
@SerializedName("usage_point")
public UsagePoint usagePoint;
public Contract contracts;
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2010-2025 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 com.google.gson.annotations.SerializedName;
/**
* The {@link UserInfo} holds informations about energy delivery point
*
* @author Laurent Arnal - Initial contribution - Rewrite addon to use official dataconect API
*/
public class ResponseIdentity {
@SerializedName("customer_id")
public String customerId;
public IdentityEntry identity;
public class IdentityEntry {
@SerializedName("natural_person")
public Identity naturalPerson;
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2010-2025 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 com.google.gson.annotations.SerializedName;
/**
* The {@link UserInfo} holds informations about energy delivery point
*
* @author Gaël L'hopital - Initial contribution
* @author Laurent Arnal - Rewrite addon to use official dataconect API
*/
public class ResponseMeter {
@SerializedName("meter_reading")
public MeterReading meterReading;
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2010-2025 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.LinkedHashMap;
/**
* The {@link UserInfo} holds informations about energy delivery point
*
* @author Gaël L'hopital - Initial contribution
* @author Laurent Arnal - Rewrite addon to use official dataconect API
*/
public class ResponseTempo extends LinkedHashMap<String, String> {
private static final long serialVersionUID = 362498820763181264L;
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2010-2025 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;
/**
* The {@link UserInfo} holds informations about energy delivery point
*
* @author Gaël L'hopital - Initial contribution
* @author Laurent Arnal - Rewrite addon to use official dataconect API
*/
public class TempoDay {
public String tempoDay;
public String tempoVal;
}

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) 2010-2025 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 com.google.gson.annotations.SerializedName;
/**
* The {@link UserInfo} holds informations about energy delivery point
*
* @author Gaël L'hopital - Initial contribution
* @author Laurent Arnal - Rewrite addon to use official dataconect API
*/
public class UsagePoint {
@SerializedName("usage_point_id")
public String usagePointId;
@SerializedName("usage_point_status")
public String usagePointStatus;
@SerializedName("meter_type")
public String meterType;
@SerializedName("usage_point_addresses")
public AddressInfo usagePointAddresses;
public class AddressInfo {
public String street;
public String locality;
@SerializedName("postal_code")
public String postalCode;
@SerializedName("insee_code")
public String inseeCode;
public String city;
public String country;
}
public static UsagePoint convertFromPrmDetail(PrmInfo prmInfo, PrmDetail prmDetail) {
UsagePoint result = new UsagePoint();
result.usagePointId = prmInfo.idPrm;
result.usagePointStatus = prmDetail.syntheseContractuelleDto.niveauOuvertureServices().libelle();
result.meterType = prmDetail.situationComptageDto.dispositifComptage().typeComptage().code();
result.usagePointAddresses = result.new AddressInfo();
result.usagePointAddresses.street = prmDetail.adresse.ligne4();
result.usagePointAddresses.locality = prmDetail.adresse.ligne6();
result.usagePointAddresses.city = prmDetail.adresse.ligne6().split(" ")[1];
result.usagePointAddresses.postalCode = prmDetail.adresse.ligne6().split(" ")[0];
result.usagePointAddresses.inseeCode = "";
result.usagePointAddresses.country = "";
return result;
}
}

View File

@ -0,0 +1,201 @@
/*
* Copyright (c) 2010-2025 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.io.IOException;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.linky.internal.LinkyAuthServlet;
import org.openhab.binding.linky.internal.LinkyBindingConstants;
import org.openhab.binding.linky.internal.LinkyException;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingStatus;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* {@link ApiBridgeHandler} is the base handler to access enedis data.
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public abstract class ApiBridgeHandler extends LinkyBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(ApiBridgeHandler.class);
private final OAuthFactory oAuthFactory;
private @Nullable OAuthClientService oAuthService;
private static @Nullable HttpServlet servlet;
protected String tokenUrl = "";
protected String authorizeUrl = "";
public ApiBridgeHandler(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);
this.oAuthFactory = oAuthFactory;
updateStatus(ThingStatus.UNKNOWN);
}
@Override
public void initialize() {
super.initialize();
this.oAuthService = oAuthFactory.createOAuthClientService(LinkyBindingConstants.BINDING_ID, tokenUrl,
authorizeUrl, getClientId(), getClientSecret(), LinkyBindingConstants.LINKY_SCOPES, true);
registerServlet();
}
public abstract String getClientId();
public abstract String getClientSecret();
public abstract boolean getIsSandbox();
private void registerServlet() {
try {
if (servlet == null) {
servlet = createServlet();
httpService.registerServlet(LinkyBindingConstants.LINKY_ALIAS, servlet, new Hashtable<>(),
httpService.createDefaultHttpContext());
httpService.registerResources(LinkyBindingConstants.LINKY_ALIAS + LinkyBindingConstants.LINKY_IMG_ALIAS,
"web", null);
}
} catch (NamespaceException | ServletException | LinkyException e) {
logger.warn("Error during linky servlet startup", e);
}
}
@Override
public void dispose() {
httpService.unregister(LinkyBindingConstants.LINKY_ALIAS);
httpService.unregister(LinkyBindingConstants.LINKY_ALIAS + LinkyBindingConstants.LINKY_IMG_ALIAS);
super.dispose();
}
/**
* Creates a new {@link LinkyAuthServlet}.
*
* @return the newly created servlet
* @throws IOException thrown when an HTML template could not be read
*/
private HttpServlet createServlet() throws LinkyException {
return new LinkyAuthServlet(this);
}
public String authorize(String redirectUri, String reqState, String reqCode) throws LinkyException {
// Will work only in case of direct oAuth2 authentification to enedis
// this is not the case in v1 as we go trough MyElectricalData
try {
logger.debug("Make call to Enedis to get access token.");
OAuthClientService lcOAuthService = this.oAuthService;
if (lcOAuthService == null) {
return "";
}
final AccessTokenResponse credentials = lcOAuthService
.getAccessTokenByClientCredentials(LinkyBindingConstants.LINKY_SCOPES);
String accessToken = credentials.getAccessToken();
logger.debug("Acces token: {}", accessToken);
return accessToken;
} catch (RuntimeException | OAuthException | IOException e) {
throw new LinkyException("Error during oAuth authorize :" + e.getMessage(), e);
} catch (final OAuthResponseException e) {
throw new LinkyException("Error during oAuth authorize :" + e.getMessage(), e);
}
}
public boolean isAuthorized() {
final AccessTokenResponse accessTokenResponse = getAccessTokenResponse();
return accessTokenResponse != null && accessTokenResponse.getAccessToken() != null
&& accessTokenResponse.getRefreshToken() != null;
}
protected @Nullable AccessTokenResponse getAccessTokenByClientCredentials() {
try {
OAuthClientService lcOAuthService = this.oAuthService;
if (lcOAuthService == null) {
return null;
}
return lcOAuthService.getAccessTokenByClientCredentials(LinkyBindingConstants.LINKY_SCOPES);
} catch (OAuthException | IOException | OAuthResponseException | RuntimeException e) {
logger.debug("Exception checking authorization: ", e);
return null;
}
}
protected @Nullable AccessTokenResponse getAccessTokenResponse() {
try {
OAuthClientService lcOAuthService = this.oAuthService;
if (lcOAuthService == null) {
return null;
}
return lcOAuthService.getAccessTokenResponse();
} catch (OAuthException | IOException | OAuthResponseException | RuntimeException e) {
logger.debug("Exception checking authorization: ", e);
return null;
}
}
public String formatAuthorizationUrl(String redirectUri) {
try {
OAuthClientService lcOAuthService = this.oAuthService;
if (lcOAuthService == null) {
return "";
}
String uri = lcOAuthService.getAuthorizationUrl(redirectUri, LinkyBindingConstants.LINKY_SCOPES,
LinkyBindingConstants.BINDING_ID);
return uri;
} catch (final OAuthException e) {
logger.debug("Error constructing AuthorizationUrl: ", e);
return "";
}
}
@Override
public boolean supportNewApiFormat() {
return true;
}
}

View File

@ -0,0 +1,200 @@
/*
* Copyright (c) 2010-2025 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 org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.linky.internal.LinkyConfiguration;
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;
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;
/**
* {@link EnedisBridgeHandler} is the base handler to access enedis data.
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public class EnedisBridgeHandler extends ApiBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(EnedisBridgeHandler.class);
private static final String BASE_URL_PREPROD = "https://gw.ext.prod-sandbox.api.enedis.fr/";
private static final String ENEDIS_ACCOUNT_URL_PREPROD = "gw.ext.prod-sandbox.api.enedis.fr";
private static final String BASE_URL_PROD = "https://gw.ext.prod.api.enedis.fr/";
public static final String ENEDIS_ACCOUNT_URL_PROD = "https://mon-compte-particulier.enedis.fr/";
private static final String CONTRACT_URL = "customers_upc/v5/usage_points/contracts?usage_point_id=%s";
private static final String IDENTITY_URL = "customers_i/v5/identity?usage_point_id=%s";
private static final String CONTACT_URL = "customers_cd/v5/contact_data?usage_point_id=%s";
private static final String ADDRESS_URL = "customers_upa/v5/usage_points/addresses?usage_point_id=%s";
private static final String MEASURE_DAILY_CONSUMPTION_URL = "metering_data_dc/v5/daily_consumption?usage_point_id=%s&start=%s&end=%s";
private static final String MEASURE_MAX_POWER_URL = "metering_data_dcmp/v5/daily_consumption_max_power?usage_point_id=%s&start=%s&end=%s";
private static final String LOAD_CURVE_CONSUMPTION_URL = "metering_data_clc/v5/consumption_load_curve?usage_point_id=%s&start=%s&end=%s";
public static final String ENEDIS_AUTHORIZE_URL = "dataconnect/v1/oauth2/authorize?duration=P36M";
public static final String ENEDIS_API_TOKEN_URL = "oauth2/v3/token";
private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter API_DATE_FORMAT_YEAR_FIRST = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final String BASE_MYELECT_URL = "https://www.myelectricaldata.fr/";
private static final String TEMPO_URL = BASE_MYELECT_URL + "rte/tempo/%s/%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) {
super(bridge, httpClientFactory, oAuthFactory, httpService, thingRegistry, componentContext, gson);
}
@Override
public void initialize() {
tokenUrl = getBaseUrl() + EnedisBridgeHandler.ENEDIS_API_TOKEN_URL;
authorizeUrl = getAccountUrl() + EnedisBridgeHandler.ENEDIS_AUTHORIZE_URL;
super.initialize();
}
public String getAccountUrl() {
if (getIsSandbox()) {
return ENEDIS_ACCOUNT_URL_PREPROD;
} else {
return ENEDIS_ACCOUNT_URL_PROD;
}
}
@Override
public String getClientId() {
LinkyConfiguration lcConfig = config;
if (lcConfig != null) {
return lcConfig.clientId;
}
return "";
}
@Override
public String getClientSecret() {
LinkyConfiguration lcConfig = config;
if (lcConfig != null) {
return lcConfig.clientSecret;
}
return "";
}
@Override
public boolean getIsSandbox() {
LinkyConfiguration lcConfig = config;
return (lcConfig != null) ? lcConfig.isSandbox : false;
}
@Override
public void dispose() {
logger.debug("Shutting down Enedis bridge handler.");
super.dispose();
}
@Override
public void connectionInit() {
}
@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 double getDivider() {
return 1000.00;
}
@Override
public String getBaseUrl() {
if (getIsSandbox()) {
return BASE_URL_PREPROD;
} else {
return BASE_URL_PROD;
}
}
@Override
public String getContactUrl() {
return getBaseUrl() + CONTACT_URL;
}
@Override
public String getContractUrl() {
return getBaseUrl() + CONTRACT_URL;
}
@Override
public String getIdentityUrl() {
return getBaseUrl() + IDENTITY_URL;
}
@Override
public String getAddressUrl() {
return getBaseUrl() + ADDRESS_URL;
}
@Override
public String getDailyConsumptionUrl() {
return getBaseUrl() + MEASURE_DAILY_CONSUMPTION_URL;
}
@Override
public String getMaxPowerUrl() {
return getBaseUrl() + MEASURE_MAX_POWER_URL;
}
@Override
public String getLoadCurveUrl() {
return getBaseUrl() + LOAD_CURVE_CONSUMPTION_URL;
}
@Override
public String getTempoUrl() {
return TEMPO_URL;
}
@Override
public DateTimeFormatter getApiDateFormat() {
return API_DATE_FORMAT;
}
@Override
public DateTimeFormatter getApiDateFormatYearsFirst() {
return API_DATE_FORMAT_YEAR_FIRST;
}
}

View File

@ -0,0 +1,282 @@
/*
* Copyright (c) 2010-2025 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.net.HttpCookie;
import java.net.URI;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ws.rs.core.MediaType;
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.eclipse.jetty.http.HttpStatus;
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.dto.AuthData;
import org.openhab.binding.linky.internal.dto.AuthResult;
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 LinkyBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(EnedisWebBridgeHandler.class);
public static final String ENEDIS_DOMAIN = ".enedis.fr";
private static final String BASE_URL = "https://alex.microapplications" + ENEDIS_DOMAIN;
public static final String URL_MON_COMPTE = "https://mon-compte" + ENEDIS_DOMAIN;
public static final String URL_COMPTE_PART = URL_MON_COMPTE.replace("compte", "compte-particulier");
public static final URI COOKIE_URI = URI.create(URL_COMPTE_PART);
private static final String USER_INFO_CONTRACT_URL = BASE_URL + "/mon-compte-client/api/private/v1/userinfos";
private static final String USER_INFO_URL = BASE_URL + "/userinfos";
private static final String PRM_INFO_BASE_URL = BASE_URL + "/mes-mesures-prm/api/private/v1/personnes/";
private static final String PRM_INFO_URL = BASE_URL + "/mes-prms-part/api/private/v2/personnes/%s/prms";
private static final String MEASURE_DAILY_CONSUMPTION_URL = PRM_INFO_BASE_URL
+ "%s/prms/%s/donnees-energetiques?mesuresTypeCode=ENERGIE&mesuresCorrigees=false&typeDonnees=CONS";
private static final String MEASURE_MAX_POWER_URL = PRM_INFO_BASE_URL
+ "%s/prms/%s/donnees-energetiques?mesuresTypeCode=PMAX&mesuresCorrigees=false&typeDonnees=CONS";
private static final String LOAD_CURVE_CONSUMPTION_URL = PRM_INFO_BASE_URL
+ "%s/prms/%s/donnees-energetiques?mesuresTypeCode=COURBE&mesuresCorrigees=false&typeDonnees=CONS&dateDebut=%s";
private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter API_DATE_FORMAT_YEAR_FIRST = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final String URL_ENEDIS_AUTHENTICATE = BASE_URL + "/authenticate?target=" + URL_COMPTE_PART;
private static final Pattern REQ_PATTERN = Pattern.compile("ReqID%(.*?)%26");
private static final String BASE_MYELECT_URL = "https://www.myelectricaldata.fr/";
private static final String TEMPO_URL = BASE_MYELECT_URL + "rte/tempo/%s/%s";
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 String getToken(LinkyHandler handler) throws LinkyException {
return "";
}
@Override
public double getDivider() {
return 1.00;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
@Override
public String getContactUrl() {
return USER_INFO_URL;
}
@Override
public String getContractUrl() {
return PRM_INFO_URL;
}
@Override
public String getIdentityUrl() {
return USER_INFO_URL;
}
@Override
public String getAddressUrl() {
return "";
}
@Override
public String getDailyConsumptionUrl() {
return MEASURE_DAILY_CONSUMPTION_URL;
}
@Override
public String getMaxPowerUrl() {
return MEASURE_MAX_POWER_URL;
}
@Override
public String getLoadCurveUrl() {
return LOAD_CURVE_CONSUMPTION_URL;
}
@Override
public String getTempoUrl() {
return TEMPO_URL;
}
@Override
public DateTimeFormatter getApiDateFormat() {
return API_DATE_FORMAT;
}
@Override
public DateTimeFormatter getApiDateFormatYearsFirst() {
return API_DATE_FORMAT_YEAR_FIRST;
}
@Override
public synchronized void connectionInit() throws LinkyException {
LinkyConfiguration lcConfig = config;
if (lcConfig == null) {
return;
}
logger.debug("Starting login process for user: {}", lcConfig.username);
try {
enedisApi.addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, lcConfig.internalAuthId);
logger.debug("Step 1: getting authentification");
String data = enedisApi.getContent(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() != HttpStatus.FOUND_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 = 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" + BASE_URL
+ "/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() != HttpStatus.OK_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() || !lcConfig.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 = lcConfig.password;
logger.debug("Step 4: auth2 - send the auth data");
result = httpClient.POST(authenticateUrl).header(HttpHeader.CONTENT_TYPE, MediaType.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() != HttpStatus.OK_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.getContent(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() != HttpStatus.FOUND_302) {
throw new LinkyException("Connection failed step 6");
}
logger.debug("Step 7: retrieve ");
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"));
enedisApi.addCookie(cookieKey, cookieVal);
connected = true;
} catch (InterruptedException | TimeoutException | ExecutionException | JsonSyntaxException e) {
throw new LinkyException(e, "Error opening connection with Enedis webservice");
}
}
@Override
public boolean supportNewApiFormat() {
return false;
}
}

View File

@ -0,0 +1,198 @@
/*
* Copyright (c) 2010-2025 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.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.ssl.SslContextFactory;
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.io.net.http.HttpClientFactory;
import org.openhab.core.io.net.http.TrustAllTrustManager;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.osgi.framework.BundleContext;
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;
/**
* {@link LinkyBridgeHandler} is the base handler to access enedis data.
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public abstract class LinkyBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(LinkyBridgeHandler.class);
protected final HttpService httpService;
protected final BundleContext bundleContext;
protected final HttpClient httpClient;
protected final EnedisHttpApi enedisApi;
protected final ThingRegistry thingRegistry;
protected final Gson gson;
protected @Nullable LinkyConfiguration config;
protected boolean connected = false;
private static final int REQUEST_BUFFER_SIZE = 8000;
private static final int RESPONSE_BUFFER_SIZE = 200000;
private List<String> registeredPrmId = new ArrayList<>();
public LinkyBridgeHandler(Bridge bridge, final @Reference HttpClientFactory httpClientFactory,
final @Reference OAuthFactory oAuthFactory, final @Reference HttpService httpService,
final @Reference ThingRegistry thingRegistry, ComponentContext componentContext, Gson gson) {
super(bridge);
SslContextFactory sslContextFactory = new SslContextFactory.Client();
try {
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, new TrustManager[] { TrustAllTrustManager.getInstance() }, null);
sslContextFactory.setSslContext(sslContext);
} catch (NoSuchAlgorithmException e) {
logger.warn("An exception occurred while requesting the SSL encryption algorithm : '{}'", e.getMessage(),
e);
} catch (KeyManagementException e) {
logger.warn("An exception occurred while initialising the SSL context : '{}'", e.getMessage(), e);
}
this.gson = gson;
this.httpService = httpService;
this.thingRegistry = thingRegistry;
this.bundleContext = componentContext.getBundleContext();
this.httpClient = httpClientFactory.createHttpClient(LinkyBindingConstants.BINDING_ID, sslContextFactory);
this.httpClient.setFollowRedirects(false);
this.httpClient.setRequestBufferSize(REQUEST_BUFFER_SIZE);
this.httpClient.setResponseBufferSize(RESPONSE_BUFFER_SIZE);
try {
httpClient.start();
} catch (Exception e) {
logger.warn("Unable to start Jetty HttpClient {}", e.getMessage());
}
this.enedisApi = new EnedisHttpApi(this, gson, this.httpClient);
updateStatus(ThingStatus.UNKNOWN);
}
public BundleContext getBundleContext() {
return bundleContext;
}
@Override
public synchronized void initialize() {
logger.debug("Initializing Linky API bridge handler.");
config = getConfigAs(LinkyConfiguration.class);
updateStatus(ThingStatus.UNKNOWN);
scheduler.submit(() -> {
try {
connectionInit();
updateStatus(ThingStatus.ONLINE);
} catch (LinkyException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
});
}
public abstract void connectionInit() throws LinkyException;
public void registerNewPrmId(String prmId) {
if (!registeredPrmId.contains(prmId)) {
registeredPrmId.add(prmId);
}
}
public List<String> getAllPrmId() {
return registeredPrmId;
}
public boolean isConnected() {
return connected;
}
public @Nullable EnedisHttpApi getEnedisApi() {
return enedisApi;
}
@Override
public void dispose() {
logger.debug("Shutting down Linky API bridge handler.");
super.dispose();
}
@Override
protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
super.updateStatus(status, statusDetail, description);
}
public abstract String getToken(LinkyHandler handler) throws LinkyException;
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
public abstract double getDivider();
public abstract String getBaseUrl();
public abstract String getContactUrl();
public abstract String getContractUrl();
public abstract String getIdentityUrl();
public abstract String getAddressUrl();
public abstract String getDailyConsumptionUrl();
public abstract String getMaxPowerUrl();
public abstract String getLoadCurveUrl();
public abstract String getTempoUrl();
public abstract DateTimeFormatter getApiDateFormat();
public abstract DateTimeFormatter getApiDateFormatYearsFirst();
public abstract boolean supportNewApiFormat();
}

View File

@ -0,0 +1,221 @@
/*
* Copyright (c) 2010-2025 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.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;
import org.osgi.service.http.HttpService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* {@link MyElectricalDataBridgeHandler} is the base handler to access enedis data.
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public class MyElectricalDataBridgeHandler extends ApiBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(MyElectricalDataBridgeHandler.class);
private static final String BASE_URL = "https://www.myelectricaldata.fr/";
private static final String CONTRACT_URL = BASE_URL + "contracts/%s/cache/";
private static final String IDENTITY_URL = BASE_URL + "identity/%s/cache/";
private static final String CONTACT_URL = BASE_URL + "contact/%s/cache/";
private static final String ADDRESS_URL = BASE_URL + "addresses/%s/cache/";
private static final String MEASURE_DAILY_CONSUMPTION_URL = BASE_URL + "daily_consumption/%s/start/%s/end/%s/cache";
private static final String MEASURE_MAX_POWER_URL = BASE_URL
+ "daily_consumption_max_power/%s/start/%s/end/%s/cache";
private static final String LOAD_CURVE_CONSUMPTION_URL = BASE_URL
+ "consumption_load_curve/%s/start/%s/end/%s/cache";
// 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 = EnedisBridgeHandler.ENEDIS_ACCOUNT_URL_PROD
+ EnedisBridgeHandler.ENEDIS_AUTHORIZE_URL;
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";
public static final String LINKY_MYELECTRICALDATA_CLIENT_ID = "_h7zLaRr2INxqBI8jhDUQXsa_G4a";
private static final String TEMPO_URL = BASE_URL + "rte/tempo/%s/%s";
private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter API_DATE_FORMAT_YEAR_FIRST = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 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) {
super(bridge, httpClientFactory, oAuthFactory, httpService, thingRegistry, componentContext, gson);
}
@Override
public void connectionInit() {
connected = true;
}
@Override
public void initialize() {
tokenUrl = MyElectricalDataBridgeHandler.LINKY_MYELECTRICALDATA_API_TOKEN_URL;
authorizeUrl = MyElectricalDataBridgeHandler.LINKY_MYELECTRICALDATA_AUTHORIZE_URL;
super.initialize();
}
@Override
public String getClientId() {
return MyElectricalDataBridgeHandler.LINKY_MYELECTRICALDATA_CLIENT_ID;
}
@Override
public String getClientSecret() {
return "";
}
@Override
public boolean getIsSandbox() {
return false;
}
@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(MyElectricalDataBridgeHandler.LINKY_MYELECTRICALDATA_API_TOKEN_URL, getClientId(),
reqCode);
EnedisHttpApi enedisApi = getEnedisApi();
if (enedisApi == null) {
return "";
}
String token = enedisApi.getContent(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.");
super.dispose();
}
@Override
public String getToken(LinkyHandler handler) throws LinkyException {
LinkyConfiguration config = handler.getLinkyConfig();
if (config == null) {
return "";
}
return config.token;
}
@Override
public double getDivider() {
return 1000.00;
}
@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 getLoadCurveUrl() {
return LOAD_CURVE_CONSUMPTION_URL;
}
@Override
public String getTempoUrl() {
return TEMPO_URL;
}
@Override
public DateTimeFormatter getApiDateFormat() {
return API_DATE_FORMAT;
}
@Override
public DateTimeFormatter getApiDateFormatYearsFirst() {
return API_DATE_FORMAT_YEAR_FIRST;
}
}

View File

@ -43,4 +43,4 @@ channel-type.linky.timestamp.label = Timestamp
# Thing status descriptions
offline.config-error-mandatory-settings = Username, password and authId are mandatory.
offline.config-error-mandatory-settings = The prmId is mandatory.

View File

@ -1,46 +0,0 @@
# add-on
addon.linky.name = Extension Linky
addon.linky.description = Cette extension collecte vos données de consommation d'énergie depuis le site internet d'Enedis.
# thing types
thing-type.linky.linky.label = Linky
thing-type.linky.linky.description = Fournit vos données de consommation d'énergie. Pour recevoir ces données, vous devez activer votre compte sur https\://espace-client-particuliers.enedis.fr/web/espace-particuliers/compteur-linky.
# thing types config
thing-type.config.linky.linky.internalAuthId.label = ID d'authentification
thing-type.config.linky.linky.internalAuthId.description = ID d'authentification délivré après le captcha (voir documentation).
thing-type.config.linky.linky.password.label = Mot de Passe
thing-type.config.linky.linky.password.description = Votre mot de passe Enedis
thing-type.config.linky.linky.username.label = Nom d'utilisateur
thing-type.config.linky.linky.username.description = Votre nom d'utilisateur Enedis
# channel group types
channel-group-type.linky.daily.label = Consommation quotidienne
channel-group-type.linky.daily.channel.timestamp.label = Horodatage Pic
channel-group-type.linky.daily.channel.timestamp.description = Horodatage du pic maximum de consommation d'énergie
channel-group-type.linky.daily.channel.yesterday.label = Consommation Hier
channel-group-type.linky.monthly.label = Consommation mensuelle
channel-group-type.linky.monthly.channel.lastMonth.label = Consommation Mois Dernier
channel-group-type.linky.monthly.channel.thisMonth.label = Consommation Mois Actuel
channel-group-type.linky.weekly.label = Consommation hebdomadaire
channel-group-type.linky.weekly.channel.lastWeek.label = Consommation Semaine Dernière
channel-group-type.linky.weekly.channel.thisWeek.label = Consommation Semaine Actuelle
channel-group-type.linky.yearly.label = Consommation annuelle
channel-group-type.linky.yearly.channel.lastYear.label = Consommation Année Dernière
channel-group-type.linky.yearly.channel.thisYear.label = Consommation Année Actuelle
# channel types
channel-type.linky.consumption.label = Consommation Totale
channel-type.linky.consumption.description = Consommation pour un intervalle de temps donné
channel-type.linky.power.label = Pic Consommation Hier
channel-type.linky.power.description = Pic maximum de consommation d'énergie hier
channel-type.linky.timestamp.label = Horodatage
# Thing status descriptions
offline.config-error-mandatory-settings = Le nom d'utilisateur, le mot de passe et l'ID d'authentification sont obligatoires.

View File

@ -4,20 +4,39 @@
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="linky">
<label>Linky</label>
<bridge-type id="enedis">
<label>EnedisBridge</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>
<channel-groups>
<channel-group typeId="daily" id="daily"/>
<channel-group typeId="weekly" id="weekly"/>
<channel-group typeId="monthly" id="monthly"/>
<channel-group typeId="yearly" id="yearly"/>
</channel-groups>
<config-description>
<parameter name="clientId" type="text" required="false">
<label>clientId</label>
<description>Your Enedis clientId</description>
</parameter>
<parameter name="clientSecret" type="text" required="false">
<label>clientSecret</label>
<description>Your Enedis clientSecret</description>
</parameter>
<parameter name="isSandbox" type="boolean" required="false">
<label>isSandbox</label>
<description>To test on the sandbox environment</description>
<advanced>true</advanced>
</parameter>
</config-description>
</bridge-type>
<bridge-type id="enedis-web">
<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">
@ -35,24 +54,127 @@
<description>Authentication ID delivered after the captcha (see documentation).</description>
</parameter>
</config-description>
</bridge-type>
<bridge-type id="my-electrical-data">
<label>MyElectricalDataBridge</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>
</config-description>
</bridge-type>
<thing-type id="linky">
<supported-bridge-type-refs>
<bridge-type-ref id="enedis"/>
<bridge-type-ref id="enedis-web"/>
<bridge-type-ref id="my-electrical-data"/>
</supported-bridge-type-refs>
<label>Linky</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>
<channel-groups>
<channel-group typeId="main" id="main"/>
<channel-group typeId="tempo" id="tempo"/>
<channel-group typeId="loadCurve" id="loadCurve"/>
<channel-group typeId="daily" id="daily"/>
<channel-group typeId="weekly" id="weekly"/>
<channel-group typeId="monthly" id="monthly"/>
<channel-group typeId="yearly" id="yearly"/>
</channel-groups>
<config-description>
<parameter name="prmId" type="text" required="true">
<label>prmId</label>
<description>Your prmId</description>
</parameter>
<parameter name="timezone" type="text" required="false">
<label>timezone</label>
<description>The timezone associated with your Point of delivery.
Will default to openhab default timezone.
You will
need to change this if your linky is located in a different timezone that your openhab location.
You can use an
offset, or a label like Europe/Paris</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>
</thing-type>
<channel-group-type id="loadCurve">
<label>Load curve</label>
<channels>
<channel id="power" typeId="power">
<label>Load Curve Power</label>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="daily">
<label>Daily consumption</label>
<label>Daily Consumption</label>
<channels>
<channel id="yesterday" typeId="consumption">
<label>Yesterday Consumption</label>
</channel>
<channel id="power" typeId="power"/>
<channel id="day-2" typeId="consumption">
<label>Yesterday Consumption</label>
</channel>
<channel id="day-3" typeId="consumption">
<label>Yesterday Consumption</label>
</channel>
<channel id="consumption" typeId="consumption">
<label>Consumption</label>
</channel>
<channel id="maxPower" typeId="power">
<label>Peak Value</label>
<description>Maximum power usage value</description>
</channel>
<channel id="power" typeId="power">
<label>Peak Value Yesterday</label>
<description>Maximum power usage value for Yesterday</description>
</channel>
<channel id="timestamp" typeId="timestamp">
<label>Peak Timestamp</label>
<description>Maximum power usage timestamp</description>
<label>Peak Timestamp Yesterday</label>
<description>Maximum power usage timestamp for Yesterday</description>
</channel>
<channel id="power-2" typeId="power">
<label>Peak Value Day-2</label>
<description>Maximum power usage value for Day-2</description>
</channel>
<channel id="timestamp-2" typeId="timestamp">
<label>Peak Timestamp Day-2</label>
<description>Maximum power usage timestamp for Day-2</description>
</channel>
<channel id="power-3" typeId="power">
<label>Peak Value Day-3</label>
<description>Maximum power usage value for Day-3</description>
</channel>
<channel id="timestamp-3" typeId="timestamp">
<label>Peak Timestamp Day-3</label>
<description>Maximum power usage timestamp for Day-3</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="weekly">
<label>Weekly consumption</label>
<label>Weekly Consumption</label>
<channels>
<channel id="thisWeek" typeId="consumption">
<label>This Week Consumption</label>
@ -60,11 +182,21 @@
<channel id="lastWeek" typeId="consumption">
<label>Last Week Consumption</label>
</channel>
<channel id="week-2" typeId="consumption">
<label>Last Week Consumption</label>
</channel>
<channel id="consumption" typeId="consumption">
<label>Consumption</label>
</channel>
<channel id="maxPower" typeId="power">
<label>Peak Value</label>
<description>Maximum power usage value</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="monthly">
<label>Monthly consumption</label>
<label>Monthly Consumption</label>
<channels>
<channel id="thisMonth" typeId="consumption">
<label>This Month Consumption</label>
@ -72,11 +204,21 @@
<channel id="lastMonth" typeId="consumption">
<label>Last Month Consumption</label>
</channel>
<channel id="month-2" typeId="consumption">
<label>Last Month Consumption</label>
</channel>
<channel id="consumption" typeId="consumption">
<label>Consumption</label>
</channel>
<channel id="maxPower" typeId="power">
<label>Peak Value</label>
<description>Maximum power usage value</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="yearly">
<label>Yearly consumption</label>
<label>Yearly Consumption</label>
<channels>
<channel id="thisYear" typeId="consumption">
<label>This Year Consumption</label>
@ -84,9 +226,129 @@
<channel id="lastYear" typeId="consumption">
<label>Last Year Consumption</label>
</channel>
<channel id="year-2" typeId="consumption">
<label>Last Year Consumption</label>
</channel>
<channel id="consumption" typeId="consumption">
<label>Consumption</label>
</channel>
<channel id="maxPower" typeId="power">
<label>Peak Value</label>
<description>Maximum power usage value</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="tempo">
<label>Tempo</label>
<channels>
<channel id="tempoInfoToday" typeId="tempoValue">
<label>Tempo Today Color</label>
</channel>
<channel id="tempoInfoTomorrow" typeId="tempoValue">
<label>Tempo Today Color</label>
</channel>
<channel id="tempoInfoTimeSeries" typeId="tempoValue">
<label>Tempo Day Information</label>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="main">
<label>Main</label>
<channels>
<channel id="identity" typeId="info">
<label>Identity</label>
</channel>
<channel id="contractSubscribedPower" typeId="info">
<label>Subscribed Power</label>
</channel>
<channel id="contractLastActivationDate" typeId="infoDate">
<label>Last Activation Date</label>
</channel>
<channel id="contractDistributionTariff" typeId="info">
<label>Distribution Tariff</label>
</channel>
<channel id="contractOffpeakHours" typeId="info">
<label>Offpeak Hours</label>
</channel>
<channel id="contractStatus" typeId="info">
<label>Contract Status</label>
</channel>
<channel id="contractType" typeId="info">
<label>Contract Type</label>
</channel>
<channel id="contractLastDistributionTariffChangeDate" typeId="infoDate">
<label>Last Distribution Tariff ChangeDate</label>
</channel>
<channel id="contractSegment" typeId="info">
<label>Contract Segment</label>
</channel>
<channel id="usagePointId" typeId="info">
<label>UsagePoint Id</label>
</channel>
<channel id="usagePointStatus" typeId="info">
<label>UsagePoin Status</label>
</channel>
<channel id="usagePointMeterType" typeId="info">
<label>UsagePoint Meter Type</label>
</channel>
<channel id="usagePointAddressCity" typeId="info">
<label>City</label>
</channel>
<channel id="usagePointAddressCountry" typeId="info">
<label>Country</label>
</channel>
<channel id="usagePointAddressInseeCode" typeId="info">
<label>Insee Code</label>
</channel>
<channel id="usagePointAddressPostalCode" typeId="info">
<label>Postal Code</label>
</channel>
<channel id="usagePointAddressStreet" typeId="info">
<label>Street</label>
</channel>
<channel id="contactMail" typeId="info">
<label>Mail</label>
</channel>
<channel id="contactPhone" typeId="info">
<label>Phone</label>
</channel>
</channels>
</channel-group-type>
<channel-type id="tempoValue">
<item-type>Number</item-type>
<label>Tempo Color Information</label>
<description>This status describes the tempo color of a day.</description>
<state>
<options>
<option value="0">Blue</option>
<option value="1">White</option>
<option value="2">Red</option>
</options>
</state>
</channel-type>
<channel-type id="info" advanced="false">
<item-type>String</item-type>
<label>Information</label>
<description>an information</description>
<category>energy</category>
<state readOnly="true"></state>
</channel-type>
<channel-type id="infoDate">
<item-type>DateTime</item-type>
<label>Information Date</label>
<description>an information of type date</description>
<state readOnly="true" pattern="%1$tF"/>
</channel-type>
<channel-type id="consumption">
<item-type>Number:Energy</item-type>
<label>Total Consumption</label>

View File

@ -0,0 +1,181 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
${pageRefresh}
<title>OpenHAB Linky binding for Enedis</title>
<link rel="icon" href="img/favicon.ico" type="image/vnd.microsoft.icon">
<link>
<style>
html {
font-family: "Roboto", Helvetica, Arial, sans-serif;
}
.block {
border: 1px solid #bbb;
background-color: white;
margin: 10px 0;
padding: 8px 10px;
}
.error {
background: #FFC0C0;
border: 1px solid darkred;
color: darkred
}
.authorized {
border: 1px solid #90EE90;
background-color: #E0FFE0;
}
.button {
margin-left:30px;
margin-bottom: 10px;
float:left;
}
.olList {
margin-top:20px;
}
.olList li {
margin:20px;
}
.box {
float:left;
transform: translate(0%, -30%);
}
.box select {
background-color: #0563af;
color: white;
padding: 12px;
width: 350px;
border: none;
font-size: 20px;
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
-webkit-appearance: button;
appearance: button;
outline: none;
}
/* Style the arrow inside the select element: */
.box::before {
content: "\f13a";
font-family: FontAwesome;
position: absolute;
top: 0;
right: 0;
width: 50px;
height: 100%;
text-align: center;
font-size: 28px;
line-height: 45px;
color: rgba(255, 255, 255, 0.5);
background-color: rgba(255, 255, 255, 0.1);
pointer-events: none;
}
.logo {
display: inline;
}
.logoEnedis {
display: inline;
}
.logoTransfer {
display: inline;
margin-left:300px;
top: -30px;
}
.button a {
background: #1ED760;
border-radius: 500px;
color: white;
padding: 10px 20px 10px;
font-size: 16px;
font-weight: 700;
border-width: 0;
text-decoration: none;
}
</style>
<script language="javascript">
function retrieveToken()
{
var prmId = document.getElementById('prmId').value;
document.location= "${retrieveToken.uri}" + "&code=" + prmId;
}
</script>
</head>
<body>
<div style="margin:50px">
<div class="logo" style="vertical-align:top;">
<div style="display: inline-block;width:500;margin-right:100px;margin-top:0px;margin-bottom:0px;">
<img src="/connectlinky/img/openhab_logo.svg" height="100"/>
</div>
<div style="display: inline-block;width:100;margin-top:0px;margin-bottom:0px;">
</div>
<div style="display: inline-block;width:300;margin-top:0px;margin-bottom:0px;">
<img src="/connectlinky/img/enedis.png" height="100">
</div>
</div>
<br/><br/><br/>
<hr style="margin:0px;padding-left:20px;padding-right:20px;width:97%;"/>
<div style="display: block;width:400;margin-top:0px;margin-bottom:0px;float:right;top:60px;right:160px;position:absolute;">
<svg xmlns="http://www.w3.org/2000/svg" width="400" version="1.1">
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<circle cx="100" cy="110" r="20" stroke="black" stroke-width="1" fill="red" />
<text x="100" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">1</text>
<polyline points="130,110 170,110" fill="none" stroke="black" marker-end="url(#arrow)" />
<circle cx="200" cy="110" r="20" stroke="red" stroke-width="1" fill="none" />
<text x="200" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">2</text>
<polyline points="230,110 270,110" fill="none" stroke="black" marker-end="url(#arrow)" />
<circle cx="300" cy="110" r="20" stroke="black" stroke-width="1" fill="none" />
<text x="300" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">3</text>
</svg>
</div>
<div style="background:linear-gradient(#faf3ff, #e8cbfd);padding:20px;margin:0px;display:inline-block; width:97%;align:center;">
<h3>Plugin Linky / Enedis pour Openhab</h3>
<br/>
<p>Ce plugin permet d'exploiter vos données de consommation éléctrique fournis par Enedis au sein d'OpenHab.</p>
<p>Enedis gère le réseau délectricité jusquau compteur délectricité<br/>
Enedis est le gestionnaire du réseau public de distribution délectricité sur 95% du territoire français continental.</p>
<p>Grace à ce plugin, vous serez en mesure de : </p>
<ul>
<li>Consulter les informations contractuelles liés à votre compte.</li>
<li>Créer des graphes de consommation par jour / semaine / mois / annéee.</li>
<li>Consulter la puissance maximum utilisé sur une période donnée.</li>
<li>Load curve</li>
</ul>
<br/><br/>
<div>
<div class="button">
<a href="/connectlinky/enedis-step2">Suite
</a>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,174 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
${pageRefresh}
<title>OpenHAB Linky binding for Enedis</title>
<link rel="icon" href="img/favicon.ico" type="image/vnd.microsoft.icon">
<link>
<style>
html {
font-family: "Roboto", Helvetica, Arial, sans-serif;
}
.block {
border: 1px solid #bbb;
background-color: white;
margin: 10px 0;
padding: 8px 10px;
}
.error {
background: #FFC0C0;
border: 1px solid darkred;
color: darkred
}
.authorized {
border: 1px solid #90EE90;
background-color: #E0FFE0;
}
.olList {
margin-top:20px;
}
.olList li {
margin:20px;
}
.box {
float:left;
transform: translate(0%, -30%);
}
.box select {
background-color: #0563af;
color: white;
padding: 12px;
width: 350px;
border: none;
font-size: 20px;
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
-webkit-appearance: button;
appearance: button;
outline: none;
}
/* Style the arrow inside the select element: */
.box::before {
content: "\f13a";
font-family: FontAwesome;
position: absolute;
top: 0;
right: 0;
width: 50px;
height: 100%;
text-align: center;
font-size: 28px;
line-height: 45px;
color: rgba(255, 255, 255, 0.5);
background-color: rgba(255, 255, 255, 0.1);
pointer-events: none;
}
.logo {
display: inline;
}
.logoEnedis {
display: inline;
}
.logoTransfer {
display: inline;
margin-left:300px;
top: -30px;
}
</style>
<script language="javascript">
function retrieveToken()
{
var prmId = document.getElementById('prmId').value;
document.location= "${retrieveToken.uri}" + "&code=" + prmId;
}
</script>
</head>
<body>
<div style="margin:50px">
<div class="logo" style="vertical-align:top;">
<div style="display: inline-block;width:500;margin-right:100px;">
<img src="/connectlinky/img/openhab_logo.svg" height="100"/>
</div>
<div style="display: inline-block;width:100;">
</div>
<div style="display: inline-block;width:300;">
<img src="/connectlinky/img/enedis.png" height="100">
</div>
</div>
<br/><br/><br/>
<hr style="margin:0px;padding-left:20px;padding-right:20px;width:97%;"/>
<div style="display: block;width:400;margin-top:0px;margin-bottom:0px;float:right;top:60px;right:160px;position:absolute;">
<svg xmlns="http://www.w3.org/2000/svg" width="400" version="1.1">
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<circle cx="100" cy="110" r="20" stroke="black" stroke-width="1" fill="none" />
<text x="100" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">1</text>
<polyline points="130,110 170,110" fill="none" stroke="black" marker-end="url(#arrow)" />
<circle cx="200" cy="110" r="20" stroke="red" stroke-width="1" fill="red" />
<text x="200" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">2</text>
<polyline points="230,110 270,110" fill="none" stroke="black" marker-end="url(#arrow)" />
<circle cx="300" cy="110" r="20" stroke="black" stroke-width="1" fill="none" />
<text x="300" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">3</text>
</svg>
</div>
<div style="background:linear-gradient(#faf3ff, #e8cbfd);padding:20px;margin:0px;display:inline-block; width:97%;align:center;">
<h3>Plugin Linky / Enedis pour Openhab</h3>
<br/>
<div style="float:left; margin:30px;">
<img src="/connectlinky/img/linky.svg"/>
</div>
<p>Pour utiliser ce plugin, vous devez donner votre accord pour qu'Enedis transmette vos données.</p>
<p><b>
Enedis est le gestionnaire du réseau public de distribution délectricité sur 95% du territoire français continental.<br/>
Enedis gère le réseau délectricité jusquau compteur délectricité.
</b>
</p>
<p>Pour donner votre autorisation, vous devez avoir un compte personnel Enedis. <br/>
Ce compte vous permet également de suivre et gérer vos données de consommation [ou production en fonction de votre service] délectricité.</p>
<p>Si vous n'avez pas de compte, vous pouvez le créer depuis cette <a href="https://mon-compte-client.enedis.fr/">page</a>.<br/>
Munissez-vous pour celà de votre facture délectricité pour créer votre espace.</p>
<p>En cliquant sur le bouton ci-dessous, vous allez accéder à votre compte personnel Enedis où vous pourrez donner votre accord pour quEnedis nous transmette vos données.</p>
<p>Une fois cette opération effectué, vous serez rediriger vers une page de confirmation.</p>
<div class="button" style="float:right;margin-right:30px;margin-bottom:10px;height:100px;position:relative;display:block;">
<a href=${authorize.uri}><img src="/connectlinky/img/boutonEnedis.png"/>
</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,174 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
${pageRefresh}
<title>OpenHAB Linky binding for Enedis</title>
<link rel="icon" href="img/favicon.ico" type="image/vnd.microsoft.icon">
<link>
<style>
html {
font-family: "Roboto", Helvetica, Arial, sans-serif;
}
.block {
border: 1px solid #bbb;
background-color: white;
margin: 10px 0;
padding: 8px 10px;
}
.error {
background: #FFC0C0;
border: 1px solid darkred;
color: darkred
}
.authorized {
border: 1px solid #90EE90;
background-color: #E0FFE0;
}
.button {
margin-left:30px;
margin-bottom: 10px;
float:left;
}
.olList {
margin-top:20px;
}
.olList li {
margin:20px;
}
.box {
float:left;
transform: translate(0%, -30%);
}
.box select {
background-color: #0563af;
color: white;
padding: 12px;
width: 350px;
border: none;
font-size: 20px;
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
-webkit-appearance: button;
appearance: button;
outline: none;
}
/* Style the arrow inside the select element: */
.box::before {
content: "\f13a";
font-family: FontAwesome;
position: absolute;
top: 0;
right: 0;
width: 50px;
height: 100%;
text-align: center;
font-size: 28px;
line-height: 45px;
color: rgba(255, 255, 255, 0.5);
background-color: rgba(255, 255, 255, 0.1);
pointer-events: none;
}
.logo {
display: inline;
}
.logoEnedis {
display: inline;
}
.logoTransfer {
display: inline;
margin-left:300px;
top: -30px;
}
.button a {
background: #1ED760;
border-radius: 500px;
color: white;
padding: 10px 20px 10px;
font-size: 16px;
font-weight: 700;
border-width: 0;
text-decoration: none;
}
</style>
<script language="javascript">
function retrieveToken()
{
var prmId = document.getElementById('prmId').value;
document.location= "${retrieveToken.uri}" + "&code=" + prmId;
}
</script>
</head>
<body>
<div style="margin:50px">
<div class="logo" style="vertical-align:top;">
<div style="display: inline-block;width:500;margin-right:100px;">
<img src="/connectlinky/img/openhab_logo.svg" height="100"/>
</div>
<div style="display: inline-block;width:100;">
</div>
<div style="display: inline-block;width:300;">
<img src="/connectlinky/img/enedis.png" height="100">
</div>
</div>
<br/><br/><br/>
<hr style="margin:0px;padding-left:20px;padding-right:20px;width:97%;"/>
<div style="display: block;width:400;margin-top:0px;margin-bottom:0px;float:right;top:60px;right:160px;position:absolute;">
<svg xmlns="http://www.w3.org/2000/svg" width="400" version="1.1">
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<circle cx="100" cy="110" r="20" stroke="black" stroke-width="1" fill="none" />
<text x="100" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">1</text>
<polyline points="130,110 170,110" fill="none" stroke="black" marker-end="url(#arrow)" />
<circle cx="200" cy="110" r="20" stroke="red" stroke-width="1" fill="none" />
<text x="200" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">2</text>
<polyline points="230,110 270,110" fill="none" stroke="black" marker-end="url(#arrow)" />
<circle cx="300" cy="110" r="20" stroke="black" stroke-width="1" fill="red" />
<text x="300" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">3</text>
</svg>
</div>
<div style="background:linear-gradient(#faf3ff, #e8cbfd);padding:20px;margin:0px;display:inline-block; width:97%;align:center;">
<h3>Plugin Linky / Enedis pour Openhab</h3>
<br/>
<div style="display:${cb.displayConfirmation}">
Vous avez autorisé l'accès pour le compteur Linky : ${prmId.Value}<br/>
Vous pouvez maintenant utiliser le plugin Linky avec Enedis.<br/>
${authorizedUser}
</div>
<div style="display:${cb.displayError}">
Une erreur c'est produite:
${error}
</div>
<p>
<br/>
</body>
</html>

View File

@ -0,0 +1,152 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
${pageRefresh}
<title>Authorize openHAB Linky binding for Enedis</title>
<link rel="icon" href="img/favicon.ico" type="image/vnd.microsoft.icon">
<link>
<style>
html {
font-family: "Roboto", Helvetica, Arial, sans-serif;
}
.block {
border: 1px solid #bbb;
background-color: white;
margin: 10px 0;
padding: 8px 10px;
}
.error {
background: #FFC0C0;
border: 1px solid darkred;
color: darkred
}
.authorized {
border: 1px solid #90EE90;
background-color: #E0FFE0;
}
.button {
margin-left:30px;
margin-bottom: 10px;
float:left;
}
.olList {
margin-top:20px;
}
.olList li {
margin:20px;
}
.box {
float:left;
transform: translate(0%, -30%);
}
.box select {
background-color: #0563af;
color: white;
padding: 12px;
width: 350px;
border: none;
font-size: 20px;
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
-webkit-appearance: button;
appearance: button;
outline: none;
}
/* Style the arrow inside the select element: */
.box::before {
content: "\f13a";
font-family: FontAwesome;
position: absolute;
top: 0;
right: 0;
width: 50px;
height: 100%;
text-align: center;
font-size: 28px;
line-height: 45px;
color: rgba(255, 255, 255, 0.5);
background-color: rgba(255, 255, 255, 0.1);
pointer-events: none;
}
.logo {
display: inline;
}
.logoEnedis {
display: inline;
}
.logoTransfer {
display: inline;
margin-left:300px;
top: -30px;
}
.button a {
background: #1ED760;
border-radius: 500px;
color: white;
padding: 10px 20px 10px;
font-size: 16px;
font-weight: 700;
border-width: 0;
text-decoration: none;
}
</style>
<script language="javascript">
function retrieveToken()
{
var prmId = document.getElementById('prmId').value;
document.location= "${retrieveToken.uri}" + "&code=" + prmId;
}
</script>
</head>
<body>
<div style="margin:50px">
<div class="logo" style="vertical-align:top;">
<div style="display: inline-block;width:500;margin-right:100px;margin-top:0px;margin-bottom:0px;">
<img src="/connectlinky/img/openhab_logo.svg" height="100"/>
</div>
<div style="display: inline-block;width:100;margin-top:0px;margin-bottom:0px;">
</div>
</div>
<br/><br/><br/>
<hr style="margin:0px;padding-left:20px;padding-right:20px;width:97%;"/>
<div style="background:linear-gradient(#faf3ff, #e8cbfd);padding:20px;margin:0px;display:inline-block; width:97%;align:center;">
<h1>
Merci de sélectionner votre provider
</h1>
<div style="float:center;width:100%;">
<div style="display: inline-block;width:300;margin:100px;padding:20px;background-color:#ffffff;">
<a href="/connectlinky/myelectricaldata"><img src="/connectlinky/img/MyElectricalData.png" height="100"></a>
</div>
<div style="display: inline-block;width:300;margin:100px;padding:20px;background-color:#ffffff;">
<a href="/connectlinky/enedis"><img src="/connectlinky/img/enedis.png" height="100"></a>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,181 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
${pageRefresh}
<title>OpenHAB Linky binding for Enedis</title>
<link rel="icon" href="img/favicon.ico" type="image/vnd.microsoft.icon">
<link>
<style>
html {
font-family: "Roboto", Helvetica, Arial, sans-serif;
}
.block {
border: 1px solid #bbb;
background-color: white;
margin: 10px 0;
padding: 8px 10px;
}
.error {
background: #FFC0C0;
border: 1px solid darkred;
color: darkred
}
.authorized {
border: 1px solid #90EE90;
background-color: #E0FFE0;
}
.button {
margin-left:30px;
margin-bottom: 10px;
float:left;
}
.olList {
margin-top:20px;
}
.olList li {
margin:20px;
}
.box {
float:left;
transform: translate(0%, -30%);
}
.box select {
background-color: #0563af;
color: white;
padding: 12px;
width: 350px;
border: none;
font-size: 20px;
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
-webkit-appearance: button;
appearance: button;
outline: none;
}
/* Style the arrow inside the select element: */
.box::before {
content: "\f13a";
font-family: FontAwesome;
position: absolute;
top: 0;
right: 0;
width: 50px;
height: 100%;
text-align: center;
font-size: 28px;
line-height: 45px;
color: rgba(255, 255, 255, 0.5);
background-color: rgba(255, 255, 255, 0.1);
pointer-events: none;
}
.logo {
display: inline;
}
.logoEnedis {
display: inline;
}
.logoTransfer {
display: inline;
margin-left:300px;
top: -30px;
}
.button a {
background: #1ED760;
border-radius: 500px;
color: white;
padding: 10px 20px 10px;
font-size: 16px;
font-weight: 700;
border-width: 0;
text-decoration: none;
}
</style>
<script language="javascript">
function retrieveToken()
{
var prmId = document.getElementById('prmId').value;
document.location= "${retrieveToken.uri}" + "&code=" + prmId;
}
</script>
</head>
<body>
<div style="margin:50px">
<div class="logo" style="vertical-align:top;">
<div style="display: inline-block;width:500;margin-right:100px;margin-top:0px;margin-bottom:0px;">
<img src="/connectlinky/img/openhab_logo.svg" height="100"/>
</div>
<div style="display: inline-block;width:100;margin-top:0px;margin-bottom:0px;">
</div>
<div style="display: inline-block;width:300;margin-top:0px;margin-bottom:0px;">
<img src="/connectlinky/img/MyElectricalData.png" height="100">
</div>
</div>
<br/><br/><br/>
<hr style="margin:0px;padding-left:20px;padding-right:20px;width:97%;"/>
<div style="display: block;width:400;margin-top:0px;margin-bottom:0px;float:right;top:60px;right:160px;position:absolute;">
<svg xmlns="http://www.w3.org/2000/svg" width="400" version="1.1">
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<circle cx="100" cy="110" r="20" stroke="black" stroke-width="1" fill="red" />
<text x="100" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">1</text>
<polyline points="130,110 170,110" fill="none" stroke="black" marker-end="url(#arrow)" />
<circle cx="200" cy="110" r="20" stroke="red" stroke-width="1" fill="none" />
<text x="200" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">2</text>
<polyline points="230,110 270,110" fill="none" stroke="black" marker-end="url(#arrow)" />
<circle cx="300" cy="110" r="20" stroke="black" stroke-width="1" fill="none" />
<text x="300" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">3</text>
</svg>
</div>
<div style="background:linear-gradient(#faf3ff, #e8cbfd);padding:20px;margin:0px;display:inline-block; width:97%;align:center;">
<h3>Plugin Linky / MyElectricalData pour Openhab</h3>
<br/>
<p>Ce plugin permet d'exploiter vos données de consommation éléctrique fournis par Enedis au sein d'OpenHab.</p>
<p>Enedis gère le réseau délectricité jusquau compteur délectricité<br/>
Enedis est le gestionnaire du réseau public de distribution délectricité sur 95% du territoire français continental.</p>
<p>Grace à ce plugin, vous serez en mesure de : </p>
<ul>
<li>Consulter les informations contractuelles liés à votre compte.</li>
<li>Créer des graphes de consommation par jour / semaine / mois / annéee.</li>
<li>Consulter la puissance maximum utilisé sur une période donnée.</li>
<li>Load curve</li>
</ul>
<br/><br/>
<div>
<div class="button">
<a href="/connectlinky/myelectricaldata-step2">Suite
</a>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,169 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
${pageRefresh}
<title>OpenHAB Linky binding for Enedis</title>
<link rel="icon" href="img/favicon.ico" type="image/vnd.microsoft.icon">
<link>
<style>
html {
font-family: "Roboto", Helvetica, Arial, sans-serif;
}
.block {
border: 1px solid #bbb;
background-color: white;
margin: 10px 0;
padding: 8px 10px;
}
.error {
background: #FFC0C0;
border: 1px solid darkred;
color: darkred
}
.authorized {
border: 1px solid #90EE90;
background-color: #E0FFE0;
}
.olList {
margin-top:20px;
}
.olList li {
margin:20px;
}
.box {
float:left;
transform: translate(0%, -30%);
}
.box select {
background-color: #0563af;
color: white;
padding: 12px;
width: 350px;
border: none;
font-size: 20px;
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
-webkit-appearance: button;
appearance: button;
outline: none;
}
/* Style the arrow inside the select element: */
.box::before {
content: "\f13a";
font-family: FontAwesome;
position: absolute;
top: 0;
right: 0;
width: 50px;
height: 100%;
text-align: center;
font-size: 28px;
line-height: 45px;
color: rgba(255, 255, 255, 0.5);
background-color: rgba(255, 255, 255, 0.1);
pointer-events: none;
}
.logo {
display: inline;
}
.logoEnedis {
display: inline;
}
.logoTransfer {
display: inline;
margin-left:300px;
top: -30px;
}
</style>
<script language="javascript">
function retrieveToken()
{
var prmId = document.getElementById('prmId').value;
document.location= "${retrieveToken.uri}" + "&code=" + prmId;
}
</script>
</head>
<body>
<div style="margin:50px">
<div class="logo" style="vertical-align:top;">
<div style="display: inline-block;width:500;margin-right:100px;">
<img src="/connectlinky/img/openhab_logo.svg" height="100"/>
</div>
<div style="display: inline-block;width:100;">
</div>
<div style="display: inline-block;width:300;">
<img src="/connectlinky/img/MyElectricalData.png" height="100">
</div>
</div>
<br/><br/><br/>
<hr style="margin:0px;padding-left:20px;padding-right:20px;width:97%;"/>
<div style="display: block;width:400;margin-top:0px;margin-bottom:0px;float:right;top:60px;right:160px;position:absolute;">
<svg xmlns="http://www.w3.org/2000/svg" width="400" version="1.1">
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<circle cx="100" cy="110" r="20" stroke="black" stroke-width="1" fill="none" />
<text x="100" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">1</text>
<polyline points="130,110 170,110" fill="none" stroke="black" marker-end="url(#arrow)" />
<circle cx="200" cy="110" r="20" stroke="red" stroke-width="1" fill="red" />
<text x="200" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">2</text>
<polyline points="230,110 270,110" fill="none" stroke="black" marker-end="url(#arrow)" />
<circle cx="300" cy="110" r="20" stroke="black" stroke-width="1" fill="none" />
<text x="300" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">3</text>
</svg>
</div>
<div style="background:linear-gradient(#faf3ff, #e8cbfd);padding:20px;margin:0px;display:inline-block; width:97%;align:center;">
<h3>Plugin Linky / MyElectricalData pour Openhab</h3>
<br/>
<div style="float:left; margin:30px;">
<img src="/connectlinky/img/linky.svg"/>
</div>
<p>Pour utiliser ce plugin, vous devez donner votre accord pour qu'Enedis transmette vos données.</p>
<p>Pour donner votre autorisation, vous devez avoir un compte personnel Enedis. <br/>
Ce compte vous permet également de suivre et gérer vos données de consommation [ou production en fonction de votre service] délectricité.</p>
<p>Si vous n'avez pas de compte, vous pouvez le créer depuis cette <a href="https://mon-compte-client.enedis.fr/">page</a>.<br/>
Munissez-vous pour celà de votre facture délectricité pour créer votre espace.</p>
<p>En cliquant sur le bouton ci-dessous, vous allez accéder à votre compte personnel Enedis où vous pourrez donner votre accord pour quEnedis nous transmette vos données.</p>
<p>Une fois cette opération effectué, vous devez vous rendre manuellement sur cette <a href="/connectlinky/myelectricaldata-step3">page</a> pour terminer la procédure.</p>
<div class="button" style="float:right;margin-right:30px;margin-bottom:10px;height:100px;position:relative;display:block;">
<a href=${authorize.uri}><img src="/connectlinky/img/boutonEnedis.png"/>
</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,201 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
${pageRefresh}
<title>OpenHAB Linky binding for Enedis</title>
<link rel="icon" href="img/favicon.ico" type="image/vnd.microsoft.icon">
<link>
<style>
html {
font-family: "Roboto", Helvetica, Arial, sans-serif;
}
.block {
border: 1px solid #bbb;
background-color: white;
margin: 10px 0;
padding: 8px 10px;
}
.error {
background: #FFC0C0;
border: 1px solid darkred;
color: darkred
}
.authorized {
border: 1px solid #90EE90;
background-color: #E0FFE0;
}
.button {
margin-left:30px;
margin-bottom: 10px;
float:left;
}
.olList {
margin-top:20px;
}
.olList li {
margin:20px;
}
.box {
float:left;
transform: translate(0%, -30%);
}
.box select {
background-color: #0563af;
color: white;
padding: 12px;
width: 350px;
border: none;
font-size: 20px;
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
-webkit-appearance: button;
appearance: button;
outline: none;
}
/* Style the arrow inside the select element: */
.box::before {
content: "\f13a";
font-family: FontAwesome;
position: absolute;
top: 0;
right: 0;
width: 50px;
height: 100%;
text-align: center;
font-size: 28px;
line-height: 45px;
color: rgba(255, 255, 255, 0.5);
background-color: rgba(255, 255, 255, 0.1);
pointer-events: none;
}
.logo {
display: inline;
}
.logoEnedis {
display: inline;
}
.logoTransfer {
display: inline;
margin-left:300px;
top: -30px;
}
.button a {
background: #1ED760;
border-radius: 500px;
color: white;
padding: 10px 20px 10px;
font-size: 16px;
font-weight: 700;
border-width: 0;
text-decoration: none;
}
</style>
<script language="javascript">
function retrieveToken()
{
var prmId = document.getElementById('prmId').value;
document.location= "${retrieveToken.uri}" + "&code=" + prmId;
}
</script>
</head>
<body>
<div style="margin:50px">
<div class="logo" style="vertical-align:top;">
<div style="display: inline-block;width:500;margin-right:100px;">
<img src="/connectlinky/img/openhab_logo.svg" height="100"/>
</div>
<div style="display: inline-block;width:100;">
</div>
<div style="display: inline-block;width:300;">
<img src="/connectlinky/img/MyElectricalData.png" height="100">
</div>
</div>
<br/><br/><br/>
<hr style="margin:0px;padding-left:20px;padding-right:20px;width:97%;"/>
<div style="display: block;width:400;margin-top:0px;margin-bottom:0px;float:right;top:60px;right:160px;position:absolute;">
<svg xmlns="http://www.w3.org/2000/svg" width="400" version="1.1">
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<circle cx="100" cy="110" r="20" stroke="black" stroke-width="1" fill="none" />
<text x="100" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">1</text>
<polyline points="130,110 170,110" fill="none" stroke="black" marker-end="url(#arrow)" />
<circle cx="200" cy="110" r="20" stroke="red" stroke-width="1" fill="none" />
<text x="200" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">2</text>
<polyline points="230,110 270,110" fill="none" stroke="black" marker-end="url(#arrow)" />
<circle cx="300" cy="110" r="20" stroke="black" stroke-width="1" fill="red" />
<text x="300" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">3</text>
</svg>
</div>
<div style="background:linear-gradient(#faf3ff, #e8cbfd);padding:20px;margin:0px;display:inline-block; width:97%;align:center;">
<h3>Plugin Linky / MyElectricalData pour Openhab</h3>
<br/>
<div style="display:${cb.displayConfirmation}">
Vous pouvez maintenant utiliser le plugin Linky avec MyElectricalData.
${authorizedUser}
</div>
<div style="display:${cb.displayError}">
Une erreur c'est produite:
${error}
</div>
<div style="display:${cb.displayInstruction}">
<p>
Vous devez maintenant récupérer le token depuis MyElectricalData.
</p>
<p>
Pour ce faire :
<ul>
<li>Sélectionner le numéro de prmId dans la combobox ci-dessous.</li>
<li>Cliquer sur le bouton "Retrive token".</li>
</ul>
</p>
<br/>
<br/><br/><br/>
<div class="block${bridge.authorized}">
<br/>
<div>
<div class="box">
<b>Please select your prmId :</b>
<select id="prmId">
${prmId.Option}
</select>
</div>
<div class="button">
<a href="javascript:retrieveToken()">Retrieve token
</a>
</div>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="88px" height="130px" viewBox="0 0 88 130" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 54.1 (76490) - https://sketchapp.com -->
<title>A0924594-7213-430D-A943-FE07E1B6E747</title>
<desc>Created with sketchtool.</desc>
<defs>
<polygon id="path-1" points="0.110730673 0.170093458 86.2396006 0.170093458 86.2396006 129.793739 0.110730673 129.793739"></polygon>
<polygon id="path-3" points="0.325427204 0.129727087 48.7098019 0.129727087 48.7098019 56.2562126 0.325427204 56.2562126"></polygon>
<path d="M0.196850252,0.568317324 L0.196850252,4.99144282 C0.196850252,5.15434811 0.336285847,5.2833148 0.506637026,5.28578306 L0.506637026,5.28578306 L8.31313756,5.35242613 C8.48285781,5.35242613 8.6222934,5.22099118 8.6222934,5.06055415 L8.6222934,5.06055415 L8.6222934,0.645450511 C8.6222934,0.488098809 8.48285781,0.362834513 8.31313756,0.35913212 L8.31313756,0.35913212 L0.506637026,0.280764802 C0.336285847,0.28508426 0.196850252,0.412199753 0.196850252,0.568317324 L0.196850252,0.568317324 Z" id="path-5"></path>
<path d="M0.196850252,0.568317324 L0.196850252,4.99144282 C0.196850252,5.15434811 0.336285847,5.2833148 0.506637026,5.28578306 L0.506637026,5.28578306 L8.31313756,5.35242613 C8.48285781,5.35242613 8.6222934,5.22099118 8.6222934,5.06055415 L8.6222934,5.06055415 L8.6222934,0.645450511 C8.6222934,0.488098809 8.48285781,0.362834513 8.31313756,0.35913212 L8.31313756,0.35913212 L0.506637026,0.280764802 C0.336285847,0.28508426 0.196850252,0.412199753 0.196850252,0.568317324" id="path-7"></path>
<polygon id="path-9" points="0.363135436 4.99082575 1.58601855 4.98835749 1.58601855 0.284467195 0.363135436 0.271508819"></polygon>
</defs>
<g id="1---CONSOMMATION" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="1-SDM---J-5.1---Pmax-Tableau" transform="translate(-354.000000, -281.000000)">
<g id="Emilie-/-SDM-/-Compteur-Nav-gauche" transform="translate(265.000000, 265.000000)">
<g id="Group-3">
<g id="Atome-/-illustration-/-Compteur-/-Linky" transform="translate(87.000000, 16.000000)">
<g id="Group-61" transform="translate(2.816327, 0.000000)">
<g id="Group-10">
<g id="Group-3">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="Clip-2"></g>
<path d="M86.2396006,125.153794 C86.2396006,127.724636 83.7948891,129.804028 80.7559186,129.793701 L5.57894389,129.683748 C2.56124301,129.681318 0.110730673,127.59585 0.110730673,125.035944 L0.110730673,4.80052336 C0.110730673,2.2412243 2.56124301,0.156364486 5.57894389,0.169728972 L80.7559186,0.277859813 C83.7948891,0.292439252 86.2396006,2.36757944 86.2396006,4.92748598 L86.2396006,125.153794 Z" id="Fill-1" fill="#CCD41F" mask="url(#mask-2)"></path>
</g>
<path d="M68.6672615,86.6276869 C68.6672615,89.3388551 66.0730185,91.5373131 62.8761376,91.5415654 L18.4530282,91.4692757 C15.2548582,91.4692757 12.6515917,89.2550234 12.6515917,86.5353505 L12.6515917,31.9049766 C12.6515917,29.1871262 15.2548582,26.9825935 18.4530282,26.9917056 L62.8761376,27.0573131 C66.0730185,27.0621729 68.6672615,29.268528 68.6672615,31.9948832 L68.6672615,86.6276869 Z" id="Fill-4" fill="#B7C243"></path>
<path d="M68.6672615,86.6276869 C68.6672615,89.3388551 66.0730185,91.5373131 62.8761376,91.5415654 L18.4530282,91.4692757 C15.2548582,91.4692757 12.6515917,89.2550234 12.6515917,86.5353505 L12.6515917,31.9049766 C12.6515917,29.1871262 15.2548582,26.9825935 18.4530282,26.9917056 L62.8761376,27.0573131 C66.0730185,27.0621729 68.6672615,29.268528 68.6672615,31.9948832 L68.6672615,86.6276869 Z" id="Stroke-6" stroke="#B7C243" stroke-width="0.365"></path>
<path d="M69.6296774,82.3895654 C69.6296774,84.9537243 67.1765869,87.0227897 64.1530852,87.0312944 L22.1966015,86.9650794 C19.1730998,86.9602196 16.7129195,84.8729299 16.7129195,82.3008738 L16.7129195,30.7012009 C16.7129195,28.1346121 19.1730998,26.0527897 22.1966015,26.0588645 L64.1530852,26.1208271 C67.1765869,26.1226495 69.6296774,28.2069019 69.6296774,30.7844252 L69.6296774,82.3895654 Z" id="Fill-8" fill="#CFCEC4"></path>
</g>
<g id="Group-13" transform="translate(18.647495, 27.318841)">
<mask id="mask-4" fill="white">
<use xlink:href="#path-3"></use>
</mask>
<g id="Clip-12"></g>
<path d="M48.7098019,51.988681 C48.7098019,54.3470215 46.4711985,56.2519292 43.7082957,56.2562126 L5.33952434,56.2029755 C2.57855861,56.1937967 0.32510436,54.2711434 0.32510436,51.9066837 L0.32510436,4.40576441 C0.32510436,2.03946891 2.57855861,0.129665895 5.33952434,0.129665895 L43.7082957,0.190245997 C46.4711985,0.19820096 48.7098019,2.11228742 48.7098019,4.4810306 L48.7098019,51.988681 Z" id="Fill-11" fill="#F9F9F7" mask="url(#mask-4)"></path>
</g>
<g id="Group-26" transform="translate(16.684601, 25.434783)">
<polygon id="Stroke-14" stroke="#7F8074" stroke-width="0.219" points="25.6782204 61.0846162 12.9698822 61.1106135 12.9698822 53.2775704 25.6782204 53.2449227"></polygon>
<polygon id="Stroke-16" stroke="#7F8074" stroke-width="0.219" points="42.2035583 61.1719792 29.5067872 61.2028132 29.5067872 53.3540508 42.2035583 53.3365178"></polygon>
<polygon id="Fill-18" fill="#9AA3AB" points="9.48357167 28.6337747 9.48357167 40.2285647 45.1190989 40.2811639 45.1190989 28.6797234"></polygon>
<path d="M9.48357167,52.0046103 C9.48357167,52.6950498 10.1551035,53.2579213 10.9673678,53.2645718 L43.6301618,53.3099159 C44.4507801,53.3147526 45.1190989,52.7409985 45.1190989,52.0439085 L45.1190989,40.281043 L9.48357167,40.2284438 L9.48357167,52.0046103 Z" id="Fill-20" fill="#4F4F4F"></path>
<path d="M45.1191631,17.4799108 C45.1191631,16.7767748 44.4508444,16.2157171 43.6302261,16.2247859 L10.9667895,16.1746051 C10.1551678,16.1613042 9.48299332,16.739895 9.48299332,17.4363804 L9.48299332,28.6339561 L45.1191631,28.6799048 L45.1191631,17.4799108 Z" id="Fill-22" fill="#4F4F4F"></path>
<path d="M53.3570915,56.6051023 C53.3570915,59.1570685 50.9113019,61.2162955 47.8967987,61.2247597 L6.0651855,61.1588596 C3.05068234,61.1540229 0.59782398,59.0766583 0.59782398,56.5168324 L0.59782398,5.16251708 C0.59782398,2.60813248 3.05068234,0.53620916 6.0651855,0.542255041 L47.8967987,0.603923024 C50.9113019,0.605736788 53.3570915,2.68007846 53.3570915,5.24534564 L53.3570915,56.6051023 Z" id="Stroke-24" stroke="#B7C243" stroke-width="0.365"></path>
</g>
<g id="Group-33" transform="translate(38.276438, 114.927536)">
<g id="Group-29">
<mask id="mask-6" fill="white">
<use xlink:href="#path-5"></use>
</mask>
<g id="Clip-28"></g>
<path d="M8.62172556,5.06061586 C8.62172556,5.22105289 8.4829209,5.35248784 8.31320065,5.35248784 L0.506069189,5.28584476 C0.33634894,5.2833765 0.196282415,5.15440981 0.196282415,4.99150452 L0.196282415,0.56837903 C0.196282415,0.412261459 0.33634894,0.285763032 0.506069189,0.280826508 L8.31320065,0.359193827 C8.4829209,0.363513285 8.62172556,0.488160516 8.62172556,0.646746349 L8.62172556,5.06061586 Z" id="Fill-27" fill="#4F4F4F" mask="url(#mask-6)"></path>
</g>
<g id="Group-32">
<mask id="mask-8" fill="white">
<use xlink:href="#path-7"></use>
</mask>
<g id="Clip-31"></g>
<path d="M6.60445213,-0.206716942 L5.19432293,-0.220292383 L5.19432293,0.573253848 C5.19432293,1.10578137 4.84100196,1.53711016 4.40250541,1.5290883 C3.97410374,1.5290883 3.6214137,1.09158887 3.6214137,0.563997865 L3.6214137,-0.240655544 L2.2093917,-0.255465116 C2.12043053,-0.249294461 2.03525494,-0.187587911 2.03525494,-0.0931768902 L2.03525494,2.39606533 C2.03525494,2.47813504 2.12043053,2.55650236 2.2093917,2.55650236 L6.60445213,2.59414335 C6.70035354,2.59414335 6.77606517,2.52256376 6.77606517,2.4287698 L6.77606517,-0.0505993709 C6.77606517,-0.138222672 6.70035354,-0.206716942 6.60445213,-0.206716942" id="Fill-30" fill="#FEFEFE" mask="url(#mask-8)"></path>
</g>
</g>
<polygon id="Fill-34" fill="#4F4F4F" points="42.2022263 113.967631 42.2022263 109.275362 44.1651206 109.275362 44.1651206 113.985507"></polygon>
<g id="Group-38" transform="translate(42.202226, 119.637681)">
<mask id="mask-10" fill="white">
<use xlink:href="#path-9"></use>
</mask>
<g id="Clip-37"></g>
<polygon id="Fill-36" mask="url(#mask-10)" points="0.363135436 4.99082575 1.58601855 4.98835749 1.58601855 0.284467195 0.363135436 0.271508819"></polygon>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 113 KiB