diff --git a/bundles/org.openhab.binding.myenergi/pom.xml b/bundles/org.openhab.binding.myenergi/pom.xml index 96e950a7c393d..04964788621cc 100644 --- a/bundles/org.openhab.binding.myenergi/pom.xml +++ b/bundles/org.openhab.binding.myenergi/pom.xml @@ -7,7 +7,7 @@ org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 3.1.0-SNAPSHOT + 3.2.0-SNAPSHOT org.openhab.binding.myenergi diff --git a/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/MyEnergiApiClient.java b/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/MyEnergiApiClient.java index 32c0022978dd1..009aa8419ea0b 100644 --- a/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/MyEnergiApiClient.java +++ b/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/MyEnergiApiClient.java @@ -93,12 +93,12 @@ public void setHttpClientFactory(@Nullable HttpClientFactory httpClientFactory) /** * Sets the credentials (username/password) to be used for API calls. * - * @param username the username to be used. - * @param password the password to be used. + * @param hubSerialNumber the serial number of the myenergi hub + * @param password the password for this hub in the myenergi mobile app. * @throws MyEnergiApiException */ - public void initialize(final String username, final String password) throws ApiException { - this.username = username; + public void initialize(final String hubSerialNumber, final String password) throws ApiException { + this.username = hubSerialNumber; this.password = password; HttpClientFactory factory = httpClientFactory; if (factory == null) { @@ -113,17 +113,17 @@ public void initialize(final String username, final String password) throws ApiE AuthenticationStore auth = client.getAuthenticationStore(); auth.clearAuthentications(); auth.clearAuthenticationResults(); - if (host.equals("")) { - host = "s" + username.charAt(username.length() - 1) + ".myenergi.net"; + if ("".equals(host)) { + host = new MyEnergiGetHostFromDirector().getHostName(client, hubSerialNumber); } try { URL baseURL = new URL("https", host, "/"); logger.debug("API base URL: {}", baseURL.toString()); client.getAuthenticationStore().addAuthentication( - new DigestAuthentication(baseURL.toURI(), Authentication.ANY_REALM, username, password)); + new DigestAuthentication(baseURL.toURI(), Authentication.ANY_REALM, hubSerialNumber, password)); this.baseURL = baseURL; - logger.debug("Digest authentication added: {}", username); + logger.debug("Digest authentication added: {}", hubSerialNumber); if (!client.isStarted()) { client.start(); } diff --git a/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/MyEnergiBridgeConfiguration.java b/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/MyEnergiBridgeConfiguration.java index 044576d79795a..5bdbadc8372a9 100644 --- a/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/MyEnergiBridgeConfiguration.java +++ b/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/MyEnergiBridgeConfiguration.java @@ -22,7 +22,7 @@ @NonNullByDefault public class MyEnergiBridgeConfiguration { - public String username = ""; + public String hubSerialNumber = ""; public String password = ""; public int refreshInterval = 24; // 24 hours diff --git a/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/MyEnergiGetHostFromDirector.java b/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/MyEnergiGetHostFromDirector.java new file mode 100644 index 0000000000000..3383c3af1b2ec --- /dev/null +++ b/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/MyEnergiGetHostFromDirector.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2010-2021 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.myenergi.internal; + +import java.net.URL; + +import javax.validation.constraints.NotNull; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.Authentication; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.DigestAuthentication; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.myenergi.internal.exception.ApiException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link MyEnergiGetHostFromDirector} is a helper class to get the hostname on + * myenergi.net for the the myenergi API. + * It finds the server for a given hub serial number + * + * @author Volkmar Nissen - Initial contribution + */ +@NonNullByDefault +public class MyEnergiGetHostFromDirector { + private static final int SLEEP_BEFORE_REINIT_MS = 3000; + public static final String MY_ENERGI_RESPONSE_FIELD = "X_MYENERGI-asn"; + + private final Logger logger = LoggerFactory.getLogger(MyEnergiGetHostFromDirector.class); + + /** + * Finds the server for a given hub serial number + * + * @param httpClientFactory the client to be used. + * @throws ApiException + */ + + public String getHostName(@NotNull HttpClient httpClient, @NotNull String hubSerialNumber) throws ApiException { + String directorHostname = "director.myenergi.net"; + try { + URL directorURL = new URL("https", directorHostname, "/"); + // No password is needed at director.myenergie.net + httpClient.getAuthenticationStore().addAuthentication( + new DigestAuthentication(directorURL.toURI(), Authentication.ANY_REALM, hubSerialNumber, "")); + int innerLoop = 0; + while ((innerLoop < 2)) { + innerLoop++; + if (!httpClient.isStarted()) { + httpClient.start(); + } + Request request = httpClient.newRequest(directorURL.toString()).method(HttpMethod.GET); + logger.trace("sending get hostname request: {}", innerLoop); + ContentResponse response = request.send(); + String hostname = response.getHeaders().get(MY_ENERGI_RESPONSE_FIELD); + if (null != hostname) { + return hostname; + } + + if (logger.isTraceEnabled()) { + for (HttpField field : response.getHeaders()) { + logger.trace("HTTP header: {}", field.toString()); + } + } + } + } catch (Exception e) { + throw new ApiException("Exception caught during API execution", e); + } + // This code will never be executed, because the ApiException will be thrown earlier + return ""; + } +} diff --git a/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/handler/MyEnergiBridgeHandler.java b/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/handler/MyEnergiBridgeHandler.java index bf08b71a6d283..258192da9aa7a 100644 --- a/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/handler/MyEnergiBridgeHandler.java +++ b/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/handler/MyEnergiBridgeHandler.java @@ -68,9 +68,9 @@ public void initialize() { logger.debug("Initializing MyEnergiBridgeHandler"); MyEnergiBridgeConfiguration config = getConfigAs(MyEnergiBridgeConfiguration.class); - if (config.username.isEmpty() || config.password.isEmpty()) { + if (config.hubSerialNumber.isEmpty() || config.password.isEmpty()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "@text/offline.conf-error-missing-username-or-password"); + "@text/offline.conf-error-missing-hubSerialNumber-or-password"); return; } if (config.refreshInterval < 1) { @@ -81,8 +81,8 @@ public void initialize() { updateStatus(ThingStatus.UNKNOWN); try { - logger.debug("Login to MyEnergi API with username: {}", config.username); - apiClient.initialize(config.username, config.password); + logger.debug("Login to MyEnergi API with hubSerialNumber: {}", config.hubSerialNumber); + apiClient.initialize(config.hubSerialNumber, config.password); apiClient.updateTopologyCache(); logger.debug("Cache update successful, setting bridge status to ONLINE"); updateStatus(ThingStatus.ONLINE); diff --git a/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/util/ZappiBoostMode.java b/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/util/ZappiBoostMode.java index d7c7047a61a32..e75e490c20b36 100644 --- a/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/util/ZappiBoostMode.java +++ b/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/util/ZappiBoostMode.java @@ -12,12 +12,15 @@ */ package org.openhab.binding.myenergi.internal.util; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * The {@link ZappiBoostMode} enumeration is used to model the various Zappi boost charging modes. * * @author Rene Scherer - Initial contribution * */ +@NonNullByDefault public enum ZappiBoostMode { // stops the current boost cycle STOP(2), diff --git a/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/util/ZappiChargingMode.java b/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/util/ZappiChargingMode.java index 026fb02bbcf7a..180e1ff263562 100644 --- a/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/util/ZappiChargingMode.java +++ b/bundles/org.openhab.binding.myenergi/src/main/java/org/openhab/binding/myenergi/internal/util/ZappiChargingMode.java @@ -12,12 +12,15 @@ */ package org.openhab.binding.myenergi.internal.util; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * The {@link ZappiChargingMode} enumeration is used to model the various Zappi charging modes. * * @author Rene Scherer - Initial contribution * */ +@NonNullByDefault public enum ZappiChargingMode { BOOST(0), FAST(1), diff --git a/bundles/org.openhab.binding.myenergi/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.myenergi/src/main/resources/OH-INF/binding/binding.xml index 2f05181988ed3..1c27d8096c395 100644 --- a/bundles/org.openhab.binding.myenergi/src/main/resources/OH-INF/binding/binding.xml +++ b/bundles/org.openhab.binding.myenergi/src/main/resources/OH-INF/binding/binding.xml @@ -4,6 +4,10 @@ xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd"> myenergi Binding - This is the binding for myenergi. + The myenergi binding connects to the myenergi hub and interacts with the following myenergi products: + zappi wallbox, + harvi remote device for CT sensors, + eddi control device for huge power consumers like heat pumps + . diff --git a/bundles/org.openhab.binding.myenergi/src/main/resources/OH-INF/i18n/myenergi.properties b/bundles/org.openhab.binding.myenergi/src/main/resources/OH-INF/i18n/myenergi.properties index bf429264bce89..3b3388b0b3336 100644 --- a/bundles/org.openhab.binding.myenergi/src/main/resources/OH-INF/i18n/myenergi.properties +++ b/bundles/org.openhab.binding.myenergi/src/main/resources/OH-INF/i18n/myenergi.properties @@ -1,5 +1,5 @@ # thing status description offline.conf-error-invalid-refresh-intervals = Invalid refresh intervals -offline.conf-error-missing-username-or-password = Missing username or password +offline.conf-error-missing-hubSerialNumber-or-password = Missing hubSerialNumber or password offline.conf-error-authentication = Authentication Error (Invalid API key or account number) offline.comm-error-general = General communication error with API diff --git a/bundles/org.openhab.binding.myenergi/src/main/resources/OH-INF/thing/things.xml b/bundles/org.openhab.binding.myenergi/src/main/resources/OH-INF/thing/things.xml index 172188e8ac3d9..54e709c04ed96 100644 --- a/bundles/org.openhab.binding.myenergi/src/main/resources/OH-INF/thing/things.xml +++ b/bundles/org.openhab.binding.myenergi/src/main/resources/OH-INF/thing/things.xml @@ -11,9 +11,9 @@ The MyEnergi Cloud API Bridge - - - The username to access the MyEnergi API (Hub Serial Number) + + + The Hub Serial Number to access the MyEnergi API () @@ -39,12 +39,12 @@ - - - - - - + + + + + + @@ -86,16 +86,10 @@ - - - - - - - - - - + + + + @@ -123,17 +117,29 @@ - + String - - The name of the thing - + + CT Sensors have the following usages + + + + + + + + + + + + Number:Power - The measured grid power + Grid power property shows the import and export power. It is used to determine if surplus power is + available. Energy @@ -141,7 +147,7 @@ Number:Power - The generated power + Power from the generator (if available) Energy @@ -157,23 +163,37 @@ Number:Power - The total consumed power (grid + generated) + Power consumed by the house (if available) Energy - + Number:Power - The measured power at the clamp + The measured power at the CT L1 Energy + + Number:Power + + The measured power at the CT L2 + Energy + + + + Number:Power + + The measured power at the CT L3 + Energy + + Number - The phase this clamp is attached to + The phase this CT is attached to Energy @@ -188,7 +208,7 @@ Number:ElectricPotential - The measured voltage + Supply voltage to the unit Energy @@ -196,7 +216,7 @@ Number:Frequency - The measured frequency + Grid frequency Energy @@ -204,14 +224,14 @@ Number:Energy - The charge energy + Energy supplied to the EV during current charge session Number - The current locking mode of the device as bitmap + The current locking mode of the device as bitmap.Bit 0: Locked Now, Bit 1: Lock when plugged in, Bit 2: Lock when unplugged, Bit 3: Charge when locked, Bit 4: Charge Session Allowed (Even if locked) diff --git a/bundles/org.openhab.binding.myenergi/src/test/java/org/openhab/binding/myenergi/internal/MyEnergiApiClientTest.java b/bundles/org.openhab.binding.myenergi/src/test/java/org/openhab/binding/myenergi/internal/MyEnergiApiClientTest.java index c6acc461299f2..089db16d59bd2 100644 --- a/bundles/org.openhab.binding.myenergi/src/test/java/org/openhab/binding/myenergi/internal/MyEnergiApiClientTest.java +++ b/bundles/org.openhab.binding.myenergi/src/test/java/org/openhab/binding/myenergi/internal/MyEnergiApiClientTest.java @@ -25,6 +25,7 @@ import org.eclipse.jetty.client.HttpContentResponse; import org.eclipse.jetty.client.HttpRequest; import org.eclipse.jetty.client.api.AuthenticationStore; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.jupiter.api.BeforeAll; @@ -76,6 +77,7 @@ public HttpClient getCommonHttpClient() { private static final int ZAPPI_SERIAL_NUMBER = 21287642; private static MyEnergiApiClient api = new MyEnergiApiClient(); + private static HttpFields responseFields = mock(HttpFields.class); private static AuthenticationStore authenticationStore = mock(AuthenticationStore.class); private static HttpClientFactory httpClientFactory = mock(HttpClientFactoryMock.class); @@ -90,12 +92,15 @@ static void setUp() throws Exception { when(httpClient.getAuthenticationStore()).thenReturn(authenticationStore); when(httpClient.newRequest(anyString())).thenReturn(request); when(httpClient.isStarted()).thenReturn(true); + // doNothing().when(httpClient).stop(); + when(request.method(HttpMethod.GET)).thenReturn(request); when(request.send()).thenReturn(response); when(response.getStatus()).thenReturn(RESPONSE_200_STATUS); when(response.getReason()).thenReturn(RESPONSE_200_REASON); - + when(responseFields.get(MyEnergiGetHostFromDirector.MY_ENERGI_RESPONSE_FIELD)).thenReturn("SomeHost"); + when(response.getHeaders()).thenReturn(responseFields); api.setHttpClientFactory(httpClientFactory); api.initialize(TEST_USERNAME, TEST_PASSWORD_VALID); } diff --git a/bundles/org.openhab.binding.myenergi/src/test/java/org/openhab/binding/myenergi/internal/MyEnergiGetHostFromDirectorTest.java b/bundles/org.openhab.binding.myenergi/src/test/java/org/openhab/binding/myenergi/internal/MyEnergiGetHostFromDirectorTest.java new file mode 100644 index 0000000000000..598aa151cd29c --- /dev/null +++ b/bundles/org.openhab.binding.myenergi/src/test/java/org/openhab/binding/myenergi/internal/MyEnergiGetHostFromDirectorTest.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010-2021 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.myenergi.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.openhab.core.io.net.http.HttpClientFactory; + +/** + * The {@link MyEnergiGetHostFromDirectorTest} is a test class for {@link MyEnergiGetHostFromDirector}. + * + * @author Volkmar Nissen - Initial contribution + */ +@NonNullByDefault +class MyEnergiGetHostFromDirectorTest { + + class HttpClientFactoryForTest implements HttpClientFactory { + + @Override + public HttpClient createHttpClient(String consumerName) { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + sslContextFactory.setTrustAll(true); // you might want to think about this first + return new HttpClient(sslContextFactory); + } + + @Override + public HttpClient getCommonHttpClient() { + // TODO Auto-generated method stub + return new HttpClient(); + } + } + + /** + * There is no password check at director.myenergi.net. So, the password can be left empty. + * Only the hub serial number must be an existing one. May be this changes in the future. + * This test uses Internet URL to director.myenergi.net. The access is not mocked + * + * @throws Exception + */ + @Test + void testGetHostName() throws Exception { + HttpClient client = new HttpClientFactoryForTest() + .createHttpClient(MyEnergiGetHostFromDirectorTest.class.getSimpleName()); + try { + + client.start(); + String hostName = new MyEnergiGetHostFromDirector().getHostName(client, "12215753"); + Assertions.assertTrue(hostName.contains("myenergi")); + } catch (Exception e) { + Assertions.fail("Exception caught" + e.getMessage()); + } finally { + client.stop(); + } + } +}