[Linky] Make use of DataConnect Enedis API (#16355)
* fix pull request Signed-off-by: Laurent ARNAL <laurent@clae.net>pull/18740/head
|
@ -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
|
|
@ -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
|
||||
|
||||
The binding has no configuration options, all configuration is done at Thing level.
|
||||
Bridge linky:enedis:local "EnedisWebBridge" [
|
||||
username="laurent@clae.net",
|
||||
password="Mnbo32tyu123!",
|
||||
internalAuthId="eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.u_mxXO7_d4I5bLvJzGtc2MARvpkYv0iM0EsO6a24k-tW9493_Myxwg.LVlfephhGTiCBxii8bRIkA.GOf9Ea8PTGshvkfjl62b6w.hSH97IkmBcEAz2udU-FqQg"
|
||||
]
|
||||
{
|
||||
}
|
||||
|
||||
## Thing Configuration
|
||||
3. Move username, password & internalAuthId configuration parameter from the old linky thing to the bridge thing.
|
||||
|
||||
The thing has the following configuration parameters:
|
||||
4. Link your old thing to the new created bridge thing
|
||||
|
||||
| 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 |
|
||||
Thing linky:linky:linkremotemelody "Linky Melody" (linky:enedis:local)
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
Instructions given for Firefox :
|
||||
## Getting Consumption Data Online
|
||||
|
||||
1. Go to <https://mon-compte-client.enedis.fr/>.
|
||||
1. Select "Particulier" in the drop down list and click on the "Connexion" button.
|
||||
1. You'll be redirected to a page where you'll have to enter you Enedis account email address and check the "Je ne suis pas un robot" checkbox.
|
||||
1. Clic on "Suivant".
|
||||
1. In the login page, prefilled with your mail address, enter your Enedis account password and click on "Connexion à Espace Client Enedis".
|
||||
1. You will be directed to your Enedis account environment. Get back to previous page in you browser.
|
||||
1. Disconnect from your Enedis account
|
||||
1. Repeat steps 1, 2. You should arrive directly on step 5, then open the developer tool window (F12) and select "Stockage" tab. In the "Cookies" entry, select "https://mon-compte-enedis.fr". You'll find an entry named "internalAuthId", copy this value in your openHAB configuration.
|
||||
The new binding version can use multiple bridges to access consumption data.
|
||||
You can use :
|
||||
|
||||
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.
|
||||
- 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.
|
||||
|
||||
## Channels
|
||||
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.
|
||||
|
||||
The information that is retrieved is available as these channels:
|
||||
The binding will not provide this information unless this step is completed.
|
||||
|
||||
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 <https://mon-compte-client.enedis.fr/>.
|
||||
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.<br/>
|
||||
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%]" <energy> { channel="linky:linky:linkyremotexxxx:daily#yesterday" }
|
||||
Number:Energy ConsoSemaineEnCours "Conso cette semaine [%.0f %unit%]" <energy> { channel="linky:linky:linkyremotexxxx:weekly#thisWeek" }
|
||||
Number:Energy ConsoSemaineDerniere "Conso semaine dernière [%.0f %unit%]" <energy> { channel="linky:linky:linkyremotexxxx:weekly#lastWeek" }
|
||||
Number:Energy ConsoMoisEnCours "Conso ce mois [%.0f %unit%]" <energy> { channel="linky:linky:linkyremotexxxx:monthly#thisMonth" }
|
||||
Number:Energy ConsoMoisDernier "Conso mois dernier [%.0f %unit%]" <energy> { channel="linky:linky:linkyremotexxxx:monthly#lastMonth" }
|
||||
Number:Energy ConsoAnneeEnCours "Conso cette année [%.0f %unit%]" <energy> { channel="linky:linky:linkyremotexxxx:yearly#thisYear" }
|
||||
Number:Energy ConsoAnneeDerniere "Conso année dernière [%.0f %unit%]" <energy> { 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%]" <energy> { channel="linky:linky:local:daily#yesterday" }
|
||||
Number:Energy ConsoSemaineEnCours "Conso cette semaine [%.0f %unit%]" <energy> { channel="linky:linky:local:weekly#thisWeek" }
|
||||
Number:Energy ConsoSemaineDerniere "Conso semaine dernière [%.0f %unit%]" <energy> { channel="linky:linky:local:weekly#lastWeek" }
|
||||
Number:Energy ConsoMoisEnCours "Conso ce mois [%.0f %unit%]" <energy> { channel="linky:linky:local:monthly#thisMonth" }
|
||||
Number:Energy ConsoMoisDernier "Conso mois dernier [%.0f %unit%]" <energy> { channel="linky:linky:local:monthly#lastMonth" }
|
||||
Number:Energy ConsoAnneeEnCours "Conso cette année [%.0f %unit%]" <energy> { channel="linky:linky:local:yearly#thisYear" }
|
||||
Number:Energy ConsoAnneeDerniere "Conso année dernière [%.0f %unit%]" <energy> { channel="linky:linky:local:yearly#lastYear" }
|
||||
```
|
||||
|
|
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 118 KiB |
After Width: | Height: | Size: 206 KiB |
After Width: | Height: | Size: 265 KiB |
After Width: | Height: | Size: 182 KiB |
After Width: | Height: | Size: 191 KiB |
After Width: | Height: | Size: 180 KiB |
|
@ -0,0 +1,20 @@
|
|||
2. Step 2
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
3. Step 3
|
||||
|
||||
<br/>
|
||||
|
||||
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 !
|
||||
|
||||
<br/>
|
||||
|
||||
5. Step 5
|
||||
|
||||
<br/>
|
||||
|
After Width: | Height: | Size: 225 KiB |
After Width: | Height: | Size: 290 KiB |
After Width: | Height: | Size: 182 KiB |
After Width: | Height: | Size: 184 KiB |
After Width: | Height: | Size: 148 KiB |
After Width: | Height: | Size: 191 KiB |
After Width: | Height: | Size: 150 KiB |
|
@ -0,0 +1,29 @@
|
|||
2. Step 2
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
3. Step 3
|
||||
|
||||
<br/>
|
||||
|
||||
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 !
|
||||
|
||||
<br/>
|
||||
|
||||
5. Step 5
|
||||
|
||||
<br/>
|
||||
|
||||
6. Step 6
|
||||
|
||||
<br/>
|
||||
|
||||
7. Step 7
|
||||
|
||||
<br/>
|
||||
|
||||
|
|
@ -17,14 +17,4 @@
|
|||
<properties>
|
||||
<bnd.importpackage>javax.annotation.meta;resolution:=optional</bnd.importpackage>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>1.14.3</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<feature name="openhab-binding-linky" description="Linky Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle dependency="true">mvn:org.jsoup/jsoup/1.14.3</bundle>
|
||||
<feature>openhab-core-auth-oauth2client</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.linky/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
|
|
|
@ -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";
|
||||
}
|
|
@ -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<String, String> hashRes = gson.fromJson(result.getContentAsString(), HashMap.class);
|
||||
|
||||
String cookieKey;
|
||||
if (hashRes != null && hashRes.containsKey("cnAlex")) {
|
||||
cookieKey = "personne_for_" + hashRes.get("cnAlex");
|
||||
} else {
|
||||
throw new LinkyException("Connection failed step 7, missing cookieKey");
|
||||
}
|
||||
|
||||
List<HttpCookie> lCookie = httpClient.getCookieStore().getCookies();
|
||||
Optional<HttpCookie> cookie = lCookie.stream().filter(it -> it.getName().contains(cookieKey)).findFirst();
|
||||
|
||||
String cookieVal = cookie.map(HttpCookie::getValue)
|
||||
.orElseThrow(() -> new LinkyException("Connection failed step 7, missing cookieVal"));
|
||||
|
||||
addCookie(cookieKey, cookieVal);
|
||||
|
||||
connected = true;
|
||||
} catch (InterruptedException | TimeoutException | ExecutionException | JsonSyntaxException e) {
|
||||
throw new LinkyException(e, "Error opening connection with Enedis webservice");
|
||||
}
|
||||
}
|
||||
|
||||
private String getLocation(ContentResponse response) {
|
||||
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,14 +153,20 @@ public class EnedisHttpApi {
|
|||
}
|
||||
}
|
||||
|
||||
private <T> T getData(String url, Class<T> clazz) throws LinkyException {
|
||||
if (!connected) {
|
||||
initialize();
|
||||
}
|
||||
String data = getContent(url);
|
||||
if (data.isEmpty()) {
|
||||
throw new LinkyException("Requesting '%s' returned an empty response", url);
|
||||
private <T> T getData(ThingBaseRemoteHandler handler, String url, Class<T> clazz) throws LinkyException {
|
||||
if (!linkyBridgeHandler.isConnected()) {
|
||||
linkyBridgeHandler.initialize();
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -302,38 +176,131 @@ public class EnedisHttpApi {
|
|||
throw new LinkyException(e, "Requesting '%s' returned an invalid JSON response", url);
|
||||
}
|
||||
}
|
||||
} catch (LinkyException ex) {
|
||||
lastException = ex;
|
||||
|
||||
public PrmInfo getPrmInfo(String internId) throws LinkyException {
|
||||
String url = PRM_INFO_URL.formatted(internId);
|
||||
PrmInfo[] prms = getData(url, PrmInfo[].class);
|
||||
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(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");
|
||||
}
|
||||
|
||||
if (prmId.isBlank()) {
|
||||
return prms[0];
|
||||
}
|
||||
|
||||
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);
|
||||
Optional<PrmInfo> result = Arrays.stream(prms).filter(x -> x.idPrm.equals(prmId)).findFirst();
|
||||
if (result.isPresent()) {
|
||||
return result.get();
|
||||
}
|
||||
|
||||
public UserInfo getUserInfo() throws LinkyException {
|
||||
return getData(USER_INFO_URL, UserInfo.class);
|
||||
throw new LinkyException(("PRM with id : %s does not exist").formatted(prmId));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,11 +44,11 @@ public class ExpiringDayCache<V> {
|
|||
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<V> {
|
|||
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.
|
||||
*
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 = "";
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ThingTypeUID> 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";
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -40,6 +40,8 @@ public class ConsumptionReport {
|
|||
}
|
||||
|
||||
public class ChronoData {
|
||||
@SerializedName("heure")
|
||||
public Aggregate heure;
|
||||
@SerializedName("jour")
|
||||
public Aggregate days;
|
||||
@SerializedName("semaine")
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.linky.internal.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link ResponseContract} holds informations about the contract
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||
*/
|
||||
|
||||
public class ResponseContract {
|
||||
public Customer customer;
|
||||
|
||||
public class Customer {
|
||||
@SerializedName("customer_id")
|
||||
public String customerId;
|
||||
|
||||
@SerializedName("usage_points")
|
||||
public UsagePoints[] usagePoint;
|
||||
}
|
||||
|
||||
public class UsagePoints {
|
||||
@SerializedName("usage_point")
|
||||
public UsagePoint usagePoint;
|
||||
public Contract contracts;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.linky.internal.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link UserInfo} holds informations about energy delivery point
|
||||
*
|
||||
* @author Laurent Arnal - Initial contribution - Rewrite addon to use official dataconect API
|
||||
*/
|
||||
|
||||
public class ResponseIdentity {
|
||||
@SerializedName("customer_id")
|
||||
public String customerId;
|
||||
|
||||
public IdentityEntry identity;
|
||||
|
||||
public class IdentityEntry {
|
||||
@SerializedName("natural_person")
|
||||
public Identity naturalPerson;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.linky.internal.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link UserInfo} holds informations about energy delivery point
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||
*/
|
||||
|
||||
public class ResponseMeter {
|
||||
@SerializedName("meter_reading")
|
||||
public MeterReading meterReading;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.linky.internal.dto;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
/**
|
||||
* The {@link UserInfo} holds informations about energy delivery point
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||
*/
|
||||
|
||||
public class ResponseTempo extends LinkedHashMap<String, String> {
|
||||
private static final long serialVersionUID = 362498820763181264L;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.linky.internal.dto;
|
||||
|
||||
/**
|
||||
* The {@link UserInfo} holds informations about energy delivery point
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
* @author Laurent Arnal - Rewrite addon to use official dataconect API
|
||||
*/
|
||||
|
||||
public class TempoDay {
|
||||
public String tempoDay;
|
||||
public String tempoVal;
|
||||
}
|
|
@ -0,0 +1,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;
|
||||
}
|
||||
}
|
|
@ -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<ZonedDateTime>) (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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<String> 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<String> getAllPrmId() {
|
||||
return registeredPrmId;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return connected;
|
||||
}
|
||||
|
||||
public @Nullable EnedisHttpApi getEnedisApi() {
|
||||
return enedisApi;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("Shutting down Linky API bridge handler.");
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
|
||||
super.updateStatus(status, statusDetail, description);
|
||||
}
|
||||
|
||||
public abstract String getToken(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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<String, String> hashRes = gson.fromJson(result.getContentAsString(), HashMap.class);
|
||||
|
||||
String cookieKey;
|
||||
|
||||
if (hashRes != null && hashRes.containsKey("cnAlex")) {
|
||||
cookieKey = "personne_for_" + hashRes.get("cnAlex");
|
||||
} else {
|
||||
throw new LinkyException("Connection failed step 7, missing cookieKey");
|
||||
}
|
||||
|
||||
List<HttpCookie> lCookie = httpClient.getCookieStore().getCookies();
|
||||
Optional<HttpCookie> cookie = lCookie.stream().filter(it -> it.getName().contains(cookieKey)).findFirst();
|
||||
|
||||
String cookieVal = cookie.map(HttpCookie::getValue)
|
||||
.orElseThrow(() -> new LinkyException("Connection failed step 7, missing cookieVal"));
|
||||
|
||||
enedisApi.addCookie(cookieKey, cookieVal);
|
||||
|
||||
connected = true;
|
||||
} catch (InterruptedException | TimeoutException | ExecutionException | JsonSyntaxException e) {
|
||||
throw new LinkyException(e, "Error opening connection with Enedis webservice");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportNewApiFormat() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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<Thing> 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;
|
||||
}
|
||||
}
|
|
@ -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<Consumption> cachedDailyData;
|
||||
private final ExpiringDayCache<Consumption> cachedPowerData;
|
||||
private final ExpiringDayCache<Consumption> cachedMonthlyData;
|
||||
private final ExpiringDayCache<Consumption> 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<String> reportValues(LocalDate startDay, LocalDate endDay, @Nullable String separator) {
|
||||
List<String> report = buildReport(startDay, endDay, separator);
|
||||
disconnect();
|
||||
return report;
|
||||
}
|
||||
|
||||
private List<String> buildReport(LocalDate startDay, LocalDate endDay, @Nullable String separator) {
|
||||
List<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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> metaData;
|
||||
private final ExpiringDayCache<MeterReading> dailyConsumption;
|
||||
private final ExpiringDayCache<MeterReading> dailyConsumptionMaxPower;
|
||||
private final ExpiringDayCache<MeterReading> 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<String, String> 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<String, String> 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 <T extends Quantity<T>> void updateTimeSeries(String groupId, String channelId,
|
||||
IntervalReading[] iv, Unit<T> 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<String> reportValues(LocalDate startDay, LocalDate endDay, @Nullable String separator) {
|
||||
return buildReport(startDay, endDay, separator);
|
||||
}
|
||||
|
||||
private List<String> buildReport(LocalDate startDay, LocalDate endDay, @Nullable String separator) {
|
||||
List<String> 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);
|
||||
}
|
||||
}
|
|
@ -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<ResponseTempo> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = "<p class='block authorized'>Addon authorized for %s.</p>";
|
||||
private static final String HTML_ERROR = "<p class='block error'>Call to Enedis failed with error: %s</p>";
|
||||
|
||||
// Keys present in the index.html
|
||||
private static final String KEY_AUTHORIZE_URI = "authorize.uri";
|
||||
private static final String KEY_RETRIEVE_TOKEN_URI = "retrieveToken.uri";
|
||||
private static final String KEY_REDIRECT_URI = "redirectUri";
|
||||
private static final String KEY_CODE = "code.Value";
|
||||
private static final String KEY_PRMID = "prmId.Value";
|
||||
private static final String KEY_PRMID_OPTION = "prmId.Option";
|
||||
private static final String KEY_AUTHORIZED_USER = "authorizedUser";
|
||||
private static final String KEY_CB_DISPLAY_CONFIRMATION = "cb.displayConfirmation";
|
||||
private static final String KEY_CB_DISPLAY_ERROR = "cb.displayError";
|
||||
private static final String KEY_CB_DISPLAY_INSTRUCTION = "cb.displayInstruction";
|
||||
private static final String KEY_ERROR = "error";
|
||||
private static final String KEY_PAGE_REFRESH = "pageRefresh";
|
||||
private static final String TEMPLATE_PATH = "templates/";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(LinkyAuthServlet.class);
|
||||
private final String index;
|
||||
private final String enedisStep1;
|
||||
private final String enedisStep2;
|
||||
private final String enedisStep3;
|
||||
private final String myelectricaldataStep1;
|
||||
private final String myelectricaldataStep2;
|
||||
private final String myelectricaldataStep3;
|
||||
|
||||
private 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<String, String> replaceMap = new HashMap<>();
|
||||
|
||||
StringBuffer requestUrl = req.getRequestURL();
|
||||
String servletBaseUrl = requestUrl != null ? requestUrl.toString() : "";
|
||||
|
||||
String template = "";
|
||||
if (servletBaseUrl.contains("index")) {
|
||||
template = index;
|
||||
} else if (servletBaseUrl.contains("enedis-step1")) {
|
||||
template = enedisStep1;
|
||||
} else if (servletBaseUrl.contains("enedis-step2")) {
|
||||
template = enedisStep2;
|
||||
} else if (servletBaseUrl.contains("enedis-step3-cb")) {
|
||||
template = enedisStep3;
|
||||
} else if (servletBaseUrl.contains("myelectricaldata-step1")) {
|
||||
template = myelectricaldataStep1;
|
||||
} else if (servletBaseUrl.contains("myelectricaldata-step2")) {
|
||||
template = myelectricaldataStep2;
|
||||
} else if (servletBaseUrl.contains("myelectricaldata-step3")) {
|
||||
template = myelectricaldataStep3;
|
||||
} else if (servletBaseUrl.contains("enedis")) {
|
||||
template = enedisStep1;
|
||||
} else if (servletBaseUrl.contains("myelectricaldata")) {
|
||||
template = myelectricaldataStep1;
|
||||
} else {
|
||||
template = index;
|
||||
}
|
||||
|
||||
// for some unknown reason, getRequestURL return a malformed URL mixing http:// and port 443
|
||||
if (servletBaseUrl.contains(":443")) {
|
||||
servletBaseUrl = servletBaseUrl.replace("http://", "https://");
|
||||
servletBaseUrl = servletBaseUrl.replace(":443", "");
|
||||
}
|
||||
|
||||
try {
|
||||
handleLinkyRedirect(replaceMap, servletBaseUrl, req.getQueryString());
|
||||
|
||||
resp.setContentType(CONTENT_TYPE);
|
||||
|
||||
StringBuffer optionBuffer = new StringBuffer();
|
||||
|
||||
List<String> prmIds = apiBridgeHandler.getAllPrmId();
|
||||
for (String prmId : prmIds) {
|
||||
optionBuffer.append("<option value=\"" + prmId + "\">" + prmId + "</option>");
|
||||
}
|
||||
|
||||
final MultiMap<@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<String, String> replaceMap, String servletBaseURL,
|
||||
@Nullable String queryString) throws LinkyException {
|
||||
replaceMap.put(KEY_AUTHORIZED_USER, "");
|
||||
replaceMap.put(KEY_ERROR, "");
|
||||
replaceMap.put(KEY_PAGE_REFRESH, "");
|
||||
replaceMap.put(KEY_CB_DISPLAY_CONFIRMATION, "none");
|
||||
replaceMap.put(KEY_CB_DISPLAY_ERROR, "none");
|
||||
replaceMap.put(KEY_CB_DISPLAY_INSTRUCTION, "true");
|
||||
|
||||
if (queryString != null) {
|
||||
final MultiMap<@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<String, String> map) {
|
||||
final Matcher m = MESSAGE_KEY_PATTERN.matcher(template);
|
||||
final StringBuffer sb = new StringBuffer();
|
||||
|
||||
while (m.find()) {
|
||||
try {
|
||||
final String key = m.group(1);
|
||||
m.appendReplacement(sb, Matcher.quoteReplacement(map.getOrDefault(key, "${" + key + '}')));
|
||||
} catch (RuntimeException e) {
|
||||
logger.debug("Error occurred during template filling, cause ", e);
|
||||
}
|
||||
}
|
||||
m.appendTail(sb);
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
@ -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,10 +32,13 @@ import com.google.gson.stream.JsonWriter;
|
|||
*
|
||||
* @author Laurent Arnal - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class DoubleTypeAdapter extends TypeAdapter<Double> {
|
||||
|
||||
@Override
|
||||
public Double read(JsonReader reader) throws IOException {
|
||||
public @NonNull Double read(@Nullable JsonReader reader) throws IOException {
|
||||
if (reader != null) {
|
||||
if (reader.peek() == JsonToken.NULL) {
|
||||
reader.nextNull();
|
||||
return Double.NaN;
|
||||
|
@ -44,13 +51,17 @@ public class DoubleTypeAdapter extends TypeAdapter<Double> {
|
|||
return Double.NaN;
|
||||
}
|
||||
}
|
||||
return Double.NaN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(JsonWriter writer, Double value) throws IOException {
|
||||
public void write(@Nullable JsonWriter writer, @Nullable Double value) throws IOException {
|
||||
if (writer != null) {
|
||||
if (value == null) {
|
||||
writer.nullValue();
|
||||
return;
|
||||
}
|
||||
writer.value(value.doubleValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="linky"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
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">
|
||||
|
||||
<bridge-type id="enedis">
|
||||
<label>Enedis Web Bridge</label>
|
||||
<description>
|
||||
Provides your energy consumption data.
|
||||
In order to receive the data, you must activate your account at
|
||||
https://espace-client-particuliers.enedis.fr/web/espace-particuliers/compteur-linky.
|
||||
</description>
|
||||
|
||||
<config-description>
|
||||
<parameter name="username" type="text" required="true">
|
||||
<label>Username</label>
|
||||
<context>email</context>
|
||||
<description>Your Enedis Username</description>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" required="true">
|
||||
<label>Password</label>
|
||||
<context>password</context>
|
||||
<description>Your Enedis Password</description>
|
||||
</parameter>
|
||||
<parameter name="internalAuthId" type="text" required="true">
|
||||
<label>Auth ID</label>
|
||||
<description>Authentication ID delivered after the captcha (see documentation).</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
</thing:thing-descriptions>
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="linky"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
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">
|
||||
|
||||
<bridge-type id="enedis-api">
|
||||
<label>Enedis API Bridge</label>
|
||||
<description>
|
||||
Provides your energy consumption data.
|
||||
In order to receive the data, you must activate your account at
|
||||
https://espace-client-particuliers.enedis.fr/web/espace-particuliers/compteur-linky.
|
||||
</description>
|
||||
|
||||
<config-description>
|
||||
<parameter name="clientId" type="text" required="false">
|
||||
<label>clientId</label>
|
||||
<description>Your Enedis clientId</description>
|
||||
</parameter>
|
||||
<parameter name="clientSecret" type="text" required="false">
|
||||
<label>clientSecret</label>
|
||||
<description>Your Enedis clientSecret</description>
|
||||
</parameter>
|
||||
<parameter name="isSandbox" type="boolean" required="false">
|
||||
<label>isSandbox</label>
|
||||
<description>To test on the sandbox environment</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</bridge-type>
|
||||
</thing:thing-descriptions>
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="linky"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
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">
|
||||
|
||||
<bridge-type id="my-electrical-data">
|
||||
<label>MyElectricalData Bridge</label>
|
||||
<description>
|
||||
Provides your energy consumption data.
|
||||
In order to receive the data, you must activate your account at
|
||||
https://espace-client-particuliers.enedis.fr/web/espace-particuliers/compteur-linky.
|
||||
</description>
|
||||
|
||||
<config-description>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
</thing:thing-descriptions>
|
|
@ -0,0 +1,67 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="linky"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
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">
|
||||
|
||||
<channel-group-type id="daily">
|
||||
<label>Daily Consumption</label>
|
||||
<channels>
|
||||
<channel id="yesterday" typeId="consumption">
|
||||
<label>Yesterday Consumption</label>
|
||||
<description>The energy consumption for previous day</description>
|
||||
</channel>
|
||||
|
||||
<channel id="day-2" typeId="consumption">
|
||||
<label>Day -2 Consumption</label>
|
||||
<description>The energy consumption for day -2</description>
|
||||
</channel>
|
||||
|
||||
<channel id="day-3" typeId="consumption">
|
||||
<label>Day -3 Consumption</label>
|
||||
<description>The energy consumption for day -3</description>
|
||||
</channel>
|
||||
|
||||
<channel id="consumption" typeId="consumption">
|
||||
<label>Consumption</label>
|
||||
<description>The energy consumption</description>
|
||||
</channel>
|
||||
|
||||
<channel id="max-power" typeId="power">
|
||||
<label>Peak Value</label>
|
||||
<description>Maximum power usage value</description>
|
||||
</channel>
|
||||
|
||||
<channel id="power" typeId="power">
|
||||
<label>Peak Value Yesterday</label>
|
||||
<description>Maximum power usage value for Yesterday</description>
|
||||
</channel>
|
||||
|
||||
<channel id="timestamp" typeId="timestamp">
|
||||
<label>Peak Timestamp Yesterday</label>
|
||||
<description>Maximum power usage timestamp for Yesterday</description>
|
||||
</channel>
|
||||
|
||||
<channel id="power-2" typeId="power">
|
||||
<label>Peak Value Day-2</label>
|
||||
<description>Maximum power usage value for Day-2</description>
|
||||
</channel>
|
||||
|
||||
<channel id="timestamp-2" typeId="timestamp">
|
||||
<label>Peak Timestamp Day-2</label>
|
||||
<description>Maximum power usage timestamp for Day-2</description>
|
||||
</channel>
|
||||
|
||||
<channel id="power-3" typeId="power">
|
||||
<label>Peak Value Day-3</label>
|
||||
<description>Maximum power usage value for Day-3</description>
|
||||
</channel>
|
||||
|
||||
<channel id="timestamp-3" typeId="timestamp">
|
||||
<label>Peak Timestamp Day-3</label>
|
||||
<description>Maximum power usage timestamp for Day-3</description>
|
||||
</channel>
|
||||
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
</thing:thing-descriptions>
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="linky"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
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">
|
||||
|
||||
<channel-group-type id="load-curve">
|
||||
<label>Load curve</label>
|
||||
<channels>
|
||||
<channel id="power" typeId="power">
|
||||
<label>Load Curve Power</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
</thing:thing-descriptions>
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="linky"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
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">
|
||||
|
||||
<channel-group-type id="monthly">
|
||||
<label>Monthly Consumption</label>
|
||||
<channels>
|
||||
<channel id="thisMonth" typeId="consumption">
|
||||
<label>This Month Consumption</label>
|
||||
<description>The energy consumption for the current Month</description>
|
||||
</channel>
|
||||
|
||||
<channel id="lastMonth" typeId="consumption">
|
||||
<label>Last Month Consumption</label>
|
||||
<description>The energy consumption for the previous Month</description>
|
||||
</channel>
|
||||
|
||||
<channel id="month-2" typeId="consumption">
|
||||
<label>Month -2 Consumption</label>
|
||||
<description>The energy consumption for the Month -2</description>
|
||||
</channel>
|
||||
|
||||
<channel id="consumption" typeId="consumption">
|
||||
<label>Consumption</label>
|
||||
<description>The energy consumption</description>
|
||||
</channel>
|
||||
|
||||
<channel id="max-power" typeId="power">
|
||||
<label>Peak Value</label>
|
||||
<description>Maximum power usage value</description>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
</thing:thing-descriptions>
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="linky"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
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">
|
||||
|
||||
<channel-group-type id="weekly">
|
||||
<label>Weekly Consumption</label>
|
||||
<channels>
|
||||
<channel id="thisWeek" typeId="consumption">
|
||||
<label>This Week Consumption</label>
|
||||
<description>The energy consumption for the current Week</description>
|
||||
</channel>
|
||||
<channel id="lastWeek" typeId="consumption">
|
||||
<label>Last Week Consumption</label>
|
||||
<description>The energy consumption for the previous Week</description>
|
||||
</channel>
|
||||
<channel id="week-2" typeId="consumption">
|
||||
<label>Week -2 Consumption</label>
|
||||
<description>The energy consumption for the Week -2</description>
|
||||
</channel>
|
||||
<channel id="consumption" typeId="consumption">
|
||||
<label>Consumption</label>
|
||||
<description>The energy consumption</description>
|
||||
</channel>
|
||||
<channel id="max-power" typeId="power">
|
||||
<label>Peak Value</label>
|
||||
<description>Maximum power usage value</description>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
</thing:thing-descriptions>
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="linky"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
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">
|
||||
|
||||
<channel-group-type id="yearly">
|
||||
<label>Yearly Consumption</label>
|
||||
<channels>
|
||||
<channel id="thisYear" typeId="consumption">
|
||||
<label>This Year Consumption</label>
|
||||
<description>The energy consumption for the current Year</description>
|
||||
</channel>
|
||||
<channel id="lastYear" typeId="consumption">
|
||||
<label>Last Year Consumption</label>
|
||||
<description>The energy consumption for the previous Year</description>
|
||||
</channel>
|
||||
<channel id="year-2" typeId="consumption">
|
||||
<label>Year -2 Consumption</label>
|
||||
<description>The energy consumption for the Year -2</description>
|
||||
</channel>
|
||||
<channel id="consumption" typeId="consumption">
|
||||
<label>Consumption</label>
|
||||
<description>The energy consumption</description>
|
||||
</channel>
|
||||
<channel id="max-power" typeId="power">
|
||||
<label>Peak Value</label>
|
||||
<description>Maximum power usage value</description>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
</thing:thing-descriptions>
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="linky"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
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">
|
||||
|
||||
<channel-group-type id="tempo-calendar">
|
||||
<label>Tempo</label>
|
||||
<channels>
|
||||
<channel id="tempo-info-today" typeId="tempo-value">
|
||||
<label>Tempo Today Color</label>
|
||||
</channel>
|
||||
<channel id="tempo-info-tomorrow" typeId="tempo-value">
|
||||
<label>Tempo Today Color</label>
|
||||
</channel>
|
||||
<channel id="tempo-info-timeseries" typeId="tempo-value">
|
||||
<label>Tempo Day Information</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
</thing:thing-descriptions>
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="linky"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="linky">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="enedis"/>
|
||||
<bridge-type-ref id="enedis-api"/>
|
||||
<bridge-type-ref id="my-electrical-data"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Linky</label>
|
||||
<description>
|
||||
Provides your energy consumption data.
|
||||
In order to receive the data, you must activate your account at
|
||||
https://espace-client-particuliers.enedis.fr/web/espace-particuliers/compteur-linky.
|
||||
</description>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group typeId="load-curve" id="load-curve"/>
|
||||
<channel-group typeId="daily" id="daily"/>
|
||||
<channel-group typeId="weekly" id="weekly"/>
|
||||
<channel-group typeId="monthly" id="monthly"/>
|
||||
<channel-group typeId="yearly" id="yearly"/>
|
||||
</channel-groups>
|
||||
|
||||
<config-description>
|
||||
<parameter name="prmId" type="text" required="false">
|
||||
<label>prmId</label>
|
||||
<description>The Meter Id (PRM). If not provided, the binding will use the first registered meter found on your
|
||||
Enedis account.</description>
|
||||
</parameter>
|
||||
<parameter name="timezone" type="text" required="false">
|
||||
<label>timezone</label>
|
||||
<description>The timezone associated with your Point of delivery.
|
||||
Will default to openhab default timezone.
|
||||
You will
|
||||
need to change this if your linky is located in a different timezone that your openhab location.
|
||||
You can use an
|
||||
offset, or a label like Europe/Paris</description>
|
||||
</parameter>
|
||||
<parameter name="token" type="text" required="false">
|
||||
<label>Token</label>
|
||||
<description>Your Enedis token (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)</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
|
||||
</thing:thing-descriptions>
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="linky"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="tempo-calendar">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="my-electrical-data"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Tempo Calendar</label>
|
||||
<description>Provides tempo calendar</description>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group typeId="tempo-calendar" id="tempo-calendar"/>
|
||||
</channel-groups>
|
||||
</thing-type>
|
||||
|
||||
|
||||
</thing:thing-descriptions>
|
|
@ -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">
|
||||
|
||||
<thing-type id="linky">
|
||||
<label>Linky</label>
|
||||
<description>
|
||||
Provides your energy consumption data.
|
||||
In order to receive the data, you must activate your account at
|
||||
https://espace-client-particuliers.enedis.fr/web/espace-particuliers/compteur-linky.
|
||||
</description>
|
||||
<semantic-equipment-tag>WebService</semantic-equipment-tag>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group typeId="daily" id="daily"/>
|
||||
<channel-group typeId="weekly" id="weekly"/>
|
||||
<channel-group typeId="monthly" id="monthly"/>
|
||||
<channel-group typeId="yearly" id="yearly"/>
|
||||
</channel-groups>
|
||||
|
||||
<config-description>
|
||||
<parameter name="username" type="text" required="true">
|
||||
<label>Username</label>
|
||||
<context>email</context>
|
||||
<description>Your Enedis Username</description>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" required="true">
|
||||
<label>Password</label>
|
||||
<context>password</context>
|
||||
<description>Your Enedis Password</description>
|
||||
</parameter>
|
||||
<parameter name="internalAuthId" type="text" required="true">
|
||||
<label>Auth ID</label>
|
||||
<description>Authentication ID delivered after the captcha (see documentation).</description>
|
||||
</parameter>
|
||||
<parameter name="timezone" type="text" required="false">
|
||||
<label>Timezone</label>
|
||||
<description>The timezone associated with your Point of delivery.
|
||||
Will default to openHAB default timezone.
|
||||
You will
|
||||
need to change this if your Linky is located in a different timezone that your openHAB location.
|
||||
You can use an
|
||||
offset, or a label like Europe/Paris</description>
|
||||
</parameter>
|
||||
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-group-type id="daily">
|
||||
<label>Daily consumption</label>
|
||||
<channels>
|
||||
<channel id="yesterday" typeId="consumption">
|
||||
<label>Yesterday Consumption</label>
|
||||
</channel>
|
||||
<channel id="power" typeId="power"/>
|
||||
<channel id="timestamp" typeId="timestamp">
|
||||
<label>Peak Timestamp</label>
|
||||
<description>Maximum power usage timestamp</description>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="weekly">
|
||||
<label>Weekly consumption</label>
|
||||
<channels>
|
||||
<channel id="thisWeek" typeId="consumption">
|
||||
<label>This Week Consumption</label>
|
||||
</channel>
|
||||
<channel id="lastWeek" typeId="consumption">
|
||||
<label>Last Week Consumption</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="monthly">
|
||||
<label>Monthly consumption</label>
|
||||
<channels>
|
||||
<channel id="thisMonth" typeId="consumption">
|
||||
<label>This Month Consumption</label>
|
||||
</channel>
|
||||
<channel id="lastMonth" typeId="consumption">
|
||||
<label>Last Month Consumption</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="yearly">
|
||||
<label>Yearly consumption</label>
|
||||
<channels>
|
||||
<channel id="thisYear" typeId="consumption">
|
||||
<label>This Year Consumption</label>
|
||||
</channel>
|
||||
<channel id="lastYear" typeId="consumption">
|
||||
<label>Last Year Consumption</label>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
<channel-type id="tempo-value">
|
||||
<item-type>Number</item-type>
|
||||
<label>Tempo Color Information</label>
|
||||
<description>This status describes the tempo color of a day.</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="0">Blue</option>
|
||||
<option value="1">White</option>
|
||||
<option value="2">Red</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="consumption">
|
||||
<item-type>Number:Energy</item-type>
|
||||
|
@ -108,8 +27,8 @@
|
|||
|
||||
<channel-type id="power">
|
||||
<item-type unitHint="kVA">Number:Power</item-type>
|
||||
<label>Yesterday Peak Power</label>
|
||||
<description>Maximum power usage yesterday</description>
|
||||
<label>Power Usage</label>
|
||||
<description>Power usage for a period</description>
|
||||
<state readOnly="true" pattern="%.2f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
${pageRefresh}
|
||||
<title>OpenHAB Linky binding for Enedis</title>
|
||||
<link rel="icon" href="img/favicon.ico" type="image/vnd.microsoft.icon">
|
||||
<link>
|
||||
<style>
|
||||
html {
|
||||
font-family: "Roboto", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
.block {
|
||||
border: 1px solid #bbb;
|
||||
background-color: white;
|
||||
margin: 10px 0;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #FFC0C0;
|
||||
border: 1px solid darkred;
|
||||
color: darkred
|
||||
}
|
||||
|
||||
.authorized {
|
||||
border: 1px solid #90EE90;
|
||||
background-color: #E0FFE0;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-left:30px;
|
||||
margin-bottom: 10px;
|
||||
float:left;
|
||||
}
|
||||
|
||||
.olList {
|
||||
margin-top:20px;
|
||||
}
|
||||
|
||||
.olList li {
|
||||
margin:20px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.box {
|
||||
float:left;
|
||||
transform: translate(0%, -30%);
|
||||
}
|
||||
|
||||
.box select {
|
||||
background-color: #0563af;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
width: 350px;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
|
||||
-webkit-appearance: button;
|
||||
appearance: button;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Style the arrow inside the select element: */
|
||||
.box::before {
|
||||
content: "\f13a";
|
||||
font-family: FontAwesome;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50px;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
line-height: 45px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
.logo {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.logoEnedis {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.logoTransfer {
|
||||
display: inline;
|
||||
margin-left:300px;
|
||||
top: -30px;
|
||||
}
|
||||
|
||||
.button a {
|
||||
background: #1ED760;
|
||||
border-radius: 500px;
|
||||
color: white;
|
||||
padding: 10px 20px 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
border-width: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
<script language="javascript">
|
||||
function retrieveToken()
|
||||
{
|
||||
var prmId = document.getElementById('prmId').value;
|
||||
document.location= "${retrieveToken.uri}" + "&code=" + prmId;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="margin:50px">
|
||||
<div class="logo" style="vertical-align:top;">
|
||||
<div style="display: inline-block;width:500;margin-right:100px;margin-top:0px;margin-bottom:0px;">
|
||||
<img src="/connectlinky/img/openhab_logo.svg" height="100"/>
|
||||
</div>
|
||||
<div style="display: inline-block;width:100;margin-top:0px;margin-bottom:0px;">
|
||||
</div>
|
||||
<div style="display: inline-block;width:300;margin-top:0px;margin-bottom:0px;">
|
||||
<img src="/connectlinky/img/enedis.png" height="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/><br/><br/>
|
||||
<hr style="margin:0px;padding-left:20px;padding-right:20px;width:97%;"/>
|
||||
<div style="display: block;width:400;margin-top:0px;margin-bottom:0px;float:right;top:60px;right:160px;position:absolute;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="400" version="1.1">
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
|
||||
<circle cx="100" cy="110" r="20" stroke="black" stroke-width="1" fill="red" />
|
||||
<text x="100" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">1</text>
|
||||
<polyline points="130,110 170,110" fill="none" stroke="black" marker-end="url(#arrow)" />
|
||||
|
||||
<circle cx="200" cy="110" r="20" stroke="red" stroke-width="1" fill="none" />
|
||||
<text x="200" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">2</text>
|
||||
<polyline points="230,110 270,110" fill="none" stroke="black" marker-end="url(#arrow)" />
|
||||
|
||||
<circle cx="300" cy="110" r="20" stroke="black" stroke-width="1" fill="none" />
|
||||
<text x="300" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">3</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="background:linear-gradient(#faf3ff, #e8cbfd);padding:20px;margin:0px;display:inline-block; width:97%;align:center;">
|
||||
<h3>Plugin Linky / Enedis pour Openhab</h3>
|
||||
<br/>
|
||||
|
||||
<p>Ce plugin permet d'exploiter vos données de consommation éléctrique fournis par Enedis au sein d'OpenHab.</p>
|
||||
|
||||
<p>Enedis gère le réseau d’électricité jusqu’au compteur d’électricité<br/>
|
||||
Enedis est le gestionnaire du réseau public de distribution d’électricité sur 95% du territoire français continental.</p>
|
||||
|
||||
<p>Grace à ce plugin, vous serez en mesure de : </p>
|
||||
|
||||
<ul>
|
||||
<li>Consulter les informations contractuelles liés à votre compte.</li>
|
||||
<li>Créer des graphes de consommation par jour / semaine / mois / annéee.</li>
|
||||
<li>Consulter la puissance maximum utilisé sur une période donnée.</li>
|
||||
<li>Load curve</li>
|
||||
</ul>
|
||||
|
||||
<br/><br/>
|
||||
<div>
|
||||
<div class="button">
|
||||
<a href="/connectlinky/enedis-step2">Suite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,174 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
${pageRefresh}
|
||||
<title>OpenHAB Linky binding for Enedis</title>
|
||||
<link rel="icon" href="img/favicon.ico" type="image/vnd.microsoft.icon">
|
||||
<link>
|
||||
<style>
|
||||
html {
|
||||
font-family: "Roboto", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
.block {
|
||||
border: 1px solid #bbb;
|
||||
background-color: white;
|
||||
margin: 10px 0;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #FFC0C0;
|
||||
border: 1px solid darkred;
|
||||
color: darkred
|
||||
}
|
||||
|
||||
.authorized {
|
||||
border: 1px solid #90EE90;
|
||||
background-color: #E0FFE0;
|
||||
}
|
||||
|
||||
.olList {
|
||||
margin-top:20px;
|
||||
}
|
||||
|
||||
.olList li {
|
||||
margin:20px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.box {
|
||||
float:left;
|
||||
transform: translate(0%, -30%);
|
||||
}
|
||||
|
||||
.box select {
|
||||
background-color: #0563af;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
width: 350px;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
|
||||
-webkit-appearance: button;
|
||||
appearance: button;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Style the arrow inside the select element: */
|
||||
.box::before {
|
||||
content: "\f13a";
|
||||
font-family: FontAwesome;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50px;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
line-height: 45px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
.logo {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.logoEnedis {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.logoTransfer {
|
||||
display: inline;
|
||||
margin-left:300px;
|
||||
top: -30px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
<script language="javascript">
|
||||
function retrieveToken()
|
||||
{
|
||||
var prmId = document.getElementById('prmId').value;
|
||||
document.location= "${retrieveToken.uri}" + "&code=" + prmId;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="margin:50px">
|
||||
<div class="logo" style="vertical-align:top;">
|
||||
<div style="display: inline-block;width:500;margin-right:100px;">
|
||||
<img src="/connectlinky/img/openhab_logo.svg" height="100"/>
|
||||
</div>
|
||||
<div style="display: inline-block;width:100;">
|
||||
</div>
|
||||
<div style="display: inline-block;width:300;">
|
||||
<img src="/connectlinky/img/enedis.png" height="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/><br/><br/>
|
||||
<hr style="margin:0px;padding-left:20px;padding-right:20px;width:97%;"/>
|
||||
<div style="display: block;width:400;margin-top:0px;margin-bottom:0px;float:right;top:60px;right:160px;position:absolute;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="400" version="1.1">
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
|
||||
<circle cx="100" cy="110" r="20" stroke="black" stroke-width="1" fill="none" />
|
||||
<text x="100" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">1</text>
|
||||
<polyline points="130,110 170,110" fill="none" stroke="black" marker-end="url(#arrow)" />
|
||||
|
||||
<circle cx="200" cy="110" r="20" stroke="red" stroke-width="1" fill="red" />
|
||||
<text x="200" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">2</text>
|
||||
<polyline points="230,110 270,110" fill="none" stroke="black" marker-end="url(#arrow)" />
|
||||
|
||||
<circle cx="300" cy="110" r="20" stroke="black" stroke-width="1" fill="none" />
|
||||
<text x="300" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">3</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="background:linear-gradient(#faf3ff, #e8cbfd);padding:20px;margin:0px;display:inline-block; width:97%;align:center;">
|
||||
<h3>Plugin Linky / Enedis pour Openhab</h3>
|
||||
<br/>
|
||||
|
||||
<div style="float:left; margin:30px;">
|
||||
<img src="/connectlinky/img/linky.svg"/>
|
||||
</div>
|
||||
|
||||
<p>Pour utiliser ce plugin, vous devez donner votre accord pour qu'Enedis transmette vos données.</p>
|
||||
|
||||
<p><b>
|
||||
Enedis est le gestionnaire du réseau public de distribution d’électricité sur 95% du territoire français continental.<br/>
|
||||
Enedis gère le réseau d’électricité jusqu’au compteur d’électricité.
|
||||
</b>
|
||||
</p>
|
||||
|
||||
<p>Pour donner votre autorisation, vous devez avoir un compte personnel Enedis. <br/>
|
||||
Ce compte vous permet également de suivre et gérer vos données de consommation [ou production en fonction de votre service] d’électricité.</p>
|
||||
|
||||
<p>Si vous n'avez pas de compte, vous pouvez le créer depuis cette <a href="https://mon-compte-client.enedis.fr/">page</a>.<br/>
|
||||
Munissez-vous pour celà de votre facture d’électricité pour créer votre espace.</p>
|
||||
|
||||
<p>En cliquant sur le bouton ci-dessous, vous allez accéder à votre compte personnel Enedis où vous pourrez donner votre accord pour qu’Enedis nous transmette vos données.</p>
|
||||
<p>Une fois cette opération effectué, vous serez rediriger vers une page de confirmation.</p>
|
||||
|
||||
<div class="button" style="float:right;margin-right:30px;margin-bottom:10px;height:100px;position:relative;display:block;">
|
||||
|
||||
<a href=${authorize.uri}><img src="/connectlinky/img/boutonEnedis.png"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,174 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
${pageRefresh}
|
||||
<title>OpenHAB Linky binding for Enedis</title>
|
||||
<link rel="icon" href="img/favicon.ico" type="image/vnd.microsoft.icon">
|
||||
<link>
|
||||
<style>
|
||||
html {
|
||||
font-family: "Roboto", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
.block {
|
||||
border: 1px solid #bbb;
|
||||
background-color: white;
|
||||
margin: 10px 0;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #FFC0C0;
|
||||
border: 1px solid darkred;
|
||||
color: darkred
|
||||
}
|
||||
|
||||
.authorized {
|
||||
border: 1px solid #90EE90;
|
||||
background-color: #E0FFE0;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-left:30px;
|
||||
margin-bottom: 10px;
|
||||
float:left;
|
||||
}
|
||||
|
||||
.olList {
|
||||
margin-top:20px;
|
||||
}
|
||||
|
||||
.olList li {
|
||||
margin:20px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.box {
|
||||
float:left;
|
||||
transform: translate(0%, -30%);
|
||||
}
|
||||
|
||||
.box select {
|
||||
background-color: #0563af;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
width: 350px;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
|
||||
-webkit-appearance: button;
|
||||
appearance: button;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Style the arrow inside the select element: */
|
||||
.box::before {
|
||||
content: "\f13a";
|
||||
font-family: FontAwesome;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50px;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
line-height: 45px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
.logo {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.logoEnedis {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.logoTransfer {
|
||||
display: inline;
|
||||
margin-left:300px;
|
||||
top: -30px;
|
||||
}
|
||||
|
||||
.button a {
|
||||
background: #1ED760;
|
||||
border-radius: 500px;
|
||||
color: white;
|
||||
padding: 10px 20px 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
border-width: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
<script language="javascript">
|
||||
function retrieveToken()
|
||||
{
|
||||
var prmId = document.getElementById('prmId').value;
|
||||
document.location= "${retrieveToken.uri}" + "&code=" + prmId;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="margin:50px">
|
||||
<div class="logo" style="vertical-align:top;">
|
||||
<div style="display: inline-block;width:500;margin-right:100px;">
|
||||
<img src="/connectlinky/img/openhab_logo.svg" height="100"/>
|
||||
</div>
|
||||
<div style="display: inline-block;width:100;">
|
||||
</div>
|
||||
<div style="display: inline-block;width:300;">
|
||||
<img src="/connectlinky/img/enedis.png" height="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/><br/><br/>
|
||||
<hr style="margin:0px;padding-left:20px;padding-right:20px;width:97%;"/>
|
||||
<div style="display: block;width:400;margin-top:0px;margin-bottom:0px;float:right;top:60px;right:160px;position:absolute;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="400" version="1.1">
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
|
||||
<circle cx="100" cy="110" r="20" stroke="black" stroke-width="1" fill="none" />
|
||||
<text x="100" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">1</text>
|
||||
<polyline points="130,110 170,110" fill="none" stroke="black" marker-end="url(#arrow)" />
|
||||
|
||||
<circle cx="200" cy="110" r="20" stroke="red" stroke-width="1" fill="none" />
|
||||
<text x="200" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">2</text>
|
||||
<polyline points="230,110 270,110" fill="none" stroke="black" marker-end="url(#arrow)" />
|
||||
|
||||
<circle cx="300" cy="110" r="20" stroke="black" stroke-width="1" fill="red" />
|
||||
<text x="300" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">3</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="background:linear-gradient(#faf3ff, #e8cbfd);padding:20px;margin:0px;display:inline-block; width:97%;align:center;">
|
||||
<h3>Plugin Linky / Enedis pour Openhab</h3>
|
||||
<br/>
|
||||
|
||||
<div style="display:${cb.displayConfirmation}">
|
||||
Vous avez autorisé l'accès pour le compteur Linky : ${prmId.Value}<br/>
|
||||
Vous pouvez maintenant utiliser le plugin Linky avec Enedis.<br/>
|
||||
${authorizedUser}
|
||||
</div>
|
||||
|
||||
<div style="display:${cb.displayError}">
|
||||
Une erreur c'est produite:
|
||||
${error}
|
||||
</div>
|
||||
|
||||
|
||||
<p>
|
||||
<br/>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,152 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
${pageRefresh}
|
||||
<title>Authorize openHAB Linky binding for Enedis</title>
|
||||
<link rel="icon" href="img/favicon.ico" type="image/vnd.microsoft.icon">
|
||||
<link>
|
||||
<style>
|
||||
html {
|
||||
font-family: "Roboto", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
.block {
|
||||
border: 1px solid #bbb;
|
||||
background-color: white;
|
||||
margin: 10px 0;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #FFC0C0;
|
||||
border: 1px solid darkred;
|
||||
color: darkred
|
||||
}
|
||||
|
||||
.authorized {
|
||||
border: 1px solid #90EE90;
|
||||
background-color: #E0FFE0;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-left:30px;
|
||||
margin-bottom: 10px;
|
||||
float:left;
|
||||
}
|
||||
|
||||
.olList {
|
||||
margin-top:20px;
|
||||
}
|
||||
|
||||
.olList li {
|
||||
margin:20px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.box {
|
||||
float:left;
|
||||
transform: translate(0%, -30%);
|
||||
}
|
||||
|
||||
.box select {
|
||||
background-color: #0563af;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
width: 350px;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
|
||||
-webkit-appearance: button;
|
||||
appearance: button;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Style the arrow inside the select element: */
|
||||
.box::before {
|
||||
content: "\f13a";
|
||||
font-family: FontAwesome;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50px;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
line-height: 45px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
.logo {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.logoEnedis {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.logoTransfer {
|
||||
display: inline;
|
||||
margin-left:300px;
|
||||
top: -30px;
|
||||
}
|
||||
|
||||
.button a {
|
||||
background: #1ED760;
|
||||
border-radius: 500px;
|
||||
color: white;
|
||||
padding: 10px 20px 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
border-width: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
<script language="javascript">
|
||||
function retrieveToken()
|
||||
{
|
||||
var prmId = document.getElementById('prmId').value;
|
||||
document.location= "${retrieveToken.uri}" + "&code=" + prmId;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="margin:50px">
|
||||
<div class="logo" style="vertical-align:top;">
|
||||
<div style="display: inline-block;width:500;margin-right:100px;margin-top:0px;margin-bottom:0px;">
|
||||
<img src="/connectlinky/img/openhab_logo.svg" height="100"/>
|
||||
</div>
|
||||
<div style="display: inline-block;width:100;margin-top:0px;margin-bottom:0px;">
|
||||
</div>
|
||||
</div>
|
||||
<br/><br/><br/>
|
||||
|
||||
<hr style="margin:0px;padding-left:20px;padding-right:20px;width:97%;"/>
|
||||
<div style="background:linear-gradient(#faf3ff, #e8cbfd);padding:20px;margin:0px;display:inline-block; width:97%;align:center;">
|
||||
<h1>
|
||||
Merci de sélectionner votre provider
|
||||
</h1>
|
||||
|
||||
<div style="float:center;width:100%;">
|
||||
<div style="display: inline-block;width:300;margin:100px;padding:20px;background-color:#ffffff;">
|
||||
<a href="/connectlinky/myelectricaldata"><img src="/connectlinky/img/MyElectricalData.png" height="100"></a>
|
||||
</div>
|
||||
<div style="display: inline-block;width:300;margin:100px;padding:20px;background-color:#ffffff;">
|
||||
<a href="/connectlinky/enedis"><img src="/connectlinky/img/enedis.png" height="100"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,181 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
${pageRefresh}
|
||||
<title>OpenHAB Linky binding for Enedis</title>
|
||||
<link rel="icon" href="img/favicon.ico" type="image/vnd.microsoft.icon">
|
||||
<link>
|
||||
<style>
|
||||
html {
|
||||
font-family: "Roboto", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
.block {
|
||||
border: 1px solid #bbb;
|
||||
background-color: white;
|
||||
margin: 10px 0;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #FFC0C0;
|
||||
border: 1px solid darkred;
|
||||
color: darkred
|
||||
}
|
||||
|
||||
.authorized {
|
||||
border: 1px solid #90EE90;
|
||||
background-color: #E0FFE0;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-left:30px;
|
||||
margin-bottom: 10px;
|
||||
float:left;
|
||||
}
|
||||
|
||||
.olList {
|
||||
margin-top:20px;
|
||||
}
|
||||
|
||||
.olList li {
|
||||
margin:20px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.box {
|
||||
float:left;
|
||||
transform: translate(0%, -30%);
|
||||
}
|
||||
|
||||
.box select {
|
||||
background-color: #0563af;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
width: 350px;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
|
||||
-webkit-appearance: button;
|
||||
appearance: button;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Style the arrow inside the select element: */
|
||||
.box::before {
|
||||
content: "\f13a";
|
||||
font-family: FontAwesome;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50px;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
line-height: 45px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
.logo {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.logoEnedis {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.logoTransfer {
|
||||
display: inline;
|
||||
margin-left:300px;
|
||||
top: -30px;
|
||||
}
|
||||
|
||||
.button a {
|
||||
background: #1ED760;
|
||||
border-radius: 500px;
|
||||
color: white;
|
||||
padding: 10px 20px 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
border-width: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
<script language="javascript">
|
||||
function retrieveToken()
|
||||
{
|
||||
var prmId = document.getElementById('prmId').value;
|
||||
document.location= "${retrieveToken.uri}" + "&code=" + prmId;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="margin:50px">
|
||||
<div class="logo" style="vertical-align:top;">
|
||||
<div style="display: inline-block;width:500;margin-right:100px;margin-top:0px;margin-bottom:0px;">
|
||||
<img src="/connectlinky/img/openhab_logo.svg" height="100"/>
|
||||
</div>
|
||||
<div style="display: inline-block;width:100;margin-top:0px;margin-bottom:0px;">
|
||||
</div>
|
||||
<div style="display: inline-block;width:300;margin-top:0px;margin-bottom:0px;">
|
||||
<img src="/connectlinky/img/MyElectricalData.png" height="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/><br/><br/>
|
||||
<hr style="margin:0px;padding-left:20px;padding-right:20px;width:97%;"/>
|
||||
<div style="display: block;width:400;margin-top:0px;margin-bottom:0px;float:right;top:60px;right:160px;position:absolute;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="400" version="1.1">
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
|
||||
<circle cx="100" cy="110" r="20" stroke="black" stroke-width="1" fill="red" />
|
||||
<text x="100" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">1</text>
|
||||
<polyline points="130,110 170,110" fill="none" stroke="black" marker-end="url(#arrow)" />
|
||||
|
||||
<circle cx="200" cy="110" r="20" stroke="red" stroke-width="1" fill="none" />
|
||||
<text x="200" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">2</text>
|
||||
<polyline points="230,110 270,110" fill="none" stroke="black" marker-end="url(#arrow)" />
|
||||
|
||||
<circle cx="300" cy="110" r="20" stroke="black" stroke-width="1" fill="none" />
|
||||
<text x="300" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">3</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="background:linear-gradient(#faf3ff, #e8cbfd);padding:20px;margin:0px;display:inline-block; width:97%;align:center;">
|
||||
<h3>Plugin Linky / MyElectricalData pour Openhab</h3>
|
||||
<br/>
|
||||
|
||||
<p>Ce plugin permet d'exploiter vos données de consommation éléctrique fournis par Enedis au sein d'OpenHab.</p>
|
||||
|
||||
<p>Enedis gère le réseau d’électricité jusqu’au compteur d’électricité<br/>
|
||||
Enedis est le gestionnaire du réseau public de distribution d’électricité sur 95% du territoire français continental.</p>
|
||||
|
||||
<p>Grace à ce plugin, vous serez en mesure de : </p>
|
||||
|
||||
<ul>
|
||||
<li>Consulter les informations contractuelles liés à votre compte.</li>
|
||||
<li>Créer des graphes de consommation par jour / semaine / mois / annéee.</li>
|
||||
<li>Consulter la puissance maximum utilisé sur une période donnée.</li>
|
||||
<li>Load curve</li>
|
||||
</ul>
|
||||
|
||||
<br/><br/>
|
||||
<div>
|
||||
<div class="button">
|
||||
<a href="/connectlinky/myelectricaldata-step2">Suite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,169 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
${pageRefresh}
|
||||
<title>OpenHAB Linky binding for Enedis</title>
|
||||
<link rel="icon" href="img/favicon.ico" type="image/vnd.microsoft.icon">
|
||||
<link>
|
||||
<style>
|
||||
html {
|
||||
font-family: "Roboto", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
.block {
|
||||
border: 1px solid #bbb;
|
||||
background-color: white;
|
||||
margin: 10px 0;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #FFC0C0;
|
||||
border: 1px solid darkred;
|
||||
color: darkred
|
||||
}
|
||||
|
||||
.authorized {
|
||||
border: 1px solid #90EE90;
|
||||
background-color: #E0FFE0;
|
||||
}
|
||||
|
||||
.olList {
|
||||
margin-top:20px;
|
||||
}
|
||||
|
||||
.olList li {
|
||||
margin:20px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.box {
|
||||
float:left;
|
||||
transform: translate(0%, -30%);
|
||||
}
|
||||
|
||||
.box select {
|
||||
background-color: #0563af;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
width: 350px;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
|
||||
-webkit-appearance: button;
|
||||
appearance: button;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Style the arrow inside the select element: */
|
||||
.box::before {
|
||||
content: "\f13a";
|
||||
font-family: FontAwesome;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50px;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
line-height: 45px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
.logo {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.logoEnedis {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.logoTransfer {
|
||||
display: inline;
|
||||
margin-left:300px;
|
||||
top: -30px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
<script language="javascript">
|
||||
function retrieveToken()
|
||||
{
|
||||
var prmId = document.getElementById('prmId').value;
|
||||
document.location= "${retrieveToken.uri}" + "&code=" + prmId;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="margin:50px">
|
||||
<div class="logo" style="vertical-align:top;">
|
||||
<div style="display: inline-block;width:500;margin-right:100px;">
|
||||
<img src="/connectlinky/img/openhab_logo.svg" height="100"/>
|
||||
</div>
|
||||
<div style="display: inline-block;width:100;">
|
||||
</div>
|
||||
<div style="display: inline-block;width:300;">
|
||||
<img src="/connectlinky/img/MyElectricalData.png" height="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/><br/><br/>
|
||||
<hr style="margin:0px;padding-left:20px;padding-right:20px;width:97%;"/>
|
||||
<div style="display: block;width:400;margin-top:0px;margin-bottom:0px;float:right;top:60px;right:160px;position:absolute;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="400" version="1.1">
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
|
||||
<circle cx="100" cy="110" r="20" stroke="black" stroke-width="1" fill="none" />
|
||||
<text x="100" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">1</text>
|
||||
<polyline points="130,110 170,110" fill="none" stroke="black" marker-end="url(#arrow)" />
|
||||
|
||||
<circle cx="200" cy="110" r="20" stroke="red" stroke-width="1" fill="red" />
|
||||
<text x="200" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">2</text>
|
||||
<polyline points="230,110 270,110" fill="none" stroke="black" marker-end="url(#arrow)" />
|
||||
|
||||
<circle cx="300" cy="110" r="20" stroke="black" stroke-width="1" fill="none" />
|
||||
<text x="300" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">3</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="background:linear-gradient(#faf3ff, #e8cbfd);padding:20px;margin:0px;display:inline-block; width:97%;align:center;">
|
||||
<h3>Plugin Linky / MyElectricalData pour Openhab</h3>
|
||||
<br/>
|
||||
|
||||
<div style="float:left; margin:30px;">
|
||||
<img src="/connectlinky/img/linky.svg"/>
|
||||
</div>
|
||||
|
||||
<p>Pour utiliser ce plugin, vous devez donner votre accord pour qu'Enedis transmette vos données.</p>
|
||||
|
||||
<p>Pour donner votre autorisation, vous devez avoir un compte personnel Enedis. <br/>
|
||||
Ce compte vous permet également de suivre et gérer vos données de consommation [ou production en fonction de votre service] d’électricité.</p>
|
||||
|
||||
<p>Si vous n'avez pas de compte, vous pouvez le créer depuis cette <a href="https://mon-compte-client.enedis.fr/">page</a>.<br/>
|
||||
Munissez-vous pour celà de votre facture d’électricité pour créer votre espace.</p>
|
||||
|
||||
<p>En cliquant sur le bouton ci-dessous, vous allez accéder à votre compte personnel Enedis où vous pourrez donner votre accord pour qu’Enedis nous transmette vos données.</p>
|
||||
<p>Une fois cette opération effectué, vous devez vous rendre manuellement sur cette <a href="/connectlinky/myelectricaldata-step3">page</a> pour terminer la procédure.</p>
|
||||
|
||||
|
||||
<div class="button" style="float:right;margin-right:30px;margin-bottom:10px;height:100px;position:relative;display:block;">
|
||||
|
||||
<a href=${authorize.uri}><img src="/connectlinky/img/boutonEnedis.png"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,201 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
${pageRefresh}
|
||||
<title>OpenHAB Linky binding for Enedis</title>
|
||||
<link rel="icon" href="img/favicon.ico" type="image/vnd.microsoft.icon">
|
||||
<link>
|
||||
<style>
|
||||
html {
|
||||
font-family: "Roboto", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
.block {
|
||||
border: 1px solid #bbb;
|
||||
background-color: white;
|
||||
margin: 10px 0;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #FFC0C0;
|
||||
border: 1px solid darkred;
|
||||
color: darkred
|
||||
}
|
||||
|
||||
.authorized {
|
||||
border: 1px solid #90EE90;
|
||||
background-color: #E0FFE0;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-left:30px;
|
||||
margin-bottom: 10px;
|
||||
float:left;
|
||||
}
|
||||
|
||||
.olList {
|
||||
margin-top:20px;
|
||||
}
|
||||
|
||||
.olList li {
|
||||
margin:20px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.box {
|
||||
float:left;
|
||||
transform: translate(0%, -30%);
|
||||
}
|
||||
|
||||
.box select {
|
||||
background-color: #0563af;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
width: 350px;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
|
||||
-webkit-appearance: button;
|
||||
appearance: button;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Style the arrow inside the select element: */
|
||||
.box::before {
|
||||
content: "\f13a";
|
||||
font-family: FontAwesome;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50px;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
line-height: 45px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
.logo {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.logoEnedis {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.logoTransfer {
|
||||
display: inline;
|
||||
margin-left:300px;
|
||||
top: -30px;
|
||||
}
|
||||
|
||||
.button a {
|
||||
background: #1ED760;
|
||||
border-radius: 500px;
|
||||
color: white;
|
||||
padding: 10px 20px 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
border-width: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
<script language="javascript">
|
||||
function retrieveToken()
|
||||
{
|
||||
var prmId = document.getElementById('prmId').value;
|
||||
document.location= "${retrieveToken.uri}" + "&code=" + prmId;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="margin:50px">
|
||||
<div class="logo" style="vertical-align:top;">
|
||||
<div style="display: inline-block;width:500;margin-right:100px;">
|
||||
<img src="/connectlinky/img/openhab_logo.svg" height="100"/>
|
||||
</div>
|
||||
<div style="display: inline-block;width:100;">
|
||||
</div>
|
||||
<div style="display: inline-block;width:300;">
|
||||
<img src="/connectlinky/img/MyElectricalData.png" height="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/><br/><br/>
|
||||
<hr style="margin:0px;padding-left:20px;padding-right:20px;width:97%;"/>
|
||||
<div style="display: block;width:400;margin-top:0px;margin-bottom:0px;float:right;top:60px;right:160px;position:absolute;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="400" version="1.1">
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
|
||||
<circle cx="100" cy="110" r="20" stroke="black" stroke-width="1" fill="none" />
|
||||
<text x="100" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">1</text>
|
||||
<polyline points="130,110 170,110" fill="none" stroke="black" marker-end="url(#arrow)" />
|
||||
|
||||
<circle cx="200" cy="110" r="20" stroke="red" stroke-width="1" fill="none" />
|
||||
<text x="200" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">2</text>
|
||||
<polyline points="230,110 270,110" fill="none" stroke="black" marker-end="url(#arrow)" />
|
||||
|
||||
<circle cx="300" cy="110" r="20" stroke="black" stroke-width="1" fill="red" />
|
||||
<text x="300" y="110" stroke="#51c5cf" stroke-width="2px" dy=".3em" text-anchor="middle">3</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="background:linear-gradient(#faf3ff, #e8cbfd);padding:20px;margin:0px;display:inline-block; width:97%;align:center;">
|
||||
<h3>Plugin Linky / MyElectricalData pour Openhab</h3>
|
||||
<br/>
|
||||
|
||||
<div style="display:${cb.displayConfirmation}">
|
||||
Vous pouvez maintenant utiliser le plugin Linky avec MyElectricalData.
|
||||
${authorizedUser}
|
||||
</div>
|
||||
|
||||
<div style="display:${cb.displayError}">
|
||||
Une erreur c'est produite:
|
||||
${error}
|
||||
</div>
|
||||
|
||||
<div style="display:${cb.displayInstruction}">
|
||||
<p>
|
||||
Vous devez maintenant récupérer le token depuis MyElectricalData.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Pour ce faire :
|
||||
<ul>
|
||||
<li>Sélectionner le numéro de prmId dans la combobox ci-dessous.</li>
|
||||
<li>Cliquer sur le bouton "Retrive token".</li>
|
||||
</ul>
|
||||
</p>
|
||||
<br/>
|
||||
|
||||
<br/><br/><br/>
|
||||
<div class="block${bridge.authorized}">
|
||||
<br/>
|
||||
<div>
|
||||
<div class="box">
|
||||
<b>Please select your prmId :</b>
|
||||
<select id="prmId">
|
||||
${prmId.Option}
|
||||
</select>
|
||||
</div>
|
||||
<div class="button">
|
||||
<a href="javascript:retrieveToken()">Retrieve token
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,76 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="88px" height="130px" viewBox="0 0 88 130" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: sketchtool 54.1 (76490) - https://sketchapp.com -->
|
||||
<title>A0924594-7213-430D-A943-FE07E1B6E747</title>
|
||||
<desc>Created with sketchtool.</desc>
|
||||
<defs>
|
||||
<polygon id="path-1" points="0.110730673 0.170093458 86.2396006 0.170093458 86.2396006 129.793739 0.110730673 129.793739"></polygon>
|
||||
<polygon id="path-3" points="0.325427204 0.129727087 48.7098019 0.129727087 48.7098019 56.2562126 0.325427204 56.2562126"></polygon>
|
||||
<path d="M0.196850252,0.568317324 L0.196850252,4.99144282 C0.196850252,5.15434811 0.336285847,5.2833148 0.506637026,5.28578306 L0.506637026,5.28578306 L8.31313756,5.35242613 C8.48285781,5.35242613 8.6222934,5.22099118 8.6222934,5.06055415 L8.6222934,5.06055415 L8.6222934,0.645450511 C8.6222934,0.488098809 8.48285781,0.362834513 8.31313756,0.35913212 L8.31313756,0.35913212 L0.506637026,0.280764802 C0.336285847,0.28508426 0.196850252,0.412199753 0.196850252,0.568317324 L0.196850252,0.568317324 Z" id="path-5"></path>
|
||||
<path d="M0.196850252,0.568317324 L0.196850252,4.99144282 C0.196850252,5.15434811 0.336285847,5.2833148 0.506637026,5.28578306 L0.506637026,5.28578306 L8.31313756,5.35242613 C8.48285781,5.35242613 8.6222934,5.22099118 8.6222934,5.06055415 L8.6222934,5.06055415 L8.6222934,0.645450511 C8.6222934,0.488098809 8.48285781,0.362834513 8.31313756,0.35913212 L8.31313756,0.35913212 L0.506637026,0.280764802 C0.336285847,0.28508426 0.196850252,0.412199753 0.196850252,0.568317324" id="path-7"></path>
|
||||
<polygon id="path-9" points="0.363135436 4.99082575 1.58601855 4.98835749 1.58601855 0.284467195 0.363135436 0.271508819"></polygon>
|
||||
</defs>
|
||||
<g id="1---CONSOMMATION" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="1-SDM---J-5.1---Pmax-Tableau" transform="translate(-354.000000, -281.000000)">
|
||||
<g id="Emilie-/-SDM-/-Compteur-Nav-gauche" transform="translate(265.000000, 265.000000)">
|
||||
<g id="Group-3">
|
||||
<g id="Atome-/-illustration-/-Compteur-/-Linky" transform="translate(87.000000, 16.000000)">
|
||||
<g id="Group-61" transform="translate(2.816327, 0.000000)">
|
||||
<g id="Group-10">
|
||||
<g id="Group-3">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<g id="Clip-2"></g>
|
||||
<path d="M86.2396006,125.153794 C86.2396006,127.724636 83.7948891,129.804028 80.7559186,129.793701 L5.57894389,129.683748 C2.56124301,129.681318 0.110730673,127.59585 0.110730673,125.035944 L0.110730673,4.80052336 C0.110730673,2.2412243 2.56124301,0.156364486 5.57894389,0.169728972 L80.7559186,0.277859813 C83.7948891,0.292439252 86.2396006,2.36757944 86.2396006,4.92748598 L86.2396006,125.153794 Z" id="Fill-1" fill="#CCD41F" mask="url(#mask-2)"></path>
|
||||
</g>
|
||||
<path d="M68.6672615,86.6276869 C68.6672615,89.3388551 66.0730185,91.5373131 62.8761376,91.5415654 L18.4530282,91.4692757 C15.2548582,91.4692757 12.6515917,89.2550234 12.6515917,86.5353505 L12.6515917,31.9049766 C12.6515917,29.1871262 15.2548582,26.9825935 18.4530282,26.9917056 L62.8761376,27.0573131 C66.0730185,27.0621729 68.6672615,29.268528 68.6672615,31.9948832 L68.6672615,86.6276869 Z" id="Fill-4" fill="#B7C243"></path>
|
||||
<path d="M68.6672615,86.6276869 C68.6672615,89.3388551 66.0730185,91.5373131 62.8761376,91.5415654 L18.4530282,91.4692757 C15.2548582,91.4692757 12.6515917,89.2550234 12.6515917,86.5353505 L12.6515917,31.9049766 C12.6515917,29.1871262 15.2548582,26.9825935 18.4530282,26.9917056 L62.8761376,27.0573131 C66.0730185,27.0621729 68.6672615,29.268528 68.6672615,31.9948832 L68.6672615,86.6276869 Z" id="Stroke-6" stroke="#B7C243" stroke-width="0.365"></path>
|
||||
<path d="M69.6296774,82.3895654 C69.6296774,84.9537243 67.1765869,87.0227897 64.1530852,87.0312944 L22.1966015,86.9650794 C19.1730998,86.9602196 16.7129195,84.8729299 16.7129195,82.3008738 L16.7129195,30.7012009 C16.7129195,28.1346121 19.1730998,26.0527897 22.1966015,26.0588645 L64.1530852,26.1208271 C67.1765869,26.1226495 69.6296774,28.2069019 69.6296774,30.7844252 L69.6296774,82.3895654 Z" id="Fill-8" fill="#CFCEC4"></path>
|
||||
</g>
|
||||
<g id="Group-13" transform="translate(18.647495, 27.318841)">
|
||||
<mask id="mask-4" fill="white">
|
||||
<use xlink:href="#path-3"></use>
|
||||
</mask>
|
||||
<g id="Clip-12"></g>
|
||||
<path d="M48.7098019,51.988681 C48.7098019,54.3470215 46.4711985,56.2519292 43.7082957,56.2562126 L5.33952434,56.2029755 C2.57855861,56.1937967 0.32510436,54.2711434 0.32510436,51.9066837 L0.32510436,4.40576441 C0.32510436,2.03946891 2.57855861,0.129665895 5.33952434,0.129665895 L43.7082957,0.190245997 C46.4711985,0.19820096 48.7098019,2.11228742 48.7098019,4.4810306 L48.7098019,51.988681 Z" id="Fill-11" fill="#F9F9F7" mask="url(#mask-4)"></path>
|
||||
</g>
|
||||
<g id="Group-26" transform="translate(16.684601, 25.434783)">
|
||||
<polygon id="Stroke-14" stroke="#7F8074" stroke-width="0.219" points="25.6782204 61.0846162 12.9698822 61.1106135 12.9698822 53.2775704 25.6782204 53.2449227"></polygon>
|
||||
<polygon id="Stroke-16" stroke="#7F8074" stroke-width="0.219" points="42.2035583 61.1719792 29.5067872 61.2028132 29.5067872 53.3540508 42.2035583 53.3365178"></polygon>
|
||||
<polygon id="Fill-18" fill="#9AA3AB" points="9.48357167 28.6337747 9.48357167 40.2285647 45.1190989 40.2811639 45.1190989 28.6797234"></polygon>
|
||||
<path d="M9.48357167,52.0046103 C9.48357167,52.6950498 10.1551035,53.2579213 10.9673678,53.2645718 L43.6301618,53.3099159 C44.4507801,53.3147526 45.1190989,52.7409985 45.1190989,52.0439085 L45.1190989,40.281043 L9.48357167,40.2284438 L9.48357167,52.0046103 Z" id="Fill-20" fill="#4F4F4F"></path>
|
||||
<path d="M45.1191631,17.4799108 C45.1191631,16.7767748 44.4508444,16.2157171 43.6302261,16.2247859 L10.9667895,16.1746051 C10.1551678,16.1613042 9.48299332,16.739895 9.48299332,17.4363804 L9.48299332,28.6339561 L45.1191631,28.6799048 L45.1191631,17.4799108 Z" id="Fill-22" fill="#4F4F4F"></path>
|
||||
<path d="M53.3570915,56.6051023 C53.3570915,59.1570685 50.9113019,61.2162955 47.8967987,61.2247597 L6.0651855,61.1588596 C3.05068234,61.1540229 0.59782398,59.0766583 0.59782398,56.5168324 L0.59782398,5.16251708 C0.59782398,2.60813248 3.05068234,0.53620916 6.0651855,0.542255041 L47.8967987,0.603923024 C50.9113019,0.605736788 53.3570915,2.68007846 53.3570915,5.24534564 L53.3570915,56.6051023 Z" id="Stroke-24" stroke="#B7C243" stroke-width="0.365"></path>
|
||||
</g>
|
||||
<g id="Group-33" transform="translate(38.276438, 114.927536)">
|
||||
<g id="Group-29">
|
||||
<mask id="mask-6" fill="white">
|
||||
<use xlink:href="#path-5"></use>
|
||||
</mask>
|
||||
<g id="Clip-28"></g>
|
||||
<path d="M8.62172556,5.06061586 C8.62172556,5.22105289 8.4829209,5.35248784 8.31320065,5.35248784 L0.506069189,5.28584476 C0.33634894,5.2833765 0.196282415,5.15440981 0.196282415,4.99150452 L0.196282415,0.56837903 C0.196282415,0.412261459 0.33634894,0.285763032 0.506069189,0.280826508 L8.31320065,0.359193827 C8.4829209,0.363513285 8.62172556,0.488160516 8.62172556,0.646746349 L8.62172556,5.06061586 Z" id="Fill-27" fill="#4F4F4F" mask="url(#mask-6)"></path>
|
||||
</g>
|
||||
<g id="Group-32">
|
||||
<mask id="mask-8" fill="white">
|
||||
<use xlink:href="#path-7"></use>
|
||||
</mask>
|
||||
<g id="Clip-31"></g>
|
||||
<path d="M6.60445213,-0.206716942 L5.19432293,-0.220292383 L5.19432293,0.573253848 C5.19432293,1.10578137 4.84100196,1.53711016 4.40250541,1.5290883 C3.97410374,1.5290883 3.6214137,1.09158887 3.6214137,0.563997865 L3.6214137,-0.240655544 L2.2093917,-0.255465116 C2.12043053,-0.249294461 2.03525494,-0.187587911 2.03525494,-0.0931768902 L2.03525494,2.39606533 C2.03525494,2.47813504 2.12043053,2.55650236 2.2093917,2.55650236 L6.60445213,2.59414335 C6.70035354,2.59414335 6.77606517,2.52256376 6.77606517,2.4287698 L6.77606517,-0.0505993709 C6.77606517,-0.138222672 6.70035354,-0.206716942 6.60445213,-0.206716942" id="Fill-30" fill="#FEFEFE" mask="url(#mask-8)"></path>
|
||||
</g>
|
||||
</g>
|
||||
<polygon id="Fill-34" fill="#4F4F4F" points="42.2022263 113.967631 42.2022263 109.275362 44.1651206 109.275362 44.1651206 113.985507"></polygon>
|
||||
<g id="Group-38" transform="translate(42.202226, 119.637681)">
|
||||
<mask id="mask-10" fill="white">
|
||||
<use xlink:href="#path-9"></use>
|
||||
</mask>
|
||||
<g id="Clip-37"></g>
|
||||
<polygon id="Fill-36" mask="url(#mask-10)" points="0.363135436 4.99082575 1.58601855 4.98835749 1.58601855 0.284467195 0.363135436 0.271508819"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 113 KiB |
|
@ -0,0 +1,332 @@
|
|||
/*
|
||||
* 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.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.openhab.binding.linky.internal.config.LinkyThingRemoteConfiguration;
|
||||
import org.openhab.binding.linky.internal.dto.IntervalReading;
|
||||
import org.openhab.binding.linky.internal.dto.MeterReading;
|
||||
import org.openhab.core.i18n.LocaleProvider;
|
||||
import org.openhab.core.i18n.TimeZoneProvider;
|
||||
import org.openhab.core.thing.Thing;
|
||||
|
||||
/**
|
||||
* The {@link ThingLinkyRemoteHandler} is responsible for extra validation for Raw things.
|
||||
*
|
||||
* @author Laurent Arnal - Initial contribution
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class ThingLinkyRemoteHandlerTest {
|
||||
|
||||
private ThingLinkyRemoteHandler handler;
|
||||
|
||||
@Mock
|
||||
LocaleProvider localProvider;
|
||||
|
||||
@Mock
|
||||
Thing thing;
|
||||
|
||||
@Mock
|
||||
TimeZoneProvider tzProvider;
|
||||
|
||||
@Mock
|
||||
LinkyThingRemoteConfiguration config;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
when(thing.getConfiguration()).thenReturn(new LinkyThingRemoteConfiguration());
|
||||
}
|
||||
|
||||
/*
|
||||
* @AfterAll
|
||||
* public void tearDown() {
|
||||
* // handler.dispose();
|
||||
* }
|
||||
*/
|
||||
|
||||
public @Nullable MeterReading getMeterReadingAfterChecks(ThingLinkyRemoteHandler handler,
|
||||
@Nullable MeterReading meterReading) {
|
||||
return handler.getMeterReadingAfterChecks(meterReading);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBase() {
|
||||
handler = new ThingLinkyRemoteHandler(thing, localProvider, tzProvider);
|
||||
|
||||
MeterReading mr = getMeterReadingAfterChecks(handler, null);
|
||||
assertEquals(mr, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidRange1() {
|
||||
handler = new ThingLinkyRemoteHandler(thing, localProvider, tzProvider);
|
||||
|
||||
MeterReading mr = new MeterReading();
|
||||
mr.baseValue = new IntervalReading[75];
|
||||
LocalDateTime startDate = LocalDateTime.of(2025, 1, 1, 0, 0, 0);
|
||||
for (int idx = 0; idx < 75; idx++) {
|
||||
mr.baseValue[idx] = new IntervalReading();
|
||||
mr.baseValue[idx].value = Double.valueOf(idx);
|
||||
mr.baseValue[idx].date = startDate.plusDays(idx);
|
||||
}
|
||||
|
||||
mr = getMeterReadingAfterChecks(handler, mr);
|
||||
assertNotEquals(mr, null);
|
||||
if (mr == null) {
|
||||
return;
|
||||
}
|
||||
assertNotEquals(mr.weekValue, null);
|
||||
assertNotEquals(mr.monthValue, null);
|
||||
assertNotEquals(mr.yearValue, null);
|
||||
|
||||
assertEquals(mr.weekValue.length, 11);
|
||||
assertEquals(mr.monthValue.length, 3);
|
||||
assertEquals(mr.yearValue.length, 1);
|
||||
|
||||
assertEquals(mr.weekValue[0].value, 10);
|
||||
|
||||
assertEquals(mr.weekValue[1].value, 56);
|
||||
assertEquals(mr.weekValue[1].date, LocalDateTime.of(2025, 1, 6, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[4].value, 203);
|
||||
assertEquals(mr.weekValue[4].date, LocalDateTime.of(2025, 1, 27, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[10].value, 497);
|
||||
assertEquals(mr.weekValue[10].date, LocalDateTime.of(2025, 3, 10, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[0].value, 465);
|
||||
assertEquals(mr.monthValue[0].date, LocalDateTime.of(2025, 1, 1, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[1].value, 1246);
|
||||
assertEquals(mr.monthValue[1].date, LocalDateTime.of(2025, 2, 1, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[2].value, 1064);
|
||||
assertEquals(mr.monthValue[2].date, LocalDateTime.of(2025, 3, 1, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.yearValue[0].value, 2775);
|
||||
assertEquals(mr.yearValue[0].date, LocalDateTime.of(2025, 1, 1, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidRange2() {
|
||||
handler = new ThingLinkyRemoteHandler(thing, localProvider, tzProvider);
|
||||
|
||||
MeterReading mr = new MeterReading();
|
||||
mr.baseValue = new IntervalReading[128];
|
||||
LocalDateTime startDate = LocalDateTime.of(2024, 11, 6, 0, 0, 0);
|
||||
for (int idx = 0; idx < 128; idx++) {
|
||||
mr.baseValue[idx] = new IntervalReading();
|
||||
mr.baseValue[idx].value = Double.valueOf(idx);
|
||||
mr.baseValue[idx].date = startDate.plusDays(idx);
|
||||
}
|
||||
|
||||
mr = getMeterReadingAfterChecks(handler, mr);
|
||||
assertNotEquals(mr, null);
|
||||
if (mr == null) {
|
||||
return;
|
||||
}
|
||||
assertNotEquals(mr.weekValue, null);
|
||||
assertNotEquals(mr.monthValue, null);
|
||||
assertNotEquals(mr.yearValue, null);
|
||||
|
||||
assertEquals(mr.weekValue.length, 19);
|
||||
assertEquals(mr.monthValue.length, 5);
|
||||
assertEquals(mr.yearValue.length, 2);
|
||||
|
||||
assertEquals(mr.weekValue[0].value, 10);
|
||||
assertEquals(mr.weekValue[0].date, LocalDateTime.of(2024, 11, 6, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[2].value, 105);
|
||||
assertEquals(mr.weekValue[2].date, LocalDateTime.of(2024, 11, 18, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[6].value, 301);
|
||||
assertEquals(mr.weekValue[6].date, LocalDateTime.of(2024, 12, 16, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[8].value, 399);
|
||||
assertEquals(mr.weekValue[8].date, LocalDateTime.of(2024, 12, 30, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[12].value, 595);
|
||||
assertEquals(mr.weekValue[12].date, LocalDateTime.of(2025, 01, 27, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[18].value, 502);
|
||||
assertEquals(mr.weekValue[18].date, LocalDateTime.of(2025, 03, 10, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[0].value, 300);
|
||||
assertEquals(mr.monthValue[0].date, LocalDateTime.of(2024, 11, 1, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[1].value, 1240);
|
||||
assertEquals(mr.monthValue[1].date, LocalDateTime.of(2024, 12, 1, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[2].value, 2201);
|
||||
assertEquals(mr.monthValue[2].date, LocalDateTime.of(2025, 1, 1, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[3].value, 2814);
|
||||
assertEquals(mr.monthValue[3].date, LocalDateTime.of(2025, 2, 1, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[4].value, 1573);
|
||||
assertEquals(mr.monthValue[4].date, LocalDateTime.of(2025, 3, 1, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.yearValue[0].value, 1540);
|
||||
assertEquals(mr.yearValue[0].date, LocalDateTime.of(2024, 1, 1, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.yearValue[1].value, 6588);
|
||||
assertEquals(mr.yearValue[1].date, LocalDateTime.of(2025, 1, 1, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidRange3() {
|
||||
handler = new ThingLinkyRemoteHandler(thing, localProvider, tzProvider);
|
||||
|
||||
MeterReading mr = new MeterReading();
|
||||
mr.baseValue = new IntervalReading[716];
|
||||
LocalDateTime startDate = LocalDateTime.of(2023, 03, 29, 0, 0, 0);
|
||||
for (int idx = 0; idx < 716; idx++) {
|
||||
mr.baseValue[idx] = new IntervalReading();
|
||||
mr.baseValue[idx].value = Double.valueOf(idx);
|
||||
mr.baseValue[idx].date = startDate.plusDays(idx);
|
||||
}
|
||||
|
||||
mr = getMeterReadingAfterChecks(handler, mr);
|
||||
assertNotEquals(mr, null);
|
||||
if (mr == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
assertNotEquals(mr.weekValue, null);
|
||||
assertNotEquals(mr.monthValue, null);
|
||||
assertNotEquals(mr.yearValue, null);
|
||||
|
||||
assertEquals(mr.weekValue.length, 103);
|
||||
assertEquals(mr.monthValue.length, 25);
|
||||
assertEquals(mr.yearValue.length, 3);
|
||||
|
||||
assertEquals(mr.weekValue[0].value, 10);
|
||||
assertEquals(mr.weekValue[0].date, LocalDateTime.of(2023, 03, 29, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[2].value, 105);
|
||||
assertEquals(mr.weekValue[2].date, LocalDateTime.of(2023, 04, 10, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[15].value, 742);
|
||||
assertEquals(mr.weekValue[15].date, LocalDateTime.of(2023, 07, 10, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[39].value, 1918);
|
||||
assertEquals(mr.weekValue[39].date, LocalDateTime.of(2023, 12, 25, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[56].value, 2751);
|
||||
assertEquals(mr.weekValue[56].date, LocalDateTime.of(2024, 04, 22, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[90].value, 4417);
|
||||
assertEquals(mr.weekValue[90].date, LocalDateTime.of(2024, 12, 16, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[97].value, 4760);
|
||||
assertEquals(mr.weekValue[97].date, LocalDateTime.of(2025, 02, 03, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[102].value, 2854);
|
||||
assertEquals(mr.weekValue[102].date, LocalDateTime.of(2025, 03, 10, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[0].value, 3);
|
||||
assertEquals(mr.monthValue[0].date, LocalDateTime.of(2023, 03, 01, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[5].value, 4340);
|
||||
assertEquals(mr.monthValue[5].date, LocalDateTime.of(2023, 8, 01, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[9].value, 8122);
|
||||
assertEquals(mr.monthValue[9].date, LocalDateTime.of(2023, 12, 1, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[10].value, 9083);
|
||||
assertEquals(mr.monthValue[10].date, LocalDateTime.of(2024, 01, 01, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[17].value, 15686);
|
||||
assertEquals(mr.monthValue[17].date, LocalDateTime.of(2024, 8, 01, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[22].value, 20429);
|
||||
assertEquals(mr.monthValue[22].date, LocalDateTime.of(2025, 01, 01, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[23].value, 19278);
|
||||
assertEquals(mr.monthValue[23].date, LocalDateTime.of(2025, 02, 01, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[24].value, 9217);
|
||||
assertEquals(mr.monthValue[24].date, LocalDateTime.of(2025, 03, 01, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.yearValue[0].value, 38503);
|
||||
assertEquals(mr.yearValue[0].date, LocalDateTime.of(2023, 1, 1, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.yearValue[1].value, 168543);
|
||||
assertEquals(mr.yearValue[1].date, LocalDateTime.of(2024, 1, 1, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.yearValue[2].value, 48924);
|
||||
assertEquals(mr.yearValue[2].date, LocalDateTime.of(2025, 1, 1, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidRange4() {
|
||||
handler = new ThingLinkyRemoteHandler(thing, localProvider, tzProvider);
|
||||
|
||||
MeterReading mr = new MeterReading();
|
||||
mr.baseValue = new IntervalReading[35];
|
||||
LocalDateTime startDate = LocalDateTime.of(2025, 02, 10, 0, 0, 0);
|
||||
for (int idx = 0; idx < 35; idx++) {
|
||||
mr.baseValue[idx] = new IntervalReading();
|
||||
mr.baseValue[idx].value = Double.valueOf(idx);
|
||||
mr.baseValue[idx].date = startDate.plusDays(idx);
|
||||
}
|
||||
|
||||
mr = getMeterReadingAfterChecks(handler, mr);
|
||||
assertNotEquals(mr, null);
|
||||
if (mr == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
assertNotEquals(mr.weekValue, null);
|
||||
assertNotEquals(mr.monthValue, null);
|
||||
assertNotEquals(mr.yearValue, null);
|
||||
|
||||
assertEquals(mr.weekValue.length, 5);
|
||||
assertEquals(mr.monthValue.length, 2);
|
||||
assertEquals(mr.yearValue.length, 1);
|
||||
|
||||
assertEquals(mr.weekValue[0].value, 21);
|
||||
assertEquals(mr.weekValue[0].date, LocalDateTime.of(2025, 2, 10, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[1].value, 70);
|
||||
assertEquals(mr.weekValue[1].date, LocalDateTime.of(2025, 2, 17, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[2].value, 119);
|
||||
assertEquals(mr.weekValue[2].date, LocalDateTime.of(2025, 2, 24, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[3].value, 168);
|
||||
assertEquals(mr.weekValue[3].date, LocalDateTime.of(2025, 3, 3, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.weekValue[4].value, 217);
|
||||
assertEquals(mr.weekValue[4].date, LocalDateTime.of(2025, 3, 10, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[0].value, 171);
|
||||
assertEquals(mr.monthValue[0].date, LocalDateTime.of(2025, 2, 1, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.monthValue[1].value, 424);
|
||||
assertEquals(mr.monthValue[1].date, LocalDateTime.of(2025, 3, 1, 0, 0, 0));
|
||||
|
||||
assertEquals(mr.yearValue[0].value, 595);
|
||||
assertEquals(mr.yearValue[0].date, LocalDateTime.of(2025, 1, 1, 0, 0, 0));
|
||||
}
|
||||
}
|