diff --git a/CODEOWNERS b/CODEOWNERS
index 4bba2703041..fbe2c959274 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -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
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 839603965c4..ae6156a70c0 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -806,6 +806,11 @@
org.openhab.binding.intesis${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.iotawatt
+ ${project.version}
+ org.openhab.addons.bundlesorg.openhab.binding.ipcamera
diff --git a/bundles/org.openhab.binding.iotawatt/NOTICE b/bundles/org.openhab.binding.iotawatt/NOTICE
new file mode 100644
index 00000000000..38d625e3492
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/NOTICE
@@ -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
diff --git a/bundles/org.openhab.binding.iotawatt/README.md b/bundles/org.openhab.binding.iotawatt/README.md
new file mode 100644
index 00000000000..3f1dace0b38
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/README.md
@@ -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" }
+```
diff --git a/bundles/org.openhab.binding.iotawatt/pom.xml b/bundles/org.openhab.binding.iotawatt/pom.xml
new file mode 100644
index 00000000000..609e15590f3
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/pom.xml
@@ -0,0 +1,25 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 4.2.0-SNAPSHOT
+
+
+ org.openhab.binding.iotawatt
+
+ openHAB Add-ons :: Bundles :: IoTaWatt Binding
+
+
+
+ org.mockito
+ mockito-core
+ 5.11.0
+ test
+
+
+
diff --git a/bundles/org.openhab.binding.iotawatt/src/main/feature/feature.xml b/bundles/org.openhab.binding.iotawatt/src/main/feature/feature.xml
new file mode 100644
index 00000000000..97902fed9cc
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.iotawatt/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/IoTaWattBindingConstants.java b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/IoTaWattBindingConstants.java
new file mode 100644
index 00000000000..dced823a3e2
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/IoTaWattBindingConstants.java
@@ -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");
+}
diff --git a/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/IoTaWattConfiguration.java b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/IoTaWattConfiguration.java
new file mode 100644
index 00000000000..98647feed58
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/IoTaWattConfiguration.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/IoTaWattHandlerFactory.java b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/IoTaWattHandlerFactory.java
new file mode 100644
index 00000000000..82f74ae3685
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/IoTaWattHandlerFactory.java
@@ -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 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();
+ }
+}
diff --git a/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/client/IoTaWattClient.java b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/client/IoTaWattClient.java
new file mode 100644
index 00000000000..a6df365f878
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/client/IoTaWattClient.java
@@ -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 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();
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/client/IoTaWattClientCommunicationException.java b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/client/IoTaWattClientCommunicationException.java
new file mode 100644
index 00000000000..e882df25511
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/client/IoTaWattClientCommunicationException.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/client/IoTaWattClientConfigurationException.java b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/client/IoTaWattClientConfigurationException.java
new file mode 100644
index 00000000000..2bd5ffa9648
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/client/IoTaWattClientConfigurationException.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/client/IoTaWattClientException.java b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/client/IoTaWattClientException.java
new file mode 100644
index 00000000000..1832907eefe
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/client/IoTaWattClientException.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/client/IoTaWattClientInterruptedException.java b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/client/IoTaWattClientInterruptedException.java
new file mode 100644
index 00000000000..bbf4de1db59
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/client/IoTaWattClientInterruptedException.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/handler/FetchDataServiceProvider.java b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/handler/FetchDataServiceProvider.java
new file mode 100644
index 00000000000..dbe934eb292
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/handler/FetchDataServiceProvider.java
@@ -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);
+}
diff --git a/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/handler/HttpClientProvider.java b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/handler/HttpClientProvider.java
new file mode 100644
index 00000000000..766692bc1c9
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/handler/HttpClientProvider.java
@@ -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();
+}
diff --git a/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/handler/IoTaWattClientProvider.java b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/handler/IoTaWattClientProvider.java
new file mode 100644
index 00000000000..1c5a96492a8
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/handler/IoTaWattClientProvider.java
@@ -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);
+}
diff --git a/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/handler/IoTaWattHandler.java b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/handler/IoTaWattHandler.java
new file mode 100644
index 00000000000..77398b69a7f
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/handler/IoTaWattHandler.java
@@ -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());
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/model/IoTaWattChannelType.java b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/model/IoTaWattChannelType.java
new file mode 100644
index 00000000000..15f00272668
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/model/IoTaWattChannelType.java
@@ -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);
+ };
+ }
+}
diff --git a/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/model/StatusResponse.java b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/model/StatusResponse.java
new file mode 100644
index 00000000000..ffe08510885
--- /dev/null
+++ b/bundles/org.openhab.binding.iotawatt/src/main/java/org/openhab/binding/iotawatt/internal/model/StatusResponse.java
@@ -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 inputs, @Nullable List