[sedif] Initial contribution (#18718)

* add new addon to import water consumption from sedif website !

Signed-off-by: Laurent ARNAL <laurent@clae.net>
Co-authored-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
lo92fr
2025-12-14 10:01:26 +01:00
committed by GitHub
co-authored by Laurent Garnier
parent ee974395a8
commit b6e2fc9e52
47 changed files with 4006 additions and 0 deletions
+1
View File
@@ -348,6 +348,7 @@
/bundles/org.openhab.binding.samsungtv/ @NickWaterton
/bundles/org.openhab.binding.satel/ @druciak
/bundles/org.openhab.binding.sbus/ @cipianpascu
/bundles/org.openhab.binding.sedif/ @lo92fr
/bundles/org.openhab.binding.semsportal/ @itb3
/bundles/org.openhab.binding.senechome/ @vctender @KorbinianP @eguib
/bundles/org.openhab.binding.seneye/ @nikotanghe
+5
View File
@@ -1721,6 +1721,11 @@
<artifactId>org.openhab.binding.sbus</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.sedif</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.semsportal</artifactId>
+15
View File
@@ -0,0 +1,15 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons
== Third-party Content
+221
View File
@@ -0,0 +1,221 @@
# Sedif Binding
This binding enables you to retrieve water consumption data for consumers in the Île-de-France region of France.
It is based on the new Sedif - Eau Ile de France website : https://www.sedif.com/.
## Supported Things
The binding supports two different types of things: the `gateway` bridge and the `meter`.
- `gateway`: This bridge acts as the gateway between your Sedif account and the meter thing.
- `meter`: A meter thing that represents your water meter and exposes your water consumption.
### Gateway Bridge Configuration
To retrieve data, you need a `gateway` bridge linked to your Sedif Web account.
You will need to create an account prior to configuring your bridge.
Go to the login page and click on the "Je crée mon espace" button:
https://connexion.leaudiledefrance.fr/s/login/
After this, add your bridge and fill in your username and password.
| Parameter | Description |
|----------------|--------------------------------------------|
| username | Your Sedif platform username. |
| password | Your Sedif platform password. |
```java
Bridge sedif:gateway:local "Gateway" [username="testuser@test.fr", password="mypassword"]
```
### Water Meter Discovery
After creating the bridge, the binding will populate the inbox with meters registered in your Sedif account.
### Meter Thing Configuration
To create a meter thing, you will need your contractId.
You can find it on the Sedif website, under the section "Tous mes contrats".
You will see a list where the first column labeled "Contrat" is the contractId.
If you have multiple meters on the same contract, you will also need to get your(s) meterId's.
MeterId is displayed at the contract details page.
Just click on the contract number in the contract list, and you will have a detailed pages with a label Compteur n°D08MAxxxxxx.
Note that you do not need to create the meter manually.
Once you create the gateway, the inbox will be populated automatically with all meter's that are registered on your account.
| Name | Type | Description | Default | Required | Advanced |
|-----------------|---------|---------------------------------------------------------|---------|----------|----------|
| contractId | text | The identifier of your contract | N/A | yes | no |
| meterId | text | The identifier of the meter associated with this thing | N/A | no | no |
```java
Thing sedif:meter:meter1 "Sedif Meter 1" (sedif:gateway:local)
[
contractId="907....", meterId="D08MA......"
]
``
### Meter Thing Channels
| Channel | Type | Read/Write | Description |
|---------------------------------------------|--------------|------------|------------------------------------------|
| base#mean-water-price | numeric | R | The water mean price |
The daily group will give consumption information with day granularity
| Channel | Item type | Read/Write | Description |
|---------------------------------------------|----------------|------------|------------------------------------------|
| daily-consumption#yesterday | Number:Volume | R | The water consumption from yesterday |
| daily-consumption#day-2 | Number:Volume | R | The day-2 water consumption |
| daily-consumption#day-3 | Number:Volume | R | The day-3 water consumption |
| daily-consumption#consumption | Number:Volume | R | Timeseries for water consumption |
The weekly group will give consumption information with week granularity
| Channel | Item type | Read/Write | Description |
|---------------------------------------------|----------------|------------|------------------------------------------|
| weekly-consumption#this-week | Number:Volume | R | The current week water consumption |
| weekly-consumption#last-week | Number:Volume | R | The last week water consumption |
| weekly-consumption#week-2 | Number:Volume | R | The week-2 water consumption |
| weekly-consumption#consumption | Number:Volume | R | Timeseries for weekly water consumption |
The monthly group will give consumption information with month granularity
| Channel | Item type | Read/Write | Description |
|---------------------------------------------|----------------|------------|------------------------------------------|
| monthly-consumption#this-month | Number:Volume | R | The current month water consumption |
| monthly-consumption#last-month | Number:Volume | R | The last month water consumption |
| monthly-consumption#month-2 | Number:Volume | R | The month-2 water consumption |
| monthly-consumption#consumption | Number:Volume | R | Timeseries for monthly water consumption |
The yearly group will give consumption information with year granularity
| Channel | Item type | Read/Write | Description |
|---------------------------------------------|----------------|------------|------------------------------------------|
| yearly-consumption#this-year | Number:Volume | R | The current year water consumption |
| yearly-consumption#last-year | Number:Volume | R | The last year water consumption |
| yearly-consumption#year-2 | Number:Volume | R | The year-2 water consumption |
| yearly-consumption#consumption | Number:Volume | R | Timeseries for yearly water consumption |
### Full Example
```java
Bridge sedif:gateway:local "GatewayBridge" [username="testuser@test.fr", password="mypassword"] {
Thing sedif:meter:meter1 "Meter 1" (sedif:gateway:local) [ contractId="907....", meterId="D08MA......" ]
Thing sedif:meter:meter1 "Meter 1" (sedif:gateway:local) [ contractId="908....", meterId="D08MA......" ]
}
Number:Volume ConsoDaily "Daily Conso [%.0f %unit%]" <energy> { channel="sedif:meter:meter1:daily-consumption#consumption" }
Number:Volume ConsoYesterday "Conso Yesterday [%.0f %unit%]" <energy> { channel="sedif:meter:meter1:daily-consumption#yesterday" }
Number:Volume ConsoDayMinus2 "Conso Day-2 [%.0f %unit%]" <energy> { channel="sedif:meter:meter1:daily-consumption#day-2" }
Number:Volume ConsoDayMinus3 "Conso Day-3 [%.0f %unit%]" <energy> { channel="sedif:meter:meter1:daily-consumption#day-3" }
Number:Volume ConsoWeekly "Weekly Conso [%.0f %unit%]" <energy> { channel="sedif:meter:meter1:weekly-consumption#consumption" }
Number:Volume ConsoThisWeek "Conso This Week [%.0f %unit%]" <energy> { channel="sedif:meter:meter1:weekly-consumption#thisWeek" }
Number:Volume ConsoLastWeek "Conso Last Week [%.0f %unit%]" <energy> { channel="sedif:meter:meter1:weekly-consumption#lastWeek" }
Number:Volume ConsoWeekMinus2 "Conso Week - 2 [%.0f %unit%]" <energy> { channel="sedif:meter:meter1:weekly-consumption#week-2" }
Number:Volume ConsoMonthly "Montlhy Conso [%.0f %unit%]" <energy> { channel="sedif:meter:meter1:monthly-consumption#consumption" }
Number:Volume ConsoThisMonth "Conso This Month [%.0f %unit%]" <energy> { channel="sedif:meter:meter1:monthly-consumption#thisMonth" }
Number:Volume ConsoLastMonth "Conso Last Month [%.0f %unit%]" <energy> { channel="sedif:meter:meter1:monthly-consumption#lastMonth" }
Number:Volume ConsoMonthMinus2 "Conso Month - 2 [%.0f %unit%]" <energy> { channel="sedif:meter:meter1:monthly-consumption#month-2" }
Number:Volume ConsoYearly "Yearly Conso [%.0f %unit%]" <energy> { channel="sedif:meter:meter1:yearly-consumption#consumption" }
Number:Volume ConsoThisYear "Conso This Year [%.0f %unit%]" <energy> { channel="sedif:meter:meter1:yearly-consumption#thisYear" }
Number:Volume ConsoLastYear "Conso Last Year [%.0f %unit%]" <energy> { channel="sedif:meter:meter1:yearly-consumption#lastYear" }
Number:Volume ConsoYearMinus2 "Conso Year - 2 [%.0f %unit%]" <energy> { channel="sedif:meter:meter1:yearly-consumption#year-2" }
```
### Timeseries and Graphs
Thanks to the timeseries channels, you will be able to expose your water consumption as historical graphs.
For example, a weekly graph might look like this:
![SedifGraph](doc/SedifGraph.png)
Here's the code to produce the graph.
Note that you bind your series to one of the timeseries channels: ConsoDaily, ConsoWeekly, ConsoMonthly, or ConsoYearly.
```yaml
config:
label: Sedif Conso Weekly -x Histo
order: "10000000"
period: 4M
sidebar: false
slots:
dataZoom:
- component: oh-chart-datazoom
config:
type: inside
grid:
- component: oh-chart-grid
config:
includeLabels: true
legend:
- component: oh-chart-legend
config:
bottom: 3
type: scroll
series:
- component: oh-time-series
config:
gridIndex: 0
item: ConsoWeekly
label:
formatter: =v=>Number.parseFloat(v.data[1]).toFixed(4)*1000 + " L"
position: inside
show: true
name: Sedif Conso Weekly -x Histo
noBoundary: true
noItemState: true
service: inmemory
type: bar
xAxisIndex: 0
yAxisIndex: 0
tooltip:
- component: oh-chart-tooltip
config:
confine: true
smartFormatter: true
xAxis:
- component: oh-time-axis
config:
gridIndex: 0
yAxis:
- component: oh-value-axis
config:
gridIndex: 0
```
### Historic Data Retrieval
The first time you launch this binding, it will send multiple requests to the Sedif website to retrieve historical data.
This is done in 3-month chunks, from the end to the beginning of the contract.
Be aware that this process can take some time (around 23 minutes for 10 years of data).
This data is then cached in userdata/sedif/sedif.json.
On subsequent runs, the binding only retrieves the most recent data and refreshes daily.
If something goes wrong, you can delete the sedif.json file to start fresh.
### Multiple Contracts
You may have multiple contracts on your Sedif account:
Multiple properties with different meters:
If this is the case, simply create one Sedif thing for each separate contract.
Contract change on the same meter:
Sometimes, you might have two contracts for the same meter. In this scenario, create only one Sedif thing and follow these steps:
Set the contractId to the older contract and let the binding retrieve the full historical data.
Then, update the contractId to the new contract and restart openHAB.
By following this process, you will preserve the full history of the meter.
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>5.1.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.sedif</artifactId>
<name>openHAB Add-ons :: Bundles :: Sedif Binding</name>
<properties>
<bnd.importpackage>javax.annotation.meta;resolution:=optional</bnd.importpackage>
</properties>
</project>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.sedif-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-sedif" description="Sedif Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-core-auth-oauth2client</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.sedif/${project.version}</bundle>
</feature>
</features>
@@ -0,0 +1,127 @@
/*
* 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.sedif.internal.api;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Optional;
import java.util.function.Supplier;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is a simple expiring and reloading cache implementation.
*
* There must be provided an action in order to retrieve/calculate the value. This action will be called only if the
* answer from the last calculation is not valid anymore, i.e. if it is expired.
*
* The cache expires after the current day; it is possible to shift the beginning time of the day.
*
* Soft Reference is not used to store the cached value because JVM Garbage Collector is clearing it too much often.
*
* @author Laurent Garnier - Initial contribution
*
* @param <V> the type of the value
*/
@NonNullByDefault
public class ExpiringDayCache<V> {
private final Logger logger = LoggerFactory.getLogger(ExpiringDayCache.class);
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.
*
* @param name the name of this cache
* @param beginningHour the hour in the day at which the validity period is starting
* @param action the action to retrieve/calculate the value
*/
public ExpiringDayCache(String name, int beginningHour, int beginningMinute, Supplier<@Nullable V> action) {
this.name = name;
this.beginningHour = beginningHour;
this.beginningMinute = beginningMinute;
this.expiresAt = calcAlreadyExpired();
this.action = action;
}
/**
* Returns the value - possibly from the cache, if it is still valid.
*/
public synchronized Optional<V> getValue() {
@Nullable
V cachedValue = value;
if (cachedValue == null || isExpired()) {
logger.debug("getValue from cache \"{}\" is requiring a fresh value", name);
cachedValue = refreshValue();
} else {
logger.debug("getValue from cache \"{}\" is returning a cached value", name);
}
return Optional.ofNullable(cachedValue);
}
/**
* Returns if the value is Present or not.
*/
public boolean isPresent() {
@Nullable
V cachedValue = value;
return (cachedValue != null && !isExpired());
}
public void invalidate() {
value = null;
}
/**
* Refreshes and returns the value in the cache.
*
* @return the new value
*/
public synchronized @Nullable V refreshValue() {
value = action.get();
expiresAt = calcNextExpiresAt();
return value;
}
/**
* Checks if the value is expired.
*
* @return true if the value is expired
*/
public boolean isExpired() {
return !LocalDateTime.now().isBefore(expiresAt);
}
private LocalDateTime calcNextExpiresAt() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime limit = now.withHour(beginningHour).withMinute(beginningMinute).truncatedTo(ChronoUnit.MINUTES);
LocalDateTime result = now.isBefore(limit) ? limit : limit.plusDays(1);
logger.debug("calcNextExpiresAt result = {}", result.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
return result;
}
private LocalDateTime calcAlreadyExpired() {
return LocalDateTime.now().minusDays(1);
}
}
@@ -0,0 +1,452 @@
/*
* 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.sedif.internal.api;
import java.net.HttpCookie;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
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.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.Fields;
import org.openhab.binding.sedif.internal.constants.SedifBindingConstants;
import org.openhab.binding.sedif.internal.dto.Action;
import org.openhab.binding.sedif.internal.dto.Action.ReturnValue;
import org.openhab.binding.sedif.internal.dto.Actions;
import org.openhab.binding.sedif.internal.dto.AuraCommand;
import org.openhab.binding.sedif.internal.dto.AuraContext;
import org.openhab.binding.sedif.internal.dto.AuraResponse;
import org.openhab.binding.sedif.internal.dto.Contract;
import org.openhab.binding.sedif.internal.dto.ContractDetail;
import org.openhab.binding.sedif.internal.dto.ContractDetail.CompteInfo;
import org.openhab.binding.sedif.internal.dto.Contracts;
import org.openhab.binding.sedif.internal.dto.Event;
import org.openhab.binding.sedif.internal.dto.MeterReading;
import org.openhab.binding.sedif.internal.types.CommunicationFailedException;
import org.openhab.binding.sedif.internal.types.InvalidSessionException;
import org.openhab.binding.sedif.internal.types.SedifException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* {@link SedifHttpApi} wraps the Sedif Webservice.
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public class SedifHttpApi {
private final Logger logger = LoggerFactory.getLogger(SedifHttpApi.class);
private final Gson gson;
private final HttpClient httpClient;
private @Nullable AuraContext appCtx;
private @Nullable String token = "";
protected boolean connected = false;
public static final String SEDIF_DOMAIN = ".leaudiledefrance.fr";
public static final String BASE_URL = "https://connexion" + SEDIF_DOMAIN;
public static final String URL_SEDIF_AUTHENTICATE = BASE_URL + "/s/login/";
public static final String URL_SEDIF_AUTHENTICATE_POST = BASE_URL
+ "/s/sfsites/aura?r=1&other.LightningLoginForm.login=1";
public static final String URL_SEDIF_CONTRAT = BASE_URL + "/espace-particuliers/s/contrat?tab=Detail";
public static final String URL_SEDIF_SITE = BASE_URL
+ "/espace-particuliers/s/sfsites/aura?r=36&aura.ApexAction.execute=1";
private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0";
private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private HashMap<String, Contract> contractDict = new HashMap<>();
public SedifHttpApi(Gson gson, HttpClient httpClient) {
this.gson = gson;
this.httpClient = httpClient;
}
public void connectionInit(String userName, String userPassword) throws SedifException {
if (connected) {
return;
}
removeAllCookie();
// =====================================================================
// Step 1: getting salesforces context from login page
// =====================================================================
String resultSt = getContent(SedifHttpApi.URL_SEDIF_AUTHENTICATE);
appCtx = extractAuraContext(resultSt);
if (appCtx == null) {
throw new SedifException("Unable to find app context in login process");
} else {
logger.debug("Account {}: Successfully retrieved context", userName);
}
// =====================================================================
// Step 2: Authenticate
// =====================================================================
AuraResponse resp = doAuth(userName, userPassword);
String urlRedir = "";
if (resp != null) {
Event event = resp.events.getFirst();
Event.Attributes attr = event.attributes;
if (attr != null) {
urlRedir = (String) attr.values.get("url");
}
if (urlRedir.isBlank()) {
throw new SedifException("Unable to find redir url in login process");
}
}
// =====================================================================
// Step 3: Confirm login
// =====================================================================
resultSt = getContent(urlRedir);
// =====================================================================
// Step 4: Get contract page
// =====================================================================
resultSt = getContent(SedifHttpApi.URL_SEDIF_CONTRAT);
appCtx = extractAuraContext(resultSt);
if (appCtx == null) {
throw new SedifException("Unable to find app context in login process");
} else {
logger.debug("Successfully retrieved contract context");
}
// =====================================================================
// Step 5: Get cookie auth
// =====================================================================
List<HttpCookie> lCookie = httpClient.getCookieStore().getCookies();
token = "";
for (HttpCookie cookie : lCookie) {
if (cookie.getName().startsWith("__Host-ERIC_")) {
token = cookie.getValue();
}
}
if (token == null) {
throw new SedifException("Unable to find token in login process");
} else {
logger.debug("Account: Successfully asquire token");
}
// =====================================================================
// Step 6a: Get contract
// =====================================================================
Contracts contracts = getContracts();
if (contracts != null && contracts.contracts != null) {
for (Contract contract : contracts.contracts) {
String contractName = contract.name;
if (contractName != null) {
contractDict.put(contractName, contract);
}
}
}
connected = true;
}
public boolean isConnected() {
return connected;
}
public void disconnect() {
connected = false;
}
public void removeAllCookie() {
httpClient.getCookieStore().removeAll();
}
public String getContent(String url) throws SedifException {
return getContent("", null, url, null);
}
private String getContent(String contractId, @Nullable CompteInfo meterInfo, String url, @Nullable AuraCommand cmd)
throws SedifException {
try {
Request request = httpClient.newRequest(url);
request = request.timeout(SedifBindingConstants.REQUEST_TIMEOUT, TimeUnit.SECONDS);
request = request.agent(USER_AGENT);
if (cmd == null) {
request = request.method(HttpMethod.GET);
} else {
Hashtable<String, Object> paramsSub = cmd.getParamsSub();
Fields fields = new Fields();
request = request.method(HttpMethod.POST);
if (appCtx != null) {
fields.put("aura.context", getAuraContextPayload(appCtx));
}
fields.put("aura.token", token);
// We put the to key syntax contratId & contractId because of an error on Sedif side.
// Some api calls need the contratId syntax, and other the contactId syntax, so putting it both is ok
if (!contractId.isBlank()) {
paramsSub.put("contratId", contractId);
paramsSub.put("contractId", contractId);
}
// If we have a meterInfo in params, put this info on request
// We need this to read meter specific data like consumption
if (meterInfo != null) {
String meterIdB = meterInfo.eLmb;
String meterIdA = meterInfo.eLma;
if (!meterIdB.isBlank()) {
paramsSub.put("NUMERO_COMPTEUR", meterIdB);
}
if (!meterIdA.isBlank()) {
paramsSub.put("ID_PDS", meterIdA);
}
}
String msg = getActionPayload(cmd);
fields.put("message", msg);
request = request.content(new FormContentProvider(fields));
}
ContentResponse result = request.send();
if (result.getStatus() != HttpStatus.OK_200) {
throw new SedifException("Error requesting '%s': %s", url, result.getContentAsString());
}
return result.getContentAsString();
} catch (ExecutionException | TimeoutException e) {
throw new CommunicationFailedException("Error getting url: '%s'", e, url);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new CommunicationFailedException("Error getting url: '%s'", e, url);
}
}
public @Nullable AuraResponse doAuth(String userName, String userPassword) throws SedifException {
// =====================================================================
logger.trace("Step 3: Invoke the Sedif auth endpoint to do the login");
AuraCommand cmd = AuraCommand.make("", "", "");
cmd.setUser(userName, userPassword);
return getData("", null, URL_SEDIF_AUTHENTICATE_POST, cmd, AuraResponse.class);
}
public @Nullable Contracts getContracts() throws SedifException {
// =====================================================================
logger.trace("Step 6: Get the contracts associated with the sedif accounts");
AuraCommand cmd = AuraCommand.make("", "LTN009_ICL_ContratsGroupements", "getContratsGroupements");
Actions actions = getData("", null, URL_SEDIF_SITE, cmd, Actions.class);
if (actions != null) {
ReturnValue retValue = actions.actions.get(0).returnValue;
return (Contracts) ((retValue == null) ? null : retValue.returnValue);
}
return null;
}
public @Nullable ContractDetail getContractDetails(String contractId) throws SedifException {
// =====================================================================
logger.trace("Step 7: Get the contract details to have information about end user");
AuraCommand cmd = AuraCommand.make("", "LTN008_ICL_ContratDetails", "getContratDetails");
Hashtable<String, Object> paramsSub = cmd.getParamsSub();
// We put the to key syntax contratId & contractId because of an error on Sedif side.
// Some api calls need the contratId syntax, and other the contactId syntax, so putting it both is ok
if (!contractId.isBlank()) {
paramsSub.put("contratId", contractId);
paramsSub.put("contractId", contractId);
}
Actions actions = getData(contractId, null, URL_SEDIF_SITE, cmd, Actions.class);
if (actions != null) {
ReturnValue retValue = actions.actions.get(0).returnValue;
return (ContractDetail) ((retValue == null) ? null : retValue.returnValue);
}
return null;
}
public @Nullable MeterReading getConsumptionData(String contractId, @Nullable CompteInfo meterInfo, LocalDate from,
LocalDate to) throws SedifException {
logger.trace(" Step 8: getConsumptionData() {} {} {}", contractId, from, to);
AuraCommand cmd = AuraCommand.make("", "LTN015_ICL_ContratConsoHisto", "getData");
Hashtable<String, Object> paramsSub = cmd.getParamsSub();
paramsSub.put("TYPE_PAS", "JOURNEE"); // SEMAINE MOIS
return getMeasures(contractId, meterInfo, URL_SEDIF_SITE, cmd, from, to);
}
private @Nullable MeterReading getMeasures(String contractId, @Nullable CompteInfo meterInfo, String apiUrl,
AuraCommand cmd, LocalDate from, LocalDate to) throws SedifException {
String dtStart = from.format(API_DATE_FORMAT);
String dtEnd = to.format(API_DATE_FORMAT);
Hashtable<String, Object> paramsSub = cmd.getParamsSub();
paramsSub.put("DATE_DEBUT", dtStart);
paramsSub.put("DATE_FIN", dtEnd);
Actions actions = getData(contractId, meterInfo, apiUrl, cmd, Actions.class);
if (actions != null) {
ReturnValue retValue = actions.actions.get(0).returnValue;
return (MeterReading) ((retValue == null) ? null : retValue.returnValue);
}
return null;
}
private @Nullable <T> T getData(String contractId, @Nullable CompteInfo meterInfo, String url,
@Nullable AuraCommand cmd, Class<T> clazz) throws SedifException {
logger.debug("getData begin {}: {}", clazz.getName(), url);
try {
String data = getContent(contractId, meterInfo, url, cmd);
if (data.contains("aura:invalidSession")) {
throw new InvalidSessionException("Communication with sedif failed, session invalid");
}
if (!data.isEmpty()) {
try {
T result = Objects.requireNonNull(gson.fromJson(data, clazz));
logger.debug("getData success {}: {}", clazz.getName(), url);
return result;
} catch (JsonSyntaxException e) {
logger.debug("Invalid JSON response not matching {}: {}", clazz.getName(), data);
throw new SedifException("Requesting '%s' returned an invalid JSON response", e, url);
}
}
} catch (SedifException ex) {
throw ex;
}
return null;
}
private String getAuraContextPayload(@Nullable AuraContext context) {
if (context == null) {
return "";
}
return gson.toJson(context);
}
private String getActionPayload(AuraCommand cmd) {
Actions actions = new Actions();
Action action = new Action();
AuraContext lcAppCtx = appCtx;
if (lcAppCtx != null) {
String app = lcAppCtx.app;
if (app != null && app.contains("login")) {
action.descriptor = "apex://LightningLoginFormController/ACTION$login";
action.callingDescriptor = "markup://c:loginForm";
} else {
action.descriptor = "aura://ApexActionController/ACTION$execute";
action.callingDescriptor = "UNKNOWN";
}
}
String nameSpace = cmd.getNameSpace();
String className = cmd.getClassName();
String method = cmd.getMethodName();
String userName = cmd.getUserName();
String userPassword = cmd.getUserPassword();
if (userName != null && !userName.isBlank()) {
action.params.put("username", userName);
}
if (userPassword != null && !userPassword.isBlank()) {
action.params.put("password", userPassword);
}
if (nameSpace != null && !nameSpace.isBlank()) {
action.params.put("namespace", nameSpace);
}
if (className != null && !className.isBlank()) {
action.params.put("classname", className);
}
if (method != null && !method.isBlank()) {
action.params.put("method", method);
}
action.params.put("cacheable", false);
action.params.put("isContinuation", false);
action.params.put("params", cmd.getParamsSub());
actions.actions.add(action);
return gson.toJson(actions);
}
public @Nullable AuraContext extractAuraContext(String html) throws SedifException {
int pos1 = html.indexOf("resources.js");
if (pos1 >= 0) {
String sub1 = html.substring(0, pos1 + 1);
int pos2 = sub1.lastIndexOf("<script");
if (pos2 < 0) {
throw new SedifException("Unable to find app context in login process");
}
int pos3 = sub1.indexOf("%7B", pos2 + 1);
if (pos3 < 0) {
throw new SedifException("Unable to find app context in login process");
}
int pos4 = sub1.lastIndexOf("%7D");
if (pos4 < 0) {
throw new SedifException("Unable to find app context in login process");
}
String sub2 = sub1.substring(pos3, pos4 + 3);
sub2 = URLDecoder.decode(sub2, StandardCharsets.UTF_8);
return gson.fromJson(sub2, AuraContext.class);
}
return null;
}
public @Nullable Contract getContract(String contractName) {
return contractDict.get(contractName);
}
public Map<String, Contract> getAllContracts() {
return contractDict;
}
}
@@ -0,0 +1,66 @@
/*
* 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.sedif.internal.api.gson;
import java.io.IOException;
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;
import com.google.gson.stream.JsonWriter;
/**
* {@link FloatTypeAdapter} A type adapter for gson / double.
*
* Will prevent Null exception error when api return incomplete value.
* We can have this scenario when we ask to today consumption after midnight, but before enedis update the value.
* In this case, we don't want to failed all the data just because of one missing value.
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public class FloatTypeAdapter extends TypeAdapter<Float> {
@Override
public @Nullable Float read(@Nullable JsonReader reader) throws IOException {
if (reader != null) {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return Float.NaN;
}
String stringValue = reader.nextString();
try {
Float value = Float.valueOf(stringValue);
return value;
} catch (NumberFormatException e) {
return Float.NaN;
}
}
return Float.NaN;
}
@Override
public void write(@Nullable JsonWriter writer, @Nullable Float value) throws IOException {
if (writer != null) {
if (value == null) {
writer.nullValue();
return;
}
writer.value(value.floatValue());
}
}
}
@@ -0,0 +1,250 @@
/*
* 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.sedif.internal.api.helpers;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.WeekFields;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.sedif.internal.dto.MeterReading;
import org.openhab.binding.sedif.internal.dto.MeterReading.Data;
import org.openhab.binding.sedif.internal.dto.MeterReading.Data.Consommation;
import org.openhab.binding.sedif.internal.types.SedifException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link MeterReadingHelper} helper methods for MeterReading
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public class MeterReadingHelper {
private static final Logger logger = LoggerFactory.getLogger(MeterReadingHelper.class);
public static MeterReading merge(MeterReading currentMeterReading, MeterReading incomingMeterReading)
throws SedifException {
Data.Consommation[] incomingConso = incomingMeterReading.data.consommation;
if (incomingConso == null) {
throw new SedifException("Invalid meterReading data: no day period");
}
// Normalize dateIndex
for (int idx = 0; idx < incomingConso.length; idx++) {
Consommation conso = incomingConso[idx];
conso.dateIndex = conso.dateIndex.withHour(0).withMinute(0).withSecond(0);
conso.consommation *= 1000.00;
}
if (currentMeterReading.data.consommation == null) {
currentMeterReading.data.consommation = incomingConso;
} else {
Data.Consommation[] existingConso = currentMeterReading.data.consommation;
if (existingConso == null) {
throw new SedifException("Invalid meterReading data: no day period");
}
LocalDate firstDateExistingConso = existingConso[0].dateIndex.toLocalDate();
LocalDate lastDateExistingConso = existingConso[existingConso.length - 1].dateIndex.toLocalDate();
LocalDate firstDateIncomingConso = incomingConso[0].dateIndex.toLocalDate();
LocalDate lastDateIncomingConso = incomingConso[incomingConso.length - 1].dateIndex.toLocalDate();
Consommation[] newConso = null;
// The new block of data is before existing data
if (firstDateIncomingConso.isBefore(firstDateExistingConso)) {
// We browse the incoming data backward from the end to find first mergeable index
int idx = incomingConso.length - 1;
// While go backward until we find a date in incomingConso before the firstDate of existing Conso
while (idx > 0 && incomingConso[idx].dateIndex.toLocalDate().isAfter(firstDateExistingConso)) {
idx--;
}
// We need idx element from incomingConso, and full existingConso in new tab
newConso = new Consommation[idx + existingConso.length];
System.arraycopy(incomingConso, 0, newConso, 0, idx);
System.arraycopy(existingConso, 0, newConso, idx, existingConso.length);
}
// The new block of data is after existing Data
else if (lastDateIncomingConso.isAfter(lastDateExistingConso)) {
// We browse the incoming data forward from the start to find first mergeable index
int idx = 0;
while (idx < incomingConso.length
&& incomingConso[idx].dateIndex.toLocalDate().compareTo(lastDateExistingConso) <= 0) {
idx++;
}
// We need full existingConso and incomingConsol.length - idx element from incomingConso in the new tab
newConso = new Consommation[existingConso.length + incomingConso.length - idx];
System.arraycopy(existingConso, 0, newConso, 0, existingConso.length);
System.arraycopy(incomingConso, idx, newConso, existingConso.length, incomingConso.length - idx);
}
// The new block of data is in middle of existing data
else {
// We browse the incoming data forward from the start to find first mergeable index
int idxStart = 0;
while (idxStart < existingConso.length
&& existingConso[idxStart].dateIndex.toLocalDate().compareTo(firstDateIncomingConso) < 0) {
idxStart++;
}
idxStart--;
int idxEnd = existingConso.length - 1;
// While go backward until we find a date in incomingConso before the firstDate of existing Conso
while (idxEnd > 0 && existingConso[idxEnd].dateIndex.toLocalDate().isAfter(lastDateIncomingConso)) {
idxEnd--;
}
idxEnd++;
int len = idxStart + 1 + incomingConso.length + existingConso.length - idxEnd;
newConso = new Consommation[len];
System.arraycopy(existingConso, 0, newConso, 0, idxStart + 1);
System.arraycopy(incomingConso, 0, newConso, idxStart + 1, incomingConso.length);
if (idxEnd < existingConso.length) {
System.arraycopy(existingConso, idxEnd, newConso, idxEnd + incomingConso.length - 1,
existingConso.length - idxEnd);
}
}
currentMeterReading.data.consommation = newConso;
}
currentMeterReading.prixMoyenEau = incomingMeterReading.prixMoyenEau;
return currentMeterReading;
}
public static void check(MeterReading meterReading) throws SedifException {
if (meterReading.data.consommation == null) {
throw new SedifException("Invalid meterReading data: no day period");
}
if (meterReading.data.consommation.length == 0) {
throw new SedifException("Invalid meterReading data: no day period");
}
}
public static void calcAgregat(MeterReading meterReading) throws SedifException {
Data.Consommation[] lcConso = meterReading.data.consommation;
if (lcConso == null) {
throw new SedifException("Invalid meterReading data: no day period");
}
for (int idx = 0; idx < lcConso.length; idx++) {
LocalDate dt = lcConso[idx].dateIndex.toLocalDate();
meterReading.data.putEntries(dt.toString(), lcConso[idx]);
}
LocalDate startDate = lcConso[0].dateIndex.toLocalDate();
LocalDate endDate = lcConso[meterReading.data.consommation.length - 1].dateIndex.toLocalDate();
LocalDate realStartDate = startDate.atStartOfDay().with(TemporalAdjusters.previous(DayOfWeek.MONDAY))
.toLocalDate();
LocalDate realEndDate = endDate.atStartOfDay().with(TemporalAdjusters.next(DayOfWeek.SUNDAY)).toLocalDate();
int yearsNum = realEndDate.getYear() - realStartDate.getYear() + 1;
int monthsNum = (realEndDate.getYear() - realStartDate.getYear()) * 12 + realEndDate.getMonthValue()
- realStartDate.getMonthValue() + 1;
int weeksNum = (int) ChronoUnit.WEEKS.between(realStartDate, realEndDate) + 1;
meterReading.data.weekConso = new Consommation[weeksNum];
meterReading.data.monthConso = new Consommation[monthsNum];
meterReading.data.yearConso = new Consommation[yearsNum];
for (int idxWeek = 0; idxWeek < weeksNum; idxWeek++) {
LocalDate startOfWeek = realStartDate.plusWeeks(idxWeek);
LocalDate endOfWeek = startOfWeek.plusDays(6);
Consommation weekConso = meterReading.data.new Consommation();
meterReading.data.weekConso[idxWeek] = weekConso;
float indexDeb = getIndex(meterReading, startOfWeek.minusDays(1), startDate, endDate);
float indexFin = getIndex(meterReading, endOfWeek, startDate, endDate);
float indexDiff = indexFin - indexDeb;
weekConso.consommation = indexDiff * (float) 1000.00;
weekConso.dateIndex = LocalDateTime.of(startOfWeek.getYear(), startOfWeek.getMonth(),
startOfWeek.getDayOfMonth(), 0, 0, 0);
int week = weekConso.dateIndex.get(WeekFields.ISO.weekOfWeekBasedYear());
int weekYear = weekConso.dateIndex.get(WeekFields.ISO.weekBasedYear());
String key = weekYear + "-w-" + week;
meterReading.data.putEntries(key, weekConso);
logger.debug("");
}
for (int idxMonth = 0; idxMonth < monthsNum; idxMonth++) {
LocalDate startOfMonth = realStartDate.with(TemporalAdjusters.firstDayOfMonth()).plusMonths(idxMonth);
LocalDate endOfMonth = startOfMonth.with(TemporalAdjusters.lastDayOfMonth());
Consommation monthConso = meterReading.data.new Consommation();
meterReading.data.monthConso[idxMonth] = monthConso;
float indexDeb = getIndex(meterReading, startOfMonth.minusDays(1), startDate, endDate);
float indexFin = getIndex(meterReading, endOfMonth, startDate, endDate);
float indexDiff = indexFin - indexDeb;
monthConso.consommation = indexDiff * (float) 1000.00;
monthConso.dateIndex = LocalDateTime.of(startOfMonth.getYear(), startOfMonth.getMonth(), 1, 0, 0, 0);
String key = startOfMonth.getYear() + "-" + startOfMonth.getMonthValue();
meterReading.data.putEntries(key, monthConso);
}
for (int idxYear = 0; idxYear < yearsNum; idxYear++) {
LocalDate startOfYear = realStartDate.with(TemporalAdjusters.firstDayOfYear()).plusYears(idxYear);
LocalDate endOfYear = startOfYear.with(TemporalAdjusters.lastDayOfYear());
Consommation yearConso = meterReading.data.new Consommation();
meterReading.data.yearConso[idxYear] = yearConso;
float indexDeb = getIndex(meterReading, startOfYear.minusDays(1), startDate, endDate);
float indexFin = getIndex(meterReading, endOfYear, startDate, endDate);
float indexDiff = indexFin - indexDeb;
yearConso.consommation = indexDiff * (float) 1000.00;
yearConso.dateIndex = LocalDateTime.of(startOfYear.getYear(), 1, 1, 0, 0, 0);
String key = "" + startOfYear.getYear();
meterReading.data.putEntries(key, yearConso);
}
}
public static float getIndex(MeterReading meterReading, LocalDate date, LocalDate startDate, LocalDate endDate) {
LocalDate dateToGet = date;
if (dateToGet.isBefore(startDate)) {
dateToGet = startDate;
} else if (dateToGet.isAfter(endDate)) {
dateToGet = endDate;
}
Consommation result = meterReading.data.getEntries(dateToGet.toString());
if (result != null) {
return result.valeurIndex;
} else {
Consommation r1 = meterReading.data.getEntries(dateToGet.minusDays(1).toString());
Consommation r2 = meterReading.data.getEntries(dateToGet.plusDays(1).toString());
if (r1 != null && r2 != null) {
return (r1.valeurIndex + r2.valeurIndex) / 2;
}
}
return 0;
}
}
@@ -0,0 +1,483 @@
/*
* 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
*/
/*
* Copyright (C) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Copied from
* https://raw.githubusercontent.com/google/gson/master/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java
* and repackaged here with additional content from
* com.google.gson.internal.{Streams,TypeAdapters,LazilyParsedNumber}
* to avoid using the internal package.
*/
package org.openhab.binding.sedif.internal.api.parse;
import java.io.EOFException;
import java.io.IOException;
import java.io.ObjectStreamException;
import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.google.gson.stream.MalformedJsonException;
/**
* Adapts values whose runtime type may differ from their declaration type. This
* is necessary when a field's type is not the same type that GSON should create
* when deserializing that field. For example, consider these types:
*
* <pre>
* {
* &#64;code
* abstract class Shape {
* int x;
* int y;
* }
* class Circle extends Shape {
* int radius;
* }
* class Rectangle extends Shape {
* int width;
* int height;
* }
* class Diamond extends Shape {
* int width;
* int height;
* }
* class Drawing {
* Shape bottomShape;
* Shape topShape;
* }
* }
* </pre>
* <p>
* Without additional type information, the serialized JSON is ambiguous. Is
* the bottom shape in this drawing a rectangle or a diamond?
*
* <pre>
* {@code
* {
* "bottomShape": {
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}
* </pre>
*
* This class addresses this problem by adding type information to the
* serialized JSON and honoring that type information when the JSON is
* deserialized:
*
* <pre>
* {@code
* {
* "bottomShape": {
* "type": "Diamond",
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "type": "Circle",
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}
* </pre>
*
* Both the type field name ({@code "type"}) and the type labels ({@code
* "Rectangle"}) are configurable.
*
* <h3>Registering Types</h3>
* Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field
* name to the {@link #of} factory method. If you don't supply an explicit type
* field name, {@code "type"} will be used.
*
* <pre>
* {
* &#64;code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class, "type");
* }
* </pre>
*
* Next register all of your subtypes. Every subtype must be explicitly
* registered. This protects your application from injection attacks. If you
* don't supply an explicit type label, the type's simple name will be used.
*
* <pre>
* {@code
* shapeAdapter.registerSubtype(Rectangle.class, "Rectangle");
* shapeAdapter.registerSubtype(Circle.class, "Circle");
* shapeAdapter.registerSubtype(Diamond.class, "Diamond");
* }
* </pre>
*
* Finally, register the type adapter factory in your application's GSON builder:
*
* <pre>
* {
* &#64;code
* Gson gson = new GsonBuilder().registerTypeAdapterFactory(shapeAdapterFactory).create();
* }
* </pre>
*
* Like {@code GsonBuilder}, this API supports chaining:
*
* <pre>
* {
* &#64;code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
* .registerSubtype(Rectangle.class).registerSubtype(Circle.class).registerSubtype(Diamond.class);
* }
* </pre>
*/
/**
* @author Jesse Wilson (swankjesse) - Initial contribution
*/
@NonNullByDefault
public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
private final Class<?> baseType;
private final String typeFieldName;
private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
private final Map<String, Class<?>> firstElementToSubtype = new LinkedHashMap<String, Class<?>>();
private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>();
private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName) {
this.baseType = baseType;
this.typeFieldName = typeFieldName;
}
/**
* Creates a new runtime type adapter using for {@code baseType} using {@code
* typeFieldName} as the type field name. Type field names are case sensitive.
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName);
}
/**
* Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
* the type field name.
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
return new RuntimeTypeAdapterFactory<T>(baseType, "type");
}
/**
* Registers {@code type} identified by {@code label}. Labels are case
* sensitive.
*
* @throws IllegalArgumentException if either {@code type} or {@code label}
* have already been registered on this type adapter.
*/
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String firstElement, String label) {
if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
throw new IllegalArgumentException("types and labels must be unique");
}
firstElementToSubtype.put(firstElement, type);
labelToSubtype.put(label, type);
subtypeToLabel.put(type, label);
return this;
}
/**
* Registers {@code type} identified by its {@link Class#getSimpleName simple
* name}. Labels are case sensitive.
*
* @throws IllegalArgumentException if either {@code type} or its simple name
* have already been registered on this type adapter.
*/
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
return registerSubtype(type, "", type.getSimpleName());
}
@Override
public @Nullable <R> TypeAdapter<R> create(@Nullable Gson gson, @Nullable TypeToken<R> type) {
if (type == null || type.getRawType() != baseType) {
return null;
}
if (gson == null) {
return null;
}
final Map<String, TypeAdapter<?>> labelToDelegate = new LinkedHashMap<String, TypeAdapter<?>>();
final Map<String, TypeAdapter<?>> firstElementToDelegate = new LinkedHashMap<String, TypeAdapter<?>>();
final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate = new LinkedHashMap<Class<?>, TypeAdapter<?>>();
for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
labelToDelegate.put(entry.getKey(), delegate);
subtypeToDelegate.put(entry.getValue(), delegate);
}
for (Map.Entry<String, Class<?>> entry : firstElementToSubtype.entrySet()) {
TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
firstElementToDelegate.put(entry.getKey(), delegate);
subtypeToDelegate.put(entry.getValue(), delegate);
}
return new TypeAdapter<R>() {
@Override
public @Nullable R read(JsonReader in) throws IOException {
JsonElement jsonElement = RuntimeTypeAdapterFactory.parse(in);
if (jsonElement != null) {
String firstElement = (String) jsonElement.getAsJsonObject().keySet().toArray()[0];
@SuppressWarnings("unchecked") // registration requires that subtype extends T
TypeAdapter<R> delegate = (TypeAdapter<R>) firstElementToDelegate.get(firstElement);
if (delegate == null) {
throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + "label"
+ "; did you forget to register a subtype?");
}
return delegate.fromJsonTree(jsonElement);
} else {
throw new JsonParseException("cannot deserialize " + baseType + " because jsonElement is null");
}
}
@Override
public void write(JsonWriter out, @Nullable R value) throws IOException {
if (value == null) {
return;
}
Class<?> srcType = value.getClass();
String label = subtypeToLabel.get(srcType);
@SuppressWarnings("unchecked") // registration requires that subtype extends T
TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
if (delegate == null) {
throw new JsonParseException(
"cannot serialize " + srcType.getName() + "; did you forget to register a subtype?");
}
JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
if (jsonObject.has(typeFieldName)) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ " because it already defines a field named " + typeFieldName);
}
JsonObject clone = new JsonObject();
clone.add(typeFieldName, new JsonPrimitive(label));
for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
clone.add(e.getKey(), e.getValue());
}
RuntimeTypeAdapterFactory.write(clone, out);
}
}.nullSafe();
}
/**
* Takes a reader in any state and returns the next value as a JsonElement.
*/
private static @Nullable JsonElement parse(JsonReader reader) throws JsonParseException {
boolean isEmpty = true;
try {
reader.peek();
isEmpty = false;
return RuntimeTypeAdapterFactory.JSON_ELEMENT.read(reader);
} catch (EOFException e) {
/*
* For compatibility with JSON 1.5 and earlier, we return a JsonNull for
* empty documents instead of throwing.
*/
if (isEmpty) {
return JsonNull.INSTANCE;
}
// The stream ended prematurely so it is likely a syntax error.
throw new JsonSyntaxException(e);
} catch (MalformedJsonException e) {
throw new JsonSyntaxException(e);
} catch (IOException e) {
throw new JsonIOException(e);
} catch (NumberFormatException e) {
throw new JsonSyntaxException(e);
}
}
/**
* Writes the JSON element to the writer, recursively.
*/
private static void write(JsonElement element, JsonWriter writer) throws IOException {
RuntimeTypeAdapterFactory.JSON_ELEMENT.write(writer, element);
}
private static final TypeAdapter<JsonElement> JSON_ELEMENT = new TypeAdapter<JsonElement>() {
@Override
public @Nullable JsonElement read(JsonReader in) throws IOException {
switch (in.peek()) {
case STRING:
return new JsonPrimitive(in.nextString());
case NUMBER:
String number = in.nextString();
return new JsonPrimitive(new LazilyParsedNumber(number));
case BOOLEAN:
return new JsonPrimitive(in.nextBoolean());
case NULL:
in.nextNull();
return JsonNull.INSTANCE;
case BEGIN_ARRAY:
JsonArray array = new JsonArray();
in.beginArray();
while (in.hasNext()) {
array.add(read(in));
}
in.endArray();
return array;
case BEGIN_OBJECT:
JsonObject object = new JsonObject();
in.beginObject();
while (in.hasNext()) {
object.add(in.nextName(), read(in));
}
in.endObject();
return object;
case END_DOCUMENT:
case NAME:
case END_OBJECT:
case END_ARRAY:
default:
throw new IllegalArgumentException();
}
}
@Override
public void write(JsonWriter out, @Nullable JsonElement value) throws IOException {
if (value == null || value.isJsonNull()) {
out.nullValue();
} else if (value.isJsonPrimitive()) {
JsonPrimitive primitive = value.getAsJsonPrimitive();
if (primitive.isNumber()) {
out.value(primitive.getAsNumber());
} else if (primitive.isBoolean()) {
out.value(primitive.getAsBoolean());
} else {
out.value(primitive.getAsString());
}
} else if (value.isJsonArray()) {
out.beginArray();
for (JsonElement e : value.getAsJsonArray()) {
write(out, e);
}
out.endArray();
} else if (value.isJsonObject()) {
out.beginObject();
for (Map.Entry<String, JsonElement> e : value.getAsJsonObject().entrySet()) {
out.name(e.getKey());
write(out, e.getValue());
}
out.endObject();
} else {
throw new IllegalArgumentException("Couldn't write " + value.getClass());
}
}
};
/**
* This class holds a number value that is lazily converted to a specific number type
*
* @author Inderjeet Singh
*/
public static final class LazilyParsedNumber extends Number {
private final String value;
public LazilyParsedNumber(String value) {
this.value = value;
}
@Override
public int intValue() {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
try {
return (int) Long.parseLong(value);
} catch (NumberFormatException nfe) {
return new BigDecimal(value).intValue();
}
}
}
@Override
public long longValue() {
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
return new BigDecimal(value).longValue();
}
}
@Override
public float floatValue() {
return Float.parseFloat(value);
}
@Override
public double doubleValue() {
return Double.parseDouble(value);
}
@Override
public String toString() {
return value;
}
/**
* If somebody is unlucky enough to have to serialize one of these, serialize
* it as a BigDecimal so that they won't need Gson on the other side to
* deserialize it.
*/
private Object writeReplace() throws ObjectStreamException {
return new BigDecimal(value);
}
}
}
@@ -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.sedif.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.config.core.Configuration;
/**
* The {@link SedifBridgeConfiguration} is the class used to match the
* thing configuration.
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public class SedifBridgeConfiguration extends Configuration {
public String username = "";
public String password = "";
public boolean seemsValid() {
return !username.isBlank() && !password.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.sedif.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.config.core.Configuration;
/**
* The {@link SedifConfiguration} is the class used to match the
* thing configuration.
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public class SedifConfiguration extends Configuration {
public String contractId = "";
public String meterId = "";
public boolean seemsValid() {
return !contractId.isBlank();
}
}
@@ -0,0 +1,82 @@
/*
* 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.sedif.internal.constants;
import java.util.Currency;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link SedifBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public class SedifBindingConstants {
public static final String BINDING_ID = "sedif";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_METER = new ThingTypeUID(BINDING_ID, "meter");
public static final ThingTypeUID THING_TYPE_GATEWAY_BRIDGE = new ThingTypeUID(BINDING_ID, "gateway");
public static final Set<ThingTypeUID> SUPPORTED_DEVICE_THING_TYPES_UIDS = Set.of(THING_TYPE_METER,
THING_TYPE_GATEWAY_BRIDGE);
public static final String SEDIF_LASTUPDATE_PROPS = "lastUpdate";
public static final String GROUP_BASE = "base";
public static final String GROUP_DAILY_CONSUMPTION = "daily-consumption";
public static final String GROUP_WEEKLY_CONSUMPTION = "weekly-consumption";
public static final String GROUP_MONTHLY_CONSUMPTION = "monthly-consumption";
public static final String GROUP_YEARLY_CONSUMPTION = "yearly-consumption";
public static final String CHANNEL_CONSUMPTION = "consumption";
public static final String CHANNEL_DAILY_YESTERDAY_CONSUMPTION = "yesterday";
public static final String CHANNEL_DAILY_DAY_MINUS_2_CONSUMPTION = "day-2";
public static final String CHANNEL_DAILY_DAY_MINUS_3_CONSUMPTION = "day-3";
public static final String CHANNEL_WEEKLY_THIS_WEEK_CONSUMPTION = "this-week";
public static final String CHANNEL_WEEKLY_LAST_WEEK_CONSUMPTION = "last-week";
public static final String CHANNEL_WEEKLY_WEEK_MINUS_2_CONSUMPTION = "week-2";
public static final String CHANNEL_MONTHLY_THIS_MONTH_CONSUMPTION = "this-month";
public static final String CHANNEL_MONTHLY_LAST_MONTH_CONSUMPTION = "last-month";
public static final String CHANNEL_MONTHLY_MONTH_MINUS_2_CONSUMPTION = "month-2";
public static final String CHANNEL_YEARLY_THIS_YEAR_CONSUMPTION = "this-year";
public static final String CHANNEL_YEARLY_LAST_YEAR_CONSUMPTION = "last-year";
public static final String CHANNEL_YEARLY_YEAR_MINUS_2_CONSUMPTION = "year-2";
public static final String CHANNEL_MEAN_WATER_PRICE = "mean-water-price";
public static final String PROPERTY_CONTRACT_ORGANIZING_AUTHORITY = "contractOrganizingAuthority";
public static final String PROPERTY_CONTRACT_DATE_SORTIE_EPT = "contractDateSortieEpt";
public static final String PROPERTY_CONTRACT_ICL_ACTIVE = "contractIclActive";
public static final String PROPERTY_CONTRACT_NAME = "contractName";
public static final String PROPERTY_CONTRACT_BALANCE = "balance";
public static final String PROPERTY_ELMA = "meterElema";
public static final String PROPERTY_ELMB = "meterElemb";
public static final String PROPERTY_ID_PDS = "meterIdPds";
public static final String PROPERTY_NUM_METER = "meterId";
public static final String PROPERTY_CONTRACT_ID = "contractId";
public static final Currency CURRENCY_EUR = Currency.getInstance("EUR");
public static final int REQUEST_TIMEOUT = 30;
}
@@ -0,0 +1,153 @@
/*
* 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.sedif.internal.discovery;
import static org.openhab.binding.sedif.internal.constants.SedifBindingConstants.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.sedif.internal.api.SedifHttpApi;
import org.openhab.binding.sedif.internal.dto.Contract;
import org.openhab.binding.sedif.internal.dto.ContractDetail;
import org.openhab.binding.sedif.internal.dto.ContractDetail.CompteInfo;
import org.openhab.binding.sedif.internal.handler.BridgeSedifWebHandler;
import org.openhab.binding.sedif.internal.helpers.SedifListener;
import org.openhab.binding.sedif.internal.types.SedifException;
import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ServiceScope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SedifDiscoveryService} class is the service to discover a skeleton for controller handlers.
*
* @author Laurent Arnal - Initial contribution
*/
@Component(scope = ServiceScope.PROTOTYPE, service = SedifDiscoveryService.class)
@NonNullByDefault
public class SedifDiscoveryService extends AbstractThingHandlerDiscoveryService<BridgeSedifWebHandler>
implements SedifListener {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_METER);
private static final int SCAN_DURATION_IN_S = 10;
private final Logger logger = LoggerFactory.getLogger(SedifDiscoveryService.class);
public SedifDiscoveryService() {
super(BridgeSedifWebHandler.class, SUPPORTED_THING_TYPES, SCAN_DURATION_IN_S);
}
@Override
public void setThingHandler(ThingHandler handler) {
super.setThingHandler(handler);
thingHandler.addListener(this);
}
@Override
public void deactivate() {
thingHandler.removeListener(this);
super.deactivate();
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return SUPPORTED_THING_TYPES;
}
@Override
protected void startScan() {
logger.debug("Sedif discovery: Start {}", thingHandler.getThing().getUID());
if (!(getThingHandler() instanceof BridgeSedifWebHandler bridgeHandler)) {
return;
}
SedifHttpApi api = bridgeHandler.getSedifApi();
Map<String, Contract> contracts = api.getAllContracts();
for (Contract contract : contracts.values()) {
detectNewWaterMeterFromContract(contract);
}
}
@Override
public synchronized void abortScan() {
logger.debug("Sedif discovery: Abort {}", thingHandler.getThing().getUID());
super.abortScan();
}
@Override
protected synchronized void stopScan() {
logger.debug("Sedif discovery: Stop {}", thingHandler.getThing().getUID());
super.stopScan();
}
@Override
public void onContractInit(Contract contract) {
detectNewWaterMeterFromContract(contract);
}
private void detectNewWaterMeterFromContract(final Contract contract) {
logger.trace("New water meter detection from contract {}", contract);
if (!"Actif".equals(contract.statut)) {
return;
}
if (!(getThingHandler() instanceof BridgeSedifWebHandler bridgeHandler)) {
return;
}
SedifHttpApi api = bridgeHandler.getSedifApi();
try {
String contractId = contract.id;
if (contractId != null) {
ContractDetail contractDetail = api.getContractDetails(contractId);
if (contractDetail != null) {
for (CompteInfo compteInfo : contractDetail.compteInfo) {
ThingTypeUID tpUid = THING_TYPE_METER;
ThingUID thingUID = new ThingUID(tpUid, compteInfo.numCompteur,
thingHandler.getThing().getUID().getId());
Map<String, Object> properties = new HashMap<>();
properties.put(PROPERTY_ELMA, compteInfo.eLma);
properties.put(PROPERTY_ELMB, compteInfo.eLmb);
properties.put(PROPERTY_NUM_METER, compteInfo.numCompteur);
properties.put(PROPERTY_ID_PDS, compteInfo.idPds);
properties.put(PROPERTY_CONTRACT_ID, Objects.requireNonNull(contract.name));
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
.withProperties(properties).withLabel("Water Meter " + compteInfo.numCompteur)
.withThingType(tpUid).withBridge(thingHandler.getThing().getUID())
.withRepresentationProperty(PROPERTY_NUM_METER).build();
thingDiscovered(discoveryResult);
}
}
}
} catch (SedifException ex) {
logger.debug("Unable to detect water meter for contract {}", contract, ex);
}
}
}
@@ -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.sedif.internal.dto;
import java.util.Hashtable;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link Action} holds authentication information
*
* @author Laurent Arnal - Initial contribution
*/
public class Action {
public class ReturnValue {
public @Nullable Value returnValue;
};
public @Nullable String id;
public @Nullable String descriptor;
public @Nullable String callingDescriptor;
public @Nullable String state;
public Hashtable<String, Object> params = new Hashtable<String, Object>();
public @Nullable ReturnValue returnValue;
}
@@ -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.sedif.internal.dto;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link Actions} holds authentication information
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public class Actions {
public List<Action> actions = new ArrayList<Action>();
}
@@ -0,0 +1,69 @@
/*
* 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.sedif.internal.dto;
import java.util.Hashtable;
/**
* The {@link AuraCommand} holds a command
*
* @author Laurent Arnal - Initial contribution
*/
public class AuraCommand {
private String nameSpace;
private String className;
private String methodName;
private String userName;
private String userPassword;
private Hashtable<String, Object> paramsSub = new Hashtable<String, Object>();
public AuraCommand(String nameSpace, String className, String methodName) {
this.nameSpace = nameSpace;
this.className = className;
this.methodName = methodName;
}
public static AuraCommand make(String nameSpace, String className, String methodName) {
return new AuraCommand(nameSpace, className, methodName);
}
public String getNameSpace() {
return this.nameSpace;
}
public String getClassName() {
return this.className;
}
public String getMethodName() {
return this.methodName;
}
public String getUserName() {
return this.userName;
}
public void setUser(String userName, String userPassword) {
this.userName = userName;
this.userPassword = userPassword;
}
public String getUserPassword() {
return this.userPassword;
}
public Hashtable<String, Object> getParamsSub() {
return this.paramsSub;
}
}
@@ -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.sedif.internal.dto;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link AuraContext} holds authentication information
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public class AuraContext {
public class Globals {
}
public @Nullable String mode;
public @Nullable String fwuid;
public @Nullable String app;
public Hashtable<String, String> loaded = new Hashtable<String, String>();
public @Nullable List<String> dn = new ArrayList<String>();
public @Nullable Globals globals;
public boolean uad;
}
@@ -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.sedif.internal.dto;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link AuraResponse} holds authentication information
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public class AuraResponse {
public List<Action> actions = new ArrayList<Action>();
public @Nullable AuraContext context;
public List<Event> events = new ArrayList<Event>();
public @Nullable PerfSummary perfSummary;
}
@@ -0,0 +1,45 @@
/*
* 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.sedif.internal.dto;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* The {@link Contract} holds Contract information
*
* @author Laurent Arnal - Initial contribution
*/
public class Contract {
@SerializedName("AutoriteOrganisatrice")
public String autoriteOrganisatrice;
@SerializedName("DateSortieEPT")
public String dateSortieEPT;
public boolean eFacture;
public boolean iclActive;
@SerializedName("Id")
public @Nullable String id;
@SerializedName("Name")
public @Nullable String name;
@SerializedName("PrelevAuto")
public boolean prelevAuto;
@SerializedName("SITE_Commune")
public @Nullable String siteCommune;
@SerializedName("SITE_CP")
public @Nullable String siteCp;
@SerializedName("SITE_Rue")
public @Nullable String siteRue;
@SerializedName("Statut")
public @Nullable String statut;
}
@@ -0,0 +1,77 @@
/*
* 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.sedif.internal.dto;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* The {@link ContractDetail} holds Contract information
*
* @author Laurent Arnal - Initial contribution
*/
public class ContractDetail extends Value {
public class CompteInfo {
@SerializedName("ELEMA")
public String eLma;
@SerializedName("ELEMB")
public String eLmb;
@SerializedName("ID_PDS")
public String idPds;
@SerializedName("NUM_COMPTEUR")
public String numCompteur;
}
public class Client {
@SerializedName("BillingCity")
public String billingCity;
@SerializedName("BillingPostalCode")
public String billingPostalCode;
@SerializedName("BillingStreet")
public String billingStreet;
@SerializedName("ComplementNom")
public String complementNom;
@SerializedName("Email")
public String email;
@SerializedName("FirstName")
public String firstName;
@SerializedName("GC")
public boolean gC;
@SerializedName("Id")
public String id;
@SerializedName("LastName")
public String lastName;
@SerializedName("MobilePhone")
public String mobilePhone;
@SerializedName("Name")
public String name;
@SerializedName("Salutation")
public String salutation;
@SerializedName("VCRM_ID")
public String vCrmId;
@SerializedName("VerrouillageFiche")
public boolean verrouillageFiche;
}
public List<CompteInfo> compteInfo = new ArrayList<CompteInfo>();
public @Nullable Contract contrat;
public @Nullable Client contratClient;
public @Nullable Client payeurClient;
public boolean compteParticulier;
public boolean multipleContrats;
public float solde;
}
@@ -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.sedif.internal.dto;
import java.util.ArrayList;
import java.util.List;
import com.google.gson.annotations.SerializedName;
/**
* The {@link Contracts} holds Contract information
*
* @author Laurent Arnal - Initial contribution
*/
public class Contracts extends Value {
@SerializedName("contrats")
public List<Contract> contracts = new ArrayList<Contract>();
}
@@ -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.sedif.internal.dto;
import java.util.Hashtable;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link Event} holds authentication information
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public class Event {
public class Attributes {
public Hashtable<String, Object> values = new Hashtable<String, Object>();
}
public @Nullable String descriptor;
public @Nullable Attributes attributes;
}
@@ -0,0 +1,112 @@
/*
* 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.sedif.internal.dto;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* The {@link MeterReading} holds Meter reading values
*
* @author Laurent Arnal - Initial contribution
*/
public class MeterReading extends Value {
public class Data {
public Data() {
hasModifications = false;
consoMap = new HashMap<String, Consommation>();
}
public class Consommation {
@SerializedName("CONSOMMATION")
public float consommation;
@SerializedName("DATE_INDEX")
public LocalDateTime dateIndex;
@SerializedName("DEBIT_PERMANENT")
public String debitPermanent;
@SerializedName("FLAG_ESTIMATION")
public boolean flagEstimation;
@SerializedName("FLAG_ESTIMATION_INDEX")
public boolean flagEstimationIndex;
@SerializedName("VALEUR_INDEX")
public float valeurIndex;
}
private transient Map<String, Consommation> consoMap;
public void putEntries(String key, Consommation conso) {
if (consoMap == null) {
consoMap = new HashMap<String, MeterReading.Data.Consommation>();
}
consoMap.put(key, conso);
}
public Consommation getEntries(String key) {
return consoMap.get(key);
}
@SerializedName("CONSOMMATION")
public @Nullable Consommation[] consommation;
public @Nullable Consommation[] weekConso;
public @Nullable Consommation[] monthConso;
public @Nullable Consommation[] yearConso;
@SerializedName("CONSOMMATION_MAX")
public float consommationMax;
@SerializedName("CONSOMMATION_MOYENNE")
public float consommationMoyenne;
@SerializedName("DATE_CONSOMMATION_MAX")
public String dateConsommationMax;
@SerializedName("DATE_DEBUT")
public String dateDebut;
@SerializedName("DATE_FIN")
public String dateFin;
@SerializedName("ID_PDS")
public String idPds;
@SerializedName("NUMERO_COMPTEUR")
public String numeroCompteur;
public boolean hasModifications;
public boolean hasModifications() {
return hasModifications;
}
}
public Data data;
public boolean showDebitPermanent;
public float seuilDebitPermanet;
public float prixMoyenEau;
public boolean canCompareMonth;
public String numeroCompteur;
public MeterReading() {
data = new Data();
}
}
@@ -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.sedif.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link PerfSummary} holds authentication information
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public class PerfSummary {
public @Nullable String version;
public @Nullable String request;
public int actionsTotal;
public int overhead;
}
@@ -0,0 +1,61 @@
/*
* 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.sedif.internal.dto;
import java.time.LocalDate;
import org.openhab.binding.sedif.internal.api.helpers.MeterReadingHelper;
import org.openhab.binding.sedif.internal.types.SedifException;
/**
* The {@link SedifState} a base class for Value
*
* @author Laurent Arnal - Initial contribution
*/
public class SedifState {
public boolean hasModifications() {
return hasModifications;
}
private boolean hasModifications;
private MeterReading meterReading;
private LocalDate lastIndexDate;
public LocalDate getLastIndexDate() {
return lastIndexDate;
}
public void setLastIndexDate(LocalDate lastIndexDate) {
this.lastIndexDate = lastIndexDate;
hasModifications = true;
}
public MeterReading updateMeterReading(MeterReading incomingMeterReading) throws SedifException {
if (incomingMeterReading == null) {
return this.meterReading;
}
MeterReadingHelper.check(incomingMeterReading);
if (this.meterReading == null) {
this.meterReading = new MeterReading();
}
return MeterReadingHelper.merge(this.meterReading, incomingMeterReading);
}
public MeterReading getMeterReading() {
return meterReading;
}
}
@@ -0,0 +1,21 @@
/*
* 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.sedif.internal.dto;
/**
* The {@link Value} a base class for Value
*
* @author Laurent Arnal - Initial contribution
*/
public abstract class Value {
}
@@ -0,0 +1,124 @@
/*
* 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.sedif.internal.factory;
import static org.openhab.binding.sedif.internal.constants.SedifBindingConstants.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.sedif.internal.api.gson.FloatTypeAdapter;
import org.openhab.binding.sedif.internal.api.parse.RuntimeTypeAdapterFactory;
import org.openhab.binding.sedif.internal.dto.ContractDetail;
import org.openhab.binding.sedif.internal.dto.Contracts;
import org.openhab.binding.sedif.internal.dto.MeterReading;
import org.openhab.binding.sedif.internal.dto.Value;
import org.openhab.binding.sedif.internal.handler.BridgeSedifWebHandler;
import org.openhab.binding.sedif.internal.handler.ThingSedifHandler;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
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 com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializer;
/**
* The {@link SedifHandlerFactory} is responsible for creating things handlers.
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
@Component(immediate = true, service = ThingHandlerFactory.class, configurationPid = "binding.sedif")
public class SedifHandlerFactory extends BaseThingHandlerFactory {
private final HttpClientFactory httpClientFactory;
private final TimeZoneProvider timeZoneProvider;
public static final DateTimeFormatter SEDIF_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSX");
public static final DateTimeFormatter SEDIF_LOCALDATE_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd");
public static final DateTimeFormatter SEDIF_LOCALDATETIME_FORMATTER = DateTimeFormatter
.ofPattern("uuuu-MM-dd' 'HH:mm:ss");
private final Gson gson;
private final LocaleProvider localeProvider;
@Activate
public SedifHandlerFactory(final @Reference LocaleProvider localeProvider,
final @Reference HttpClientFactory httpClientFactory, final @Reference TimeZoneProvider timeZoneProvider) {
this.localeProvider = localeProvider;
this.timeZoneProvider = timeZoneProvider;
this.httpClientFactory = httpClientFactory;
RuntimeTypeAdapterFactory<Value> adapter = RuntimeTypeAdapterFactory.of(Value.class);
adapter.registerSubtype(Contracts.class, "contrats", "Contracts");
adapter.registerSubtype(ContractDetail.class, "compteInfo", "ContractDetail");
adapter.registerSubtype(MeterReading.class, "data", "Datas");
gson = new GsonBuilder().registerTypeAdapterFactory(adapter).setDateFormat("yyyy-MM-dd")
// LocalDate
.registerTypeAdapter(LocalDate.class,
(JsonSerializer<LocalDate>) (src, typeOfSrc,
context) -> new JsonPrimitive(src.format(SEDIF_LOCALDATE_FORMATTER)))
.registerTypeAdapter(LocalDate.class,
(JsonDeserializer<LocalDate>) (json, type, jsonDeserializationContext) -> LocalDate
.parse(json.getAsJsonPrimitive().getAsString(), SEDIF_LOCALDATE_FORMATTER))
// LocalDateTime
.registerTypeAdapter(LocalDateTime.class,
(JsonDeserializer<LocalDateTime>) (json, type, jsonDeserializationContext) -> LocalDateTime
.parse(json.getAsJsonPrimitive().getAsString(), SEDIF_LOCALDATETIME_FORMATTER))
.registerTypeAdapter(LocalDateTime.class,
(JsonSerializer<LocalDateTime>) (src, typeOfSrc,
context) -> new JsonPrimitive(src.format(SEDIF_LOCALDATETIME_FORMATTER)))
.registerTypeAdapter(float.class, new FloatTypeAdapter()).setPrettyPrinting().create();
}
@Override
protected void activate(ComponentContext componentContext) {
super.activate(componentContext);
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_DEVICE_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
if (THING_TYPE_GATEWAY_BRIDGE.equals(thing.getThingTypeUID())) {
BridgeSedifWebHandler handler = new BridgeSedifWebHandler((Bridge) thing, this.httpClientFactory, gson);
return handler;
} else if (THING_TYPE_METER.equals(thing.getThingTypeUID())) {
ThingSedifHandler handler = new ThingSedifHandler(thing, localeProvider, timeZoneProvider, gson);
return handler;
}
return null;
}
}
@@ -0,0 +1,190 @@
/*
* 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.sedif.internal.handler;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
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.sedif.internal.api.SedifHttpApi;
import org.openhab.binding.sedif.internal.config.SedifBridgeConfiguration;
import org.openhab.binding.sedif.internal.constants.SedifBindingConstants;
import org.openhab.binding.sedif.internal.discovery.SedifDiscoveryService;
import org.openhab.binding.sedif.internal.dto.Contract;
import org.openhab.binding.sedif.internal.dto.ContractDetail;
import org.openhab.binding.sedif.internal.dto.MeterReading;
import org.openhab.binding.sedif.internal.helpers.SedifListener;
import org.openhab.binding.sedif.internal.types.SedifException;
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.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* {@link BridgeSedifWebHandler} is the base handler to access sedif data.
*
* @author Laurent Arnal - Initial contribution
*
*/
@NonNullByDefault
public class BridgeSedifWebHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(BridgeSedifWebHandler.class);
private Set<SedifListener> listeners = new CopyOnWriteArraySet<>();
protected final HttpClient httpClient;
protected final SedifHttpApi sedifApi;
protected final Gson gson;
private static final int REQUEST_BUFFER_SIZE = 8000;
private static final int RESPONSE_BUFFER_SIZE = 200000;
public BridgeSedifWebHandler(Bridge bridge, final @Reference HttpClientFactory httpClientFactory, 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.httpClient = httpClientFactory.createHttpClient(SedifBindingConstants.BINDING_ID, sslContextFactory);
this.httpClient.setFollowRedirects(false);
this.httpClient.setRequestBufferSize(REQUEST_BUFFER_SIZE);
this.httpClient.setResponseBufferSize(RESPONSE_BUFFER_SIZE);
try {
httpClient.start();
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
this.sedifApi = new SedifHttpApi(gson, this.httpClient);
}
@Override
public void dispose() {
super.dispose();
try {
httpClient.stop();
} catch (Exception e) {
logger.debug("HttpClient failed on bridge disposal: {}", e.getMessage(), e);
}
}
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
scheduleConnection(1);
}
public void scheduleConnection(int delay) {
scheduler.schedule(() -> {
try {
SedifBridgeConfiguration config = getConfigAs(SedifBridgeConfiguration.class);
sedifApi.connectionInit(config.username, config.password);
Map<String, Contract> contracts = sedifApi.getAllContracts();
for (Contract contract : contracts.values()) {
fireOnContractReceivedEvent(contract);
}
if (sedifApi.isConnected()) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection failed");
}
} catch (SedifException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}, delay, TimeUnit.SECONDS);
}
public void scheduleReconnect() {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
sedifApi.disconnect();
scheduleConnection(30);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
public SedifHttpApi getSedifApi() {
return sedifApi;
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Set.of(SedifDiscoveryService.class);
}
public void addListener(final SedifListener listener) {
listeners.add(listener);
}
public void removeListener(final SedifListener listener) {
listeners.remove(listener);
}
protected void fireOnContractReceivedEvent(final Contract contract) {
listeners.forEach(l -> l.onContractInit(contract));
}
public @Nullable Contract getContract(String contractName) {
return sedifApi.getContract(contractName);
}
public @Nullable ContractDetail getContractDetails(String contractId) throws SedifException {
return sedifApi.getContractDetails(contractId);
}
public @Nullable MeterReading getConsumptionData(String contractId, ContractDetail.CompteInfo meterInfo,
LocalDate from, LocalDate to) throws SedifException {
logger.debug("getConsumptionData for from {} to {}", from.format(DateTimeFormatter.ISO_LOCAL_DATE),
to.format(DateTimeFormatter.ISO_LOCAL_DATE));
return sedifApi.getConsumptionData(contractId, meterInfo, from, to);
}
}
@@ -0,0 +1,688 @@
/*
* 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.sedif.internal.handler;
import static org.openhab.binding.sedif.internal.constants.SedifBindingConstants.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.time.temporal.WeekFields;
import java.util.Map;
import java.util.Objects;
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.sedif.internal.api.ExpiringDayCache;
import org.openhab.binding.sedif.internal.api.helpers.MeterReadingHelper;
import org.openhab.binding.sedif.internal.config.SedifConfiguration;
import org.openhab.binding.sedif.internal.dto.Contract;
import org.openhab.binding.sedif.internal.dto.ContractDetail;
import org.openhab.binding.sedif.internal.dto.ContractDetail.CompteInfo;
import org.openhab.binding.sedif.internal.dto.MeterReading;
import org.openhab.binding.sedif.internal.dto.MeterReading.Data.Consommation;
import org.openhab.binding.sedif.internal.dto.SedifState;
import org.openhab.binding.sedif.internal.types.CommunicationFailedException;
import org.openhab.binding.sedif.internal.types.InvalidSessionException;
import org.openhab.binding.sedif.internal.types.SedifException;
import org.openhab.core.OpenHAB;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.CurrencyUnits;
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.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
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;
import com.google.gson.Gson;
/**
* The {@link ThingSedifHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public class ThingSedifHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(ThingSedifHandler.class);
private @Nullable ScheduledFuture<?> refreshJob;
private static final String JSON_DIR = OpenHAB.getUserDataFolder() + File.separatorChar + "sedif";
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 static final int HISTORICAL_LOOKBACK_DAYS = 89;
private String contractName;
private String contractId;
private String numCompteur;
private @Nullable CompteInfo currentMeterInfo;
private SedifState sedifState;
private final ExpiringDayCache<ContractDetail> contractDetail;
private final ExpiringDayCache<MeterReading> consumption;
private final Gson gson;
public ThingSedifHandler(Thing thing, LocaleProvider localeProvider, TimeZoneProvider timeZoneProvider, Gson gson) {
super(thing);
contractName = "";
contractId = "";
numCompteur = "";
this.gson = gson;
this.sedifState = new SedifState();
this.contractDetail = new ExpiringDayCache<ContractDetail>("contractDetail", REFRESH_HOUR_OF_DAY,
REFRESH_MINUTE_OF_DAY, () -> {
try {
Bridge lcBridge = getBridge();
if (lcBridge != null && lcBridge.getHandler() instanceof BridgeSedifWebHandler bridgeSedif) {
return bridgeSedif.getContractDetails(contractId);
}
return null;
} catch (CommunicationFailedException ex) {
// We just return null, EpiringDayCache logic will retry the operation later.
logger.error("Failed to get contract details", ex);
return null;
} catch (InvalidSessionException ex) {
// In case of session error, we force the bridge to reconnect after a delay
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
Bridge lcBridge = getBridge();
if (lcBridge != null && lcBridge.getHandler() instanceof BridgeSedifWebHandler bridgeSedif) {
bridgeSedif.scheduleReconnect();
}
return null;
} catch (SedifException ex) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ex.getMessage());
return null;
}
});
this.consumption = new ExpiringDayCache<MeterReading>("dailyConsumption", REFRESH_HOUR_OF_DAY,
REFRESH_MINUTE_OF_DAY, () -> {
LocalDate today = LocalDate.now();
try {
MeterReading meterReading = getConsumptionData(today.minusDays(HISTORICAL_LOOKBACK_DAYS), today,
false);
if (meterReading != null) {
MeterReadingHelper.calcAgregat(meterReading);
}
return meterReading;
} catch (CommunicationFailedException ex) {
// We just return null, EpxiringDayCache logic will retry the operation later.
logger.warn("Failed to get consumption data", ex);
return null;
} catch (InvalidSessionException ex) {
// In case of session error, we force the bridge to reconnect after a delay
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
Bridge lcBridge = getBridge();
if (lcBridge != null && lcBridge.getHandler() instanceof BridgeSedifWebHandler bridgeSedif) {
bridgeSedif.scheduleReconnect();
}
return null;
} catch (SedifException ex) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ex.getMessage());
return null;
}
});
}
@Override
public void initialize() {
Bridge bridge = getBridge();
if (bridge != null && bridge.getHandler() instanceof BridgeSedifWebHandler bridgeHandler) {
initialize(bridgeHandler, bridge.getStatus());
} else {
initialize(null, bridge == null ? null : bridge.getStatus());
}
}
private void initialize(@Nullable BridgeSedifWebHandler bridgeHandler, @Nullable ThingStatus bridgeStatus) {
if (bridgeHandler == null || bridgeStatus == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
return;
}
if (bridgeStatus != ThingStatus.ONLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return;
}
SedifConfiguration lcConfig = getConfigAs(SedifConfiguration.class);
if (!lcConfig.seemsValid()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.config-error-mandatory-settings");
return;
}
contractName = lcConfig.contractId;
numCompteur = lcConfig.meterId;
contractDetail.invalidate();
consumption.invalidate();
updateStatus(ThingStatus.UNKNOWN);
scheduler.submit(() -> {
initializeAndSetupRefreshJob(bridgeHandler);
});
}
private synchronized void initializeAndSetupRefreshJob(BridgeSedifWebHandler bridgeHandler) {
loadSedifState();
if (sedifState.getLastIndexDate() == null) {
sedifState.setLastIndexDate(LocalDate.of(1980, 1, 1));
saveSedifState();
}
setupRefreshJob(bridgeHandler);
}
private void loadSedifState() {
File folder = new File(JSON_DIR);
if (!folder.exists()) {
logger.debug("Creating directory {}", folder);
folder.mkdirs();
}
File file = null;
try {
file = new File(JSON_DIR + File.separator + "sedif_" + numCompteur + ".json");
if (file.exists()) {
byte[] bytes = Files.readAllBytes(file.toPath());
String content = new String(bytes, StandardCharsets.UTF_8);
SedifState newState = gson.fromJson(content, SedifState.class);
if (newState != null) {
sedifState = newState;
}
logger.debug("Sedif MetaData information read from {}", file.getAbsolutePath());
}
} catch (InterruptedIOException ioe) {
logger.warn("Couldn't read Sedif MetaData information from file '{}'.", file.getAbsolutePath());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Couldn't read Sedif MetaData information from file " + file.getAbsolutePath());
} catch (IOException ioe) {
logger.warn("Couldn't read Sedif MetaData information from file '{}'.", file.getAbsolutePath());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Couldn't read Sedif MetaData information from file " + file.getAbsolutePath());
}
}
private void saveSedifState() {
File file = null;
if (!sedifState.hasModifications()) {
return;
}
try {
file = new File(JSON_DIR + File.separator + "sedif_" + numCompteur + ".json");
if (!file.exists()) {
File parentFile = file.getParentFile();
if (parentFile != null) {
parentFile.mkdirs();
}
file.createNewFile();
}
try (FileOutputStream os = new FileOutputStream(file)) {
String js = gson.toJson(sedifState);
byte[] bt = js.getBytes();
os.write(bt);
os.flush();
}
logger.debug("Sedif MetaData information wriiten to {}", file.getAbsolutePath());
} catch (IOException ioe) {
logger.warn("Couldn't write Sedif MetaData information to file '{}'.", file.getAbsolutePath());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Couldn't write Sedif MetaData information to file " + file.getAbsolutePath());
}
}
public void cancelRefreshJob() {
ScheduledFuture<?> job = this.refreshJob;
if (job != null && !job.isCancelled()) {
job.cancel(true);
refreshJob = null;
}
}
@Override
public void dispose() {
logger.debug("Disposing the Sedif handler {}", numCompteur);
cancelRefreshJob();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
logger.debug("Refreshing channel {}", channelUID.getId());
updateData();
} else {
logger.debug("The Sedif binding is read-only and can not handle command {}", command);
}
}
/**
* Request new data and updates channels
*/
private void updateData() {
updateContractDetail();
updateHistoricalConsumptionData();
updateConsumptionData();
saveSedifState();
}
private void updateHistoricalConsumptionData() {
logger.trace("updateHistoricalConsumptionData() called");
int periodLength = 90;
LocalDate currentDate = LocalDate.now();
currentDate = currentDate.minusDays(periodLength);
LocalDate lastUpdateDate = sedifState.getLastIndexDate();
if (!currentDate.isAfter(lastUpdateDate)) {
return;
}
LocalDate newLastUpdateDate = lastUpdateDate;
boolean hasData = true;
boolean hasAlreadyRetrieveData = false;
while ((hasData || !hasAlreadyRetrieveData) && currentDate.isAfter(lastUpdateDate)) {
LocalDate startDate = currentDate.minusDays(periodLength - 1);
try {
logger.trace("Retrieve data from {} to {}:", startDate, currentDate);
MeterReading meterReading = getConsumptionData(startDate, currentDate, true);
if (meterReading != null) {
Consommation[] consommation = meterReading.data.consommation;
if (consommation != null) {
newLastUpdateDate = consommation[consommation.length - 1].dateIndex.toLocalDate();
}
hasData = true;
} else {
hasData = false;
}
if (hasData) {
hasAlreadyRetrieveData = true;
} else {
currentDate = startDate;
}
if (hasAlreadyRetrieveData) {
if (!hasData) {
continue;
}
}
} catch (SedifException ex) {
logger.warn("Unable to retrieve data from {} to {}:", startDate, currentDate, ex);
}
currentDate = startDate;
}
sedifState.setLastIndexDate(newLastUpdateDate);
saveSedifState();
}
public @Nullable MeterReading getConsumptionData(LocalDate startDate, LocalDate currentDate,
boolean updateHistorical) throws SedifException {
logger.trace("startDate: {}, currentDate: {}", startDate, currentDate);
Bridge lcBridge = getBridge();
if (lcBridge != null && lcBridge.getHandler() instanceof BridgeSedifWebHandler bridgeSedif) {
CompteInfo lcCurrentMeterInfo = currentMeterInfo;
if (lcCurrentMeterInfo != null) {
MeterReading meterReading = bridgeSedif.getConsumptionData(contractId, lcCurrentMeterInfo, startDate,
currentDate);
if (updateHistorical && meterReading == null) {
return null;
}
if (meterReading != null) {
return sedifState.updateMeterReading(meterReading);
} else {
return sedifState.getMeterReading();
}
} else {
throw new SedifException("currentMeterInfo is null");
}
}
return null;
}
/**
* Request new daily/weekly data and updates channels
*/
private void updateContractDetail() {
logger.trace("updateContractDetail() called");
contractDetail.getValue().ifPresentOrElse(values -> {
CompteInfo meterInfo = null;
for (CompteInfo compteInfo : values.compteInfo) {
if (compteInfo.numCompteur.equals(numCompteur)) {
meterInfo = compteInfo;
}
}
if (meterInfo == null || meterInfo.eLma.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/cant-find-meter");
return;
}
this.currentMeterInfo = meterInfo;
Map<String, String> props = this.editProperties();
Contract contrat = values.contrat;
if (contrat != null) {
addProps(props, PROPERTY_CONTRACT_ORGANIZING_AUTHORITY, contrat.autoriteOrganisatrice);
addProps(props, PROPERTY_CONTRACT_DATE_SORTIE_EPT, contrat.dateSortieEPT);
addProps(props, PROPERTY_CONTRACT_ICL_ACTIVE, "" + contrat.iclActive);
addProps(props, PROPERTY_CONTRACT_NAME, contrat.name);
}
addProps(props, PROPERTY_CONTRACT_BALANCE, "" + values.solde);
CompteInfo comptInfo = values.compteInfo.get(0);
addProps(props, PROPERTY_ELMA, comptInfo.eLma);
addProps(props, PROPERTY_ELMB, comptInfo.eLmb);
addProps(props, PROPERTY_ID_PDS, comptInfo.idPds);
addProps(props, PROPERTY_NUM_METER, comptInfo.numCompteur);
updateProperties(props);
}, () -> {
updateState(GROUP_BASE, CHANNEL_CONSUMPTION, new QuantityType<>(0.00, Units.LITRE));
});
}
private void addProps(Map<String, String> props, String key, @Nullable String value) {
if (value == null || value.isBlank()) {
return;
}
props.put(key, value);
}
private float getConso(MeterReading mr, String dtKey) {
Consommation conso = mr.data.getEntries(dtKey);
if (conso != null) {
return conso.consommation;
}
return 0;
}
/**
* Request new daily/weekly data and updates channels
*/
private void updateConsumptionData() {
logger.trace("updateConsumptionData() called");
consumption.getValue().ifPresentOrElse(values -> {
logger.trace("updateConsumptionData:getValue()");
LocalDate now = LocalDate.now();
Consommation[] consommation = values.data.consommation;
// ===========================
// Daily conso
// ===========================
double yesterdayConso = 0;
double dayConsoMinus2 = 0;
double dayConsoMinus3 = 0;
String yesterday = now.minusDays(1).toString();
String dayMinus2 = now.minusDays(2).toString();
String dayMinus3 = now.minusDays(3).toString();
yesterdayConso = getConso(values, yesterday);
dayConsoMinus2 = getConso(values, dayMinus2);
dayConsoMinus3 = getConso(values, dayMinus3);
logger.trace("updateConsumptionData> updateState/yesterdayConso : {}", yesterdayConso);
logger.trace("updateConsumptionData> updateState/dayConsoMinus2 : {}", dayConsoMinus2);
logger.trace("updateConsumptionData> updateState/dayConsoMinus3 : {}", dayConsoMinus3);
updateState(GROUP_DAILY_CONSUMPTION, CHANNEL_DAILY_YESTERDAY_CONSUMPTION,
new QuantityType<>(yesterdayConso, Units.LITRE));
updateState(GROUP_DAILY_CONSUMPTION, CHANNEL_DAILY_DAY_MINUS_2_CONSUMPTION,
new QuantityType<>(dayConsoMinus2, Units.LITRE));
updateState(GROUP_DAILY_CONSUMPTION, CHANNEL_DAILY_DAY_MINUS_3_CONSUMPTION,
new QuantityType<>(dayConsoMinus3, Units.LITRE));
// ===========================
// Week conso
// ===========================
double thisWeekConso = 0;
double lastWeekConso = 0;
double weekConsoMinus2 = 0;
Consommation[] weekConso = values.data.weekConso;
LocalDate thisWeek = now;
LocalDate lastWeek = now.minusWeeks(1);
LocalDate weekMinus2 = now.minusWeeks(2);
String thisWeekKey = thisWeek.get(WeekFields.ISO.weekBasedYear()) + "-w-"
+ thisWeek.get(WeekFields.ISO.weekOfWeekBasedYear());
String lastWeekKey = lastWeek.get(WeekFields.ISO.weekBasedYear()) + "-w-"
+ lastWeek.get(WeekFields.ISO.weekOfWeekBasedYear());
String weekMinus2Key = weekMinus2.get(WeekFields.ISO.weekBasedYear()) + "-w-"
+ weekMinus2.get(WeekFields.ISO.weekOfWeekBasedYear());
thisWeekConso = getConso(values, thisWeekKey);
lastWeekConso = getConso(values, lastWeekKey);
weekConsoMinus2 = getConso(values, weekMinus2Key);
updateState(GROUP_WEEKLY_CONSUMPTION, CHANNEL_WEEKLY_THIS_WEEK_CONSUMPTION,
new QuantityType<>(thisWeekConso, Units.LITRE));
updateState(GROUP_WEEKLY_CONSUMPTION, CHANNEL_WEEKLY_LAST_WEEK_CONSUMPTION,
new QuantityType<>(lastWeekConso, Units.LITRE));
updateState(GROUP_WEEKLY_CONSUMPTION, CHANNEL_WEEKLY_WEEK_MINUS_2_CONSUMPTION,
new QuantityType<>(weekConsoMinus2, Units.LITRE));
// ===========================
// Month conso
// ===========================
double thisMonthConso = 0;
double lastMonthConso = 0;
double monthConsoMinus2 = 0;
LocalDate thisMonth = now;
LocalDate lastMonth = now.minusMonths(1);
LocalDate monthMinus2 = now.minusMonths(2);
String thisMonthKey = thisMonth.getYear() + "-" + thisMonth.getMonthValue();
String lastMonthKey = lastMonth.getYear() + "-" + lastMonth.getMonthValue();
String monthMinus2Key = monthMinus2.getYear() + "-" + monthMinus2.getMonthValue();
thisMonthConso = getConso(values, thisMonthKey);
lastMonthConso = getConso(values, lastMonthKey);
monthConsoMinus2 = getConso(values, monthMinus2Key);
updateState(GROUP_MONTHLY_CONSUMPTION, CHANNEL_MONTHLY_THIS_MONTH_CONSUMPTION,
new QuantityType<>(thisMonthConso, Units.LITRE));
updateState(GROUP_MONTHLY_CONSUMPTION, CHANNEL_MONTHLY_LAST_MONTH_CONSUMPTION,
new QuantityType<>(lastMonthConso, Units.LITRE));
updateState(GROUP_MONTHLY_CONSUMPTION, CHANNEL_MONTHLY_MONTH_MINUS_2_CONSUMPTION,
new QuantityType<>(monthConsoMinus2, Units.LITRE));
// ===========================
// Year conso
// ===========================
double thisYearConso = 0;
double lastYearConso = 0;
double yearConsoMinus2 = 0;
String thisYearKey = "" + now.getYear();
String lastYearKey = "" + now.minusYears(1).getYear();
String yearMinus2Key = "" + now.minusYears(2).getYear();
thisYearConso = getConso(values, thisYearKey);
lastYearConso = getConso(values, lastYearKey);
yearConsoMinus2 = getConso(values, yearMinus2Key);
updateState(GROUP_YEARLY_CONSUMPTION, CHANNEL_YEARLY_THIS_YEAR_CONSUMPTION,
new QuantityType<>(thisYearConso, Units.LITRE));
updateState(GROUP_YEARLY_CONSUMPTION, CHANNEL_YEARLY_LAST_YEAR_CONSUMPTION,
new QuantityType<>(lastYearConso, Units.LITRE));
updateState(GROUP_YEARLY_CONSUMPTION, CHANNEL_YEARLY_YEAR_MINUS_2_CONSUMPTION,
new QuantityType<>(yearConsoMinus2, Units.LITRE));
updateState(GROUP_BASE, CHANNEL_MEAN_WATER_PRICE,
new QuantityType<>(values.prixMoyenEau, CurrencyUnits.BASE_CURRENCY));
if (consommation != null && consommation.length > 0) {
sedifState.setLastIndexDate(consommation[consommation.length - 1].dateIndex.toLocalDate());
}
updateConsumptionTimeSeries(GROUP_DAILY_CONSUMPTION, CHANNEL_CONSUMPTION, values.data.consommation);
updateConsumptionTimeSeries(GROUP_WEEKLY_CONSUMPTION, CHANNEL_CONSUMPTION, values.data.weekConso);
updateConsumptionTimeSeries(GROUP_MONTHLY_CONSUMPTION, CHANNEL_CONSUMPTION, values.data.monthConso);
updateConsumptionTimeSeries(GROUP_YEARLY_CONSUMPTION, CHANNEL_CONSUMPTION, values.data.yearConso);
logger.trace("updateConsumptionData:getValue() end");
}, () -> {
logger.trace("updateConsumptionData:getValue():noValuePresent");
updateState(GROUP_BASE, CHANNEL_CONSUMPTION, new QuantityType<>(0.00, Units.LITRE));
});
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
Bridge bridge = getBridge();
if (bridge != null && bridge.getHandler() instanceof BridgeSedifWebHandler bridgeHandler) {
initialize(bridgeHandler, bridgeStatusInfo.getStatus());
} else {
initialize(null, bridgeStatusInfo.getStatus());
}
}
private void setupRefreshJob(BridgeSedifWebHandler bridgeHandler) {
Contract contract = bridgeHandler.getContract(contractName);
if (contract != null) {
contractId = Objects.requireNonNull(contract.id);
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);
cancelRefreshJob();
long initialDelay = ChronoUnit.MINUTES.between(now, nextDayFirstTimeUpdate) % REFRESH_INTERVAL_IN_MIN + 1;
long delay = REFRESH_INTERVAL_IN_MIN;
if (!consumption.isPresent() || !contractDetail.isPresent()) {
initialDelay = 20;
}
refreshJob = scheduler.scheduleWithFixedDelay(this::updateData, initialDelay, delay, TimeUnit.MINUTES);
if (this.getThing().getStatus() == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.missing-or-invalid-contract");
}
}
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 void updateConsumptionTimeSeries(String groupId, String channelId, Consommation[] consoTab) {
TimeSeries timeSeries = new TimeSeries(Policy.REPLACE);
for (int i = 0; i < consoTab.length; i++) {
Consommation conso = consoTab[i];
if (conso == null) {
continue;
}
LocalDateTime dt = conso.dateIndex;
float consommation = conso.consommation;
if (dt == null) {
// dt can be null if we have no value in initial container day for this month !
continue;
}
Instant timestamp = dt.toInstant(ZoneOffset.UTC);
if (Double.isNaN(consommation)) {
continue;
}
timeSeries.add(timestamp, new DecimalType(consommation));
}
sendTimeSeries(groupId, channelId, timeSeries);
}
public String getContractId() {
return contractId;
}
public String getNumCompteur() {
return numCompteur;
}
}
@@ -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.sedif.internal.helpers;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.sedif.internal.dto.Contract;
/**
* The {@link SedifListener} interface defines all events pushed by a
* {@link BridgeLocalBaseHandler}.
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public interface SedifListener {
/**
* The method is call when we detect a new contract on the sedif account.
*
* @param contract : the contract that was detected.
*/
void onContractInit(Contract contract);
}
@@ -0,0 +1,45 @@
/*
* 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.sedif.internal.types;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Will be thrown for cloud errors
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public class CommunicationFailedException extends SedifException {
private static final long serialVersionUID = 3703839284673384018L;
public CommunicationFailedException() {
super();
}
public CommunicationFailedException(String message) {
super(message);
}
public CommunicationFailedException(String message, Throwable cause) {
super(message, cause);
}
public CommunicationFailedException(String message, Object... params) {
this(message.formatted(params));
}
public CommunicationFailedException(String message, Throwable cause, Object... params) {
this(message.formatted(params), cause);
}
}
@@ -0,0 +1,45 @@
/*
* 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.sedif.internal.types;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Will be thrown for cloud errors
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public class InvalidSessionException extends SedifException {
private static final long serialVersionUID = 3703839284673384018L;
public InvalidSessionException() {
super();
}
public InvalidSessionException(String message) {
super(message);
}
public InvalidSessionException(String message, Throwable cause) {
super(message, cause);
}
public InvalidSessionException(String message, Object... params) {
this(message.formatted(params));
}
public InvalidSessionException(String message, Throwable cause, Object... params) {
this(message.formatted(params), cause);
}
}
@@ -0,0 +1,45 @@
/*
* 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.sedif.internal.types;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Will be thrown for cloud errors
*
* @author Laurent Arnal - Initial contribution
*/
@NonNullByDefault
public class SedifException extends Exception {
private static final long serialVersionUID = 3703839284673384018L;
public SedifException() {
super();
}
public SedifException(String message) {
super(message);
}
public SedifException(String message, Throwable cause) {
super(message, cause);
}
public SedifException(String message, Object... params) {
this(message.formatted(params));
}
public SedifException(String message, Throwable cause, Object... params) {
this(message.formatted(params), cause);
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon:addon id="sedif" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
<type>binding</type>
<name>Sedif Binding</name>
<description>Retrieves your water consumption data from Sedif website</description>
<connection>cloud</connection>
<countries>fr</countries>
</addon:addon>
@@ -0,0 +1,59 @@
# add-on
addon.sedif.name = Sedif Binding
addon.sedif.description = Retrieves your water consumption data from Sedif website
# thing types
thing-type.sedif.gateway.label = Sedif Web Bridge
thing-type.sedif.gateway.description = Act as a gateway between your sedif Account and a Sedif mether things. In order to receive the data, you must activate your account on Sedif Web Site.
thing-type.sedif.meter.label = Meter
thing-type.sedif.meter.description = Provides your water consumption data. If you don't use autodiscovery, you will need to get your contractName from this Sedif web site, and eventually your meterId if you have multiple meter on the same account. See the documentation about where to find this informations.
# thing types config
thing-type.config.sedif.gateway.password.label = Password
thing-type.config.sedif.gateway.password.description = Your Sedif Password
thing-type.config.sedif.gateway.username.label = Username
thing-type.config.sedif.gateway.username.description = Your Sedif Username
thing-type.config.sedif.meter.contractId.label = Contract Id
thing-type.config.sedif.meter.contractId.description = Your Contract Id
thing-type.config.sedif.meter.meterId.label = Meter Id
thing-type.config.sedif.meter.meterId.description = Your Meter Id
# channel group types
channel-group-type.sedif.base.label = Standard Base
channel-group-type.sedif.base.channel.mean-water-price.label = Mean Water Price
channel-group-type.sedif.daily-consumption.label = Daily Consumption
channel-group-type.sedif.daily-consumption.channel.consumption.label = Consumption
channel-group-type.sedif.daily-consumption.channel.day-2.label = Day -2 Consumption
channel-group-type.sedif.daily-consumption.channel.day-3.label = Day -3 Consumption
channel-group-type.sedif.daily-consumption.channel.yesterday.label = Yesterday Consumption
channel-group-type.sedif.monthly-consumption.label = Monthly Consumption
channel-group-type.sedif.monthly-consumption.channel.consumption.label = Consumption
channel-group-type.sedif.monthly-consumption.channel.last-month.label = Last Month Consumption
channel-group-type.sedif.monthly-consumption.channel.month-2.label = Month -2 Consumption
channel-group-type.sedif.monthly-consumption.channel.this-month.label = This Month Consumption
channel-group-type.sedif.weekly-consumption.label = Weekly Consumption
channel-group-type.sedif.weekly-consumption.channel.consumption.label = Consumption
channel-group-type.sedif.weekly-consumption.channel.last-week.label = Last Week Consumption
channel-group-type.sedif.weekly-consumption.channel.this-week.label = This Week Consumption
channel-group-type.sedif.weekly-consumption.channel.week-2.label = Week -2 Consumption
channel-group-type.sedif.yearly-consumption.label = Yearly Consumption
channel-group-type.sedif.yearly-consumption.channel.consumption.label = Consumption
channel-group-type.sedif.yearly-consumption.channel.last-year.label = Last Year Consumption
channel-group-type.sedif.yearly-consumption.channel.this-year.label = This Year Consumption
channel-group-type.sedif.yearly-consumption.channel.year-2.label = Year -2 Consumption
# channel types
channel-type.sedif.consumption.label = Total Consumption
channel-type.sedif.consumption.description = Consumption at given time interval
channel-type.sedif.water-price.label = Water Price
# channel types
offline.config-error-mandatory-settings = Incomplete configuration
offline.missing-or-invalid-contract = The contract can''t be found, please check your contract Id
cant-find-meter = Can't find meter for meterId
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="sedif"
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="gateway">
<label>Sedif Web Bridge</label>
<description>Act as a gateway between your sedif Account and a Sedif mether things. In order to receive the data, you
must activate your account on Sedif Web Site.</description>
<semantic-equipment-tag>WebService</semantic-equipment-tag>
<config-description>
<parameter name="username" type="text" required="true">
<label>Username</label>
<context>user</context>
<description>Your Sedif Username</description>
</parameter>
<parameter name="password" type="text" required="true">
<label>Password</label>
<context>password</context>
<description>Your Sedif Password</description>
</parameter>
</config-description>
</bridge-type>
</thing:thing-descriptions>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="sedif"
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-type id="water-price">
<item-type unitHint="€">Number:Currency</item-type>
<label>Water Price</label>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="consumption">
<item-type unitHint="l">Number:Volume</item-type>
<label>Total Consumption</label>
<description>Consumption at given time interval</description>
<category>flow</category>
<tags>
<tag>Measurement</tag>
<tag>Water</tag>
</tags>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
</thing:thing-descriptions>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="sedif"
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="base">
<label>Standard Base</label>
<channels>
<channel id="mean-water-price" typeId="water-price">
<label>Mean Water Price</label>
</channel>
</channels>
</channel-group-type>
</thing:thing-descriptions>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="sedif"
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-consumption">
<label>Daily Consumption</label>
<channels>
<channel id="yesterday" typeId="consumption">
<label>Yesterday Consumption</label>
</channel>
<channel id="day-2" typeId="consumption">
<label>Day -2 Consumption</label>
</channel>
<channel id="day-3" typeId="consumption">
<label>Day -3 Consumption</label>
</channel>
<channel id="consumption" typeId="consumption">
<label>Consumption</label>
</channel>
</channels>
</channel-group-type>
</thing:thing-descriptions>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="sedif"
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-consumption">
<label>Monthly Consumption</label>
<channels>
<channel id="this-month" typeId="consumption">
<label>This Month Consumption</label>
</channel>
<channel id="last-month" typeId="consumption">
<label>Last Month Consumption</label>
</channel>
<channel id="month-2" typeId="consumption">
<label>Month -2 Consumption</label>
</channel>
<channel id="consumption" typeId="consumption">
<label>Consumption</label>
</channel>
</channels>
</channel-group-type>
</thing:thing-descriptions>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="sedif"
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-consumption">
<label>Weekly Consumption</label>
<channels>
<channel id="this-week" typeId="consumption">
<label>This Week Consumption</label>
</channel>
<channel id="last-week" typeId="consumption">
<label>Last Week Consumption</label>
</channel>
<channel id="week-2" typeId="consumption">
<label>Week -2 Consumption</label>
</channel>
<channel id="consumption" typeId="consumption">
<label>Consumption</label>
</channel>
</channels>
</channel-group-type>
</thing:thing-descriptions>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="sedif"
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-consumption">
<label>Yearly Consumption</label>
<channels>
<channel id="this-year" typeId="consumption">
<label>This Year Consumption</label>
</channel>
<channel id="last-year" typeId="consumption">
<label>Last Year Consumption</label>
</channel>
<channel id="year-2" typeId="consumption">
<label>Year -2 Consumption</label>
</channel>
<channel id="consumption" typeId="consumption">
<label>Consumption</label>
</channel>
</channels>
</channel-group-type>
</thing:thing-descriptions>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="sedif"
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="meter">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Meter</label>
<description>
Provides your water consumption data.
If you don't use autodiscovery, you will need to get your
contractName from this Sedif web site, and eventually your meterId if you have multiple meter on the same account.
See the documentation about where to find this informations.
</description>
<semantic-equipment-tag>WaterMeter</semantic-equipment-tag>
<channel-groups>
<channel-group typeId="base" id="base"/>
<channel-group typeId="daily-consumption" id="daily-consumption"/>
<channel-group typeId="weekly-consumption" id="weekly-consumption"/>
<channel-group typeId="monthly-consumption" id="monthly-consumption"/>
<channel-group typeId="yearly-consumption" id="yearly-consumption"/>
</channel-groups>
<config-description>
<parameter name="contractId" type="text" required="true">
<label>Contract Id</label>
<description>Your Contract Id</description>
</parameter>
<parameter name="meterId" type="text" required="false">
<label>Meter Id</label>
<description>Your Meter Id</description>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>
+1
View File
@@ -381,6 +381,7 @@
<module>org.openhab.binding.samsungtv</module>
<module>org.openhab.binding.satel</module>
<module>org.openhab.binding.sbus</module>
<module>org.openhab.binding.sedif</module>
<module>org.openhab.binding.semsportal</module>
<module>org.openhab.binding.senechome</module>
<module>org.openhab.binding.seneye</module>