mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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:
parent
22afd6e04f
commit
84cb3c9727
@ -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
|
||||
|
@ -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" }
|
||||
```
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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() //
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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 = "";
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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 = "";
|
||||
}
|
@ -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();
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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 = "";
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 + "]";
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
@ -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>
|
@ -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."));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
{
|
||||
"AuthenticationResult": {
|
||||
"AccessToken": "accessToken123",
|
||||
"ExpiresIn": 3600,
|
||||
"IdToken": "idToken456",
|
||||
"RefreshToken": "refreshToken789",
|
||||
"TokenType": "Bearer"
|
||||
},
|
||||
"ChallengeParameters": {
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"AuthenticationResult": {
|
||||
"AccessToken": "accessToken123",
|
||||
"ExpiresIn": 3600,
|
||||
"IdToken": "idToken456",
|
||||
"TokenType": "Bearer"
|
||||
},
|
||||
"ChallengeParameters": {
|
||||
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"__type": "InvalidParameterException",
|
||||
"message": "Missing required parameter REFRESH_TOKEN"
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"__type": "NotAuthorizedException",
|
||||
"message": "Incorrect username or password."
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"AuthFlow": "REFRESH_TOKEN_AUTH",
|
||||
"ClientId": "clientId123",
|
||||
"AuthParameters": {
|
||||
"REFRESH_TOKEN": "refreshToken123"
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"AuthFlow": "USER_SRP_AUTH",
|
||||
"ClientId": "clientId123",
|
||||
"AuthParameters": {
|
||||
"SRP_A": "srpA789",
|
||||
"USERNAME": "username456"
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -0,0 +1 @@
|
||||
{}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
@ -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"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user