[windcentrale] Backport fixes for using new API (#14820)

* [windcentrale] Adapt binding to new API (#14770)
* [windcentrale] Adapt binding to new API

Reworks the binding so it can be used with the new API that also requires authentication.

Also adds the following:

* Account things to provide authentication details
* Implementation for getting and refreshing tokens using AWS Cognito
* Windmill discovery based on the participations in projects
* Properties with additional data for windmills like turbine type, build year, location coordinates
* Adds support for "Het Vliegend Hert" windmill
* Unit tests for JSON (de)serialization

Fixes #13625

* [windcentrale] Discover windmills when account goes online (#14807)

* Backport to Java 11/openHAB 3.4

Signed-off-by: Wouter Born <github@maindrain.net>
This commit is contained in:
Wouter Born 2023-04-15 22:48:26 +02:00 committed by GitHub
parent 22afd6e04f
commit 84cb3c9727
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 2985 additions and 364 deletions

View File

@ -367,7 +367,7 @@
/bundles/org.openhab.binding.webthing/ @grro
/bundles/org.openhab.binding.wemo/ @hmerk @jlaur
/bundles/org.openhab.binding.wifiled/ @rvt @xylo
/bundles/org.openhab.binding.windcentrale/ @marcelrv
/bundles/org.openhab.binding.windcentrale/ @marcelrv @wborn
/bundles/org.openhab.binding.wlanthermo/ @CSchlipp
/bundles/org.openhab.binding.wled/ @Skinah
/bundles/org.openhab.binding.wolfsmartset/ @BoBiene

View File

@ -1,14 +1,19 @@
# Windcentrale Binding
This Binding is used to display the details of a Windcentrale windmill.
This Binding is used to display the details of Windcentrale windmills.
## Supported Things
This Binding supports Windcentrale mill devices.
The binding supports the following Windcentrale Things:
| Thing Type | Description |
|------------|-------------------------------------------|
| account | An account for using the Windcentrale API |
| windmill | Windcentrale Windmill |
## Discovery
There is no discovery available for this binding.
After creating an account Thing the Binding can discover windmills based on the participations linked to the account.
## Binding Configuration
@ -16,62 +21,72 @@ No binding configuration required.
## Thing Configuration
### Account
| Configuration Parameter | Required |
|-------------------------|----------|
| username | X |
| password | X |
### Windmill
| Configuration Parameter | Required | Default | Description |
|-------------------------|----------|---------|-----------------------------------------------------|
| millId | X | 131 | Identifies the windmill (see table below) |
| wd | | 1 | Number of wind shares ("Winddelen") |
|-------------------------|----------|------------------|-----------------------------------------------------|
| name | X | De Blauwe Reiger | Identifies the windmill (see names list below) |
| shares | | 1 | Number of wind shares ("Winddelen") |
| refreshInterval | | 30 | Refresh interval for refreshing the data in seconds |
| millId | Windmill name |
|--------|-------------------|
| 1 | De Grote Geert |
| 2 | De Jonge Held |
| 31 | Het Rode Hert |
| 41 | De Ranke Zwaan |
| 51 | De Witte Juffer |
| 111 | De Bonte Hen |
| 121 | De Trouwe Wachter |
| 131 | De Blauwe Reiger |
| 141 | De Vier Winden |
| 201 | De Boerenzwaluw |
The following windmill names are supported:
- De Blauwe Reiger
- De Boerenzwaluw
- De Bonte Hen
- De Grote Geert
- De Jonge Held
- De Ranke Zwaan
- De Trouwe Wachter
- De Vier Winden
- De Witte Juffer
- Het Rode Hert
- Het Vliegend Hert
## Channels
| Channel Type ID | Item Type | Description |
|-----------------|----------------------|-------------------------------------|
| kwh | Number:Energy | Current energy |
| kwhForecast | Number:Energy | Energy forecast |
| powerAbsTot | Number:Power | Total power |
| powerAbsWd | Number:Power | Power provided for your wind shares |
| powerRel | Number:Dimensionless | Relative power |
| runPercentage | Number:Dimensionless | Run percentage this year |
| runTime | Number:Time | Run time this year |
| Channel ID | Item Type | Description |
|----------------|----------------------|-------------------------------------|
| energy-total | Number:Energy | Total energy this year |
| power-relative | Number:Dimensionless | Relative power |
| power-shares | Number:Power | Power provided for your wind shares |
| power-total | Number:Power | Total power |
| run-percentage | Number:Dimensionless | Run percentage this year |
| run-time | Number:Time | Run time this year |
| timestamp | DateTime | Timestamp of the last update |
| windDirection | String | Current wind direction |
| windSpeed | Number | Measured current wind speed (Bft) |
| wind-direction | String | Current wind direction |
| wind-speed | Number | Measured current wind speed (Bft) |
## Example
### demo.things
```
Thing windcentrale:mill:geert [ millId=1 ]
Thing windcentrale:mill:reiger [ millId=131, wd=3, refreshInterval=60 ]
```java
Bridge windcentrale:account:demo-account [ username="johndoe@acme.com", password="Mf!BU45LTF6X2Cf36zxt" ] {
Thing windmill de-grote-geert [ name="De Grote Geert" ]
Thing windmill de-blauwe-reiger [ name="De Blauwe Reiger", shares=3, refreshInterval=60 ]
}
```
### demo.items
```
Group gReiger "Windcentrale Reiger" <wind>
```java
Group gReiger "Windcentrale Reiger"
Number ReigerWindSpeed "Wind speed [%d Bft]" <wind> (gReiger) { channel="windcentrale:mill:reiger:windSpeed" }
String ReigerWindDirection "Wind direction [%s]" <wind> (gReiger) { channel="windcentrale:mill:reiger:windDirection" }
Number:Power ReigerPowerAbsTot "Total mill power [%.1f %unit%]" <wind> (gReiger) { channel="windcentrale:mill:reiger:powerAbsTot" }
Number:Power ReigerPowerAbsWd "Wind shares power [%.1f %unit%]" <wind> (gReiger) { channel="windcentrale:mill:reiger:powerAbsWd" }
Number:Dimensionless ReigerPowerRel "Relative power [%.1f %unit%]" <wind> (gReiger) { channel="windcentrale:mill:reiger:powerRel" }
Number:Energy ReigerKwh "Current energy [%.0f %unit%]" <wind> (gReiger) { channel="windcentrale:mill:reiger:kwh" }
Number:Energy ReigerKwhForecast "Energy forecast [%.0f %unit%]" <wind> (gReiger) { channel="windcentrale:mill:reiger:kwhForecast" }
Number:Dimensionless ReigerRunPercentage "Run percentage [%.1f %unit%]" <wind> (gReiger) { channel="windcentrale:mill:reiger:runPercentage" }
Number:Time ReigerRunTime "Run time [%.0f %unit%]" <wind> (gReiger) { channel="windcentrale:mill:reiger:runTime" }
DateTime ReigerTimestamp "Update timestamp [%1$ta %1$tR]" <wind> (gReiger) { channel="windcentrale:mill:reiger:timestamp" }
Number ReigerWindSpeed "Wind speed [%d Bft]" (gReiger) { channel="windcentrale:windmill:demo-account:de-blauwe-reiger:wind-speed" }
String ReigerWindDirection "Wind direction [%s]" (gReiger) { channel="windcentrale:windmill:demo-account:de-blauwe-reiger:wind-direction" }
Number:Power ReigerPowerTotal "Total windmill power [%.1f %unit%]" (gReiger) { channel="windcentrale:windmill:demo-account:de-blauwe-reiger:power-total" }
Number:Power ReigerPowerShares "Wind shares power [%.1f %unit%]" (gReiger) { channel="windcentrale:windmill:demo-account:de-blauwe-reiger:power-shares" }
Number:Dimensionless ReigerPowerRelative "Relative power [%.1f %unit%]" (gReiger) { channel="windcentrale:windmill:demo-account:de-blauwe-reiger:power-relative" }
Number:Energy ReigerEnergyTotal "Total windmill energy [%.0f %unit%]" (gReiger) { channel="windcentrale:windmill:demo-account:de-blauwe-reiger:energy-total" }
Number:Dimensionless ReigerRunPercentage "Run percentage [%.1f %unit%]" (gReiger) { channel="windcentrale:windmill:demo-account:de-blauwe-reiger:run-percentage" }
Number:Time ReigerRunTime "Run time [%.0f %unit%]" (gReiger) { channel="windcentrale:windmill:demo-account:de-blauwe-reiger:run-time" }
DateTime ReigerTimestamp "Update timestamp [%1$ta %1$tR]" (gReiger) { channel="windcentrale:windmill:demo-account:de-blauwe-reiger:timestamp" }
```

View File

@ -12,7 +12,6 @@
*/
package org.openhab.binding.windcentrale.internal;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -23,6 +22,7 @@ import org.openhab.core.thing.ThingTypeUID;
* used across the whole binding.
*
* @author Marcel Verpaalen - Initial contribution
* @author Wouter Born - Add support for new API with authentication
*/
@NonNullByDefault
public final class WindcentraleBindingConstants {
@ -30,23 +30,31 @@ public final class WindcentraleBindingConstants {
public static final String BINDING_ID = "windcentrale";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_MILL = new ThingTypeUID(BINDING_ID, "mill");
public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
public static final ThingTypeUID THING_TYPE_WINDMILL = new ThingTypeUID(BINDING_ID, "windmill");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_MILL);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_WINDMILL);
// List of all Channel IDs
public static final String CHANNEL_WIND_SPEED = "windSpeed";
public static final String CHANNEL_WIND_DIRECTION = "windDirection";
public static final String CHANNEL_POWER_TOTAL = "powerAbsTot";
public static final String CHANNEL_POWER_PER_WD = "powerAbsWd";
public static final String CHANNEL_POWER_RELATIVE = "powerRel";
public static final String CHANNEL_ENERGY = "kwh";
public static final String CHANNEL_ENERGY_FC = "kwhForecast";
public static final String CHANNEL_RUNTIME = "runTime";
public static final String CHANNEL_RUNTIME_PER = "runPercentage";
public static final String CHANNEL_LAST_UPDATE = "timestamp";
public static final String CHANNEL_ENERGY_TOTAL = "energy-total";
public static final String CHANNEL_POWER_RELATIVE = "power-relative";
public static final String CHANNEL_POWER_SHARES = "power-shares";
public static final String CHANNEL_POWER_TOTAL = "power-total";
public static final String CHANNEL_RUN_PERCENTAGE = "run-percentage";
public static final String CHANNEL_RUN_TIME = "run-time";
public static final String CHANNEL_TIMESTAMP = "timestamp";
public static final String CHANNEL_WIND_DIRECTION = "wind-direction";
public static final String CHANNEL_WIND_SPEED = "wind-speed";
public static final String PROPERTY_MILL_ID = "millId";
public static final String PROPERTY_QTY_WINDDELEN = "wd";
public static final String PROPERTY_NAME = "name";
public static final String PROPERTY_SHARES = "shares";
public static final String PROPERTY_REFRESH_INTERVAL = "refreshInterval";
public static final String PROPERTY_BUILD_YEAR = "buildYear";
public static final String PROPERTY_COORDINATES = "coordinates";
public static final String PROPERTY_DETAILS_URL = "detailsUrl";
public static final String PROPERTY_MUNICIPALITY = "municipality";
public static final String PROPERTY_PROJECT_CODE = "projectCode";
public static final String PROPERTY_PROVINCE = "province";
public static final String PROPERTY_TOTAL_SHARES = "totalShares";
}

View File

@ -0,0 +1,169 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal;
import static org.openhab.binding.windcentrale.internal.WindcentraleBindingConstants.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.windcentrale.internal.api.WindcentraleAPI;
import org.openhab.binding.windcentrale.internal.dto.Project;
import org.openhab.binding.windcentrale.internal.dto.Windmill;
import org.openhab.binding.windcentrale.internal.exception.FailedGettingDataException;
import org.openhab.binding.windcentrale.internal.exception.InvalidAccessTokenException;
import org.openhab.binding.windcentrale.internal.handler.WindcentraleAccountHandler;
import org.openhab.binding.windcentrale.internal.handler.WindcentraleWindmillHandler;
import org.openhab.binding.windcentrale.internal.listener.ThingStatusListener;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link WindcentraleDiscoveryService} discovers windmills using the participations in projects provided by the
* Windcentrale API.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class WindcentraleDiscoveryService extends AbstractDiscoveryService
implements ThingHandlerService, ThingStatusListener {
private final Logger logger = LoggerFactory.getLogger(WindcentraleDiscoveryService.class);
private @NonNullByDefault({}) WindcentraleAccountHandler accountHandler;
private @Nullable Future<?> discoveryJob;
public WindcentraleDiscoveryService() {
super(Set.of(THING_TYPE_WINDMILL), 10, false);
}
protected void activate(ComponentContext context) {
}
@Override
public void deactivate() {
cancelDiscoveryJob();
super.deactivate();
accountHandler.removeThingStatusListener(this);
}
@Override
public @Nullable ThingHandler getThingHandler() {
return accountHandler;
}
@Override
public void setThingHandler(ThingHandler handler) {
if (handler instanceof WindcentraleAccountHandler) {
WindcentraleAccountHandler accountHandler = (WindcentraleAccountHandler) handler;
accountHandler.addThingStatusListener(this);
this.accountHandler = accountHandler;
}
}
@Override
protected void startScan() {
logger.debug("Discover windmills (manual discovery)");
cancelDiscoveryJob();
discoveryJob = scheduler.submit(this::discoverWindmills);
}
@Override
protected synchronized void stopScan() {
cancelDiscoveryJob();
super.stopScan();
}
@Override
public void thingStatusChanged(Thing thing, ThingStatus status) {
if (ThingStatus.ONLINE.equals(status)) {
logger.debug("Discover windmills (account online)");
discoverWindmills();
}
}
private void cancelDiscoveryJob() {
Future<?> localDiscoveryJob = discoveryJob;
if (localDiscoveryJob != null) {
localDiscoveryJob.cancel(true);
}
}
private void discoverWindmills() {
ThingUID bridgeUID = accountHandler.getThing().getUID();
WindcentraleAPI api = accountHandler.getAPI();
if (api == null) {
logger.debug("Cannot discover windmills because API is null for {}", bridgeUID);
return;
}
logger.debug("Starting discovery scan for {}", bridgeUID);
try {
calculateWindmillShares(api.getProjects()).entrySet()
.forEach(windmillShares -> addWindmillDiscoveryResult(bridgeUID, windmillShares.getKey(),
windmillShares.getValue()));
} catch (FailedGettingDataException | InvalidAccessTokenException e) {
logger.debug("Exception during discovery scan for {}", bridgeUID, e);
}
logger.debug("Finished discovery scan for {}", bridgeUID);
}
private Map<Windmill, Integer> calculateWindmillShares(List<Project> projects) {
Map<Windmill, Integer> windmillShares = new HashMap<>();
for (Project project : projects) {
Windmill windmill = Windmill.fromProjectCode(project.projectCode);
if (windmill != null) {
int shares = Objects.requireNonNullElse(windmillShares.get(windmill), 0);
shares += project.participations.stream()
.collect(Collectors.summingInt(participation -> participation.share));
windmillShares.put(windmill, shares);
} else {
logger.debug("Unsupported project code: {}", project.projectCode);
}
}
return windmillShares;
}
private void addWindmillDiscoveryResult(ThingUID bridgeUID, Windmill windmill, int shares) {
String deviceId = windmill.getName().toLowerCase().replaceAll(" ", "-");
ThingUID thingUID = new ThingUID(THING_TYPE_WINDMILL, bridgeUID, deviceId);
thingDiscovered(DiscoveryResultBuilder.create(thingUID) //
.withThingType(THING_TYPE_WINDMILL) //
.withLabel(windmill.getName()) //
.withBridge(bridgeUID) //
.withProperty(PROPERTY_NAME, windmill.getName()) //
.withProperty(PROPERTY_SHARES, shares) //
.withProperties(new HashMap<>(WindcentraleWindmillHandler.getWindmillProperties(windmill))) //
.withRepresentationProperty(PROPERTY_PROJECT_CODE) //
.build() //
);
}
}

View File

@ -16,24 +16,37 @@ import static org.openhab.binding.windcentrale.internal.WindcentraleBindingConst
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.windcentrale.internal.handler.WindcentraleHandler;
import org.openhab.binding.windcentrale.internal.handler.WindcentraleAccountHandler;
import org.openhab.binding.windcentrale.internal.handler.WindcentraleWindmillHandler;
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.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link WindcentraleHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Marcel Verpaalen - Initial contribution
* @author Wouter Born - Add support for new API with authentication
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.windcentrale")
@NonNullByDefault
public class WindcentraleHandlerFactory extends BaseThingHandlerFactory {
private final HttpClientFactory httpClientFactory;
@Activate
public WindcentraleHandlerFactory(final @Reference HttpClientFactory httpClientFactory) {
this.httpClientFactory = httpClientFactory;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
@ -43,8 +56,10 @@ public class WindcentraleHandlerFactory extends BaseThingHandlerFactory {
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_MILL)) {
return new WindcentraleHandler(thing);
if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) {
return new WindcentraleAccountHandler((Bridge) thing, httpClientFactory);
} else if (thingTypeUID.equals(THING_TYPE_WINDMILL)) {
return new WindcentraleWindmillHandler(thing);
}
return null;

View File

@ -0,0 +1,395 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.api;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jetty.http.HttpMethod.POST;
import static org.openhab.binding.windcentrale.internal.dto.CognitoGson.GSON;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
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.util.StringContentProvider;
import org.openhab.binding.windcentrale.internal.dto.AuthenticationResultResponse;
import org.openhab.binding.windcentrale.internal.dto.ChallengeResponse;
import org.openhab.binding.windcentrale.internal.dto.CognitoError;
import org.openhab.binding.windcentrale.internal.dto.InitiateAuthRequest;
import org.openhab.binding.windcentrale.internal.dto.RespondToAuthChallengeRequest;
import org.openhab.binding.windcentrale.internal.exception.InvalidAccessTokenException;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Helps with authenticating users to Amazon Cognito to get a JWT access token which can be used for retrieving
* information using the REST APIs.
*
* @see https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol
* @see https://stackoverflow.com/questions/67528443/cognito-srp-using-aws-java-sdk-v2-x
* @see https://github.com/aws-samples/aws-cognito-java-desktop-app/blob/master/src/main/java/com/amazonaws/sample/cognitoui/AuthenticationHelper.java
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class AuthenticationHelper {
private final Logger logger = LoggerFactory.getLogger(AuthenticationHelper.class);
private static final String SRP_N_HEX = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" //
+ "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" //
+ "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" //
+ "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" //
+ "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" //
+ "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" //
+ "83655D23DCA3AD961C62F356208552BB9ED529077096966D" //
+ "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" //
+ "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" //
+ "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" //
+ "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64" //
+ "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" //
+ "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B" //
+ "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C" //
+ "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31" //
+ "43DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF";
private static final BigInteger SRP_A;
private static final BigInteger SRP_A2;
private static final BigInteger SRP_G = BigInteger.valueOf(2);
private static final BigInteger SRP_K;
private static final BigInteger SRP_N = new BigInteger(SRP_N_HEX, 16);
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter
.ofPattern("EEE MMM d HH:mm:ss z yyyy", Locale.US).withZone(ZoneId.of("UTC"));
private static final int DERIVED_KEY_SIZE = 16;
private static final int EPHEMERAL_KEY_LENGTH = 1024;
private static final String DERIVED_KEY_INFO = "Caldera Derived Key";
private static final Duration REQUEST_TIMEOUT = Duration.ofMinutes(1);
private static final String COGNITO_URL_FORMAT = "https://cognito-idp.%s.amazonaws.com/";
private static final String INITIATE_AUTH_TARGET = "AWSCognitoIdentityProviderService.InitiateAuth";
private static final String RESPOND_TO_AUTH_TARGET = "AWSCognitoIdentityProviderService.RespondToAuthChallenge";
/**
* Internal class for doing the HKDF calculations.
*/
private static final class Hkdf {
private static final int MAX_KEY_SIZE = 255;
private final String algorithm;
private @Nullable SecretKey prk;
/**
* @param algorithm The type of HMAC algorithm to be used
*/
private Hkdf(String algorithm) {
if (!algorithm.startsWith("Hmac")) {
throw new IllegalArgumentException(
"Invalid algorithm " + algorithm + ". HKDF may only be used with HMAC algorithms.");
}
this.algorithm = algorithm;
}
/**
* @param ikm the input key material
* @param salt random bytes for salt
*/
private void init(byte[] ikm, byte[] salt) {
try {
Mac mac = Mac.getInstance(algorithm);
byte[] realSalt = salt.length == 0 ? new byte[mac.getMacLength()] : salt.clone();
mac.init(new SecretKeySpec(realSalt, algorithm));
SecretKeySpec key = new SecretKeySpec(mac.doFinal(ikm), algorithm);
unsafeInitWithoutKeyExtraction(key);
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new IllegalStateException("Failed to initialize HKDF", e);
}
}
/**
* @param rawKey current secret key
*/
private void unsafeInitWithoutKeyExtraction(SecretKey rawKey) {
if (!rawKey.getAlgorithm().equals(algorithm)) {
throw new IllegalArgumentException(
"Algorithm for the provided key must match the algorithm for this HKDF. Expected " + algorithm
+ " but found " + rawKey.getAlgorithm());
} else {
prk = rawKey;
}
}
private byte[] deriveKey(String info, int length) {
if (prk == null) {
throw new IllegalStateException("HKDF has not been initialized");
}
if (length < 0) {
throw new IllegalArgumentException("Length must be a non-negative value");
}
Mac mac = createMac();
if (length > MAX_KEY_SIZE * mac.getMacLength()) {
throw new IllegalArgumentException(
"Requested keys may not be longer than 255 times the underlying HMAC length");
}
byte[] result = new byte[length];
byte[] bytes = info.getBytes(UTF_8);
byte[] t = {};
int loc = 0;
for (byte i = 1; loc < length; ++i) {
mac.update(t);
mac.update(bytes);
mac.update(i);
t = mac.doFinal();
for (int x = 0; x < t.length && loc < length; ++loc) {
result[loc] = t[x];
++x;
}
}
return result;
}
/**
* @return the generated message authentication code
*/
private Mac createMac() {
try {
Mac mac = Mac.getInstance(algorithm);
mac.init(prk);
return mac;
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new IllegalStateException("Could not create MAC implementing algorithm: " + algorithm, e);
}
}
}
static {
// Initialize the SRP variables
try {
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(SRP_N.toByteArray());
byte[] digest = md.digest(SRP_G.toByteArray());
SRP_K = new BigInteger(1, digest);
BigInteger srpA;
BigInteger srpA2;
do {
srpA2 = new BigInteger(EPHEMERAL_KEY_LENGTH, sr).mod(SRP_N);
srpA = SRP_G.modPow(srpA2, SRP_N);
} while (srpA.mod(SRP_N).equals(BigInteger.ZERO));
SRP_A = srpA;
SRP_A2 = srpA2;
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SRP variables cannot be initialized due to missing algorithm", e);
}
}
private final HttpClient httpClient;
private final String userPoolId;
private final String clientId;
private final String region;
public AuthenticationHelper(HttpClientFactory httpClientFactory, String userPoolId, String clientId,
String region) {
this.httpClient = httpClientFactory.getCommonHttpClient();
this.userPoolId = userPoolId;
this.clientId = clientId;
this.region = region;
}
/**
* Method to orchestrate the SRP Authentication.
*
* @param username username for the SRP request
* @param password password for the SRP request
* @return JWT token if the request is successful
* @throws InvalidAccessTokenException when SRP authentication fails
*/
public AuthenticationResultResponse performSrpAuthentication(String username, String password)
throws InvalidAccessTokenException {
InitiateAuthRequest initiateAuthRequest = InitiateAuthRequest.userSrpAuth(clientId, username,
SRP_A.toString(16));
try {
ChallengeResponse challengeResponse = postInitiateAuthSrp(initiateAuthRequest);
if ("PASSWORD_VERIFIER".equals(challengeResponse.challengeName)) {
RespondToAuthChallengeRequest challengeRequest = createRespondToAuthChallengeRequest(challengeResponse,
password);
return postRespondToAuthChallenge(challengeRequest);
} else {
throw new InvalidAccessTokenException(
"Unsupported authentication challenge: " + challengeResponse.challengeName);
}
} catch (IllegalStateException | InvalidKeyException | NoSuchAlgorithmException e) {
throw new InvalidAccessTokenException("SRP Authentication failed", e);
}
}
public AuthenticationResultResponse performTokenRefresh(String refreshToken) throws InvalidAccessTokenException {
InitiateAuthRequest initiateAuthRequest = InitiateAuthRequest.refreshTokenAuth(clientId, refreshToken);
try {
return postInitiateAuthRefresh(initiateAuthRequest);
} catch (IllegalStateException e) {
throw new InvalidAccessTokenException("Token refresh failed", e);
}
}
/**
* Creates a response request to the SRP authentication challenge from the user pool.
*
* @param challengeResponse authentication challenge returned from the Cognito user pool
* @param password password to be used to respond to the authentication challenge
* @return request created for the previous authentication challenge
*/
private RespondToAuthChallengeRequest createRespondToAuthChallengeRequest(ChallengeResponse challengeResponse,
String password) throws InvalidKeyException, NoSuchAlgorithmException {
String salt = challengeResponse.getSalt();
String secretBlock = challengeResponse.getSecretBlock();
String userIdForSrp = challengeResponse.getUserIdForSrp();
String usernameInternal = challengeResponse.getUsername();
if (secretBlock.isEmpty() || userIdForSrp.isEmpty() || usernameInternal.isEmpty()) {
throw new IllegalArgumentException("Required authentication response challenge parameters are null");
}
BigInteger srpB = new BigInteger(challengeResponse.getSrpB(), 16);
if (srpB.mod(SRP_N).equals(BigInteger.ZERO)) {
throw new IllegalStateException("SRP error, B cannot be zero");
}
String timestamp = DATE_TIME_FORMATTER.format(Instant.now());
byte[] key = getPasswordAuthenticationKey(userIdForSrp, password, srpB, new BigInteger(salt, 16));
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
mac.update(userPoolId.split("_", 2)[1].getBytes(UTF_8));
mac.update(userIdForSrp.getBytes(UTF_8));
mac.update(Base64.getDecoder().decode(secretBlock));
byte[] hmac = mac.doFinal(timestamp.getBytes(UTF_8));
String signature = new String(Base64.getEncoder().encode(hmac), UTF_8);
return new RespondToAuthChallengeRequest(clientId, usernameInternal, secretBlock, signature, timestamp);
}
private byte[] getPasswordAuthenticationKey(String userId, String userPassword, BigInteger srpB, BigInteger salt) {
try {
// Authenticate the password
// srpU = H(SRP_A, srpB)
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(SRP_A.toByteArray());
BigInteger srpU = new BigInteger(1, md.digest(srpB.toByteArray()));
if (srpU.equals(BigInteger.ZERO)) {
throw new IllegalStateException("Hash of A and B cannot be zero");
}
// srpX = H(salt | H(poolName | userId | ":" | password))
md.reset();
md.update(userPoolId.split("_", 2)[1].getBytes(UTF_8));
md.update(userId.getBytes(UTF_8));
md.update(":".getBytes(UTF_8));
byte[] userIdHash = md.digest(userPassword.getBytes(UTF_8));
md.reset();
md.update(salt.toByteArray());
BigInteger srpX = new BigInteger(1, md.digest(userIdHash));
BigInteger srpS = (srpB.subtract(SRP_K.multiply(SRP_G.modPow(srpX, SRP_N)))
.modPow(SRP_A2.add(srpU.multiply(srpX)), SRP_N)).mod(SRP_N);
Hkdf hkdf = new Hkdf("HmacSHA256");
hkdf.init(srpS.toByteArray(), srpU.toByteArray());
return hkdf.deriveKey(DERIVED_KEY_INFO, DERIVED_KEY_SIZE);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
private ChallengeResponse postInitiateAuthSrp(InitiateAuthRequest request) throws InvalidAccessTokenException {
String responseContent = postJson(INITIATE_AUTH_TARGET, GSON.toJson(request));
return Objects.requireNonNull(GSON.fromJson(responseContent, ChallengeResponse.class));
}
private AuthenticationResultResponse postInitiateAuthRefresh(InitiateAuthRequest request)
throws InvalidAccessTokenException {
String responseContent = postJson(INITIATE_AUTH_TARGET, GSON.toJson(request));
return Objects.requireNonNull(GSON.fromJson(responseContent, AuthenticationResultResponse.class));
}
private AuthenticationResultResponse postRespondToAuthChallenge(RespondToAuthChallengeRequest request)
throws InvalidAccessTokenException {
String responseContent = postJson(RESPOND_TO_AUTH_TARGET, GSON.toJson(request));
return Objects.requireNonNull(GSON.fromJson(responseContent, AuthenticationResultResponse.class));
}
private String postJson(String target, String requestContent) throws InvalidAccessTokenException {
try {
String url = String.format(COGNITO_URL_FORMAT, region);
logger.debug("Posting JSON to: {}", url);
ContentResponse contentResponse = httpClient.newRequest(url) //
.method(POST) //
.header("x-amz-target", target) //
.content(new StringContentProvider(requestContent), "application/x-amz-json-1.1") //
.timeout(REQUEST_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS).send();
String response = contentResponse.getContentAsString();
if (contentResponse.getStatus() >= 400) {
logger.debug("Cognito API error: {}", response);
CognitoError error = GSON.fromJson(response, CognitoError.class);
String message;
if (error != null && !error.message.isBlank()) {
message = String.format("Cognito API error: %s (%s)", error.message, error.type);
} else {
message = String.format("Cognito API error: %s (HTTP %s)", contentResponse.getReason(),
contentResponse.getStatus());
}
throw new InvalidAccessTokenException(message);
} else {
logger.trace("Response: {}", response);
}
return response;
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new InvalidAccessTokenException("Cognito API request failed: " + e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Interface for listeners that want to monitor if {@link WindcentraleAPI} requests error or succeed.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public interface RequestListener {
void onError(Exception exception);
void onSuccess();
}

View File

@ -0,0 +1,156 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.api;
import static org.eclipse.jetty.http.HttpHeader.ACCEPT;
import static org.eclipse.jetty.http.HttpMethod.GET;
import static org.openhab.binding.windcentrale.internal.dto.WindcentraleGson.GSON;
import java.time.Duration;
import java.time.Instant;
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.api.ContentResponse;
import org.openhab.binding.windcentrale.internal.dto.AuthenticationResultResponse;
import org.openhab.binding.windcentrale.internal.dto.KeyResponse;
import org.openhab.binding.windcentrale.internal.exception.InvalidAccessTokenException;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides the JWT tokens used with the Windcentrale API by using a {@link AuthenticationHelper}.
* It also resolves the Windcentrale specific Cognito configuration required by the {@link AuthenticationHelper}.
*
* A token is obtained by calling {@link #getIdToken()}.
* The token is cached and returned in subsequent calls to {@link #getIdToken()} until it expires.
* When tokens expire they are refreshed using the refresh token when available.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class TokenProvider {
private final Logger logger = LoggerFactory.getLogger(TokenProvider.class);
private static final String DEFAULT_USER_POOL_ID = "eu-west-1_U7eYBPrBd";
private static final String DEFAULT_CLIENT_ID = "715j3r0trk7o8dqg3md57il7q0";
private static final String DEFAULT_REGION = "eu-west-1";
private static final String APPLICATION_JSON = "application/json";
private static final Duration REQUEST_TIMEOUT = Duration.ofMinutes(1);
private static final String KEY_URL = WindcentraleAPI.URL_PREFIX + "/labels/key?domain=mijn.windcentrale.nl";
private final HttpClientFactory httpClientFactory;
private final String username;
private final String password;
private @Nullable AuthenticationHelper authenticationHelper;
private String idToken = "";
private String refreshToken = "";
private Instant validityEnd = Instant.MIN;
public TokenProvider(HttpClientFactory httpClientFactory, String username, String password) {
this.httpClientFactory = httpClientFactory;
this.username = username;
this.password = password;
}
private AuthenticationHelper createHelper() {
String userPoolId = DEFAULT_USER_POOL_ID;
String clientId = DEFAULT_CLIENT_ID;
String region = DEFAULT_REGION;
try {
logger.debug("Getting JSON from: {}", KEY_URL);
ContentResponse contentResponse = httpClientFactory.getCommonHttpClient().newRequest(KEY_URL) //
.method(GET) //
.header(ACCEPT, APPLICATION_JSON) //
.timeout(REQUEST_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS) //
.send();
String response = contentResponse.getContentAsString();
if (contentResponse.getStatus() >= 400) {
logger.debug("Could not get Cognito configuration values, using default values. Error (HTTP {}): {}",
contentResponse.getStatus(), contentResponse.getReason());
} else {
logger.trace("Response: {}", response);
KeyResponse keyResponse = Objects.requireNonNullElse(GSON.fromJson(response, KeyResponse.class),
new KeyResponse());
if (!keyResponse.userPoolId.isEmpty() && !keyResponse.clientId.isEmpty()
&& keyResponse.region.isEmpty()) {
userPoolId = keyResponse.userPoolId;
clientId = keyResponse.clientId;
region = keyResponse.region;
}
}
} catch (ExecutionException | InterruptedException | TimeoutException e) {
logger.debug("Could not get Cognito configuration values, using default values", e);
}
logger.debug("Creating new AuthenticationHelper (userPoolId={}, clientId={}, region={})", userPoolId, clientId,
region);
return new AuthenticationHelper(httpClientFactory, userPoolId, clientId, region);
}
private AuthenticationHelper getOrCreateHelper() {
AuthenticationHelper helper = authenticationHelper;
if (helper == null) {
helper = createHelper();
this.authenticationHelper = helper;
}
return helper;
}
public String getIdToken() throws InvalidAccessTokenException {
boolean valid = Instant.now().plusSeconds(30).isBefore(validityEnd);
if (valid) {
logger.debug("Reusing existing valid token");
return idToken;
}
AuthenticationResultResponse result = null;
AuthenticationHelper helper = getOrCreateHelper();
if (!refreshToken.isBlank()) {
try {
logger.debug("Performing token refresh");
result = helper.performTokenRefresh(refreshToken);
logger.debug("Successfully performed token refresh");
} catch (InvalidAccessTokenException e) {
logger.debug("Token refresh failed", e);
}
}
if (result == null) {
// there is no refresh token or the refresh failed
logger.debug("Performing SRP authentication");
result = helper.performSrpAuthentication(username, password);
logger.debug("Successfully performed SRP authentication");
refreshToken = result.getRefreshToken();
}
idToken = result.getIdToken();
validityEnd = Instant.now().plusSeconds(result.getExpiresIn());
logger.debug("Token is valid until {}", validityEnd);
return idToken;
}
}

View File

@ -0,0 +1,148 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.api;
import static org.eclipse.jetty.http.HttpHeader.*;
import static org.eclipse.jetty.http.HttpMethod.GET;
import static org.openhab.binding.windcentrale.internal.dto.WindcentraleGson.*;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
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.openhab.binding.windcentrale.internal.dto.Project;
import org.openhab.binding.windcentrale.internal.dto.Windmill;
import org.openhab.binding.windcentrale.internal.dto.WindmillStatus;
import org.openhab.binding.windcentrale.internal.exception.FailedGettingDataException;
import org.openhab.binding.windcentrale.internal.exception.InvalidAccessTokenException;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link WindcentraleAPI} implements the Windcentrale REST API which allows for querying project participations and
* the current windmill status.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class WindcentraleAPI {
public static final String URL_PREFIX = "https://mijn.windcentrale.nl/api/v0";
private static final String LIVE_DATA_URL = URL_PREFIX + "/livedata";
private static final String PROJECTS_URL = URL_PREFIX + "/sustainable/projects";
private static final String APPLICATION_JSON = "application/json";
private static final String BEARER = "Bearer ";
private static final Duration REQUEST_TIMEOUT = Duration.ofMinutes(1);
private final Logger logger = LoggerFactory.getLogger(WindcentraleAPI.class);
private final HttpClient httpClient;
private final TokenProvider tokenProvider;
private final Set<RequestListener> requestListeners = ConcurrentHashMap.newKeySet();
public WindcentraleAPI(HttpClientFactory httpClientFactory, TokenProvider tokenProvider) {
this.httpClient = httpClientFactory.getCommonHttpClient();
this.tokenProvider = tokenProvider;
}
public void dispose() {
requestListeners.clear();
}
public void addRequestListener(RequestListener listener) {
requestListeners.add(listener);
}
public void removeRequestListener(RequestListener listener) {
requestListeners.remove(listener);
}
private String getAuthorizationHeader() throws InvalidAccessTokenException {
return BEARER + tokenProvider.getIdToken();
}
private String getJson(String url) throws FailedGettingDataException, InvalidAccessTokenException {
try {
logger.debug("Getting JSON from: {}", url);
ContentResponse contentResponse = httpClient.newRequest(url) //
.method(GET) //
.header(ACCEPT, APPLICATION_JSON) //
.header(AUTHORIZATION, getAuthorizationHeader()) //
.timeout(REQUEST_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS) //
.send();
if (contentResponse.getStatus() >= 400) {
FailedGettingDataException exception = new FailedGettingDataException(
String.format("Windcentrale API error: %s (HTTP %s)", contentResponse.getReason(),
contentResponse.getStatus()));
requestListeners.forEach(listener -> listener.onError(exception));
throw exception;
}
String response = contentResponse.getContentAsString();
logger.trace("Response: {}", response);
requestListeners.forEach(RequestListener::onSuccess);
return response;
} catch (ExecutionException | InterruptedException | TimeoutException e) {
FailedGettingDataException exception = new FailedGettingDataException(
"Windcentrale API request failed: " + e.getMessage(), e);
requestListeners.forEach(listener -> listener.onError(exception));
throw exception;
} catch (InvalidAccessTokenException e) {
requestListeners.forEach(listener -> listener.onError(e));
throw e;
}
}
public Map<Windmill, WindmillStatus> getLiveData() throws FailedGettingDataException, InvalidAccessTokenException {
return getLiveData(Set.of());
}
public @Nullable WindmillStatus getLiveData(Windmill windmill)
throws FailedGettingDataException, InvalidAccessTokenException {
return getLiveData(Set.of(windmill)).get(windmill);
}
public Map<Windmill, WindmillStatus> getLiveData(Set<Windmill> windmills)
throws FailedGettingDataException, InvalidAccessTokenException {
logger.debug("Getting live data: {}", windmills);
String queryParams = "";
if (!windmills.isEmpty()) {
queryParams = "?projects="
+ windmills.stream().map(Windmill::getProjectCode).collect(Collectors.joining(","));
}
String json = getJson(LIVE_DATA_URL + queryParams);
return Objects.requireNonNullElse(GSON.fromJson(json, LIVE_DATA_RESPONSE_TYPE), Map.of());
}
public List<Project> getProjects() throws FailedGettingDataException, InvalidAccessTokenException {
logger.debug("Getting projects");
String json = getJson(PROJECTS_URL);
return Objects.requireNonNullElse(GSON.fromJson(json, PROJECTS_RESPONSE_TYPE), List.of());
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The configuration of a Windcentrale account thing.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class AccountConfiguration {
public static final String USERNAME = "username";
public String username = "";
public static final String PASSWORD = "password";
public String password = "";
}

View File

@ -15,17 +15,18 @@ package org.openhab.binding.windcentrale.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The configuration of a Mill thing.
* The configuration of a Windcentrale windmill thing.
*
* @author Wouter Born - Initial contribution, add Mill configuration object
* @author Wouter Born - Add support for new API with authentication
*/
@NonNullByDefault
public class MillConfig {
public class WindmillConfiguration {
/**
* Windmill identifier
* Windmill name
*/
public int millId = 1;
public String name = "";
/**
* Refresh interval for refreshing the data in seconds
@ -35,5 +36,5 @@ public class MillConfig {
/**
* Number of wind shares ("Winddelen")
*/
public int wd = 1;
public int shares = 1;
}

View File

@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link AuthenticationResultResponse} is returned by Cognito after responding to an SRP challenge by a
* {@link RespondToAuthChallengeRequest} or when refreshing tokens using an {@link InitiateAuthRequest}.
*
* The refresh token is only provided as part of the SRP challenge response and will be empty when it is used to refresh
* tokens.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class AuthenticationResultResponse {
private static class AuthenticationResult {
public String accessToken = "";
public int expiresIn;
public String idToken = "";
public String refreshToken = "";
public String tokenType = "";
}
private AuthenticationResult authenticationResult = new AuthenticationResult();
public String getAccessToken() {
return authenticationResult.accessToken;
}
public int getExpiresIn() {
return authenticationResult.expiresIn;
}
public String getIdToken() {
return authenticationResult.idToken;
}
public String getRefreshToken() {
return authenticationResult.refreshToken;
}
public String getTokenType() {
return authenticationResult.tokenType;
}
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.dto;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ChallengeResponse} is the response of Cognito when starting user SRP authentication with a
* {@link InitiateAuthRequest}. It is answered using a {@link RespondToAuthChallengeRequest}.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class ChallengeResponse {
public String challengeName = "";
public Map<String, String> challengeParameters = Map.of();
private String getChallengeParameter(String key) {
return Objects.requireNonNullElse(challengeParameters.get(key), "");
}
public String getSalt() {
return getChallengeParameter("SALT");
}
public String getSecretBlock() {
return getChallengeParameter("SECRET_BLOCK");
}
public String getSrpB() {
return getChallengeParameter("SRP_B");
}
public String getUsername() {
return getChallengeParameter("USERNAME");
}
public String getUserIdForSrp() {
return getChallengeParameter("USER_ID_FOR_SRP");
}
}

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* An error response of the Cognito API.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class CognitoError {
@SerializedName("__type")
public String type = "";
@SerializedName("message")
public String message = "";
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* The {@link CognitoGson} class provides a {@link Gson} instance configured for (de)serializing all Cognito data
* from/to JSON.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class CognitoGson {
public static final Gson GSON = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create();
}

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.dto;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link InitiateAuthRequest} can be used to start a Cognito user SRP authentication challenge or to refresh
* expired tokens using a refresh token.
*
* When starting user SRP authentication Cognito will respond with a {@link ChallengeResponse}.
* When refreshing expired tokens Cognito grants the new tokens in a {@link AuthenticationResultResponse}.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class InitiateAuthRequest {
public String authFlow = "";
public String clientId = "";
public Map<String, String> authParameters = new TreeMap<>();
InitiateAuthRequest(String authFlow, String clientId, Map<String, String> authParameters) {
this.authFlow = authFlow;
this.clientId = clientId;
this.authParameters.putAll(authParameters);
}
public static InitiateAuthRequest userSrpAuth(String clientId, String username, String srpA) {
return new InitiateAuthRequest("USER_SRP_AUTH", clientId, Map.of("USERNAME", username, "SRP_A", srpA));
}
public static InitiateAuthRequest refreshTokenAuth(String clientId, String refreshToken) {
return new InitiateAuthRequest("REFRESH_TOKEN_AUTH", clientId, Map.of("REFRESH_TOKEN", refreshToken));
}
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Provides the details required for getting tokens using SRP from the Windcentrale Cognito user pool.
*
* @see https://mijn.windcentrale.nl/api/v0/labels/key?domain=mijn.windcentrale.nl
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class KeyResponse {
public String clientId = "";
public String region = "";
public String userPoolId = "";
}

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.dto;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Maps a subset of the Windcentrale API project details that is required for discovering windmill things.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class Project {
public static final class Participation {
public int share;
}
public String projectCode = "";
public String projectName = "";
public List<Participation> participations = List.of();
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.dto;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* The {@link RespondToAuthChallengeRequest} is sent to Cognito to respond to a user SRP {@link ChallengeResponse}.
* When the request is successful Cognito responds with a {@link AuthenticationResultResponse}.
*
* @author Wouter Born - Initial contribution
*/
public class RespondToAuthChallengeRequest {
public String challengeName = "PASSWORD_VERIFIER";
public String clientId = "";
public Map<String, String> challengeResponses = new LinkedHashMap<>();
public RespondToAuthChallengeRequest(String clientId, String username, String passwordClaimSecretBlock,
String passwordClaimSignature, String timestamp) {
this.clientId = clientId;
challengeResponses.put("USERNAME", username);
challengeResponses.put("PASSWORD_CLAIM_SECRET_BLOCK", passwordClaimSecretBlock);
challengeResponses.put("PASSWORD_CLAIM_SIGNATURE", passwordClaimSignature);
challengeResponses.put("TIMESTAMP", timestamp);
}
}

View File

@ -0,0 +1,84 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.dto;
import java.lang.reflect.Type;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.reflect.TypeToken;
/**
* The {@link WindcentraleGson} class provides a {@link Gson} instance configured for (de)serializing all Windcentrale
* data from/to JSON.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class WindcentraleGson {
public static final Gson GSON = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(Windmill.class, new WindmillConverter())
.registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeConverter()) //
.create();
public static final Type LIVE_DATA_RESPONSE_TYPE = new TypeToken<Map<Windmill, WindmillStatus>>() {
}.getType();
public static final Type PROJECTS_RESPONSE_TYPE = new TypeToken<List<Project>>() {
}.getType();
private static class WindmillConverter implements JsonSerializer<Windmill>, JsonDeserializer<Windmill> {
@Override
public JsonElement serialize(Windmill src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.getProjectCode());
}
@Override
public @Nullable Windmill deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
return Windmill.fromProjectCode(json.getAsString());
}
}
private static class ZonedDateTimeConverter
implements JsonSerializer<ZonedDateTime>, JsonDeserializer<ZonedDateTime> {
@Override
public JsonElement serialize(ZonedDateTime src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toEpochSecond());
}
@Override
public @Nullable ZonedDateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
return ZonedDateTime.ofInstant(Instant.ofEpochSecond(json.getAsLong()), ZoneId.systemDefault());
}
}
}

View File

@ -0,0 +1,133 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.dto;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Enumerates the Windcentrale windmills. The project codes are used in API requests and responses.
* The other details are used as Thing properties.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public enum Windmill {
DE_GROTE_GEERT(1, "WND-GG", "De Grote Geert", 9910, "Enercon E-70", 2008, "Delfzijl", "Groningen",
"53.280605, 6.955141", "https://www.windcentrale.nl/molens/de-grote-geert-2/"),
DE_JONGE_HELD(2, "WND-JH", "De Jonge Held", 10154, "Enercon E-70", 2008, "Delfzijl", "Groningen",
"53.277648, 6.954906", "https://www.windcentrale.nl/molens/de-jonge-held/"),
HET_RODE_HERT(31, "WND-RH", "Het Rode Hert", 6648, "Vestas V80", 2005, "Culemborg", "Gelderland",
"51.935831, 5.192109", "https://www.windcentrale.nl/molens/het-rode-hert/"),
DE_RANKE_ZWAAN(41, "WND-RZ", "De Ranke Zwaan", 6164, "Vestas V80", 2005, "Culemborg", "Gelderland",
"51.934915, 5.19989", "https://www.windcentrale.nl/molens/de-ranke-zwaan-2/"),
DE_WITTE_JUFFER(51, "WND-WJ", "De Witte Juffer", 5721, "Vestas V80", 2005, "Culemborg", "Gelderland",
"51.935178, 5.195860", "https://www.windcentrale.nl/molens/de-witte-juffer/"),
DE_BONTE_HEN(111, "WND-BH", "De Bonte Hen", 5579, "Vestas V52", 2009, "Burgerbrug", "Noord-Holland",
"52.757051, 4.684678", "https://www.windcentrale.nl/molens/de-bonte-hen-2/"),
DE_TROUWE_WACHTER(121, "WND-TW", "De Trouwe Wachter", 5602, "Vestas V52", 2009, "Burgerbrug", "Noord-Holland",
"52.758745, 4.686041", "https://www.windcentrale.nl/molens/de-trouwe-wachter-2/"),
DE_BLAUWE_REIGER(131, "WND-BR", "De Blauwe Reiger", 5534, "Vestas V52", 2009, "Burgerbrug", "Noord-Holland",
"52.760482, 4.687438", "https://www.windcentrale.nl/molens/de-blauwe-reiger/"),
DE_VIER_WINDEN(141, "WND-VW", "De Vier Winden", 5512, "Vestas V52", 2009, "Burgerbrug", "Noord-Holland",
"52.762219, 4.688837", "https://www.windcentrale.nl/molens/de-vier-winden-2/"),
DE_BOERENZWALUW(201, "WND-BZ", "De Boerenzwaluw", 3000, "Enercon E-44", 2015, "Burum", "Friesland",
"53.265572, 6.213929", "https://www.windcentrale.nl/molens/de-boerenzwaluw/"),
HET_VLIEGENDE_HERT(211, "WND-VH", "Het Vliegend Hert", 10000, "Lagerwey L82", 2019, "Rouveen", "Overijssel",
"52.595422, 6.223335", "https://www.windcentrale.nl/molens/het-vliegend-hert/");
private final int id;
private final String projectCode;
private final String name;
private final int totalShares;
private final String type;
private final int buildYear;
private final String municipality;
private final String province;
private final String coordinates;
private final String detailsUrl;
Windmill(int id, String projectCode, String name, int totalShares, String type, int buildYear, String municipality,
String province, String coordinates, String detailsUrl) {
this.id = id;
this.projectCode = projectCode;
this.name = name;
this.totalShares = totalShares;
this.type = type;
this.buildYear = buildYear;
this.municipality = municipality;
this.province = province;
this.coordinates = coordinates;
this.detailsUrl = detailsUrl;
}
public int getId() {
return id;
}
public String getProjectCode() {
return projectCode;
}
public String getName() {
return name;
}
public int getTotalShares() {
return totalShares;
}
public String getType() {
return type;
}
public int getBuildYear() {
return buildYear;
}
public String getMunicipality() {
return municipality;
}
public String getProvince() {
return province;
}
public String getCoordinates() {
return coordinates;
}
public String getDetailsUrl() {
return detailsUrl;
}
@Override
public String toString() {
return name;
}
public static @Nullable Windmill fromName(String name) {
return Arrays.stream(values()) //
.filter(windmill -> windmill.name.equals(name)) //
.findFirst().orElse(null);
}
public static @Nullable Windmill fromProjectCode(String projectCode) {
return Arrays.stream(values()) //
.filter(windmill -> windmill.projectCode.equals(projectCode)) //
.findFirst().orElse(null);
}
}

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.dto;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The live {@link WindmillStatus} provided by the Windcentrale API.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class WindmillStatus {
public int power;
public int powerPerShare;
public int powerPercentage;
public ZonedDateTime timestamp = ZonedDateTime.now();
public int totalRuntime;
public String windDirection = "";
public int windPower;
public int yearProduction;
public double yearRuntime;
@Override
public String toString() {
return "WindmillStatus [power=" + power + ", powerPerShare=" + powerPerShare + ", powerPercentage="
+ powerPercentage + ", timestamp=" + timestamp + ", totalRuntime=" + totalRuntime + ", windDirection="
+ windDirection + ", windPower=" + windPower + ", yearProduction=" + yearProduction + ", yearRuntime="
+ yearRuntime + "]";
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* An error occurred while retrieving data from the Windcentrale API.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class FailedGettingDataException extends Exception {
private static final long serialVersionUID = 4494062464212681327L;
public FailedGettingDataException(String message) {
super(message);
}
public FailedGettingDataException(String message, Throwable cause) {
super(message, cause);
}
public FailedGettingDataException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The Cognito access token used with the Windcentrale API is invalid and could not be refreshed.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class InvalidAccessTokenException extends Exception {
private static final long serialVersionUID = 9066624337663085233L;
public InvalidAccessTokenException(Exception cause) {
super(cause);
}
public InvalidAccessTokenException(String message, Throwable cause) {
super(message, cause);
}
public InvalidAccessTokenException(String message) {
super(message);
}
}

View File

@ -0,0 +1,182 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.handler;
import static java.util.function.Predicate.not;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.windcentrale.internal.WindcentraleDiscoveryService;
import org.openhab.binding.windcentrale.internal.api.RequestListener;
import org.openhab.binding.windcentrale.internal.api.TokenProvider;
import org.openhab.binding.windcentrale.internal.api.WindcentraleAPI;
import org.openhab.binding.windcentrale.internal.config.AccountConfiguration;
import org.openhab.binding.windcentrale.internal.exception.FailedGettingDataException;
import org.openhab.binding.windcentrale.internal.exception.InvalidAccessTokenException;
import org.openhab.binding.windcentrale.internal.listener.ThingStatusListener;
import org.openhab.core.io.net.http.HttpClientFactory;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link WindcentraleAccountHandler} provides the {@link WindcentraleAPI} instance used by the windmill handlers.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class WindcentraleAccountHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(WindcentraleAccountHandler.class);
private final HttpClientFactory httpClientFactory;
private final List<ThingStatusListener> thingStatusListeners = new CopyOnWriteArrayList<>();
private @Nullable WindcentraleAPI api;
private @Nullable Exception apiException;
private @Nullable Future<?> initializeFuture;
private final RequestListener requestListener = new RequestListener() {
@Override
public void onError(Exception exception) {
apiException = exception;
logger.debug("API exception occurred");
updateThingStatus();
}
@Override
public void onSuccess() {
if (apiException != null) {
apiException = null;
logger.debug("API exception cleared");
updateThingStatus();
}
}
};
public WindcentraleAccountHandler(Bridge bridge, HttpClientFactory httpClientFactory) {
super(bridge);
this.httpClientFactory = httpClientFactory;
}
public void addThingStatusListener(ThingStatusListener listener) {
thingStatusListeners.add(listener);
listener.thingStatusChanged(thing, thing.getStatus());
}
@Override
public void dispose() {
Future<?> localFuture = initializeFuture;
if (localFuture != null) {
localFuture.cancel(true);
initializeFuture = null;
}
WindcentraleAPI localAPI = api;
if (localAPI != null) {
localAPI.dispose();
api = null;
}
}
public @Nullable WindcentraleAPI getAPI() {
return api;
}
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
initializeFuture = scheduler.submit(() -> {
api = initializeAPI();
updateThingStatus();
});
}
private WindcentraleAPI initializeAPI() {
AccountConfiguration config = getConfigAs(AccountConfiguration.class);
TokenProvider tokenProvider = new TokenProvider(httpClientFactory, config.username, config.password);
WindcentraleAPI api = new WindcentraleAPI(httpClientFactory, tokenProvider);
api.addRequestListener(requestListener);
apiException = null;
try {
api.getProjects();
api.getLiveData();
} catch (FailedGettingDataException | InvalidAccessTokenException e) {
apiException = e;
}
return api;
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return List.of(WindcentraleDiscoveryService.class);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
public void removeThingStatusListener(ThingStatusListener listener) {
thingStatusListeners.remove(listener);
}
@Override
protected void updateStatus(ThingStatus status, ThingStatusDetail detail, @Nullable String comment) {
ThingStatus oldStatus = thing.getStatus();
super.updateStatus(status, detail, comment);
ThingStatus newStatus = thing.getStatus();
if (!oldStatus.equals(newStatus)) {
logger.debug("Updating listeners with status {}", status);
for (ThingStatusListener listener : thingStatusListeners) {
listener.thingStatusChanged(thing, status);
}
}
}
private void updateThingStatus() {
Exception e = apiException;
if (e != null) {
if (e instanceof InvalidAccessTokenException) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
} else {
Throwable cause = e.getCause();
String description = Stream
.of(Objects.requireNonNullElse(e.getMessage(), ""),
cause == null ? "" : Objects.requireNonNullElse(cause.getMessage(), ""))
.filter(not(String::isBlank)) //
.collect(Collectors.joining(": "));
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, description);
}
} else {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
}
}
}

View File

@ -1,166 +0,0 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.handler;
import static org.openhab.binding.windcentrale.internal.WindcentraleBindingConstants.*;
import static org.openhab.core.library.unit.MetricPrefix.KILO;
import java.io.IOException;
import java.math.BigDecimal;
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.windcentrale.internal.config.MillConfig;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
/**
* The {@link WindcentraleHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Marcel Verpaalen - Initial contribution
* @author Wouter Born - Add null annotations
*/
@NonNullByDefault
public class WindcentraleHandler extends BaseThingHandler {
private static final String HOURS_RUN_THIS_YEAR = "hoursRunThisYear";
private static final String URL_FORMAT = "https://zep-api.windcentrale.nl/production/%d/live?ignoreLoadingBar=true";
private static final long CACHE_EXPIRY = TimeUnit.SECONDS.toMillis(5);
private final Logger logger = LoggerFactory.getLogger(WindcentraleHandler.class);
private @Nullable MillConfig millConfig;
private @Nullable String millUrl;
private @Nullable ScheduledFuture<?> pollingJob;
private final ExpiringCache<@Nullable String> windcentraleCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
try {
return millUrl != null ? HttpUtil.executeUrl("GET", millUrl, 5000) : null;
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
return null;
}
});
public WindcentraleHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command == RefreshType.REFRESH) {
logger.debug("Refreshing {}", channelUID);
updateData();
} else {
logger.debug("This binding is a read-only binding and cannot handle commands");
}
}
@Override
public void initialize() {
logger.debug("Initializing Windcentrale handler '{}'", getThing().getUID());
final MillConfig config = getConfig().as(MillConfig.class);
millConfig = config;
millUrl = String.format(URL_FORMAT, config.millId);
pollingJob = scheduler.scheduleWithFixedDelay(this::updateData, 0, config.refreshInterval, TimeUnit.SECONDS);
logger.debug("Polling job scheduled to run every {} sec. for '{}'", config.refreshInterval,
getThing().getUID());
updateProperty(Thing.PROPERTY_VENDOR, "Windcentrale");
updateProperty(Thing.PROPERTY_MODEL_ID, "Windmolen");
updateProperty(Thing.PROPERTY_SERIAL_NUMBER, Integer.toString(config.millId));
}
@Override
public void dispose() {
logger.debug("Disposing Windcentrale handler '{}'", getThing().getUID());
final ScheduledFuture<?> pollingJob = this.pollingJob;
if (pollingJob != null) {
pollingJob.cancel(true);
this.pollingJob = null;
}
}
private synchronized void updateData() {
try {
logger.debug("Update windmill data '{}'", getThing().getUID());
final MillConfig config = millConfig;
final String rawMillData = windcentraleCache.getValue();
if (config == null || rawMillData == null) {
return;
}
logger.trace("Retrieved updated mill data: {}", rawMillData);
final JsonElement jsonElement = JsonParser.parseString(rawMillData);
if (!(jsonElement instanceof JsonObject)) {
throw new JsonParseException("Could not parse windmill json data");
}
final JsonObject millData = (JsonObject) jsonElement;
updateState(CHANNEL_WIND_SPEED, new DecimalType(millData.get(CHANNEL_WIND_SPEED).getAsString()));
updateState(CHANNEL_WIND_DIRECTION, new StringType(millData.get(CHANNEL_WIND_DIRECTION).getAsString()));
updateState(CHANNEL_POWER_TOTAL,
new QuantityType<>(millData.get(CHANNEL_POWER_TOTAL).getAsBigDecimal(), KILO(Units.WATT)));
updateState(CHANNEL_POWER_PER_WD,
new QuantityType<>(
millData.get(CHANNEL_POWER_PER_WD).getAsBigDecimal().multiply(new BigDecimal(config.wd)),
Units.WATT));
updateState(CHANNEL_POWER_RELATIVE,
new QuantityType<>(millData.get(CHANNEL_POWER_RELATIVE).getAsBigDecimal(), Units.PERCENT));
updateState(CHANNEL_ENERGY,
new QuantityType<>(millData.get(CHANNEL_ENERGY).getAsBigDecimal(), Units.KILOWATT_HOUR));
updateState(CHANNEL_ENERGY_FC,
new QuantityType<>(millData.get(CHANNEL_ENERGY_FC).getAsBigDecimal(), Units.KILOWATT_HOUR));
updateState(CHANNEL_RUNTIME,
new QuantityType<>(millData.get(HOURS_RUN_THIS_YEAR).getAsBigDecimal(), Units.HOUR));
updateState(CHANNEL_RUNTIME_PER,
new QuantityType<>(millData.get(CHANNEL_RUNTIME_PER).getAsBigDecimal(), Units.PERCENT));
updateState(CHANNEL_LAST_UPDATE, new DateTimeType(millData.get(CHANNEL_LAST_UPDATE).getAsString()));
if (!getThing().getStatus().equals(ThingStatus.ONLINE)) {
updateStatus(ThingStatus.ONLINE);
}
} catch (final RuntimeException e) {
logger.debug("Failed to process windmill data", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"@text/offline.mill-data-error");
}
}
}

View File

@ -0,0 +1,198 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.handler;
import static org.openhab.binding.windcentrale.internal.WindcentraleBindingConstants.*;
import static org.openhab.core.library.unit.MetricPrefix.KILO;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Map;
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.windcentrale.internal.api.WindcentraleAPI;
import org.openhab.binding.windcentrale.internal.config.WindmillConfiguration;
import org.openhab.binding.windcentrale.internal.dto.Windmill;
import org.openhab.binding.windcentrale.internal.dto.WindmillStatus;
import org.openhab.binding.windcentrale.internal.exception.FailedGettingDataException;
import org.openhab.binding.windcentrale.internal.exception.InvalidAccessTokenException;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
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.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link WindcentraleWindmillHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Marcel Verpaalen - Initial contribution
* @author Wouter Born - Add null annotations
* @author Wouter Born - Add support for new API with authentication
*/
@NonNullByDefault
public class WindcentraleWindmillHandler extends BaseThingHandler {
private static final long CACHE_EXPIRY = TimeUnit.SECONDS.toMillis(5);
private final Logger logger = LoggerFactory.getLogger(WindcentraleWindmillHandler.class);
private @NonNullByDefault({}) WindmillConfiguration config;
private @Nullable Windmill windmill;
private @Nullable ScheduledFuture<?> pollingJob;
private final ExpiringCache<@Nullable WindmillStatus> statusCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
try {
WindcentraleAPI api = getAPI();
Windmill windmill = this.windmill;
return api == null || windmill == null ? null : api.getLiveData(windmill);
} catch (FailedGettingDataException | InvalidAccessTokenException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
return null;
}
});
public WindcentraleWindmillHandler(Thing thing) {
super(thing);
}
@Override
public void dispose() {
logger.debug("Disposing Windcentrale handler '{}'", getThing().getUID());
final ScheduledFuture<?> pollingJob = this.pollingJob;
if (pollingJob != null) {
pollingJob.cancel(true);
this.pollingJob = null;
}
}
protected @Nullable WindcentraleAPI getAPI() {
Bridge bridge = getBridge();
if (bridge == null) {
return null;
}
WindcentraleAccountHandler accountHandler = ((WindcentraleAccountHandler) bridge.getHandler());
return accountHandler == null ? null : accountHandler.getAPI();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command == RefreshType.REFRESH) {
logger.debug("Refreshing {}", channelUID);
updateData();
} else {
logger.debug("This binding is a read-only binding and cannot handle commands");
}
}
@Override
public void initialize() {
logger.debug("Initializing Windcentrale handler '{}'", getThing().getUID());
WindmillConfiguration config = getConfig().as(WindmillConfiguration.class);
this.config = config;
Windmill windmill = Windmill.fromName(config.name);
this.windmill = windmill;
if (windmill == null) {
// only occurs when a mismatch is introduced between config parameter options and enum values
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"Invalid windmill name: " + config.name);
return;
}
updateProperties(getWindmillProperties(windmill));
updateStatus(ThingStatus.UNKNOWN);
pollingJob = scheduler.scheduleWithFixedDelay(this::updateData, 0, config.refreshInterval, TimeUnit.SECONDS);
logger.debug("Polling job scheduled to run every {} sec. for '{}'", config.refreshInterval,
getThing().getUID());
}
public static Map<String, String> getWindmillProperties(Windmill windmill) {
Map<String, String> properties = new HashMap<>();
properties.put(Thing.PROPERTY_VENDOR, "Windcentrale");
properties.put(Thing.PROPERTY_MODEL_ID, windmill.getType());
properties.put(Thing.PROPERTY_SERIAL_NUMBER, Integer.toString(windmill.getId()));
properties.put(PROPERTY_PROJECT_CODE, windmill.getProjectCode());
properties.put(PROPERTY_TOTAL_SHARES, Integer.toString(windmill.getTotalShares()));
properties.put(PROPERTY_BUILD_YEAR, Integer.toString(windmill.getBuildYear()));
properties.put(PROPERTY_MUNICIPALITY, windmill.getMunicipality());
properties.put(PROPERTY_PROVINCE, windmill.getProvince());
properties.put(PROPERTY_COORDINATES, windmill.getCoordinates());
properties.put(PROPERTY_DETAILS_URL, windmill.getDetailsUrl());
return properties;
}
private double yearRuntimePercentage(double yearRuntime) {
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Europe/Amsterdam"));
ZonedDateTime startOfThisYear = now.withDayOfMonth(1).withMonth(1).truncatedTo(ChronoUnit.DAYS);
long hoursThisYear = Duration.between(startOfThisYear, now).toHours();
// prevent divide by zero when the year has just started
return 100 * (hoursThisYear > 0 ? yearRuntime / hoursThisYear : 1);
}
private synchronized void updateData() {
logger.debug("Updating windmill data '{}'", getThing().getUID());
WindmillStatus status = statusCache.getValue();
if (status == null) {
return;
}
logger.trace("Retrieved updated windmill status: {}", status);
updateState(CHANNEL_ENERGY_TOTAL, new QuantityType<>(status.yearProduction, Units.KILOWATT_HOUR));
updateState(CHANNEL_POWER_RELATIVE, new QuantityType<>(status.powerPercentage, Units.PERCENT));
updateState(CHANNEL_POWER_SHARES, new QuantityType<>(
new BigDecimal(status.powerPerShare).multiply(new BigDecimal(config.shares)), Units.WATT));
updateState(CHANNEL_POWER_TOTAL, new QuantityType<>(status.power, KILO(Units.WATT)));
updateState(CHANNEL_RUN_PERCENTAGE,
status.yearRuntime >= 0 ? new QuantityType<>(yearRuntimePercentage(status.yearRuntime), Units.PERCENT)
: UnDefType.UNDEF);
updateState(CHANNEL_RUN_TIME,
status.yearRuntime >= 0 ? new QuantityType<>(new BigDecimal(status.yearRuntime), Units.HOUR)
: UnDefType.UNDEF);
updateState(CHANNEL_WIND_DIRECTION, new StringType(status.windDirection));
updateState(CHANNEL_WIND_SPEED, new DecimalType(status.windPower));
updateState(CHANNEL_TIMESTAMP, new DateTimeType(status.timestamp));
if (!ThingStatus.ONLINE.equals(getThing().getStatus())) {
updateStatus(ThingStatus.ONLINE);
}
}
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.listener;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
/**
* Interface for listeners of thing status changes.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public interface ThingStatusListener {
public void thingStatusChanged(Thing thing, ThingStatus status);
}

View File

@ -5,41 +5,31 @@ binding.windcentrale.description = Binding for Windcentrale windmills
# thing types
thing-type.windcentrale.mill.label = Windcentrale Windmill
thing-type.windcentrale.account.label = Windcentrale Account
thing-type.windcentrale.account.description = An account for using the Windcentrale API
thing-type.windcentrale.windmill.label = Windcentrale Windmill
# thing types config
thing-type.config.windcentrale.mill.millId.label = Windmill
thing-type.config.windcentrale.mill.millId.option.1 = De Grote Geert
thing-type.config.windcentrale.mill.millId.option.2 = De Jonge Held
thing-type.config.windcentrale.mill.millId.option.31 = Het Rode Hert
thing-type.config.windcentrale.mill.millId.option.41 = De Ranke Zwaan
thing-type.config.windcentrale.mill.millId.option.51 = De Witte Juffer
thing-type.config.windcentrale.mill.millId.option.111 = De Bonte Hen
thing-type.config.windcentrale.mill.millId.option.121 = De Trouwe Wachter
thing-type.config.windcentrale.mill.millId.option.131 = De Blauwe Reiger
thing-type.config.windcentrale.mill.millId.option.141 = De Vier Winden
thing-type.config.windcentrale.mill.millId.option.201 = De Boerenzwaluw
thing-type.config.windcentrale.mill.refreshInterval.label = Refresh Interval
thing-type.config.windcentrale.mill.refreshInterval.description = Refresh interval for refreshing the data in seconds
thing-type.config.windcentrale.mill.wd.label = Wind Shares
thing-type.config.windcentrale.mill.wd.description = Number of wind shares ("Winddelen")
thing-type.config.windcentrale.account.password.label = Password
thing-type.config.windcentrale.account.username.label = Username
thing-type.config.windcentrale.windmill.name.label = Windmill
thing-type.config.windcentrale.windmill.refreshInterval.label = Refresh Interval
thing-type.config.windcentrale.windmill.refreshInterval.description = Refresh interval for refreshing the data in seconds
thing-type.config.windcentrale.windmill.shares.label = Wind Shares
thing-type.config.windcentrale.windmill.shares.description = Number of wind shares ("Winddelen")
# channel types
channel-type.windcentrale.kwh.label = Energy
channel-type.windcentrale.kwhForecast.label = Energy Forecast
channel-type.windcentrale.powerAbsTot.label = Total Power
channel-type.windcentrale.powerAbsWd.label = Wind Shares Power
channel-type.windcentrale.powerRel.label = Relative Power
channel-type.windcentrale.runPercentage.label = Run Percentage
channel-type.windcentrale.runPercentage.description = Run percentage this year
channel-type.windcentrale.runTime.label = Run Time
channel-type.windcentrale.runTime.description = Run time this year
channel-type.windcentrale.energy-total.label = Total Energy
channel-type.windcentrale.energy-total.description = Energy produced this year
channel-type.windcentrale.power-relative.label = Relative Power
channel-type.windcentrale.power-shares.label = Wind Shares Power
channel-type.windcentrale.power-total.label = Total Power
channel-type.windcentrale.run-percentage.label = Run Percentage
channel-type.windcentrale.run-percentage.description = Run percentage this year
channel-type.windcentrale.run-time.label = Run Time
channel-type.windcentrale.run-time.description = Run time this year
channel-type.windcentrale.timestamp.label = Last Updated
channel-type.windcentrale.windDirection.label = Wind Direction
channel-type.windcentrale.windSpeed.label = Wind Speed
# status messages
offline.mill-data-error = Failed to process mill data
channel-type.windcentrale.wind-direction.label = Wind Direction
channel-type.windcentrale.wind-speed.label = Wind Speed

View File

@ -5,41 +5,31 @@ binding.windcentrale.description = Binding voor Windcentrale windmolens
# thing types
thing-type.windcentrale.mill.label = Windcentrale windmolen
thing-type.windcentrale.account.label = Windcentrale Account
thing-type.windcentrale.account.description = Een account voor het gebruik van de Windcentrale API
thing-type.windcentrale.windmill.label = Windcentrale windmolen
# thing types config
thing-type.config.windcentrale.mill.millId.label = Windmolen
thing-type.config.windcentrale.mill.millId.option.1 = De Grote Geert
thing-type.config.windcentrale.mill.millId.option.2 = De Jonge Held
thing-type.config.windcentrale.mill.millId.option.31 = Het Rode Hert
thing-type.config.windcentrale.mill.millId.option.41 = De Ranke Zwaan
thing-type.config.windcentrale.mill.millId.option.51 = De Witte Juffer
thing-type.config.windcentrale.mill.millId.option.111 = De Bonte Hen
thing-type.config.windcentrale.mill.millId.option.121 = De Trouwe Wachter
thing-type.config.windcentrale.mill.millId.option.131 = De Blauwe Reiger
thing-type.config.windcentrale.mill.millId.option.141 = De Vier Winden
thing-type.config.windcentrale.mill.millId.option.201 = De Boerenzwaluw
thing-type.config.windcentrale.mill.refreshInterval.label = Ververs interval
thing-type.config.windcentrale.mill.refreshInterval.description = Ververs interval in seconden
thing-type.config.windcentrale.mill.wd.label = Aantal Winddelen
thing-type.config.windcentrale.mill.wd.description = Aantal Winddelen in bezit
thing-type.config.windcentrale.account.password.label = Wachtwoord
thing-type.config.windcentrale.account.username.label = Gebruikersnaam
thing-type.config.windcentrale.windmill.name.label = Windmolen
thing-type.config.windcentrale.windmill.refreshInterval.label = Ververs interval
thing-type.config.windcentrale.windmill.refreshInterval.description = Ververs interval in seconden
thing-type.config.windcentrale.windmill.shares.label = Aantal Winddelen
thing-type.config.windcentrale.windmill.shares.description = Aantal Winddelen in bezit
# channel types
channel-type.windcentrale.kwh.label = Energie
channel-type.windcentrale.kwhForecast.label = Energie Voorspelling
channel-type.windcentrale.powerAbsTot.label = Totaal Vermogen
channel-type.windcentrale.powerAbsWd.label = Winddelen Vermogen
channel-type.windcentrale.powerRel.label = Relatief Vermogen
channel-type.windcentrale.runPercentage.label = Operationeel Percentage
channel-type.windcentrale.runPercentage.description = Het aantal procent van de tijd dat de molen operationeel is dit jaar
channel-type.windcentrale.runTime.label = Operationeel Tijd
channel-type.windcentrale.runTime.description = Het aantal uren dat de molen operationeel is dit jaar
channel-type.windcentrale.energy-total.label = Totaal Energie
channel-type.windcentrale.energy-total.description = De totale energie geproduceerd door de windmolen dit jaar
channel-type.windcentrale.power-relative.label = Relatief Vermogen
channel-type.windcentrale.power-shares.label = Winddelen Vermogen
channel-type.windcentrale.power-total.label = Totaal Vermogen
channel-type.windcentrale.run-percentage.label = Operationeel Percentage
channel-type.windcentrale.run-percentage.description = Het aantal procent van de tijd dat de windmolen operationeel is dit jaar
channel-type.windcentrale.run-time.label = Operationeel Tijd
channel-type.windcentrale.run-time.description = Het aantal uren dat de windmolen operationeel is dit jaar
channel-type.windcentrale.timestamp.label = Laatst Bijgewerkt
channel-type.windcentrale.windDirection.label = Windrichting
channel-type.windcentrale.windSpeed.label = Windkracht
# status messages
offline.mill-data-error = Fout bij het verwerken van de molen data
channel-type.windcentrale.wind-direction.label = Windrichting
channel-type.windcentrale.wind-speed.label = Windkracht

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="windcentrale"
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="account">
<label>Windcentrale Account</label>
<description>An account for using the Windcentrale API</description>
<config-description>
<parameter name="username" type="text" required="true">
<label>Username</label>
</parameter>
<parameter name="password" type="text" required="true">
<context>password</context>
<label>Password</label>
</parameter>
</config-description>
</bridge-type>
</thing:thing-descriptions>

View File

@ -4,19 +4,22 @@
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="mill">
<thing-type id="windmill">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>Windcentrale Windmill</label>
<channels>
<channel id="windSpeed" typeId="windSpeed"/>
<channel id="windDirection" typeId="windDirection"/>
<channel id="powerAbsTot" typeId="powerAbsTot"/>
<channel id="powerAbsWd" typeId="powerAbsWd"/>
<channel id="powerRel" typeId="powerRel"/>
<channel id="kwh" typeId="kwh"/>
<channel id="kwhForecast" typeId="kwhForecast"/>
<channel id="runPercentage" typeId="runPercentage"/>
<channel id="runTime" typeId="runTime"/>
<channel id="power-shares" typeId="power-shares"/>
<channel id="power-total" typeId="power-total"/>
<channel id="power-relative" typeId="power-relative"/>
<channel id="energy-total" typeId="energy-total"/>
<channel id="run-time" typeId="run-time"/>
<channel id="run-percentage" typeId="run-percentage"/>
<channel id="wind-speed" typeId="wind-speed"/>
<channel id="wind-direction" typeId="wind-direction"/>
<channel id="timestamp" typeId="timestamp"/>
</channels>
@ -24,24 +27,27 @@
<property name="vendor">Windcentrale</property>
</properties>
<representation-property>projectCode</representation-property>
<config-description>
<parameter name="millId" type="integer" required="true">
<parameter name="name" type="text" required="true">
<label>Windmill</label>
<options>
<option value="1">De Grote Geert</option>
<option value="2">De Jonge Held</option>
<option value="31">Het Rode Hert</option>
<option value="41">De Ranke Zwaan</option>
<option value="51">De Witte Juffer</option>
<option value="111">De Bonte Hen</option>
<option value="121">De Trouwe Wachter</option>
<option value="131">De Blauwe Reiger</option>
<option value="141">De Vier Winden</option>
<option value="201">De Boerenzwaluw</option>
<option value="De Blauwe Reiger">De Blauwe Reiger</option>
<option value="De Boerenzwaluw">De Boerenzwaluw</option>
<option value="De Bonte Hen">De Bonte Hen</option>
<option value="De Grote Geert">De Grote Geert</option>
<option value="De Jonge Held">De Jonge Held</option>
<option value="De Ranke Zwaan">De Ranke Zwaan</option>
<option value="De Trouwe Wachter">De Trouwe Wachter</option>
<option value="De Vier Winden">De Vier Winden</option>
<option value="De Witte Juffer">De Witte Juffer</option>
<option value="Het Rode Hert">Het Rode Hert</option>
<option value="Het Vliegend Hert">Het Vliegend Hert</option>
</options>
<default>131</default>
<default>De Blauwe Reiger</default>
</parameter>
<parameter name="wd" type="integer" required="false">
<parameter name="shares" type="integer" min="0" max="11000" required="false">
<label>Wind Shares</label>
<description>Number of wind shares ("Winddelen")</description>
<default>1</default>
@ -55,63 +61,59 @@
</config-description>
</thing-type>
<channel-type id="windSpeed">
<item-type>Number</item-type>
<label>Wind Speed</label>
<category>Wind</category>
<state pattern="%d Bft" readOnly="true"/>
<channel-type id="energy-total">
<item-type>Number:Energy</item-type>
<label>Total Energy</label>
<description>Energy produced this year</description>
<category>Energy</category>
<state pattern="%.0f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="windDirection">
<item-type>String</item-type>
<label>Wind Direction</label>
<state pattern="%s" readOnly="true"/>
</channel-type>
<channel-type id="powerRel">
<channel-type id="power-relative">
<item-type>Number:Dimensionless</item-type>
<label>Relative Power</label>
<category>Energy</category>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="runPercentage">
<channel-type id="power-shares">
<item-type>Number:Power</item-type>
<label>Wind Shares Power</label>
<category>Energy</category>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="power-total">
<item-type>Number:Power</item-type>
<label>Total Power</label>
<category>Energy</category>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="run-time">
<item-type>Number:Time</item-type>
<label>Run Time</label>
<description>Run time this year</description>
<state pattern="%.0f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="run-percentage">
<item-type>Number:Dimensionless</item-type>
<label>Run Percentage</label>
<description>Run percentage this year</description>
<category>Energy</category>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="runTime">
<item-type>Number:Time</item-type>
<label>Run Time</label>
<description>Run time this year</description>
<state pattern="%.0f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="powerAbsWd">
<item-type>Number:Power</item-type>
<label>Wind Shares Power</label>
<category>Energy</category>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="powerAbsTot">
<item-type>Number:Power</item-type>
<label>Total Power</label>
<category>Energy</category>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="kwh">
<item-type>Number:Energy</item-type>
<label>Energy</label>
<category>Energy</category>
<state pattern="%.0f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="kwhForecast">
<item-type>Number:Energy</item-type>
<label>Energy Forecast</label>
<category>Energy</category>
<state pattern="%.0f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="timestamp">
<item-type>DateTime</item-type>
<label>Last Updated</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="wind-direction">
<item-type>String</item-type>
<label>Wind Direction</label>
<state pattern="%s" readOnly="true"/>
</channel-type>
<channel-type id="wind-speed">
<item-type>Number</item-type>
<label>Wind Speed</label>
<category>Wind</category>
<state pattern="%d Bft" readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,109 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.dto;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
/**
* Tests (de)serialization of AWS Cognito requests/responses to/from JSON.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class CognitoGsonTest {
private static final DataUtil DATA_UTIL = new DataUtil(CognitoGson.GSON);
@Test
public void serializeInitiateAuthRequestSrp() throws IOException {
String json = DATA_UTIL.toJson(InitiateAuthRequest.userSrpAuth("clientId123", "username456", "srpA789"));
assertThat(json, is(DATA_UTIL.fromFile("initiate-auth-request-srp.json")));
}
@Test
public void deserializeChallengeResponseSrp() throws IOException {
ChallengeResponse response = DATA_UTIL.fromJson("challenge-response-srp.json", ChallengeResponse.class);
assertThat(response, is(notNullValue()));
assertThat(response.challengeName, is("PASSWORD_VERIFIER"));
assertThat(response.getSalt(), is("salt123"));
assertThat(response.getSecretBlock(), is("secretBlock456"));
assertThat(response.getSrpB(), is("srpB789"));
assertThat(response.getUsername(), is("username@acme.com"));
assertThat(response.getUserIdForSrp(), is("userid@acme.com"));
}
@Test
public void serializeInitiateAuthRequestRefresh() throws IOException {
String json = DATA_UTIL.toJson(InitiateAuthRequest.refreshTokenAuth("clientId123", "refreshToken123"));
assertThat(json, is(DATA_UTIL.fromFile("initiate-auth-request-refresh.json")));
}
@Test
public void deserializeInitiateAuthResponseRefresh() throws IOException {
AuthenticationResultResponse response = DATA_UTIL.fromJson("authentication-result-response-refresh.json",
AuthenticationResultResponse.class);
assertThat(response, is(notNullValue()));
assertThat(response.getAccessToken(), is("accessToken123"));
assertThat(response.getExpiresIn(), is(3600));
assertThat(response.getIdToken(), is("idToken456"));
assertThat(response.getRefreshToken(), is(""));
assertThat(response.getTokenType(), is("Bearer"));
}
@Test
public void serializeRespondToAuthChallengeRequest() throws IOException {
String json = DATA_UTIL.toJson(new RespondToAuthChallengeRequest("clientId123", "username@acme.com",
"passwordClaimSecretBlock456", "passwordClaimSignature789", "Thu Apr 6 07:16:19 UTC 2023"));
assertThat(json, is(DATA_UTIL.fromFile("respond-to-auth-challenge-request.json")));
}
@Test
public void deserializeRespondToAuthChallengeResponse() throws IOException {
AuthenticationResultResponse response = DATA_UTIL.fromJson("authentication-result-response-challenge.json",
AuthenticationResultResponse.class);
assertThat(response, is(notNullValue()));
assertThat(response.getAccessToken(), is("accessToken123"));
assertThat(response.getExpiresIn(), is(3600));
assertThat(response.getIdToken(), is("idToken456"));
assertThat(response.getRefreshToken(), is("refreshToken789"));
assertThat(response.getTokenType(), is("Bearer"));
}
@Test
public void deserializeErrorResponseInvalidParameter() throws IOException {
CognitoError response = DATA_UTIL.fromJson("cognito-error-response-invalid-parameter.json", CognitoError.class);
assertThat(response, is(notNullValue()));
assertThat(response.type, is("InvalidParameterException"));
assertThat(response.message, is("Missing required parameter REFRESH_TOKEN"));
}
@Test
public void deserializeErrorResponseNotAuthorized() throws IOException {
CognitoError response = DATA_UTIL.fromJson("cognito-error-response-not-authorized.json", CognitoError.class);
assertThat(response, is(notNullValue()));
assertThat(response.type, is("NotAuthorizedException"));
assertThat(response.message, is("Incorrect username or password."));
}
}

View File

@ -0,0 +1,74 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.dto;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.Gson;
import com.google.gson.stream.JsonWriter;
/**
* Utility class for working with test data in unit tests.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class DataUtil {
private final Gson gson;
public DataUtil(Gson gson) {
this.gson = gson;
}
@SuppressWarnings("null")
public Reader openDataReader(String fileName) throws FileNotFoundException {
String packagePath = (DataUtil.class.getPackage().getName()).replaceAll("\\.", "/");
String filePath = "src/test/resources/" + packagePath + "/" + fileName;
InputStream inputStream = new FileInputStream(filePath);
return new InputStreamReader(inputStream, StandardCharsets.UTF_8);
}
public <T> T fromJson(String fileName, Type typeOfT) throws IOException {
try (Reader reader = openDataReader(fileName)) {
return gson.fromJson(reader, typeOfT);
}
}
public String fromFile(String fileName) throws IOException {
try (Reader reader = openDataReader(fileName)) {
return new BufferedReader(reader).lines().parallel().collect(Collectors.joining("\n"));
}
}
public String toJson(Object object) {
StringWriter writer = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(writer);
jsonWriter.setIndent(" ");
gson.toJson(object, object.getClass(), jsonWriter);
return writer.toString();
}
}

View File

@ -0,0 +1,141 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.dto;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.windcentrale.internal.dto.Project.Participation;
/**
* Tests deserialization of Windcentrale API responses from JSON.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class WindcentraleGsonTest {
private static final DataUtil DATA_UTIL = new DataUtil(WindcentraleGson.GSON);
@Test
public void deserializeKeyResponse() throws IOException {
KeyResponse key = DATA_UTIL.fromJson("key-response.json", KeyResponse.class);
assertThat(key, is(notNullValue()));
assertThat(key.clientId, is("715j3r0trk7o8dqg3md57il7q0"));
assertThat(key.region, is("eu-west-1"));
assertThat(key.userPoolId, is("eu-west-1_U7eYBPrBd"));
}
@Test
public void deserializeProjectsResponse() throws IOException {
List<Project> projects = DATA_UTIL.fromJson("projects-response.json", WindcentraleGson.PROJECTS_RESPONSE_TYPE);
assertThat(projects, is(notNullValue()));
assertThat(projects.size(), is(1));
Project project = projects.get(0);
assertThat(project.projectName, is("De Grote Geert"));
assertThat(project.projectCode, is("WND-GG"));
List<Participation> participations = Objects.requireNonNull(project.participations);
assertThat(participations.size(), is(2));
assertThat(participations.get(0).share, is(20));
assertThat(participations.get(1).share, is(50));
}
@Test
public void deserializeLiveDataResponseEmpty() throws IOException {
Map<Windmill, WindmillStatus> map = DATA_UTIL.fromJson("live-data-response-empty.json",
WindcentraleGson.LIVE_DATA_RESPONSE_TYPE);
assertThat(map, is(notNullValue()));
assertThat(map.size(), is(0));
}
@Test
public void deserializeLiveDataResponseSingle() throws IOException {
Map<Windmill, WindmillStatus> map = DATA_UTIL.fromJson("live-data-response-single.json",
WindcentraleGson.LIVE_DATA_RESPONSE_TYPE);
assertThat(map, is(notNullValue()));
assertThat(map.size(), is(1));
assertDeJongeHeldStatus(map);
}
@Test
public void deserializeLiveDataResponseMultiple() throws IOException {
Map<Windmill, WindmillStatus> map = DATA_UTIL.fromJson("live-data-response-multiple.json",
WindcentraleGson.LIVE_DATA_RESPONSE_TYPE);
assertThat(map, is(notNullValue()));
assertThat(map.size(), is(11));
assertDeBlauweReigerStatus(map);
assertDeJongeHeldStatus(map);
assertDeWitteJufferStatus(map);
}
private void assertDeBlauweReigerStatus(Map<Windmill, WindmillStatus> map) {
WindmillStatus status = Objects.requireNonNull(map.get(Windmill.DE_BLAUWE_REIGER));
assertThat(status.powerPerShare, is(150));
assertThat(status.timestamp.toEpochSecond(), is(1680425425L));
assertThat(status.windPower, is(7));
assertThat(status.power, is(827));
assertThat(status.windDirection, is("O"));
assertThat(status.yearProduction, is(872488));
assertThat(status.totalRuntime, is(29470));
assertThat(status.yearRuntime, is(-98268833.015556d));
assertThat(status.powerPercentage, is(98));
}
private void assertDeJongeHeldStatus(Map<Windmill, WindmillStatus> map) {
WindmillStatus status = Objects.requireNonNull(map.get(Windmill.DE_JONGE_HELD));
assertThat(status.powerPerShare, is(52));
assertThat(status.timestamp.toEpochSecond(), is(1680425425L));
assertThat(status.windPower, is(5));
assertThat(status.power, is(522));
assertThat(status.windDirection, is("O"));
assertThat(status.yearProduction, is(1508090));
assertThat(status.totalRuntime, is(122330));
assertThat(status.yearRuntime, is(2089d));
assertThat(status.powerPercentage, is(23));
}
private void assertDeWitteJufferStatus(Map<Windmill, WindmillStatus> map) {
WindmillStatus status = Objects.requireNonNull(map.get(Windmill.DE_WITTE_JUFFER));
assertThat(status.powerPerShare, is(134));
assertThat(status.timestamp.toEpochSecond(), is(1680425425L));
assertThat(status.windPower, is(5));
assertThat(status.power, is(764));
assertThat(status.windDirection, is("NO"));
assertThat(status.yearProduction, is(1233164));
assertThat(status.totalRuntime, is(111171));
assertThat(status.yearRuntime, is(2118.266667d));
assertThat(status.powerPercentage, is(39));
}
}

View File

@ -0,0 +1,70 @@
/**
* Copyright (c) 2010-2022 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.windcentrale.internal.dto;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
/**
* Tests the {@link Windmill} enum.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class WindmillTest {
@Test
public void fromName() {
assertThat(Windmill.fromName("Unknown Windmill"), nullValue());
assertThat(Windmill.fromName("De Grote Geert"), is(Windmill.DE_GROTE_GEERT));
for (Windmill windmill : Windmill.values()) {
assertThat(Windmill.fromName(windmill.getName()), is(windmill));
}
}
@Test
public void fromProjectCode() {
assertThat(Windmill.fromProjectCode("WND-UNKNOWN"), nullValue());
assertThat(Windmill.fromProjectCode("WND-GG"), is(Windmill.DE_GROTE_GEERT));
for (Windmill windmill : Windmill.values()) {
assertThat(Windmill.fromProjectCode(windmill.getProjectCode()), is(windmill));
}
}
@Test
public void namesAreUnique() {
int count = (int) Arrays.stream(Windmill.values()) //
.map(Windmill::getName) //
.distinct() //
.count();
assertThat(count, is(Windmill.values().length));
}
@Test
public void projectCodesAreUnique() {
int count = (int) Arrays.stream(Windmill.values()) //
.map(Windmill::getProjectCode) //
.distinct() //
.count();
assertThat(count, is(Windmill.values().length));
}
}

View File

@ -0,0 +1,12 @@
{
"AuthenticationResult": {
"AccessToken": "accessToken123",
"ExpiresIn": 3600,
"IdToken": "idToken456",
"RefreshToken": "refreshToken789",
"TokenType": "Bearer"
},
"ChallengeParameters": {
}
}

View File

@ -0,0 +1,11 @@
{
"AuthenticationResult": {
"AccessToken": "accessToken123",
"ExpiresIn": 3600,
"IdToken": "idToken456",
"TokenType": "Bearer"
},
"ChallengeParameters": {
}
}

View File

@ -0,0 +1,10 @@
{
"ChallengeName": "PASSWORD_VERIFIER",
"ChallengeParameters": {
"SALT": "salt123",
"SECRET_BLOCK": "secretBlock456",
"SRP_B": "srpB789",
"USERNAME": "username@acme.com",
"USER_ID_FOR_SRP": "userid@acme.com"
}
}

View File

@ -0,0 +1,4 @@
{
"__type": "InvalidParameterException",
"message": "Missing required parameter REFRESH_TOKEN"
}

View File

@ -0,0 +1,4 @@
{
"__type": "NotAuthorizedException",
"message": "Incorrect username or password."
}

View File

@ -0,0 +1,7 @@
{
"AuthFlow": "REFRESH_TOKEN_AUTH",
"ClientId": "clientId123",
"AuthParameters": {
"REFRESH_TOKEN": "refreshToken123"
}
}

View File

@ -0,0 +1,8 @@
{
"AuthFlow": "USER_SRP_AUTH",
"ClientId": "clientId123",
"AuthParameters": {
"SRP_A": "srpA789",
"USERNAME": "username456"
}
}

View File

@ -0,0 +1,14 @@
{
"client_id": "715j3r0trk7o8dqg3md57il7q0",
"issuer": null,
"authorization_endpoint": null,
"logout_endpoint": null,
"revoke_token_endpoint": null,
"logout_redirect_url": "https://www.windcentrale.nl/",
"privacy_and_cookie_url": "http://docs.servicehouse.nl/windcentrale/algemeen_privacy_statement.pdf",
"label_key": "1024",
"ga_tracking_id": null,
"region": "eu-west-1",
"user_pool_id": "eu-west-1_U7eYBPrBd",
"cognito_active": true
}

View File

@ -0,0 +1,167 @@
{
"WND-RZ":{
"power_per_share":"118",
"id":"41",
"timestamp":"1680425425",
"wind_power":"5",
"power":"727",
"wind_direction":"NO",
"year_production":"1094409",
"rpm":"15.4",
"total_runtime":"22996",
"year_runtime":"1986.811111",
"diameter":"45",
"pulsating":"0",
"power_percentage":"37"
},
"WND-BR":{
"power_per_share":"150",
"id":"131",
"timestamp":"1680425425",
"wind_power":"7",
"power":"827",
"wind_direction":"O",
"year_production":"872488",
"rpm":"26.1",
"total_runtime":"29470",
"year_runtime":"-98268833.015556",
"diameter":"98",
"pulsating":"0",
"power_percentage":"98"
},
"WND-RH":{
"power_per_share":"75",
"id":"31",
"timestamp":"1680425425",
"wind_power":"5",
"power":"494",
"wind_direction":"NO",
"year_production":"0",
"rpm":"14.5",
"total_runtime":"0",
"year_runtime":"0",
"diameter":"37",
"pulsating":"0",
"power_percentage":"25"
},
"WND-WJ":{
"power_per_share":"134",
"id":"51",
"timestamp":"1680425425",
"wind_power":"5",
"power":"764",
"wind_direction":"NO",
"year_production":"1233164",
"rpm":"15.8",
"total_runtime":"111171",
"year_runtime":"2118.266667",
"diameter":"46",
"pulsating":"0",
"power_percentage":"39"
},
"WND-BZ":{
"power_per_share":"84",
"id":"201",
"timestamp":"1680425425",
"wind_power":"5",
"power":"251",
"wind_direction":"NO",
"year_production":"557790",
"rpm":"27.2",
"total_runtime":"55375",
"year_runtime":"1989.000000",
"diameter":"38",
"pulsating":"0",
"power_percentage":"28"
},
"WND-VW":{
"power_per_share":"148",
"id":"141",
"timestamp":"1680425425",
"wind_power":"7",
"power":"812",
"wind_direction":"O",
"year_production":"852820",
"rpm":"26.1",
"total_runtime":"118985",
"year_runtime":"-420428662.080000",
"diameter":"96",
"pulsating":"0",
"power_percentage":"96"
},
"WND-GG":{
"power_per_share":"31",
"id":"1",
"timestamp":"1680425425",
"wind_power":"4",
"power":"303",
"wind_direction":"NO",
"year_production":"1457297",
"rpm":"13.2",
"total_runtime":"122207",
"year_runtime":"2110.000000",
"diameter":"26",
"pulsating":"0",
"power_percentage":"14"
},
"WND-TW":{
"power_per_share":"141",
"id":"121",
"timestamp":"1680425425",
"wind_power":"6",
"power":"788",
"wind_direction":"NO",
"year_production":"0",
"rpm":"25.9",
"total_runtime":"0",
"year_runtime":"0",
"diameter":"93",
"pulsating":"0",
"power_percentage":"93"
},
"WND-BH":{
"power_per_share":"138",
"id":"111",
"timestamp":"1680425425",
"wind_power":"6",
"power":"768",
"wind_direction":"NO",
"year_production":"860296",
"rpm":"26.1",
"total_runtime":"117041",
"year_runtime":"-413559214.400000",
"diameter":"91",
"pulsating":"0",
"power_percentage":"91"
},
"WND-VH":{
"power_per_share":"105",
"id":"211",
"timestamp":"1680425425",
"wind_power":"5",
"power":"1020",
"wind_direction":"NO",
"year_production":"1322184",
"rpm":"16.7",
"total_runtime":"0",
"year_runtime":"0",
"diameter":"100",
"pulsating":"1",
"power_percentage":"113"
},
"WND-JH":{
"power_per_share":"52",
"id":"2",
"timestamp":"1680425425",
"wind_power":"5",
"power":"522",
"wind_direction":"O",
"year_production":"1508090",
"rpm":"14.9",
"total_runtime":"122330",
"year_runtime":"2089.000000",
"diameter":"35",
"pulsating":"0",
"power_percentage":"23"
}
}

View File

@ -0,0 +1,17 @@
{
"WND-JH":{
"power_per_share":"52",
"id":"2",
"timestamp":"1680425425",
"wind_power":"5",
"power":"522",
"wind_direction":"O",
"year_production":"1508090",
"rpm":"14.9",
"total_runtime":"122330",
"year_runtime":"2089.000000",
"diameter":"35",
"pulsating":"0",
"power_percentage":"23"
}
}

View File

@ -0,0 +1,77 @@
[
{
"project_name": "De Grote Geert",
"project_code": "WND-GG",
"project_type": "WIND",
"project_ean_number": "123456789012345678",
"project_start_date": "2013-01-01",
"project_end_date": "2030-09-01",
"energy_supplier": "GREENCHOICE",
"current_power": 100,
"power_percentage": 0,
"todays_production": 2000,
"total_production": 8000000,
"participations": [
{
"supplier": "Greenchoice",
"mutation_date": "2013-01-01",
"share": 20,
"offered_share": 0,
"participation_identifier": "WND-GG-1",
"greenchoice_id": "12345",
"address": {
"street_name": "Kerkstraat",
"house_number": "54321",
"house_postfix": null,
"postal_code": "9999XY",
"city": "Amsterdam"
},
"production": null,
"shares": [
{
"from_date": "2013-01-01",
"share": 20
}
]
},
{
"supplier": "Greenchoice",
"mutation_date": "2022-10-09",
"share": 50,
"offered_share": 0,
"participation_identifier": "WND-GG-2",
"greenchoice_id": null,
"address": {
"street_name": "Kerkstraat",
"house_number": "54321",
"house_postfix": null,
"postal_code": "9999XY",
"city": "Amsterdam"
},
"production": null,
"shares": [
{
"from_date": "2022-10-09",
"share": 50
}
]
}
],
"member_offer_settings": [
{
"from_date": "2021-10-21",
"allow_selling": true,
"min_price": 0,
"max_price": 200
}
],
"administration_costs": [
{
"from_date": "2019-09-01",
"amount": 15
}
],
"statutes_location": "https://cdn.servicehouse.nl/cooperation/20120614%20De%20Grote%20Geert%20UA%20Akte%20van%20Oprichting.pdf",
"member_agreement_location": "https://cdn.servicehouse.nl/cooperation/leden_en_winddelenovereenkomst_Grote_Geert_v1.pdf"
}
]

View File

@ -0,0 +1,10 @@
{
"ChallengeName": "PASSWORD_VERIFIER",
"ClientId": "clientId123",
"ChallengeResponses": {
"USERNAME": "username@acme.com",
"PASSWORD_CLAIM_SECRET_BLOCK": "passwordClaimSecretBlock456",
"PASSWORD_CLAIM_SIGNATURE": "passwordClaimSignature789",
"TIMESTAMP": "Thu Apr 6 07:16:19 UTC 2023"
}
}