diff --git a/bundles/org.openhab.binding.linky/NOTICE b/bundles/org.openhab.binding.linky/NOTICE
index 3e2c49e0050..81b31877eab 100644
--- a/bundles/org.openhab.binding.linky/NOTICE
+++ b/bundles/org.openhab.binding.linky/NOTICE
@@ -13,8 +13,3 @@ https://www.eclipse.org/legal/epl-2.0/.
https://github.com/openhab/openhab-addons
== Third-party Content
-
-jsoup
-* License: MIT License
-* Project: https://jsoup.org/
-* Source: https://github.com/jhy/jsoup
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.linky/README.md b/bundles/org.openhab.binding.linky/README.md
index 5c563a412c1..78fd4d0d722 100644
--- a/bundles/org.openhab.binding.linky/README.md
+++ b/bundles/org.openhab.binding.linky/README.md
@@ -1,66 +1,492 @@
# 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.
+This binding enables the exploitation of electricity consumption data, mainly for the French market.
+It supports different functionalities:
-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.
+- Connection to Enedis to retrieve consumption data online.
+- Connection to the RTE API to get Tempo Red/White/Blue calendar information.
-## 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).
+## Migration
-## Discovery
+The new binding will need some tweak to you configuration to work.
+Mainly the new binding uses Bridge to access Enedis data, so you will have to add this bridge to your configuration.
+Step are:
-This binding does not provide discovery service.
+1. before updating to openHAB 5.0, in case you defined your thing with Main UI, backup username, password & internalAuthId configuration parameters as you will need to fill them again.
-## Binding Configuration
+2. add a bridge definition
+
+ Bridge linky:enedis:local "EnedisWebBridge" [
+ username="laurent@clae.net",
+ password="Mnbo32tyu123!",
+ internalAuthId="eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.u_mxXO7_d4I5bLvJzGtc2MARvpkYv0iM0EsO6a24k-tW9493_Myxwg.LVlfephhGTiCBxii8bRIkA.GOf9Ea8PTGshvkfjl62b6w.hSH97IkmBcEAz2udU-FqQg"
+ ]
+ {
+ }
-The binding has no configuration options, all configuration is done at Thing level.
+3. Move username, password & internalAuthId configuration parameter from the old linky thing to the bridge thing.
-## Thing Configuration
+4. Link your old thing to the new created bridge thing
-The thing has the following configuration parameters:
+ Thing linky:linky:linkremotemelody "Linky Melody" (linky:enedis:local)
-| Parameter | Description |
-|----------------|--------------------------------------------|
-| username | Your Enedis platform username. |
-| password | Your Enedis platform password. |
-| internalAuthId | The internal authID |
-| timezone | The timezone at the location of your linky |
+5. Start using the new channels added by the enhanced binding..
+ Old items will work out of the box without the need to relink items to channels.
-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).
+## Getting Consumption Data Online
-Instructions given for Firefox :
+The new binding version can use multiple bridges to access consumption data.
+You can use :
-1. Go to .
-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.
+- The enedis bridge: Uses the old Enedis API, based on the Enedis website, to gather data.
+- The myelectricaldata bridge: Uses the new REST Enedis API via the MyElectricalData proxy site to access the data.
+- TThe enedis-api bridge: Also uses the new REST Enedis API, but gathers data directly from the Enedis site.
-A new timezone parameter has been introduced. If you don't put a value, it will default to the timezone of your openHAB installation. This parameter can be useful if you read data from a Linky in a different timezone.
+You first need to create an Enedis account [here](https://mon-compte-client.enedis.fr/) if you don't already have one.
+Ensure that you have accepted their conditions and check that you can see graphs on the website, especially the hourly view.
+ Enedis may require your permission the first time to start collecting hourly data.
-## Channels
+The binding will not provide this information unless this step is completed.
-The information that is retrieved is available as these channels:
+Advantage and Disadvantage of Each Method.
+
+- Enedis bridge is the older method.
+- MyelectricalData and enedis bridges both use the new API format, making them less prone to changes in website architecture.
+- MyelectricalData bridge is managed by a third-party provider but is stable.
+- Enedis-api bridge directly connects to Enedis but currently requires a complex registration process with Enedis.
+ This limitation will likely be resolved in the near future, making Enedis-api Bridge the preferred method.
+
+Be warned that MyElectricalData bridge collect data using MyElectricalData service.
+This service will store your enedis information for caching purpose.
+This cache is crypted, so it may be a very big concerns, but of course, we don't know the details about this crypting, and if it can be reverse to access your data.
+
+### Bridge Configuration
+
+To retrieve data, the Linky device needs to be linked to a LinkyBridge. The available bridge options are enedis, myelectricaldata, and enedis-api.
+
+#### Enedis Web Bridge
+
+If you select enedis web bridge, you will need :
+
+- To create an Enedis account : https://mon-compte-client.enedis.fr/
+- To provide your credentials: username, password, and InternalAuthId.
+
+ | Parameter | Description |
+ |----------------|--------------------------------------------|
+ | username | Your Enedis platform username. |
+ | password | Your Enedis platform password. |
+ | internalAuthId | The internal authentication ID. |
+
+ This version is compatible with the latest Enedis Web API (deployed from June 2020). To bypass the captcha login, log in via a standard browser (e.g., Chrome, Firefox) and retrieve the user cookies (internalAuthId).
+
+ Instructions for Firefox :
+
+ 1. Go to .
+ 2. Select "Particulier" from the drop down and click "Connexion".
+ 3. Enter your Enedis account email and check "Je ne suis pas un robot".
+ 4. Click "Suivant".
+ 5. Enter your Enedis password and click "Connexion à Espace Client Enedis".
+ 6. Navigate to your Enedis account environment, then return to the previous page in your browser.
+ 7. Log out from your Enedis account.
+ 8. Repeat steps 1-2. This time, open the developer tools window (F12) and select the "Storage" tab.
+ 9. Under "Cookies", select "https://mon-compte-client.enedis.fr/". Locate the "internalAuthId" entry and copy its value into your OpenHAB configuration.
+
+ A new timezone parameter has been introduced. If you don't put a value, it will default to the timezone of your openHAB installation.
+ This parameter can be useful if you read data from a Linky in a different timezone.
+
+ ```java
+ Bridge linky:enedis:local "EnedisWebBridge" [ username="example@domaine.fr", password="******", internalAuthId="******" ]
+ ```
+
+#### Myelectricaldata Bridge
+
+If you select MyElectricalData bridge, you will need :
+
+- To create an Enedis account : https://mon-compte-client.enedis.fr/
+
+- To 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.
+
+ You will find screenshoot of the procedure in the following directory
+ [doc/myelectricaldata/](doc/myelectricaldata/index.md)
+
+ 1. Go to the connectlinky page on OpenHAB.
+ 2. Follow the first two steps of the wizard and click "Access Enedis".
+ 3. Log into your Enedis account.
+ 4. Authorize data collection for your PRM ID.
+ If you have multiple Linky meters, repeat the procedure for each one separately; selecting multiple meters at once will not work.
+
+
+ 5. You will then be redirect to a confirmation page on MyElectricalData web site
+ 6. Return to OpenHAB, go to "connectlinky/myelectricaldata-step3", select your PRM ID from the dropdown, and click "Retrieve Token".
+ 7. A confirmation page will appear if everything is correctly set up.
+
+ ```java
+ Bridge linky:my-electrical-data:local "MyElectricalBridge" [ ]
+ ```
+
+#### Enedis Bridge
+
+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.
+
+ You will find screenshoot of the procedure in the following directory
+ [doc/enedis/](doc/enedis/index.md)
+
+
+ 1. Go to the connectlinky page on OpenHAB.
+ 2. Follow the first two steps of the wizard and click "Access Enedis".
+ 3. Log into your Enedis account.
+ 4. Authorize data collection for your PRM ID.
+ 5. A confirmation page will appear if everything is correctly set up.
+
+
+ ```java
+ Bridge linky:enedis-api:localSB "EnedisBridgeSandbox" [ clientId="myClientId...", clientSecret="myClientSecret..." ]
+ ```
+
+### Thing Configuration
+
+The remote bridge works with Linky devices to retrieve consumption data from a remote API or website.
+
+You can have multiple Linky devices in your setup if you have different houses or multiple Linky meters linked to your account.
+To do this, simply create multiple Linky devices and set the prmId to match your meter ID.
+You can find the meter ID on the Enedis website or directly on your Linky meter.
+
+You can switch the Linky device from one bridge to another if you experience issues with a particular bridge.
+The data retrieved will be almost identical regardless of the bridge you use.
+Only a few contract-related items may differ between the web bridge and the API bridge.
+
+The device has the following configuration parameters:
+
+ | Parameter | Description |
+ |----------------|------------------------------------------------------------------------------------------------------|
+ | prmId | The prmId linked to the Linky Handler (optional: if blank first registered meter will be used |
+ | timezone | The timezone associated with your Point of delivery |
+ | token | Optional: Required if a token is necessary to access this Linky device (used for MyElectricalData). |
+
+
+
+
+```java
+Thing linky:linky:linkyremote "Linky Remote" (linky:enedis:local) [ ]
+Thing linky:linky:linkyremotexxxx "Linky Remote xxxx" (linky:enedis:local) [ prmId="xxxx" ]
+Thing linky:linky:linkyremotexxxx "Linky Remote xxxx" (linky:enedis-api:local) [ prmId="xxxx" ]
+Thing linky:linky:linkyremotexxxx "Linky Remote xxxx" (linky:myelectricaldata:local) [ prmId="xxxx", token="myElectricalDataToken" ]
+```
+
+### Thing Channels
+
+The retrieved information is available in multiple groups.
+
+
+
+- The daily group will give consumtion information with day granularity
+
+ | Channel ID | Item Type | Description |
+ |---------------------------------------------------|-------------------|-------------------------------------------------------------------------------|
+ | daily#yesterday | consumption | Yesterday energy usage |
+ | daily#day-2 | consumption | Day-2 energy usage |
+ | daily#day-3 | consumption | Day-3 energy usage |
+ | daily#consumption | consumption | timeseries for energy usage (up to three years will be store if available) |
+ | daily#maw-power | power | timeseries for max-power usage |
+ | daily#power | power | Yesterday's peak power usage |
+ | daily#timestamp | timestamp | Timestamp of the power peak |
+ | daily#power-2 | power | Day-2's peak power usage |
+ | daily#timestamp-2 | timestamp | Timestamp Day-2's of the power peak |
+ | daily#power-3 | power | Day-3's peak power usage |
+ | daily#timestamp-3 | timestamp | Timestamp Day-3's of the power peak |
+
+- The weekly group will give consumtion information with week granularity
+
+ | Channel ID | Item Type | Description |
+ |---------------------------------------------------|-------------------|-------------------------------------------------------------------------------|
+ | weekly#thisWeek | consumption | Current week energy usage |
+ | weekly#lastWeek | consumption | Last week energy usage |
+ | weekly#week-2 | consumption | Week -2 energy usage |
+ | weekly#consumption | consumption | timeseries for weeks energy usage |
+ | weekly#max-power | power | timeseries for max-power weekly usage |
+
+- The monthly group will give consumtion information with month granularity
+
+ | Channel ID | Item Type | Description |
+ |---------------------------------------------------|-------------------|-------------------------------------------------------------------------------|
+ | monthly#thisMonth | consumption | Current month energy usage |
+ | monthly#lastMonth | consumption | Last month energy usage |
+ | monthly#month-2 | consumption | Month-2 energy usage |
+ | monthly#consumption | consumption | timeseries for months energy usage |
+ | monthly#max-power | power | timeseries for max-power monthly usage |
+
+- The yearly group will give consumtion information with year granularity
+
+ | Channel ID | Item Type | Description |
+ |---------------------------------------------------|-------------------|-------------------------------------------------------------------------------|
+ | yearly#thisYear | consumption | Current year energy usage |
+ | yearly#lastYear | consumption | Last year energy usage |
+ | yearly#year-2 | consumption | year-2 energy usage |
+ | yearly#consumption | consumption | timeseries for years energy usage |
+ | yearly#max-power | power | timeseries for max-power yearly usage |
+
+- The load-curve group will give you access to load curve data with granularity as low as 30mn
+
+ | Channel ID | Item Type | Description |
+ |---------------------------------------------------|-------------------|-------------------------------------------------------------------------------|
+ | load-curve#power | power | The load curve data |
+
+
+- You will also find some Information as properties on the linky things
+
+ | Channel ID | Description |
+ |---------------------------------------------------|-------------------------------------------------------------------------------|
+ | identitiy | The full name of the contract older |
+ | customerId | The internal Enedis customer ID |
+ | contractSubscribedPower | The subscribed max Power |
+ | contractLastActivationdate | The contract activation date |
+ | contractDistributionTariff | The current applied tarif |
+ | contractOffpeakHours | The OffPeakHour link to your contract |
+ | contractStatus | The current contract status |
+ | contractType | The contract type |
+ | contractLastdistributionTariffChangedate | The date of the last tariff change |
+ | contractSegment | The customer segment for this contract |
+ | usagePointId | The distribution / usage point uniq indentifier |
+ | usagePointStatus | The usage point current state |
+ | usagePointMeterType | The usage point meter type |
+ | usagePointCity | The usage point City |
+ | usagePointCountry | The usage point Country |
+ | usagePointPostalCode | The usage point Postal Code |
+ | usagePointStreet | The usage point Address Street |
+ | contactMail | The usage point Contact Mail |
+ | contactPhone | The usage point Contact Phone |
+
+### Full Example
+
+#### Remote Enedis Web Connection
+
+```java
+Bridge linky:enedis:local "EnedisWebBridge" [ username="example@domaine.fr", password="******", internalAuthId="******" ]
+
+Thing linky:linky:linkyremotexxxx "Linky Remote xxxx" (linky:enedis:local) [ prmId="xxxx" ]
+```
+
+```java
+Number:Energy ConsoHier "Conso hier [%.0f %unit%]" { channel="linky:linky:linkyremotexxxx:daily#yesterday" }
+Number:Energy ConsoSemaineEnCours "Conso cette semaine [%.0f %unit%]" { channel="linky:linky:linkyremotexxxx:weekly#thisWeek" }
+Number:Energy ConsoSemaineDerniere "Conso semaine dernière [%.0f %unit%]" { channel="linky:linky:linkyremotexxxx:weekly#lastWeek" }
+Number:Energy ConsoMoisEnCours "Conso ce mois [%.0f %unit%]" { channel="linky:linky:linkyremotexxxx:monthly#thisMonth" }
+Number:Energy ConsoMoisDernier "Conso mois dernier [%.0f %unit%]" { channel="linky:linky:linkyremotexxxx:monthly#lastMonth" }
+Number:Energy ConsoAnneeEnCours "Conso cette année [%.0f %unit%]" { channel="linky:linky:linkyremotexxxx:yearly#thisYear" }
+Number:Energy ConsoAnneeDerniere "Conso année dernière [%.0f %unit%]" { channel="linky:linky:linkyremotexxxx:yearly#lastYear" }
+```
+
+### Displaying Information Graph
+
+Using the timeseries channel, you will be able to easily create a calendar graph to display the Tempo calendar.
+To do this, you need to enable a timeseries persistence framework.
+Graph definitions will look like this:
+
+
+
+Sample code :
+
+```java
+config:
+ future: false
+ label: Linky Melody Conso Journalière
+ order: "110"
+ period: 2W
+ sidebar: true
+slots:
+ dataZoom:
+ - component: oh-chart-datazoom
+ config:
+ type: inside
+ grid:
+ - component: oh-chart-grid
+ config:
+ containLabel: true
+ includeLabels: true
+ show: true
+ legend:
+ - component: oh-chart-legend
+ config:
+ bottom: 3
+ type: scroll
+ series:
+ - component: oh-time-series
+ config:
+ areaStyle:
+ opacity: 0.2
+ gridIndex: 0
+ item: Linky_Melody_Daily_Conso_Day
+ label:
+ formatter: =v=>Number.parseFloat(v.data[1]).toFixed(2) + " Kwh"
+ position: inside
+ show: true
+ markLine:
+ data:
+ - type: average
+ markPoint:
+ data:
+ - name: min
+ type: min
+ - name: max
+ type: max
+ label:
+ backgroundColor: auto
+ name: Consumption
+ noBoundary: true
+ noItemState: true
+ service: influxdb
+ type: bar
+ xAxisIndex: 0
+ yAxisIndex: 0
+ tooltip:
+ - component: oh-chart-tooltip
+ config:
+ confine: true
+ smartFormatter: true
+ xAxis:
+ - component: oh-time-axis
+ config:
+ gridIndex: 0
+ nameLocation: center
+ splitNumber: 10
+ yAxis:
+ - component: oh-value-axis
+ config:
+ gridIndex: 0
+ max: "150"
+ min: "0"
+ name: kWh
+ nameLocation: center
+```
+
+## Getting Tempo Calendar Information
+
+### Thing Channels
+
+- The tempo group will give information about the tempo day color link to a tempo contract
+
+ | Channel ID | Item Type | Description |
+ |----------------------------------------------------------------|-------------------|-------------------------------------------------------------------------------|
+ | linky-tempo-calendar#tempo-info-today | tempo-value | The tempo color for the current day |
+ | linky-tempo-calendar#tempo-info-tomorrow | tempo-value | The tempo color for the tomorrow |
+ | linky-tempo-calendar#tempo-info-timeseries | tempo-value | A timeseries channel that will expose full tempo information for one year |
+
+### Displaying Tempo Graph
+
+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
+
+The resulting graph will look like this:
+
+
+
+
+Sample code:
+
+```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: []
+
+```
-| 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 |
## Console Commands
@@ -77,26 +503,7 @@ Start and end day are formatted yyyy-mm-dd.
Here is an example of command you can run: `openhab:linky linky:linky:local report 2020-11-15 2020-12-15`.
-## Docker specificities
+## Docker Specificities
In case you are running openHAB inside Docker, the binding will work only if you set the environment variable `CRYPTO_POLICY` to the value "unlimited" as documented [here](https://github.com/openhab/openhab-docker#java-cryptographic-strength-policy).
-## Full Example
-
-### Thing
-
-```java
-Thing linky:linky:local "Compteur Linky" [ username="example@domaine.fr", password="******", internalAuthId="******" ]
-```
-
-### Items
-
-```java
-Number:Energy ConsoHier "Conso hier [%.0f %unit%]" { channel="linky:linky:local:daily#yesterday" }
-Number:Energy ConsoSemaineEnCours "Conso cette semaine [%.0f %unit%]" { channel="linky:linky:local:weekly#thisWeek" }
-Number:Energy ConsoSemaineDerniere "Conso semaine dernière [%.0f %unit%]" { channel="linky:linky:local:weekly#lastWeek" }
-Number:Energy ConsoMoisEnCours "Conso ce mois [%.0f %unit%]" { channel="linky:linky:local:monthly#thisMonth" }
-Number:Energy ConsoMoisDernier "Conso mois dernier [%.0f %unit%]" { channel="linky:linky:local:monthly#lastMonth" }
-Number:Energy ConsoAnneeEnCours "Conso cette année [%.0f %unit%]" { channel="linky:linky:local:yearly#thisYear" }
-Number:Energy ConsoAnneeDerniere "Conso année dernière [%.0f %unit%]" { channel="linky:linky:local:yearly#lastYear" }
-```
diff --git a/bundles/org.openhab.binding.linky/doc/GraphConso.png b/bundles/org.openhab.binding.linky/doc/GraphConso.png
new file mode 100644
index 00000000000..eaa0d2075ce
Binary files /dev/null and b/bundles/org.openhab.binding.linky/doc/GraphConso.png differ
diff --git a/bundles/org.openhab.binding.linky/doc/TempoGraph.png b/bundles/org.openhab.binding.linky/doc/TempoGraph.png
new file mode 100644
index 00000000000..fc13fac0b1c
Binary files /dev/null and b/bundles/org.openhab.binding.linky/doc/TempoGraph.png differ
diff --git a/bundles/org.openhab.binding.linky/doc/connectlinky-index.png b/bundles/org.openhab.binding.linky/doc/connectlinky-index.png
new file mode 100644
index 00000000000..555b73ca0dd
Binary files /dev/null and b/bundles/org.openhab.binding.linky/doc/connectlinky-index.png differ
diff --git a/bundles/org.openhab.binding.linky/doc/enedis/connectlinky-enedis-step1.png b/bundles/org.openhab.binding.linky/doc/enedis/connectlinky-enedis-step1.png
new file mode 100644
index 00000000000..867c3af9973
Binary files /dev/null and b/bundles/org.openhab.binding.linky/doc/enedis/connectlinky-enedis-step1.png differ
diff --git a/bundles/org.openhab.binding.linky/doc/enedis/connectlinky-enedis-step2.png b/bundles/org.openhab.binding.linky/doc/enedis/connectlinky-enedis-step2.png
new file mode 100644
index 00000000000..7cf2826582c
Binary files /dev/null and b/bundles/org.openhab.binding.linky/doc/enedis/connectlinky-enedis-step2.png differ
diff --git a/bundles/org.openhab.binding.linky/doc/enedis/connectlinky-enedis-step2b.png b/bundles/org.openhab.binding.linky/doc/enedis/connectlinky-enedis-step2b.png
new file mode 100644
index 00000000000..cbac33be064
Binary files /dev/null and b/bundles/org.openhab.binding.linky/doc/enedis/connectlinky-enedis-step2b.png differ
diff --git a/bundles/org.openhab.binding.linky/doc/enedis/connectlinky-enedis-step2c.png b/bundles/org.openhab.binding.linky/doc/enedis/connectlinky-enedis-step2c.png
new file mode 100644
index 00000000000..0e9e8ddfc87
Binary files /dev/null and b/bundles/org.openhab.binding.linky/doc/enedis/connectlinky-enedis-step2c.png differ
diff --git a/bundles/org.openhab.binding.linky/doc/enedis/connectlinky-enedis-step3.png b/bundles/org.openhab.binding.linky/doc/enedis/connectlinky-enedis-step3.png
new file mode 100644
index 00000000000..8a219bd46a4
Binary files /dev/null and b/bundles/org.openhab.binding.linky/doc/enedis/connectlinky-enedis-step3.png differ
diff --git a/bundles/org.openhab.binding.linky/doc/enedis/index.md b/bundles/org.openhab.binding.linky/doc/enedis/index.md
new file mode 100644
index 00000000000..34db4308a07
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/doc/enedis/index.md
@@ -0,0 +1,20 @@
+ 2. Step 2
+
+ 
+ 
+
+ 3. Step 3
+
+ 
+
+ 4. Step 4
+
+ 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 !
+
+ 
+
+ 5. Step 5
+
+ 
+
diff --git a/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step1.png b/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step1.png
new file mode 100644
index 00000000000..163203d2a77
Binary files /dev/null and b/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step1.png differ
diff --git a/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step2.png b/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step2.png
new file mode 100644
index 00000000000..e726253cc8b
Binary files /dev/null and b/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step2.png differ
diff --git a/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step2b.png b/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step2b.png
new file mode 100644
index 00000000000..41adb8c2bef
Binary files /dev/null and b/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step2b.png differ
diff --git a/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step2c.png b/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step2c.png
new file mode 100644
index 00000000000..1c100a54846
Binary files /dev/null and b/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step2c.png differ
diff --git a/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step2d.png b/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step2d.png
new file mode 100644
index 00000000000..77c014801c2
Binary files /dev/null and b/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step2d.png differ
diff --git a/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step3.png b/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step3.png
new file mode 100644
index 00000000000..6684b6b6ca9
Binary files /dev/null and b/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step3.png differ
diff --git a/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step3b.png b/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step3b.png
new file mode 100644
index 00000000000..4d2eb813df6
Binary files /dev/null and b/bundles/org.openhab.binding.linky/doc/myelectricaldata/connectlinky-myelectricaldata-step3b.png differ
diff --git a/bundles/org.openhab.binding.linky/doc/myelectricaldata/index.md b/bundles/org.openhab.binding.linky/doc/myelectricaldata/index.md
new file mode 100644
index 00000000000..d6b3e33f5a7
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/doc/myelectricaldata/index.md
@@ -0,0 +1,29 @@
+ 2. Step 2
+
+ 
+ 
+
+ 3. Step 3
+
+ 
+
+ 4. Step 4
+
+ 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 !
+
+ 
+
+ 5. Step 5
+
+ 
+
+ 6. Step 6
+
+ 
+
+ 7. Step 7
+
+ 
+
+
diff --git a/bundles/org.openhab.binding.linky/pom.xml b/bundles/org.openhab.binding.linky/pom.xml
index 17787276d8a..2ff88b44998 100644
--- a/bundles/org.openhab.binding.linky/pom.xml
+++ b/bundles/org.openhab.binding.linky/pom.xml
@@ -17,14 +17,4 @@
javax.annotation.meta;resolution:=optional
-
-
-
- org.jsoup
- jsoup
- 1.14.3
- provided
-
-
-
diff --git a/bundles/org.openhab.binding.linky/src/main/feature/feature.xml b/bundles/org.openhab.binding.linky/src/main/feature/feature.xml
index 41dad925440..a2e4f1f26b1 100644
--- a/bundles/org.openhab.binding.linky/src/main/feature/feature.xml
+++ b/bundles/org.openhab.binding.linky/src/main/feature/feature.xml
@@ -4,7 +4,7 @@
openhab-runtime-base
- mvn:org.jsoup/jsoup/1.14.3
+ openhab-core-auth-oauth2clientmvn:org.openhab.addons.bundles/org.openhab.binding.linky/${project.version}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyBindingConstants.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyBindingConstants.java
deleted file mode 100644
index 33975c9c66f..00000000000
--- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyBindingConstants.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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 org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.core.thing.ThingTypeUID;
-
-/**
- * The {@link LinkyBindingConstants} class defines common constants, which are
- * used across the whole binding.
- *
- * @author Gaël L'hopital - Initial contribution
- */
-@NonNullByDefault
-public class LinkyBindingConstants {
-
- public static final String BINDING_ID = "linky";
-
- // List of all Thing Type UIDs
- public static final ThingTypeUID THING_TYPE_LINKY = new ThingTypeUID(BINDING_ID, "linky");
-
- // Thing properties
- public static final String PUISSANCE = "puissance";
- public static final String PRM_ID = "prmId";
- public static final String USER_ID = "av2_interne_id";
-
- // 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";
-}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java
index bce596ffd69..ba070cfa040 100644
--- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/EnedisHttpApi.java
@@ -13,42 +13,41 @@
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.BridgeRemoteBaseHandler;
+import org.openhab.binding.linky.internal.handler.BridgeRemoteEnedisWebHandler;
+import org.openhab.binding.linky.internal.handler.ThingBaseRemoteHandler;
+import org.openhab.binding.linky.internal.handler.ThingLinkyRemoteHandler;
+import org.openhab.binding.linky.internal.types.LinkyException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -59,194 +58,63 @@ 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("yyyy-MM-dd");
- 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-prm/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-energetiques?mesuresTypeCode=%s&mesuresCorrigees=false&typeDonnees=CONS&dateDebut=%s";
- 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 BridgeRemoteBaseHandler linkyBridgeHandler;
- private boolean connected = false;
-
- public EnedisHttpApi(LinkyConfiguration config, Gson gson, HttpClient httpClient) {
+ public EnedisHttpApi(BridgeRemoteBaseHandler linkyBridgeHandler, Gson gson, HttpClient httpClient) {
this.gson = gson;
this.httpClient = httpClient;
- this.config = config;
+ this.linkyBridgeHandler = linkyBridgeHandler;
+ }
+
+ public FormContentProvider getFormContent(String fieldName, String fieldValue) {
+ Fields fields = new Fields();
+ fields.put(fieldName, fieldValue);
+ return new FormContentProvider(fields);
+ }
+
+ public void addCookie(String key, String value) {
+ HttpCookie cookie = new HttpCookie(key, value);
+ cookie.setDomain(BridgeRemoteEnedisWebHandler.ENEDIS_DOMAIN);
+ cookie.setPath("/");
+ httpClient.getCookieStore().add(BridgeRemoteEnedisWebHandler.COOKIE_URI, cookie);
}
public void removeAllCookie() {
httpClient.getCookieStore().removeAll();
}
- public void initialize() throws LinkyException {
- logger.debug("Starting login process for user: {}", config.username);
-
- try {
- removeAllCookie();
-
- 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 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 lCookie = httpClient.getCookieStore().getCookies();
- Optional 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) {
+ public 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 String getContent(ThingBaseRemoteHandler handler, String url) throws LinkyException {
+ return getContent(logger, linkyBridgeHandler, url, httpClient, linkyBridgeHandler.getToken(handler));
}
- public boolean isConnected() {
- return connected;
+ public String getContent(String url) throws LinkyException {
+ return getContent(logger, linkyBridgeHandler, url, httpClient, "");
}
- 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) {
- Fields fields = new Fields();
- fields.put(fieldName, fieldValue);
- return new FormContentProvider(fields);
- }
-
- private String getContent(String url) throws LinkyException {
+ private static String getContent(Logger logger, BridgeRemoteBaseHandler linkyBridgeHandler, String url,
+ HttpClient httpClient, String token) throws LinkyException {
try {
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.TEMPORARY_REDIRECT_307
|| result.getStatus() == HttpStatus.MOVED_TEMPORARILY_302) {
@@ -256,7 +124,7 @@ public class EnedisHttpApi {
if (loc.startsWith("http://") || loc.startsWith("https://")) {
newUrl = loc;
} else {
- newUrl = URL_APPS_LINCS + loc;
+ newUrl = linkyBridgeHandler.getBaseUrl() + loc.substring(1);
}
request = httpClient.newRequest(newUrl);
@@ -273,10 +141,10 @@ public class EnedisHttpApi {
return urlParts[3];
}
}
-
- if (result.getStatus() != HttpStatus.OK_200) {
+ 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;
@@ -285,55 +153,154 @@ public class EnedisHttpApi {
}
}
- private T getData(String url, Class clazz) throws LinkyException {
- if (!connected) {
- initialize();
+ private T getData(ThingBaseRemoteHandler handler, String url, Class 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 {
- T result = Objects.requireNonNull(gson.fromJson(data, clazz));
- logger.trace("getData success {}: {}", clazz.getName(), url);
- return result;
- } 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 {
+ T result = Objects.requireNonNull(gson.fromJson(data, clazz));
+ logger.trace("getData success {}: {}", clazz.getName(), url);
+ return result;
+ } 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(ThingLinkyRemoteHandler 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];
+
+ if (prmId.isBlank()) {
+ return prms[0];
+ }
+
+ Optional 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
- + "?embed=SITALI&embed=SITCOM&embed=SITCON&embed=SYNCON";
- return getData(url, PrmDetail.class);
- }
-
- public UserInfo getUserInfo() throws LinkyException {
- return getData(USER_INFO_URL, UserInfo.class);
- }
-
- private Consumption getMeasures(String userId, String prmId, LocalDate from, LocalDate to, String request)
+ public PrmDetail getPrmDetails(ThingLinkyRemoteHandler handler, String internId, String prmId)
throws LinkyException {
- String url = String.format(MEASURE_URL, userId, prmId, request, from.format(API_DATE_FORMAT));
- ConsumptionReport report = getData(url, ConsumptionReport.class);
- return report.consumptions;
+ String prmInfoUrl = linkyBridgeHandler.getContractUrl();
+ String url = prmInfoUrl.formatted(internId) + "/" + prmId
+ + "?embed=SITALI&embed=SITCOM&embed=SITCON&embed=SYNCON";
+ return getData(handler, url, PrmDetail.class);
}
- public Consumption getEnergyData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
- return getMeasures(userId, prmId, from, to, "ENERGIE");
+ public UserInfo getUserInfo(ThingLinkyRemoteHandler handler) throws LinkyException {
+ String userInfoUrl = linkyBridgeHandler.getContactUrl();
+ return getData(handler, userInfoUrl, UserInfo.class);
}
- public Consumption getPowerData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
- return getMeasures(userId, prmId, from, to, "PMAX");
+ public String formatUrl(String apiUrl, String prmId) {
+ return apiUrl.formatted(prmId);
+ }
+
+ public Contract getContract(ThingLinkyRemoteHandler 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(ThingLinkyRemoteHandler 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(ThingLinkyRemoteHandler 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(ThingLinkyRemoteHandler handler, String prmId) throws LinkyException {
+ String contactUrl = linkyBridgeHandler.getContactUrl().formatted(prmId);
+ ResponseContact contactResponse = getData(handler, contactUrl, ResponseContact.class);
+ return contactResponse.contact;
+ }
+
+ private MeterReading getMeasures(ThingLinkyRemoteHandler 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(ThingLinkyRemoteHandler handler, String mps, String prmId, LocalDate from,
+ LocalDate to) throws LinkyException {
+ return getMeasures(handler, linkyBridgeHandler.getDailyConsumptionUrl(), mps, prmId, from, to);
+ }
+
+ public MeterReading getLoadCurveData(ThingLinkyRemoteHandler handler, String mps, String prmId, LocalDate from,
+ LocalDate to) throws LinkyException {
+ return getMeasures(handler, linkyBridgeHandler.getLoadCurveUrl(), mps, prmId, from, to);
+ }
+
+ public MeterReading getPowerData(ThingLinkyRemoteHandler handler, String mps, String prmId, LocalDate from,
+ LocalDate to) throws LinkyException {
+ return getMeasures(handler, linkyBridgeHandler.getMaxPowerUrl(), mps, prmId, from, to);
+ }
+
+ public ResponseTempo getTempoData(ThingBaseRemoteHandler 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;
}
}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java
index 5621534a75d..11977c63739 100644
--- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/api/ExpiringDayCache.java
@@ -44,11 +44,11 @@ public class ExpiringDayCache {
private final String name;
private final int beginningHour;
private final int beginningMinute;
-
private Supplier<@Nullable V> action;
private @Nullable V value;
private LocalDateTime expiresAt;
+ public boolean missingData = false;
/**
* Create a new instance.
@@ -80,6 +80,18 @@ public class ExpiringDayCache {
return Optional.ofNullable(cachedValue);
}
+ /**
+ * Returns if the value is Present or not.
+ */
+ public boolean isPresent() {
+ V cachedValue = value;
+ return (cachedValue != null && !isExpired());
+ }
+
+ public void invalidate() {
+ value = null;
+ }
+
/**
* Refreshes and returns the value in the cache.
*
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/config/LinkyBridgeApiConfiguration.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/config/LinkyBridgeApiConfiguration.java
new file mode 100644
index 00000000000..3085c75ad4a
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/config/LinkyBridgeApiConfiguration.java
@@ -0,0 +1,34 @@
+/*
+ * 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.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link LinkyBridgeApiConfiguration} 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 LinkyBridgeApiConfiguration extends LinkyBridgeConfiguration {
+ public String clientId = "";
+ public String clientSecret = "";
+ public boolean isSandbox = false;
+
+ @Override
+ public boolean seemsValid() {
+ return !clientId.isBlank() && !clientSecret.isBlank();
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/config/LinkyBridgeConfiguration.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/config/LinkyBridgeConfiguration.java
new file mode 100644
index 00000000000..7f001fbfe05
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/config/LinkyBridgeConfiguration.java
@@ -0,0 +1,30 @@
+/*
+ * 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.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.config.core.Configuration;
+
+/**
+ * The {@link LinkyBridgeConfiguration} 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 abstract class LinkyBridgeConfiguration extends Configuration {
+ public static final String INTERNAL_AUTH_ID = "internalAuthId";
+
+ public abstract boolean seemsValid();
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyConfiguration.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/config/LinkyBridgeWebConfiguration.java
similarity index 74%
rename from bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyConfiguration.java
rename to bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/config/LinkyBridgeWebConfiguration.java
index 6021cae5cea..0620c2b8d8d 100644
--- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyConfiguration.java
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/config/LinkyBridgeWebConfiguration.java
@@ -10,24 +10,26 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.linky.internal;
+package org.openhab.binding.linky.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
- * The {@link LinkyConfiguration} is the class used to match the
+ * The {@link LinkyBridgeWebConfiguration} 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 LinkyBridgeWebConfiguration extends LinkyBridgeConfiguration {
public static final String INTERNAL_AUTH_ID = "internalAuthId";
+
public String username = "";
public String password = "";
public String internalAuthId = "";
- public String timezone = "";
+ @Override
public boolean seemsValid() {
return !username.isBlank() && !password.isBlank() && !internalAuthId.isBlank();
}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/config/LinkyThingConfiguration.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/config/LinkyThingConfiguration.java
new file mode 100644
index 00000000000..c7f239418b7
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/config/LinkyThingConfiguration.java
@@ -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.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.config.core.Configuration;
+
+/**
+ * The {@link LinkyThingConfiguration} 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 abstract class LinkyThingConfiguration extends Configuration {
+ public String prmId = "";
+
+ public boolean seemsValid() {
+ return true;
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/config/LinkyThingRemoteConfiguration.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/config/LinkyThingRemoteConfiguration.java
new file mode 100644
index 00000000000..1a8295dad28
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/config/LinkyThingRemoteConfiguration.java
@@ -0,0 +1,28 @@
+/*
+ * 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.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link LinkyThingRemoteConfiguration} 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 LinkyThingRemoteConfiguration extends LinkyThingConfiguration {
+ public String token = "";
+ public String timezone = "";
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/console/LinkyCommandExtension.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/console/LinkyCommandExtension.java
index d178b3dd026..158f084cb84 100644
--- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/console/LinkyCommandExtension.java
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/console/LinkyCommandExtension.java
@@ -21,8 +21,8 @@ import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.linky.internal.LinkyBindingConstants;
-import org.openhab.binding.linky.internal.handler.LinkyHandler;
+import org.openhab.binding.linky.internal.constants.LinkyBindingConstants;
+import org.openhab.binding.linky.internal.handler.ThingLinkyRemoteHandler;
import org.openhab.core.io.console.Console;
import org.openhab.core.io.console.ConsoleCommandCompleter;
import org.openhab.core.io.console.StringsCompleter;
@@ -62,10 +62,10 @@ public class LinkyCommandExtension extends AbstractConsoleCommandExtension imple
if (args.length >= 2) {
Thing thing = getThing(args[0]);
ThingHandler thingHandler = null;
- LinkyHandler handler = null;
+ ThingLinkyRemoteHandler handler = null;
if (thing != null) {
thingHandler = thing.getHandler();
- if (thingHandler instanceof LinkyHandler linkyHandler) {
+ if (thingHandler instanceof ThingLinkyRemoteHandler linkyHandler) {
handler = linkyHandler;
}
}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java
new file mode 100644
index 00000000000..bccd2b5d1d3
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/constants/LinkyBindingConstants.java
@@ -0,0 +1,137 @@
+/*
+ * 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.constants;
+
+import java.util.Currency;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link LinkyBindingConstants} class defines common constants, which are
+ * 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 {
+
+ 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-api");
+ public static final ThingTypeUID THING_TYPE_API_MYELECTRICALDATA_BRIDGE = new ThingTypeUID(BINDING_ID,
+ "my-electrical-data");
+ public static final ThingTypeUID THING_TYPE_WEB_ENEDIS_BRIDGE = new ThingTypeUID(BINDING_ID, "enedis");
+ public static final ThingTypeUID THING_TYPE_LINKY = new ThingTypeUID(BINDING_ID, "linky");
+
+ public static final ThingTypeUID THING_TYPE_TEMPO_CALENDAR = new ThingTypeUID(BINDING_ID, "tempo-calendar");
+
+ public static final Set SUPPORTED_DEVICE_THING_TYPES_UIDS = Set.of(THING_TYPE_API_ENEDIS_BRIDGE,
+ THING_TYPE_WEB_ENEDIS_BRIDGE, THING_TYPE_API_MYELECTRICALDATA_BRIDGE, THING_TYPE_LINKY,
+ THING_TYPE_TEMPO_CALENDAR);
+
+ // 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 = "customerId";
+ public static final String AV2_ID = "av2_interne_id";
+
+ public static final String LINKY_REMOTE_DAILY_GROUP = "daily";
+ public static final String LINKY_REMOTE_WEEKLY_GROUP = "weekly";
+ public static final String LINKY_REMOTE_MONTHLY_GROUP = "monthly";
+ public static final String LINKY_REMOTE_YEARLY_GROUP = "yearly";
+ public static final String LINKY_TEMPO_CALENDAR_GROUP = "tempo-calendar";
+ public static final String LINKY_REMOTE_LOAD_CURVE_GROUP = "load-curve";
+
+ // List of all Channel id's
+ public static final String CHANNEL_CONSUMPTION = "consumption";
+ public static final String CHANNEL_MAX_POWER = "max-power";
+ public static final String CHANNEL_POWER = "power";
+ public static final String CHANNEL_TIMESTAMP_CHANNEL = "power";
+
+ public static final String CHANNEL_DAY_MINUS_1 = "yesterday";
+ public static final String CHANNEL_DAY_MINUS_2 = "day-2";
+ public static final String CHANNEL_DAY_MINUS_3 = "day-3";
+
+ public static final String CHANNEL_PEAK_POWER_DAY_MINUS_1 = "power";
+ public static final String CHANNEL_PEAK_POWER_TS_DAY_MINUS_1 = "timestamp";
+
+ public static final String CHANNEL_PEAK_POWER_DAY_MINUS_2 = "power-2";
+ public static final String CHANNEL_PEAK_POWER_TS_DAY_MINUS_2 = "timestamp-2";
+
+ public static final String CHANNEL_PEAK_POWER_DAY_MINUS_3 = "power-3";
+ public static final String CHANNEL_PEAK_POWER_TS_DAY_MINUS_3 = "timestamp-3";
+
+ public static final String CHANNEL_WEEK_MINUS_0 = "thisWeek";
+ public static final String CHANNEL_WEEK_MINUS_1 = "lastWeek";
+ public static final String CHANNEL_WEEK_MINUS_2 = "week-2";
+
+ public static final String CHANNEL_MONTH_MINUS_0 = "thisMonth";
+ public static final String CHANNEL_MONTH_MINUS_1 = "lastMonth";
+ public static final String CHANNEL_MONTH_MINUS_2 = "month-2";
+
+ public static final String CHANNEL_YEAR_MINUS_0 = "thisYear";
+ public static final String CHANNEL_YEAR_MINUS_1 = "lastYear";
+ public static final String CHANNEL_YEAR_MINUS_2 = "year-2";
+
+ public static final String CHANNEL_TEMPO_TODAY_INFO = "tempo-info-today";
+ public static final String CHANNEL_TEMPO_TOMORROW_INFO = "tempo-info-tomorrow";
+ public static final String CHANNEL_TEMPO_TEMPO_INFO_TIME_SERIES = "tempo-info-timeseries";
+
+ public static final String PROPERTY_IDENTITY = "identity";
+
+ public static final String PROPERTY_CONTRACT_LAST_ACTIVATION_DATE = "contractLastActivationdate";
+ public static final String PROPERTY_CONTRACT_DISTRIBUTION_TARIFF = "contractDistributionTariff";
+ public static final String PROPERTY_CONTRACT_OFF_PEAK_HOURS = "contractOffpeakHours";
+ public static final String PROPERTY_CONTRACT_CONTRACT_STATUS = "contractStatus";
+ public static final String PROPERTY_CONTRACT_CONTRACT_TYPE = "contractType";
+ public static final String PROPERTY_CONTRACT_LAST_DISTRIBUTION_TARIFF_CHANGE_DATE = "contractLastdistributionTariffChangedate";
+ public static final String PROPERTY_CONTRACT_SEGMENT = "contractSegment";
+ public static final String PROPERTY_CONTRACT_SUBSCRIBED_POWER = "contractSubscribedPower";
+
+ public static final String PROPERTY_USAGEPOINT_ID = "usagePointId";
+ public static final String PROPERTY_USAGEPOINT_STATUS = "usagePointStatus";
+ public static final String PROPERTY_USAGEPOINT_METER_TYPE = "usagePointMeterType";
+
+ public static final String PROPERTY_USAGEPOINT_METER_ADDRESS_CITY = "usagePointCity";
+ public static final String PROPERTY_USAGEPOINT_METER_ADDRESS_COUNTRY = "usagePointCountry";
+ public static final String PROPERTY_USAGEPOINT_METER_ADDRESS_POSTAL_CODE = "usagePointPostalCode";
+ public static final String PROPERTY_USAGEPOINT_METER_ADDRESS_STREET = "usagePointStreet";
+
+ public static final String PROPERTY_CONTACT_MAIL = "contactMail";
+ public static final String PROPERTY_CONTACT_PHONE = "contactPhone";
+
+ // Authorization related Servlet and resources aliases.
+ public static final String LINKY_ALIAS = "/connectlinky";
+ public static final String LINKY_IMG_ALIAS = "/img";
+
+ // List of all Channel ids
+
+ public static final Currency CURRENCY_EUR = Currency.getInstance("EUR");
+
+ public static final String ERROR_OFFLINE_SERIAL_NOT_FOUND = "@text/linky.thingstate.serial_notfound";
+ public static final String ERROR_OFFLINE_SERIAL_INUSE = "@text/linky.thingstate.serial_inuse";
+ public static final String ERROR_OFFLINE_SERIAL_UNSUPPORTED = "@text/linky.thingstate.serial_unsupported";
+ public static final String ERROR_OFFLINE_SERIAL_LISTENERS = "@text/linky.thingstate.serial_listeners";
+ public static final String ERROR_OFFLINE_CONTROLLER_OFFLINE = "@text/linky.thingstate.controller_offline";
+ public static final String ERROR_UNKNOWN_RETRY_IN_PROGRESS = "@text/linky.thingstate.controller_unknown_retry_inprogress";
+
+ /**
+ * Smartthings scopes needed by this binding to work.
+ */
+ public static final String LINKY_SCOPES = "am_application_scope default";
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/AuthData.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/AuthData.java
index ea5f309fa76..06cc7cc4930 100644
--- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/AuthData.java
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/AuthData.java
@@ -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;
}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ConsumptionReport.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ConsumptionReport.java
index 6f38eee8663..10348728252 100644
--- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ConsumptionReport.java
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ConsumptionReport.java
@@ -40,6 +40,8 @@ public class ConsumptionReport {
}
public class ChronoData {
+ @SerializedName("heure")
+ public Aggregate heure;
@SerializedName("jour")
public Aggregate days;
@SerializedName("semaine")
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Contact.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Contact.java
new file mode 100644
index 00000000000..6b1b5c09374
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Contact.java
@@ -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 Contact} holds informations about the contact of a contract
+ *
+ * @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;
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Contract.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Contract.java
new file mode 100644
index 00000000000..de04bdd58fe
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Contract.java
@@ -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 Contract} holds informations about the supplier contract
+ *
+ * @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;
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Identity.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Identity.java
new file mode 100644
index 00000000000..34d85016899
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/Identity.java
@@ -0,0 +1,39 @@
+/*
+ * 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 Identity} holds the informations about the contractor identity
+ *
+ * @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 String internId;
+
+ public static Identity convertFromUserInfo(UserInfo userInfo) {
+ Identity result = new Identity();
+
+ result.firstname = userInfo.userProperties.firstName;
+ result.lastname = userInfo.userProperties.name;
+ result.title = "";
+ result.internId = userInfo.userProperties.internId;
+
+ return result;
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java
new file mode 100644
index 00000000000..31bc99aba91
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/IntervalReading.java
@@ -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 IntervalReading} holds informations for the energy consumption of a period
+ *
+ * @author Gaël L'hopital - Initial contribution
+ * @author Laurent Arnal - Rewrite addon to use official dataconect API
+ */
+
+public class IntervalReading {
+ public Double value = 0.0;
+ public LocalDateTime date;
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MetaData.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MetaData.java
new file mode 100644
index 00000000000..5f0cc1b1833
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MetaData.java
@@ -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;
+
+/**
+ * The {@link MetaData} holds authentication information
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+
+public class MetaData {
+ public Identity identity;
+ public Contact contact;
+ public Contract contract;
+ public UsagePoint usagePoint;
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java
new file mode 100644
index 00000000000..92d849e777e
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/MeterReading.java
@@ -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 MeterReading} holds informations about energy consumption
+ *
+ * @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;
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/PrmDetail.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/PrmDetail.java
index e87b7fa147e..32eaac0448f 100644
--- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/PrmDetail.java
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/PrmDetail.java
@@ -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) {
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ReadingType.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ReadingType.java
new file mode 100644
index 00000000000..0b71fb81547
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ReadingType.java
@@ -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 ReadingType} the type info associate with a meter reading
+ *
+ * @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;
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ResponseContact.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ResponseContact.java
new file mode 100644
index 00000000000..e3cdd5ca0c8
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ResponseContact.java
@@ -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 ResponseContact} holds informations about the person contact associate with a contract
+ *
+ * @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;
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ResponseContract.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ResponseContract.java
new file mode 100644
index 00000000000..7385f7d0992
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ResponseContract.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ResponseIdentity.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ResponseIdentity.java
new file mode 100644
index 00000000000..00b07e46ad6
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ResponseIdentity.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ResponseMeter.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ResponseMeter.java
new file mode 100644
index 00000000000..36c0e7ff092
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ResponseMeter.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ResponseTempo.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ResponseTempo.java
new file mode 100644
index 00000000000..a6a1a5d0bc1
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/ResponseTempo.java
@@ -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 {
+ private static final long serialVersionUID = 362498820763181264L;
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/TempoDay.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/TempoDay.java
new file mode 100644
index 00000000000..132089acf24
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/TempoDay.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/UsagePoint.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/UsagePoint.java
new file mode 100644
index 00000000000..371e2a8be80
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/dto/UsagePoint.java
@@ -0,0 +1,70 @@
+/*
+ * 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.Arrays;
+
+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();
+ String[] cityParts = prmDetail.adresse.ligne6().split(" ");
+ result.usagePointAddresses.city = String.join(" ", Arrays.copyOfRange(cityParts, 1, cityParts.length));
+ result.usagePointAddresses.postalCode = prmDetail.adresse.ligne6().split(" ")[0];
+ result.usagePointAddresses.inseeCode = "";
+ result.usagePointAddresses.country = "";
+
+ return result;
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/factory/LinkyHandlerFactory.java
similarity index 56%
rename from bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java
rename to bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/factory/LinkyHandlerFactory.java
index 36ccf55c3c6..e8a0555b21d 100644
--- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyHandlerFactory.java
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/factory/LinkyHandlerFactory.java
@@ -10,13 +10,11 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.linky.internal;
+package org.openhab.binding.linky.internal.factory;
import static java.time.temporal.ChronoField.*;
-import static org.openhab.binding.linky.internal.LinkyBindingConstants.THING_TYPE_LINKY;
+import static org.openhab.binding.linky.internal.constants.LinkyBindingConstants.*;
-import java.security.KeyManagementException;
-import java.security.NoSuchAlgorithmException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
@@ -24,19 +22,19 @@ import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
-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.handler.LinkyHandler;
+import org.openhab.binding.linky.internal.handler.BridgeRemoteEnedisHandler;
+import org.openhab.binding.linky.internal.handler.BridgeRemoteEnedisWebHandler;
+import org.openhab.binding.linky.internal.handler.BridgeRemoteMyElectricalDataHandler;
+import org.openhab.binding.linky.internal.handler.ThingLinkyRemoteHandler;
+import org.openhab.binding.linky.internal.handler.ThingTempoCalendarHandler;
import org.openhab.binding.linky.internal.utils.DoubleTypeAdapter;
+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.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
@@ -46,8 +44,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;
@@ -57,20 +54,23 @@ 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 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();
+ .appendPattern("uuuu-MM-dd['T'][' ']HH:mm").optionalStart().appendLiteral(':')
+ .appendValue(SECOND_OF_MINUTE, 2).optionalStart().appendFraction(NANO_OF_SECOND, 0, 9, true).toFormatter();
- private static final int REQUEST_BUFFER_SIZE = 8000;
- private static final int RESPONSE_BUFFER_SIZE = 200000;
+ private final HttpClientFactory httpClientFactory;
+ private final OAuthFactory oAuthFactory;
+ private final HttpService httpService;
+ private final ComponentContext componentContext;
+ private final TimeZoneProvider timeZoneProvider;
- private final Logger logger = LoggerFactory.getLogger(LinkyHandlerFactory.class);
private final Gson gson = new GsonBuilder()
.registerTypeAdapter(ZonedDateTime.class,
(JsonDeserializer) (json, type, jsonDeserializationContext) -> ZonedDateTime
@@ -88,62 +88,55 @@ public class LinkyHandlerFactory extends BaseThingHandlerFactory {
.atStartOfDay();
}
})
- .registerTypeAdapter(Double.class, new DoubleTypeAdapter()).serializeNulls().create();
+ .registerTypeAdapter(Double.class, new DoubleTypeAdapter()).create();
+
private final LocaleProvider localeProvider;
- private final HttpClient httpClient;
- private final TimeZoneProvider timeZoneProvider;
@Activate
public LinkyHandlerFactory(final @Reference LocaleProvider localeProvider,
- final @Reference HttpClientFactory httpClientFactory, final @Reference TimeZoneProvider timeZoneProvider) {
+ final @Reference HttpClientFactory httpClientFactory, final @Reference OAuthFactory oAuthFactory,
+ final @Reference HttpService httpService, ComponentContext componentContext,
+ final @Reference TimeZoneProvider timeZoneProvider) {
this.localeProvider = localeProvider;
this.timeZoneProvider = timeZoneProvider;
- 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.httpClientFactory = httpClientFactory;
+ this.oAuthFactory = oAuthFactory;
+ this.httpService = httpService;
+ 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, timeZoneProvider)
- : null;
+ if (THING_TYPE_API_ENEDIS_BRIDGE.equals(thing.getThingTypeUID())) {
+ BridgeRemoteEnedisHandler handler = new BridgeRemoteEnedisHandler((Bridge) thing, this.httpClientFactory,
+ this.oAuthFactory, this.httpService, componentContext, gson);
+ return handler;
+ } else if (THING_TYPE_WEB_ENEDIS_BRIDGE.equals(thing.getThingTypeUID())) {
+ BridgeRemoteEnedisWebHandler handler = new BridgeRemoteEnedisWebHandler((Bridge) thing,
+ this.httpClientFactory, this.oAuthFactory, this.httpService, componentContext, gson);
+ return handler;
+ } else if (THING_TYPE_API_MYELECTRICALDATA_BRIDGE.equals(thing.getThingTypeUID())) {
+ BridgeRemoteMyElectricalDataHandler handler = new BridgeRemoteMyElectricalDataHandler((Bridge) thing,
+ this.httpClientFactory, this.oAuthFactory, this.httpService, componentContext, gson);
+ return handler;
+ } else if (THING_TYPE_LINKY.equals(thing.getThingTypeUID())) {
+ ThingLinkyRemoteHandler handler = new ThingLinkyRemoteHandler(thing, localeProvider, timeZoneProvider);
+ return handler;
+ } else if (THING_TYPE_TEMPO_CALENDAR.equals(thing.getThingTypeUID())) {
+ ThingHandler handler = new ThingTempoCalendarHandler(thing);
+ return handler;
+ }
+
+ return null;
}
}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteApiHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteApiHandler.java
new file mode 100644
index 00000000000..8204fdaebb4
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteApiHandler.java
@@ -0,0 +1,210 @@
+/*
+ * 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 java.util.Objects;
+
+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.config.LinkyBridgeApiConfiguration;
+import org.openhab.binding.linky.internal.constants.LinkyBindingConstants;
+import org.openhab.binding.linky.internal.helpers.LinkyAuthServlet;
+import org.openhab.binding.linky.internal.types.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.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+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 BridgeRemoteApiHandler} is the base handler to access enedis data.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public abstract class BridgeRemoteApiHandler extends BridgeRemoteBaseHandler {
+ private final Logger logger = LoggerFactory.getLogger(BridgeRemoteApiHandler.class);
+
+ private final OAuthFactory oAuthFactory;
+
+ private @Nullable OAuthClientService oAuthService;
+
+ private static @Nullable HttpServlet servlet;
+
+ protected String tokenUrl = "";
+ protected String authorizeUrl = "";
+
+ public BridgeRemoteApiHandler(Bridge bridge, final @Reference HttpClientFactory httpClientFactory,
+ final @Reference OAuthFactory oAuthFactory, final @Reference HttpService httpService,
+ ComponentContext componentContext, Gson gson) {
+ super(bridge, httpClientFactory, oAuthFactory, httpService, componentContext, gson);
+
+ this.oAuthFactory = oAuthFactory;
+
+ updateStatus(ThingStatus.UNKNOWN);
+ }
+
+ @Override
+ public void initialize() {
+ super.initialize();
+
+ config = getConfigAs(LinkyBridgeApiConfiguration.class);
+
+ if (Objects.requireNonNull(config).seemsValid()) {
+ this.oAuthService = oAuthFactory.createOAuthClientService(LinkyBindingConstants.BINDING_ID, tokenUrl,
+ authorizeUrl, getClientId(), getClientSecret(), LinkyBindingConstants.LINKY_SCOPES, true);
+
+ registerServlet();
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/offline.config-error-mandatory-settings");
+ }
+ }
+
+ 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;
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteBaseHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteBaseHandler.java
new file mode 100644
index 00000000000..4ff0bf05956
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteBaseHandler.java
@@ -0,0 +1,196 @@
+/*
+ * 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.api.EnedisHttpApi;
+import org.openhab.binding.linky.internal.config.LinkyBridgeConfiguration;
+import org.openhab.binding.linky.internal.constants.LinkyBindingConstants;
+import org.openhab.binding.linky.internal.types.LinkyException;
+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.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 BridgeRemoteBaseHandler} is the base handler to access enedis data.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public abstract class BridgeRemoteBaseHandler extends BaseBridgeHandler {
+ private final Logger logger = LoggerFactory.getLogger(BridgeRemoteBaseHandler.class);
+
+ protected final HttpService httpService;
+ protected final BundleContext bundleContext;
+ protected final HttpClient httpClient;
+ protected final EnedisHttpApi enedisApi;
+
+ protected final Gson gson;
+
+ protected @Nullable LinkyBridgeConfiguration config;
+ protected boolean connected = false;
+
+ private static final int REQUEST_BUFFER_SIZE = 8000;
+ private static final int RESPONSE_BUFFER_SIZE = 200000;
+
+ private List registeredPrmId = new ArrayList<>();
+
+ public BridgeRemoteBaseHandler(Bridge bridge, final @Reference HttpClientFactory httpClientFactory,
+ final @Reference OAuthFactory oAuthFactory, final @Reference HttpService httpService,
+ 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.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);
+
+ this.enedisApi = new EnedisHttpApi(this, gson, this.httpClient);
+ }
+
+ public BundleContext getBundleContext() {
+ return bundleContext;
+ }
+
+ @Override
+ public synchronized void initialize() {
+ logger.debug("Initializing Linky Remote bridge handler.");
+
+ updateStatus(ThingStatus.UNKNOWN);
+
+ scheduler.submit(() -> {
+ try {
+ httpClient.start();
+
+ try {
+ connectionInit();
+ updateStatus(ThingStatus.ONLINE);
+ } catch (LinkyException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ }
+ } catch (Exception e) {
+ logger.warn("Unable to start Jetty HttpClient {}", e.getMessage());
+ 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 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(ThingBaseRemoteHandler 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();
+
+ public Gson getGson() {
+ return gson;
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisHandler.java
new file mode 100644
index 00000000000..acb014d60d4
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisHandler.java
@@ -0,0 +1,206 @@
+/*
+ * 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.Instant;
+import java.time.format.DateTimeFormatter;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.linky.internal.config.LinkyBridgeApiConfiguration;
+import org.openhab.binding.linky.internal.types.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.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 BridgeRemoteEnedisHandler} is the base handler to access enedis data.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class BridgeRemoteEnedisHandler extends BridgeRemoteApiHandler {
+ private final Logger logger = LoggerFactory.getLogger(BridgeRemoteEnedisHandler.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 BridgeRemoteEnedisHandler(Bridge bridge, final @Reference HttpClientFactory httpClientFactory,
+ final @Reference OAuthFactory oAuthFactory, final @Reference HttpService httpService,
+ ComponentContext componentContext, Gson gson) {
+ super(bridge, httpClientFactory, oAuthFactory, httpService, componentContext, gson);
+ }
+
+ @Override
+ public void initialize() {
+ tokenUrl = getBaseUrl() + BridgeRemoteEnedisHandler.ENEDIS_API_TOKEN_URL;
+ authorizeUrl = getAccountUrl() + BridgeRemoteEnedisHandler.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() {
+ LinkyBridgeApiConfiguration lcConfig = (LinkyBridgeApiConfiguration) config;
+ if (lcConfig != null) {
+ return lcConfig.clientId;
+ }
+ return "";
+ }
+
+ @Override
+ public String getClientSecret() {
+ LinkyBridgeApiConfiguration lcConfig = (LinkyBridgeApiConfiguration) config;
+ if (lcConfig != null) {
+ return lcConfig.clientSecret;
+ }
+ return "";
+ }
+
+ @Override
+ public boolean getIsSandbox() {
+ LinkyBridgeApiConfiguration lcConfig = (LinkyBridgeApiConfiguration) 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(ThingBaseRemoteHandler handler) throws LinkyException {
+ AccessTokenResponse accesToken = getAccessTokenResponse();
+
+ // Store token is about to expire, ask for a new one.
+ if (accesToken != null && accesToken.isExpired(Instant.now(), 1200)) {
+ accesToken = null;
+ }
+
+ 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;
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisWebHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisWebHandler.java
new file mode 100644
index 00000000000..0d46ec4c5ef
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteEnedisWebHandler.java
@@ -0,0 +1,358 @@
+/*
+ * 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 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.openhab.binding.linky.internal.config.LinkyBridgeWebConfiguration;
+import org.openhab.binding.linky.internal.dto.AuthData;
+import org.openhab.binding.linky.internal.dto.AuthResult;
+import org.openhab.binding.linky.internal.types.LinkyException;
+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.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+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 BridgeRemoteEnedisHandler} is the base handler to access enedis data.
+ *
+ * @author Laurent Arnal - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class BridgeRemoteEnedisWebHandler extends BridgeRemoteBaseHandler {
+ private final Logger logger = LoggerFactory.getLogger(BridgeRemoteEnedisWebHandler.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 String BASE_MYELECT_URL = "https://www.myelectricaldata.fr/";
+ private static final String TEMPO_URL = BASE_MYELECT_URL + "rte/tempo/%s/%s";
+
+ public BridgeRemoteEnedisWebHandler(Bridge bridge, final @Reference HttpClientFactory httpClientFactory,
+ final @Reference OAuthFactory oAuthFactory, final @Reference HttpService httpService,
+ ComponentContext componentContext, Gson gson) {
+ super(bridge, httpClientFactory, oAuthFactory, httpService, componentContext, gson);
+ }
+
+ @Override
+ public void initialize() {
+ super.initialize();
+
+ config = getConfigAs(LinkyBridgeWebConfiguration.class);
+ if (!Objects.requireNonNull(config).seemsValid()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/offline.config-error-mandatory-settings");
+ }
+ }
+
+ @Override
+ public String getToken(ThingBaseRemoteHandler 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 {
+ LinkyBridgeWebConfiguration lcConfig = (LinkyBridgeWebConfiguration) config;
+ if (lcConfig == null) {
+ return;
+ }
+
+ logger.debug("Starting login process for user: {}", lcConfig.username);
+
+ try {
+ ContentResponse result = null;
+ String uri = "";
+ String gotoUri = "";
+
+ // has we reconnect, remove all previous cookie to start from fresh session
+ enedisApi.removeAllCookie();
+
+ enedisApi.addCookie(LinkyBridgeWebConfiguration.INTERNAL_AUTH_ID, lcConfig.internalAuthId);
+
+ // ======================================================
+ logger.debug("Step 1a: getting authentification");
+ // ======================================================
+ uri = URL_ENEDIS_AUTHENTICATE;
+ result = httpClient.GET(uri);
+
+ if (result.getStatus() != HttpStatus.MOVED_TEMPORARILY_302) {
+ throw new LinkyException("Connection failed step 1a - auth1: %d %s", result.getStatus(),
+ result.getContentAsString());
+ }
+
+ // ======================================================
+ logger.debug("Step 1b: ...");
+ // ======================================================
+ uri = BASE_URL + result.getHeaders().get("Location");
+ result = httpClient.GET(uri);
+
+ if (result.getStatus() != HttpStatus.MOVED_TEMPORARILY_302) {
+ throw new LinkyException("Connection failed step 1b - auth1: %d %s", result.getStatus(),
+ result.getContentAsString());
+ }
+
+ // ======================================================
+ logger.debug("Step 1c: ...");
+ // ======================================================
+ uri = result.getHeaders().get("Location");
+
+ result = httpClient.GET(uri);
+
+ if (result.getStatus() != HttpStatus.MOVED_TEMPORARILY_302) {
+ throw new LinkyException("Connection failed step 1c - auth1: %d %s", result.getStatus(),
+ result.getContentAsString());
+ }
+
+ // ======================================================
+ logger.debug("Step 1d: ...");
+ // ======================================================
+ uri = result.getHeaders().get("Location");
+ int idx = uri.indexOf("goto=");
+ gotoUri = uri.substring(idx + 5);
+
+ result = httpClient.GET(uri);
+
+ if (result.getStatus() != HttpStatus.MOVED_TEMPORARILY_302) {
+ throw new LinkyException("Connection failed step 1d - auth1: %d %s", result.getStatus(),
+ result.getContentAsString());
+ }
+
+ // ======================================================
+ logger.debug("Step 1e: ...");
+ // ======================================================
+ uri = URL_MON_COMPTE + result.getHeaders().get("Location");
+ result = httpClient.GET(uri);
+
+ if (result.getStatus() != HttpStatus.OK_200) {
+ throw new LinkyException("Connection failed step 1e - auth1: %d %s", result.getStatus(),
+ result.getContentAsString());
+ }
+
+ // ======================================================
+ logger.debug("Step 2: auth1 - retrieve the template, thanks to cookie internalAuthId user is already set");
+ // ======================================================
+ uri = URL_MON_COMPTE + "/auth/json/authenticate?realm=/enedis&goto=" + gotoUri;
+
+ result = httpClient.POST(uri).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) {
+ if (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 3: auth2 - send the auth data");
+ // ======================================================
+ result = httpClient.POST(uri).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);
+
+ logger.debug("Add the tokenId cookie");
+ if (authResult == null) {
+ throw new LinkyException("Errors on step3 : authResult=null");
+ }
+
+ enedisApi.addCookie("enedisExt", authResult.tokenId);
+ // ======================================================
+ logger.debug("Step 4a: Confirm login");
+ // ======================================================
+ uri = authResult.successUrl;
+ result = httpClient.GET(uri);
+ if (result.getStatus() != HttpStatus.MOVED_TEMPORARILY_302) {
+ throw new LinkyException("Connection failed step 4a - auth2: %d %s", result.getStatus(),
+ result.getContentAsString());
+ }
+
+ // ======================================================
+ logger.debug("Step 4b:Confirm login");
+ // ======================================================
+ uri = result.getHeaders().get("Location");
+ result = httpClient.GET(uri);
+ if (result.getStatus() != HttpStatus.MOVED_TEMPORARILY_302) {
+ throw new LinkyException("Connection failed step 4b - auth2: %d %s", result.getStatus(),
+ result.getContentAsString());
+ }
+
+ // ======================================================
+ logger.debug("Step 4c: Confirm login");
+ // ======================================================
+ uri = BASE_URL + "/authenticate?target=https://mon-compte-client.enedis.fr%2Fhub%3FallEspace%3Dfalse";
+ // "result.getHeaders().get("Location");
+
+ result = httpClient.GET(uri);
+ if (result.getStatus() != HttpStatus.TEMPORARY_REDIRECT_307) {
+ throw new LinkyException("Connection failed step 4c - auth2: %d %s", result.getStatus(),
+ result.getContentAsString());
+ }
+
+ // ===========================================================
+ logger.debug("Step 5: retrieve user information andd cookie");
+ // ===========================================================
+ result = httpClient.GET(USER_INFO_CONTRACT_URL);
+
+ @SuppressWarnings("unchecked")
+ HashMap 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 lCookie = httpClient.getCookieStore().getCookies();
+ Optional 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;
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteMyElectricalDataHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteMyElectricalDataHandler.java
new file mode 100644
index 00000000000..f27dd895e4b
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/BridgeRemoteMyElectricalDataHandler.java
@@ -0,0 +1,219 @@
+/*
+ * 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.api.EnedisHttpApi;
+import org.openhab.binding.linky.internal.config.LinkyThingRemoteConfiguration;
+import org.openhab.binding.linky.internal.constants.LinkyBindingConstants;
+import org.openhab.binding.linky.internal.types.LinkyException;
+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.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 BridgeRemoteMyElectricalDataHandler} is the base handler to access enedis data.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class BridgeRemoteMyElectricalDataHandler extends BridgeRemoteApiHandler {
+ private final Logger logger = LoggerFactory.getLogger(BridgeRemoteMyElectricalDataHandler.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 = BridgeRemoteEnedisHandler.ENEDIS_ACCOUNT_URL_PROD
+ + BridgeRemoteEnedisHandler.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 BridgeRemoteMyElectricalDataHandler(Bridge bridge, final @Reference HttpClientFactory httpClientFactory,
+ final @Reference OAuthFactory oAuthFactory, final @Reference HttpService httpService,
+ ComponentContext componentContext, Gson gson) {
+ super(bridge, httpClientFactory, oAuthFactory, httpService, componentContext, gson);
+ }
+
+ @Override
+ public void connectionInit() {
+ connected = true;
+ }
+
+ @Override
+ public void initialize() {
+ tokenUrl = BridgeRemoteMyElectricalDataHandler.LINKY_MYELECTRICALDATA_API_TOKEN_URL;
+ authorizeUrl = BridgeRemoteMyElectricalDataHandler.LINKY_MYELECTRICALDATA_AUTHORIZE_URL;
+
+ super.initialize();
+ }
+
+ @Override
+ public String getClientId() {
+ return BridgeRemoteMyElectricalDataHandler.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(BridgeRemoteMyElectricalDataHandler.LINKY_MYELECTRICALDATA_API_TOKEN_URL,
+ getClientId(), reqCode);
+ EnedisHttpApi enedisApi = getEnedisApi();
+ if (enedisApi == null) {
+ return "";
+ }
+ String token = enedisApi.getContent(url);
+
+ logger.debug("token: {}", token);
+
+ Collection col = this.getThing().getThings();
+
+ 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);
+ ThingLinkyRemoteHandler handler = (ThingLinkyRemoteHandler) 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(ThingBaseRemoteHandler handler) throws LinkyException {
+ if (handler.getLinkyConfig() instanceof LinkyThingRemoteConfiguration config) {
+ return config.token;
+ }
+ return "";
+ }
+
+ @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;
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java
deleted file mode 100644
index 20f502a9572..00000000000
--- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java
+++ /dev/null
@@ -1,569 +0,0 @@
-/*
- * 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 static org.openhab.binding.linky.internal.LinkyBindingConstants.*;
-
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
-import java.time.temporal.ChronoUnit;
-import java.time.temporal.WeekFields;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Random;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.eclipse.jetty.client.HttpClient;
-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.binding.linky.internal.api.ExpiringDayCache;
-import org.openhab.binding.linky.internal.dto.ConsumptionReport.Aggregate;
-import org.openhab.binding.linky.internal.dto.ConsumptionReport.Consumption;
-import org.openhab.binding.linky.internal.dto.PrmDetail;
-import org.openhab.binding.linky.internal.dto.PrmInfo;
-import org.openhab.binding.linky.internal.dto.UserInfo;
-import org.openhab.core.i18n.LocaleProvider;
-import org.openhab.core.i18n.TimeZoneProvider;
-import org.openhab.core.library.types.DateTimeType;
-import org.openhab.core.library.types.QuantityType;
-import org.openhab.core.library.unit.MetricPrefix;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.thing.ThingStatus;
-import org.openhab.core.thing.ThingStatusDetail;
-import org.openhab.core.thing.binding.BaseThingHandler;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
-import org.openhab.core.types.UnDefType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.Gson;
-
-/**
- * The {@link LinkyHandler} is responsible for handling commands, which are
- * sent to one of the channels.
- *
- * @author Gaël L'hopital - Initial contribution
- */
-
-@NonNullByDefault
-public class LinkyHandler extends BaseThingHandler {
- private static final Random randomNumbers = new Random();
- private static final int REFRESH_HOUR_OF_DAY = 1;
- private static final int REFRESH_MINUTE_OF_DAY = randomNumbers.nextInt(60);
- private static final int REFRESH_INTERVAL_IN_MIN = 120;
-
- private final TimeZoneProvider timeZoneProvider;
- private final Logger logger = LoggerFactory.getLogger(LinkyHandler.class);
- private final HttpClient httpClient;
- private final Gson gson;
- private final WeekFields weekFields;
-
- private final ExpiringDayCache cachedDailyData;
- private final ExpiringDayCache cachedPowerData;
- private final ExpiringDayCache cachedMonthlyData;
- private final ExpiringDayCache cachedYearlyData;
-
- private ZoneId zoneId = ZoneId.systemDefault();
-
- private @Nullable ScheduledFuture> refreshJob;
- private @Nullable EnedisHttpApi enedisApi;
-
- private @NonNullByDefault({}) String prmId;
- private @NonNullByDefault({}) String userId;
-
- private enum Target {
- FIRST,
- LAST,
- ALL
- }
-
- public LinkyHandler(Thing thing, LocaleProvider localeProvider, Gson gson, HttpClient httpClient,
- TimeZoneProvider timeZoneProvider) {
- super(thing);
- this.gson = gson;
- this.httpClient = httpClient;
- this.weekFields = WeekFields.of(localeProvider.getLocale());
- this.timeZoneProvider = timeZoneProvider;
-
- this.cachedDailyData = new ExpiringDayCache<>("daily cache", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, () -> {
- LocalDate today = LocalDate.now();
- Consumption consumption = getConsumptionData(today.minusDays(15), today);
-
- if (consumption != null) {
- logData(consumption.aggregats.days, "Day", false, DateTimeFormatter.ISO_LOCAL_DATE, Target.ALL);
- logData(consumption.aggregats.weeks, "Week", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME, Target.ALL);
- consumption = getConsumptionAfterChecks(consumption, Target.LAST);
- }
- return consumption;
- });
-
- this.cachedPowerData = new ExpiringDayCache<>("power cache", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, () -> {
- // We request data for yesterday and the day before yesterday,
- // even if the data for the day before yesterday is not needed by the binding.
- // This is only a workaround to an API bug that will return INTERNAL_SERVER_ERROR rather
- // than the expected data with a NaN value when the data for yesterday is not yet available.
- // By requesting two days, the API is not failing and you get the expected NaN value for yesterday
- // when the data is not yet available.
- LocalDate today = LocalDate.now();
- Consumption consumption = getPowerData(today.minusDays(2), today);
- if (consumption != null) {
- logData(consumption.aggregats.days, "Day (peak)", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME,
- Target.ALL);
- consumption = getConsumptionAfterChecks(consumption, Target.LAST);
- }
- return consumption;
- });
-
- this.cachedMonthlyData = new ExpiringDayCache<>("monthly cache", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY,
- () -> {
- LocalDate today = LocalDate.now();
- Consumption consumption = getConsumptionData(today.withDayOfMonth(1).minusMonths(1), today);
- if (consumption != null) {
- logData(consumption.aggregats.months, "Month", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME,
- Target.ALL);
- consumption = getConsumptionAfterChecks(consumption, Target.LAST);
- }
- return consumption;
- });
-
- this.cachedYearlyData = new ExpiringDayCache<>("yearly cache", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY,
- () -> {
- LocalDate today = LocalDate.now();
- Consumption consumption = getConsumptionData(LocalDate.of(today.getYear() - 1, 1, 1), today);
- if (consumption != null) {
- logData(consumption.aggregats.years, "Year", true, DateTimeFormatter.ISO_LOCAL_DATE_TIME,
- Target.ALL);
- consumption = getConsumptionAfterChecks(consumption, Target.LAST);
- }
- return consumption;
- });
- }
-
- @Override
- public void initialize() {
- logger.debug("Initializing Linky handler.");
- updateStatus(ThingStatus.UNKNOWN);
-
- LinkyConfiguration config = getConfigAs(LinkyConfiguration.class);
- if (config.seemsValid()) {
-
- if (config.timezone.isBlank()) {
- zoneId = this.timeZoneProvider.getTimeZone();
- } else {
- zoneId = ZoneId.of(config.timezone);
- }
-
- enedisApi = new EnedisHttpApi(config, gson, httpClient);
- scheduler.submit(() -> {
- try {
- EnedisHttpApi api = this.enedisApi;
- api.initialize();
- updateStatus(ThingStatus.ONLINE);
-
- updateData();
-
- disconnect();
-
- final LocalDateTime now = LocalDateTime.now();
- final LocalDateTime nextDayFirstTimeUpdate = now.plusDays(1).withHour(REFRESH_HOUR_OF_DAY)
- .withMinute(REFRESH_MINUTE_OF_DAY).truncatedTo(ChronoUnit.MINUTES);
-
- refreshJob = scheduler.scheduleWithFixedDelay(this::updateData,
- ChronoUnit.MINUTES.between(now, nextDayFirstTimeUpdate) % REFRESH_INTERVAL_IN_MIN + 1,
- REFRESH_INTERVAL_IN_MIN, TimeUnit.MINUTES);
- } catch (LinkyException e) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
- }
- });
- } else {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
- "@text/offline.config-error-mandatory-settings");
- }
- }
-
- private synchronized void updateMetaData() throws LinkyException {
- EnedisHttpApi api = this.enedisApi;
- if (api != null) {
- UserInfo userInfo = api.getUserInfo();
- PrmInfo prmInfo = api.getPrmInfo(userInfo.userProperties.internId);
- PrmDetail details = api.getPrmDetails(userInfo.userProperties.internId, prmInfo.idPrm);
- updateProperties(Map.of(USER_ID, userInfo.userProperties.internId, PUISSANCE,
- details.situationContractuelleDtos[0].structureTarifaire().puissanceSouscrite().valeur() + " kVA",
- PRM_ID, prmInfo.idPrm));
-
- prmId = thing.getProperties().get(PRM_ID);
- userId = thing.getProperties().get(USER_ID);
- }
- }
-
- /**
- * Request new data and updates channels
- */
- private synchronized void updateData() {
- boolean connectedBefore = isConnected();
- try {
- updateMetaData();
- updatePowerData();
- updateDailyWeeklyData();
- updateMonthlyData();
- updateYearlyData();
- if (!connectedBefore && isConnected()) {
- disconnect();
- }
- } catch (LinkyException e) {
- logger.debug("Exception occurs during data update {}", e.getMessage(), e);
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
- }
- }
-
- private synchronized void updatePowerData() {
- if (isLinked(PEAK_POWER) || isLinked(PEAK_TIMESTAMP)) {
- cachedPowerData.getValue().ifPresentOrElse(values -> {
- Aggregate days = values.aggregats.days;
- updatekVAChannel(PEAK_POWER, days.datas.get(days.datas.size() - 1).valeur);
- updateState(PEAK_TIMESTAMP,
- new DateTimeType(days.datas.get(days.datas.size() - 1).dateDebut.atZone(zoneId)));
- }, () -> {
- updateKwhChannel(PEAK_POWER, Double.NaN);
- updateState(PEAK_TIMESTAMP, UnDefType.UNDEF);
- });
- }
- }
-
- private void setCurrentAndPrevious(Aggregate periods, String currentChannel, String previousChannel) {
- double currentValue = 0.0;
- double previousValue = 0.0;
- if (!periods.datas.isEmpty()) {
- currentValue = periods.datas.get(periods.datas.size() - 1).valeur;
- if (periods.datas.size() > 1) {
- previousValue = periods.datas.get(periods.datas.size() - 2).valeur;
- }
- }
- updateKwhChannel(currentChannel, currentValue);
- updateKwhChannel(previousChannel, previousValue);
- }
-
- /**
- * Request new dayly/weekly data and updates channels
- */
- private synchronized void updateDailyWeeklyData() {
- if (isLinked(YESTERDAY) || isLinked(LAST_WEEK) || isLinked(THIS_WEEK)) {
- cachedDailyData.getValue().ifPresentOrElse(values -> {
- Aggregate days = values.aggregats.days;
- updateKwhChannel(YESTERDAY, days.datas.get(days.datas.size() - 1).valeur);
- setCurrentAndPrevious(values.aggregats.weeks, THIS_WEEK, LAST_WEEK);
- }, () -> {
- updateKwhChannel(YESTERDAY, Double.NaN);
- if (ZonedDateTime.now().get(weekFields.dayOfWeek()) == 1) {
- updateKwhChannel(THIS_WEEK, 0.0);
- updateKwhChannel(LAST_WEEK, Double.NaN);
- } else {
- updateKwhChannel(THIS_WEEK, Double.NaN);
- }
- });
- }
- }
-
- /**
- * Request new monthly data and updates channels
- */
- private synchronized void updateMonthlyData() {
- if (isLinked(LAST_MONTH) || isLinked(THIS_MONTH)) {
- cachedMonthlyData.getValue().ifPresentOrElse(
- values -> setCurrentAndPrevious(values.aggregats.months, THIS_MONTH, LAST_MONTH), () -> {
- if (ZonedDateTime.now().getDayOfMonth() == 1) {
- updateKwhChannel(THIS_MONTH, 0.0);
- updateKwhChannel(LAST_MONTH, Double.NaN);
- } else {
- updateKwhChannel(THIS_MONTH, Double.NaN);
- }
- });
- }
- }
-
- /**
- * Request new yearly data and updates channels
- */
- private synchronized void updateYearlyData() {
- if (isLinked(LAST_YEAR) || isLinked(THIS_YEAR)) {
- cachedYearlyData.getValue().ifPresentOrElse(
- values -> setCurrentAndPrevious(values.aggregats.years, THIS_YEAR, LAST_YEAR), () -> {
- if (ZonedDateTime.now().getDayOfYear() == 1) {
- updateKwhChannel(THIS_YEAR, 0.0);
- updateKwhChannel(LAST_YEAR, Double.NaN);
- } else {
- updateKwhChannel(THIS_YEAR, Double.NaN);
- }
- });
- }
- }
-
- private void updateKwhChannel(String channelId, double consumption) {
- logger.debug("Update channel {} with {}", channelId, consumption);
- updateState(channelId,
- Double.isNaN(consumption) ? UnDefType.UNDEF : new QuantityType<>(consumption, Units.KILOWATT_HOUR));
- }
-
- private void updatekVAChannel(String channelId, double power) {
- logger.debug("Update channel {} with {}", channelId, power);
- updateState(channelId, Double.isNaN(power) ? UnDefType.UNDEF
- : new QuantityType<>(power, MetricPrefix.KILO(Units.VOLT_AMPERE)));
- }
-
- /**
- * Produce a report of all daily values between two dates
- *
- * @param startDay the start day of the report
- * @param endDay the end day of the report
- * @param separator the separator to be used betwwen the date and the value
- *
- * @return the report as a list of string
- */
- public synchronized List reportValues(LocalDate startDay, LocalDate endDay, @Nullable String separator) {
- List report = buildReport(startDay, endDay, separator);
- disconnect();
- return report;
- }
-
- private List buildReport(LocalDate startDay, LocalDate endDay, @Nullable String separator) {
- List report = new ArrayList<>();
- if (startDay.getYear() == endDay.getYear() && startDay.getMonthValue() == endDay.getMonthValue()) {
- // All values in the same month
- Consumption result = getConsumptionData(startDay, endDay.plusDays(1));
- if (result != null) {
- Aggregate days = result.aggregats.days;
- int size = (days.datas == null) ? 0 : days.datas.size();
- for (int i = 0; i < size; i++) {
- double consumption = days.datas.get(i).valeur;
- LocalDate day = days.datas.get(i).dateDebut.toLocalDate();
- // Filter data in case it contains data from dates outside the requested period
- if (day.isBefore(startDay) || day.isAfter(endDay)) {
- continue;
- }
- String line = days.datas.get(i).dateDebut.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator;
- if (consumption >= 0) {
- line += String.valueOf(consumption);
- }
- report.add(line);
- }
- } else {
- LocalDate currentDay = startDay;
- while (!currentDay.isAfter(endDay)) {
- report.add(currentDay.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator);
- currentDay = currentDay.plusDays(1);
- }
- }
- } else {
- // Concatenate the report produced for each month between the two dates
- LocalDate first = startDay;
- do {
- LocalDate last = first.withDayOfMonth(first.lengthOfMonth());
- if (last.isAfter(endDay)) {
- last = endDay;
- }
- report.addAll(buildReport(first, last, separator));
- first = last.plusDays(1);
- } while (!first.isAfter(endDay));
- }
- return report;
- }
-
- private @Nullable Consumption getConsumptionData(LocalDate from, LocalDate to) {
- logger.debug("getConsumptionData from {} to {}", from.format(DateTimeFormatter.ISO_LOCAL_DATE),
- to.format(DateTimeFormatter.ISO_LOCAL_DATE));
- EnedisHttpApi api = this.enedisApi;
- if (api != null) {
- try {
- Consumption consumption = api.getEnergyData(userId, prmId, from, to);
- updateStatus(ThingStatus.ONLINE);
- return consumption;
- } catch (LinkyException e) {
- logger.debug("Exception when getting consumption data: {}", e.getMessage(), e);
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
- }
- }
- return null;
- }
-
- private @Nullable Consumption getPowerData(LocalDate from, LocalDate to) {
- logger.debug("getPowerData from {} to {}", from.format(DateTimeFormatter.ISO_LOCAL_DATE),
- to.format(DateTimeFormatter.ISO_LOCAL_DATE));
- EnedisHttpApi api = this.enedisApi;
- if (api != null) {
- try {
- Consumption consumption = api.getPowerData(userId, prmId, from, to);
- updateStatus(ThingStatus.ONLINE);
- return consumption;
- } catch (LinkyException e) {
- logger.debug("Exception when getting power data: {}", e.getMessage(), e);
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
- }
- }
- return null;
- }
-
- private boolean isConnected() {
- EnedisHttpApi api = this.enedisApi;
- return api == null ? false : api.isConnected();
- }
-
- private void disconnect() {
- EnedisHttpApi api = this.enedisApi;
- if (api != null) {
- try {
- api.dispose();
- } catch (LinkyException e) {
- logger.debug("disconnect: {}", e.getMessage());
- }
- }
- }
-
- @Override
- public void dispose() {
- logger.debug("Disposing the Linky handler.");
- ScheduledFuture> job = this.refreshJob;
- if (job != null && !job.isCancelled()) {
- job.cancel(true);
- refreshJob = null;
- }
- disconnect();
- enedisApi = null;
- }
-
- @Override
- public synchronized void handleCommand(ChannelUID channelUID, Command command) {
- if (command instanceof RefreshType) {
- logger.debug("Refreshing channel {}", channelUID.getId());
- boolean connectedBefore = isConnected();
-
- try {
- updateMetaData();
- switch (channelUID.getId()) {
- case YESTERDAY:
- case LAST_WEEK:
- case THIS_WEEK:
- updateDailyWeeklyData();
- break;
- case LAST_MONTH:
- case THIS_MONTH:
- updateMonthlyData();
- break;
- case LAST_YEAR:
- case THIS_YEAR:
- updateYearlyData();
- break;
- case PEAK_POWER:
- case PEAK_TIMESTAMP:
- updatePowerData();
- break;
- default:
- break;
- }
- } catch (LinkyException ex) {
- logger.debug("Unable to handleCommand refresh", ex);
- }
- if (!connectedBefore && isConnected()) {
- disconnect();
- }
- } else {
- logger.debug("The Linky binding is read-only and can not handle command {}", command);
- }
- }
-
- private @Nullable Consumption getConsumptionAfterChecks(Consumption consumption, Target target) {
- try {
- checkData(consumption);
- } catch (LinkyException e) {
- logger.debug("Consumption data: {}", e.getMessage());
- return null;
- }
- if (target == Target.FIRST && !isDataFirstDayAvailable(consumption)) {
- logger.debug("Data including yesterday are not yet available");
- return null;
- }
- if (target == Target.LAST && !isDataLastDayAvailable(consumption)) {
- logger.debug("Data including yesterday are not yet available");
- return null;
- }
- return consumption;
- }
-
- private void checkData(Consumption consumption) throws LinkyException {
- if (consumption.aggregats.days != null && consumption.aggregats.days.datas.isEmpty()) {
- throw new LinkyException("Invalid consumptions data: no day period");
- }
- if (consumption.aggregats.weeks != null && consumption.aggregats.weeks.datas.isEmpty()) {
- throw new LinkyException("Invalid consumptions data: no week period");
- }
- if (consumption.aggregats.months != null && consumption.aggregats.months.datas.isEmpty()) {
- throw new LinkyException("Invalid consumptions data: no month period");
- }
- if (consumption.aggregats.years != null && consumption.aggregats.years.datas.isEmpty()) {
- throw new LinkyException("Invalid consumptions data: no year period");
- }
- }
-
- private boolean isDataFirstDayAvailable(Consumption consumption) {
- Aggregate days = consumption.aggregats.days;
- logData(days, "First day", false, DateTimeFormatter.ISO_LOCAL_DATE, Target.FIRST);
- return days.datas != null && !days.datas.isEmpty() && !days.datas.get(0).valeur.isNaN();
- }
-
- private boolean isDataLastDayAvailable(Consumption consumption) {
- Aggregate days = consumption.aggregats.days;
- logData(days, "Last day", false, DateTimeFormatter.ISO_LOCAL_DATE, Target.LAST);
- return days.datas != null && !days.datas.isEmpty() && !days.datas.get(days.datas.size() - 1).valeur.isNaN();
- }
-
- private void logData(Aggregate aggregate, String title, boolean withDateFin, DateTimeFormatter dateTimeFormatter,
- Target target) {
- if (logger.isDebugEnabled()) {
- int size = (aggregate.datas == null) ? 0 : aggregate.datas.size();
- if (target == Target.FIRST) {
- if (size > 0) {
- logData(aggregate, 0, title, withDateFin, dateTimeFormatter);
- }
- } else if (target == Target.LAST) {
- if (size > 0) {
- logData(aggregate, size - 1, title, withDateFin, dateTimeFormatter);
- }
- } else {
- for (int i = 0; i < size; i++) {
- logData(aggregate, i, title, withDateFin, dateTimeFormatter);
- }
- }
- }
- }
-
- private void logData(Aggregate aggregate, int index, String title, boolean withDateFin,
- DateTimeFormatter dateTimeFormatter) {
- if (withDateFin) {
- logger.debug("{} {} {} value {}", title, aggregate.datas.get(index).dateDebut.format(dateTimeFormatter),
- aggregate.datas.get(index).dateFin.format(dateTimeFormatter), aggregate.datas.get(index).valeur);
- } else {
- logger.debug("{} {} value {}", title, aggregate.datas.get(index).dateDebut.format(dateTimeFormatter),
- aggregate.datas.get(index).valeur);
- }
- }
-}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingBaseRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingBaseRemoteHandler.java
new file mode 100644
index 00000000000..c1273c85d51
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingBaseRemoteHandler.java
@@ -0,0 +1,53 @@
+/*
+ * 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 org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.linky.internal.config.LinkyThingRemoteConfiguration;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+
+/**
+ * The {@link ThingBaseRemoteHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ * @author Laurent Arnal - Rewrite addon to use official dataconect API
+ */
+
+@NonNullByDefault
+public class ThingBaseRemoteHandler extends BaseThingHandler {
+
+ protected LinkyThingRemoteConfiguration config;
+
+ public ThingBaseRemoteHandler(Thing thing) {
+ super(thing);
+
+ config = getConfigAs(LinkyThingRemoteConfiguration.class);
+ }
+
+ @Override
+ public synchronized void initialize() {
+ }
+
+ @Override
+ public synchronized void handleCommand(ChannelUID channelUID, Command command) {
+ }
+
+ public @Nullable LinkyThingRemoteConfiguration getLinkyConfig() {
+ return config;
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java
new file mode 100644
index 00000000000..7e1a439127d
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingLinkyRemoteHandler.java
@@ -0,0 +1,834 @@
+/*
+ * 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 static org.openhab.binding.linky.internal.constants.LinkyBindingConstants.*;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.WeekFields;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import javax.measure.Quantity;
+import javax.measure.Unit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.linky.internal.api.EnedisHttpApi;
+import org.openhab.binding.linky.internal.api.ExpiringDayCache;
+import org.openhab.binding.linky.internal.config.LinkyThingRemoteConfiguration;
+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.IntervalReading;
+import org.openhab.binding.linky.internal.dto.MetaData;
+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.UsagePoint;
+import org.openhab.binding.linky.internal.dto.UserInfo;
+import org.openhab.binding.linky.internal.types.LinkyException;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.i18n.LocaleProvider;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.MetricPrefix;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.TimeSeries;
+import org.openhab.core.types.TimeSeries.Policy;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link ThingLinkyRemoteHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Gaël L'hopital - Initial contribution
+ * @author Laurent Arnal - Rewrite addon to use official dataconect API
+ */
+
+@NonNullByDefault
+public class ThingLinkyRemoteHandler extends ThingBaseRemoteHandler {
+ private static final Random RANDOM_NUMBERS = new Random();
+ private static final int REFRESH_HOUR_OF_DAY = 1;
+ private static final int REFRESH_MINUTE_OF_DAY = RANDOM_NUMBERS.nextInt(60);
+ private static final int REFRESH_INTERVAL_IN_MIN = 120;
+
+ private final TimeZoneProvider timeZoneProvider;
+ private final Logger logger = LoggerFactory.getLogger(ThingLinkyRemoteHandler.class);
+
+ private final ExpiringDayCache metaData;
+ private final ExpiringDayCache dailyConsumption;
+ private final ExpiringDayCache dailyConsumptionMaxPower;
+ private final ExpiringDayCache loadCurveConsumption;
+
+ private ZoneId zoneId = ZoneId.systemDefault();
+ private @Nullable ScheduledFuture> refreshJob;
+ private @Nullable EnedisHttpApi enedisApi;
+ private double divider = 1.00;
+
+ public String userId = "";
+
+ private @Nullable ScheduledFuture> pollingJob = null;
+
+ private enum Target {
+ FIRST,
+ LAST,
+ ALL
+ }
+
+ public ThingLinkyRemoteHandler(Thing thing, LocaleProvider localeProvider, TimeZoneProvider timeZoneProvider) {
+ super(thing);
+
+ this.timeZoneProvider = timeZoneProvider;
+
+ this.metaData = new ExpiringDayCache<>("metaData", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY, () -> {
+ MetaData metaData = getMetaData();
+ return metaData;
+ });
+
+ this.dailyConsumption = new ExpiringDayCache<>("dailyConsumption", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY,
+ () -> {
+ LocalDate today = LocalDate.now();
+ MeterReading meterReading = getConsumptionData(today.minusDays(1095), today);
+ meterReading = getMeterReadingAfterChecks(meterReading);
+ if (meterReading != null) {
+ logData(meterReading.baseValue, "Day", DateTimeFormatter.ISO_LOCAL_DATE, Target.ALL);
+ logData(meterReading.weekValue, "Week", DateTimeFormatter.ISO_LOCAL_DATE_TIME, Target.ALL);
+ }
+ return meterReading;
+ });
+
+ // We request data for yesterday and the day before yesterday
+ // even if the data for the day before yesterday
+ // This is only a workaround to an API bug that will return INTERNAL_SERVER_ERROR rather
+ // than the expected data with a NaN value when the data for yesterday is not yet available.
+ // By requesting two days, the API is not failing and you get the expected NaN value for yesterday
+ // when the data is not yet available.
+ this.dailyConsumptionMaxPower = new ExpiringDayCache<>("dailyConsumptionMaxPower", REFRESH_HOUR_OF_DAY,
+ REFRESH_MINUTE_OF_DAY, () -> {
+ LocalDate today = LocalDate.now();
+ MeterReading meterReading = getPowerData(today.minusDays(1095), today);
+ meterReading = getMeterReadingAfterChecks(meterReading);
+ if (meterReading != null) {
+ logData(meterReading.baseValue, "Day (peak)", DateTimeFormatter.ISO_LOCAL_DATE, Target.ALL);
+ }
+ return meterReading;
+ });
+
+ // Comsuption Load Curve
+ this.loadCurveConsumption = new ExpiringDayCache<>("loadCurveConsumption", REFRESH_HOUR_OF_DAY,
+ REFRESH_MINUTE_OF_DAY, () -> {
+ LocalDate today = LocalDate.now();
+ MeterReading meterReading = getLoadCurveConsumption(today.minusDays(6), today);
+ meterReading = getMeterReadingAfterChecks(meterReading);
+ if (meterReading != null) {
+ logData(meterReading.baseValue, "Day (peak)", DateTimeFormatter.ISO_LOCAL_DATE, Target.ALL);
+ }
+ return meterReading;
+ });
+ }
+
+ @Override
+ public synchronized void initialize() {
+ logger.debug("Initializing Linky handler for {}", config.prmId);
+
+ // reread config to update timezone field
+ config = getConfigAs(LinkyThingRemoteConfiguration.class);
+
+ Bridge bridge = getBridge();
+ if (bridge == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, "@text/offline.missing-bridge");
+ return;
+ }
+
+ if (bridge.getHandler() instanceof BridgeRemoteBaseHandler bridgeHandler) {
+ enedisApi = bridgeHandler.getEnedisApi();
+ divider = bridgeHandler.getDivider();
+
+ updateStatus(ThingStatus.UNKNOWN);
+
+ if (config.seemsValid()) {
+ if (config.timezone.isBlank()) {
+ zoneId = this.timeZoneProvider.getTimeZone();
+ } else {
+ zoneId = ZoneId.of(config.timezone);
+ }
+
+ if (bridgeHandler instanceof BridgeRemoteApiHandler && config.prmId.isBlank()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/offline.config-error-mandatory-settings");
+ return;
+ }
+
+ if (!config.prmId.isBlank()) {
+ bridgeHandler.registerNewPrmId(config.prmId);
+ }
+ pollingJob = scheduler.schedule(this::pollingCode, 5, TimeUnit.SECONDS);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/offline.config-error-mandatory-settings");
+ }
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
+ }
+ }
+
+ public boolean supportNewApiFormat() throws LinkyException {
+ Bridge bridge = getBridge();
+ if (bridge == null) {
+ throw new LinkyException("Unable to get bridge in supportNewApiFormat()");
+ }
+
+ if (bridge.getHandler() instanceof BridgeRemoteBaseHandler bridgeHandler) {
+ return bridgeHandler.supportNewApiFormat();
+ } else {
+ throw new LinkyException("Unable to get bridgeHandler in supportNewApiFormat()");
+ }
+ }
+
+ private void pollingCode() {
+ try {
+ EnedisHttpApi api = this.enedisApi;
+
+ if (api != null) {
+ Bridge lcBridge = getBridge();
+ ScheduledFuture> lcPollingJob = pollingJob;
+
+ if (lcBridge == null || lcBridge.getStatus() != ThingStatus.ONLINE) {
+ return;
+ }
+
+ BridgeRemoteBaseHandler bridgeHandler = (BridgeRemoteBaseHandler) lcBridge.getHandler();
+ if (bridgeHandler == null) {
+ return;
+ }
+
+ if (!bridgeHandler.isConnected()) {
+ bridgeHandler.connectionInit();
+ }
+
+ updateData();
+
+ final LocalDateTime now = LocalDateTime.now();
+ final LocalDateTime nextDayFirstTimeUpdate = now.plusDays(1).withHour(REFRESH_HOUR_OF_DAY)
+ .withMinute(REFRESH_MINUTE_OF_DAY).truncatedTo(ChronoUnit.MINUTES);
+
+ updateStatus(ThingStatus.ONLINE);
+
+ if (lcPollingJob != null) {
+ lcPollingJob.cancel(false);
+ pollingJob = null;
+ }
+
+ refreshJob = scheduler.scheduleWithFixedDelay(this::updateData,
+ ChronoUnit.MINUTES.between(now, nextDayFirstTimeUpdate) % REFRESH_INTERVAL_IN_MIN + 1,
+ REFRESH_INTERVAL_IN_MIN, TimeUnit.MINUTES);
+ }
+ } catch (LinkyException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ }
+ }
+
+ private synchronized void updateMetaData() {
+ metaData.getValue().ifPresentOrElse(values -> {
+ String title = values.identity.title;
+ String firstName = values.identity.firstname;
+ String lastName = values.identity.lastname;
+
+ Map props = this.editProperties();
+
+ if (values.identity.internId == null) {
+ values.identity.internId = values.identity.firstname + " " + values.identity.lastname;
+ }
+ userId = values.identity.internId;
+
+ addProps(props, USER_ID, userId);
+
+ addProps(props, PROPERTY_USAGEPOINT_ID, values.usagePoint.usagePointId);
+
+ addProps(props, PROPERTY_IDENTITY, title + " " + firstName + " " + lastName);
+
+ addProps(props, PROPERTY_CONTRACT_SEGMENT, values.contract.segment);
+ addProps(props, PROPERTY_CONTRACT_CONTRACT_STATUS, values.contract.contractStatus);
+ addProps(props, PROPERTY_CONTRACT_CONTRACT_TYPE, values.contract.contractType);
+ addProps(props, PROPERTY_CONTRACT_DISTRIBUTION_TARIFF, values.contract.distributionTariff);
+ addProps(props, PROPERTY_CONTRACT_LAST_ACTIVATION_DATE, values.contract.lastActivationDate);
+ addProps(props, PROPERTY_CONTRACT_LAST_DISTRIBUTION_TARIFF_CHANGE_DATE,
+ values.contract.lastDistributionTariffChangeDate);
+ addProps(props, PROPERTY_CONTRACT_OFF_PEAK_HOURS, values.contract.offpeakHours);
+ addProps(props, PROPERTY_CONTRACT_SUBSCRIBED_POWER, values.contract.subscribedPower + " kVA");
+
+ addProps(props, PROPERTY_USAGEPOINT_STATUS, values.usagePoint.usagePointStatus);
+ addProps(props, PROPERTY_USAGEPOINT_METER_TYPE, values.usagePoint.meterType);
+ addProps(props, PROPERTY_USAGEPOINT_METER_ADDRESS_CITY, values.usagePoint.usagePointAddresses.city);
+ addProps(props, PROPERTY_USAGEPOINT_METER_ADDRESS_COUNTRY, values.usagePoint.usagePointAddresses.country);
+ addProps(props, PROPERTY_USAGEPOINT_METER_ADDRESS_POSTAL_CODE,
+ values.usagePoint.usagePointAddresses.postalCode);
+ addProps(props, PROPERTY_USAGEPOINT_METER_ADDRESS_STREET, values.usagePoint.usagePointAddresses.street);
+
+ addProps(props, PROPERTY_CONTACT_MAIL, values.contact.email);
+ addProps(props, PROPERTY_CONTACT_PHONE, values.contact.phone);
+
+ this.updateProperties(props);
+ }, () -> {
+ });
+ }
+
+ private void addProps(Map props, String key, @Nullable String value) {
+ if (value == null || value.isBlank()) {
+ return;
+ }
+
+ props.put(key, value);
+ }
+
+ private @Nullable MetaData getMetaData() {
+ try {
+ EnedisHttpApi api = this.enedisApi;
+ MetaData result = new MetaData();
+ if (api != null) {
+ if (supportNewApiFormat()) {
+ if (config.prmId.isBlank()) {
+ throw new LinkyException("@text/offline.config-error-mandatory-settings");
+ }
+ result.identity = api.getIdentity(this, config.prmId);
+ result.contact = api.getContact(this, config.prmId);
+ result.contract = api.getContract(this, config.prmId);
+ result.usagePoint = api.getUsagePoint(this, config.prmId);
+ } else {
+ UserInfo userInfo = api.getUserInfo(this);
+ PrmInfo prmInfo = api.getPrmInfo(this, userInfo.userProperties.internId, config.prmId);
+ PrmDetail details = api.getPrmDetails(this, userInfo.userProperties.internId, prmInfo.idPrm);
+
+ config.prmId = prmInfo.idPrm;
+ result.identity = Identity.convertFromUserInfo(userInfo);
+ result.contact = Contact.convertFromUserInfo(userInfo);
+ result.contract = Contract.convertFromPrmDetail(details);
+ result.usagePoint = UsagePoint.convertFromPrmDetail(prmInfo, details);
+ }
+ }
+ return result;
+ } catch (LinkyException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ }
+
+ return null;
+ }
+
+ /**
+ * Request new data and updates channels
+ */
+ private synchronized void updateData() {
+ // If one of the cache is expired, force also a metaData refresh to prevent 500 error from Enedis servers !
+ logger.debug("updateData() called");
+ logger.debug("Cache state {} {} {}", dailyConsumption.isPresent(), dailyConsumptionMaxPower.isPresent(),
+ loadCurveConsumption.isPresent());
+
+ if (!dailyConsumption.isPresent() || !dailyConsumptionMaxPower.isPresent()
+ || !loadCurveConsumption.isPresent()) {
+ logger.debug("invalidate metaData cache to force refresh");
+ metaData.invalidate();
+ }
+
+ updateMetaData();
+ // Stop there if we are not able to get Metadata
+ if (thing.getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+
+ updateEnergyData();
+ updatePowerData();
+ updateLoadCurveData();
+ }
+
+ private synchronized void updatePowerData() {
+ if (isLinkedPowerData()) {
+ dailyConsumptionMaxPower.getValue().ifPresentOrElse(values -> {
+ int dSize = values.baseValue.length;
+
+ updatekVAChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_DAY_MINUS_1,
+ values.baseValue[dSize - 1].value);
+ updateState(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_TS_DAY_MINUS_1,
+ new DateTimeType(values.baseValue[dSize - 1].date.atZone(zoneId)));
+
+ updatekVAChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_DAY_MINUS_2,
+ values.baseValue[dSize - 2].value);
+ updateState(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_TS_DAY_MINUS_2,
+ new DateTimeType(values.baseValue[dSize - 2].date.atZone(zoneId)));
+
+ updatekVAChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_DAY_MINUS_3,
+ values.baseValue[dSize - 3].value);
+ updateState(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_TS_DAY_MINUS_3,
+ new DateTimeType(values.baseValue[dSize - 3].date.atZone(zoneId)));
+
+ updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, CHANNEL_MAX_POWER, values.baseValue,
+ MetricPrefix.KILO(Units.VOLT_AMPERE));
+
+ updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_MAX_POWER, values.weekValue,
+ MetricPrefix.KILO(Units.VOLT_AMPERE));
+
+ updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MAX_POWER, values.monthValue,
+ MetricPrefix.KILO(Units.VOLT_AMPERE));
+
+ updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_MAX_POWER, values.yearValue,
+ MetricPrefix.KILO(Units.VOLT_AMPERE));
+ }, () -> {
+ updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_DAY_MINUS_1, Double.NaN);
+ updateState(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_TS_DAY_MINUS_1, UnDefType.UNDEF);
+
+ updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_DAY_MINUS_2, Double.NaN);
+ updateState(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_TS_DAY_MINUS_2, UnDefType.UNDEF);
+
+ updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_DAY_MINUS_3, Double.NaN);
+ updateState(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_TS_DAY_MINUS_3, UnDefType.UNDEF);
+ });
+ }
+ }
+
+ /**
+ * Request new daily/weekly data and updates channels
+ */
+ private synchronized void updateEnergyData() {
+ dailyConsumption.getValue().ifPresentOrElse(values -> {
+ int dSize = values.baseValue.length;
+
+ updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_1, values.baseValue[dSize - 1].value);
+ updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_2, values.baseValue[dSize - 2].value);
+ updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_3, values.baseValue[dSize - 3].value);
+
+ int idxCurrentYear = values.yearValue.length - 1;
+ int idxCurrentWeek = values.weekValue.length - 1;
+ int idxCurrentMonth = values.monthValue.length - 1;
+
+ updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_0, values.weekValue[idxCurrentWeek].value);
+ updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_1,
+ values.weekValue[idxCurrentWeek - 1].value);
+ if (idxCurrentWeek - 2 >= 0) {
+ updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_2,
+ values.weekValue[idxCurrentWeek - 2].value);
+ }
+
+ updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_0,
+ values.monthValue[idxCurrentMonth].value);
+ updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_1,
+ values.monthValue[idxCurrentMonth - 1].value);
+ if (idxCurrentMonth - 2 >= 0) {
+ updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_2,
+ values.monthValue[idxCurrentMonth - 2].value);
+ }
+
+ updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_0, values.yearValue[idxCurrentYear].value);
+ updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_1,
+ values.yearValue[idxCurrentYear - 1].value);
+ if (idxCurrentYear - 2 >= 0) {
+ updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_2,
+ values.yearValue[idxCurrentYear - 2].value);
+ }
+
+ updateTimeSeries(LINKY_REMOTE_DAILY_GROUP, CHANNEL_CONSUMPTION, values.baseValue, Units.KILOWATT_HOUR);
+ updateTimeSeries(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_CONSUMPTION, values.weekValue, Units.KILOWATT_HOUR);
+ updateTimeSeries(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_CONSUMPTION, values.monthValue, Units.KILOWATT_HOUR);
+ updateTimeSeries(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_CONSUMPTION, values.yearValue, Units.KILOWATT_HOUR);
+ }, () -> {
+ updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_1, Double.NaN);
+ updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_2, Double.NaN);
+ updateKwhChannel(LINKY_REMOTE_DAILY_GROUP, CHANNEL_DAY_MINUS_3, Double.NaN);
+
+ updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_0, Double.NaN);
+ updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_1, Double.NaN);
+ updateKwhChannel(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_WEEK_MINUS_2, Double.NaN);
+
+ updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_0, Double.NaN);
+ updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_1, Double.NaN);
+ updateKwhChannel(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MONTH_MINUS_2, Double.NaN);
+
+ updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_0, Double.NaN);
+ updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_1, Double.NaN);
+ updateKwhChannel(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_YEAR_MINUS_2, Double.NaN);
+ });
+ }
+
+ /**
+ * Request new loadCurve data and updates channels
+ */
+ private synchronized void updateLoadCurveData() {
+ if (isLinked(LINKY_REMOTE_LOAD_CURVE_GROUP, CHANNEL_POWER)) {
+ loadCurveConsumption.getValue().ifPresentOrElse(values -> {
+ updateTimeSeries(LINKY_REMOTE_LOAD_CURVE_GROUP, CHANNEL_POWER, values.baseValue,
+ MetricPrefix.KILO(Units.VOLT_AMPERE));
+ }, () -> {
+ });
+ }
+ }
+
+ private synchronized > void updateTimeSeries(String groupId, String channelId,
+ IntervalReading[] iv, Unit unit) {
+ TimeSeries timeSeries = new TimeSeries(Policy.REPLACE);
+
+ for (int i = 0; i < iv.length; i++) {
+ try {
+ if (iv[i].date == null) {
+ continue;
+ }
+
+ Instant timestamp = iv[i].date.atZone(zoneId).toInstant();
+
+ if (Double.isNaN(iv[i].value)) {
+ continue;
+ }
+ timeSeries.add(timestamp, new QuantityType<>(iv[i].value, unit));
+ } catch (Exception ex) {
+ logger.error("error occurs durring updatePowerTimeSeries for {} : {}", config.prmId, ex.getMessage(),
+ ex);
+ }
+ }
+
+ sendTimeSeries(groupId, channelId, timeSeries);
+ }
+
+ private void updateKwhChannel(String groupId, String channelId, double consumption) {
+ updateState(groupId, channelId,
+ Double.isNaN(consumption) ? UnDefType.UNDEF : new QuantityType<>(consumption, Units.KILOWATT_HOUR));
+ }
+
+ private void updatekVAChannel(String groupId, String channelId, double power) {
+ updateState(groupId, channelId, Double.isNaN(power) ? UnDefType.UNDEF
+ : new QuantityType<>(power, MetricPrefix.KILO(Units.VOLT_AMPERE)));
+ }
+
+ protected void updateState(String groupId, String channelID, State state) {
+ super.updateState(groupId + "#" + channelID, state);
+ }
+
+ protected void sendTimeSeries(String groupId, String channelID, TimeSeries timeSeries) {
+ super.sendTimeSeries(groupId + "#" + channelID, timeSeries);
+ }
+
+ /**
+ * Produce a report of all daily values between two dates
+ *
+ * @param startDay the start day of the report
+ * @param endDay the end day of the report
+ * @param separator the separator to be used betwwen the date and the value
+ *
+ * @return the report as a list of string
+ */
+
+ public synchronized List reportValues(LocalDate startDay, LocalDate endDay, @Nullable String separator) {
+ return buildReport(startDay, endDay, separator);
+ }
+
+ private List buildReport(LocalDate startDay, LocalDate endDay, @Nullable String separator) {
+ List report = new ArrayList<>();
+ if (startDay.getYear() == endDay.getYear() && startDay.getMonthValue() == endDay.getMonthValue()) {
+ // All values in the same month
+ MeterReading meterReading = getConsumptionData(startDay, endDay.plusDays(1));
+ if (meterReading != null) {
+ IntervalReading[] days = meterReading.baseValue;
+
+ int size = days.length;
+
+ for (int i = 0; i < size; i++) {
+ double consumption = days[i].value;
+ LocalDate day = days[i].date.toLocalDate();
+ // Filter data in case it contains data from dates outside the requested period
+ if (day.isBefore(startDay) || day.isAfter(endDay)) {
+ continue;
+ }
+ String line = days[i].date.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator;
+ if (consumption >= 0) {
+ line += String.valueOf(consumption);
+ }
+ report.add(line);
+ }
+ } else {
+ LocalDate currentDay = startDay;
+ while (!currentDay.isAfter(endDay)) {
+ report.add(currentDay.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator);
+ currentDay = currentDay.plusDays(1);
+ }
+ }
+ } else {
+ // Concatenate the report produced for each month between the two dates
+ LocalDate first = startDay;
+ do {
+ LocalDate last = first.withDayOfMonth(first.lengthOfMonth());
+ if (last.isAfter(endDay)) {
+ last = endDay;
+ }
+ report.addAll(buildReport(first, last, separator));
+ first = last.plusDays(1);
+ } while (!first.isAfter(endDay));
+ }
+ return report;
+ }
+
+ private @Nullable MeterReading getConsumptionData(LocalDate from, LocalDate to) {
+ logger.debug("getConsumptionData for {} from {} to {}", config.prmId,
+ from.format(DateTimeFormatter.ISO_LOCAL_DATE), to.format(DateTimeFormatter.ISO_LOCAL_DATE));
+
+ EnedisHttpApi api = this.enedisApi;
+ if (api != null) {
+ try {
+ return api.getEnergyData(this, this.userId, config.prmId, from, to);
+ } catch (LinkyException e) {
+ logger.debug("Exception when getting consumption data for {} : {}", config.prmId, e.getMessage(), e);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
+ }
+ }
+
+ return null;
+ }
+
+ private @Nullable MeterReading getLoadCurveConsumption(LocalDate from, LocalDate to) {
+ logger.debug("getLoadCurveConsumption for {} from {} to {}", config.prmId,
+ from.format(DateTimeFormatter.ISO_LOCAL_DATE), to.format(DateTimeFormatter.ISO_LOCAL_DATE));
+
+ EnedisHttpApi api = this.enedisApi;
+ if (api != null) {
+ try {
+ return api.getLoadCurveData(this, this.userId, config.prmId, from, to);
+ } catch (LinkyException e) {
+ logger.debug("Exception when getting consumption data: {}", e.getMessage(), e);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
+ }
+ }
+
+ return null;
+ }
+
+ private @Nullable MeterReading getPowerData(LocalDate from, LocalDate to) {
+ logger.debug("getPowerData for {} from {} to {}", config.prmId, from.format(DateTimeFormatter.ISO_LOCAL_DATE),
+ to.format(DateTimeFormatter.ISO_LOCAL_DATE));
+
+ EnedisHttpApi api = this.enedisApi;
+ if (api != null) {
+ try {
+ return api.getPowerData(this, this.userId, config.prmId, from, to);
+ } catch (LinkyException e) {
+ logger.debug("Exception when getting power data: {}", e.getMessage(), e);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void dispose() {
+ logger.debug("Disposing the Linky handler {}", config.prmId);
+ ScheduledFuture> job = this.refreshJob;
+ if (job != null && !job.isCancelled()) {
+ job.cancel(true);
+ refreshJob = null;
+ }
+
+ ScheduledFuture> lcPollingJob = pollingJob;
+ if (lcPollingJob != null) {
+ lcPollingJob.cancel(true);
+ pollingJob = null;
+ }
+ enedisApi = null;
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (command instanceof RefreshType) {
+ logger.debug("Refreshing channel {} {}", config.prmId, channelUID.getId());
+ updateData();
+ } else {
+ logger.debug("The Linky binding is read-only and can not handle command {}", command);
+ }
+ }
+
+ public @Nullable MeterReading getMeterReadingAfterChecks(@Nullable MeterReading meterReading) {
+ try {
+ checkData(meterReading);
+ } catch (LinkyException e) {
+ logger.debug("Consumption data: {} {}", config.prmId, e.getMessage());
+ return null;
+ }
+
+ if (!isDataLastDayAvailable(meterReading)) {
+ logger.debug("Data including yesterday are not yet available");
+ return null;
+ }
+
+ if (meterReading != null) {
+ if (meterReading.weekValue == null) {
+ LocalDate startDate = meterReading.baseValue[0].date.toLocalDate();
+ LocalDate endDate = meterReading.baseValue[meterReading.baseValue.length - 1].date.toLocalDate();
+
+ int startWeek = startDate.get(WeekFields.of(Locale.FRANCE).weekOfYear());
+ int endWeek = endDate.get(WeekFields.of(Locale.FRANCE).weekOfYear());
+
+ int yearsNum = endDate.getYear() - startDate.getYear() + 1;
+ int monthsNum = (endDate.getYear() - startDate.getYear()) * 12 + endDate.getMonthValue()
+ - startDate.getMonthValue() + 1;
+
+ int weeksNum = (endDate.getYear() - startDate.getYear()) * 52 + endWeek - startWeek + 1;
+
+ meterReading.weekValue = new IntervalReading[weeksNum];
+ meterReading.monthValue = new IntervalReading[monthsNum];
+ meterReading.yearValue = new IntervalReading[yearsNum];
+
+ for (int idx = 0; idx < weeksNum; idx++) {
+ meterReading.weekValue[idx] = new IntervalReading();
+ }
+ for (int idx = 0; idx < monthsNum; idx++) {
+ meterReading.monthValue[idx] = new IntervalReading();
+ }
+ for (int idx = 0; idx < yearsNum; idx++) {
+ meterReading.yearValue[idx] = new IntervalReading();
+ }
+
+ int size = meterReading.baseValue.length;
+ int baseYear = startDate.getYear();
+ int baseMonth = startDate.getMonthValue();
+ int baseWeek = startWeek;
+
+ for (int idx = 0; idx < size; idx++) {
+ IntervalReading ir = meterReading.baseValue[idx];
+ LocalDateTime dt = ir.date;
+ double value = ir.value;
+ value = value / divider;
+ ir.value = value;
+
+ int idxYear = dt.getYear() - baseYear;
+ int idxMonth = idxYear * 12 + dt.getMonthValue() - baseMonth;
+ int dtWeek = dt.get(WeekFields.of(Locale.FRANCE).weekOfYear());
+ int idxWeek = (idxYear * 52) + dtWeek - baseWeek;
+ int month = dt.getMonthValue();
+
+ if (idxWeek < weeksNum) {
+ meterReading.weekValue[idxWeek].value += value;
+ if (meterReading.weekValue[idxWeek].date == null) {
+ meterReading.weekValue[idxWeek].date = dt;
+ }
+ }
+ if (idxMonth < monthsNum) {
+ meterReading.monthValue[idxMonth].value += value;
+ if (meterReading.monthValue[idxMonth].date == null) {
+ meterReading.monthValue[idxMonth].date = LocalDateTime.of(dt.getYear(), month, 1, 0, 0);
+ }
+ }
+
+ if (idxYear < yearsNum) {
+ meterReading.yearValue[idxYear].value += value;
+ if (meterReading.yearValue[idxYear].date == null) {
+ meterReading.yearValue[idxYear].date = LocalDateTime.of(dt.getYear(), 1, 1, 0, 0);
+ }
+ }
+ }
+ }
+ }
+
+ return meterReading;
+ }
+
+ private void checkData(@Nullable MeterReading meterReading) throws LinkyException {
+ if (meterReading != null) {
+ if (meterReading.baseValue.length == 0) {
+ throw new LinkyException("Invalid meterReading data: no day period");
+ }
+ } else {
+ throw new LinkyException("Invalid meterReading == null");
+ }
+ }
+
+ private boolean isDataLastDayAvailable(@Nullable MeterReading meterReading) {
+ if (meterReading != null) {
+ IntervalReading[] iv = meterReading.baseValue;
+
+ logData(iv, "Last day", DateTimeFormatter.ISO_LOCAL_DATE, Target.LAST);
+ return iv.length != 0 && !iv[iv.length - 1].value.isNaN();
+ }
+
+ return false;
+ }
+
+ private void logData(IntervalReading[] ivArray, String title, DateTimeFormatter dateTimeFormatter, Target target) {
+ if (logger.isDebugEnabled()) {
+ int size = ivArray.length;
+
+ if (target == Target.FIRST) {
+ if (size > 0) {
+ logData(ivArray, 0, title, dateTimeFormatter);
+ }
+ } else if (target == Target.LAST) {
+ if (size > 0) {
+ logData(ivArray, size - 1, title, dateTimeFormatter);
+ }
+ } else {
+ for (int i = size - 3; i < size; i++) {
+ logData(ivArray, i, title, dateTimeFormatter);
+ }
+ }
+ }
+ }
+
+ private void logData(IntervalReading[] ivArray, int index, String title, DateTimeFormatter dateTimeFormatter) {
+ try {
+ IntervalReading iv = ivArray[index];
+ String date = "";
+ if (iv.date != null) {
+ date = iv.date.format(dateTimeFormatter);
+ }
+ logger.debug("({}) {} {} value {}", config.prmId, title, date, iv.value);
+ } catch (Exception e) {
+ logger.error("error during logData", e);
+ }
+ }
+
+ public void saveConfiguration(Configuration config) {
+ updateConfiguration(config);
+ }
+
+ private boolean isLinkedPowerData() {
+ return (isLinked(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_DAY_MINUS_1)
+ || isLinked(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_TS_DAY_MINUS_1)
+ || isLinked(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_DAY_MINUS_2)
+ || isLinked(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_TS_DAY_MINUS_2)
+ || isLinked(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_DAY_MINUS_3)
+ || isLinked(LINKY_REMOTE_DAILY_GROUP, CHANNEL_PEAK_POWER_TS_DAY_MINUS_3)
+ || isLinked(LINKY_REMOTE_DAILY_GROUP, CHANNEL_MAX_POWER)
+ || isLinked(LINKY_REMOTE_WEEKLY_GROUP, CHANNEL_MAX_POWER)
+ || isLinked(LINKY_REMOTE_MONTHLY_GROUP, CHANNEL_MAX_POWER)
+ || isLinked(LINKY_REMOTE_YEARLY_GROUP, CHANNEL_MAX_POWER));
+ }
+
+ private boolean isLinked(String groupName, String channelName) {
+ return isLinked(groupName + "#" + channelName);
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingTempoCalendarHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingTempoCalendarHandler.java
new file mode 100644
index 00000000000..d0b3fbed278
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/ThingTempoCalendarHandler.java
@@ -0,0 +1,285 @@
+/*
+ * 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 static org.openhab.binding.linky.internal.constants.LinkyBindingConstants.*;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.Random;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.linky.internal.api.EnedisHttpApi;
+import org.openhab.binding.linky.internal.api.ExpiringDayCache;
+import org.openhab.binding.linky.internal.dto.ResponseTempo;
+import org.openhab.binding.linky.internal.types.LinkyException;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.TimeSeries;
+import org.openhab.core.types.TimeSeries.Policy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link ThingTempoCalendarHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+
+@NonNullByDefault
+@SuppressWarnings("null")
+public class ThingTempoCalendarHandler extends ThingBaseRemoteHandler {
+
+ private static final Random RANDOM_NUMBERS = new Random();
+ private static final int REFRESH_HOUR_OF_DAY = 1;
+ private static final int REFRESH_MINUTE_OF_DAY = RANDOM_NUMBERS.nextInt(60);
+ private static final int REFRESH_INTERVAL_IN_MIN = 120;
+
+ private final Logger logger = LoggerFactory.getLogger(ThingTempoCalendarHandler.class);
+
+ private final ExpiringDayCache tempoInformation;
+
+ private @Nullable ScheduledFuture> refreshJob;
+ private @Nullable EnedisHttpApi enedisApi;
+
+ public String userId = "";
+
+ private @Nullable ScheduledFuture> pollingJob = null;
+
+ public ThingTempoCalendarHandler(Thing thing) {
+ super(thing);
+
+ // Read Tempo Information
+ this.tempoInformation = new ExpiringDayCache<>("tempoInformation", REFRESH_HOUR_OF_DAY, REFRESH_MINUTE_OF_DAY,
+ () -> {
+ LocalDate today = LocalDate.now();
+
+ ResponseTempo tempoData = getTempoData(today.minusDays(1095), today.plusDays(1));
+ return tempoData;
+ });
+ }
+
+ @Override
+ public synchronized void initialize() {
+ logger.debug("Initializing Linky tempo handler");
+
+ Bridge bridge = getBridge();
+ if (bridge == null) {
+ return;
+ }
+
+ BridgeRemoteBaseHandler bridgeHandler = (BridgeRemoteBaseHandler) bridge.getHandler();
+ if (bridgeHandler == null) {
+ return;
+ }
+ enedisApi = bridgeHandler.getEnedisApi();
+
+ updateStatus(ThingStatus.UNKNOWN);
+
+ pollingJob = scheduler.schedule(this::pollingCode, 5, TimeUnit.SECONDS);
+ }
+
+ @Override
+ protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
+ super.updateStatus(status, statusDetail, description);
+ }
+
+ public boolean supportNewApiFormat() throws LinkyException {
+ Bridge bridge = getBridge();
+ if (bridge == null) {
+ throw new LinkyException("Unable to get bridge in supportNewApiFormat()");
+ }
+
+ BridgeRemoteBaseHandler bridgeHandler = (BridgeRemoteBaseHandler) bridge.getHandler();
+ if (bridgeHandler == null) {
+ throw new LinkyException("Unable to get bridgeHandler in supportNewApiFormat()");
+ }
+
+ return bridgeHandler.supportNewApiFormat();
+ }
+
+ private void pollingCode() {
+ try {
+ EnedisHttpApi api = this.enedisApi;
+
+ if (api != null) {
+ Bridge lcBridge = getBridge();
+ ScheduledFuture> lcPollingJob = pollingJob;
+
+ if (lcBridge == null || lcBridge.getStatus() != ThingStatus.ONLINE) {
+ return;
+ }
+
+ BridgeRemoteBaseHandler bridgeHandler = (BridgeRemoteBaseHandler) lcBridge.getHandler();
+ if (bridgeHandler == null) {
+ return;
+ }
+
+ if (!bridgeHandler.isConnected()) {
+ bridgeHandler.connectionInit();
+ }
+
+ updateData();
+
+ final LocalDateTime now = LocalDateTime.now();
+ final LocalDateTime nextDayFirstTimeUpdate = now.plusDays(1).withHour(REFRESH_HOUR_OF_DAY)
+ .withMinute(REFRESH_MINUTE_OF_DAY).truncatedTo(ChronoUnit.MINUTES);
+
+ if (this.getThing().getStatusInfo().getStatusDetail() != ThingStatusDetail.COMMUNICATION_ERROR) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+
+ if (lcPollingJob != null) {
+ lcPollingJob.cancel(false);
+ pollingJob = null;
+ }
+
+ refreshJob = scheduler.scheduleWithFixedDelay(this::updateData,
+ ChronoUnit.MINUTES.between(now, nextDayFirstTimeUpdate) % REFRESH_INTERVAL_IN_MIN + 1,
+ REFRESH_INTERVAL_IN_MIN, TimeUnit.MINUTES);
+ }
+ } catch (LinkyException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ }
+ }
+
+ /**
+ * Request new data and updates channels
+ */
+ private synchronized void updateData() {
+ // If one of the cache is expired, force also a metaData refresh to prevent 500 error from Enedis servers !
+ logger.info("updateData() called");
+ logger.info("updateTempoData() called");
+ updateTempoTimeSeries();
+ }
+
+ private synchronized void updateTempoTimeSeries() {
+ tempoInformation.getValue().ifPresentOrElse(values -> {
+ TimeSeries timeSeries = new TimeSeries(Policy.REPLACE);
+
+ values.forEach((k, v) -> {
+ try {
+ SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
+ Date date = df.parse(k);
+ long epoch = date.getTime();
+ Instant timestamp = Instant.ofEpochMilli(epoch);
+
+ timeSeries.add(timestamp, new DecimalType(getTempoIdx(v)));
+ } catch (ParseException ex) {
+ }
+ });
+
+ int size = values.size();
+ Object[] tempoValues = values.values().toArray();
+
+ updateTempoChannel(LINKY_TEMPO_CALENDAR_GROUP, CHANNEL_TEMPO_TODAY_INFO,
+ getTempoIdx((String) tempoValues[size - 2]));
+ updateTempoChannel(LINKY_TEMPO_CALENDAR_GROUP, CHANNEL_TEMPO_TOMORROW_INFO,
+ getTempoIdx((String) tempoValues[size - 1]));
+
+ sendTimeSeries(LINKY_TEMPO_CALENDAR_GROUP, CHANNEL_TEMPO_TEMPO_INFO_TIME_SERIES, timeSeries);
+ updateState(LINKY_TEMPO_CALENDAR_GROUP, CHANNEL_TEMPO_TEMPO_INFO_TIME_SERIES,
+ new DecimalType(getTempoIdx((String) tempoValues[size - 2])));
+ }, () -> {
+ updateTempoChannel(LINKY_TEMPO_CALENDAR_GROUP, CHANNEL_TEMPO_TODAY_INFO, -1);
+ updateTempoChannel(LINKY_TEMPO_CALENDAR_GROUP, CHANNEL_TEMPO_TOMORROW_INFO, -1);
+ });
+ }
+
+ private int getTempoIdx(String color) {
+ int val = 0;
+ if ("BLUE".equals(color)) {
+ val = 0;
+ }
+ if ("WHITE".equals(color)) {
+ val = 1;
+ }
+ if ("RED".equals(color)) {
+ val = 2;
+ }
+
+ return val;
+ }
+
+ private void updateTempoChannel(String groupId, String channelId, int tempoValue) {
+ logger.debug("Update channel ({}) {} with {}", config.prmId, channelId, tempoValue);
+ updateState(groupId + "#" + channelId, new DecimalType(tempoValue));
+ }
+
+ protected void updateState(String groupId, String channelID, State state) {
+ super.updateState(groupId + "#" + channelID, state);
+ }
+
+ protected void sendTimeSeries(String groupId, String channelID, TimeSeries timeSeries) {
+ super.sendTimeSeries(groupId + "#" + channelID, timeSeries);
+ }
+
+ private @Nullable ResponseTempo getTempoData(LocalDate from, LocalDate to) {
+ logger.debug("getTempoData from");
+
+ EnedisHttpApi api = this.enedisApi;
+ if (api != null) {
+ try {
+ ResponseTempo result = api.getTempoData(this, from, to);
+ return result;
+ } catch (LinkyException e) {
+ logger.debug("Exception when getting tempo data: {}", e.getMessage(), e);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void dispose() {
+ logger.debug("Disposing the Linky handler {}", config.prmId);
+ ScheduledFuture> job = this.refreshJob;
+ if (job != null && !job.isCancelled()) {
+ job.cancel(true);
+ refreshJob = null;
+ }
+
+ ScheduledFuture> lcPollingJob = pollingJob;
+ if (lcPollingJob != null) {
+ lcPollingJob.cancel(true);
+ pollingJob = null;
+ }
+ enedisApi = null;
+ }
+
+ @Override
+ public synchronized void handleCommand(ChannelUID channelUID, Command command) {
+ if (command instanceof RefreshType) {
+ logger.debug("Refreshing channel {} {}", config.prmId, channelUID.getId());
+ updateData();
+ } else {
+ logger.debug("The Linky binding is read-only and can not handle command {}", command);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/helpers/LinkyAuthServlet.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/helpers/LinkyAuthServlet.java
new file mode 100644
index 00000000000..8911d66f1cc
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/helpers/LinkyAuthServlet.java
@@ -0,0 +1,272 @@
+/*
+ * 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.helpers;
+
+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.BridgeRemoteApiHandler;
+import org.openhab.binding.linky.internal.types.LinkyException;
+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 = "
Addon authorized for %s.
";
+ private static final String HTML_ERROR = "
Call to Enedis failed with error: %s
";
+
+ // 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 BridgeRemoteApiHandler apiBridgeHandler;
+
+ public LinkyAuthServlet(BridgeRemoteApiHandler 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 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 prmIds = apiBridgeHandler.getAllPrmId();
+ for (String prmId : prmIds) {
+ optionBuffer.append("");
+ }
+
+ final MultiMap<@Nullable 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 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<@Nullable 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 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();
+ }
+}
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyException.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/types/LinkyException.java
similarity index 95%
rename from bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyException.java
rename to bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/types/LinkyException.java
index 591707377fb..de3d10bb206 100644
--- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/LinkyException.java
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/types/LinkyException.java
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.linky.internal;
+package org.openhab.binding.linky.internal.types;
import org.eclipse.jdt.annotation.NonNullByDefault;
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/utils/DoubleTypeAdapter.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/utils/DoubleTypeAdapter.java
index c4ebf749971..c3c927be81c 100644
--- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/utils/DoubleTypeAdapter.java
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/utils/DoubleTypeAdapter.java
@@ -14,6 +14,10 @@ package org.openhab.binding.linky.internal.utils;
import java.io.IOException;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
@@ -28,29 +32,36 @@ import com.google.gson.stream.JsonWriter;
*
* @author Laurent Arnal - Initial contribution
*/
+
+@NonNullByDefault
public class DoubleTypeAdapter extends TypeAdapter {
@Override
- public Double read(JsonReader reader) throws IOException {
- if (reader.peek() == JsonToken.NULL) {
- reader.nextNull();
- return Double.NaN;
- }
- String stringValue = reader.nextString();
- try {
- Double value = Double.valueOf(stringValue);
- return value;
- } catch (NumberFormatException e) {
- return Double.NaN;
+ public @NonNull Double read(@Nullable JsonReader reader) throws IOException {
+ if (reader != null) {
+ if (reader.peek() == JsonToken.NULL) {
+ reader.nextNull();
+ return Double.NaN;
+ }
+ String stringValue = reader.nextString();
+ try {
+ Double value = Double.valueOf(stringValue);
+ return value;
+ } catch (NumberFormatException e) {
+ return Double.NaN;
+ }
}
+ return Double.NaN;
}
@Override
- public void write(JsonWriter writer, Double value) throws IOException {
- if (value == null) {
- writer.nullValue();
- return;
+ public void write(@Nullable JsonWriter writer, @Nullable Double value) throws IOException {
+ if (writer != null) {
+ if (value == null) {
+ writer.nullValue();
+ return;
+ }
+ writer.value(value.doubleValue());
}
- writer.value(value.doubleValue());
}
}
diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky.properties b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky.properties
index fa77fc096ce..0da0c67a429 100644
--- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky.properties
+++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/i18n/linky.properties
@@ -5,44 +5,112 @@ addon.linky.description = Retrieves your energy consumption data from Enedis web
# thing types
+thing-type.linky.enedis-api.label = Enedis API Bridge
+thing-type.linky.enedis-api.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.
+thing-type.linky.enedis.label = Enedis Web Bridge
+thing-type.linky.enedis.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.
thing-type.linky.linky.label = Linky
thing-type.linky.linky.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.
+thing-type.linky.my-electrical-data.label = MyElectricalData Bridge
+thing-type.linky.my-electrical-data.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.
+thing-type.linky.tempo-calendar.label = Tempo Calendar
+thing-type.linky.tempo-calendar.description = Provides tempo calendar
# thing types config
-thing-type.config.linky.linky.internalAuthId.label = Auth ID
-thing-type.config.linky.linky.internalAuthId.description = Authentication ID delivered after the captcha (see documentation).
-thing-type.config.linky.linky.password.label = Password
-thing-type.config.linky.linky.password.description = Your Enedis Password
-thing-type.config.linky.linky.timezone.label = Timezone
-thing-type.config.linky.linky.timezone.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
-thing-type.config.linky.linky.username.label = Username
-thing-type.config.linky.linky.username.description = Your Enedis Username
+thing-type.config.linky.enedis-api.clientId.label = clientId
+thing-type.config.linky.enedis-api.clientId.description = Your Enedis clientId
+thing-type.config.linky.enedis-api.clientSecret.label = clientSecret
+thing-type.config.linky.enedis-api.clientSecret.description = Your Enedis clientSecret
+thing-type.config.linky.enedis-api.isSandbox.label = isSandbox
+thing-type.config.linky.enedis-api.isSandbox.description = To test on the sandbox environment
+thing-type.config.linky.enedis.internalAuthId.label = Auth ID
+thing-type.config.linky.enedis.internalAuthId.description = Authentication ID delivered after the captcha (see documentation).
+thing-type.config.linky.enedis.password.label = Password
+thing-type.config.linky.enedis.password.description = Your Enedis Password
+thing-type.config.linky.enedis.username.label = Username
+thing-type.config.linky.enedis.username.description = Your Enedis Username
+thing-type.config.linky.linky.prmId.label = prmId
+thing-type.config.linky.linky.prmId.description = The Meter Id (PRM). If not provided, the binding will use the first registered meter found on your Enedis account.
+thing-type.config.linky.linky.timezone.label = timezone
+thing-type.config.linky.linky.timezone.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
+thing-type.config.linky.linky.token.label = Token
+thing-type.config.linky.linky.token.description = Your Enedis token (you will need it only if you use MyElectricalData bridge. This can be left empty, the connection page will automatically fill it http://youopenhab/connectlinky)
# channel group types
-channel-group-type.linky.daily.label = Daily consumption
-channel-group-type.linky.daily.channel.timestamp.label = Peak Timestamp
-channel-group-type.linky.daily.channel.timestamp.description = Maximum power usage timestamp
+channel-group-type.linky.daily.label = Daily Consumption
+channel-group-type.linky.daily.channel.consumption.label = Consumption
+channel-group-type.linky.daily.channel.consumption.description = The energy consumption
+channel-group-type.linky.daily.channel.day-2.label = Day -2 Consumption
+channel-group-type.linky.daily.channel.day-2.description = The energy consumption for day -2
+channel-group-type.linky.daily.channel.day-3.label = Day -3 Consumption
+channel-group-type.linky.daily.channel.day-3.description = The energy consumption for day -3
+channel-group-type.linky.daily.channel.max-power.label = Peak Value
+channel-group-type.linky.daily.channel.max-power.description = Maximum power usage value
+channel-group-type.linky.daily.channel.power.label = Peak Value Yesterday
+channel-group-type.linky.daily.channel.power.description = Maximum power usage value for Yesterday
+channel-group-type.linky.daily.channel.power-2.label = Peak Value Day-2
+channel-group-type.linky.daily.channel.power-2.description = Maximum power usage value for Day-2
+channel-group-type.linky.daily.channel.power-3.label = Peak Value Day-3
+channel-group-type.linky.daily.channel.power-3.description = Maximum power usage value for Day-3
+channel-group-type.linky.daily.channel.timestamp.label = Peak Timestamp Yesterday
+channel-group-type.linky.daily.channel.timestamp.description = Maximum power usage timestamp for Yesterday
+channel-group-type.linky.daily.channel.timestamp-2.label = Peak Timestamp Day-2
+channel-group-type.linky.daily.channel.timestamp-2.description = Maximum power usage timestamp for Day-2
+channel-group-type.linky.daily.channel.timestamp-3.label = Peak Timestamp Day-3
+channel-group-type.linky.daily.channel.timestamp-3.description = Maximum power usage timestamp for Day-3
channel-group-type.linky.daily.channel.yesterday.label = Yesterday Consumption
-channel-group-type.linky.monthly.label = Monthly consumption
+channel-group-type.linky.daily.channel.yesterday.description = The energy consumption for previous day
+channel-group-type.linky.load-curve.label = Load curve
+channel-group-type.linky.load-curve.channel.power.label = Load Curve Power
+channel-group-type.linky.monthly.label = Monthly Consumption
+channel-group-type.linky.monthly.channel.consumption.label = Consumption
+channel-group-type.linky.monthly.channel.consumption.description = The energy consumption
channel-group-type.linky.monthly.channel.lastMonth.label = Last Month Consumption
+channel-group-type.linky.monthly.channel.lastMonth.description = The energy consumption for the previous Month
+channel-group-type.linky.monthly.channel.max-power.label = Peak Value
+channel-group-type.linky.monthly.channel.max-power.description = Maximum power usage value
+channel-group-type.linky.monthly.channel.month-2.label = Month -2 Consumption
+channel-group-type.linky.monthly.channel.month-2.description = The energy consumption for the Month -2
channel-group-type.linky.monthly.channel.thisMonth.label = This Month Consumption
-channel-group-type.linky.weekly.label = Weekly consumption
+channel-group-type.linky.monthly.channel.thisMonth.description = The energy consumption for the current Month
+channel-group-type.linky.tempo-calendar.label = Tempo
+channel-group-type.linky.tempo-calendar.channel.tempo-info-timeseries.label = Tempo Day Information
+channel-group-type.linky.tempo-calendar.channel.tempo-info-today.label = Tempo Today Color
+channel-group-type.linky.tempo-calendar.channel.tempo-info-tomorrow.label = Tempo Today Color
+channel-group-type.linky.weekly.label = Weekly Consumption
+channel-group-type.linky.weekly.channel.consumption.label = Consumption
+channel-group-type.linky.weekly.channel.consumption.description = The energy consumption
channel-group-type.linky.weekly.channel.lastWeek.label = Last Week Consumption
+channel-group-type.linky.weekly.channel.lastWeek.description = The energy consumption for the previous Week
+channel-group-type.linky.weekly.channel.max-power.label = Peak Value
+channel-group-type.linky.weekly.channel.max-power.description = Maximum power usage value
channel-group-type.linky.weekly.channel.thisWeek.label = This Week Consumption
-channel-group-type.linky.yearly.label = Yearly consumption
+channel-group-type.linky.weekly.channel.thisWeek.description = The energy consumption for the current Week
+channel-group-type.linky.weekly.channel.week-2.label = Week -2 Consumption
+channel-group-type.linky.weekly.channel.week-2.description = The energy consumption for the Week -2
+channel-group-type.linky.yearly.label = Yearly Consumption
+channel-group-type.linky.yearly.channel.consumption.label = Consumption
+channel-group-type.linky.yearly.channel.consumption.description = The energy consumption
channel-group-type.linky.yearly.channel.lastYear.label = Last Year Consumption
+channel-group-type.linky.yearly.channel.lastYear.description = The energy consumption for the previous Year
+channel-group-type.linky.yearly.channel.max-power.label = Peak Value
+channel-group-type.linky.yearly.channel.max-power.description = Maximum power usage value
channel-group-type.linky.yearly.channel.thisYear.label = This Year Consumption
+channel-group-type.linky.yearly.channel.thisYear.description = The energy consumption for the current Year
+channel-group-type.linky.yearly.channel.year-2.label = Year -2 Consumption
+channel-group-type.linky.yearly.channel.year-2.description = The energy consumption for the Year -2
# channel types
channel-type.linky.consumption.label = Total Consumption
channel-type.linky.consumption.description = Consumption at given time interval
-channel-type.linky.power.label = Yesterday Peak Power
-channel-type.linky.power.description = Maximum power usage yesterday
+channel-type.linky.power.label = Power Usage
+channel-type.linky.power.description = Power usage for a period
+channel-type.linky.tempo-value.label = Tempo Color Information
+channel-type.linky.tempo-value.description = This status describes the tempo color of a day.
+channel-type.linky.tempo-value.state.option.0 = Blue
+channel-type.linky.tempo-value.state.option.1 = White
+channel-type.linky.tempo-value.state.option.2 = Red
channel-type.linky.timestamp.label = Timestamp
-
-# thing status descriptions
-
-offline.config-error-mandatory-settings = Username, password and authId are mandatory.
diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/bridge-remote-enedis-web.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/bridge-remote-enedis-web.xml
new file mode 100644
index 00000000000..7876c219e18
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/bridge-remote-enedis-web.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+ email
+ Your Enedis Username
+
+
+
+ password
+ Your Enedis Password
+
+
+
+ Authentication ID delivered after the captcha (see documentation).
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/bridge-remote-enedis.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/bridge-remote-enedis.xml
new file mode 100644
index 00000000000..fac1f49342f
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/bridge-remote-enedis.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+ Your Enedis clientId
+
+
+
+ Your Enedis clientSecret
+
+
+
+ To test on the sandbox environment
+ true
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/bridge-remote-my-electrical-data.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/bridge-remote-my-electrical-data.xml
new file mode 100644
index 00000000000..9832e02b813
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/bridge-remote-my-electrical-data.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml
new file mode 100644
index 00000000000..a79f1384e7f
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-daily.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+ The energy consumption for previous day
+
+
+
+
+ The energy consumption for day -2
+
+
+
+
+ The energy consumption for day -3
+
+
+
+
+ The energy consumption
+
+
+
+
+ Maximum power usage value
+
+
+
+
+ Maximum power usage value for Yesterday
+
+
+
+
+ Maximum power usage timestamp for Yesterday
+
+
+
+
+ Maximum power usage value for Day-2
+
+
+
+
+ Maximum power usage timestamp for Day-2
+
+
+
+
+ Maximum power usage value for Day-3
+
+
+
+
+ Maximum power usage timestamp for Day-3
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-load-curve.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-load-curve.xml
new file mode 100644
index 00000000000..4f2dcc1a80b
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-load-curve.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-monthly.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-monthly.xml
new file mode 100644
index 00000000000..3b6695f8d70
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-monthly.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+ The energy consumption for the current Month
+
+
+
+
+ The energy consumption for the previous Month
+
+
+
+
+ The energy consumption for the Month -2
+
+
+
+
+ The energy consumption
+
+
+
+
+ Maximum power usage value
+
+
+
+
diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml
new file mode 100644
index 00000000000..3b2e59ca141
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-weekly.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+ The energy consumption for the current Week
+
+
+
+ The energy consumption for the previous Week
+
+
+
+ The energy consumption for the Week -2
+
+
+
+ The energy consumption
+
+
+
+ Maximum power usage value
+
+
+
+
diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml
new file mode 100644
index 00000000000..9ad761e6ead
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-remote-yearly.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+ The energy consumption for the current Year
+
+
+
+ The energy consumption for the previous Year
+
+
+
+ The energy consumption for the Year -2
+
+
+
+ The energy consumption
+
+
+
+ Maximum power usage value
+
+
+
+
diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-tempo-calendar.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-tempo-calendar.xml
new file mode 100644
index 00000000000..98bf6b57f8e
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/group-linky-tempo-calendar.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-linky-remote.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-linky-remote.xml
new file mode 100644
index 00000000000..9b9ac3e7af4
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-linky-remote.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The Meter Id (PRM). If not provided, the binding will use the first registered meter found on your
+ Enedis account.
+
+
+
+ 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
+
+
+
+ Your Enedis token (you will need it only if you use MyElectricalData bridge. This can be left empty,
+ the connection page will automatically fill it
+ http://youopenhab/connectlinky)
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-linky-tempo-calendar.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-linky-tempo-calendar.xml
new file mode 100644
index 00000000000..97a22ad31d0
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-linky-tempo-calendar.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+ Provides tempo calendar
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml
index d743ed723b3..e37b85a6055 100644
--- a/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml
+++ b/bundles/org.openhab.binding.linky/src/main/resources/OH-INF/thing/thing-types.xml
@@ -4,99 +4,18 @@
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">
-
-
-
- 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.
-
- WebService
-
-
-
-
-
-
-
-
-
-
-
- email
- Your Enedis Username
-
-
-
- password
- Your Enedis Password
-
-
-
- Authentication ID delivered after the captcha (see documentation).
-
-
-
- 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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Maximum power usage timestamp
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ Number
+
+ This status describes the tempo color of a day.
+
+
+
+
+
+
+
+ Number:Energy
@@ -108,8 +27,8 @@
Number:Power
-
- Maximum power usage yesterday
+
+ Power usage for a period
diff --git a/bundles/org.openhab.binding.linky/src/main/resources/templates/enedis-step1.html b/bundles/org.openhab.binding.linky/src/main/resources/templates/enedis-step1.html
new file mode 100644
index 00000000000..73e9d5bba25
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/resources/templates/enedis-step1.html
@@ -0,0 +1,181 @@
+
+
+
+
+
+${pageRefresh}
+OpenHAB Linky binding for Enedis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Plugin Linky / Enedis pour Openhab
+
+
+
Ce plugin permet d'exploiter vos données de consommation éléctrique fournis par Enedis au sein d'OpenHab.
+
+
Enedis gère le réseau d’électricité jusqu’au compteur d’électricité
+ Enedis est le gestionnaire du réseau public de distribution d’électricité sur 95% du territoire français continental.
+
+
Grace à ce plugin, vous serez en mesure de :
+
+
+
Consulter les informations contractuelles liés à votre compte.
+
Créer des graphes de consommation par jour / semaine / mois / annéee.
+
Consulter la puissance maximum utilisé sur une période donnée.
+
+
+
diff --git a/bundles/org.openhab.binding.linky/src/main/resources/templates/enedis-step2.html b/bundles/org.openhab.binding.linky/src/main/resources/templates/enedis-step2.html
new file mode 100644
index 00000000000..92737e5b6d4
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/resources/templates/enedis-step2.html
@@ -0,0 +1,174 @@
+
+
+
+
+
+${pageRefresh}
+OpenHAB Linky binding for Enedis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Plugin Linky / Enedis pour Openhab
+
+
+
+
+
+
+
Pour utiliser ce plugin, vous devez donner votre accord pour qu'Enedis transmette vos données.
+
+
+ Enedis est le gestionnaire du réseau public de distribution d’électricité sur 95% du territoire français continental.
+ Enedis gère le réseau d’électricité jusqu’au compteur d’électricité.
+
+
+
+
Pour donner votre autorisation, vous devez avoir un compte personnel Enedis.
+ Ce compte vous permet également de suivre et gérer vos données de consommation [ou production en fonction de votre service] d’électricité.
+
+
Si vous n'avez pas de compte, vous pouvez le créer depuis cette page.
+ Munissez-vous pour celà de votre facture d’électricité pour créer votre espace.
+
+
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.
+
Une fois cette opération effectué, vous serez rediriger vers une page de confirmation.
+
+
+
diff --git a/bundles/org.openhab.binding.linky/src/main/resources/templates/enedis-step3-cb.html b/bundles/org.openhab.binding.linky/src/main/resources/templates/enedis-step3-cb.html
new file mode 100644
index 00000000000..5efc6abb2a5
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/resources/templates/enedis-step3-cb.html
@@ -0,0 +1,174 @@
+
+
+
+
+
+${pageRefresh}
+OpenHAB Linky binding for Enedis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Plugin Linky / Enedis pour Openhab
+
+
+
+ Vous avez autorisé l'accès pour le compteur Linky : ${prmId.Value}
+ Vous pouvez maintenant utiliser le plugin Linky avec Enedis.
+ ${authorizedUser}
+
Ce plugin permet d'exploiter vos données de consommation éléctrique fournis par Enedis au sein d'OpenHab.
+
+
Enedis gère le réseau d’électricité jusqu’au compteur d’électricité
+ Enedis est le gestionnaire du réseau public de distribution d’électricité sur 95% du territoire français continental.
+
+
Grace à ce plugin, vous serez en mesure de :
+
+
+
Consulter les informations contractuelles liés à votre compte.
+
Créer des graphes de consommation par jour / semaine / mois / annéee.
+
Consulter la puissance maximum utilisé sur une période donnée.
+
+
+
diff --git a/bundles/org.openhab.binding.linky/src/main/resources/templates/myelectricaldata-step2.html b/bundles/org.openhab.binding.linky/src/main/resources/templates/myelectricaldata-step2.html
new file mode 100644
index 00000000000..ab19fedc48f
--- /dev/null
+++ b/bundles/org.openhab.binding.linky/src/main/resources/templates/myelectricaldata-step2.html
@@ -0,0 +1,169 @@
+
+
+
+
+
+${pageRefresh}
+OpenHAB Linky binding for Enedis
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Plugin Linky / MyElectricalData pour Openhab
+
+
+
+
+
+
+
Pour utiliser ce plugin, vous devez donner votre accord pour qu'Enedis transmette vos données.
+
+
Pour donner votre autorisation, vous devez avoir un compte personnel Enedis.
+ Ce compte vous permet également de suivre et gérer vos données de consommation [ou production en fonction de votre service] d’électricité.
+
+
Si vous n'avez pas de compte, vous pouvez le créer depuis cette page.
+ Munissez-vous pour celà de votre facture d’électricité pour créer votre espace.
+
+
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.
+
Une fois cette opération effectué, vous devez vous rendre manuellement sur cette page pour terminer la procédure.