mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 07:02:02 +01:00
[iotawatt] Initial contribution (#16491)
* [iotawatt] generate new binding Signed-off-by: Peter Rosenberg <prosenb.dev@gmail.com>
This commit is contained in:
parent
40ed4f7781
commit
a4ad7b27b7
@ -162,6 +162,7 @@
|
||||
/bundles/org.openhab.binding.ihc/ @paulianttila
|
||||
/bundles/org.openhab.binding.insteon/ @robnielsen
|
||||
/bundles/org.openhab.binding.intesis/ @hmerk
|
||||
/bundles/org.openhab.binding.iotawatt/ @PRosenb
|
||||
/bundles/org.openhab.binding.ipcamera/ @Skinah
|
||||
/bundles/org.openhab.binding.ipobserver/ @Skinah
|
||||
/bundles/org.openhab.binding.ipp/ @peuter
|
||||
|
@ -806,6 +806,11 @@
|
||||
<artifactId>org.openhab.binding.intesis</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.iotawatt</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.ipcamera</artifactId>
|
||||
|
13
bundles/org.openhab.binding.iotawatt/NOTICE
Normal file
13
bundles/org.openhab.binding.iotawatt/NOTICE
Normal file
@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
86
bundles/org.openhab.binding.iotawatt/README.md
Normal file
86
bundles/org.openhab.binding.iotawatt/README.md
Normal file
@ -0,0 +1,86 @@
|
||||
# IoTaWatt Binding
|
||||
|
||||
This binding integrates [IoTaWatt™ Open WiFi Electric Power Monitor](https://iotawatt.com/) into openHAB.
|
||||
|
||||
Limitations of this version:
|
||||
|
||||
- No authentication support
|
||||
|
||||
## Supported Things
|
||||
|
||||
The IoTaWatt binding supports one Thing called `iotawatt`.
|
||||
|
||||
## Discovery
|
||||
|
||||
The binding does not auto-discover the IoTaWatt device.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
### IoTaWatt Thing Configuration
|
||||
|
||||
| Name | Type | Description | Default | Required | Advanced |
|
||||
|-----------------|---------|------------------------------------------------|---------|----------|----------|
|
||||
| hostname | text | Hostname or IP address of the device | N/A | yes | no |
|
||||
| refreshInterval | integer | Interval the device is polled in sec. | 10 | no | no |
|
||||
| requestTimeout | long | The request timeout to call the device in sec. | 10 | no | no |
|
||||
|
||||
## Channels
|
||||
|
||||
The binding detects configured inputs and outputs and creates channels for them.
|
||||
|
||||
| Channel | Type | ID | Read/Write | Description |
|
||||
|---------------------|--------------------------|---------------------|------------|---------------------------------|
|
||||
| Amps | Number:Power | amps | RO | The current amps |
|
||||
| Frequency | Number:Frequency | frequency | RO | The current AC frequency |
|
||||
| Power Factor | Number:Dimensionless | power-factor | RO | The current power factor |
|
||||
| Apparent Power | Number:Power | apparent-power | RO | The current apparent power |
|
||||
| Reactive Power | Number:Power | reactive-power | RO | The current reactive power |
|
||||
| Reactive Power hour | Number:Power | reactive-power-hour | RO | The current reactive power hour |
|
||||
| Voltage | Number:ElectricPotential | voltage | RO | The current voltage |
|
||||
| Power Consumption | Number:Power | watts | RO | The current power consumption |
|
||||
| Phase | Number:Dimensionless | phase | RO | The current phase |
|
||||
|
||||
## Example Configuration
|
||||
|
||||
### Thing with Channels
|
||||
|
||||
```java
|
||||
Thing iotawatt:iotawatt:iotawatt1 "IoTaWatt 1" [ hostname="192.168.1.10" ] {
|
||||
Channels:
|
||||
Type voltage : input_00#voltage "Voltage"
|
||||
Type frequency : input_00#frequency "AC Frequency"
|
||||
Type phase : input_00#phase "Phase"
|
||||
Type watts : input_01#watts "Power Consumption"
|
||||
Type power-factor : input_01#power-factor "Power Factor"
|
||||
Type phase : input_01#phase "Phase"
|
||||
|
||||
Type amps : output_00#Input_1_amps "Amps"
|
||||
Type frequency : output_01#Input_1_hz "Frequency"
|
||||
Type power-factor : output_02#Input_1_pf "Power Factor"
|
||||
Type apparent-power : output_03#Input_1_va "Apparent Power"
|
||||
Type reactive-power : output_04#Input_1_var "Reactive Power"
|
||||
Type reactive-power-hour : output_05#Input_1_varh "Reactive Power Hour"
|
||||
Type voltage : output_06#Input_1_volts "Voltage"
|
||||
Type watts : output_07#Input_1_watts "Watts"
|
||||
}
|
||||
```
|
||||
|
||||
### Items
|
||||
|
||||
```java
|
||||
Number:ElectricPotential input_voltage "Voltage" { channel="iotawatt:iotawatt:iotawatt1:input_00#voltage" }
|
||||
Number:Frequency input_frequency "AC Frequency" { channel="iotawatt:iotawatt:iotawatt1:input_00#frequency" }
|
||||
Number:Dimensionless input_phase0 "Phase" { channel="iotawatt:iotawatt:iotawatt1:input_00#phase" }
|
||||
Number:Power input_watts "Watts" { channel="iotawatt:iotawatt:iotawatt1:input_01#watts" }
|
||||
Number:Dimensionless input_power_factor "Power Factor" { channel="iotawatt:iotawatt:iotawatt1:input_01#power-factor" }
|
||||
Number:Dimensionless input_phase1 "Phase" { channel="iotawatt:iotawatt:iotawatt1:input_01#phase" }
|
||||
|
||||
Number:ElectricCurrent output_amps "Amps" { channel="iotawatt:iotawatt:iotawatt1:output_00#Input_1_amps" }
|
||||
Number:Frequency output_frequency "AC Frequency" { channel="iotawatt:iotawatt:iotawatt1:output_01#Input_1_hz" }
|
||||
Number:Dimensionless output_power_factor "Power Factor" { channel="iotawatt:iotawatt:iotawatt1:output_02#Input_1_pf" }
|
||||
Number:Power output_apparent_power "Apparent Power" { channel="iotawatt:iotawatt:iotawatt1:output_03#Input_1_va" }
|
||||
Number:Power output_reactive_power "Reactive Power" { channel="iotawatt:iotawatt:iotawatt1:output_04#Input_1_var" }
|
||||
Number:Energy output_reactive_power_hour "Reactive Power Hour" { channel="iotawatt:iotawatt:iotawatt1:output_05#Input_1_varh" }
|
||||
Number:ElectricPotential output_voltage "Voltage" { channel="iotawatt:iotawatt:iotawatt1:output_06#Input_1_volts" }
|
||||
Number:Power output_watts "Watts" { channel="iotawatt:iotawatt:iotawatt1:output_07#Input_1_watts" }
|
||||
```
|
25
bundles/org.openhab.binding.iotawatt/pom.xml
Normal file
25
bundles/org.openhab.binding.iotawatt/pom.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.2.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.iotawatt</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: IoTaWatt Binding</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>5.11.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.iotawatt-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-iotawatt" description="IoTaWatt Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.iotawatt/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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.iotawatt.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link IoTaWattBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class IoTaWattBindingConstants {
|
||||
/**
|
||||
* The binding ID of the IoTaWatt binding
|
||||
*/
|
||||
public static final String BINDING_ID = "iotawatt";
|
||||
|
||||
/**
|
||||
* The list of all Thing Type UIDs
|
||||
*/
|
||||
public static final ThingTypeUID THING_TYPE_IOTAWATT = new ThingTypeUID(BINDING_ID, "iotawatt");
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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.iotawatt.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link IoTaWattConfiguration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class IoTaWattConfiguration {
|
||||
private final Logger logger = LoggerFactory.getLogger(IoTaWattConfiguration.class);
|
||||
/**
|
||||
* The default refresh interval of the IoTaWatt device
|
||||
*/
|
||||
public static final int REFRESH_INTERVAL_DEFAULT = 10;
|
||||
/**
|
||||
* The default of the request timeout
|
||||
*/
|
||||
public static final long REQUEST_TIMEOUT_DEFAULT = 10;
|
||||
|
||||
/**
|
||||
* Configuration parameters
|
||||
*/
|
||||
public String hostname = "";
|
||||
/**
|
||||
* The request timeout in seconds when fetching data from the IoTaWatt device
|
||||
*/
|
||||
public long requestTimeout = REQUEST_TIMEOUT_DEFAULT;
|
||||
/**
|
||||
* The refresh interval of the IoTaWatt device in seconds
|
||||
*/
|
||||
public int refreshInterval = REFRESH_INTERVAL_DEFAULT;
|
||||
|
||||
public boolean isValid() {
|
||||
if (hostname.trim().isBlank()) {
|
||||
logger.warn("Hostname is blank, please specify the hostname/IP address of IoTaWatt.");
|
||||
return false;
|
||||
}
|
||||
if (requestTimeout <= 0) {
|
||||
logger.warn("Invalid requestTimeout {}, please use a positive number", requestTimeout);
|
||||
return false;
|
||||
}
|
||||
if (refreshInterval <= 0) {
|
||||
logger.warn("Invalid refreshInterval {}, please use a positive number", refreshInterval);
|
||||
return false;
|
||||
}
|
||||
// Also update "configuration-error" in src/main/resources/OH-INF/i18n/iotawatt_en.properties
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 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.iotawatt.internal;
|
||||
|
||||
import static org.openhab.binding.iotawatt.internal.IoTaWattBindingConstants.THING_TYPE_IOTAWATT;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.openhab.binding.iotawatt.internal.client.IoTaWattClient;
|
||||
import org.openhab.binding.iotawatt.internal.handler.FetchDataServiceProvider;
|
||||
import org.openhab.binding.iotawatt.internal.handler.HttpClientProvider;
|
||||
import org.openhab.binding.iotawatt.internal.handler.IoTaWattClientProvider;
|
||||
import org.openhab.binding.iotawatt.internal.handler.IoTaWattHandler;
|
||||
import org.openhab.binding.iotawatt.internal.service.DeviceHandlerCallback;
|
||||
import org.openhab.binding.iotawatt.internal.service.FetchDataService;
|
||||
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.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The {@link IoTaWattHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.iotawatt", service = ThingHandlerFactory.class)
|
||||
public class IoTaWattHandlerFactory extends BaseThingHandlerFactory
|
||||
implements HttpClientProvider, IoTaWattClientProvider, FetchDataServiceProvider {
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_IOTAWATT);
|
||||
|
||||
private final HttpClient insecureClient;
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
/**
|
||||
* Creates a IoTaWattHandlerFactory
|
||||
*/
|
||||
public IoTaWattHandlerFactory() {
|
||||
this.insecureClient = new HttpClient(new SslContextFactory.Client(true));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (THING_TYPE_IOTAWATT.equals(thingTypeUID)) {
|
||||
return new IoTaWattHandler(thing, this, this);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpClient getInsecureClient() {
|
||||
return insecureClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IoTaWattClient getIoTaWattClient(String hostname, long requestTimeout) {
|
||||
return new IoTaWattClient(hostname, requestTimeout, insecureClient, gson);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FetchDataService getFetchDataService(DeviceHandlerCallback deviceHandlerCallback) {
|
||||
return new FetchDataService(deviceHandlerCallback);
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
public void deactivate() {
|
||||
insecureClient.destroy();
|
||||
}
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 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.iotawatt.internal.client;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.openhab.binding.iotawatt.internal.model.StatusResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* Encapsulates the communication with the IoTaWatt device.
|
||||
*
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class IoTaWattClient {
|
||||
private static final String REQUEST_URL = "http://%s/status?state=&inputs=&outputs=";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(IoTaWattClient.class);
|
||||
|
||||
/**
|
||||
* The hostname the IoTaWattClient connects to
|
||||
*/
|
||||
public final String hostname;
|
||||
private final long requestTimeout;
|
||||
private final HttpClient httpClient;
|
||||
private final Gson gson;
|
||||
|
||||
/**
|
||||
* Creates an IoTaWattClient
|
||||
*
|
||||
* @param hostname The hostname of the IoTaWatt device to connect to
|
||||
* @param httpClient The HttpClient to use
|
||||
* @param gson The Gson decoder to use
|
||||
*/
|
||||
public IoTaWattClient(String hostname, long requestTimeout, HttpClient httpClient, Gson gson) {
|
||||
this.httpClient = httpClient;
|
||||
this.requestTimeout = requestTimeout;
|
||||
this.hostname = hostname;
|
||||
this.gson = gson;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
try {
|
||||
httpClient.start();
|
||||
} catch (Exception e) {
|
||||
// catching exception is necessary due to the signature of HttpClient.start()
|
||||
logger.warn("Failed to start http client: {}", e.getMessage());
|
||||
throw new IllegalStateException("Could not create HttpClient", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
try {
|
||||
httpClient.stop();
|
||||
} catch (Exception e) {
|
||||
// catching exception is necessary due to the signature of HttpClient.stop()
|
||||
logger.warn("Failed to stop http client: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current status from the device.
|
||||
* The errors are handled by the caller to update the Thing status accordingly.
|
||||
*
|
||||
* @throws IoTaWattClientCommunicationException On communication errors
|
||||
* @throws IoTaWattClientInterruptedException When sending the request is interrupted
|
||||
* @throws IoTaWattClientConfigurationException When the URI is wrong
|
||||
* @throws IoTaWattClientException When an unknown error occurs
|
||||
* @return The optional StatusResponse fetched from the device
|
||||
*/
|
||||
public Optional<StatusResponse> fetchStatus() throws IoTaWattClientCommunicationException,
|
||||
IoTaWattClientInterruptedException, IoTaWattClientException, IoTaWattClientConfigurationException {
|
||||
try {
|
||||
final URI uri = new URI(String.format(REQUEST_URL, hostname));
|
||||
final Request request = httpClient.newRequest(uri).method(HttpMethod.GET).timeout(requestTimeout,
|
||||
TimeUnit.SECONDS);
|
||||
final ContentResponse response = request.send();
|
||||
if (response.getStatus() != HttpStatus.OK_200) {
|
||||
throw new IoTaWattClientCommunicationException("HttpStatus " + response.getStatus());
|
||||
}
|
||||
final String content = response.getContentAsString();
|
||||
@Nullable
|
||||
final StatusResponse statusResponse = gson.fromJson(content, StatusResponse.class);
|
||||
logger.trace("statusResponse: {}", statusResponse);
|
||||
if (statusResponse.inputs() == null) {
|
||||
logger.warn("List of inputs in response from IoTaWatt is null on device {}.", hostname);
|
||||
}
|
||||
if (statusResponse.outputs() == null) {
|
||||
logger.warn("List of outputs in response from IoTaWatt is null on device {}.", hostname);
|
||||
}
|
||||
// noinspection ConstantConditions
|
||||
return Optional.ofNullable(statusResponse);
|
||||
} catch (InterruptedException e) {
|
||||
throw new IoTaWattClientInterruptedException();
|
||||
} catch (TimeoutException e) {
|
||||
throw new IoTaWattClientCommunicationException();
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IoTaWattClientConfigurationException(e);
|
||||
} catch (ExecutionException e) {
|
||||
logger.debug("Error on getting data from IoTaWatt {}", hostname);
|
||||
throw new IoTaWattClientException();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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.iotawatt.internal.client;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Thrown on communication errors with the IoTaWatt device.
|
||||
*
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class IoTaWattClientCommunicationException extends Exception {
|
||||
static final long serialVersionUID = 7960876940928850536L;
|
||||
|
||||
IoTaWattClientCommunicationException() {
|
||||
}
|
||||
|
||||
public IoTaWattClientCommunicationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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.iotawatt.internal.client;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Thrown on configuration errors.
|
||||
*
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class IoTaWattClientConfigurationException extends Exception {
|
||||
static final long serialVersionUID = 4028095925746584345L;
|
||||
|
||||
public IoTaWattClientConfigurationException() {
|
||||
}
|
||||
|
||||
public IoTaWattClientConfigurationException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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.iotawatt.internal.client;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Thrown on unknown IoTaWattClient errors.
|
||||
*
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class IoTaWattClientException extends Throwable {
|
||||
static final long serialVersionUID = 411877996315818807L;
|
||||
|
||||
public IoTaWattClientException() {
|
||||
}
|
||||
|
||||
public IoTaWattClientException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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.iotawatt.internal.client;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Thrown when the thread is interrupted.
|
||||
*
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class IoTaWattClientInterruptedException extends Exception {
|
||||
static final long serialVersionUID = -3355456899013127876L;
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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.iotawatt.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.iotawatt.internal.service.DeviceHandlerCallback;
|
||||
import org.openhab.binding.iotawatt.internal.service.FetchDataService;
|
||||
|
||||
/**
|
||||
* Provides a FetchDataService.
|
||||
*
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface FetchDataServiceProvider {
|
||||
/**
|
||||
* Get the service to handle data fetching.
|
||||
*
|
||||
* @param deviceHandlerCallback The DeviceHandlerCallback to assign to the FetchDataService
|
||||
* @return The provided FetchDataService
|
||||
*/
|
||||
FetchDataService getFetchDataService(DeviceHandlerCallback deviceHandlerCallback);
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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.iotawatt.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
|
||||
/**
|
||||
* Provides a HttpClient.
|
||||
*
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface HttpClientProvider {
|
||||
/**
|
||||
* Get the insecure http client (ignores SSL errors)
|
||||
*
|
||||
* @return The provided HttpClient
|
||||
*/
|
||||
HttpClient getInsecureClient();
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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.iotawatt.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.iotawatt.internal.client.IoTaWattClient;
|
||||
|
||||
/**
|
||||
* Provides an IoTaWattClient.
|
||||
*
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface IoTaWattClientProvider {
|
||||
/**
|
||||
* get the client to talk to IoTaWatt
|
||||
*
|
||||
* @param hostname The hostname of the IoTaWatt device
|
||||
* @return The provided IoTaWattClient
|
||||
*/
|
||||
IoTaWattClient getIoTaWattClient(String hostname, long requestTimeout);
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 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.iotawatt.internal.handler;
|
||||
|
||||
import static org.openhab.binding.iotawatt.internal.IoTaWattBindingConstants.BINDING_ID;
|
||||
|
||||
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.iotawatt.internal.IoTaWattConfiguration;
|
||||
import org.openhab.binding.iotawatt.internal.client.IoTaWattClient;
|
||||
import org.openhab.binding.iotawatt.internal.model.IoTaWattChannelType;
|
||||
import org.openhab.binding.iotawatt.internal.service.DeviceHandlerCallback;
|
||||
import org.openhab.binding.iotawatt.internal.service.FetchDataService;
|
||||
import org.openhab.core.thing.Channel;
|
||||
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.ThingUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
/**
|
||||
* The {@link IoTaWattHandler} is responsible for the communication between the external device and openHAB.
|
||||
*
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class IoTaWattHandler extends BaseThingHandler implements DeviceHandlerCallback {
|
||||
private final IoTaWattClientProvider ioTaWattClientProvider;
|
||||
private final FetchDataService fetchDataService;
|
||||
private @Nullable IoTaWattClient ioTaWattClient;
|
||||
private @Nullable ScheduledFuture<?> fetchDataJob;
|
||||
|
||||
/**
|
||||
* Creates an IoTaWattHandler
|
||||
*
|
||||
* @param thing The Thing of the IoTaWattHandler
|
||||
* @param ioTaWattClientProvider The IoTaWattClientProvider to use
|
||||
* @param fetchDataServiceProvider The FetchDataServiceProvider to use to fetch data
|
||||
*/
|
||||
public IoTaWattHandler(Thing thing, IoTaWattClientProvider ioTaWattClientProvider,
|
||||
FetchDataServiceProvider fetchDataServiceProvider) {
|
||||
super(thing);
|
||||
this.ioTaWattClientProvider = ioTaWattClientProvider;
|
||||
this.fetchDataService = fetchDataServiceProvider.getFetchDataService(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
final IoTaWattConfiguration config = getConfigAs(IoTaWattConfiguration.class);
|
||||
if (!config.isValid()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/configuration-error");
|
||||
return;
|
||||
}
|
||||
|
||||
final IoTaWattClient ioTaWattClient = ioTaWattClientProvider.getIoTaWattClient(config.hostname,
|
||||
config.requestTimeout);
|
||||
ioTaWattClient.start();
|
||||
fetchDataService.setIoTaWattClient(ioTaWattClient);
|
||||
this.ioTaWattClient = ioTaWattClient;
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
|
||||
fetchDataJob = scheduler.scheduleWithFixedDelay(fetchDataService::pollDevice, 0, config.refreshInterval,
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
ScheduledFuture<?> fetchDataJobLocal = this.fetchDataJob;
|
||||
if (fetchDataJobLocal != null) {
|
||||
fetchDataJobLocal.cancel(true);
|
||||
this.fetchDataJob = null;
|
||||
}
|
||||
IoTaWattClient ioTaWattClient = this.ioTaWattClient;
|
||||
if (ioTaWattClient != null) {
|
||||
ioTaWattClient.stop();
|
||||
this.ioTaWattClient = null;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
// Callbacks
|
||||
// --------------------------------------------------------------------------------------------
|
||||
@Override
|
||||
public void updateStatus(ThingStatus status) {
|
||||
super.updateStatus(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail) {
|
||||
super.updateStatus(status, statusDetail);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
|
||||
super.updateStatus(status, statusDetail, description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateState(ChannelUID channelUID, State state) {
|
||||
super.updateState(channelUID, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThingUID getThingUID() {
|
||||
return getThing().getUID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addChannelIfNotExists(ChannelUID channelUID, IoTaWattChannelType ioTaWattChannelType) {
|
||||
final ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, ioTaWattChannelType.typeId);
|
||||
if (getThing().getChannel(channelUID) == null) {
|
||||
final ThingBuilder thingBuilder = editThing();
|
||||
final Channel channel = ChannelBuilder.create(channelUID, ioTaWattChannelType.acceptedItemType)
|
||||
.withType(channelTypeUID).build();
|
||||
thingBuilder.withChannel(channel);
|
||||
updateThing(thingBuilder.build());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 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.iotawatt.internal.model;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
|
||||
/**
|
||||
* Enum for each channel type of IoTaWatt supported by this binding.
|
||||
*
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum IoTaWattChannelType {
|
||||
/**
|
||||
* Electrical current
|
||||
*/
|
||||
AMPS("amps", "amps", "Number:power", Units.AMPERE),
|
||||
/**
|
||||
* AC Frequency
|
||||
*/
|
||||
FREQUENCY("frequency", "frequency", "Number:Frequency", Units.HERTZ),
|
||||
/**
|
||||
* Power Factor
|
||||
*/
|
||||
POWER_FACTOR("power-factor", "power-factor", "Number:Dimensionless", Units.ONE),
|
||||
/**
|
||||
* Apparent Power
|
||||
*/
|
||||
APPARENT_POWER("apparent-power", "apparent-power", "Number:power", Units.VOLT_AMPERE),
|
||||
/**
|
||||
* Reactive Power
|
||||
*/
|
||||
REACTIVE_POWER("reactive-power", "reactive-power", "Number:power", Units.VAR),
|
||||
/**
|
||||
* Reactive Power Hour
|
||||
*/
|
||||
REACTIVE_POWER_HOUR("reactive-power-hour", "reactive-power-hour", "Number:Energy", Units.VAR_HOUR),
|
||||
/**
|
||||
* Voltage
|
||||
*/
|
||||
VOLTAGE("voltage", "voltage", "Number:ElectricPotential", Units.VOLT),
|
||||
/**
|
||||
* Watt, Active Power
|
||||
*/
|
||||
WATTS("watts", "watts", "Number:Power", Units.WATT),
|
||||
/**
|
||||
* Phase
|
||||
*/
|
||||
PHASE("phase", "phase", "Number:Dimensionless", Units.ONE);
|
||||
|
||||
/**
|
||||
* Id of the channel in XML definition channel-type id.
|
||||
*/
|
||||
public final String typeId;
|
||||
/**
|
||||
* Defines the last part of the channel UID.
|
||||
*/
|
||||
public final String channelIdSuffix;
|
||||
/**
|
||||
* The value type the channel accepts.
|
||||
*/
|
||||
public final String acceptedItemType;
|
||||
/**
|
||||
* The unit of the channel.
|
||||
*/
|
||||
public final Unit<?> unit;
|
||||
|
||||
/**
|
||||
* Creates an IoTaWattChannelType
|
||||
*
|
||||
* @param typeId The TypeId
|
||||
* @param channelIdSuffix The suffix of the channelId
|
||||
* @param acceptedItemType The acceptedItemType
|
||||
* @param unit The unit of the channel
|
||||
*/
|
||||
IoTaWattChannelType(String typeId, String channelIdSuffix, String acceptedItemType, Unit<?> unit) {
|
||||
this.acceptedItemType = acceptedItemType;
|
||||
this.typeId = typeId;
|
||||
this.channelIdSuffix = channelIdSuffix;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an IoTaWattChannelType
|
||||
*
|
||||
* @param value The units to get an IoTaWattChannelType from
|
||||
* @return The IoTaWattChannelType
|
||||
*/
|
||||
public static IoTaWattChannelType fromOutputUnits(String value) {
|
||||
return switch (value) {
|
||||
case "Amps" -> IoTaWattChannelType.AMPS;
|
||||
case "Hz" -> IoTaWattChannelType.FREQUENCY;
|
||||
case "PF" -> IoTaWattChannelType.POWER_FACTOR;
|
||||
case "VA" -> IoTaWattChannelType.APPARENT_POWER;
|
||||
case "VAR" -> IoTaWattChannelType.REACTIVE_POWER;
|
||||
case "VARh" -> IoTaWattChannelType.REACTIVE_POWER_HOUR;
|
||||
case "Volts" -> IoTaWattChannelType.VOLTAGE;
|
||||
case "Watts" -> IoTaWattChannelType.WATTS;
|
||||
default -> throw new IllegalArgumentException("Unknown value " + value);
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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.iotawatt.internal.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Status response of IoTaWatt.
|
||||
*
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public record StatusResponse(@Nullable List<Input> inputs, @Nullable List<Output> outputs) {
|
||||
/**
|
||||
* Represents the inputs of IoTaWatt
|
||||
*
|
||||
* @param channel The channel ID
|
||||
* @param vrms Current VRMS
|
||||
* @param hz Current frequency
|
||||
* @param phase Current phase
|
||||
* @param watts Current watts
|
||||
* @param pf Current power factor
|
||||
*/
|
||||
public record Input(int channel, @Nullable @SerializedName("Vrms") Float vrms,
|
||||
@Nullable @SerializedName("Hz") Float hz, @Nullable Float phase,
|
||||
@Nullable @SerializedName("Watts") Float watts, @Nullable @SerializedName("Pf") Float pf) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the outputs of IoTaWatt
|
||||
*
|
||||
* @param name Name of the output
|
||||
* @param units Unit of the output
|
||||
* @param value Current value of the output
|
||||
*/
|
||||
public record Output(String name, String units, Float value) {
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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.iotawatt.internal.service;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.iotawatt.internal.model.IoTaWattChannelType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
/**
|
||||
* Allows the service to do callback to the device handler.
|
||||
*
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface DeviceHandlerCallback {
|
||||
/**
|
||||
* Updates the status of the thing. The detail of the status will be 'NONE'.
|
||||
*
|
||||
* @param status the status
|
||||
*/
|
||||
void updateStatus(ThingStatus status);
|
||||
|
||||
/**
|
||||
* Updates the status of the thing.
|
||||
*
|
||||
* @param status the status
|
||||
* @param statusDetail the detail of the status
|
||||
*/
|
||||
void updateStatus(ThingStatus status, ThingStatusDetail statusDetail);
|
||||
|
||||
/**
|
||||
* Updates the status of the thing.
|
||||
*
|
||||
* @param status the status
|
||||
* @param statusDetail the detail of the status
|
||||
* @param description the description of the status
|
||||
*/
|
||||
void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description);
|
||||
|
||||
/**
|
||||
*
|
||||
* Updates the state of the thing.
|
||||
*
|
||||
* @param channelUID unique id of the channel, which was updated
|
||||
* @param state new state
|
||||
*/
|
||||
void updateState(ChannelUID channelUID, State state);
|
||||
|
||||
/**
|
||||
* @return The ThingUID of the Thing
|
||||
*/
|
||||
ThingUID getThingUID();
|
||||
|
||||
/**
|
||||
* Adds the channel to the Thing if the channel does not yet exist.
|
||||
*
|
||||
* @param channelUID The ChannelUID of the channel to add
|
||||
* @param ioTaWattChannelType The IoTaWattChannelType of the channel to add
|
||||
*/
|
||||
void addChannelIfNotExists(ChannelUID channelUID, IoTaWattChannelType ioTaWattChannelType);
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 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.iotawatt.internal.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.iotawatt.internal.client.IoTaWattClient;
|
||||
import org.openhab.binding.iotawatt.internal.client.IoTaWattClientCommunicationException;
|
||||
import org.openhab.binding.iotawatt.internal.client.IoTaWattClientConfigurationException;
|
||||
import org.openhab.binding.iotawatt.internal.client.IoTaWattClientException;
|
||||
import org.openhab.binding.iotawatt.internal.client.IoTaWattClientInterruptedException;
|
||||
import org.openhab.binding.iotawatt.internal.model.IoTaWattChannelType;
|
||||
import org.openhab.binding.iotawatt.internal.model.StatusResponse;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
|
||||
/**
|
||||
* Fetches data from IoTaWatt and updates the channels accordingly.
|
||||
*
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FetchDataService {
|
||||
static final String INPUT_CHANNEL_ID_PREFIX = "input_";
|
||||
static final String OUTPUT_CHANNEL_ID_PREFIX = "output_";
|
||||
|
||||
private final DeviceHandlerCallback deviceHandlerCallback;
|
||||
private @Nullable IoTaWattClient ioTaWattClient;
|
||||
|
||||
/**
|
||||
* Creates a FetchDataService.
|
||||
*
|
||||
* @param deviceHandlerCallback The ThingHandler used for callbacks
|
||||
*/
|
||||
public FetchDataService(DeviceHandlerCallback deviceHandlerCallback) {
|
||||
this.deviceHandlerCallback = deviceHandlerCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the IoTaWattClient
|
||||
*
|
||||
* @param ioTaWattClient The IoTaWattClient to use
|
||||
*/
|
||||
public void setIoTaWattClient(IoTaWattClient ioTaWattClient) {
|
||||
this.ioTaWattClient = ioTaWattClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll the device once without retry.
|
||||
* Handles error cases and updates the Thing accordingly.
|
||||
*/
|
||||
public void pollDevice() {
|
||||
Optional.ofNullable(ioTaWattClient).ifPresentOrElse(this::pollDevice, () -> deviceHandlerCallback
|
||||
.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR));
|
||||
}
|
||||
|
||||
private void pollDevice(IoTaWattClient client) {
|
||||
try {
|
||||
final Optional<StatusResponse> statusResponse = client.fetchStatus();
|
||||
if (statusResponse.isPresent()) {
|
||||
deviceHandlerCallback.updateStatus(ThingStatus.ONLINE);
|
||||
updateChannels(statusResponse.get());
|
||||
} else {
|
||||
deviceHandlerCallback.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
}
|
||||
} catch (IoTaWattClientInterruptedException e) {
|
||||
deviceHandlerCallback.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NOT_YET_READY);
|
||||
} catch (IoTaWattClientCommunicationException e) {
|
||||
deviceHandlerCallback.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
} catch (IoTaWattClientConfigurationException e) {
|
||||
deviceHandlerCallback.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
getErrorMessage(e));
|
||||
} catch (IoTaWattClientException e) {
|
||||
deviceHandlerCallback.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, getErrorMessage(e));
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getErrorMessage(Throwable t) {
|
||||
final Throwable cause = t.getCause();
|
||||
return Objects.requireNonNullElse(cause, t).getMessage();
|
||||
}
|
||||
|
||||
private void updateChannels(StatusResponse statusResponse) {
|
||||
Optional.ofNullable(statusResponse.inputs()).ifPresent(this::updateInputs);
|
||||
Optional.ofNullable(statusResponse.outputs()).ifPresent(this::updateOutputs);
|
||||
}
|
||||
|
||||
private void updateInputs(List<StatusResponse.Input> inputs) {
|
||||
for (final StatusResponse.Input input : inputs) {
|
||||
final int channelNumber = input.channel();
|
||||
createAndUpdateInputChannel(channelNumber, input.watts(), IoTaWattChannelType.WATTS);
|
||||
createAndUpdateInputChannel(channelNumber, input.vrms(), IoTaWattChannelType.VOLTAGE);
|
||||
createAndUpdateInputChannel(channelNumber, input.hz(), IoTaWattChannelType.FREQUENCY);
|
||||
createAndUpdateInputChannel(channelNumber, input.pf(), IoTaWattChannelType.POWER_FACTOR);
|
||||
createAndUpdateInputChannel(channelNumber, input.phase(), IoTaWattChannelType.PHASE);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateOutputs(List<StatusResponse.Output> outputs) {
|
||||
int index = 0;
|
||||
for (final StatusResponse.Output output : outputs) {
|
||||
final ChannelUID channelUID = new ChannelUID(deviceHandlerCallback.getThingUID(),
|
||||
OUTPUT_CHANNEL_ID_PREFIX + toTwoDigits(index++) + "#" + output.name());
|
||||
final Float value = output.value();
|
||||
final IoTaWattChannelType ioTaWattChannelType = IoTaWattChannelType.fromOutputUnits(output.units());
|
||||
deviceHandlerCallback.addChannelIfNotExists(channelUID, ioTaWattChannelType);
|
||||
deviceHandlerCallback.updateState(channelUID, new QuantityType<>(value, ioTaWattChannelType.unit));
|
||||
// TODO removed channels are not in array anymore
|
||||
}
|
||||
}
|
||||
|
||||
private void createAndUpdateInputChannel(int channelNumber, @Nullable Number value,
|
||||
IoTaWattChannelType ioTaWattChannelType) {
|
||||
final ChannelUID channelUID = getInputChannelUID(channelNumber, ioTaWattChannelType);
|
||||
if (value != null) {
|
||||
deviceHandlerCallback.addChannelIfNotExists(channelUID, ioTaWattChannelType);
|
||||
deviceHandlerCallback.updateState(channelUID, new QuantityType<>(value, ioTaWattChannelType.unit));
|
||||
// TODO removed channels are not in array anymore
|
||||
}
|
||||
}
|
||||
|
||||
private ChannelUID getInputChannelUID(int channelNumber, IoTaWattChannelType ioTaWattChannelType) {
|
||||
return new ChannelUID(deviceHandlerCallback.getThingUID(),
|
||||
INPUT_CHANNEL_ID_PREFIX + toTwoDigits(channelNumber) + "#" + ioTaWattChannelType.channelIdSuffix);
|
||||
}
|
||||
|
||||
private String toTwoDigits(int value) {
|
||||
return value < 10 ? ("0" + value) : String.valueOf(value);
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<addon:addon id="iotawatt" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
|
||||
|
||||
<type>binding</type>
|
||||
<name>IoTaWatt Binding</name>
|
||||
<description>This is the binding for IoTaWatt.</description>
|
||||
<connection>local</connection>
|
||||
|
||||
</addon:addon>
|
@ -0,0 +1,42 @@
|
||||
# add-on
|
||||
|
||||
addon.iotawatt.name = IoTaWatt Binding
|
||||
addon.iotawatt.description = This is the binding for IoTaWatt.
|
||||
|
||||
# thing types
|
||||
|
||||
thing-type.iotawatt.iotawatt.label = IoTaWatt Binding Thing
|
||||
thing-type.iotawatt.iotawatt.description = An IoTaWatt devices
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.iotawatt.iotawatt.hostname.label = Hostname
|
||||
thing-type.config.iotawatt.iotawatt.hostname.description = Hostname or IP address of the device
|
||||
thing-type.config.iotawatt.iotawatt.refreshInterval.label = Refresh Interval
|
||||
thing-type.config.iotawatt.iotawatt.refreshInterval.description = Interval the device is polled in sec.
|
||||
thing-type.config.iotawatt.iotawatt.requestTimeout.label = Request timeout
|
||||
thing-type.config.iotawatt.iotawatt.requestTimeout.description = The request timeout to call the device in sec.
|
||||
|
||||
# channel types
|
||||
|
||||
channel-type.iotawatt.amps.label = Amps
|
||||
channel-type.iotawatt.amps.description = The current Amps.
|
||||
channel-type.iotawatt.apparent-power.label = Apparent Power
|
||||
channel-type.iotawatt.apparent-power.description = The current apparent power.
|
||||
channel-type.iotawatt.frequency.label = AC Frequency
|
||||
channel-type.iotawatt.frequency.description = The current AC frequency.
|
||||
channel-type.iotawatt.phase.label = Phase
|
||||
channel-type.iotawatt.phase.description = The current phase.
|
||||
channel-type.iotawatt.power-factor.label = Power Factor
|
||||
channel-type.iotawatt.power-factor.description = The current power factor.
|
||||
channel-type.iotawatt.reactive-power-hour.label = Reactive Power Hour
|
||||
channel-type.iotawatt.reactive-power-hour.description = The current reactive power hour.
|
||||
channel-type.iotawatt.reactive-power.label = Reactive Power
|
||||
channel-type.iotawatt.reactive-power.description = The current reactive power.
|
||||
channel-type.iotawatt.voltage.label = Voltage
|
||||
channel-type.iotawatt.voltage.description = The current voltage.
|
||||
channel-type.iotawatt.watts.label = Power Consumption
|
||||
channel-type.iotawatt.watts.description = The current power consumption.
|
||||
|
||||
# channel types
|
||||
configuration-error = The configuration is wrong, please check if you configured a hostname/IP address and positive numbers for the timeout settings.
|
@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="iotawatt"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<!-- Thing Type -->
|
||||
<thing-type id="iotawatt">
|
||||
<label>IoTaWatt Binding Thing</label>
|
||||
<description>An IoTaWatt devices</description>
|
||||
|
||||
<config-description>
|
||||
<parameter name="hostname" type="text" required="true">
|
||||
<context>network-address</context>
|
||||
<label>Hostname</label>
|
||||
<description>Hostname or IP address of the device</description>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" unit="s" min="1">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Interval the device is polled in sec.</description>
|
||||
<default>10</default>
|
||||
<advanced>false</advanced>
|
||||
</parameter>
|
||||
<parameter name="requestTimeout" type="integer" unit="s" min="1">
|
||||
<label>Request timeout</label>
|
||||
<description>The request timeout to call the device in sec.</description>
|
||||
<default>10</default>
|
||||
<advanced>false</advanced>
|
||||
</parameter>
|
||||
<!-- run mvn i18n:generate-default-translations when updating the params -->
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<!-- Channel Types -->
|
||||
<channel-type id="amps">
|
||||
<item-type>Number:ElectricCurrent</item-type>
|
||||
<label>Amps</label>
|
||||
<description>The current Amps.</description>
|
||||
<state pattern="%.2f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="frequency">
|
||||
<item-type>Number:Frequency</item-type>
|
||||
<label>AC Frequency</label>
|
||||
<description>The current AC frequency.</description>
|
||||
<state pattern="%.2f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="power-factor">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Power Factor</label>
|
||||
<description>The current power factor.</description>
|
||||
<state pattern="%.2f" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="apparent-power">
|
||||
<item-type>Number:Power</item-type>
|
||||
<label>Apparent Power</label>
|
||||
<description>The current apparent power.</description>
|
||||
<state pattern="%.2f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="reactive-power">
|
||||
<item-type>Number:Power</item-type>
|
||||
<label>Reactive Power</label>
|
||||
<description>The current reactive power.</description>
|
||||
<state pattern="%.2f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="reactive-power-hour">
|
||||
<item-type>Number:Energy</item-type>
|
||||
<label>Reactive Power Hour</label>
|
||||
<description>The current reactive power hour.</description>
|
||||
<state pattern="%.2f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="voltage">
|
||||
<item-type>Number:ElectricPotential</item-type>
|
||||
<label>Voltage</label>
|
||||
<description>The current voltage.</description>
|
||||
<category>Energy</category>
|
||||
<tags>
|
||||
<tag>Measurement</tag>
|
||||
<tag>Voltage</tag>
|
||||
</tags>
|
||||
<state pattern="%.3f %unit%" readOnly="true">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="watts">
|
||||
<item-type>Number:Power</item-type>
|
||||
<label>Power Consumption</label>
|
||||
<description>The current power consumption.</description>
|
||||
<category>Energy</category>
|
||||
<tags>
|
||||
<tag>Measurement</tag>
|
||||
<tag>Power</tag>
|
||||
</tags>
|
||||
<state pattern="%.2f %unit%" readOnly="true">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="phase">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Phase</label>
|
||||
<description>The current phase.</description>
|
||||
<state pattern="%.2f" readOnly="true"/>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,216 @@
|
||||
/**
|
||||
* 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.iotawatt.internal.client;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
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.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.openhab.binding.iotawatt.internal.model.StatusResponse;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@NonNullByDefault
|
||||
class IoTaWattClientTest {
|
||||
private static final String DEVICE_STATUS_RESPONSE_FILE = "apiResponses/device-status-response.json";
|
||||
|
||||
@Mock
|
||||
@NonNullByDefault({})
|
||||
private HttpClient httpClient;
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
@Test
|
||||
void fetchStatus_whenValidJson_returnObject() throws IOException, ExecutionException, InterruptedException,
|
||||
TimeoutException, IoTaWattClientInterruptedException, IoTaWattClientCommunicationException,
|
||||
IoTaWattClientConfigurationException, IoTaWattClientException {
|
||||
// given
|
||||
final IoTaWattClient client = new IoTaWattClient("hostname", 10, httpClient, gson);
|
||||
Request request = mock(Request.class);
|
||||
when(httpClient.newRequest(any(URI.class))).thenReturn(request);
|
||||
when(request.method(any(HttpMethod.class))).thenReturn(request);
|
||||
when(request.timeout(anyLong(), any(TimeUnit.class))).thenReturn(request);
|
||||
ContentResponse contentResponse = mock(ContentResponse.class);
|
||||
when(request.send()).thenReturn(contentResponse);
|
||||
when(contentResponse.getStatus()).thenReturn(HttpStatus.OK_200);
|
||||
when(contentResponse.getContentAsString()).thenReturn(readFile(DEVICE_STATUS_RESPONSE_FILE));
|
||||
|
||||
// when
|
||||
Optional<StatusResponse> resultOptional = client.fetchStatus();
|
||||
|
||||
// then
|
||||
// noinspection OptionalGetWithoutIsPresent
|
||||
StatusResponse result = resultOptional.get();
|
||||
assertThat(result.inputs().size(), is(2));
|
||||
StatusResponse.Input input0 = result.inputs().get(0);
|
||||
assertThat(input0.channel(), is(0));
|
||||
assertThat(input0.vrms(), is(254.2972F));
|
||||
assertThat(input0.hz(), is(50.02768F));
|
||||
assertThat(input0.phase(), is(0.92F));
|
||||
StatusResponse.Input input1 = result.inputs().get(1);
|
||||
assertThat(input1.channel(), is(1));
|
||||
assertThat(input1.watts(), is(1.42F));
|
||||
assertThat(input1.phase(), is(2.2F));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fetchStatus_whenWrongHost_throwException() {
|
||||
// given
|
||||
final IoTaWattClient client = new IoTaWattClient(" ", 10, httpClient, mock(Gson.class));
|
||||
|
||||
// when
|
||||
assertThrows(IoTaWattClientConfigurationException.class, client::fetchStatus);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fetchStatus_whenInputsAndOutputsEmpty_returnEmpty()
|
||||
throws ExecutionException, InterruptedException, TimeoutException, IoTaWattClientInterruptedException,
|
||||
IoTaWattClientCommunicationException, IoTaWattClientConfigurationException, IoTaWattClientException {
|
||||
// given
|
||||
final IoTaWattClient client = new IoTaWattClient("hostname", 10, httpClient, gson);
|
||||
Request request = mock(Request.class);
|
||||
when(httpClient.newRequest(any(URI.class))).thenReturn(request);
|
||||
when(request.method(any(HttpMethod.class))).thenReturn(request);
|
||||
when(request.timeout(anyLong(), any(TimeUnit.class))).thenReturn(request);
|
||||
ContentResponse contentResponse = mock(ContentResponse.class);
|
||||
when(request.send()).thenReturn(contentResponse);
|
||||
when(contentResponse.getContentAsString()).thenReturn("{}");
|
||||
when(contentResponse.getStatus()).thenReturn(HttpStatus.OK_200);
|
||||
|
||||
// when
|
||||
Optional<StatusResponse> resultOptional = client.fetchStatus();
|
||||
|
||||
// then
|
||||
// noinspection OptionalGetWithoutIsPresent
|
||||
StatusResponse result = resultOptional.get();
|
||||
assertNull(result.inputs());
|
||||
assertNull(result.outputs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fetchStatus_whenNot200Response_throwsException()
|
||||
throws ExecutionException, InterruptedException, TimeoutException {
|
||||
// given
|
||||
final IoTaWattClient client = new IoTaWattClient("hostname", 10, httpClient, gson);
|
||||
Request request = mock(Request.class);
|
||||
when(httpClient.newRequest(any(URI.class))).thenReturn(request);
|
||||
when(request.method(any(HttpMethod.class))).thenReturn(request);
|
||||
when(request.timeout(anyLong(), any(TimeUnit.class))).thenReturn(request);
|
||||
ContentResponse contentResponse = mock(ContentResponse.class);
|
||||
when(request.send()).thenReturn(contentResponse);
|
||||
when(contentResponse.getStatus()).thenReturn(HttpStatus.BAD_REQUEST_400);
|
||||
|
||||
// when/then
|
||||
assertThrows(IoTaWattClientCommunicationException.class, client::fetchStatus);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideParamsForThrowCases")
|
||||
void fetchStatus_whenExceptions_throwsCustomException(Class<Throwable> thrownException,
|
||||
Class<Throwable> expectedException) throws ExecutionException, InterruptedException, TimeoutException {
|
||||
// given
|
||||
final IoTaWattClient client = new IoTaWattClient("hostname", 10, httpClient, gson);
|
||||
Request request = mock(Request.class);
|
||||
when(httpClient.newRequest(any(URI.class))).thenReturn(request);
|
||||
when(request.method(any(HttpMethod.class))).thenReturn(request);
|
||||
when(request.timeout(anyLong(), any(TimeUnit.class))).thenReturn(request);
|
||||
when(request.send()).thenThrow(thrownException);
|
||||
|
||||
// when/then
|
||||
assertThrows(expectedException, client::fetchStatus);
|
||||
}
|
||||
|
||||
@Test
|
||||
void start_whenSuccess_noException() {
|
||||
// given
|
||||
final IoTaWattClient client = new IoTaWattClient("hostname", 10, httpClient, gson);
|
||||
// when
|
||||
client.start();
|
||||
// then
|
||||
// doesn't throw an exception
|
||||
}
|
||||
|
||||
@Test
|
||||
void start_whenError_throwException() throws Exception {
|
||||
// given
|
||||
final IoTaWattClient client = new IoTaWattClient("hostname", 10, httpClient, gson);
|
||||
doThrow(Exception.class).when(httpClient).start();
|
||||
// when/then
|
||||
assertThrows(IllegalStateException.class, client::start);
|
||||
}
|
||||
|
||||
@Test
|
||||
void stop_whenSuccess_noException() {
|
||||
// given
|
||||
final IoTaWattClient client = new IoTaWattClient("hostname", 10, httpClient, gson);
|
||||
// when
|
||||
client.stop();
|
||||
// then
|
||||
// doesn't throw an exception
|
||||
}
|
||||
|
||||
@Test
|
||||
void stop_whenError_noException() throws Exception {
|
||||
// given
|
||||
final IoTaWattClient client = new IoTaWattClient("hostname", 10, httpClient, gson);
|
||||
doThrow(Exception.class).when(httpClient).stop();
|
||||
// when
|
||||
client.stop();
|
||||
// then
|
||||
// doesn't throw an exception
|
||||
}
|
||||
|
||||
private static Stream<Arguments> provideParamsForThrowCases() {
|
||||
return Stream.of(Arguments.of(InterruptedException.class, IoTaWattClientInterruptedException.class),
|
||||
Arguments.of(TimeoutException.class, IoTaWattClientCommunicationException.class),
|
||||
Arguments.of(ExecutionException.class, IoTaWattClientException.class));
|
||||
}
|
||||
|
||||
private String readFile(String filename) throws IOException {
|
||||
final Path workingDir = Path.of("", "src/test/resources");
|
||||
final Path file = workingDir.resolve(filename);
|
||||
return Files.readString(file);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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.iotawatt.internal.model;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class IoTaWattChannelTypeTest {
|
||||
|
||||
@Test
|
||||
void valueOf_whenUnknownValue_thenThrowException() {
|
||||
// given
|
||||
final String unknownValue = "unknownValue";
|
||||
|
||||
// when/then
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
assertThrows(IllegalArgumentException.class, () -> IoTaWattChannelType.fromOutputUnits(unknownValue));
|
||||
}
|
||||
}
|
@ -0,0 +1,251 @@
|
||||
/**
|
||||
* 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.iotawatt.internal.service;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.openhab.binding.iotawatt.internal.service.FetchDataService.INPUT_CHANNEL_ID_PREFIX;
|
||||
import static org.openhab.binding.iotawatt.internal.service.FetchDataService.OUTPUT_CHANNEL_ID_PREFIX;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.openhab.binding.iotawatt.internal.IoTaWattBindingConstants;
|
||||
import org.openhab.binding.iotawatt.internal.client.IoTaWattClient;
|
||||
import org.openhab.binding.iotawatt.internal.client.IoTaWattClientCommunicationException;
|
||||
import org.openhab.binding.iotawatt.internal.client.IoTaWattClientConfigurationException;
|
||||
import org.openhab.binding.iotawatt.internal.client.IoTaWattClientException;
|
||||
import org.openhab.binding.iotawatt.internal.client.IoTaWattClientInterruptedException;
|
||||
import org.openhab.binding.iotawatt.internal.model.StatusResponse;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
/**
|
||||
* @author Peter Rosenberg - Initial contribution
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@NonNullByDefault
|
||||
class FetchDataServiceTest {
|
||||
@Mock
|
||||
@NonNullByDefault({})
|
||||
private DeviceHandlerCallback deviceHandlerCallback;
|
||||
@Mock
|
||||
@NonNullByDefault({})
|
||||
private IoTaWattClient ioTaWattClient;
|
||||
@InjectMocks
|
||||
@NonNullByDefault({})
|
||||
private FetchDataService service;
|
||||
|
||||
private final ThingUID thingUID = new ThingUID(IoTaWattBindingConstants.BINDING_ID, "d231dea2e4");
|
||||
|
||||
@Test
|
||||
void pollDevice_whenAllSupportedInputTypes_updateAllChannels() throws IoTaWattClientInterruptedException,
|
||||
IoTaWattClientCommunicationException, IoTaWattClientConfigurationException, IoTaWattClientException {
|
||||
// given
|
||||
service.setIoTaWattClient(ioTaWattClient);
|
||||
final Float voltageRms = 259.1f;
|
||||
final Float hertz = 50.1f;
|
||||
final Float phase0 = 0.1f;
|
||||
final Float wattsValue = 1.1f;
|
||||
final Float phase1 = 0.2f;
|
||||
final Float powerFactor = 0.3f;
|
||||
when(deviceHandlerCallback.getThingUID()).thenReturn(thingUID);
|
||||
final List<StatusResponse.Input> inputs = List.of(
|
||||
new StatusResponse.Input(0, voltageRms, hertz, phase0, null, null),
|
||||
new StatusResponse.Input(1, null, null, phase1, wattsValue, powerFactor));
|
||||
final StatusResponse statusResponse = new StatusResponse(inputs, List.of());
|
||||
when(ioTaWattClient.fetchStatus()).thenReturn(Optional.of(statusResponse));
|
||||
|
||||
// when
|
||||
service.pollDevice();
|
||||
|
||||
// then
|
||||
verify(deviceHandlerCallback).updateStatus(ThingStatus.ONLINE);
|
||||
verify(deviceHandlerCallback).updateState(createInputChannelUID("00", "voltage"),
|
||||
createState(voltageRms, Units.VOLT));
|
||||
verify(deviceHandlerCallback).updateState(createInputChannelUID("00", "frequency"),
|
||||
createState(hertz, Units.HERTZ));
|
||||
verify(deviceHandlerCallback).updateState(createInputChannelUID("00", "phase"), createState(phase0, Units.ONE));
|
||||
verify(deviceHandlerCallback).updateState(createInputChannelUID("01", "watts"),
|
||||
createState(wattsValue, Units.WATT));
|
||||
verify(deviceHandlerCallback).updateState(createInputChannelUID("01", "phase"), createState(phase1, Units.ONE));
|
||||
verify(deviceHandlerCallback).updateState(createInputChannelUID("01", "power-factor"),
|
||||
createState(powerFactor, Units.ONE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pollDevice_whenAllSupportedOutputTypes_updateAllChannels() throws IoTaWattClientInterruptedException,
|
||||
IoTaWattClientCommunicationException, IoTaWattClientConfigurationException, IoTaWattClientException {
|
||||
// given
|
||||
service.setIoTaWattClient(ioTaWattClient);
|
||||
when(deviceHandlerCallback.getThingUID()).thenReturn(thingUID);
|
||||
final List<StatusResponse.Output> outputs = List.of(new StatusResponse.Output("name_amps", "Amps", 1.01f),
|
||||
new StatusResponse.Output("name_hz", "Hz", 1.02f), new StatusResponse.Output("name_pf", "PF", 1.03f),
|
||||
new StatusResponse.Output("name_va", "VA", 1.04f), new StatusResponse.Output("name_var", "VAR", 1.05f),
|
||||
new StatusResponse.Output("name_varh", "VARh", 1.06f),
|
||||
new StatusResponse.Output("name_volts", "Volts", 1.07f),
|
||||
new StatusResponse.Output("name_watts", "Watts", 1.08f));
|
||||
final StatusResponse statusResponse = new StatusResponse(List.of(), outputs);
|
||||
when(ioTaWattClient.fetchStatus()).thenReturn(Optional.of(statusResponse));
|
||||
|
||||
// when
|
||||
service.pollDevice();
|
||||
|
||||
// then
|
||||
verify(deviceHandlerCallback).updateStatus(ThingStatus.ONLINE);
|
||||
verify(deviceHandlerCallback).updateState(createOutputChannelUID("00", "name_amps"),
|
||||
createState(1.01f, Units.AMPERE));
|
||||
verify(deviceHandlerCallback).updateState(createOutputChannelUID("01", "name_hz"),
|
||||
createState(1.02f, Units.HERTZ));
|
||||
verify(deviceHandlerCallback).updateState(createOutputChannelUID("02", "name_pf"),
|
||||
createState(1.03f, Units.ONE));
|
||||
verify(deviceHandlerCallback).updateState(createOutputChannelUID("03", "name_va"),
|
||||
createState(1.04f, Units.VOLT_AMPERE));
|
||||
verify(deviceHandlerCallback).updateState(createOutputChannelUID("04", "name_var"),
|
||||
createState(1.05f, Units.VAR));
|
||||
verify(deviceHandlerCallback).updateState(createOutputChannelUID("05", "name_varh"),
|
||||
createState(1.06f, Units.VAR_HOUR));
|
||||
verify(deviceHandlerCallback).updateState(createOutputChannelUID("06", "name_volts"),
|
||||
createState(1.07f, Units.VOLT));
|
||||
verify(deviceHandlerCallback).updateState(createOutputChannelUID("07", "name_watts"),
|
||||
createState(1.08f, Units.WATT));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pollDevice_whenResponseWithNoChannels_updateStatusToOnlineAndDoNotUpdateChannels()
|
||||
throws IoTaWattClientInterruptedException, IoTaWattClientCommunicationException,
|
||||
IoTaWattClientConfigurationException, IoTaWattClientException {
|
||||
// given
|
||||
service.setIoTaWattClient(ioTaWattClient);
|
||||
when(ioTaWattClient.fetchStatus()).thenReturn(Optional.empty());
|
||||
|
||||
// when
|
||||
service.pollDevice();
|
||||
|
||||
// then
|
||||
verify(deviceHandlerCallback).updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
verify(deviceHandlerCallback, never()).updateState(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pollDevice_whenExceptionWithCase_useCauseMessage() throws IoTaWattClientInterruptedException,
|
||||
IoTaWattClientCommunicationException, IoTaWattClientConfigurationException, IoTaWattClientException {
|
||||
// given
|
||||
final String exceptionMessage = "test message";
|
||||
service.setIoTaWattClient(ioTaWattClient);
|
||||
final Throwable exception = new IoTaWattClientConfigurationException(new Throwable(exceptionMessage));
|
||||
when(ioTaWattClient.fetchStatus()).thenThrow(exception);
|
||||
|
||||
// when
|
||||
service.pollDevice();
|
||||
|
||||
// then
|
||||
verify(deviceHandlerCallback).updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
exceptionMessage);
|
||||
verify(deviceHandlerCallback, never()).updateState(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pollDevice_whenEmptyResponse_updateStatusToOffline() {
|
||||
// given
|
||||
// do not set service.setIoTaWattClient(ioTaWattClient);
|
||||
|
||||
// when
|
||||
service.pollDevice();
|
||||
|
||||
// then
|
||||
verify(deviceHandlerCallback).updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR);
|
||||
verify(deviceHandlerCallback, never()).updateState(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pollDevice_whenNotInitialised_fail() throws IoTaWattClientInterruptedException,
|
||||
IoTaWattClientCommunicationException, IoTaWattClientConfigurationException, IoTaWattClientException {
|
||||
// given
|
||||
service.setIoTaWattClient(ioTaWattClient);
|
||||
final StatusResponse statusResponse = new StatusResponse(List.of(), List.of());
|
||||
when(ioTaWattClient.fetchStatus()).thenReturn(Optional.of(statusResponse));
|
||||
|
||||
// when
|
||||
service.pollDevice();
|
||||
|
||||
// then
|
||||
verify(deviceHandlerCallback).updateStatus(ThingStatus.ONLINE);
|
||||
verify(deviceHandlerCallback, never()).updateState(any(), any());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideParamsForThrowCases")
|
||||
void pollDevice_whenApiRequestThrowsInterruptedException_updateStatusAccordingly(Class<Throwable> throwableClass,
|
||||
ThingStatusDetail thingStatusDetail, boolean withErrorMessage) throws IoTaWattClientInterruptedException,
|
||||
IoTaWattClientCommunicationException, IoTaWattClientConfigurationException, IoTaWattClientException {
|
||||
// given
|
||||
final String errorMessage = "Error message";
|
||||
service.setIoTaWattClient(ioTaWattClient);
|
||||
final Throwable thrownThrowable = mock(throwableClass);
|
||||
if (withErrorMessage) {
|
||||
when(thrownThrowable.getMessage()).thenReturn(errorMessage);
|
||||
}
|
||||
when(ioTaWattClient.fetchStatus()).thenThrow(thrownThrowable);
|
||||
|
||||
// when
|
||||
service.pollDevice();
|
||||
|
||||
// then
|
||||
if (withErrorMessage) {
|
||||
verify(deviceHandlerCallback).updateStatus(ThingStatus.OFFLINE, thingStatusDetail, errorMessage);
|
||||
} else {
|
||||
verify(deviceHandlerCallback).updateStatus(ThingStatus.OFFLINE, thingStatusDetail);
|
||||
}
|
||||
verify(deviceHandlerCallback, never()).updateState(any(), any());
|
||||
}
|
||||
|
||||
private static Stream<Arguments> provideParamsForThrowCases() {
|
||||
return Stream.of(Arguments.of(IoTaWattClientInterruptedException.class, ThingStatusDetail.NOT_YET_READY, false),
|
||||
Arguments.of(IoTaWattClientCommunicationException.class, ThingStatusDetail.COMMUNICATION_ERROR, false),
|
||||
Arguments.of(IoTaWattClientConfigurationException.class, ThingStatusDetail.CONFIGURATION_ERROR, true),
|
||||
Arguments.of(IoTaWattClientException.class, ThingStatusDetail.NONE, true));
|
||||
}
|
||||
|
||||
private ChannelUID createInputChannelUID(String channelNumberStr, String channelName) {
|
||||
return new ChannelUID(thingUID, INPUT_CHANNEL_ID_PREFIX + channelNumberStr + "#" + channelName);
|
||||
}
|
||||
|
||||
private ChannelUID createOutputChannelUID(String channelNumber, String channelName) {
|
||||
return new ChannelUID(thingUID, OUTPUT_CHANNEL_ID_PREFIX + channelNumber + "#" + channelName);
|
||||
}
|
||||
|
||||
private State createState(Float value, Unit<?> unit) {
|
||||
return new QuantityType<>(value, unit);
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
{
|
||||
"stats": {
|
||||
"cyclerate": 771.9443,
|
||||
"chanrate": 32.45549,
|
||||
"starttime": 1708508856,
|
||||
"currenttime": 1710326560,
|
||||
"runseconds": 1817704,
|
||||
"stack": 24472,
|
||||
"version": "02_08_03",
|
||||
"frequency": 49.97682,
|
||||
"lowbat": false
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"channel": 0,
|
||||
"Vrms": 254.2972,
|
||||
"Hz": 50.02768,
|
||||
"phase": 0.92
|
||||
},
|
||||
{
|
||||
"channel": 1,
|
||||
"Watts": 1.42,
|
||||
"Pf": 0,
|
||||
"phase": 2.2,
|
||||
"lastphase": 1.28
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "Input_1_amps",
|
||||
"units": "Amps",
|
||||
"value": 0.106694
|
||||
},
|
||||
{
|
||||
"name": "Input_1_hz",
|
||||
"units": "Hz",
|
||||
"value": 49.96615
|
||||
},
|
||||
{
|
||||
"name": "Input_1_pf",
|
||||
"units": "PF",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"name": "Input_1_va",
|
||||
"units": "VA",
|
||||
"value": 26.28139
|
||||
},
|
||||
{
|
||||
"name": "Input_1_var",
|
||||
"units": "VAR",
|
||||
"value": 26.28139
|
||||
},
|
||||
{
|
||||
"name": "Input_1_varh",
|
||||
"units": "VARh",
|
||||
"value": 26.28139
|
||||
},
|
||||
{
|
||||
"name": "Input_1_volts",
|
||||
"units": "Volts",
|
||||
"value": 246.3257
|
||||
},
|
||||
{
|
||||
"name": "Input_1_watts",
|
||||
"units": "Watts",
|
||||
"value": 0
|
||||
}
|
||||
],
|
||||
"influx1": {
|
||||
"state": "not running"
|
||||
},
|
||||
"influx2": {
|
||||
"state": "not running"
|
||||
},
|
||||
"emoncms": {
|
||||
"state": "not running"
|
||||
},
|
||||
"pvoutput": {
|
||||
"state": "not running"
|
||||
},
|
||||
"datalogs": [
|
||||
{
|
||||
"id": "Current",
|
||||
"firstkey": 1707199250,
|
||||
"lastkey": 1710326560,
|
||||
"size": 152183040,
|
||||
"interval": 5
|
||||
},
|
||||
{
|
||||
"id": "History",
|
||||
"firstkey": 1707199260,
|
||||
"lastkey": 1710326520,
|
||||
"size": 12699648,
|
||||
"interval": 60
|
||||
}
|
||||
],
|
||||
"wifi": {
|
||||
"connecttime": 1707993027,
|
||||
"SSID": "mywifi",
|
||||
"IP": "192.168.1.2",
|
||||
"channel": 6,
|
||||
"RSSI": -60,
|
||||
"mac": "AA:BB:CC:DD:EE:AA"
|
||||
}
|
||||
}
|
@ -197,6 +197,7 @@
|
||||
<module>org.openhab.binding.ipcamera</module>
|
||||
<module>org.openhab.binding.ipobserver</module>
|
||||
<module>org.openhab.binding.intesis</module>
|
||||
<module>org.openhab.binding.iotawatt</module>
|
||||
<module>org.openhab.binding.ipp</module>
|
||||
<module>org.openhab.binding.irobot</module>
|
||||
<module>org.openhab.binding.irtrans</module>
|
||||
|
Loading…
Reference in New Issue
Block a user