Skip to content

Commit

Permalink
[aWATTar] Refactor AwattarApi initialization to include configuration…
Browse files Browse the repository at this point in the history
… and adapt tests

Signed-off-by: Thomas Leber <thomas@tl-photography.at>
  • Loading branch information
tl-photography committed Jul 31, 2024
1 parent b5644fe commit b5afeac
Show file tree
Hide file tree
Showing 7 changed files with 632 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
*
* @author Wolfgang Klimt - initial contribution
* @author Jan N. Klug - Refactored to record
*
* @param netPrice the net price in €/kWh
* @param grossPrice the gross price in €/kWh
* @param netTotal the net total price in €
* @param grossTotal the gross total price in €
* @param timerange the time range of the price
*/
@NonNullByDefault
public record AwattarPrice(double netPrice, double grossPrice, double netTotal, double grossTotal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.api;
package org.openhab.binding.awattar.internal.api;

import static org.eclipse.jetty.http.HttpMethod.GET;
import static org.eclipse.jetty.http.HttpStatus.OK_200;
Expand Down Expand Up @@ -78,19 +78,12 @@ public AwattarApiException(String message) {
* @param httpClient the HTTP client to use
* @param zone the time zone to use
*/
public AwattarApi(HttpClient httpClient, ZoneId zone) {
public AwattarApi(HttpClient httpClient, ZoneId zone, AwattarBridgeConfiguration config) {
this.zone = zone;
this.httpClient = httpClient;

this.gson = new Gson();
}

/**
* Initialize the aWATTar API.
*
* @param config the configuration to use
*/
public void initialize(AwattarBridgeConfiguration config) {
vatFactor = 1 + (config.vatPercent / 100);
basePrice = config.basePrice;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.api.AwattarApi;
import org.openhab.binding.api.AwattarApi.AwattarApiException;
import org.openhab.binding.awattar.internal.AwattarBridgeConfiguration;
import org.openhab.binding.awattar.internal.AwattarPrice;
import org.openhab.binding.awattar.internal.api.AwattarApi;
import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
Expand Down Expand Up @@ -62,13 +62,14 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
private @Nullable SortedSet<AwattarPrice> prices;
private ZoneId zone;

private AwattarApi awattarApi;
private @NonNullByDefault AwattarApi awattarApi;
private HttpClient httpClient;

public AwattarBridgeHandler(Bridge thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) {

Check failure on line 68 in bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandler.java

View workflow job for this annotation

GitHub Actions / Build (Java 17, ubuntu-22.04)

The @nonnull field awattarApi may not have been initialized
super(thing);
zone = timeZoneProvider.getTimeZone();

awattarApi = new AwattarApi(httpClient, zone);
this.httpClient = httpClient;
}

@Override
Expand All @@ -77,7 +78,7 @@ public void initialize() {
AwattarBridgeConfiguration config = getConfigAs(AwattarBridgeConfiguration.class);

try {
awattarApi.initialize(config);
awattarApi = new AwattarApi(httpClient, zone, config);
} catch (IllegalArgumentException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.unsupported.country");
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Copyright (c) 2010-2024 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.awattar.internal.api;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.util.Objects;
import java.util.SortedSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.openhab.binding.awattar.internal.AwattarBridgeConfiguration;
import org.openhab.binding.awattar.internal.AwattarPrice;
import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException;
import org.openhab.binding.awattar.internal.handler.AwattarBridgeHandler;
import org.openhab.binding.awattar.internal.handler.AwattarBridgeHandlerTest;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.test.java.JavaTest;

/**
* The {@link AwattarBridgeHandlerTest} contains tests for the {@link AwattarBridgeHandler}
*
* @author Jan N. Klug - Initial contribution
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
class AwattarApiTest extends JavaTest {
// API Mocks
private @Mock @NonNullByDefault({}) HttpClient httpClientMock;
private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock;
private @Mock @NonNullByDefault({}) Request requestMock;
private @Mock @NonNullByDefault({}) ContentResponse contentResponseMock;
private @Mock @NonNullByDefault({}) AwattarBridgeConfiguration config;

// sut
private @NonNullByDefault({}) AwattarApi api;

@BeforeEach
public void setUp() throws IOException, ExecutionException, InterruptedException, TimeoutException {
try (InputStream inputStream = AwattarApiTest.class.getResourceAsStream("api_response.json")) {
if (inputStream == null) {
throw new IOException("inputstream is null");
}
byte[] bytes = inputStream.readAllBytes();
if (bytes == null) {
throw new IOException("Resulting byte-array empty");
}
when(contentResponseMock.getContentAsString()).thenReturn(new String(bytes, StandardCharsets.UTF_8));
}
when(contentResponseMock.getStatus()).thenReturn(HttpStatus.OK_200);
when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
when(requestMock.method(HttpMethod.GET)).thenReturn(requestMock);
when(requestMock.timeout(10, TimeUnit.SECONDS)).thenReturn(requestMock);
when(requestMock.send()).thenReturn(contentResponseMock);

when(timeZoneProviderMock.getTimeZone()).thenReturn(ZoneId.of("GMT+2"));

config.basePrice = 0.0;
config.vatPercent = 0.0;
config.country = "DE";

api = new AwattarApi(httpClientMock, ZoneId.of("GMT+2"), config);
}

@Test
void testPricesRetrieval() throws AwattarApiException {
SortedSet<AwattarPrice> prices = api.getData();

assertThat(prices, hasSize(72));

Objects.requireNonNull(prices);

// check if first and last element are correct
assertThat(prices.first().timerange().start(), is(1718316000000L));
assertThat(prices.last().timerange().end(), is(1718575200000L));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,41 +12,33 @@
*/
package org.openhab.binding.awattar.internal.handler;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.lang.reflect.Field;
import java.time.ZoneId;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.List;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.platform.commons.support.HierarchyTraversalMode;
import org.junit.platform.commons.support.ReflectionSupport;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.openhab.binding.awattar.internal.AwattarBindingConstants;
import org.openhab.binding.awattar.internal.api.AwattarApi;
import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.test.java.JavaTest;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerCallback;

/**
Expand All @@ -58,15 +50,13 @@
@MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
public class AwattarBridgeHandlerRefreshTest extends JavaTest {
public static final ThingUID BRIDGE_UID = new ThingUID(AwattarBindingConstants.THING_TYPE_BRIDGE, "testBridge");

// bridge mocks
private @Mock @NonNullByDefault({}) Bridge bridgeMock;
private @Mock @NonNullByDefault({}) ThingHandlerCallback bridgeCallbackMock;
private @Mock @NonNullByDefault({}) HttpClient httpClientMock;
private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock;
private @Mock @NonNullByDefault({}) Request requestMock;
private @Mock @NonNullByDefault({}) ContentResponse contentResponseMock;
private @Mock @NonNullByDefault({}) AwattarApi awattarApiMock;

// best price handler mocks
private @Mock @NonNullByDefault({}) Thing bestpriceMock;
Expand All @@ -75,64 +65,53 @@ public class AwattarBridgeHandlerRefreshTest extends JavaTest {
private @NonNullByDefault({}) AwattarBridgeHandler bridgeHandler;

@BeforeEach
public void setUp() throws IOException, ExecutionException, InterruptedException, TimeoutException {
try (InputStream inputStream = AwattarBridgeHandlerRefreshTest.class.getResourceAsStream("api_response.json")) {
if (inputStream == null) {
throw new IOException("inputstream is null");
}
byte[] bytes = inputStream.readAllBytes();
if (bytes == null) {
throw new IOException("Resulting byte-array empty");
}
when(contentResponseMock.getContentAsString()).thenReturn(new String(bytes, StandardCharsets.UTF_8));
}
when(contentResponseMock.getStatus()).thenReturn(HttpStatus.OK_200);
when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
when(requestMock.method(HttpMethod.GET)).thenReturn(requestMock);
when(requestMock.timeout(10, TimeUnit.SECONDS)).thenReturn(requestMock);
when(requestMock.send()).thenReturn(contentResponseMock);
public void setUp() throws IllegalArgumentException, IllegalAccessException {

when(timeZoneProviderMock.getTimeZone()).thenReturn(ZoneId.of("GMT+2"));

bridgeHandler = new AwattarBridgeHandler(bridgeMock, httpClientMock, timeZoneProviderMock);
bridgeHandler.setCallback(bridgeCallbackMock);

when(bridgeMock.getHandler()).thenReturn(bridgeHandler);

// other mocks
when(bestpriceMock.getBridgeUID()).thenReturn(BRIDGE_UID);
List<Field> fields = ReflectionSupport.findFields(AwattarBridgeHandler.class,
field -> field.getName().equals("awattarApi"), HierarchyTraversalMode.BOTTOM_UP);

when(bestPriceCallbackMock.getBridge(any())).thenReturn(bridgeMock);
when(bestPriceCallbackMock.isChannelLinked(any())).thenReturn(true);
for (Field field : fields) {
field.setAccessible(true);
field.set(bridgeHandler, awattarApiMock);
}
}

/**
* Test the refreshIfNeeded method with a bridge that is offline.
*
* @throws SecurityException
* @throws AwattarApiException
*/
@Test
void testRefreshIfNeeded_ThingOffline() throws SecurityException {
void testRefreshIfNeeded_ThingOffline() throws SecurityException, AwattarApiException {
when(bridgeMock.getStatus()).thenReturn(ThingStatus.OFFLINE);

bridgeHandler.refreshIfNeeded();

verify(bridgeCallbackMock).statusUpdated(bridgeMock,
new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null));
verify(awattarApiMock).getData();
}

/**
* Test the refreshIfNeeded method with a bridge that is online and the data is empty.
*
* @throws SecurityException
* @throws AwattarApiException
*/
@Test
void testRefreshIfNeeded_DataEmpty() throws SecurityException {
void testRefreshIfNeeded_DataEmpty() throws SecurityException, AwattarApiException {
when(bridgeMock.getStatus()).thenReturn(ThingStatus.ONLINE);

bridgeHandler.refreshIfNeeded();

verify(bridgeCallbackMock).statusUpdated(bridgeMock,
new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null));
verify(awattarApiMock).getData();
}
}
Loading

0 comments on commit b5afeac

Please sign in to comment.