Merge bdca404dea
into f6efa87fb2
@ -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" }
|
||||
```
|
||||
|
||||
|
BIN
bundles/org.openhab.binding.linky/doc/GraphConso.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
bundles/org.openhab.binding.linky/doc/TempoGraph.png
Normal file
After Width: | Height: | Size: 59 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 102 KiB |
After Width: | Height: | Size: 177 KiB |
After Width: | Height: | Size: 106 KiB |
BIN
bundles/org.openhab.binding.linky/doc/connectlinky-index.png
Normal file
After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 106 KiB |
After Width: | Height: | Size: 102 KiB |
After Width: | Height: | Size: 169 KiB |
After Width: | Height: | Size: 99 KiB |
After Width: | Height: | Size: 121 KiB |
After Width: | Height: | Size: 98 KiB |
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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.
|
@ -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>
|
||||
|
@ -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é jusqu’au 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>
|
@ -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é jusqu’au 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 qu’Enedis 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>
|
@ -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>
|
@ -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>
|
@ -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é jusqu’au 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>
|
@ -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 qu’Enedis 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>
|
@ -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>
|
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 13 KiB |
@ -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 |
After Width: | Height: | Size: 113 KiB |