diff --git a/CODEOWNERS b/CODEOWNERS
index d2acdc2d78059..0526f553154f3 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -30,8 +30,8 @@
/bundles/org.openhab.binding.anel/ @paphko
/bundles/org.openhab.binding.anthem/ @mhilbush
/bundles/org.openhab.binding.argoclima/ @mbronk
-/bundles/org.openhab.binding.asuswrt/ @wildcs
/bundles/org.openhab.binding.astro/ @gerrieg
+/bundles/org.openhab.binding.asuswrt/ @wildcs
/bundles/org.openhab.binding.atlona/ @mlobstein
/bundles/org.openhab.binding.autelis/ @digitaldan
/bundles/org.openhab.binding.automower/ @maxpg
@@ -58,6 +58,7 @@
/bundles/org.openhab.binding.boschindego/ @jofleck @jlaur
/bundles/org.openhab.binding.boschshc/ @david-pace @GerdZanker
/bundles/org.openhab.binding.bosesoundtouch/ @marvkis @tratho
+/bundles/org.openhab.binding.broadlink/ @dag81
/bundles/org.openhab.binding.broadlinkthermostat/ @flo-02-mu
/bundles/org.openhab.binding.bsblan/ @hypetsch
/bundles/org.openhab.binding.bticinosmarther/ @MrRonfo
@@ -118,6 +119,7 @@
/bundles/org.openhab.binding.fenecon/ @nixoso
/bundles/org.openhab.binding.fineoffsetweatherstation/ @Andy2003
/bundles/org.openhab.binding.flicbutton/ @pfink
+/bundles/org.openhab.binding.flume/ @jsjames
/bundles/org.openhab.binding.fmiweather/ @ssalonen
/bundles/org.openhab.binding.folderwatcher/ @goopilot
/bundles/org.openhab.binding.folding/ @fa2k
@@ -139,6 +141,7 @@
/bundles/org.openhab.binding.gpio/ @nils-bauer
/bundles/org.openhab.binding.gpstracker/ @gbicskei
/bundles/org.openhab.binding.gree/ @markus7017
+/bundles/org.openhab.binding.gridbox/ @benediktkuntz
/bundles/org.openhab.binding.groheondus/ @FlorianSW
/bundles/org.openhab.binding.groupepsa/ @arjanmels
/bundles/org.openhab.binding.growatt/ @andrewfg
@@ -159,6 +162,7 @@
/bundles/org.openhab.binding.hpprinter/ @cossey
/bundles/org.openhab.binding.http/ @J-N-K
/bundles/org.openhab.binding.hue/ @cweitkamp @andrewfg
+/bundles/org.openhab.binding.huesync/ @pgfeller
/bundles/org.openhab.binding.hydrawise/ @digitaldan
/bundles/org.openhab.binding.hyperion/ @tavalin
/bundles/org.openhab.binding.iammeter/ @lewei50
@@ -194,6 +198,7 @@
/bundles/org.openhab.binding.lgtvserial/ @fa2k
/bundles/org.openhab.binding.lgwebos/ @sprehn
/bundles/org.openhab.binding.lifx/ @wborn
+/bundles/org.openhab.binding.linktap/ @dag81
/bundles/org.openhab.binding.linky/ @clinique @lolodomo
/bundles/org.openhab.binding.linuxinput/ @t-8ch
/bundles/org.openhab.binding.liquidcheck/ @marcelGoerentz
@@ -216,6 +221,7 @@
/bundles/org.openhab.binding.meteoblue/ @9037568
/bundles/org.openhab.binding.meteofrance/ @clinique
/bundles/org.openhab.binding.meteostick/ @cdjackson
+/bundles/org.openhab.binding.metofficedatahub/ @dag81
/bundles/org.openhab.binding.mffan/ @mark-brooks-180
/bundles/org.openhab.binding.miele/ @kgoderis @jlaur
/bundles/org.openhab.binding.mielecloud/ @BjoernLange
@@ -228,6 +234,7 @@
/bundles/org.openhab.binding.modbus/ @ssalonen
/bundles/org.openhab.binding.modbus.e3dc/ @weymann
/bundles/org.openhab.binding.modbus.helioseasycontrols/ @bern77
+/bundles/org.openhab.binding.modbus.kermi/ @KaaNee
/bundles/org.openhab.binding.modbus.sbc/ @fwolter
/bundles/org.openhab.binding.modbus.stiebeleltron/ @pail23
/bundles/org.openhab.binding.modbus.studer/ @giovannimirulla
@@ -242,10 +249,11 @@
/bundles/org.openhab.binding.mqtt.homeassistant/ @antroids @ccutrer
/bundles/org.openhab.binding.mqtt.homie/ @ccutrer
/bundles/org.openhab.binding.mqtt.ruuvigateway/ @ssalonen
-/bundles/org.openhab.binding.mycroft/ @dalgwen
/bundles/org.openhab.binding.mybmw/ @ntruchsess @mherwege @martingrassl
+/bundles/org.openhab.binding.mycroft/ @dalgwen
/bundles/org.openhab.binding.mynice/ @clinique
/bundles/org.openhab.binding.mystrom/ @pail23
+/bundles/org.openhab.binding.myuplink/ @alexf2015
/bundles/org.openhab.binding.nanoleaf/ @stefan-hoehn
/bundles/org.openhab.binding.neato/ @jjlauterbach
/bundles/org.openhab.binding.neeo/ @morph166955
@@ -308,10 +316,10 @@
/bundles/org.openhab.binding.radiobrowser/ @skinah
/bundles/org.openhab.binding.radiothermostat/ @mlobstein
/bundles/org.openhab.binding.regoheatpump/ @crnjan
-/bundles/org.openhab.binding.revogi/ @andibraeu
/bundles/org.openhab.binding.remoteopenhab/ @lolodomo
/bundles/org.openhab.binding.renault/ @dougculnane
/bundles/org.openhab.binding.resol/ @ramack
+/bundles/org.openhab.binding.revogi/ @andibraeu
/bundles/org.openhab.binding.rfxcom/ @martinvw @paulianttila
/bundles/org.openhab.binding.rme/ @kgoderis
/bundles/org.openhab.binding.robonect/ @reyem
@@ -332,6 +340,7 @@
/bundles/org.openhab.binding.serial/ @MikeJMajor
/bundles/org.openhab.binding.serialbutton/ @kaikreuzer
/bundles/org.openhab.binding.shelly/ @markus7017
+/bundles/org.openhab.binding.siemenshvac/ @lo92fr
/bundles/org.openhab.binding.siemensrds/ @andrewfg
/bundles/org.openhab.binding.silvercrestwifisocket/ @jmvaz
/bundles/org.openhab.binding.sinope/ @chaton78
@@ -402,6 +411,7 @@
/bundles/org.openhab.binding.visualcrossing/ @magx2
/bundles/org.openhab.binding.vitotronic/ @steand
/bundles/org.openhab.binding.vizio/ @mlobstein
+/bundles/org.openhab.binding.volumio/ @miloit
/bundles/org.openhab.binding.volvooncall/ @Jamstah
/bundles/org.openhab.binding.warmup/ @jamesmelville
/bundles/org.openhab.binding.weathercompany/ @mhilbush
@@ -411,6 +421,7 @@
/bundles/org.openhab.binding.wemo/ @hmerk @jlaur
/bundles/org.openhab.binding.wifiled/ @openhab/add-ons-maintainers
/bundles/org.openhab.binding.windcentrale/ @marcelrv @wborn
+/bundles/org.openhab.binding.wiz/ @ccutrer @frejos
/bundles/org.openhab.binding.wlanthermo/ @CSchlipp
/bundles/org.openhab.binding.wled/ @Skinah
/bundles/org.openhab.binding.wolfsmartset/ @BoBiene
@@ -422,7 +433,6 @@
/bundles/org.openhab.binding.yamahareceiver/ @zarusz
/bundles/org.openhab.binding.yeelight/ @claell
/bundles/org.openhab.binding.yioremote/ @miloit
-/bundles/org.openhab.binding.volumio/ @miloit
/bundles/org.openhab.binding.zoneminder/ @mhilbush
/bundles/org.openhab.binding.zway/ @pathec
/bundles/org.openhab.io.homekit/ @andylintner @ccutrer @yfre
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 358238e10e565..a546e9c9cbed4 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -796,6 +796,11 @@
org.openhab.binding.hue
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.huesync
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.hydrawise
@@ -1236,6 +1241,11 @@
org.openhab.binding.mystrom
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.myuplink
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.nanoleaf
@@ -2071,6 +2081,11 @@
org.openhab.binding.windcentrale
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.wiz
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.wlanthermo
diff --git a/bundles/org.openhab.automation.groovyscripting/src/main/java/org/openhab/automation/groovyscripting/internal/GroovyScriptEngineFactory.java b/bundles/org.openhab.automation.groovyscripting/src/main/java/org/openhab/automation/groovyscripting/internal/GroovyScriptEngineFactory.java
index d2dac88609dd4..327fb0f615f73 100644
--- a/bundles/org.openhab.automation.groovyscripting/src/main/java/org/openhab/automation/groovyscripting/internal/GroovyScriptEngineFactory.java
+++ b/bundles/org.openhab.automation.groovyscripting/src/main/java/org/openhab/automation/groovyscripting/internal/GroovyScriptEngineFactory.java
@@ -51,7 +51,15 @@ public void scopeValues(ScriptEngine scriptEngine, Map scopeValu
ImportCustomizer importCustomizer = new ImportCustomizer();
for (Map.Entry entry : scopeValues.entrySet()) {
if (entry.getValue() instanceof Class> clazz) {
- importCustomizer.addImport(entry.getKey(), clazz.getCanonicalName());
+ String canonicalName = clazz.getCanonicalName();
+ try {
+ // Only add imports for classes that are available to the classloader
+ getClass().getClassLoader().loadClass(canonicalName);
+ importCustomizer.addImport(entry.getKey(), canonicalName);
+ logger.debug("Added import for {} as {}", entry.getKey(), canonicalName);
+ } catch (ClassNotFoundException e) {
+ logger.debug("Unable to add import for {} as {}", entry.getKey(), canonicalName, e);
+ }
} else {
scriptEngine.put(entry.getKey(), entry.getValue());
}
diff --git a/bundles/org.openhab.automation.jrubyscripting/pom.xml b/bundles/org.openhab.automation.jrubyscripting/pom.xml
index e2689c0172e9e..c44671a0aed84 100644
--- a/bundles/org.openhab.automation.jrubyscripting/pom.xml
+++ b/bundles/org.openhab.automation.jrubyscripting/pom.xml
@@ -15,8 +15,8 @@
openHAB Add-ons :: Bundles :: Automation :: JRuby Scripting
- com.sun.nio.*;resolution:=optional,com.sun.security.*;resolution:=optional,org.apache.tools.ant.*;resolution:=optional,org.bouncycastle.*;resolution:=optional,org.joda.*;resolution:=optional,sun.management.*;resolution:=optional,sun.nio.*;resolution:=optional,jakarta.annotation;resolution:=optional
- 9.4.8.0
+ com.sun.nio.*;resolution:=optional,com.sun.security.*;resolution:=optional,org.apache.tools.ant.*;resolution:=optional,org.bouncycastle.*;resolution:=optional,org.joda.*;resolution:=optional,sun.management.*;resolution:=optional,sun.nio.*;resolution:=optional,jakarta.annotation;resolution:=optional,jdk.crac.management;resolution:=optional
+ 9.4.9.0
diff --git a/bundles/org.openhab.automation.jsscripting/README.md b/bundles/org.openhab.automation.jsscripting/README.md
index 0063b1c4aac5c..14b3300a9d4b9 100644
--- a/bundles/org.openhab.automation.jsscripting/README.md
+++ b/bundles/org.openhab.automation.jsscripting/README.md
@@ -809,13 +809,20 @@ See [openhab-js : actions.NotificationBuilder](https://openhab.github.io/openhab
The cache namespace provides both a private and a shared cache that can be used to set and retrieve data that will be persisted between subsequent runs of the same or between scripts.
The private cache can only be accessed by the same script and is cleared when the script is unloaded.
-You can use it to store both primitives and objects, e.g. store timers or counters between subsequent runs of that script.
+You can use it to store primitives and objects, e.g. store timers or counters between subsequent runs of that script.
When a script is unloaded and its cache is cleared, all timers (see [`createTimer`](#createtimer)) stored in its private cache are automatically cancelled.
The shared cache is shared across all rules and scripts, it can therefore be accessed from any automation language.
The access to every key is tracked and the key is removed when all scripts that ever accessed that key are unloaded.
If that key stored a timer, the timer will be cancelled.
-You can use it to store **only primitives**, as storing objects is not thread-safe and can cause script execution failures.
+You can use it to store primitives and **Java** objects, e.g. store timers or counters between multiple scripts.
+
+Due to a multi-threading limitation in GraalJS (the JavaScript engine used by JavaScript Scripting), it is not recommended to store JavaScript objects in the shared cache.
+Multi-threaded access to JavaScript objects will lead to script execution failure!
+You can work-around that limitation by either serialising and deserialising JS objects or by switching to their Java counterparts.
+
+Timers as created by [`createTimer`](#createtimer) can be stored in the shared cache.
+The ids of timers and intervals as created by `setTimeout` and `setInterval` cannot be shared across scripts as these ids are local to the script where they were created.
See [openhab-js : cache](https://openhab.github.io/openhab-js/cache.html) for full API documentation.
@@ -1240,7 +1247,7 @@ Operations and conditions can also optionally take functions:
```javascript
rules.when().item("F1_light").changed().then(event => {
- console.log(event);
+ console.log(event);
}).build("Test Rule", "My Test Rule");
```
diff --git a/bundles/org.openhab.automation.jsscripting/pom.xml b/bundles/org.openhab.automation.jsscripting/pom.xml
index 2c6324d3ac2a2..7484a086d99f4 100644
--- a/bundles/org.openhab.automation.jsscripting/pom.xml
+++ b/bundles/org.openhab.automation.jsscripting/pom.xml
@@ -25,7 +25,7 @@
22.0.0.2
${project.version}
- openhab@5.8.0
+ openhab@5.8.1
diff --git a/bundles/org.openhab.binding.airgradient/README.md b/bundles/org.openhab.binding.airgradient/README.md
index 30f27be5e5d7d..d85f7780665d9 100644
--- a/bundles/org.openhab.binding.airgradient/README.md
+++ b/bundles/org.openhab.binding.airgradient/README.md
@@ -77,7 +77,7 @@ For more information about the data in the channels, please refer to the models
| tvoc | Number:Density | Read | Total Volatile Organic Compounds |
| atmp | Number:Temperature | Read | Ambient Temperature |
| rhum | Number:Dimensionless | Read | Relative Humidity Percentage |
-| wifi | Number | Read | Received signal strength indicator |
+| wifi | Number:Power | Read | Received signal strength indicator |
| uploads-since-boot | Number:Dimensionless | Read | Number of measure uploads since last reboot (boot) |
| leds | String | Read/Write | Sets the leds mode (off/co2/pm) |
| calibration | String | Write | Triggers co2 calibration on the device |
diff --git a/bundles/org.openhab.binding.airgradient/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.airgradient/src/main/resources/OH-INF/thing/thing-types.xml
index b1fd1adaa1476..45c554e34d759 100644
--- a/bundles/org.openhab.binding.airgradient/src/main/resources/OH-INF/thing/thing-types.xml
+++ b/bundles/org.openhab.binding.airgradient/src/main/resources/OH-INF/thing/thing-types.xml
@@ -72,6 +72,7 @@
+ 1
serialNumber
@@ -121,6 +122,7 @@
+ 1
location
@@ -161,7 +163,7 @@
- Number
+ Number:Power
RSSI
Received signal strength indicator
QualityOfService
diff --git a/bundles/org.openhab.binding.airgradient/src/main/resources/OH-INF/update/update.xml b/bundles/org.openhab.binding.airgradient/src/main/resources/OH-INF/update/update.xml
new file mode 100644
index 0000000000000..b4712ae87bac4
--- /dev/null
+++ b/bundles/org.openhab.binding.airgradient/src/main/resources/OH-INF/update/update.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ airgradient:wifi
+
+
+
+
+
+
+
+ airgradient:wifi
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/googletv/GoogleTVConnectionManager.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/googletv/GoogleTVConnectionManager.java
index 1ba30cb807472..0724f3f95fa33 100644
--- a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/googletv/GoogleTVConnectionManager.java
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/googletv/GoogleTVConnectionManager.java
@@ -579,7 +579,8 @@ public void connect() {
logger.trace("{} - Starting Reader Thread for {}:{}", handler.getThingID(), config.ipAddress,
config.googletvPort);
- Thread readerThread = new Thread(this::readerThreadJob, "GoogleTV reader " + handler.getThingID());
+ Thread readerThread = new Thread(this::readerThreadJob,
+ "OH-binding-" + handler.getThingUID() + "-GoogleTVReader");
readerThread.setDaemon(true);
readerThread.start();
this.readerThread = readerThread;
@@ -587,7 +588,8 @@ public void connect() {
logger.trace("{} - Starting Sender Thread for {}:{}", handler.getThingID(), config.ipAddress,
config.googletvPort);
- Thread senderThread = new Thread(this::senderThreadJob, "GoogleTV sender " + handler.getThingID());
+ Thread senderThread = new Thread(this::senderThreadJob,
+ "OH-binding-" + handler.getThingUID() + "-GoogleTVSender");
senderThread.setDaemon(true);
senderThread.start();
this.senderThread = senderThread;
@@ -698,12 +700,14 @@ public void shimInitialize() {
this.shimServerSocket = serverSocket;
this.shimQueue.clear();
- Thread readerThread = new Thread(this::shimReaderThreadJob, "GoogleTV shim reader");
+ Thread readerThread = new Thread(this::shimReaderThreadJob,
+ "OH-binding-" + handler.getThingUID() + "-GoogleTVShimReader");
readerThread.setDaemon(true);
readerThread.start();
this.shimReaderThread = readerThread;
- Thread senderThread = new Thread(this::shimSenderThreadJob, "GoogleTV shim sender");
+ Thread senderThread = new Thread(this::shimSenderThreadJob,
+ "OH-binding-" + handler.getThingUID() + "-GoogleTVShimSender");
senderThread.setDaemon(true);
senderThread.start();
this.shimSenderThread = senderThread;
diff --git a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/shieldtv/ShieldTVConnectionManager.java b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/shieldtv/ShieldTVConnectionManager.java
index 0c79a8f04c558..bf6f173a2801e 100644
--- a/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/shieldtv/ShieldTVConnectionManager.java
+++ b/bundles/org.openhab.binding.androidtv/src/main/java/org/openhab/binding/androidtv/internal/protocol/shieldtv/ShieldTVConnectionManager.java
@@ -443,12 +443,14 @@ public void connect() {
setStatus(false, "offline.initializing");
- Thread readerThread = new Thread(this::readerThreadJob, "ShieldTV reader " + handler.getThingID());
+ Thread readerThread = new Thread(this::readerThreadJob,
+ "OH-binding-" + handler.getThingUID() + "-ShieldTVReader");
readerThread.setDaemon(true);
readerThread.start();
this.readerThread = readerThread;
- Thread senderThread = new Thread(this::senderThreadJob, "ShieldTV sender " + handler.getThingID());
+ Thread senderThread = new Thread(this::senderThreadJob,
+ "OH-binding-" + handler.getThingUID() + "-ShieldTVSender");
senderThread.setDaemon(true);
senderThread.start();
this.senderThread = senderThread;
@@ -513,13 +515,13 @@ public void shimInitialize() {
this.shimServerSocket = serverSocket;
Thread readerThread = new Thread(this::shimReaderThreadJob,
- "ShieldTV shim reader " + handler.getThingID());
+ "OH-binding-" + handler.getThingUID() + "-ShieldTVShimReader");
readerThread.setDaemon(true);
readerThread.start();
this.shimReaderThread = readerThread;
Thread senderThread = new Thread(this::shimSenderThreadJob,
- "ShieldTV shim sender" + handler.getThingID());
+ "OH-binding-" + handler.getThingUID() + "-ShieldTVShimSender");
senderThread.setDaemon(true);
senderThread.start();
this.shimSenderThread = senderThread;
diff --git a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemHandler.java b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemHandler.java
index 4ea2e505322bd..24445dc02590a 100644
--- a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemHandler.java
+++ b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemHandler.java
@@ -242,12 +242,12 @@ private synchronized void connect() {
scheduleConnectRetry(reconnectIntervalMinutes);
return;
}
- Thread localReaderThread = new Thread(this::readerThreadJob, "Anthem reader");
+ Thread localReaderThread = new Thread(this::readerThreadJob, "OH-binding-" + getThing().getUID() + "-Reader");
localReaderThread.setDaemon(true);
localReaderThread.start();
this.readerThread = localReaderThread;
- Thread localSenderThread = new Thread(this::senderThreadJob, "Anthem sender");
+ Thread localSenderThread = new Thread(this::senderThreadJob, "OH-binding-" + getThing().getUID() + "-Sender");
localSenderThread.setDaemon(true);
localSenderThread.start();
this.senderThread = localSenderThread;
diff --git a/bundles/org.openhab.binding.argoclima/src/main/java/org/openhab/binding/argoclima/internal/device/passthrough/RemoteArgoApiServerStub.java b/bundles/org.openhab.binding.argoclima/src/main/java/org/openhab/binding/argoclima/internal/device/passthrough/RemoteArgoApiServerStub.java
index ce3adcbd489fd..64dd206e9aa6e 100644
--- a/bundles/org.openhab.binding.argoclima/src/main/java/org/openhab/binding/argoclima/internal/device/passthrough/RemoteArgoApiServerStub.java
+++ b/bundles/org.openhab.binding.argoclima/src/main/java/org/openhab/binding/argoclima/internal/device/passthrough/RemoteArgoApiServerStub.java
@@ -290,7 +290,7 @@ public synchronized void start() throws ArgoRemoteServerStubStartupException {
// to stop, actually)
s.setStopTimeout(1000L);
try {
- new Thread() {
+ new Thread("OH-binding-" + this.id + "-APIStub") {
@Override
public void run() {
try {
diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarConsecutiveBestPriceResult.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarConsecutiveBestPriceResult.java
index 1318348085b65..9aa80dd26d188 100644
--- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarConsecutiveBestPriceResult.java
+++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarConsecutiveBestPriceResult.java
@@ -17,6 +17,7 @@
import java.time.Instant;
import java.time.ZoneId;
+import java.util.Comparator;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -33,23 +34,45 @@ public class AwattarConsecutiveBestPriceResult extends AwattarBestPriceResult {
private final String hours;
private final ZoneId zoneId;
- public AwattarConsecutiveBestPriceResult(List prices, ZoneId zoneId) {
+ public AwattarConsecutiveBestPriceResult(List prices, int length, ZoneId zoneId) {
super();
this.zoneId = zoneId;
- StringBuilder hours = new StringBuilder();
- boolean second = false;
- for (AwattarPrice price : prices) {
+
+ // sort the prices by timerange
+ prices.sort(Comparator.comparing(AwattarPrice::timerange));
+
+ // calculate the range with the lowest accumulated price of length hours from the given prices
+ double minPrice = Double.MAX_VALUE;
+ int minIndex = 0;
+ for (int i = 0; i <= prices.size() - length; i++) {
+ double sum = 0;
+ for (int j = 0; j < length; j++) {
+ sum += prices.get(i + j).netPrice();
+ }
+ if (sum < minPrice) {
+ minPrice = sum;
+ minIndex = i;
+ }
+ }
+
+ // calculate the accumulated price and the range of the best price
+ for (int i = 0; i < length; i++) {
+ AwattarPrice price = prices.get(minIndex + i);
priceSum += price.netPrice();
- length++;
updateStart(price.timerange().start());
updateEnd(price.timerange().end());
- if (second) {
- hours.append(',');
+ }
+
+ // create a list of hours for the best price range
+ StringBuilder locHours = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ if (i > 0) {
+ locHours.append(",");
}
- hours.append(getHourFrom(price.timerange().start(), zoneId));
- second = true;
+ locHours.append(getHourFrom(prices.get(minIndex + i).timerange().start(), zoneId));
}
- this.hours = hours.toString();
+
+ this.hours = locHours.toString();
}
@Override
@@ -61,10 +84,6 @@ public boolean contains(long timestamp) {
return timestamp >= getStart() && timestamp < getEnd();
}
- public double getPriceSum() {
- return priceSum;
- }
-
@Override
public String toString() {
return String.format("{%s, %s, %.2f}", formatDate(getStart(), zoneId), formatDate(getEnd(), zoneId),
diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarNonConsecutiveBestPriceResult.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarNonConsecutiveBestPriceResult.java
index bf623d24c170b..461292b66f7e8 100644
--- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarNonConsecutiveBestPriceResult.java
+++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarNonConsecutiveBestPriceResult.java
@@ -17,6 +17,7 @@
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@@ -33,13 +34,29 @@ public class AwattarNonConsecutiveBestPriceResult extends AwattarBestPriceResult
private final ZoneId zoneId;
private boolean sorted = true;
- public AwattarNonConsecutiveBestPriceResult(ZoneId zoneId) {
+ public AwattarNonConsecutiveBestPriceResult(List prices, int length, boolean inverted,
+ ZoneId zoneId) {
super();
this.zoneId = zoneId;
members = new ArrayList<>();
+
+ prices.sort(Comparator.naturalOrder());
+
+ // sort in descending order when inverted
+ if (inverted) {
+ Collections.reverse(prices);
+ }
+
+ // take up to config.length prices
+ for (int i = 0; i < Math.min(length, prices.size()); i++) {
+ addMember(prices.get(i));
+ }
+
+ // sort the members
+ members.sort(Comparator.comparing(AwattarPrice::timerange));
}
- public void addMember(AwattarPrice member) {
+ private void addMember(AwattarPrice member) {
sorted = false;
members.add(member);
updateStart(member.timerange().start());
@@ -67,6 +84,7 @@ public String getHours() {
boolean second = false;
sort();
StringBuilder res = new StringBuilder();
+
for (AwattarPrice price : members) {
if (second) {
res.append(',');
diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestPriceHandler.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestPriceHandler.java
index 39540ae5de466..9c7e3de1883e7 100644
--- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestPriceHandler.java
+++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestPriceHandler.java
@@ -28,8 +28,6 @@
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
import java.util.List;
import java.util.SortedSet;
import java.util.concurrent.ScheduledFuture;
@@ -128,11 +126,13 @@ public void refreshChannels() {
public void refreshChannel(ChannelUID channelUID) {
State state = UnDefType.UNDEF;
Bridge bridge = getBridge();
+
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.bridge.missing");
updateState(channelUID, state);
return;
}
+
AwattarBridgeHandler bridgeHandler = (AwattarBridgeHandler) bridge.getHandler();
if (bridgeHandler == null || bridgeHandler.getPrices() == null) {
logger.debug("No prices available, so can't refresh channel.");
@@ -140,8 +140,11 @@ public void refreshChannel(ChannelUID channelUID) {
updateState(channelUID, state);
return;
}
+
+ ZoneId zoneId = bridgeHandler.getTimeZone();
+
AwattarBestPriceConfiguration config = getConfigAs(AwattarBestPriceConfiguration.class);
- TimeRange timerange = getRange(config.rangeStart, config.rangeDuration, bridgeHandler.getTimeZone());
+ TimeRange timerange = getRange(config.rangeStart, config.rangeDuration, zoneId);
if (!(bridgeHandler.containsPriceFor(timerange))) {
updateState(channelUID, state);
return;
@@ -151,36 +154,11 @@ public void refreshChannel(ChannelUID channelUID) {
List range = getPriceRange(bridgeHandler, timerange);
if (config.consecutive) {
- range.sort(Comparator.comparing(AwattarPrice::timerange));
- AwattarConsecutiveBestPriceResult res = new AwattarConsecutiveBestPriceResult(
- range.subList(0, config.length), bridgeHandler.getTimeZone());
-
- for (int i = 1; i <= range.size() - config.length; i++) {
- AwattarConsecutiveBestPriceResult res2 = new AwattarConsecutiveBestPriceResult(
- range.subList(i, i + config.length), bridgeHandler.getTimeZone());
- if (res2.getPriceSum() < res.getPriceSum()) {
- res = res2;
- }
- }
- result = res;
+ result = new AwattarConsecutiveBestPriceResult(range, config.length, zoneId);
} else {
- range.sort(Comparator.naturalOrder());
-
- // sort in descending order when inverted
- if (config.inverted) {
- Collections.reverse(range);
- }
-
- AwattarNonConsecutiveBestPriceResult res = new AwattarNonConsecutiveBestPriceResult(
- bridgeHandler.getTimeZone());
-
- // take up to config.length prices
- for (int i = 0; i < Math.min(config.length, range.size()); i++) {
- res.addMember(range.get(i));
- }
-
- result = res;
+ result = new AwattarNonConsecutiveBestPriceResult(range, config.length, config.inverted, zoneId);
}
+
String channelId = channelUID.getIdWithoutGroup();
long diff;
switch (channelId) {
diff --git a/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/AwattarBestPriceTest.java b/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/AwattarBestPriceTest.java
new file mode 100644
index 0000000000000..b180f11482a1d
--- /dev/null
+++ b/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/AwattarBestPriceTest.java
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.openhab.binding.awattar.internal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.awattar.internal.handler.TimeRange;
+
+/**
+ * The {@link AwattarBestPriceTest} contains tests for the
+ * {@link AwattarConsecutiveBestPriceResult} and {@link AwattarNonConsecutiveBestPriceResult} logic.
+ *
+ * @author Thomas Leber - Initial contribution
+ */
+public class AwattarBestPriceTest {
+
+ private ZoneId zoneId = ZoneId.of("GMT");
+
+ public static ZonedDateTime getCalendarForHour(int hour, ZoneId zone) {
+ return ZonedDateTime.ofInstant(Instant.ofEpochSecond(1731283200L), zone).truncatedTo(ChronoUnit.HOURS)
+ .plusHours(hour);
+ }
+
+ public synchronized SortedSet getPrices() {
+ SortedSet prices = new TreeSet<>(Comparator.comparing(AwattarPrice::timerange));
+
+ prices.add(new AwattarPrice(103.87, 103.87, 103.87, 103.87, new TimeRange(1731283200000L, 1731286800000L)));
+ prices.add(new AwattarPrice(100.06, 100.06, 100.06, 100.06, new TimeRange(1731286800000L, 1731290400000L)));
+ prices.add(new AwattarPrice(99.06, 99.06, 99.06, 99.06, new TimeRange(1731290400000L, 1731294000000L)));
+ prices.add(new AwattarPrice(99.12, 99.12, 99.12, 99.12, new TimeRange(1731294000000L, 1731297600000L)));
+ prices.add(new AwattarPrice(105.16, 105.16, 105.16, 105.16, new TimeRange(1731297600000L, 1731301200000L)));
+ prices.add(new AwattarPrice(124.96, 124.96, 124.96, 124.96, new TimeRange(1731301200000L, 1731304800000L)));
+ prices.add(new AwattarPrice(143.91, 143.91, 143.91, 143.91, new TimeRange(1731304800000L, 1731308400000L)));
+ prices.add(new AwattarPrice(141.95, 141.95, 141.95, 141.95, new TimeRange(1731308400000L, 1731312000000L)));
+ prices.add(new AwattarPrice(135.95, 135.95, 135.95, 135.95, new TimeRange(1731312000000L, 1731315600000L)));
+ prices.add(new AwattarPrice(130.39, 130.39, 130.39, 130.39, new TimeRange(1731315600000L, 1731319200000L)));
+ prices.add(new AwattarPrice(124.5, 124.5, 124.5, 124.5, new TimeRange(1731319200000L, 1731322800000L)));
+ prices.add(new AwattarPrice(119.79, 119.79, 119.79, 119.79, new TimeRange(1731322800000L, 1731326400000L)));
+ prices.add(new AwattarPrice(131.13, 131.13, 131.13, 131.13, new TimeRange(1731326400000L, 1731330000000L)));
+ prices.add(new AwattarPrice(133.72, 133.72, 133.72, 133.72, new TimeRange(1731330000000L, 1731333600000L)));
+ prices.add(new AwattarPrice(141.58, 141.58, 141.58, 141.58, new TimeRange(1731333600000L, 1731337200000L)));
+ prices.add(new AwattarPrice(146.94, 146.94, 146.94, 146.94, new TimeRange(1731337200000L, 1731340800000L)));
+ prices.add(new AwattarPrice(150.08, 150.08, 150.08, 150.08, new TimeRange(1731340800000L, 1731344400000L)));
+ prices.add(new AwattarPrice(146.9, 146.9, 146.9, 146.9, new TimeRange(1731344400000L, 1731348000000L)));
+ prices.add(new AwattarPrice(139.87, 139.87, 139.87, 139.87, new TimeRange(1731348000000L, 1731351600000L)));
+ prices.add(new AwattarPrice(123.78, 123.78, 123.78, 123.78, new TimeRange(1731351600000L, 1731355200000L)));
+ prices.add(new AwattarPrice(119.02, 119.02, 119.02, 119.02, new TimeRange(1731355200000L, 1731358800000L)));
+ prices.add(new AwattarPrice(116.87, 116.87, 116.87, 116.87, new TimeRange(1731358800000L, 1731362400000L)));
+ prices.add(new AwattarPrice(109.72, 109.72, 109.72, 109.72, new TimeRange(1731362400000L, 1731366000000L)));
+ prices.add(new AwattarPrice(107.89, 107.89, 107.89, 107.89, new TimeRange(1731366000000L, 1731369600000L)));
+
+ return prices;
+ }
+
+ @Test
+ void AwattarConsecutiveBestPriceResult() {
+ int length = 8;
+
+ List range = new ArrayList<>(getPrices());
+
+ range.sort(Comparator.comparing(AwattarPrice::timerange));
+ AwattarConsecutiveBestPriceResult result = new AwattarConsecutiveBestPriceResult(range, length, zoneId);
+ assertEquals("00,01,02,03,04,05,06,07", result.getHours());
+ }
+
+ @Test
+ void AwattarNonConsecutiveBestPriceResult_nonInverted() {
+ int length = 6;
+ boolean inverted = false;
+
+ List range = new ArrayList<>(getPrices());
+
+ range.sort(Comparator.comparing(AwattarPrice::timerange));
+ AwattarNonConsecutiveBestPriceResult result = new AwattarNonConsecutiveBestPriceResult(range, length, inverted,
+ zoneId);
+ assertEquals("00,01,02,03,04,23", result.getHours());
+ }
+
+ @Test
+ void AwattarNonConsecutiveBestPriceResult_inverted() {
+ int length = 4;
+ boolean inverted = true;
+
+ List range = new ArrayList<>(getPrices());
+
+ range.sort(Comparator.comparing(AwattarPrice::timerange));
+ AwattarNonConsecutiveBestPriceResult result = new AwattarNonConsecutiveBestPriceResult(range, length, inverted,
+ zoneId);
+ assertEquals("06,15,16,17", result.getHours());
+ }
+}
diff --git a/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/api/AwattarApiTest.java b/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/api/AwattarApiTest.java
index f543757746d5a..3d7d232f4d03e 100644
--- a/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/api/AwattarApiTest.java
+++ b/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/api/AwattarApiTest.java
@@ -45,8 +45,6 @@
import org.openhab.binding.awattar.internal.AwattarBridgeConfiguration;
import org.openhab.binding.awattar.internal.AwattarPrice;
import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException;
-import org.openhab.binding.awattar.internal.handler.AwattarBridgeHandler;
-import org.openhab.binding.awattar.internal.handler.AwattarBridgeHandlerTest;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.test.java.JavaTest;
diff --git a/bundles/org.openhab.binding.bluetooth/README.md b/bundles/org.openhab.binding.bluetooth/README.md
index daf6019fce898..840c98b630106 100644
--- a/bundles/org.openhab.binding.bluetooth/README.md
+++ b/bundles/org.openhab.binding.bluetooth/README.md
@@ -39,9 +39,9 @@ Other configuration parameters may be required depending on the bluetooth thing
Every Bluetooth thing has the following channel:
-| Channel ID | Item Type | Description |
-|------------|-----------|-----------------------------------------------------------------------------------------------------|
-| rssi | Number | The "Received Signal Strength Indicator", the [RSSI](https://blog.bluetooth.com/proximity-and-rssi) |
+| Channel ID | Item Type | Description |
+|------------|--------------|-----------------------------------------------------------------------------------------------------|
+| rssi | Number:Power | The "Received Signal Strength Indicator", the [RSSI](https://blog.bluetooth.com/proximity-and-rssi) |
## Full Example
@@ -54,7 +54,7 @@ bluetooth:beacon:hci0:b1 "BLE Beacon" (bluetooth:bluez:hci0) [ address="68:64:4
demo.items:
```java
-Number Beacon_RSSI "My Beacon [%.0f]" { channel="bluetooth:beacon:hci0:b1:rssi" }
+Number:Power Beacon_RSSI "My Beacon [%.0f %unit%]" { unit="dBm", channel="bluetooth:beacon:hci0:b1:rssi" }
```
demo.sitemap:
diff --git a/bundles/org.openhab.binding.bluetooth/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.bluetooth/src/main/resources/OH-INF/thing/channels.xml
index b840e0328deea..822a1cee70a5b 100644
--- a/bundles/org.openhab.binding.bluetooth/src/main/resources/OH-INF/thing/channels.xml
+++ b/bundles/org.openhab.binding.bluetooth/src/main/resources/OH-INF/thing/channels.xml
@@ -5,11 +5,11 @@
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
diff --git a/bundles/org.openhab.binding.bluetooth/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.bluetooth/src/main/resources/OH-INF/thing/thing-types.xml
index ba279f4eef34c..e628849760835 100644
--- a/bundles/org.openhab.binding.bluetooth/src/main/resources/OH-INF/thing/thing-types.xml
+++ b/bundles/org.openhab.binding.bluetooth/src/main/resources/OH-INF/thing/thing-types.xml
@@ -17,6 +17,10 @@
+
+ 1
+
+
Address
diff --git a/bundles/org.openhab.binding.bluetooth/src/main/resources/OH-INF/update/update.xml b/bundles/org.openhab.binding.bluetooth/src/main/resources/OH-INF/update/update.xml
new file mode 100644
index 0000000000000..91449818f0041
--- /dev/null
+++ b/bundles/org.openhab.binding.bluetooth/src/main/resources/OH-INF/update/update.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DiscoveryProtocol.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DiscoveryProtocol.java
index d4841f47ab3b4..1f4fe26d8dab8 100644
--- a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DiscoveryProtocol.java
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DiscoveryProtocol.java
@@ -17,6 +17,7 @@
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.broadlink.internal.BroadlinkBindingConstants;
import org.openhab.binding.broadlink.internal.BroadlinkProtocol;
import org.openhab.binding.broadlink.internal.NetworkUtils;
import org.openhab.binding.broadlink.internal.socket.BroadlinkSocket;
@@ -37,6 +38,7 @@ private static class AsyncDiscoveryThread extends Thread {
AsyncDiscoveryThread(BroadlinkSocketListener listener, long timeoutMillis,
DiscoveryFinishedListener finishedListener, Logger logger) {
+ super(String.format("OH-binding-%s-%s", BroadlinkBindingConstants.BINDING_ID, "Discovery"));
this.listener = listener;
this.timeoutMillis = timeoutMillis;
this.finishedListener = finishedListener;
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/BroadlinkSocket.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/BroadlinkSocket.java
index c7c9169e92427..4e4909c9b7ccc 100644
--- a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/BroadlinkSocket.java
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/BroadlinkSocket.java
@@ -23,6 +23,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.broadlink.internal.BroadlinkBindingConstants;
import org.openhab.binding.broadlink.internal.ModelMapper;
import org.slf4j.Logger;
@@ -99,6 +100,7 @@ private void receiveData(@Nullable MulticastSocket socket, DatagramPacket dgram)
}
private ReceiverThread(Logger logger) {
+ super(String.format("OH-binding-%s-%s", BroadlinkBindingConstants.BINDING_ID, "Receiver"));
this.logger = logger;
}
}
diff --git a/bundles/org.openhab.binding.cm11a/src/main/java/org/openhab/binding/cm11a/internal/X10Interface.java b/bundles/org.openhab.binding.cm11a/src/main/java/org/openhab/binding/cm11a/internal/X10Interface.java
index 4acc3951b19a2..5329a660ffded 100644
--- a/bundles/org.openhab.binding.cm11a/src/main/java/org/openhab/binding/cm11a/internal/X10Interface.java
+++ b/bundles/org.openhab.binding.cm11a/src/main/java/org/openhab/binding/cm11a/internal/X10Interface.java
@@ -243,7 +243,7 @@ public class X10Interface extends Thread implements SerialPortEventListener {
*
*/
public X10Interface(String serialPort, Cm11aBridgeHandler bridgeHandler) throws NoSuchPortException {
- super();
+ super("OH-binding-" + bridgeHandler.getThing().getUID());
logger.trace("**** Constructing X10Interface for serial port: {} *******", serialPort);
portId = CommPortIdentifier.getPortIdentifier(serialPort);
this.bridgeHandler = bridgeHandler;
diff --git a/bundles/org.openhab.binding.denonmarantz/src/main/java/org/openhab/binding/denonmarantz/internal/connector/telnet/DenonMarantzTelnetClientThread.java b/bundles/org.openhab.binding.denonmarantz/src/main/java/org/openhab/binding/denonmarantz/internal/connector/telnet/DenonMarantzTelnetClientThread.java
index 69ac85b926e7d..dae1c97731540 100644
--- a/bundles/org.openhab.binding.denonmarantz/src/main/java/org/openhab/binding/denonmarantz/internal/connector/telnet/DenonMarantzTelnetClientThread.java
+++ b/bundles/org.openhab.binding.denonmarantz/src/main/java/org/openhab/binding/denonmarantz/internal/connector/telnet/DenonMarantzTelnetClientThread.java
@@ -22,6 +22,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.denonmarantz.internal.DenonMarantzBindingConstants;
import org.openhab.binding.denonmarantz.internal.config.DenonMarantzConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -54,6 +55,7 @@ public class DenonMarantzTelnetClientThread extends Thread {
private @Nullable BufferedReader in;
public DenonMarantzTelnetClientThread(DenonMarantzConfiguration config, DenonMarantzTelnetListener listener) {
+ super(String.format("OH-binding-%s-%s", DenonMarantzBindingConstants.BINDING_ID, "TelnetClient"));
logger.debug("Denon listener created");
this.config = config;
this.listener = listener;
diff --git a/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/DigiplexBindingConstants.java b/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/DigiplexBindingConstants.java
index 1e9522497c1a5..22d3e1ff6b010 100644
--- a/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/DigiplexBindingConstants.java
+++ b/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/DigiplexBindingConstants.java
@@ -31,7 +31,7 @@
@NonNullByDefault
public class DigiplexBindingConstants {
- private static final String BINDING_ID = "digiplex";
+ public static final String BINDING_ID = "digiplex";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
diff --git a/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/handler/DigiplexBridgeHandler.java b/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/handler/DigiplexBridgeHandler.java
index f805eba944d1f..95a7771987b27 100644
--- a/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/handler/DigiplexBridgeHandler.java
+++ b/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/handler/DigiplexBridgeHandler.java
@@ -31,6 +31,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.digiplex.internal.DigiplexBindingConstants;
import org.openhab.binding.digiplex.internal.DigiplexBridgeConfiguration;
import org.openhab.binding.digiplex.internal.communication.CommunicationStatus;
import org.openhab.binding.digiplex.internal.communication.DigiplexMessageHandler;
@@ -303,7 +304,7 @@ private class DigiplexReceiverThread extends Thread {
private final InputStream stream;
DigiplexReceiverThread(InputStream stream) {
- super("DigiplexReceiveThread");
+ super(String.format("OH-binding-%s-%s", DigiplexBindingConstants.BINDING_ID, "Receiver"));
this.stream = stream;
}
diff --git a/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/listener/DoorbirdUdpListener.java b/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/listener/DoorbirdUdpListener.java
index 5fb5a9266d6f0..c8b8bfd64fc4e 100644
--- a/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/listener/DoorbirdUdpListener.java
+++ b/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/listener/DoorbirdUdpListener.java
@@ -22,6 +22,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.doorbird.internal.DoorbirdBindingConstants;
import org.openhab.binding.doorbird.internal.api.DoorbirdSession;
import org.openhab.binding.doorbird.internal.handler.DoorbellHandler;
import org.slf4j.Logger;
@@ -58,6 +59,7 @@ public class DoorbirdUdpListener extends Thread {
private long lastDataTime;
public DoorbirdUdpListener(DoorbellHandler thingHandler) {
+ super(String.format("OH-binding-%s-%s", DoorbirdBindingConstants.BINDING_ID, "UdpListener"));
this.thingHandler = thingHandler;
}
diff --git a/bundles/org.openhab.binding.draytonwiser/README.md b/bundles/org.openhab.binding.draytonwiser/README.md
index c97d01dfd5144..e42686e333006 100644
--- a/bundles/org.openhab.binding.draytonwiser/README.md
+++ b/bundles/org.openhab.binding.draytonwiser/README.md
@@ -52,7 +52,7 @@ The `awaySetPoint` defines the temperature in degrees Celsius that will be sent
| `heatChannel1DemandState` | Switch | Is channel 1 calling the boiler for heat |
| `heatChannel2Demand` | Number:Dimensionless | Current demand level of heating channel 2 |
| `heatChannel2DemandState` | Switch | Is channel 2 calling the boiler for heat |
-| `currentSignalRSSI` | Number | Relative Signal Strength Indicator |
+| `currentSignalRSSI` | Number:Power | Relative Signal Strength Indicator |
| `currentWiserSignalStrength` | String | Human readable signal strength |
| `currentSignalStrength` | Number | Signal strength value that maps to qualityofservice icon |
@@ -87,7 +87,7 @@ The `awaySetPoint` defines the temperature in degrees Celsius that will be sent
| `currentBatteryVoltage` | Number:ElectricPotential | Currently reported battery voltage |
| `currentWiserBatteryLevel` | String | Human readable battery level |
| `currentBatteryLevel` | Number | Battery level in percent |
-| `currentSignalRSSI` | Number | Relative Signal Strength Indicator |
+| `currentSignalRSSI` | Number:Power | Relative Signal Strength Indicator |
| `currentSignalLQI` | Number | Link Quality Indicator |
| `currentWiserSignalStrength` | String | Human readable signal strength |
| `currentSignalStrength` | Number | Signal strength value that maps to qualityofservice icon |
@@ -103,7 +103,7 @@ The `awaySetPoint` defines the temperature in degrees Celsius that will be sent
| `currentBatteryVoltage` | Number:ElectricPotential | Currently reported battery voltage |
| `currentWiserBatteryLevel` | String | Human readable battery level |
| `currentBatteryLevel` | Number | Battery level in percent |
-| `currentSignalRSSI` | Number | Relative Signal Strength Indicator |
+| `currentSignalRSSI` | Number:Power | Relative Signal Strength Indicator |
| `currentSignalLQI` | Number | Link Quality Indicator |
| `currentWiserSignalStrength` | String | Human readable signal strength |
| `currentSignalStrength` | Number | Signal strength value that maps to qualityofservice icon |
@@ -113,7 +113,7 @@ The `awaySetPoint` defines the temperature in degrees Celsius that will be sent
| Channel | Item Type | Description |
|--------------------------|---------------|--------------------------------------------|
-| `currentSignalRSSI` | Number | Relative Signal Strength Indicator |
+| `currentSignalRSSI` | Number:Power | Relative Signal Strength Indicator |
| `currentSignalLQI` | Number | Link Quality Indicator |
| `zigbeeConnected` | Switch | Is the TRV joined to network |
| `plugInstantaneousPower` | Number:Power | Current Power being drawn through the plug |
diff --git a/bundles/org.openhab.binding.draytonwiser/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.draytonwiser/src/main/resources/OH-INF/thing/thing-types.xml
index 6fafad1aaee7b..2ca034e72201b 100644
--- a/bundles/org.openhab.binding.draytonwiser/src/main/resources/OH-INF/thing/thing-types.xml
+++ b/bundles/org.openhab.binding.draytonwiser/src/main/resources/OH-INF/thing/thing-types.xml
@@ -65,6 +65,9 @@
+
+ 1
+
id
@@ -158,6 +161,9 @@
+
+ 1
+
serialNumber
@@ -192,6 +198,9 @@
+
+ 1
+
serialNumber
@@ -223,6 +232,9 @@
+
+ 1
+
serialNumber
@@ -294,10 +306,10 @@
diff --git a/bundles/org.openhab.binding.draytonwiser/src/main/resources/OH-INF/update/update.xml b/bundles/org.openhab.binding.draytonwiser/src/main/resources/OH-INF/update/update.xml
new file mode 100644
index 0000000000000..7e9b122120a57
--- /dev/null
+++ b/bundles/org.openhab.binding.draytonwiser/src/main/resources/OH-INF/update/update.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.dsmr/src/main/java/org/openhab/binding/dsmr/internal/device/connector/DSMRErrorStatus.java b/bundles/org.openhab.binding.dsmr/src/main/java/org/openhab/binding/dsmr/internal/device/connector/DSMRErrorStatus.java
index 3d14c32afaa94..a197777038b33 100644
--- a/bundles/org.openhab.binding.dsmr/src/main/java/org/openhab/binding/dsmr/internal/device/connector/DSMRErrorStatus.java
+++ b/bundles/org.openhab.binding.dsmr/src/main/java/org/openhab/binding/dsmr/internal/device/connector/DSMRErrorStatus.java
@@ -44,6 +44,10 @@ public enum DSMRErrorStatus {
* Serial port doesn't support the configured settings.
*/
PORT_NOT_COMPATIBLE(true),
+ /**
+ * Serial port time out or illegal state.
+ */
+ PORT_PORT_TIMEOUT(false),
/**
* Reading data from the serial port failed.
*/
diff --git a/bundles/org.openhab.binding.dsmr/src/main/java/org/openhab/binding/dsmr/internal/device/connector/DSMRSerialConnector.java b/bundles/org.openhab.binding.dsmr/src/main/java/org/openhab/binding/dsmr/internal/device/connector/DSMRSerialConnector.java
index 550325e00ca2c..5e7dbb014f36d 100644
--- a/bundles/org.openhab.binding.dsmr/src/main/java/org/openhab/binding/dsmr/internal/device/connector/DSMRSerialConnector.java
+++ b/bundles/org.openhab.binding.dsmr/src/main/java/org/openhab/binding/dsmr/internal/device/connector/DSMRSerialConnector.java
@@ -172,6 +172,10 @@ public void open(final DSMRSerialSettings portSettings) {
logger.warn("Possible bug because a new serial port value was set during opening new port.");
errorStatus = DSMRErrorStatus.PORT_INTERNAL_ERROR;
}
+ } catch (final IllegalStateException ise) {
+ logger.debug("Failed communicating, probably time out", ise);
+
+ errorStatus = DSMRErrorStatus.PORT_PORT_TIMEOUT;
} catch (final IOException ioe) {
logger.debug("Failed to get inputstream for serialPort", ioe);
diff --git a/bundles/org.openhab.binding.easee/src/main/resources/OH-INF/thing/easee-readonly-channel-types.xml b/bundles/org.openhab.binding.easee/src/main/resources/OH-INF/thing/easee-readonly-channel-types.xml
index eaf3c73ff9450..9c4cabe19c2f7 100644
--- a/bundles/org.openhab.binding.easee/src/main/resources/OH-INF/thing/easee-readonly-channel-types.xml
+++ b/bundles/org.openhab.binding.easee/src/main/resources/OH-INF/thing/easee-readonly-channel-types.xml
@@ -4,7 +4,7 @@
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">
|
+
+## Example Configuration
+
+### huesyncbox.things
+
+```java
+Thing huesync:box:LivingRoom "Philips Hue HDMI Sync Box, LivingRoom" [
+ host="192.168.2.115",
+ httpPollingInterval=60,
+ apiAccessToken="yourTokenGoesHere=",
+ registrationId="8",
+ port=443,
+ statusUpdateInterval=10
+]
+```
+
+### huesyncbox.items
+
+Both item and sitemap configuration example use the `iconify` support for the `firmware` as well as `input1` and `input2`.
+Those icons loaded if needed from the internet and not suited for a pure offline setup.
+The other items use the `classic` icons.
+Read the documentation about the offline provider for `iconify` icons, or use the `classic` icons bundled with openHAB.
+
+```java
+// Firmware
+Group firmware "Firmware Information"
+String firmware_version "Current Firmware" (firmware) {channel="huesync:box:LivingRoom:device-firmware#firmware"}
+String latest_firmware_version "Latest Firmware" (firmware) {channel="huesync:box:LivingRoom:device-firmware#available-firmware"}
+
+//HDMI Input 1
+Group hdmi_in1 "HDMI 1"
+String friendly_name_input1 "Friendly Name" (hdmi_in1) {channel="huesync:box:LivingRoom:device-hdmi-in-1#name"}
+String friendly_type_input1 "Friendly Type" (hdmi_in1) {channel="huesync:box:LivingRoom:device-hdmi-in-1#type"}
+String hdmi_connection_status_input1 "Connection Status" (hdmi_in1) {channel="huesync:box:LivingRoom:device-hdmi-in-1#status"}
+String last_sync_mode_input1 "Last Sync Mode " (hdmi_in1) {channel="huesync:box:LivingRoom:device-hdmi-in-1#mode"}
+
+//HDMI Input 2
+Group hdmi_in2
+String friendly_name_input2 "Friendly Name" {channel="huesync:box:LivingRoom:device-hdmi-in-2#name"}
+String friendly_type_input2 "Friendly Type" {channel="huesync:box:LivingRoom:device-hdmi-in-2#type"}
+String hdmi_connection_status_input2 "Connection Status" {channel="huesync:box:LivingRoom:device-hdmi-in-2#status"}
+String last_sync_mode_input2 "Last Sync Mode" {channel="huesync:box:LivingRoom:device-hdmi-in-2#mode"}
+
+//HDMI Input 3
+String friendly_name_input3 "Friendly Name" {channel="huesync:box:LivingRoom:device-hdmi-in-3#name"}
+String friendly_type_input3 "Friendly Type" {channel="huesync:box:LivingRoom:device-hdmi-in-3#type"}
+String hdmi_connection_status_input3 "Connection Status" {channel="huesync:box:LivingRoom:device-hdmi-in-3#status"}
+String last_sync_mode_input3 "Last Sync Mode" {channel="huesync:box:LivingRoom:device-hdmi-in-3#mode"}
+
+//HDMI Input 4
+String friendly_name_input4 "Friendly Name" {channel="huesync:box:LivingRoom:device-hdmi-in-4#name"}
+String friendly_type_input4 "Friendly Type" {channel="huesync:box:LivingRoom:device-hdmi-in-4#type"}
+String hdmi_connection_status_input4 "Connection Status" {channel="huesync:box:LivingRoom:device-hdmi-in-4#status"}
+String last_sync_mode_input4 "Last Sync Mode" {channel="huesync:box:LivingRoom:device-hdmi-in-4#mode"}
+
+
+//HDMI output
+String friendly_name_output "Friendly Name" {channel="huesync:box:LivingRoom:device-hdmi-out#name"}
+String friendly_type_output "Friendly Type" {channel="huesync:box:LivingRoom:device-hdmi-out#type"}
+String hdmi_connection_status_output "Connection Status" {channel="huesync:box:LivingRoom:device-hdmi-out#status"}
+String last_sync_mode_output "Last Sync Mode" {channel="huesync:box:LivingRoom:device-hdmi-out#mode"}
+
+//Commands
+String huesync_mode "Mode" {channel="huesync:box:LivingRoom:device-commands#mode"}
+Switch sync_active "Sync active" {channel="huesync:box:LivingRoom:device-commands#sync-active"}
+Switch hdmi_active "HDMI active" {channel="huesync:box:LivingRoom:device-commands#hdmi-active"}
+String hdmi_source "HDMI Source" {channel="huesync:box:LivingRoom:device-commands#hdmi-source"}
+Dimmer huesync_brightness "Brightness" {channel="huesync:box:LivingRoom:device-commands#brightness"}
+
+```
+
+### example.sitemap
+
+```java
+sitemap demo label="Hue Sync Box" {
+ Frame {
+ Group item=firmware
+ }
+ Frame label="Commands" icon=settings {
+ Text item=huesync_mode
+ Text item=hdmi_active
+ Switch item=sync_active
+ Text item=hdmi_source
+ Buttongrid label="HDMI Source" staticIcon=player {
+ Button row=1 column=1 item=hdmi_source label="Source 1" stateless click=input1
+ Button row=2 column=1 item=hdmi_source label="Source 2" stateless click=input2
+ Button row=3 column=1 item=hdmi_source label="Source 3" stateless click=input3
+ Button row=4 column=1 item=hdmi_source label="Source 4" stateless click=input4
+ }
+ Selection item=hdmi_source mappings=[input1="Source 1", input2="Source 2", input3="Source 3", input3="Source 4"]
+
+ Slider item=huesync_brightness minValue=0 maxValue=200 step=10
+ }
+ Frame label="HDMI Inputs 1 & 2" icon="iconify:mdi:hdmi-port" {
+ Default item=hdmi_in1
+
+ Group item=hdmi_in2 label="HDMI 2" icon="iconify:mdi:hdmi-port" {
+ Default item=friendly_name_input2 icon="iconify:mdi:text"
+ Default item=friendly_type_input2 icon="iconify:mdi:devices"
+ Default item=hdmi_connection_status_input2 icon="iconify:mdi:connection"
+ Default item=last_sync_mode_input2 icon="iconify:mdi:multimedia"
+ }
+ }
+ Frame label="HDMI 3" icon=player {
+ Default item=friendly_name_input3
+ Default item=friendly_type_input3
+ Default item=hdmi_connection_status_input3
+ Default item=last_sync_mode_input3
+ }
+ Frame label="HDMI 4" icon=player {
+ Text item=friendly_name_input4
+ Text item=friendly_type_input4
+ Text item=hdmi_connection_status_input4
+ Text item=last_sync_mode_input4
+ }
+```
diff --git a/bundles/org.openhab.binding.huesync/doc/device_registration.png b/bundles/org.openhab.binding.huesync/doc/device_registration.png
new file mode 100644
index 0000000000000..1afa074091b4f
Binary files /dev/null and b/bundles/org.openhab.binding.huesync/doc/device_registration.png differ
diff --git a/bundles/org.openhab.binding.huesync/pom.xml b/bundles/org.openhab.binding.huesync/pom.xml
new file mode 100644
index 0000000000000..7ca10a423d798
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 4.3.0-SNAPSHOT
+
+
+ org.openhab.binding.huesync
+
+ openHAB Add-ons :: Bundles :: Hue Sync Box Binding
+
+
diff --git a/bundles/org.openhab.binding.huesync/src/main/feature/feature.xml b/bundles/org.openhab.binding.huesync/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..3d292e7237ad3
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/feature/feature.xml
@@ -0,0 +1,10 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ openhab-transport-mdns
+ mvn:org.openhab.addons.bundles/org.openhab.binding.huesync/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/HdmiChannels.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/HdmiChannels.java
new file mode 100644
index 0000000000000..cb3355b1293d5
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/HdmiChannels.java
@@ -0,0 +1,34 @@
+/**
+ * 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.huesync.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * @author Patrik Gfeller - Initial contribution
+ */
+@NonNullByDefault
+public class HdmiChannels {
+ public String name;
+ public String type;
+ public String mode;
+ public String status;
+
+ public HdmiChannels(String name, String type, String mode, String status) {
+ this.name = name;
+ this.type = type;
+ this.mode = mode;
+ this.status = status;
+ }
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/HueSyncConstants.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/HueSyncConstants.java
new file mode 100644
index 0000000000000..2c59c18b75936
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/HueSyncConstants.java
@@ -0,0 +1,89 @@
+/**
+ * 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.huesync.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link HueSyncConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Patrik Gfeller - Initial contribution
+ */
+@NonNullByDefault
+public class HueSyncConstants {
+ public static class ENDPOINTS {
+ public static final String DEVICE = "device";
+ public static final String REGISTRATIONS = "registrations";
+ public static final String HDMI = "hdmi";
+ public static final String EXECUTION = "execution";
+
+ public static class COMMANDS {
+ public static final String MODE = "mode";
+ public static final String SYNC = "syncActive";
+ public static final String HDMI = "hdmiActive";
+ public static final String SOURCE = "hdmiSource";
+ public static final String BRIGHTNESS = "brightness";
+ }
+ }
+
+ public static class CHANNELS {
+ public static class DEVICE {
+ public static class INFORMATION {
+ public static final String FIRMWARE = "device-firmware#firmware";
+ public static final String FIRMWARE_AVAILABLE = "device-firmware#available-firmware";
+ }
+ }
+
+ public static class COMMANDS {
+ public static final String MODE = "device-commands#mode";
+ public static final String SYNC = "device-commands#sync-active";
+ public static final String HDMI = "device-commands#hdmi-active";
+ public static final String SOURCE = "device-commands#hdmi-source";
+ public static final String BRIGHTNESS = "device-commands#brightness";
+ }
+
+ public static class HDMI {
+ public static final HdmiChannels IN_1 = new HdmiChannels("device-hdmi-in-1#name", "device-hdmi-in-1#type",
+ "device-hdmi-in-1#mode", "device-hdmi-in-1#status");
+ public static final HdmiChannels IN_2 = new HdmiChannels("device-hdmi-in-2#name", "device-hdmi-in-2#type",
+ "device-hdmi-in-2#mode", "device-hdmi-in-2#status");
+ public static final HdmiChannels IN_3 = new HdmiChannels("device-hdmi-in-3#name", "device-hdmi-in-3#type",
+ "device-hdmi-in-3#mode", "device-hdmi-in-3#status");
+ public static final HdmiChannels IN_4 = new HdmiChannels("device-hdmi-in-4#name", "device-hdmi-in-4#type",
+ "device-hdmi-in-4#mode", "device-hdmi-in-4#status");
+
+ public static final HdmiChannels OUT = new HdmiChannels("device-hdmi-out#name", "device-hdmi-out#type",
+ "device-hdmi-out#mode", "device-hdmi-out#status");
+ }
+ }
+
+ public static final String APPLICATION_NAME = "openHAB";
+
+ /** Minimal API Version required. Only apiLevel >= 7 is supported. */
+ public static final Integer MINIMAL_API_VERSION = 7;
+
+ public static final String BINDING_ID = "huesync";
+ public static final String THING_TYPE_ID = "box";
+ public static final ThingTypeUID THING_TYPE_UID = new ThingTypeUID(BINDING_ID, THING_TYPE_ID);
+
+ public static final String PARAMETER_HOST = "host";
+ public static final String PARAMETER_PORT = "port";
+
+ public static final Integer REGISTRATION_INITIAL_DELAY = 3;
+ public static final Integer REGISTRATION_INTERVAL = 1;
+
+ public static final String REGISTRATION_ID = "registrationId";
+ public static final String API_TOKEN = "apiAccessToken";
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/device/HueSyncDevice.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/device/HueSyncDevice.java
new file mode 100644
index 0000000000000..57a5ce5b3ed63
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/device/HueSyncDevice.java
@@ -0,0 +1,66 @@
+/**
+ * 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.huesync.internal.api.dto.device;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * HDMI Sync Box Device Information
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ *
+ * @see Hue
+ * HDMI Sync Box API
+ */
+@NonNullByDefault
+public class HueSyncDevice {
+ /** Friendly name of the device */
+ public @Nullable String name;
+ /** Device Type identifier */
+ public @Nullable String deviceType;
+ /**
+ * Capitalized hex string of the 6 byte / 12 characters device id without
+ * delimiters. Used as unique id on label, certificate common name, hostname
+ * etc.
+ */
+ public @Nullable String uniqueId;
+ /**
+ * Increased between firmware versions when api changes. Only apiLevel >= 7 is
+ * supported.
+ */
+ public int apiLevel = 0;
+ /**
+ * User readable version of the device firmware, starting with decimal major
+ * .minor .maintenance format e.g. “1.12.3”
+ */
+ public @Nullable String firmwareVersion;
+ /**
+ * Build number of the firmware. Unique for every build with newer builds
+ * guaranteed a higher number than older.
+ */
+ public int buildNumber = 0;
+
+ public boolean termsAgreed;
+
+ /** uninitialized, disconnected, lan, wan */
+ public @Nullable String wifiState;
+ public @Nullable String ipAddress;
+
+ public @Nullable HueSyncDeviceCapabilitiesInfo capabilities;
+
+ public boolean beta;
+ public boolean overheating;
+ public boolean bluetooth;
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/device/HueSyncDeviceCapabilitiesInfo.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/device/HueSyncDeviceCapabilitiesInfo.java
new file mode 100644
index 0000000000000..8049f1557dbf4
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/device/HueSyncDeviceCapabilitiesInfo.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.huesync.internal.api.dto.device;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * HDMI Sync Box Device Information Capabilities
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ *
+ * @see Hue
+ * HDMI Sync Box API
+ */
+@NonNullByDefault
+public class HueSyncDeviceCapabilitiesInfo {
+ /** The total number of IR codes configurable */
+ public int maxIrCodes;
+ /** The total number of Presets configurable */
+ public int maxPresets;
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/device/HueSyncDeviceDetailed.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/device/HueSyncDeviceDetailed.java
new file mode 100644
index 0000000000000..12bb1ff358c6d
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/device/HueSyncDeviceDetailed.java
@@ -0,0 +1,57 @@
+/**
+ * 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.huesync.internal.api.dto.device;
+
+import java.util.Date;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * HDMI Sync Box Device Information - Extended information (only available
+ * to registered clients)
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ *
+ * @see Hue
+ * HDMI Sync Box API
+ */
+@NonNullByDefault
+public class HueSyncDeviceDetailed extends HueSyncDevice {
+ public @Nullable HueSyncDeviceDetailedWifiInfo wifi;
+ public @Nullable HueSyncDeviceDetailedUpdateInfo update;
+
+ /** UTC time when last check for update was performed. */
+ public @Nullable Date lastCheckedUpdate;
+ /**
+ * Build number that is available to update to. Item is set to null when there
+ * is no update available.
+ */
+ public int updatableBuildNumber;
+ /**
+ * User readable version of the firmware the device can upgrade to. Item is set
+ * to null when there is no update available.
+ */
+ public @Nullable String updatableFirmwareVersion;
+ /**
+ * 1 = regular;
+ * 0 = off in powersave, passthrough or sync mode;
+ * 2 = dimmed in powersave or passthrough mode and off in sync mode
+ */
+ public int ledMode = -1;
+
+ /** none, doSoftwareRestart, doFirmwareUpdate */
+ public @Nullable String action;
+ public @Nullable String pushlink;
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/device/HueSyncDeviceDetailedUpdateInfo.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/device/HueSyncDeviceDetailedUpdateInfo.java
new file mode 100644
index 0000000000000..73c99bb466b24
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/device/HueSyncDeviceDetailedUpdateInfo.java
@@ -0,0 +1,40 @@
+/**
+ * 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.huesync.internal.api.dto.device;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * HDMI Sync Box Device Information - Automatic Firmware update
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ *
+ * @see Hue
+ * HDMI Sync Box API
+ */
+@NonNullByDefault
+public class HueSyncDeviceDetailedUpdateInfo {
+ /**
+ * Sync Box checks daily for a firmware update. If true, an available update
+ * will automatically be installed. This will be postponed if Sync Box is
+ * passing through content to the TV and being used.
+ */
+ public boolean autoUpdateEnabled;
+ /**
+ * TC hour when the automatic update will check and execute, values 0 – 23.
+ * Default is 10. Ideally this value should be set to 3AM according to user’s
+ * timezone.
+ */
+ public int autoUpdateTime;
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/device/HueSyncDeviceDetailedWifiInfo.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/device/HueSyncDeviceDetailedWifiInfo.java
new file mode 100644
index 0000000000000..0c99f86acb5eb
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/device/HueSyncDeviceDetailedWifiInfo.java
@@ -0,0 +1,39 @@
+/**
+ * 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.huesync.internal.api.dto.device;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * HDMI Sync Box Device Information - Wifi connection information
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ *
+ * @see Hue
+ * HDMI Sync Box API
+ */
+@NonNullByDefault
+public class HueSyncDeviceDetailedWifiInfo {
+ /** Wifi SSID */
+ public @Nullable String ssid;
+ /**
+ * 0 = not connected;
+ * 1 = weak;
+ * 2 = fair;
+ * 3 = good;
+ * 4 = excellent
+ */
+ public int strength;
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/execution/HueSyncExecution.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/execution/HueSyncExecution.java
new file mode 100644
index 0000000000000..88a51b78b0052
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/execution/HueSyncExecution.java
@@ -0,0 +1,102 @@
+/**
+ * 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.huesync.internal.api.dto.execution;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Root object for execution resource
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ *
+ */
+@NonNullByDefault
+public class HueSyncExecution {
+ private final Logger logger = LoggerFactory.getLogger(HueSyncExecution.class);
+
+ public static final List KNOWN_MODES = Collections
+ .unmodifiableList(Arrays.asList("powersave", "passthrough", "video", "game", "music"));
+
+ private @Nullable String mode;
+
+ /**
+ *
+ * @return powersave, passthrough, video, game, music
+ */
+ @JsonProperty("mode")
+ public @Nullable String getMode() {
+ return this.mode;
+ }
+
+ /**
+ *
+ * @apiNote More modes can be added in the future, so clients must gracefully
+ * handle modes they don’t recognize. If an unknown mode is received, a
+ * warning will be logged and mode will fallback to "unknown"
+ *
+ * @param mode powersave, passthrough, video, game, music
+ */
+ public void setMode(String mode) {
+ if (!HueSyncExecution.KNOWN_MODES.contains(mode)) {
+ logger.warn(
+ "device mode [{}] is not known by this version of the binding. Please open an issue to notify the maintainer(s). Fallback will be used. ",
+ mode);
+ }
+
+ this.mode = HueSyncExecution.KNOWN_MODES.contains(mode) ? mode : "unknown";
+ }
+
+ /**
+ * Reports `false` in case of `powersave` or `passthrough` mode, and `true` in case of `video`, `game`, or `music`
+ * mode.
+ * When changed from false to true, it will start syncing in last used mode for current source.
+ * When changed from true to false, will set passthrough mode.
+ */
+ public boolean syncActive;
+ /**
+ * Reports `false` in case of `powersave mode`, and true in case of `passthrough`, `video`, `game`, `music` mode.
+ * When changed from false to true, it will set passthrough mode. When changed from `true` to `false`, will set
+ * powersave mode.
+ */
+ public boolean hdmiActive;
+
+ /**
+ * Currently selected hdmi input: `input1`, `input2`, `input3,` `input4`
+ */
+ public @Nullable String hdmiSource;
+
+ public @Nullable String hueTarget;
+ public @Nullable String lastSyncMode;
+ public @Nullable String preset;
+
+ /**
+ * brightness:
+ * - Get, Put
+ * - number, uint
+ * - 0 ... 200 (100 = no brightness reduction/boost compared to input, 0 = max reduction, 200 = max boost)
+ */
+ public int brightness;
+
+ public @Nullable HueSyncExecutionVideo video;
+ public @Nullable HueSyncExecutionGame game;
+ public @Nullable HueSyncExecutionMusic music;
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/execution/HueSyncExecutionGame.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/execution/HueSyncExecutionGame.java
new file mode 100644
index 0000000000000..f806d2efa425c
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/execution/HueSyncExecutionGame.java
@@ -0,0 +1,28 @@
+/**
+ * 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.huesync.internal.api.dto.execution;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ *
+ */
+@NonNullByDefault
+public class HueSyncExecutionGame {
+ public @Nullable String intensity;
+
+ public boolean backgroundLighting;
+}
diff --git a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/IHttpClientProvider.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/execution/HueSyncExecutionMusic.java
similarity index 68%
rename from bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/IHttpClientProvider.java
rename to bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/execution/HueSyncExecutionMusic.java
index 1698126e1e49b..d56a92782d4d5 100644
--- a/bundles/org.openhab.binding.vesync/src/main/java/org/openhab/binding/vesync/internal/api/IHttpClientProvider.java
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/execution/HueSyncExecutionMusic.java
@@ -10,17 +10,18 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.vesync.internal.api;
+package org.openhab.binding.huesync.internal.api.dto.execution;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
-import org.eclipse.jetty.client.HttpClient;
/**
- * @author David Goodyear - Initial contribution
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ *
*/
@NonNullByDefault
-public interface IHttpClientProvider {
- @Nullable
- HttpClient getHttpClient();
+public class HueSyncExecutionMusic {
+ public @Nullable String intensity;
+ public @Nullable String palette;
}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/execution/HueSyncExecutionVideo.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/execution/HueSyncExecutionVideo.java
new file mode 100644
index 0000000000000..46cb01e574a91
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/execution/HueSyncExecutionVideo.java
@@ -0,0 +1,28 @@
+/**
+ * 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.huesync.internal.api.dto.execution;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ *
+ */
+@NonNullByDefault
+public class HueSyncExecutionVideo {
+ public @Nullable String intensity;
+
+ public boolean backgroundLighting;
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/hdmi/HueSyncHdmi.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/hdmi/HueSyncHdmi.java
new file mode 100644
index 0000000000000..d4457528b4339
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/hdmi/HueSyncHdmi.java
@@ -0,0 +1,39 @@
+/**
+ * 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.huesync.internal.api.dto.hdmi;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ *
+ */
+@NonNullByDefault
+public class HueSyncHdmi {
+ public @Nullable HueSyncHdmiConnectionInfo input1;
+ public @Nullable HueSyncHdmiConnectionInfo input2;
+ public @Nullable HueSyncHdmiConnectionInfo input3;
+ public @Nullable HueSyncHdmiConnectionInfo input4;
+
+ public @Nullable HueSyncHdmiConnectionInfo output;
+
+ /** x @ – */
+ public @Nullable String contentSpecs;
+
+ /** Current content specs supported for video sync (video/game mode) */
+ public boolean videoSyncSupported;
+ /** Current content specs supported for audio sync (music mode) */
+ public boolean audioSyncSupported;
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/hdmi/HueSyncHdmiConnectionInfo.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/hdmi/HueSyncHdmiConnectionInfo.java
new file mode 100644
index 0000000000000..1dac4c657e80c
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/hdmi/HueSyncHdmiConnectionInfo.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.huesync.internal.api.dto.hdmi;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ *
+ */
+@NonNullByDefault
+public class HueSyncHdmiConnectionInfo {
+ /** Friendly name, not empty */
+ public @Nullable String name;
+ /**
+ * Friendly type:
+ * generic,
+ * video,
+ * game,
+ * music,
+ * xbox,
+ * playstation,
+ * nintendoswitch,
+ * phone,
+ * desktop,
+ * laptop,
+ * appletv,
+ * roku,
+ * shield,
+ * chromecast,
+ * firetv,
+ * diskplayer,
+ * settopbox,
+ * satellite,
+ * avreceiver,
+ * soundbar,
+ * hdmiswitch
+ */
+ public @Nullable String type;
+ /**
+ * unplugged,
+ * plugged,
+ * linked,
+ * unknown
+ */
+ public @Nullable String status;
+ /**
+ * video,
+ * game,
+ * music
+ */
+ public @Nullable String lastSyncMode;
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/registration/HueSyncRegistration.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/registration/HueSyncRegistration.java
new file mode 100644
index 0000000000000..89b2343dab412
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/registration/HueSyncRegistration.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.huesync.internal.api.dto.registration;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ */
+@NonNullByDefault
+public class HueSyncRegistration {
+ public String registrationId = "";
+ public String accessToken = "";
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/registration/HueSyncRegistrationRequest.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/registration/HueSyncRegistrationRequest.java
new file mode 100644
index 0000000000000..d7651a6872f89
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/api/dto/registration/HueSyncRegistrationRequest.java
@@ -0,0 +1,28 @@
+/**
+ * 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.huesync.internal.api.dto.registration;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ */
+@NonNullByDefault
+public class HueSyncRegistrationRequest {
+ /** User recognizable name of registered application */
+ public @Nullable String appName;
+ /** User recognizable name of application instance. */
+ public @Nullable String instanceName;
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/config/HueSyncConfiguration.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/config/HueSyncConfiguration.java
new file mode 100644
index 0000000000000..08c87729b2117
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/config/HueSyncConfiguration.java
@@ -0,0 +1,29 @@
+/**
+ * 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.huesync.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Binding configuration parameters,
+ *
+ * @author Patrik Gfeller - Initial contribution
+ */
+@NonNullByDefault
+public class HueSyncConfiguration {
+ public String registrationId = "";
+ public String apiAccessToken = "";
+ public String host = "";
+ public Integer port = 443;
+ public Integer statusUpdateInterval = 10;
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncAuthenticationResult.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncAuthenticationResult.java
new file mode 100644
index 0000000000000..d33081d86afb4
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncAuthenticationResult.java
@@ -0,0 +1,52 @@
+/**
+ * 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.huesync.internal.connection;
+
+import java.net.URI;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.Authentication.Result;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpHeader;
+
+/**
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ */
+@NonNullByDefault
+public class HueSyncAuthenticationResult implements Result {
+ private final String token;
+ private final URI uri;
+
+ public HueSyncAuthenticationResult(URI uri, String token) {
+ this.uri = uri;
+ this.token = token;
+ }
+
+ public String getToken() {
+ return this.token;
+ }
+
+ @Override
+ public URI getURI() {
+ return this.uri;
+ }
+
+ @Override
+ public void apply(@Nullable Request request) {
+ if (request != null && !request.getHeaders().contains(HttpHeader.AUTHORIZATION)) {
+ request.header(HttpHeader.AUTHORIZATION, "Bearer " + this.token);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncConnection.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncConnection.java
new file mode 100644
index 0000000000000..afc670b364420
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncConnection.java
@@ -0,0 +1,248 @@
+/**
+ * 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.huesync.internal.connection;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.cert.CertificateException;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+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.HttpResponseException;
+import org.eclipse.jetty.client.api.AuthenticationStore;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.MimeTypes;
+import org.openhab.binding.huesync.internal.HueSyncConstants.ENDPOINTS;
+import org.openhab.binding.huesync.internal.exceptions.HueSyncConnectionException;
+import org.openhab.core.io.net.http.TlsTrustManagerProvider;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceRegistration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ */
+@NonNullByDefault
+public class HueSyncConnection {
+ public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ /**
+ * Request format: The Sync Box API can be accessed locally via HTTPS on root level (port 443,
+ * /api/v1), resource level /api/v1/ and in some cases sub-resource level
+ * /api/v1//.
+ */
+ private static final String REQUEST_FORMAT = "https://%s:%s/%s/%s";
+ private static final String API = "api/v1";
+ private final Logger logger = LoggerFactory.getLogger(HueSyncConnection.class);
+
+ private final Integer port;
+ private final String host;
+
+ private final ServiceRegistration> tlsProviderService;
+ private final HttpClient httpClient;
+ private final URI deviceUri;
+
+ private Optional authentication = Optional.empty();
+
+ protected String registrationId = "";
+
+ public HueSyncConnection(HttpClient httpClient, String host, Integer port)
+ throws CertificateException, IOException, URISyntaxException {
+ this.host = host;
+ this.port = port;
+
+ this.deviceUri = new URI(String.format("https://%s:%s", this.host, this.port));
+
+ HueSyncTrustManagerProvider trustManagerProvider = new HueSyncTrustManagerProvider(this.host, this.port);
+ BundleContext context = FrameworkUtil.getBundle(getClass()).getBundleContext();
+
+ this.tlsProviderService = context.registerService(TlsTrustManagerProvider.class.getName(), trustManagerProvider,
+ null);
+ this.httpClient = httpClient;
+ }
+
+ public void updateAuthentication(String id, String token) {
+ this.removeAuthentication();
+
+ if (!id.isBlank() && !token.isBlank()) {
+ this.registrationId = id;
+
+ this.authentication = Optional.of(new HueSyncAuthenticationResult(this.deviceUri, token));
+ this.httpClient.getAuthenticationStore().addAuthenticationResult(this.authentication.get());
+ }
+ }
+
+ // #region protected
+ protected @Nullable T executeRequest(HttpMethod method, String endpoint, String payload,
+ @Nullable Class type) {
+ try {
+ return this.processedResponse(this.executeRequest(method, endpoint, payload), type);
+ } catch (ExecutionException e) {
+ this.handleExecutionException(e);
+ } catch (InterruptedException | TimeoutException e) {
+ this.logger.warn("{}", e.getMessage());
+ }
+
+ return null;
+ }
+
+ protected @Nullable T executeGetRequest(String endpoint, Class type) {
+ try {
+ return this.processedResponse(this.executeGetRequest(endpoint), type);
+ } catch (ExecutionException e) {
+ this.handleExecutionException(e);
+ } catch (InterruptedException | TimeoutException e) {
+ this.logger.warn("{}", e.getMessage());
+ }
+
+ return null;
+ }
+
+ protected boolean isRegistered() {
+ return this.authentication.isPresent();
+ }
+
+ protected void unregisterDevice() {
+ if (this.isRegistered()) {
+ try {
+ String endpoint = ENDPOINTS.REGISTRATIONS + "/" + this.registrationId;
+ ContentResponse response = this.executeRequest(HttpMethod.DELETE, endpoint);
+
+ if (response.getStatus() == HttpStatus.OK_200) {
+ this.removeAuthentication();
+ }
+ } catch (InterruptedException | TimeoutException | ExecutionException e) {
+ this.logger.warn("{}", e.getMessage());
+ }
+ }
+ }
+
+ protected void dispose() {
+ this.tlsProviderService.unregister();
+ }
+ // #endregion
+
+ // #region private
+ private @Nullable T processedResponse(Response response, @Nullable Class type) {
+ int status = response.getStatus();
+ try {
+ /*
+ * 400 Invalid State: Registration in progress
+ *
+ * 401 Authentication failed: If credentials are missing or invalid, errors out. If
+ * credentials are missing, continues on to GET only the Configuration state when
+ * unauthenticated, to allow for device identification.
+ *
+ * 404 Invalid URI Path: Accessing URI path which is not supported
+ *
+ * 500 Internal: Internal errors like out of memory
+ */
+ switch (status) {
+ case HttpStatus.OK_200 -> {
+ return (type != null && (response instanceof ContentResponse))
+ ? this.deserialize(((ContentResponse) response).getContentAsString(), type)
+ : null;
+ }
+ case HttpStatus.BAD_REQUEST_400 -> this.logger.debug("registration in progress: no token received yet");
+ case HttpStatus.UNAUTHORIZED_401 -> {
+ this.authentication = Optional.empty();
+ throw new HueSyncConnectionException("@text/connection.invalid-login");
+ }
+ case HttpStatus.NOT_FOUND_404 -> this.logger.warn("invalid device URI or API endpoint");
+ case HttpStatus.INTERNAL_SERVER_ERROR_500 -> this.logger.warn("hue sync box server problem");
+ default -> this.logger.warn("unexpected HTTP status: {}", status);
+ }
+ } catch (HueSyncConnectionException e) {
+ this.logger.warn("{}", e.getMessage());
+ }
+ return null;
+ }
+
+ private @Nullable T deserialize(String json, Class type) {
+ try {
+ return OBJECT_MAPPER.readValue(json, type);
+ } catch (JsonProcessingException | NoClassDefFoundError e) {
+ this.logger.error("{}", e.getMessage());
+
+ return null;
+ }
+ }
+
+ private ContentResponse executeRequest(HttpMethod method, String endpoint)
+ throws InterruptedException, TimeoutException, ExecutionException {
+ return this.executeRequest(method, endpoint, "");
+ }
+
+ private ContentResponse executeGetRequest(String endpoint)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ String uri = String.format(REQUEST_FORMAT, this.host, this.port, API, endpoint);
+
+ return httpClient.GET(uri);
+ }
+
+ private ContentResponse executeRequest(HttpMethod method, String endpoint, String payload)
+ throws InterruptedException, TimeoutException, ExecutionException {
+ String uri = String.format(REQUEST_FORMAT, this.host, this.port, API, endpoint);
+
+ Request request = this.httpClient.newRequest(uri).method(method);
+
+ this.logger.trace("uri: {}", uri);
+ this.logger.trace("method: {}", method);
+ this.logger.trace("payload: {}", payload);
+
+ if (!payload.isBlank()) {
+ request.header(HttpHeader.CONTENT_TYPE, MimeTypes.Type.APPLICATION_JSON_UTF_8.toString())
+ .content(new StringContentProvider(payload));
+ }
+
+ return request.send();
+ }
+
+ private void handleExecutionException(ExecutionException e) {
+ this.logger.warn("{}", e.getMessage());
+
+ Throwable cause = e.getCause();
+ if (cause != null && cause instanceof HttpResponseException) {
+ processedResponse(((HttpResponseException) cause).getResponse(), null);
+ }
+ }
+
+ private void removeAuthentication() {
+ AuthenticationStore store = this.httpClient.getAuthenticationStore();
+ store.clearAuthenticationResults();
+ this.httpClient.setAuthenticationStore(store);
+
+ this.registrationId = "";
+ this.authentication = Optional.empty();
+ }
+
+ // #endregion
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncDeviceConnection.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncDeviceConnection.java
new file mode 100644
index 0000000000000..c6e590dcc5db0
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncDeviceConnection.java
@@ -0,0 +1,197 @@
+/**
+ * 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.huesync.internal.connection;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.security.cert.CertificateException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.huesync.internal.HueSyncConstants;
+import org.openhab.binding.huesync.internal.HueSyncConstants.ENDPOINTS;
+import org.openhab.binding.huesync.internal.api.dto.device.HueSyncDevice;
+import org.openhab.binding.huesync.internal.api.dto.device.HueSyncDeviceDetailed;
+import org.openhab.binding.huesync.internal.api.dto.execution.HueSyncExecution;
+import org.openhab.binding.huesync.internal.api.dto.hdmi.HueSyncHdmi;
+import org.openhab.binding.huesync.internal.api.dto.registration.HueSyncRegistration;
+import org.openhab.binding.huesync.internal.api.dto.registration.HueSyncRegistrationRequest;
+import org.openhab.binding.huesync.internal.config.HueSyncConfiguration;
+import org.openhab.binding.huesync.internal.exceptions.HueSyncConnectionException;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+/**
+ * Handles the connection to a Hue HDMI Sync Box using the official API.
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ */
+@NonNullByDefault
+public class HueSyncDeviceConnection {
+ private final Logger logger = LoggerFactory.getLogger(HueSyncDeviceConnection.class);
+
+ private final HueSyncConnection connection;
+
+ private final Map> deviceCommandExecutors = new HashMap<>();
+
+ public HueSyncDeviceConnection(HttpClient httpClient, HueSyncConfiguration configuration)
+ throws CertificateException, IOException, URISyntaxException {
+ this.connection = new HueSyncConnection(httpClient, configuration.host, configuration.port);
+
+ registerCommandHandlers();
+ }
+
+ // #region private
+
+ private void registerCommandHandlers() {
+ this.deviceCommandExecutors.put(HueSyncConstants.CHANNELS.COMMANDS.MODE,
+ defaultHandler(HueSyncConstants.ENDPOINTS.COMMANDS.MODE));
+ this.deviceCommandExecutors.put(HueSyncConstants.CHANNELS.COMMANDS.SOURCE,
+ defaultHandler(HueSyncConstants.ENDPOINTS.COMMANDS.SOURCE));
+ this.deviceCommandExecutors.put(HueSyncConstants.CHANNELS.COMMANDS.BRIGHTNESS,
+ defaultHandler(HueSyncConstants.ENDPOINTS.COMMANDS.BRIGHTNESS));
+ this.deviceCommandExecutors.put(HueSyncConstants.CHANNELS.COMMANDS.SYNC,
+ defaultHandler(HueSyncConstants.ENDPOINTS.COMMANDS.SYNC));
+ this.deviceCommandExecutors.put(HueSyncConstants.CHANNELS.COMMANDS.HDMI,
+ defaultHandler(HueSyncConstants.ENDPOINTS.COMMANDS.HDMI));
+ }
+
+ private Consumer defaultHandler(String endpoint) {
+ return command -> {
+ execute(endpoint, command);
+ };
+ }
+
+ private void execute(String key, Command command) {
+ this.logger.debug("Command executor: {} - {}", key, command);
+
+ if (!this.connection.isRegistered()) {
+ this.logger.warn("Device is not registered - ignoring command: {}", command);
+ return;
+ }
+
+ String value;
+
+ if (command instanceof QuantityType quantityCommand) {
+ value = Integer.toString(quantityCommand.intValue());
+ } else if (command instanceof OnOffType) {
+ value = command == OnOffType.ON ? "true" : "false";
+ } else if (command instanceof StringType) {
+ value = '"' + command.toString() + '"';
+ } else {
+ this.logger.warn("Type [{}] not supported by this connection", command.getClass().getCanonicalName());
+ return;
+ }
+
+ String json = String.format("{ \"%s\": %s }", key, value);
+
+ this.connection.executeRequest(HttpMethod.PUT, ENDPOINTS.EXECUTION, json, null);
+ }
+
+ // #endregion
+
+ public void executeCommand(Channel channel, Command command) {
+ String uid = channel.getUID().getAsString();
+ String commandId = channel.getUID().getId();
+
+ this.logger.debug("Channel UID: {} - Command: {}", uid, command.toFullString());
+
+ if (RefreshType.REFRESH.equals(command)) {
+ return;
+ }
+
+ if (this.deviceCommandExecutors.containsKey(commandId)) {
+ Objects.requireNonNull(this.deviceCommandExecutors.get(commandId)).accept(command);
+ } else {
+ this.logger.error("No executor registered for command {} - please report this as an issue", commandId);
+ }
+ }
+
+ public @Nullable HueSyncDevice getDeviceInfo() {
+ return this.connection.executeGetRequest(ENDPOINTS.DEVICE, HueSyncDevice.class);
+ }
+
+ public @Nullable HueSyncDeviceDetailed getDetailedDeviceInfo() {
+ return this.connection.isRegistered()
+ ? this.connection.executeRequest(HttpMethod.GET, ENDPOINTS.DEVICE, "", HueSyncDeviceDetailed.class)
+ : null;
+ }
+
+ public @Nullable HueSyncHdmi getHdmiInfo() {
+ return this.connection.isRegistered()
+ ? this.connection.executeRequest(HttpMethod.GET, ENDPOINTS.HDMI, "", HueSyncHdmi.class)
+ : null;
+ }
+
+ public @Nullable HueSyncExecution getExecutionInfo() {
+ return this.connection.isRegistered()
+ ? this.connection.executeRequest(HttpMethod.GET, ENDPOINTS.EXECUTION, "", HueSyncExecution.class)
+ : null;
+ }
+
+ public @Nullable HueSyncRegistration registerDevice(String id) throws HueSyncConnectionException {
+ if (!id.isBlank()) {
+ try {
+ HueSyncRegistrationRequest dto = new HueSyncRegistrationRequest();
+ dto.appName = HueSyncConstants.APPLICATION_NAME;
+ dto.instanceName = id;
+
+ String payload = HueSyncConnection.OBJECT_MAPPER.writeValueAsString(dto);
+
+ HueSyncRegistration registration = this.connection.executeRequest(HttpMethod.POST,
+ ENDPOINTS.REGISTRATIONS, payload, HueSyncRegistration.class);
+ if (registration != null) {
+ this.connection.updateAuthentication(id, registration.accessToken);
+
+ return registration;
+ }
+ } catch (JsonProcessingException e) {
+ this.logger.warn("{}", e.getMessage());
+ }
+ }
+ return null;
+ }
+
+ public boolean isRegistered() {
+ return this.connection.isRegistered();
+ }
+
+ public void unregisterDevice() {
+ this.connection.unregisterDevice();
+ }
+
+ public void dispose() {
+ this.connection.dispose();
+ }
+
+ public void updateConfiguration(HueSyncConfiguration config) {
+ this.logger.debug("Connection configuration update for device {}:{} - Registration Id [{}]", config.host,
+ config.port, config.registrationId);
+
+ this.connection.updateAuthentication(config.registrationId, config.apiAccessToken);
+ }
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncTrustManagerProvider.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncTrustManagerProvider.java
new file mode 100644
index 0000000000000..a6ad57a9dcefb
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/connection/HueSyncTrustManagerProvider.java
@@ -0,0 +1,52 @@
+/**
+ * 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.huesync.internal.connection;
+
+import java.io.IOException;
+import java.security.cert.CertificateException;
+
+import javax.net.ssl.X509ExtendedTrustManager;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.io.net.http.PEMTrustManager;
+import org.openhab.core.io.net.http.TlsTrustManagerProvider;
+
+/**
+ * Provides a {@link PEMTrustManager} to allow secure connections to a Hue HDMI
+ * Sync Box
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ */
+@NonNullByDefault
+public class HueSyncTrustManagerProvider implements TlsTrustManagerProvider {
+ private final String host;
+ private final Integer port;
+
+ private final X509ExtendedTrustManager trustManager;
+
+ public HueSyncTrustManagerProvider(String host, Integer port) throws IOException, CertificateException {
+ this.trustManager = PEMTrustManager.getInstanceFromServer("https://" + host);
+ this.port = port;
+ this.host = host;
+ }
+
+ @Override
+ public String getHostName() {
+ return this.host + ":" + this.port;
+ }
+
+ @Override
+ public X509ExtendedTrustManager getTrustManager() {
+ return this.trustManager;
+ }
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/discovery/HueSyncDiscoveryParticipant.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/discovery/HueSyncDiscoveryParticipant.java
new file mode 100644
index 0000000000000..2365a98b173dc
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/discovery/HueSyncDiscoveryParticipant.java
@@ -0,0 +1,143 @@
+/**
+ * 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.huesync.internal.discovery;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jmdns.ServiceInfo;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.huesync.internal.HueSyncConstants;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link HueSyncDiscoveryParticipant} is responsible for discovering
+ * the remote huesync.boxes using mDNS discovery service.
+ *
+ * @author Patrik Gfeller - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = MDNSDiscoveryParticipant.class, configurationPid = "mdnsdiscovery.huesync")
+public class HueSyncDiscoveryParticipant implements MDNSDiscoveryParticipant {
+ private final Logger logger = LoggerFactory.getLogger(HueSyncDiscoveryParticipant.class);
+
+ /**
+ *
+ * Match the hostname + identifier of the discovered huesync-box.
+ * Input is like "HueSyncBox-XXXXXXXXXXXX._huesync._tcp.local."
+ *
+ * @see·
+ * Service·Name·and·Transport·Protocol·Port·Number·Registry
+ */
+ private static final String SERVICE_TYPE = "_huesync._tcp.local.";
+
+ private boolean autoDiscoveryEnabled = true;
+
+ protected final ThingRegistry thingRegistry;
+
+ @Activate
+ public HueSyncDiscoveryParticipant(final @Reference ThingRegistry thingRegistry) {
+ this.thingRegistry = thingRegistry;
+ }
+
+ @Override
+ public Set getSupportedThingTypeUIDs() {
+ return Collections.singleton(HueSyncConstants.THING_TYPE_UID);
+ }
+
+ @Override
+ public String getServiceType() {
+ return SERVICE_TYPE;
+ }
+
+ @Override
+ public @Nullable DiscoveryResult createResult(ServiceInfo service) {
+ if (this.autoDiscoveryEnabled) {
+ ThingUID uid = getThingUID(service);
+ if (uid != null) {
+ try {
+ logger.debug("HDMI Sync Box {} discovered at {}:{}", service.getName(),
+ service.getHostAddresses()[0], service.getPort());
+
+ Map properties = new HashMap<>();
+
+ properties.put(HueSyncConstants.PARAMETER_HOST, service.getHostAddresses()[0]);
+ properties.put(HueSyncConstants.PARAMETER_PORT, service.getPort());
+
+ return DiscoveryResultBuilder.create(uid).withLabel(service.getName()).withProperties(properties)
+ .build();
+ } catch (Exception e) {
+ logger.debug("Unable to query device information for {}: {}", service.getQualifiedName(),
+ e.getMessage());
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public @Nullable ThingUID getThingUID(ServiceInfo service) {
+ String id = service.getName();
+ String[] addresses = service.getHostAddresses();
+
+ if (addresses.length == 0 || id == null || id.isBlank()) {
+ logger.debug("Incomplete mDNS device discovery information - {} ignored.",
+ id == null ? "[name: null]" : id);
+ return null;
+ }
+
+ return new ThingUID(HueSyncConstants.THING_TYPE_UID, id);
+ }
+
+ @Activate
+ protected void activate(ComponentContext componentContext) {
+ updateService(componentContext);
+ }
+
+ @Modified
+ protected void modified(ComponentContext componentContext) {
+ updateService(componentContext);
+ }
+
+ private void updateService(ComponentContext componentContext) {
+ String autoDiscoveryPropertyValue = (String) componentContext.getProperties()
+ .get(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY);
+
+ if (autoDiscoveryPropertyValue != null && !autoDiscoveryPropertyValue.isBlank()) {
+ boolean value = Boolean.parseBoolean(autoDiscoveryPropertyValue);
+ if (value != this.autoDiscoveryEnabled) {
+ logger.debug("{} update: {} - {}", DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY,
+ autoDiscoveryPropertyValue, value);
+ this.autoDiscoveryEnabled = value;
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncApiException.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncApiException.java
new file mode 100644
index 0000000000000..c4096dfec7dfe
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncApiException.java
@@ -0,0 +1,28 @@
+/**
+ * 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.huesync.internal.exceptions;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * @author Patrik Gfeller - Initial contribution
+ */
+@NonNullByDefault
+public class HueSyncApiException extends HueSyncException {
+ private static final long serialVersionUID = 0L;
+
+ public HueSyncApiException(String message) {
+ super(message);
+ }
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncConnectionException.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncConnectionException.java
new file mode 100644
index 0000000000000..b42393814da1c
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncConnectionException.java
@@ -0,0 +1,28 @@
+/**
+ * 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.huesync.internal.exceptions;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * @author Patrik Gfeller - Initial contribution
+ */
+@NonNullByDefault
+public class HueSyncConnectionException extends HueSyncException {
+ private static final long serialVersionUID = 0L;
+
+ public HueSyncConnectionException(String message) {
+ super(message);
+ }
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncException.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncException.java
new file mode 100644
index 0000000000000..583d169e8b814
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncException.java
@@ -0,0 +1,30 @@
+/**
+ * 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.huesync.internal.exceptions;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.huesync.internal.i18n.HueSyncLocalizer;
+
+/**
+ * Base class for all HueSyncExceptions
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ */
+@NonNullByDefault
+public abstract class HueSyncException extends Exception {
+ private static final long serialVersionUID = 0L;
+
+ public HueSyncException(String message) {
+ super(message.startsWith("@text") ? HueSyncLocalizer.getResourceString(message) : message);
+ }
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncTaskException.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncTaskException.java
new file mode 100644
index 0000000000000..c3c288ff7bbd5
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/exceptions/HueSyncTaskException.java
@@ -0,0 +1,28 @@
+/**
+ * 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.huesync.internal.exceptions;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * @author Patrik Gfeller - Initial contribution
+ */
+@NonNullByDefault
+public class HueSyncTaskException extends HueSyncException {
+ private static final long serialVersionUID = 0L;
+
+ public HueSyncTaskException(String message) {
+ super(message);
+ }
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/factory/HueSyncHandlerFactory.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/factory/HueSyncHandlerFactory.java
new file mode 100644
index 0000000000000..5ce343649ba1f
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/factory/HueSyncHandlerFactory.java
@@ -0,0 +1,79 @@
+/**
+ * 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.huesync.internal.factory;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.security.cert.CertificateException;
+import java.util.Collections;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.huesync.internal.HueSyncConstants;
+import org.openhab.binding.huesync.internal.handler.HueSyncHandler;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link HueSyncHandlerFactory} is responsible for creating things and
+ * thing
+ * handlers.
+ *
+ * @author Patrik Gfeller - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.huesync", service = ThingHandlerFactory.class)
+public class HueSyncHandlerFactory extends BaseThingHandlerFactory {
+
+ private final HttpClientFactory httpClientFactory;
+ private final Logger logger = LoggerFactory.getLogger(HueSyncHandlerFactory.class);
+
+ @Activate
+ public HueSyncHandlerFactory(@Reference final HttpClientFactory httpClientFactory) throws Exception {
+ this.httpClientFactory = httpClientFactory;
+ }
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Collections
+ .singleton(HueSyncConstants.THING_TYPE_UID);
+
+ @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 (HueSyncConstants.THING_TYPE_UID.equals(thingTypeUID)) {
+ try {
+ return new HueSyncHandler(thing, this.httpClientFactory);
+ } catch (IOException | URISyntaxException | CertificateException e) {
+ this.logger.warn("It was not possible to create a handler for {}: {}", thingTypeUID.getId(),
+ e.getMessage());
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/HueSyncHandler.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/HueSyncHandler.java
new file mode 100644
index 0000000000000..22ba24b596328
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/HueSyncHandler.java
@@ -0,0 +1,349 @@
+/**
+ * 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.huesync.internal.handler;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.security.cert.CertificateException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.huesync.internal.HdmiChannels;
+import org.openhab.binding.huesync.internal.HueSyncConstants;
+import org.openhab.binding.huesync.internal.api.dto.device.HueSyncDevice;
+import org.openhab.binding.huesync.internal.api.dto.device.HueSyncDeviceDetailed;
+import org.openhab.binding.huesync.internal.api.dto.execution.HueSyncExecution;
+import org.openhab.binding.huesync.internal.api.dto.hdmi.HueSyncHdmi;
+import org.openhab.binding.huesync.internal.api.dto.hdmi.HueSyncHdmiConnectionInfo;
+import org.openhab.binding.huesync.internal.api.dto.registration.HueSyncRegistration;
+import org.openhab.binding.huesync.internal.config.HueSyncConfiguration;
+import org.openhab.binding.huesync.internal.connection.HueSyncDeviceConnection;
+import org.openhab.binding.huesync.internal.exceptions.HueSyncApiException;
+import org.openhab.binding.huesync.internal.handler.tasks.HueSyncRegistrationTask;
+import org.openhab.binding.huesync.internal.handler.tasks.HueSyncUpdateTask;
+import org.openhab.binding.huesync.internal.handler.tasks.HueSyncUpdateTaskResult;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+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.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link HueSyncHandler} is responsible for handling commands, which are sent to one of the
+ * channels.
+ *
+ * @author Patrik Gfeller - Initial contribution
+ */
+@NonNullByDefault
+public class HueSyncHandler extends BaseThingHandler {
+ private static final String REGISTER = "Registration";
+ private static final String POLL = "Update";
+
+ private static final String PROPERTY_API_VERSION = "apiVersion";
+
+ private final Logger logger = LoggerFactory.getLogger(HueSyncHandler.class);
+
+ Map> tasks = new HashMap<>();
+
+ private Optional deviceInfo = Optional.empty();
+
+ private final HueSyncDeviceConnection connection;
+ private final HttpClient httpClient;
+
+ public HueSyncHandler(Thing thing, HttpClientFactory httpClientFactory)
+ throws CertificateException, IOException, URISyntaxException {
+ super(thing);
+
+ this.httpClient = httpClientFactory.getCommonHttpClient();
+
+ this.connection = new HueSyncDeviceConnection(this.httpClient, this.getConfigAs(HueSyncConfiguration.class));
+ }
+
+ // #region private
+ private Runnable initializeConnection() {
+ return () -> {
+ this.deviceInfo = Optional.ofNullable(this.connection.getDeviceInfo());
+ this.deviceInfo.ifPresent(info -> {
+ setProperty(Thing.PROPERTY_SERIAL_NUMBER, info.uniqueId != null ? info.uniqueId : "");
+ setProperty(Thing.PROPERTY_MODEL_ID, info.deviceType);
+ setProperty(Thing.PROPERTY_FIRMWARE_VERSION, info.firmwareVersion);
+
+ setProperty(HueSyncHandler.PROPERTY_API_VERSION, String.format("%d", info.apiLevel));
+
+ try {
+ this.checkCompatibility();
+ } catch (HueSyncApiException e) {
+ this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+ } finally {
+ this.startTasks();
+ }
+ });
+ };
+ }
+
+ private void stopTask(@Nullable ScheduledFuture> task) {
+ if (task == null || task.isCancelled() || task.isDone()) {
+ return;
+ }
+
+ task.cancel(true);
+ }
+
+ private @Nullable ScheduledFuture> executeTask(Runnable task, long initialDelay, long interval) {
+ return scheduler.scheduleWithFixedDelay(task, initialDelay, interval, TimeUnit.SECONDS);
+ }
+
+ private void startTasks() {
+ this.stopTasks();
+
+ this.connection.updateConfiguration(this.getConfigAs(HueSyncConfiguration.class));
+
+ Runnable task = null;
+ String id = this.connection.isRegistered() ? POLL : REGISTER;
+
+ this.logger.debug("startTasks - [{}]", id);
+
+ long initialDelay = 0;
+ long interval = 0;
+
+ switch (id) {
+ case POLL -> {
+ initialDelay = 0;
+ interval = this.getConfigAs(HueSyncConfiguration.class).statusUpdateInterval;
+
+ this.updateStatus(ThingStatus.ONLINE);
+
+ task = new HueSyncUpdateTask(this.connection, this.deviceInfo.get(),
+ deviceStatus -> this.handleUpdate(deviceStatus));
+ }
+ case REGISTER -> {
+ initialDelay = HueSyncConstants.REGISTRATION_INITIAL_DELAY;
+ interval = HueSyncConstants.REGISTRATION_INTERVAL;
+
+ this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+ "@text/thing.config.huesync.box.registration");
+
+ task = new HueSyncRegistrationTask(this.connection, this.deviceInfo.get(),
+ registration -> this.handleRegistration(registration));
+ }
+ }
+
+ if (task != null) {
+ logger.debug("Starting task [{}]", id);
+ this.tasks.put(id, this.executeTask(task, initialDelay, interval));
+ }
+ }
+
+ private void stopTasks() {
+ logger.debug("Stopping {} task(s): {}", this.tasks.values().size(), String.join(",", this.tasks.keySet()));
+
+ this.tasks.values().forEach(task -> this.stopTask(task));
+ this.tasks.clear();
+
+ this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+ "@text/thing.config.huesync.box.registration");
+ }
+
+ private void handleUpdate(@Nullable HueSyncUpdateTaskResult dto) {
+ try {
+ HueSyncUpdateTaskResult update = Optional.ofNullable(dto).get();
+
+ try {
+ this.updateFirmwareInformation(Optional.ofNullable(update.deviceStatus).get());
+ } catch (NoSuchElementException e) {
+ this.logMissingUpdateInformation("device");
+ }
+
+ this.updateHdmiInformation(Optional.ofNullable(update.hdmiStatus).get());
+ this.updateExecutionInformation(Optional.ofNullable(update.execution).get());
+ } catch (NoSuchElementException e) {
+ Configuration configuration = this.editConfiguration();
+
+ configuration.put(HueSyncConstants.REGISTRATION_ID, "");
+ configuration.put(HueSyncConstants.API_TOKEN, "");
+
+ this.updateConfiguration(configuration);
+
+ this.startTasks();
+ }
+ }
+
+ private void logMissingUpdateInformation(String api) {
+ this.logger.warn("Device information - {} status missing", api);
+ }
+
+ private void updateHdmiInformation(HueSyncHdmi hdmiStatus) {
+ updateHdmiStatus(HueSyncConstants.CHANNELS.HDMI.IN_1, hdmiStatus.input1);
+ updateHdmiStatus(HueSyncConstants.CHANNELS.HDMI.IN_2, hdmiStatus.input2);
+ updateHdmiStatus(HueSyncConstants.CHANNELS.HDMI.IN_3, hdmiStatus.input3);
+ updateHdmiStatus(HueSyncConstants.CHANNELS.HDMI.IN_4, hdmiStatus.input4);
+
+ updateHdmiStatus(HueSyncConstants.CHANNELS.HDMI.OUT, hdmiStatus.output);
+ }
+
+ private void updateHdmiStatus(HdmiChannels channels, @Nullable HueSyncHdmiConnectionInfo hdmiStatusInfo) {
+ if (hdmiStatusInfo != null) {
+ this.updateState(channels.name, new StringType(hdmiStatusInfo.name));
+ this.updateState(channels.type, new StringType(hdmiStatusInfo.type));
+ this.updateState(channels.mode, new StringType(hdmiStatusInfo.lastSyncMode));
+ this.updateState(channels.status, new StringType(hdmiStatusInfo.status));
+ }
+ }
+
+ private void updateFirmwareInformation(HueSyncDeviceDetailed deviceStatus) {
+ State firmwareState = new StringType(deviceStatus.firmwareVersion);
+ State firmwareAvailableState = new StringType(
+ deviceStatus.updatableFirmwareVersion != null ? deviceStatus.updatableFirmwareVersion
+ : deviceStatus.firmwareVersion);
+
+ setProperty(Thing.PROPERTY_FIRMWARE_VERSION, deviceStatus.firmwareVersion);
+ setProperty(HueSyncHandler.PROPERTY_API_VERSION, String.format("%d", deviceStatus.apiLevel));
+
+ this.updateState(HueSyncConstants.CHANNELS.DEVICE.INFORMATION.FIRMWARE, firmwareState);
+ this.updateState(HueSyncConstants.CHANNELS.DEVICE.INFORMATION.FIRMWARE_AVAILABLE, firmwareAvailableState);
+ }
+
+ private void updateExecutionInformation(HueSyncExecution executionStatus) {
+ this.updateState(HueSyncConstants.CHANNELS.COMMANDS.MODE, new StringType(executionStatus.getMode()));
+ this.updateState(HueSyncConstants.CHANNELS.COMMANDS.SYNC,
+ executionStatus.syncActive ? OnOffType.ON : OnOffType.OFF);
+ this.updateState(HueSyncConstants.CHANNELS.COMMANDS.HDMI,
+ executionStatus.hdmiActive ? OnOffType.ON : OnOffType.OFF);
+ this.updateState(HueSyncConstants.CHANNELS.COMMANDS.SOURCE, new StringType(executionStatus.hdmiSource));
+ this.updateState(HueSyncConstants.CHANNELS.COMMANDS.BRIGHTNESS, new DecimalType(executionStatus.brightness));
+ }
+
+ private void handleRegistration(HueSyncRegistration registration) {
+ this.stopTasks();
+
+ setProperty(HueSyncConstants.REGISTRATION_ID, registration.registrationId);
+
+ Configuration configuration = this.editConfiguration();
+
+ configuration.put(HueSyncConstants.REGISTRATION_ID, registration.registrationId);
+ configuration.put(HueSyncConstants.API_TOKEN, registration.accessToken);
+
+ this.updateConfiguration(configuration);
+
+ this.startTasks();
+ }
+
+ private void checkCompatibility() throws HueSyncApiException {
+ try {
+ HueSyncDevice deviceInformation = this.deviceInfo.orElseThrow();
+
+ if (deviceInformation.apiLevel < HueSyncConstants.MINIMAL_API_VERSION) {
+ throw new HueSyncApiException("@text/api.minimal-version");
+ }
+ } catch (NoSuchElementException e) {
+ throw new HueSyncApiException("@text/api.communication-problem");
+ }
+ }
+
+ private void setProperty(String key, @Nullable String value) {
+ if (value != null) {
+ Map properties = this.editProperties();
+
+ if (properties.containsKey(key)) {
+ @Nullable
+ String currentValue = properties.get(key);
+ if (!(value.equals(currentValue))) {
+ saveProperty(key, value, properties);
+ }
+ } else {
+ saveProperty(key, value, properties);
+ }
+ }
+ }
+
+ private void saveProperty(String key, String value, Map properties) {
+ properties.put(key, value);
+ this.updateProperties(properties);
+ }
+
+ // #endregion
+
+ // #region Override
+ @Override
+ public void initialize() {
+ try {
+ updateStatus(ThingStatus.UNKNOWN);
+
+ this.stopTasks();
+
+ scheduler.execute(initializeConnection());
+ } catch (Exception e) {
+ this.logger.warn("{}", e.getMessage());
+
+ this.updateStatus(ThingStatus.OFFLINE);
+ }
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (thing.getStatus() != ThingStatus.ONLINE) {
+ this.logger.warn("Device status: {} - Command {} for chanel {} will be ignored",
+ thing.getStatus().toString(), command.toFullString(), channelUID.toString());
+ return;
+ }
+
+ Channel channel = thing.getChannel(channelUID);
+
+ if (channel == null) {
+ logger.error("Channel UID:{} does not exist - please report this as an issue", channelUID);
+ return;
+ }
+
+ this.connection.executeCommand(channel, command);
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+
+ try {
+ this.stopTasks();
+ this.connection.dispose();
+ } catch (Exception e) {
+ this.logger.warn("{}", e.getMessage());
+ } finally {
+ this.logger.debug("Thing {} ({}) disposed.", this.thing.getLabel(), this.thing.getUID());
+ }
+ }
+
+ @Override
+ public void handleRemoval() {
+ super.handleRemoval();
+
+ this.connection.unregisterDevice();
+ }
+
+ // #endregion
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncRegistrationTask.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncRegistrationTask.java
new file mode 100644
index 0000000000000..5b6d2c27bb75b
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncRegistrationTask.java
@@ -0,0 +1,68 @@
+/**
+ * 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.huesync.internal.handler.tasks;
+
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.huesync.internal.api.dto.device.HueSyncDevice;
+import org.openhab.binding.huesync.internal.api.dto.registration.HueSyncRegistration;
+import org.openhab.binding.huesync.internal.connection.HueSyncDeviceConnection;
+import org.openhab.binding.huesync.internal.exceptions.HueSyncConnectionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Task to handle device registration.
+ *
+ * @author Patrik Gfeller - Initial contribution
+ */
+@NonNullByDefault
+public class HueSyncRegistrationTask implements Runnable {
+ private final Logger logger = LoggerFactory.getLogger(HueSyncRegistrationTask.class);
+
+ private final HueSyncDeviceConnection connection;
+ private final HueSyncDevice deviceInfo;
+ private final Consumer action;
+
+ public HueSyncRegistrationTask(HueSyncDeviceConnection connection, HueSyncDevice deviceInfo,
+ Consumer action) {
+ this.connection = connection;
+ this.deviceInfo = deviceInfo;
+ this.action = action;
+ }
+
+ @Override
+ public void run() {
+ try {
+ String id = this.deviceInfo.uniqueId;
+
+ if (this.connection.isRegistered() || id == null) {
+ return;
+ }
+
+ this.logger.debug("Listening for device registration - {} {}:{}", this.deviceInfo.name,
+ this.deviceInfo.deviceType, id);
+
+ HueSyncRegistration registration = this.connection.registerDevice(id);
+
+ if (registration != null) {
+ this.logger.debug("API token for {} received", this.deviceInfo.name);
+
+ this.action.accept(registration);
+ }
+ } catch (HueSyncConnectionException e) {
+ this.logger.warn("{}", e.getMessage());
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncUpdateTask.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncUpdateTask.java
new file mode 100644
index 0000000000000..18067fa39f6ce
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncUpdateTask.java
@@ -0,0 +1,69 @@
+/**
+ * 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.huesync.internal.handler.tasks;
+
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.huesync.internal.api.dto.device.HueSyncDevice;
+import org.openhab.binding.huesync.internal.connection.HueSyncDeviceConnection;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Task to handle device information update.
+ *
+ * @author Patrik Gfeller - Initial contribution
+ */
+@NonNullByDefault
+public class HueSyncUpdateTask implements Runnable {
+
+ private final Logger logger = LoggerFactory.getLogger(HueSyncUpdateTask.class);
+
+ private final HueSyncDeviceConnection connection;
+ private final HueSyncDevice deviceInfo;
+
+ private final Consumer<@Nullable HueSyncUpdateTaskResult> action;
+
+ public HueSyncUpdateTask(HueSyncDeviceConnection connection, HueSyncDevice deviceInfo,
+ Consumer<@Nullable HueSyncUpdateTaskResult> action) {
+ this.connection = connection;
+ this.deviceInfo = deviceInfo;
+
+ this.action = action;
+ }
+
+ @Override
+ public void run() {
+ try {
+ this.logger.debug("Status update query for {} {}:{}", this.deviceInfo.name, this.deviceInfo.deviceType,
+ this.deviceInfo.uniqueId);
+
+ if (!this.connection.isRegistered()) {
+ this.action.accept(null);
+ }
+
+ HueSyncUpdateTaskResult updateInfo = new HueSyncUpdateTaskResult();
+
+ updateInfo.deviceStatus = this.connection.getDetailedDeviceInfo();
+ updateInfo.hdmiStatus = this.connection.getHdmiInfo();
+ updateInfo.execution = this.connection.getExecutionInfo();
+
+ this.action.accept(updateInfo);
+ } catch (Exception e) {
+ this.logger.debug("{}", e.getMessage());
+ this.action.accept(null);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncUpdateTaskResult.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncUpdateTaskResult.java
new file mode 100644
index 0000000000000..7f44a86312747
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/handler/tasks/HueSyncUpdateTaskResult.java
@@ -0,0 +1,30 @@
+/**
+ * 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.huesync.internal.handler.tasks;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.huesync.internal.api.dto.device.HueSyncDeviceDetailed;
+import org.openhab.binding.huesync.internal.api.dto.execution.HueSyncExecution;
+import org.openhab.binding.huesync.internal.api.dto.hdmi.HueSyncHdmi;
+
+/**
+ *
+ * @author Patrik Gfeller - Initial contribution
+ */
+@NonNullByDefault
+public class HueSyncUpdateTaskResult {
+ public @Nullable HueSyncDeviceDetailed deviceStatus;
+ public @Nullable HueSyncHdmi hdmiStatus;
+ public @Nullable HueSyncExecution execution;
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/i18n/HueSyncLocalizer.java b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/i18n/HueSyncLocalizer.java
new file mode 100644
index 0000000000000..d6c2eebfd0680
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/java/org/openhab/binding/huesync/internal/i18n/HueSyncLocalizer.java
@@ -0,0 +1,49 @@
+/**
+ * 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.huesync.internal.i18n;
+
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.i18n.TranslationProvider;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+
+/**
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ */
+@NonNullByDefault
+public class HueSyncLocalizer {
+ private static final Locale LOCALE = Locale.ENGLISH;
+ private static final BundleContext BUNDLE_CONTEXT = FrameworkUtil.getBundle(HueSyncLocalizer.class)
+ .getBundleContext();
+ private static final ServiceReference SERVICE_REFERENCE = BUNDLE_CONTEXT
+ .getServiceReference(TranslationProvider.class);
+ private static final Bundle BUNDLE = BUNDLE_CONTEXT.getBundle();
+
+ public static String getResourceString(String key) {
+ String lookupKey = key.replace("@text/", "");
+
+ String missingKey = "Missing Translation: " + key;
+
+ String result = (BUNDLE_CONTEXT
+ .getService(SERVICE_REFERENCE) instanceof TranslationProvider translationProvider)
+ ? translationProvider.getText(BUNDLE, lookupKey, missingKey, LOCALE)
+ : missingKey;
+
+ return result == null ? missingKey : result;
+ }
+}
diff --git a/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644
index 0000000000000..b9afa2d7f4b11
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/addon/addon.xml
@@ -0,0 +1,23 @@
+
+
+
+ binding
+ Hue HDMI Sync Box Binding
+ Binding for the Hue HDMI Sync Box.
+ local
+
+
+
+ mdns
+
+
+ mdnsServiceType
+ _huesync._tcp.local.
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/config/config.xml
new file mode 100644
index 0000000000000..55c4017329927
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/config/config.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+ Connection
+
+
+
+ network-address
+ Address
+ Network address of the HDMI Sync Box.
+ true
+
+
+
+ Port
+ Port of the HDMI Sync Box.
+ true
+ 443
+ true
+
+
+
+ Registration Id
+ The id of the API registration.
+ true
+
+
+ password
+ Access Token
+ To enable the binding to communicate with the device, a registration is required. Once the registration
+ process is completed, the acquired token will authorize the binding to interact with the device. After initial
+ discovery and thing creation the device will stay offline. Press the registration button on the sync box for 3
+ seconds to grant the binding the required permissions.
+ true
+
+
+ Update Interval
+ Seconds between fetching values from the Hue Sync Box.
+ true
+ true
+ 10
+ Seconds
+
+
+
diff --git a/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/i18n/huesync.properties b/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/i18n/huesync.properties
new file mode 100644
index 0000000000000..8b4a0f04aabf1
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/i18n/huesync.properties
@@ -0,0 +1,110 @@
+# add-on
+
+addon.huesync.name = Hue HDMI Sync Box Binding
+addon.huesync.description = Binding for the Hue HDMI Sync Box.
+
+# thing types
+
+thing-type.huesync.box.label = HDMI Sync Box
+thing-type.huesync.box.description = Sync your smart lights to your on-screen TV content with the Philips Hue Play HDMI Sync Box. Four HDMI inputs allow you to connect your media devices to your Hue setup, resulting in a fast, seamless display of colorful smart light that responds to and reflects the content you watch or listen to.
+
+# thing types config
+
+thing-type.config.box.thing.apiAccessToken.label = Access Token
+thing-type.config.box.thing.apiAccessToken.description = To enable the binding to communicate with the device, a registration is required. Once the registration process is completed, the acquired token will authorize the binding to interact with the device. After initial discovery and thing creation the device will stay offline. Press the registration button on the sync box for 3 seconds to grant the binding the required permissions.
+thing-type.config.box.thing.group.connection.label = Connection
+thing-type.config.box.thing.host.label = Address
+thing-type.config.box.thing.host.description = Network address of the HDMI Sync Box.
+thing-type.config.box.thing.port.label = Port
+thing-type.config.box.thing.port.description = Port of the HDMI Sync Box.
+thing-type.config.box.thing.registrationId.label = Registration Id
+thing-type.config.box.thing.registrationId.description = The id of the API registration.
+thing-type.config.box.thing.statusUpdateInterval.label = Update Interval
+thing-type.config.box.thing.statusUpdateInterval.description = Seconds between fetching values from the Hue Sync Box.
+
+# channel group types
+
+channel-group-type.huesync.device-commands.label = Commands
+channel-group-type.huesync.device-commands.description = Commands are used to control the real-time behavior of the hue sync box. These commands allow you to influence how the lights react to your entertainment.
+channel-group-type.huesync.device-firmware.label = Firmware
+channel-group-type.huesync.device-firmware.description = Information about the installed device firmware and available updates.
+channel-group-type.huesync.device-hdmi-connection-in.label = HDMI Input
+channel-group-type.huesync.device-hdmi-connection-in.description = HDMI connection
+channel-group-type.huesync.device-hdmi-connection-out.label = HDMI Output
+channel-group-type.huesync.device-hdmi-connection-out.description = HDMI connection
+
+# channel types
+
+channel-type.huesync.connection-last-sync-mode.label = Last Mode
+channel-type.huesync.connection-last-sync-mode.description = Last sync mode used for this channel
+channel-type.huesync.connection-last-sync-mode.command.option.video = Video
+channel-type.huesync.connection-last-sync-mode.command.option.game = Game
+channel-type.huesync.connection-last-sync-mode.command.option.music = Music
+channel-type.huesync.connection-name.label = HDMI Name
+channel-type.huesync.connection-name.description = Friendly name of the HDMI connection
+channel-type.huesync.connection-status.label = HDMI Status
+channel-type.huesync.connection-status.description = Status of the HDMI input
+channel-type.huesync.connection-status.command.option.unplugged = Unplugged
+channel-type.huesync.connection-status.command.option.plugged = Plugged
+channel-type.huesync.connection-status.command.option.linked = Linked
+channel-type.huesync.connection-status.command.option.unknown = Unknown
+channel-type.huesync.connection-type.label = HDMI Type
+channel-type.huesync.connection-type.description = Type of the connected HDMI device
+channel-type.huesync.connection-type.command.option.generic = Generic
+channel-type.huesync.connection-type.command.option.video = Video
+channel-type.huesync.connection-type.command.option.game = Game
+channel-type.huesync.connection-type.command.option.music = Music
+channel-type.huesync.connection-type.command.option.xbox = XBox
+channel-type.huesync.connection-type.command.option.playstation = PlayStation
+channel-type.huesync.connection-type.command.option.nintendoswitch = Nintendo Switch
+channel-type.huesync.connection-type.command.option.phone = Phone
+channel-type.huesync.connection-type.command.option.desktop = Desktop
+channel-type.huesync.connection-type.command.option.laptop = Laptop
+channel-type.huesync.connection-type.command.option.appletv = Apple TV
+channel-type.huesync.connection-type.command.option.roku = Roku
+channel-type.huesync.connection-type.command.option.shield = Nvidia Shield
+channel-type.huesync.connection-type.command.option.chromecast = Chromecast
+channel-type.huesync.connection-type.command.option.firetv = Amazon Fire TV
+channel-type.huesync.connection-type.command.option.diskplayer = Disk Player
+channel-type.huesync.connection-type.command.option.settopbox = Set-top box
+channel-type.huesync.connection-type.command.option.satellite = Satellite
+channel-type.huesync.connection-type.command.option.avreceiver = AV receiver
+channel-type.huesync.connection-type.command.option.soundbar = Soundbar
+channel-type.huesync.connection-type.command.option.hdmiswitch = HDMI switch
+channel-type.huesync.device-info-firmware-available.label = Latest Firmware
+channel-type.huesync.device-info-firmware-available.description = Latest available firmware version
+channel-type.huesync.device-info-firmware.label = Firmware
+channel-type.huesync.device-info-firmware.description = Installed firmware version
+channel-type.huesync.execution-brightness.label = Brightness
+channel-type.huesync.execution-brightness.description = 0 ... 200
0 = max reduction 100 = no brightness reduction/boost compared to input 200 = max boost
+channel-type.huesync.execution-hdmi-active.label = HDMI Active
+channel-type.huesync.execution-hdmi-active.description = OFF in case of powersave mode and ON in case of passthrough , video , game or music mode.
When changed from OFF to ON , it will set passthrough mode. When changed from ON to OFF , will set powersave mode.
+channel-type.huesync.execution-hdmi-source.label = HDMI Input
+channel-type.huesync.execution-hdmi-source.description =
input1 input2 input3 input4
+channel-type.huesync.execution-mode.label = Mode
+channel-type.huesync.execution-mode.description =
"Video": Analyzes the on-screen visuals, translating colors and brightness into corresponding light effects for an immersive movie-watching experience.
"Music": Analyzes the rhythm and beat of your music, creating dynamic light along to your tunes.
"Game": Reacts to the action on your screen, intensifying the in-game atmosphere with bursts of light that correspond to explosions, gunfire, and other gameplay events.
"Passthrough" "Powersave"
+channel-type.huesync.execution-mode.command.option.powersave = Powersave
+channel-type.huesync.execution-mode.command.option.passthrough = Passthrough
+channel-type.huesync.execution-mode.command.option.video = Video
+channel-type.huesync.execution-mode.command.option.game = Game
+channel-type.huesync.execution-mode.command.option.music = Music
+channel-type.huesync.execution-sync-active.label = Synchronization Active
+channel-type.huesync.execution-sync-active.description = OFF in case of powersave or passthrough mode, and ON in case of video , game or music mode.
When changed from OFF to ON , it will start syncing in last used mode for current source. When changed from ON to OFF , will set passthrough mode.
+
+# *** exceptions ***
+
+exception.generic.connection = "Unable to connect to device."
+
+# api & connection exceptions
+
+api.minimal-version = Only devices with API level >= 7 are supported
+api.communication-problem = Communication problem with the device
+connection.invalid-login = Invalid or missing credentials
+
+# registration
+
+thing.config.huesync.box.registration = Device registration pending. Please press the HDMI Sync Box device button for 3 seconds.
+
+# logger (to keep text in sync with on-screen messages, log messages will always be in locale.english)
+
+logger.initialization-problem = Unable to initialize handler for {} ({}): {}
diff --git a/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/thing/channel-types.xml
new file mode 100644
index 0000000000000..0f423c075b680
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/thing/channel-types.xml
@@ -0,0 +1,225 @@
+
+
+
+
+ String
+ Firmware
+ Installed firmware version
+ text
+
+
+
+
+ String
+ Latest Firmware
+ Latest available firmware version
+ text
+
+
+
+
+ String
+ HDMI Name
+ Friendly name of the HDMI connection
+ text
+
+
+
+
+ String
+ HDMI Status
+ Status of the HDMI input
+ status
+
+
+
+ Unplugged
+ Plugged
+ Linked
+ Unknown
+
+
+
+
+
+ String
+ HDMI Type
+ Type of the connected HDMI device
+ text
+
+
+
+ Generic
+ Video
+ Game
+ Music
+ XBox
+ PlayStation
+ Nintendo Switch
+ Phone
+ Desktop
+ Laptop
+ Apple TV
+ Roku
+ Nvidia Shield
+ Chromecast
+ Amazon Fire TV
+ Disk Player
+ Set-top box
+ Satellite
+ AV receiver
+ Soundbar
+ HDMI switch
+
+
+
+
+
+ String
+ Last Mode
+ Last sync mode used for this channel
+ text
+
+
+
+ Video
+ Game
+ Music
+
+
+
+
+
+ String
+ Mode
+
+
+
+
+ "Video":
+
+ Analyzes the on-screen visuals, translating colors and brightness into corresponding light
+ effects for an immersive movie-watching experience.
+
+
+
+ "Music":
+
+ Analyzes the rhythm and beat of your music, creating
+ dynamic light along to your tunes.
+
+
+
+ "Game":
+
+ Reacts to the action on your screen, intensifying the in-game atmosphere
+ with bursts of light that correspond to explosions, gunfire, and other gameplay events.
+
+ "Passthrough"
+ "Powersave"
+
+
+ ]]>
+
+ text
+
+
+ Powersave
+ Passthrough
+ Video
+ Game
+ Music
+
+
+
+
+
+ Switch
+ Synchronization Active
+
+
+ OFF in case of powersave or passthrough mode, and ON in case of video , game or music mode.
+
+
+ When changed from OFF to ON , it will start syncing in last used mode for current source.
+ When changed from ON to OFF , will set passthrough mode.
+
+ ]]>
+
+ switch
+
+
+
+ Switch
+ HDMI Active
+
+
+ OFF in case of powersave mode and ON in case of passthrough , video , game or music mode.
+
+
+ When changed from OFF to ON , it will set passthrough mode.
+ When changed from ON to OFF , will set powersave mode.
+
+ ]]>
+
+ switch
+
+
+
+ String
+ HDMI Input
+
+
+
+ input1
+ input2
+ input3
+ input4
+
+
+ ]]>
+
+ receiver
+
+
+
+
+
+
+
+
+
+
+
+
+ Number:Dimensionless
+ Brightness
+
+
+ 0 ... 200
+
+ 0 = max reduction
+ 100 = no brightness reduction/boost compared to input
+ 200 = max boost
+
+
+ ]]>
+
+ slider
+
+
+
+
diff --git a/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 0000000000000..043dc019c8f31
--- /dev/null
+++ b/bundles/org.openhab.binding.huesync/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,84 @@
+
+
+
+
+ HDMI Sync Box
+
+ Sync your smart lights to your on-screen TV content with the Philips Hue Play HDMI Sync Box. Four HDMI
+ inputs allow you to connect your media devices to your Hue setup, resulting in a fast, seamless display of colorful
+ smart light that responds to and reflects the content you watch or listen to.
+
+
+ receiver
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Philips
+
+
+ host
+
+
+
+
+
+ Firmware
+ Information about the installed device firmware and available updates.
+ text
+
+
+
+
+
+
+ HDMI Input
+ HDMI connection
+ settings
+
+
+
+
+
+
+
+
+ HDMI Output
+ HDMI connection
+ settings
+
+
+
+
+
+
+
+
+ Commands
+ Commands are used to control the real-time behavior of the hue sync box. These
+ commands allow you to
+ influence how the lights react to your entertainment.
+ settings
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.icalendar/src/main/resources/OH-INF/i18n/icalendar_fr.properties b/bundles/org.openhab.binding.icalendar/src/main/resources/OH-INF/i18n/icalendar_fr.properties
index f5269f0354ec1..66538322f4bdc 100644
--- a/bundles/org.openhab.binding.icalendar/src/main/resources/OH-INF/i18n/icalendar_fr.properties
+++ b/bundles/org.openhab.binding.icalendar/src/main/resources/OH-INF/i18n/icalendar_fr.properties
@@ -24,10 +24,17 @@ thing-type.config.icalendar.calendar.refreshTime.label = Fréquence de rafraîch
thing-type.config.icalendar.calendar.refreshTime.description = Fréquence de scan des modifications en minutes
thing-type.config.icalendar.calendar.url.label = URL
thing-type.config.icalendar.calendar.url.description = URL pour télécharger les événements iCalendar
+thing-type.config.icalendar.calendar.userAgent.label = Agent utilisateur
+thing-type.config.icalendar.calendar.userAgent.description = Certains fournisseurs nécessitent un en-tête spécifique de l'agent utilisateur. Si laissé vide, l'en-tête Jetty par défaut est utilisé.
thing-type.config.icalendar.calendar.username.label = Nom d'utilisateur
thing-type.config.icalendar.calendar.username.description = Nom d'utilisateur pour la récupération du calendrier (utilisé en combinaison avec le mot de passe dans l'authentification basique HTTP)
thing-type.config.icalendar.eventfilter.datetimeEnd.label = Fin
thing-type.config.icalendar.eventfilter.datetimeEnd.description = Fin de la plage de temps pour trouver des événements en relatif par rapport à "maintenant" (exclusif)
+thing-type.config.icalendar.eventfilter.datetimeMode.label = Mode de recherche
+thing-type.config.icalendar.eventfilter.datetimeMode.description = Définit quelle partie d'un événement doit être comprise dans la période de recherche entre le début et la fin
+thing-type.config.icalendar.eventfilter.datetimeMode.option.START = Événements qui commencent dans la période
+thing-type.config.icalendar.eventfilter.datetimeMode.option.ACTIVE = Événements actifs à toute phase de la période
+thing-type.config.icalendar.eventfilter.datetimeMode.option.END = Événements qui se terminent dans la période
thing-type.config.icalendar.eventfilter.datetimeRound.label = Arrondir à l'unité de Date/Heure
thing-type.config.icalendar.eventfilter.datetimeRound.description = Définir ceci va arrondir la date/heure de début et de fin à l'unité de temps la plus proche en-dessous (par exemple, si l'unité est le jour \: le début et la fin seront arrondis à 0\:00 heure du jour)
thing-type.config.icalendar.eventfilter.datetimeStart.label = Début
diff --git a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/IhcClient.java b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/IhcClient.java
index 989b3aaaf0891..8441ca7b7e867 100644
--- a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/IhcClient.java
+++ b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/IhcClient.java
@@ -27,6 +27,7 @@
import java.util.Set;
import java.util.zip.GZIPInputStream;
+import org.openhab.binding.ihc.internal.IhcBindingConstants;
import org.openhab.binding.ihc.internal.ws.datatypes.WSControllerState;
import org.openhab.binding.ihc.internal.ws.datatypes.WSFile;
import org.openhab.binding.ihc.internal.ws.datatypes.WSLoginResult;
@@ -456,6 +457,10 @@ public boolean resourceUpdate(WSResourceValue value) throws IhcExecption {
private class IhcResourceValueNotificationListener extends Thread {
private volatile boolean interrupted = false;
+ public IhcResourceValueNotificationListener() {
+ super(String.format("OH-binding-%s-%s", IhcBindingConstants.BINDING_ID, "NotificationListener"));
+ }
+
public void setInterrupted(boolean interrupted) {
this.interrupted = interrupted;
this.interrupt();
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/transport/LegacyPort.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/transport/LegacyPort.java
index 664a1f8386938..28ee847c0be02 100644
--- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/transport/LegacyPort.java
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/transport/LegacyPort.java
@@ -22,6 +22,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.insteon.internal.InsteonBindingConstants;
import org.openhab.binding.insteon.internal.InsteonLegacyBindingConstants;
import org.openhab.binding.insteon.internal.config.InsteonLegacyNetworkConfiguration;
import org.openhab.binding.insteon.internal.device.InsteonAddress;
@@ -178,9 +179,9 @@ public void start() {
}
readThread = new Thread(reader);
- setParamsAndStart(readThread, "Reader");
+ setParamsAndStart(readThread, "OH-binding-" + InsteonBindingConstants.BINDING_ID + "-LegacyReader");
writeThread = new Thread(writer);
- setParamsAndStart(writeThread, "Writer");
+ setParamsAndStart(writeThread, "OH-binding-" + InsteonBindingConstants.BINDING_ID + "-LegacyWriter");
if (!mdbb.isComplete()) {
modem.initialize();
diff --git a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/handler/IntesisBoxHandler.java b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/handler/IntesisBoxHandler.java
index 5a82a2e231899..44ab794a716f6 100644
--- a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/handler/IntesisBoxHandler.java
+++ b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/handler/IntesisBoxHandler.java
@@ -93,11 +93,8 @@ public void initialize() {
if (!config.ipAddress.isEmpty()) {
updateStatus(ThingStatus.UNKNOWN);
scheduler.submit(() -> {
-
- String readerThreadName = "OH-binding-" + getThing().getUID().getAsString();
-
IntesisBoxSocketApi intesisLocalApi = intesisBoxSocketApi = new IntesisBoxSocketApi(config.ipAddress,
- config.port, readerThreadName);
+ config.port, "OH-binding-" + getThing().getUID());
intesisLocalApi.addIntesisBoxChangeListener(this);
try {
intesisLocalApi.openConnection();
diff --git a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/Ffmpeg.java b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/Ffmpeg.java
index 11b84a92d0937..639e23ae2fa47 100644
--- a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/Ffmpeg.java
+++ b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/Ffmpeg.java
@@ -103,6 +103,7 @@ private class IpCameraFfmpegThread extends Thread {
public int countOfMotions;
IpCameraFfmpegThread() {
+ super(String.format("OH-binding-%s-%s", IpCameraBindingConstants.BINDING_ID, "Ffmpeg"));
setDaemon(true);
}
diff --git a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/IpCameraBindingConstants.java b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/IpCameraBindingConstants.java
index 6023bd76dd508..7c297d07cda48 100644
--- a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/IpCameraBindingConstants.java
+++ b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/IpCameraBindingConstants.java
@@ -28,7 +28,7 @@
*/
@NonNullByDefault
public class IpCameraBindingConstants {
- private static final String BINDING_ID = "ipcamera";
+ public static final String BINDING_ID = "ipcamera";
public static final String AUTH_HANDLER = "authorizationHandler";
public static final String AMCREST_HANDLER = "amcrestHandler";
public static final String COMMON_HANDLER = "commonHandler";
diff --git a/bundles/org.openhab.binding.ism8/README.md b/bundles/org.openhab.binding.ism8/README.md
index e47939cff7ef5..ee74733e44901 100644
--- a/bundles/org.openhab.binding.ism8/README.md
+++ b/bundles/org.openhab.binding.ism8/README.md
@@ -81,6 +81,9 @@ For the moment, the following data types are implemented:
Date and Time types used by for CWL Excellent and CWL2 are currently not supported by the ISM8 add-on.
+*Attention:* Due to a bug in the original implementation, the states for DPT 1.009 are inverted (i.e., `1` is mapped to `OPEN` instead of `CLOSE`).
+A change would break all existing installations and is therefore not implemented.
+
## Full Example
### ism8.things
diff --git a/bundles/org.openhab.binding.ism8/src/main/java/org/openhab/binding/ism8/internal/Ism8Handler.java b/bundles/org.openhab.binding.ism8/src/main/java/org/openhab/binding/ism8/internal/Ism8Handler.java
index b36d857440b54..53713712fd397 100644
--- a/bundles/org.openhab.binding.ism8/src/main/java/org/openhab/binding/ism8/internal/Ism8Handler.java
+++ b/bundles/org.openhab.binding.ism8/src/main/java/org/openhab/binding/ism8/internal/Ism8Handler.java
@@ -29,6 +29,7 @@
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
@@ -86,6 +87,14 @@ public void handleCommand(ChannelUID channelUID, Command command) {
if (command == RefreshType.REFRESH) {
updateChannel(dataPoint);
} else {
+ ChannelTypeUID channelType = channel.getChannelTypeUID();
+ if (channelType != null) {
+ if (channelType.getId().endsWith("-r")) {
+ logger.warn("Ism8: channel {} of type {} is read-only, cannot send command", channelUID.getId(),
+ channelType.toString());
+ return;
+ }
+ }
setDataPoint(dataPoint, command);
}
}
diff --git a/bundles/org.openhab.binding.ism8/src/main/java/org/openhab/binding/ism8/internal/util/Ism8DomainMap.java b/bundles/org.openhab.binding.ism8/src/main/java/org/openhab/binding/ism8/internal/util/Ism8DomainMap.java
index 4ecb42d491dfe..2729f70c47062 100644
--- a/bundles/org.openhab.binding.ism8/src/main/java/org/openhab/binding/ism8/internal/util/Ism8DomainMap.java
+++ b/bundles/org.openhab.binding.ism8/src/main/java/org/openhab/binding/ism8/internal/util/Ism8DomainMap.java
@@ -25,6 +25,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.ism8.server.IDataPoint;
import org.openhab.core.library.dimension.VolumetricFlowRate;
+import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
@@ -71,8 +72,10 @@ public static State toOpenHABState(IDataPoint dataPoint) {
return new QuantityType((Double) value, Units.PERCENT);
} else if (Units.ONE.equals(unit)) {
return new QuantityType((Double) value, Units.ONE);
- } else if (value instanceof Boolean) {
- return OnOffType.from((boolean) value);
+ } else if (value instanceof Boolean b) {
+ // DecimalType is compatible with Switch and Contact items, OH mapping is 0-off-closed and 1-on-open;
+ // note that this is opposite to definition of KNX DPT 1.009
+ return b ? DecimalType.valueOf("1") : DecimalType.valueOf("0");
} else if (value instanceof Byte) {
return new QuantityType((byte) value, Units.ONE);
} else if (value instanceof Integer) {
diff --git a/bundles/org.openhab.binding.ism8/src/main/java/org/openhab/binding/ism8/server/DataPointBool.java b/bundles/org.openhab.binding.ism8/src/main/java/org/openhab/binding/ism8/server/DataPointBool.java
index 8eba2c97fdb7a..cea2ecfd10f0b 100644
--- a/bundles/org.openhab.binding.ism8/src/main/java/org/openhab/binding/ism8/server/DataPointBool.java
+++ b/bundles/org.openhab.binding.ism8/src/main/java/org/openhab/binding/ism8/server/DataPointBool.java
@@ -55,7 +55,8 @@ public void processData(byte[] data) {
@Override
protected byte[] convertWriteValue(Object value) {
String valueText = value.toString().toLowerCase();
- if ("true".equalsIgnoreCase(valueText) || "1".equalsIgnoreCase(valueText) || "ON".equalsIgnoreCase(valueText)) {
+ if ("true".equalsIgnoreCase(valueText) || "1".equalsIgnoreCase(valueText) || "ON".equalsIgnoreCase(valueText)
+ || "OPEN".equalsIgnoreCase(valueText)) {
this.setValue(true);
return new byte[] { 0x01 };
}
diff --git a/bundles/org.openhab.binding.ism8/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.ism8/src/main/resources/OH-INF/thing/thing-types.xml
index cb7d5b8603cfc..17471acee28d5 100644
--- a/bundles/org.openhab.binding.ism8/src/main/resources/OH-INF/thing/thing-types.xml
+++ b/bundles/org.openhab.binding.ism8/src/main/resources/OH-INF/thing/thing-types.xml
@@ -43,6 +43,7 @@
Switch
Digital Readonly DataPoint
+ veto
DP ID
@@ -65,6 +66,7 @@
Number:Dimensionless
Percentage Readonly DataPoint
+ veto
DP ID
@@ -102,6 +104,7 @@
Number:Dimensionless
Numeric Readonly DataPoint
+ veto
DP ID
@@ -122,6 +125,7 @@
Number:Temperature
Temperature Readonly DataPoint
+ veto
DP ID
@@ -162,6 +166,7 @@
Number:Pressure
Pressure Readonly DataPoint
+ veto
DP ID
@@ -181,6 +186,7 @@
Number:Power
Power Readonly DataPoint
+ veto
DP ID
@@ -200,6 +206,7 @@
Number:VolumetricFlowRate
Flowrate Readonly DataPoint
+ veto
DP ID
@@ -220,6 +227,7 @@
Number:Energy
Active Energy Readonly DataPoint
+ veto
DP ID
@@ -239,6 +247,8 @@
Number:Dimensionless
Mode Readonly DataPoint
+
+ veto
DP ID
diff --git a/bundles/org.openhab.binding.ism8/src/test/java/org/openhab/binding/ism8/internal/Ism8HandlerTest.java b/bundles/org.openhab.binding.ism8/src/test/java/org/openhab/binding/ism8/internal/Ism8HandlerTest.java
index cdcb3a1e389be..8bb46e138e36e 100644
--- a/bundles/org.openhab.binding.ism8/src/test/java/org/openhab/binding/ism8/internal/Ism8HandlerTest.java
+++ b/bundles/org.openhab.binding.ism8/src/test/java/org/openhab/binding/ism8/internal/Ism8HandlerTest.java
@@ -12,6 +12,7 @@
*/
package org.openhab.binding.ism8.internal;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.openhab.binding.ism8.internal.Ism8BindingConstants.*;
@@ -31,7 +32,9 @@
import org.openhab.binding.ism8.server.DataPointValue;
import org.openhab.binding.ism8.server.IDataPoint;
import org.openhab.core.config.core.Configuration;
+import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
@@ -57,6 +60,7 @@ public class Ism8HandlerTest {
private @NonNullByDefault({}) Ism8Handler thingHandler;
private ThingUID thingUID = new ThingUID(BINDING_ID, "ism8server");
private ChannelUID channel1001 = new ChannelUID(thingUID, "switch1");
+ private ChannelUID channel1001c = new ChannelUID(thingUID, "contact1");
private ChannelUID channel9001 = new ChannelUID(thingUID, "tempC");
private ChannelUID channel9002 = new ChannelUID(thingUID, "tempD");
private ChannelUID channel20001 = new ChannelUID(thingUID, "mode1");
@@ -72,6 +76,8 @@ public void initialize() {
.withConfiguration(createChannelConfig("4", "9.001")).build())
.withChannel(ChannelBuilder.create(channel1001, "Switch")
.withConfiguration(createChannelConfig("9", "1.001")).build())
+ .withChannel(ChannelBuilder.create(channel1001c, "Contact")
+ .withConfiguration(createChannelConfig("8", "1.001")).build())
.withChannel(ChannelBuilder.create(channel20001, "Switch")
.withConfiguration(createChannelConfig("2", "20.001")).build())
.build();
@@ -99,7 +105,24 @@ public void process1001MessageAndUpdateChannel() {
thingHandler.dataPointChanged(event);
// assert
- Mockito.verify(thingHandlerCallback).stateUpdated(eq(channel1001), eq(OnOffType.from(false)));
+ Mockito.verify(thingHandlerCallback).stateUpdated(eq(channel1001), eq(DecimalType.valueOf("0")));
+ assertEquals(OnOffType.from(false), OnOffType.from(DecimalType.valueOf("0").toString()));
+ }
+
+ // @Test
+ public void process1001cMessageAndUpdateChannel() {
+ // arrange
+ IDataPoint dataPoint = new DataPointBool(8, "1.001", "Datapoint_1.001");
+ dataPoint.processData(HexUtils.hexToBytes("0008030100"));
+ DataPointChangedEvent event = new DataPointChangedEvent(new Object(), dataPoint);
+ thingHandler.setCallback(thingHandlerCallback);
+
+ // act
+ thingHandler.dataPointChanged(event);
+
+ // assert
+ Mockito.verify(thingHandlerCallback).stateUpdated(eq(channel1001c), eq(DecimalType.valueOf("0")));
+ assertEquals(OpenClosedType.CLOSED.as(DecimalType.class), DecimalType.valueOf("0"));
}
@Test
diff --git a/bundles/org.openhab.binding.ism8/src/test/java/org/openhab/binding/ism8/internal/util/Ism8DomainMapTest.java b/bundles/org.openhab.binding.ism8/src/test/java/org/openhab/binding/ism8/internal/util/Ism8DomainMapTest.java
index bdc49c2d1e97d..d21bd4505ce5d 100644
--- a/bundles/org.openhab.binding.ism8/src/test/java/org/openhab/binding/ism8/internal/util/Ism8DomainMapTest.java
+++ b/bundles/org.openhab.binding.ism8/src/test/java/org/openhab/binding/ism8/internal/util/Ism8DomainMapTest.java
@@ -29,7 +29,9 @@
import org.openhab.binding.ism8.server.DataPointScaling;
import org.openhab.binding.ism8.server.DataPointValue;
import org.openhab.binding.ism8.server.IDataPoint;
+import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
@@ -118,7 +120,8 @@ public void mapDataPointBoolToOHState() {
State result = Ism8DomainMap.toOpenHABState(dataPoint);
// assert
- assertEquals(OnOffType.from(false), result);
+ assertEquals(DecimalType.valueOf("0"), result);
+ assertEquals(OnOffType.from(false), OnOffType.from(DecimalType.valueOf("0").toString()));
}
{
// arrange
@@ -129,7 +132,8 @@ public void mapDataPointBoolToOHState() {
State result = Ism8DomainMap.toOpenHABState(dataPoint);
// assert
- assertEquals(OnOffType.from(true), result);
+ assertEquals(DecimalType.valueOf("1"), result);
+ assertEquals(OnOffType.from(true), OnOffType.from(DecimalType.valueOf("1").toString()));
}
{
// arrange
@@ -140,7 +144,8 @@ public void mapDataPointBoolToOHState() {
State result = Ism8DomainMap.toOpenHABState(dataPoint);
// assert
- assertEquals(OnOffType.from(true), result);
+ assertEquals(DecimalType.valueOf("1"), result);
+ assertEquals(OnOffType.from(true), OnOffType.from(DecimalType.valueOf("1").toString()));
}
{
// arrange
@@ -151,7 +156,8 @@ public void mapDataPointBoolToOHState() {
State result = Ism8DomainMap.toOpenHABState(dataPoint);
// assert
- assertEquals(OnOffType.from(true), result);
+ assertEquals(DecimalType.valueOf("1"), result);
+ assertEquals(OnOffType.from(true), OnOffType.from(DecimalType.valueOf("1").toString()));
}
{
// arrange
@@ -162,9 +168,11 @@ public void mapDataPointBoolToOHState() {
State result = Ism8DomainMap.toOpenHABState(dataPoint);
// assert
- assertEquals(OnOffType.from(true), result);
- // TODO: check if OpenClosedType is appropriate
- // assertEquals(OpenClosedType.valueOf("OPEN"), result);
+ assertEquals(DecimalType.valueOf("1"), result);
+ assertEquals(OnOffType.ON, OnOffType.from(DecimalType.valueOf("1").toString()));
+ // DecimalType is compatible with Switch and Contact items, OH mapping is 0-off-closed and 1-on-open;
+ // note that this is opposite to definition of KNX DPT 1.009
+ assertEquals(OpenClosedType.CLOSED.as(DecimalType.class), DecimalType.valueOf("0"));
}
}
diff --git a/bundles/org.openhab.binding.keba/src/main/java/org/openhab/binding/keba/internal/handler/KeContactTransceiver.java b/bundles/org.openhab.binding.keba/src/main/java/org/openhab/binding/keba/internal/handler/KeContactTransceiver.java
index 2e2d4503d6814..315009c78d265 100644
--- a/bundles/org.openhab.binding.keba/src/main/java/org/openhab/binding/keba/internal/handler/KeContactTransceiver.java
+++ b/bundles/org.openhab.binding.keba/src/main/java/org/openhab/binding/keba/internal/handler/KeContactTransceiver.java
@@ -32,6 +32,7 @@
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
+import org.openhab.binding.keba.internal.KebaBindingConstants;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger;
@@ -71,7 +72,8 @@ public void start() {
selector = Selector.open();
if (transceiverThread == null) {
- transceiverThread = new Thread(transceiverRunnable, "OH-binding-Keba-Transceiver");
+ transceiverThread = new Thread(transceiverRunnable,
+ "OH-binding-" + KebaBindingConstants.BINDING_ID + "-Transceiver");
transceiverThread.start();
}
diff --git a/bundles/org.openhab.binding.knx/pom.xml b/bundles/org.openhab.binding.knx/pom.xml
index 6455077f12b38..468ce879db29b 100644
--- a/bundles/org.openhab.binding.knx/pom.xml
+++ b/bundles/org.openhab.binding.knx/pom.xml
@@ -40,7 +40,7 @@
com.github.calimero
calimero-device
- 2.5.1
+ 2.6-rc1
compile
diff --git a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java
index 09b2b38112dea..d38809b29a903 100644
--- a/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java
+++ b/bundles/org.openhab.binding.linky/src/main/java/org/openhab/binding/linky/internal/handler/LinkyHandler.java
@@ -326,6 +326,11 @@ private List buildReport(LocalDate startDay, LocalDate endDay, @Nullable
: (days.datas.size() <= days.periodes.size() ? days.datas.size() : days.periodes.size());
for (int i = 0; i < size; i++) {
double consumption = days.datas.get(i);
+ LocalDate day = days.periodes.get(i).dateDebut.toLocalDate();
+ // Filter data in case it contains data from dates outside the requested period
+ if (day.isBefore(startDay) || day.isAfter(endDay)) {
+ continue;
+ }
String line = days.periodes.get(i).dateDebut.format(DateTimeFormatter.ISO_LOCAL_DATE) + separator;
if (consumption >= 0) {
line += String.valueOf(consumption);
diff --git a/bundles/org.openhab.binding.lirc/src/main/java/org/openhab/binding/lirc/internal/connector/LIRCStreamReader.java b/bundles/org.openhab.binding.lirc/src/main/java/org/openhab/binding/lirc/internal/connector/LIRCStreamReader.java
index 19c6ce02806df..f8782ee7c962d 100644
--- a/bundles/org.openhab.binding.lirc/src/main/java/org/openhab/binding/lirc/internal/connector/LIRCStreamReader.java
+++ b/bundles/org.openhab.binding.lirc/src/main/java/org/openhab/binding/lirc/internal/connector/LIRCStreamReader.java
@@ -22,6 +22,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.openhab.binding.lirc.internal.LIRCBindingConstants;
import org.openhab.binding.lirc.internal.LIRCResponseException;
import org.openhab.binding.lirc.internal.messages.LIRCButtonEvent;
import org.openhab.binding.lirc.internal.messages.LIRCResponse;
@@ -45,6 +46,7 @@ public class LIRCStreamReader extends Thread {
private LIRCConnector connector;
public LIRCStreamReader(LIRCConnector connector, InputStream in) {
+ super(String.format("OH-binding-%s-%s", LIRCBindingConstants.BINDING_ID, "StreamReader"));
this.connector = connector;
this.in = in;
}
diff --git a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxServerHandler.java b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxServerHandler.java
index 6f1baa38a20f1..0af2c7b043658 100644
--- a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxServerHandler.java
+++ b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxServerHandler.java
@@ -663,6 +663,7 @@ private class LxServerThread extends Thread {
private Instant lastKeepAlive;
LxServerThread(int id) {
+ super(String.format("OH-binding-%s-%s", LxBindingConstants.BINDING_ID, "Server"));
debugId = id;
}
diff --git a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/security/LxWsSecurity.java b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/security/LxWsSecurity.java
index e9f0d155f85fb..65bd792a96ac9 100644
--- a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/security/LxWsSecurity.java
+++ b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/security/LxWsSecurity.java
@@ -86,7 +86,7 @@ public void authenticate(BiConsumer doneCallback) {
authenticationLock.unlock();
}
};
- new Thread(init).start();
+ new Thread(init, "OH-binding-" + thingHandler.getThingId() + "-Authenticate").start();
}
/**
diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/IPBridgeHandler.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/IPBridgeHandler.java
index 9ea61279430a2..c24ffe8f55c46 100644
--- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/IPBridgeHandler.java
+++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/IPBridgeHandler.java
@@ -216,7 +216,7 @@ private synchronized void connect() {
sendCommand(new LIPCommand(TargetType.BRIDGE, LutronOperation.QUERY, LutronCommandType.SYSTEM, null,
SYSTEM_DBEXPORTDATETIME));
- messageSender = new Thread(this::sendCommandsThread, "Lutron sender");
+ messageSender = new Thread(this::sendCommandsThread, "OH-binding-" + getThing().getUID() + "-IPBridgeSender");
messageSender.start();
logger.debug("Starting keepAlive job with interval {}", heartbeatInterval);
diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/LeapBridgeHandler.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/LeapBridgeHandler.java
index 0bd9a24760cb8..f48d75fdb8ed7 100644
--- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/LeapBridgeHandler.java
+++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/LeapBridgeHandler.java
@@ -304,12 +304,12 @@ private synchronized void connect() {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, STATUS_INITIALIZING);
- Thread readerThread = new Thread(this::readerThreadJob, "Lutron reader");
+ Thread readerThread = new Thread(this::readerThreadJob, "OH-binding-" + getThing().getUID() + "-BridgeReader");
readerThread.setDaemon(true);
readerThread.start();
this.readerThread = readerThread;
- Thread senderThread = new Thread(this::senderThreadJob, "Lutron sender");
+ Thread senderThread = new Thread(this::senderThreadJob, "OH-binding-" + getThing().getUID() + "-BridgeSender");
senderThread.setDaemon(true);
senderThread.start();
this.senderThread = senderThread;
diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomBridgeHandler.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomBridgeHandler.java
index 0072b7d578376..ae7d77f391d1b 100644
--- a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomBridgeHandler.java
+++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomBridgeHandler.java
@@ -139,7 +139,7 @@ public void startProcessing() {
updateStatus(ThingStatus.ONLINE);
- messageSender = new Thread(this::sendCommandsThread, "Luxom sender");
+ messageSender = new Thread(this::sendCommandsThread, "OH-binding-" + getThing().getUID() + "-Sender");
messageSender.start();
logger.debug("Starting heartbeat job with interval {} (seconds)", HEARTBEAT_INTERVAL_SECONDS);
diff --git a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVPoweroffListener.java b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVPoweroffListener.java
index e48a97e73c4f4..177d390d2c510 100644
--- a/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVPoweroffListener.java
+++ b/bundles/org.openhab.binding.magentatv/src/main/java/org/openhab/binding/magentatv/internal/network/MagentaTVPoweroffListener.java
@@ -23,6 +23,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.magentatv.internal.MagentaTVBindingConstants;
import org.openhab.binding.magentatv.internal.MagentaTVHandlerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -50,7 +51,7 @@ public class MagentaTVPoweroffListener extends Thread {
public MagentaTVPoweroffListener(MagentaTVHandlerFactory handlerFactory,
@Nullable NetworkInterface networkInterface) throws IOException {
- setName("OH-Binding-magentatv-upnp-listener");
+ super(String.format("OH-binding-%s-%s", MagentaTVBindingConstants.BINDING_ID, "PoweroffListener"));
setDaemon(true);
this.handlerFactory = handlerFactory;
diff --git a/bundles/org.openhab.binding.mail/src/main/resources/OH-INF/i18n/mail_fr.properties b/bundles/org.openhab.binding.mail/src/main/resources/OH-INF/i18n/mail_fr.properties
index 105e9ccfb7cde..f458df4869ff4 100644
--- a/bundles/org.openhab.binding.mail/src/main/resources/OH-INF/i18n/mail_fr.properties
+++ b/bundles/org.openhab.binding.mail/src/main/resources/OH-INF/i18n/mail_fr.properties
@@ -37,7 +37,7 @@ channel-type.config.mail.content.sender.description = Un filtre (expression rég
channel-type.config.mail.content.subject.label = Filtre de Sujet
channel-type.config.mail.content.subject.description = Un filtre (expression régulière) pour le sujet du mail.
channel-type.config.mail.content.transformation.label = Transformation
-channel-type.config.mail.content.transformation.description = Motif de transformation utilisé lors du traitement des messages. Les transformations multiples peuvent être enchaînées en utilisant "∩".
+channel-type.config.mail.content.transformation.description = Motif de transformation utilisé lors du traitement des messages. Les transformations multiples peuvent être enchaînées en listant chaque transformation sur une ligne séparée, ou en les concaténant avec "∩".
channel-type.config.mail.mailcount.folder.label = Nom de dossier
channel-type.config.mail.mailcount.type.label = Type de compteur
channel-type.config.mail.mailcount.type.option.UNREAD = Non lus
diff --git a/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/handler/MaxCubeBridgeHandler.java b/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/handler/MaxCubeBridgeHandler.java
index ea8f42a33087b..4df534dd79634 100644
--- a/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/handler/MaxCubeBridgeHandler.java
+++ b/bundles/org.openhab.binding.max/src/main/java/org/openhab/binding/max/internal/handler/MaxCubeBridgeHandler.java
@@ -340,7 +340,8 @@ private synchronized void startAutomaticRefresh() {
pollingJob = scheduler.scheduleWithFixedDelay(this::refreshData, 0, refreshInterval, TimeUnit.SECONDS);
}
if (queueConsumerThread == null || !queueConsumerThread.isAlive()) {
- queueConsumerThread = new Thread(new QueueConsumer(commandQueue), "max-queue-consumer");
+ queueConsumerThread = new Thread(new QueueConsumer(commandQueue),
+ "OH-binding-" + getThing().getUID() + "-max-queue-consumer");
queueConsumerThread.setDaemon(true);
queueConsumerThread.start();
}
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/AccountHandler.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/AccountHandler.java
index ded788084a32b..ab2d583ad0cb1 100644
--- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/AccountHandler.java
+++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/AccountHandler.java
@@ -12,6 +12,7 @@
*/
package org.openhab.binding.mercedesme.internal.handler;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -57,8 +58,15 @@
import org.slf4j.LoggerFactory;
import com.daimler.mbcarkit.proto.Client.ClientMessage;
+import com.daimler.mbcarkit.proto.Protos.AcknowledgeAssignedVehicles;
+import com.daimler.mbcarkit.proto.VehicleEvents;
+import com.daimler.mbcarkit.proto.VehicleEvents.AcknowledgeVEPUpdatesByVIN;
+import com.daimler.mbcarkit.proto.VehicleEvents.PushMessage;
import com.daimler.mbcarkit.proto.VehicleEvents.VEPUpdate;
+import com.daimler.mbcarkit.proto.Vehicleapi.AcknowledgeAppTwinCommandStatusUpdatesByVIN;
import com.daimler.mbcarkit.proto.Vehicleapi.AppTwinCommandStatusUpdatesByPID;
+import com.daimler.mbcarkit.proto.Vehicleapi.AppTwinCommandStatusUpdatesByVIN;
+import com.daimler.mbcarkit.proto.Vehicleapi.AppTwinPendingCommandsRequest;
/**
* The {@link AccountHandler} acts as Bridge between MercedesMe Account and the associated vehicles
@@ -82,7 +90,9 @@ public class AccountHandler extends BaseBridgeHandler implements AccessTokenRefr
private Optional server = Optional.empty();
private Optional authService = Optional.empty();
- private Optional> scheduledFuture = Optional.empty();
+ private Optional> refreshScheduler = Optional.empty();
+ private List eventQueue = new ArrayList<>();
+ private boolean updateRunning = false;
private String capabilitiesEndpoint = "/v1/vehicle/%s/capabilities";
private String commandCapabilitiesEndpoint = "/v1/vehicle/%s/capabilities/commands";
@@ -128,13 +138,13 @@ public void initialize() {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
textKey + " [\"" + thing.getProperties().get("callbackUrl") + "\"]");
} else {
- scheduledFuture = Optional.of(scheduler.scheduleWithFixedDelay(this::update, 0,
+ refreshScheduler = Optional.of(scheduler.scheduleWithFixedDelay(this::refresh, 0,
config.get().refreshInterval, TimeUnit.MINUTES));
}
}
}
- public void update() {
+ public void refresh() {
if (server.isPresent()) {
if (!Constants.NOT_SET.equals(authService.get().getToken())) {
ws.run();
@@ -203,12 +213,13 @@ public void dispose() {
server = Optional.empty();
Utils.removePort(config.get().callbackPort);
}
- ws.interrupt();
- scheduledFuture.ifPresent(schedule -> {
+ refreshScheduler.ifPresent(schedule -> {
if (!schedule.isCancelled()) {
schedule.cancel(true);
}
});
+ ws.interrupt();
+ eventQueue.clear();
}
/**
@@ -217,7 +228,7 @@ public void dispose() {
@Override
public void onAccessTokenResponse(AccessTokenResponse tokenResponse) {
if (!Constants.NOT_SET.equals(tokenResponse.getAccessToken())) {
- scheduler.schedule(this::update, 2, TimeUnit.SECONDS);
+ scheduler.schedule(this::refresh, 2, TimeUnit.SECONDS);
} else if (server.isEmpty()) {
// server not running - fix first
String textKey = Constants.STATUS_TEXT_PREFIX + thing.getThingTypeUID().getId()
@@ -262,7 +273,7 @@ public void registerVin(String vin, VehicleHandler handler) {
activeVehicleHandlerMap.put(vin, handler);
VEPUpdate updateForVin = vepUpdateMap.get(vin);
if (updateForVin != null) {
- handler.distributeContent(updateForVin);
+ handler.enqueueUpdate(updateForVin);
}
}
@@ -284,12 +295,97 @@ public void getVehicleCapabilities(String vin) {
}
}
+ /**
+ * functions for websocket handling
+ */
+
+ public void enqueueMessage(byte[] data) {
+ synchronized (eventQueue) {
+ eventQueue.add(data);
+ scheduler.execute(this::scheduleMessage);
+ }
+ }
+
+ private void scheduleMessage() {
+ byte[] data;
+ synchronized (eventQueue) {
+ while (updateRunning) {
+ try {
+ eventQueue.wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ eventQueue.clear();
+ return;
+ }
+ }
+ if (!eventQueue.isEmpty()) {
+ data = eventQueue.remove(0);
+ } else {
+ return;
+ }
+ updateRunning = true;
+ }
+ try {
+ handleMessage(data);
+ } finally {
+ synchronized (eventQueue) {
+ updateRunning = false;
+ eventQueue.notifyAll();
+ }
+ }
+ }
+
+ private void handleMessage(byte[] array) {
+ try {
+ PushMessage pm = VehicleEvents.PushMessage.parseFrom(array);
+ if (pm.hasVepUpdates()) {
+ boolean distributed = distributeVepUpdates(pm.getVepUpdates().getUpdatesMap());
+ logger.trace("Distributed VEPUpdate {}", distributed);
+ if (distributed) {
+ AcknowledgeVEPUpdatesByVIN ack = AcknowledgeVEPUpdatesByVIN.newBuilder()
+ .setSequenceNumber(pm.getVepUpdates().getSequenceNumber()).build();
+ ClientMessage cm = ClientMessage.newBuilder().setAcknowledgeVepUpdatesByVin(ack).build();
+ ws.sendAcknowledgeMessage(cm);
+ }
+ } else if (pm.hasAssignedVehicles()) {
+ for (int i = 0; i < pm.getAssignedVehicles().getVinsCount(); i++) {
+ String vin = pm.getAssignedVehicles().getVins(0);
+ discovery(vin);
+ }
+ AcknowledgeAssignedVehicles ack = AcknowledgeAssignedVehicles.newBuilder().build();
+ ClientMessage cm = ClientMessage.newBuilder().setAcknowledgeAssignedVehicles(ack).build();
+ ws.sendAcknowledgeMessage(cm);
+ } else if (pm.hasApptwinCommandStatusUpdatesByVin()) {
+ AppTwinCommandStatusUpdatesByVIN csubv = pm.getApptwinCommandStatusUpdatesByVin();
+ commandStatusUpdate(csubv.getUpdatesByVinMap());
+ AcknowledgeAppTwinCommandStatusUpdatesByVIN ack = AcknowledgeAppTwinCommandStatusUpdatesByVIN
+ .newBuilder().setSequenceNumber(csubv.getSequenceNumber()).build();
+ ClientMessage cm = ClientMessage.newBuilder().setAcknowledgeApptwinCommandStatusUpdateByVin(ack)
+ .build();
+ ws.sendAcknowledgeMessage(cm);
+ } else if (pm.hasApptwinPendingCommandRequest()) {
+ AppTwinPendingCommandsRequest pending = pm.getApptwinPendingCommandRequest();
+ if (!pending.getAllFields().isEmpty()) {
+ logger.trace("Pending Command {}", pending.getAllFields());
+ }
+ } else if (pm.hasDebugMessage()) {
+ logger.trace("MB Debug Message: {}", pm.getDebugMessage().getMessage());
+ } else {
+ logger.trace("MB Message: {} not handled", pm.getAllFields());
+ }
+ } catch (IOException e) {
+ logger.trace("IOException decoding message {}", e.getMessage());
+ } catch (Error err) {
+ logger.debug("Error caught {}", err.getMessage());
+ }
+ }
+
public boolean distributeVepUpdates(Map map) {
List notFoundList = new ArrayList<>();
map.forEach((key, value) -> {
VehicleHandler h = activeVehicleHandlerMap.get(key);
if (h != null) {
- h.distributeContent(value);
+ h.enqueueUpdate(value);
} else {
if (value.getFullUpdate()) {
vepUpdateMap.put(key, value);
@@ -430,7 +526,7 @@ public void sendCommand(@Nullable ClientMessage cm) {
if (cm != null) {
ws.setCommand(cm);
}
- scheduler.schedule(this::update, 2, TimeUnit.SECONDS);
+ scheduler.schedule(this::refresh, 2, TimeUnit.SECONDS);
}
public void keepAlive(boolean b) {
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandler.java
index 40afa829c54e6..5087958788051 100644
--- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandler.java
+++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandler.java
@@ -134,6 +134,8 @@ public class VehicleHandler extends BaseThingHandler {
private JSONObject chargeGroupValueStorage = new JSONObject();
private Map hvacGroupValueStorage = new HashMap<>();
private String vehicleType = NOT_SET;
+ private List eventQueue = new ArrayList<>();
+ private boolean updateRunning = false;
Map eventStorage = new HashMap<>();
Optional accountHandler = Optional.empty();
@@ -182,6 +184,7 @@ public void dispose() {
accountHandler.ifPresent(ah -> {
ah.unregisterVin(config.get().vin);
});
+ eventQueue.clear();
super.dispose();
}
@@ -587,13 +590,49 @@ public void distributeCommandStatus(AppTwinCommandStatusUpdatesByPID cmdUpdates)
});
}
- public void distributeContent(VEPUpdate data) {
+ public void enqueueUpdate(VEPUpdate update) {
+ synchronized (eventQueue) {
+ eventQueue.add(update);
+ scheduler.execute(this::scheduleUpdate);
+ }
+ }
+
+ private void scheduleUpdate() {
+ VEPUpdate data;
+ synchronized (eventQueue) {
+ while (updateRunning) {
+ try {
+ eventQueue.wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ eventQueue.clear();
+ return;
+ }
+ }
+ if (!eventQueue.isEmpty()) {
+ data = eventQueue.remove(0);
+ } else {
+ return;
+ }
+ updateRunning = true;
+ }
+ try {
+ handleUpdate(data);
+ } finally {
+ synchronized (eventQueue) {
+ updateRunning = false;
+ eventQueue.notifyAll();
+ }
+ }
+ }
+
+ public void handleUpdate(VEPUpdate update) {
updateStatus(ThingStatus.ONLINE);
- boolean fullUpdate = data.getFullUpdate();
+ boolean fullUpdate = update.getFullUpdate();
/**
* Deliver proto update
*/
- String newProto = Utils.proto2Json(data, thing.getThingTypeUID());
+ String newProto = Utils.proto2Json(update, thing.getThingTypeUID());
String combinedProto = newProto;
ChannelUID protoUpdateChannelUID = new ChannelUID(thing.getUID(), GROUP_VEHICLE, OH_CHANNEL_PROTO_UPDATE);
ChannelStateMap oldProtoMap = eventStorage.get(protoUpdateChannelUID.getId());
@@ -609,7 +648,7 @@ public void distributeContent(VEPUpdate data) {
StringType.valueOf(combinedProto));
updateChannel(dataUpdateMap);
- Map atts = data.getAttributesMap();
+ Map atts = update.getAttributesMap();
/**
* handle "simple" values
*/
diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/MBWebsocket.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/MBWebsocket.java
index 206b4f9dee74e..1e2bc1cedd64f 100644
--- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/MBWebsocket.java
+++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/MBWebsocket.java
@@ -39,13 +39,6 @@
import org.slf4j.LoggerFactory;
import com.daimler.mbcarkit.proto.Client.ClientMessage;
-import com.daimler.mbcarkit.proto.Protos.AcknowledgeAssignedVehicles;
-import com.daimler.mbcarkit.proto.VehicleEvents;
-import com.daimler.mbcarkit.proto.VehicleEvents.AcknowledgeVEPUpdatesByVIN;
-import com.daimler.mbcarkit.proto.VehicleEvents.PushMessage;
-import com.daimler.mbcarkit.proto.Vehicleapi.AcknowledgeAppTwinCommandStatusUpdatesByVIN;
-import com.daimler.mbcarkit.proto.Vehicleapi.AppTwinCommandStatusUpdatesByVIN;
-import com.daimler.mbcarkit.proto.Vehicleapi.AppTwinPendingCommandsRequest;
/**
* {@link MBWebsocket} as socket endpoint to communicate with Mercedes
@@ -128,9 +121,10 @@ public void run() {
accountHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/mercedesme.account.status.websocket-failure");
logger.warn("Websocket handling exception: {}", t.getMessage());
- }
- synchronized (this) {
- running = false;
+ } finally {
+ synchronized (this) {
+ running = false;
+ }
}
}
@@ -157,7 +151,7 @@ private boolean sendMessage() {
return false;
}
- private void sendAcknowledgeMessage(ClientMessage message) {
+ public void sendAcknowledgeMessage(ClientMessage message) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
message.writeTo(baos);
@@ -169,10 +163,6 @@ private void sendAcknowledgeMessage(ClientMessage message) {
}
}
- public boolean isRunning() {
- return running;
- }
-
public void interrupt() {
synchronized (this) {
runTill = Instant.MIN;
@@ -202,47 +192,20 @@ public void keepAlive(boolean b) {
@OnWebSocketMessage
public void onBytes(InputStream is) {
try {
- PushMessage pm = VehicleEvents.PushMessage.parseFrom(is);
- if (pm.hasVepUpdates()) {
- boolean distributed = accountHandler.distributeVepUpdates(pm.getVepUpdates().getUpdatesMap());
- if (distributed) {
- AcknowledgeVEPUpdatesByVIN ack = AcknowledgeVEPUpdatesByVIN.newBuilder()
- .setSequenceNumber(pm.getVepUpdates().getSequenceNumber()).build();
- ClientMessage cm = ClientMessage.newBuilder().setAcknowledgeVepUpdatesByVin(ack).build();
- sendAcknowledgeMessage(cm);
- }
- } else if (pm.hasAssignedVehicles()) {
- for (int i = 0; i < pm.getAssignedVehicles().getVinsCount(); i++) {
- String vin = pm.getAssignedVehicles().getVins(0);
- accountHandler.discovery(vin);
- }
- AcknowledgeAssignedVehicles ack = AcknowledgeAssignedVehicles.newBuilder().build();
- ClientMessage cm = ClientMessage.newBuilder().setAcknowledgeAssignedVehicles(ack).build();
- sendAcknowledgeMessage(cm);
- } else if (pm.hasApptwinCommandStatusUpdatesByVin()) {
- AppTwinCommandStatusUpdatesByVIN csubv = pm.getApptwinCommandStatusUpdatesByVin();
- accountHandler.commandStatusUpdate(csubv.getUpdatesByVinMap());
- AcknowledgeAppTwinCommandStatusUpdatesByVIN ack = AcknowledgeAppTwinCommandStatusUpdatesByVIN
- .newBuilder().setSequenceNumber(csubv.getSequenceNumber()).build();
- ClientMessage cm = ClientMessage.newBuilder().setAcknowledgeApptwinCommandStatusUpdateByVin(ack)
- .build();
- sendAcknowledgeMessage(cm);
- } else if (pm.hasApptwinPendingCommandRequest()) {
- AppTwinPendingCommandsRequest pending = pm.getApptwinPendingCommandRequest();
- if (!pending.getAllFields().isEmpty()) {
- logger.trace("Pending Command {}", pending.getAllFields());
- }
- } else if (pm.hasDebugMessage()) {
- logger.trace("MB Debug Message: {}", pm.getDebugMessage().getMessage());
- } else {
- logger.trace("MB Message: {} not handled", pm.getAllFields());
- }
+ byte[] array = is.readAllBytes();
+ is.close();
+ accountHandler.enqueueMessage(array);
+ /**
+ * https://community.openhab.org/t/mercedes-me/136866/12
+ * Release Websocket thread as early as possible to avoid execeptions
+ *
+ * 1. Websocket thread responsible for reading stream in bytes and enqueue for AccountHandler.
+ * 2. AccountHamdler thread responsible for encoding proto message. In case of update enqueue proto message
+ * at VehicleHandöer
+ * 3. VehicleHandler responsible to update channels
+ */
} catch (IOException e) {
- // don't report thing status errors here.
- // Sometimes messages cannot be decoded which doesn't effect the overall functionality
- logger.trace("IOException {}", e.getMessage());
- } catch (Error err) {
- logger.trace("Error caught {}", err.getMessage());
+ logger.debug("IOException reading input stream {}", e.getMessage());
}
}
diff --git a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/internal/handler/ThingCallbackListener.java b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/internal/handler/ThingCallbackListener.java
index d785afedb1c47..f041deb805b39 100644
--- a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/internal/handler/ThingCallbackListener.java
+++ b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/internal/handler/ThingCallbackListener.java
@@ -12,8 +12,11 @@
*/
package org.openhab.binding.mercedesme.internal.handler;
+import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.mock;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -53,6 +56,7 @@ public class ThingCallbackListener implements ThingHandlerCallback {
public Map> updatesPerGroupMap = new HashMap<>();
public boolean linked = false;
public Optional status = Optional.empty();
+ private Instant waitTime = Instant.MAX;
public ThingStatusInfo getThingStatus() {
return status.get();
@@ -78,6 +82,23 @@ public void stateUpdated(ChannelUID channelUID, State state) {
}
}
groupMap.put(channelUID.toString(), state);
+ synchronized (updatesReceived) {
+ waitTime = Instant.now().plus(500, ChronoUnit.MILLIS);
+ }
+ }
+
+ public void waitForUpdates() {
+ Instant maxWaitTime = Instant.now().plus(5000, ChronoUnit.MILLIS);
+ synchronized (updatesReceived) {
+ while (Instant.now().isBefore(maxWaitTime) && waitTime.isAfter(Instant.now())) {
+ try {
+ updatesReceived.wait(50);
+ } catch (InterruptedException e) {
+ fail();
+ }
+ }
+ }
+ waitTime = Instant.MAX;
}
@Override
diff --git a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandlerTest.java b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandlerTest.java
index 8675b4d0ac43a..65cf867783e67 100644
--- a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandlerTest.java
+++ b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/internal/handler/VehicleHandlerTest.java
@@ -115,7 +115,8 @@ public void testBEVFullUpdateNoCapacities() {
String json = FileReader.readFileInString("src/test/resources/proto-json/MB-BEV-EQA.json");
VEPUpdate update = ProtoConverter.json2Proto(json, true);
- vh.distributeContent(update);
+ vh.enqueueUpdate(update);
+ updateListener.waitForUpdates();
assertEquals(GROUP_COUNT, updateListener.updatesPerGroupMap.size(), "Group Update Count");
assertEquals(10, updateListener.getUpdatesForGroup("doors"), "Doors Update Count");
@@ -151,7 +152,8 @@ public void testBEVImperialUnits() {
String json = FileReader.readFileInString("src/test/resources/proto-json/MB-BEV-ImperialUnits.json");
VEPUpdate update = ProtoConverter.json2Proto(json, true);
- vh.distributeContent(update);
+ vh.enqueueUpdate(update);
+ updateListener.waitForUpdates();
assertEquals(GROUP_COUNT, updateListener.updatesPerGroupMap.size(), "Group Update Count");
assertEquals(10, updateListener.getUpdatesForGroup("doors"), "Doors Update Count");
@@ -187,7 +189,9 @@ public void testBEVImperialUnits() {
// overwrite with EU Units
json = FileReader.readFileInString("src/test/resources/proto-json/MB-BEV-EQA.json");
update = ProtoConverter.json2Proto(json, true);
- vh.distributeContent(update);
+ vh.enqueueUpdate(update);
+ updateListener.waitForUpdates();
+
assertEquals("%.1f °C", patternMock.patternMap.get("test::bev:hvac#temperature"), "Temperature Pattern");
commandOptionMock.getCommandList("test::bev:hvac#temperature").forEach(cmd -> {
assertTrue(cmd.getCommand().endsWith(" °C"), "Command Option Celsius Unit");
@@ -209,7 +213,8 @@ public void testBEVCharging() {
String json = FileReader.readFileInString("src/test/resources/proto-json/MB-BEV-EQA-Charging.json");
VEPUpdate update = ProtoConverter.json2Proto(json, true);
- vh.distributeContent(update);
+ vh.enqueueUpdate(update);
+ updateListener.waitForUpdates();
assertEquals(GROUP_COUNT, updateListener.updatesPerGroupMap.size(), "Group Update Count");
assertEquals(10, updateListener.getUpdatesForGroup("doors"), "Doors Update Count");
@@ -246,13 +251,17 @@ public void testBEVChargeEndtime() {
String json = FileReader.readFileInString("src/test/resources/proto-json/MB-BEV-EQA-Charging-Weekday.json");
VEPUpdate update = ProtoConverter.json2Proto(json, true);
- vh.distributeContent(update);
+ vh.enqueueUpdate(update);
+ updateListener.waitForUpdates();
+
assertEquals("2023-09-09 13:54", ((DateTimeType) updateListener.getResponse("test::bev:charge#end-time"))
.format("%1$tY-%1$tm-%1$td %1$tH:%1$tM"), "End of Charge Time");
json = FileReader.readFileInString("src/test/resources/proto-json/MB-BEV-EQA-Charging-Weekday-Underrun.json");
update = ProtoConverter.json2Proto(json, true);
- vh.distributeContent(update);
+ vh.enqueueUpdate(update);
+ updateListener.waitForUpdates();
+
assertEquals("2023-09-11 13:55", ((DateTimeType) updateListener.getResponse("test::bev:charge#end-time"))
.format("%1$tY-%1$tm-%1$td %1$tH:%1$tM"), "End of Charge Time");
}
@@ -272,7 +281,9 @@ public void testBEVPartialChargingUpdate() {
String json = FileReader.readFileInString("src/test/resources/proto-json/PartialUpdate-Charging.json");
VEPUpdate update = ProtoConverter.json2Proto(json, false);
- vh.distributeContent(update);
+ vh.enqueueUpdate(update);
+ updateListener.waitForUpdates();
+
assertEquals(2, updateListener.updatesReceived.size(), "Update Count");
assertEquals("2023-09-19 20:45", ((DateTimeType) updateListener.getResponse("test::bev:charge#end-time"))
.format("%1$tY-%1$tm-%1$td %1$tH:%1$tM"), "End of Charge Time");
@@ -294,7 +305,8 @@ public void testBEVPartialGPSUpdate() {
String json = FileReader.readFileInString("src/test/resources/proto-json/PartialUpdate-GPS.json");
VEPUpdate update = ProtoConverter.json2Proto(json, false);
- vh.distributeContent(update);
+ vh.enqueueUpdate(update);
+ updateListener.waitForUpdates();
assertEquals(3, updateListener.updatesReceived.size(), "Update Count");
assertEquals("1.23,4.56", updateListener.getResponse("test::bev:position#gps").toFullString(), "GPS update");
assertEquals("41.9 °", updateListener.getResponse("test::bev:position#heading").toFullString(),
@@ -316,7 +328,9 @@ public void testBEVPartialRangeUpdate() {
String json = FileReader.readFileInString("src/test/resources/proto-json/PartialUpdate-Range.json");
VEPUpdate update = ProtoConverter.json2Proto(json, false);
- vh.distributeContent(update);
+ vh.enqueueUpdate(update);
+ updateListener.waitForUpdates();
+
assertEquals(3, updateListener.updatesReceived.size(), "Update Count");
assertEquals("15017 km", updateListener.getResponse("test::bev:range#mileage").toFullString(),
"Mileage Update");
@@ -341,7 +355,8 @@ public void testHybridFullUpdateNoCapacities() {
String json = FileReader.readFileInString("src/test/resources/proto-json/MB-Hybrid-Charging.json");
VEPUpdate update = ProtoConverter.json2Proto(json, true);
- vh.distributeContent(update);
+ vh.enqueueUpdate(update);
+ updateListener.waitForUpdates();
assertEquals(GROUP_COUNT, updateListener.updatesPerGroupMap.size(), "Group Update Count");
assertEquals(10, updateListener.getUpdatesForGroup("doors"), "Doors Update Count");
@@ -374,7 +389,8 @@ public void testHybridFullUpadteWithCapacities() {
String json = FileReader.readFileInString("src/test/resources/proto-json/MB-Hybrid-Charging.json");
VEPUpdate update = ProtoConverter.json2Proto(json, true);
- vh.distributeContent(update);
+ vh.enqueueUpdate(update);
+ updateListener.waitForUpdates();
// Test charged / uncharged battery and filled / unfilled tank volume
assertEquals("5.800000190734863 kWh", updateListener.getResponse("test::hybrid:range#charged").toFullString(),
@@ -403,7 +419,8 @@ public void testEventStorage() {
String json = FileReader.readFileInString("src/test/resources/proto-json/MB-BEV-EQA.json");
VEPUpdate update = ProtoConverter.json2Proto(json, true);
- vh.distributeContent(update);
+ vh.enqueueUpdate(update);
+ updateListener.waitForUpdates();
assertEquals(GROUP_COUNT, updateListener.updatesPerGroupMap.size(), "Group Update Count");
assertEquals(10, updateListener.getUpdatesForGroup("doors"), "Doors Update Count");
@@ -450,12 +467,14 @@ public void testProtoChannelLinked() {
String json = FileReader.readFileInString("src/test/resources/proto-json/MB-BEV-EQA.json");
VEPUpdate update = ProtoConverter.json2Proto(json, true);
- vh.distributeContent(update);
+ vh.enqueueUpdate(update);
+ updateListener.waitForUpdates();
assertFalse(updateListener.updatesReceived.containsKey("test::bev:vehicle#proto-update"),
"Proto Channel not updated");
updateListener.linked = true;
- vh.distributeContent(update);
+ vh.enqueueUpdate(update);
+ updateListener.waitForUpdates();
assertTrue(updateListener.updatesReceived.containsKey("test::bev:vehicle#proto-update"),
"Proto Channel not updated");
}
@@ -477,7 +496,8 @@ public void testTemperaturePoints() {
String json = FileReader.readFileInString("src/test/resources/proto-json/MB-Unknown.json");
VEPUpdate update = ProtoConverter.json2Proto(json, true);
- vh.distributeContent(update);
+ vh.enqueueUpdate(update);
+ updateListener.waitForUpdates();
assertEquals("22 °C", updateListener.getResponse("test::bev:hvac#temperature").toFullString(),
"Temperature Point One Updated");
@@ -508,7 +528,8 @@ public void testTemperaturePointSelection() {
vh.setCallback(updateListener);
String json = FileReader.readFileInString("src/test/resources/proto-json/MB-Unknown.json");
VEPUpdate update = ProtoConverter.json2Proto(json, true);
- vh.distributeContent(update);
+ vh.enqueueUpdate(update);
+ updateListener.waitForUpdates();
ChannelUID cuid = new ChannelUID(thingMock.getUID(), Constants.GROUP_HVAC, "temperature");
updateListener = new ThingCallbackListener();
@@ -538,7 +559,8 @@ public void testChargeProgramSelection() {
String json = FileReader.readFileInString("src/test/resources/proto-json/MB-BEV-EQA.json");
VEPUpdate update = ProtoConverter.json2Proto(json, true);
- vh.distributeContent(update);
+ vh.enqueueUpdate(update);
+ updateListener.waitForUpdates();
ChannelUID cuid = new ChannelUID(thingMock.getUID(), Constants.GROUP_CHARGE, "max-soc");
vh.handleCommand(cuid, QuantityType.valueOf("90 %"));
@@ -586,7 +608,8 @@ public void testPositioning() {
String json = FileReader.readFileInString("src/test/resources/proto-json/MB-BEV-EQA.json");
VEPUpdate update = ProtoConverter.json2Proto(json, true);
- vHandler.distributeContent(update);
+ vHandler.enqueueUpdate(update);
+ updateListener.waitForUpdates();
assertEquals(POSITIONING_UPDATE_COUNT, updateListener.getUpdatesForGroup("position"), "Position Update Count");
assertEquals("1.23,4.56", updateListener.getResponse("test::bev:position#gps").toFullString(),
@@ -608,7 +631,8 @@ public void testHVAC() {
String json = FileReader.readFileInString("src/test/resources/proto-json/MB-BEV-EQA.json");
VEPUpdate update = ProtoConverter.json2Proto(json, true);
- vHandler.distributeContent(update);
+ vHandler.enqueueUpdate(update);
+ updateListener.waitForUpdates();
assertEquals(HVAC_UPDATE_COUNT, updateListener.getUpdatesForGroup("hvac"), "HVAC Update Count");
assertEquals(0, ((DecimalType) updateListener.getResponse("test::bev:hvac#ac-status")).intValue(),
@@ -627,7 +651,8 @@ public void testEcoScore() {
String json = FileReader.readFileInString("src/test/resources/proto-json/MB-BEV-EQA.json");
VEPUpdate update = ProtoConverter.json2Proto(json, true);
- vHandler.distributeContent(update);
+ vHandler.enqueueUpdate(update);
+ updateListener.waitForUpdates();
assertEquals("72 %", updateListener.getResponse("test::bev:eco#accel").toFullString(), "Eco Acceleration");
assertEquals("81 %", updateListener.getResponse("test::bev:eco#coasting").toFullString(), "Eco Coasting");
@@ -647,7 +672,8 @@ public void testAdBlue() {
String json = FileReader.readFileInString("src/test/resources/proto-json/MB-Combustion.json");
VEPUpdate update = ProtoConverter.json2Proto(json, true);
- vHandler.distributeContent(update);
+ vHandler.enqueueUpdate(update);
+ updateListener.waitForUpdates();
assertEquals("29 %", updateListener.getResponse("test::combustion:range#adblue-level").toFullString(),
"AdBlue Tank Level");
diff --git a/bundles/org.openhab.binding.meteostick/src/main/java/org/openhab/binding/meteostick/internal/handler/MeteostickBridgeHandler.java b/bundles/org.openhab.binding.meteostick/src/main/java/org/openhab/binding/meteostick/internal/handler/MeteostickBridgeHandler.java
index 51b90a0332eed..8e66c8b0f3390 100644
--- a/bundles/org.openhab.binding.meteostick/src/main/java/org/openhab/binding/meteostick/internal/handler/MeteostickBridgeHandler.java
+++ b/bundles/org.openhab.binding.meteostick/src/main/java/org/openhab/binding/meteostick/internal/handler/MeteostickBridgeHandler.java
@@ -27,6 +27,7 @@
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
+import org.openhab.binding.meteostick.internal.MeteostickBindingConstants;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.transport.serial.PortInUseException;
import org.openhab.core.io.transport.serial.SerialPort;
@@ -222,6 +223,10 @@ private void sendToMeteostick(String string) {
private class ReceiveThread extends Thread implements SerialPortEventListener {
private final Logger logger = LoggerFactory.getLogger(ReceiveThread.class);
+ public ReceiveThread() {
+ super(String.format("OH-binding-%s-%s", MeteostickBindingConstants.BINDING_ID, "Receiver"));
+ }
+
@Override
public void serialEvent(SerialPortEvent arg0) {
try {
diff --git a/bundles/org.openhab.binding.metofficedatahub/README.md b/bundles/org.openhab.binding.metofficedatahub/README.md
index ca948b58d709e..62d97211f33c7 100644
--- a/bundles/org.openhab.binding.metofficedatahub/README.md
+++ b/bundles/org.openhab.binding.metofficedatahub/README.md
@@ -14,8 +14,8 @@ A possible use case could be to pull forecast data, for the next day to determin
## Prerequisite
In order to use this binding, you will need a Met Office Data Hub account.
-Once created you will need to create a plan for access to the "Site Specific" subscriptions.
-This will give you the client id and secret required for the bridge.
+Once created you will need to create a plan for access to the "Site Specific" Global Spot subscriptions.
+This will give you the API key required for the bridge.
## Supported Things
@@ -24,14 +24,14 @@ You can then add things to get the forecast's for a specific location (site), us
This binding supports the follow thing types:
-| Type UID | Discovery | Description |
-|-----------|-----------|---------------------------------------------------------------------------------------------|
-| bridge | Manual | A single connection to the Met Office DataHub API with daily poll limiting for the Site API |
-| site | Manual | Provides the hourly and daily forecast data for a give location (site) |
+| Type UID | Discovery | Description |
+|----------|-----------|---------------------------------------------------------------------------------------------|
+| account | Manual | A single connection to the Met Office DataHub API with daily poll limiting for the Site API |
+| site | Manual | Provides the hourly and daily forecast data for a give location (site) |
## Configuration
-### `bridge` Configuration
+### `account` Configuration
The bridge counts the total number of requests from 00:00 -> 23:59 under its properties during the runtime of the system.
(This reset's if OH restarts, or the binding resets).
@@ -167,7 +167,7 @@ current-forecast-**plus02**#air-temp-current
### Configuration (*.things)
```java
-Bridge metofficedatahub:site:metoffice [siteRateDailyLimit=200, siteApiKey=""] {
+Bridge metofficedatahub:account:metoffice [siteRateDailyLimit=200, siteApiKey=""] {
site londonForecast "London Forecast" [hourlyForecastPollRate=1, dailyForecastPollRate=3, location="51.509865,-0.118092"]
}
```
@@ -192,7 +192,7 @@ Number:Speed ForecastLondonPrecipitationRate (gLondonCurren
Number:Length ForecastLondonPrecipitationAmount (gLondonCurrentHour) { unit="mm",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#precip-total" }
Number:Length ForecastLondonSnowAmount (gLondonCurrentHour) { unit="mm",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#snow-total" }
Number:Dimensionless ForecastLondonUvIndex (gLondonCurrentHour) { channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#uv-index" }
-Number:Pressure ForecastLondonpressure (gLondonCurrentHour) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#pressure" }
+Number:Pressure ForecastLondonPressure (gLondonCurrentHour) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#pressure" }
Number:Speed ForecastLondon10mWindSpeed (gLondonCurrentHour) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#wind-speed" }
Number:Speed ForecastLondon10mGustWindSpeed (gLondonCurrentHour) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#wind-speed-gust" }
Number:Speed ForecastLondon10mMaxGustWindSpeed (gLondonCurrentHour) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#wind-gust-max" }
@@ -213,7 +213,7 @@ Number:Dimensionless ForecastLondonPlus01PrecipitationProb (gLondon
Number:Length ForecastLondonPlus01PrecipitationAmount (gLondonNextHour) { unit="mm",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#precip-total" }
Number:Length ForecastLondonPlus01SnowAmount (gLondonNextHour) { unit="mm",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#snow-total" }
Number:Dimensionless ForecastLondonPlus01UvIndex (gLondonNextHour) { channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#uv-index" }
-Number:Pressure ForecastLondonPlus01pressure (gLondonNextHour) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#pressure" }
+Number:Pressure ForecastLondonPlus01Pressure (gLondonNextHour) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#pressure" }
Number:Speed ForecastLondonPlus0110mWindSpeed (gLondonNextHour) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#wind-speed" }
Number:Speed ForecastLondonPlus0110mGustWindSpeed (gLondonNextHour) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#wind-speed-gust" }
Number:Speed ForecastLondonPlus0110mMaxGustWindSpeed (gLondonNextHour) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#wind-gust-max" }
@@ -224,21 +224,21 @@ Number:Temperature ForecastLondonPlus01DewPointTemp (gLondon
#### Daily Forecast `example.items`
```java
-Group gdaily-forecast "Current Daily Forecast"
-Group gLondonCurrentDay "London Current Forecast" (gLondon,gdaily-forecast)
+Group gDailyForecast "Current Daily Forecast"
+Group gLondonCurrentDay "London Current Forecast" (gLondon,gDailyForecast)
DateTime ForecastLondonDailyForecastTs (gLondonCurrentDay) { channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#forecast-ts" }
Number:Speed ForecastLondonMiddayWindSpeed10m (gLondonCurrentDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#wind-speed-day" }
Number:Speed ForecastLondonMidnightWindSpeed10m (gLondonCurrentDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#wind-speed-night" }
-Number:Angle ForecastLondonMidday10MWindDirection (gLondonCurrentDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#wind-direction-day" }
-Number:Angle ForecastLondonMidnight10MWindDirection (gLondonCurrentDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#wind-direction-night" }
+Number:Angle ForecastLondonMidday10MWindDirection (gLondonCurrentDay) { unit="°",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#wind-direction-day" }
+Number:Angle ForecastLondonMidnight10MWindDirection (gLondonCurrentDay) { unit="°",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#wind-direction-night" }
Number:Speed ForecastLondonMidday10mWindGust (gLondonCurrentDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#wind-gust-day" }
Number:Speed ForecastLondonMidnight10mWindGust (gLondonCurrentDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#wind-gust-night" }
Number:Length ForecastLondonMiddayVisibility (gLondonCurrentDay) { unit="m",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#visibility-day" }
Number:Length ForecastLondonMidnightVisibility (gLondonCurrentDay) { unit="m",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#visibility-night" }
Number:Dimensionless ForecastLondonMiddayRelativeHumidity (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#humidity-day" }
Number:Dimensionless ForecastLondonMidnightRelativeHumidity (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#humidity-night" }
-Number:Pressure ForecastLondonMiddaypressure (gLondonCurrentDay) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#pressure-day" }
-Number:Pressure ForecastLondonMidnightpressure (gLondonCurrentDay) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#pressure-night" }
+Number:Pressure ForecastLondonMiddaypressure (gLondonCurrentDay) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#pressure-day" }
+Number:Pressure ForecastLondonMidnightpressure (gLondonCurrentDay) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#pressure-night" }
Number:Dimensionless ForecastLondonMaxUvIndex (gLondonCurrentDay) { channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#uv-max" }
Number:Temperature ForecastLondonNightUpperBoundMinTemp (gLondonCurrentDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#temp-min-ub-night" }
Number:Temperature ForecastLondonDayLowerBoundMaxTemp (gLondonCurrentDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#temp-max-lb-day" }
@@ -266,21 +266,21 @@ Number:Dimensionless ForecastLondonNightProbabilityOfHail (gLondonCurr
Number:Dimensionless ForecastLondonDayProbabilityOfSferics (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#sferics-prob-day" }
Number:Dimensionless ForecastLondonNightProbabilityOfSferics (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#sferics-prob-night" }
-Group gCurrentDailyPlus01Forecast "Current Day +1 Daily Forecast"
-Group gLondonNextDay "London Next Day Forecast" (gLondon,gCurrentDailyPlus01Forecast)
+Group gDailyPlus01Forecast "Current Day +1 Daily Forecast"
+Group gLondonNextDay "London Next Day Forecast" (gLondon,gDailyPlus01Forecast)
DateTime ForecastLondonPlus01DailyForecastTs (gLondonNextDay) { channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#forecast-ts" }
Number:Speed ForecastLondonPlus01MiddayWindSpeed10m (gLondonNextDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#wind-speed-day" }
Number:Speed ForecastLondonPlus01MidnightWindSpeed10m (gLondonNextDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#wind-speed-night" }
-Number:Angle ForecastLondonPlus01Midday10MWindDirection (gLondonNextDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#wind-direction-day" }
-Number:Angle ForecastLondonPlus01Midnight10MWindDirection (gLondonNextDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#wind-direction-night" }
+Number:Angle ForecastLondonPlus01Midday10MWindDirection (gLondonNextDay) { unit="°",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#wind-direction-day" }
+Number:Angle ForecastLondonPlus01Midnight10MWindDirection (gLondonNextDay) { unit="°",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#wind-direction-night" }
Number:Speed ForecastLondonPlus01Midday10mWindGust (gLondonNextDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#wind-gust-day" }
Number:Speed ForecastLondonPlus01Midnight10mWindGust (gLondonNextDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#wind-gust-night" }
Number:Length ForecastLondonPlus01MiddayVisibility (gLondonNextDay) { unit="m",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#visibility-day" }
Number:Length ForecastLondonPlus01MidnightVisibility (gLondonNextDay) { unit="m",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#visibility-night" }
Number:Dimensionless ForecastLondonPlus01MiddayRelativeHumidity (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#humidity-day" }
Number:Dimensionless ForecastLondonPlus01MidnightRelativeHumidity (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#humidity-night" }
-Number:Pressure ForecastLondonPlus01Middaypressure (gLondonNextDay) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#pressure-day" }
-Number:Pressure ForecastLondonPlus01Midnightpressure (gLondonNextDay) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#pressure-night" }
+Number:Pressure ForecastLondonPlus01MiddayPressure (gLondonNextDay) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#pressure-day" }
+Number:Pressure ForecastLondonPlus01MidnightPressure (gLondonNextDay) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#pressure-night" }
Number:Dimensionless ForecastLondonPlus01MaxUvIndex (gLondonNextDay) { channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#uv-max" }
Number:Temperature ForecastLondonPlus01NightUpperBoundMinTemp (gLondonNextDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#temp-min-ub-night" }
Number:Temperature ForecastLondonPlus01DayLowerBoundMaxTemp (gLondonNextDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#temp-max-lb-day" }
@@ -327,7 +327,7 @@ Frame {
Text item=ForecastLondonPrecipitationAmount icon="rain"
Text item=ForecastLondonSnowAmount icon="rain"
Text item=ForecastLondonUvIndex icon="sun"
- Text item=ForecastLondonpressure icon="pressure"
+ Text item=ForecastLondonPressure icon="pressure"
Text item=ForecastLondon10mWindSpeed icon="wind"
Text item=ForecastLondon10mGustWindSpeed icon="wind"
Text item=ForecastLondon10mMaxGustWindSpeed icon="wind"
@@ -348,7 +348,7 @@ Frame {
Text item=ForecastLondonPlus01PrecipitationAmount icon="rain"
Text item=ForecastLondonPlus01SnowAmount icon="rain"
Text item=ForecastLondonPlus01UvIndex icon="sun"
- Text item=ForecastLondonPlus01pressure icon="pressure"
+ Text item=ForecastLondonPlus01Pressure icon="pressure"
Text item=ForecastLondonPlus0110mWindSpeed icon="wind"
Text item=ForecastLondonPlus0110mGustWindSpeed icon="wind"
Text item=ForecastLondonPlus0110mMaxGustWindSpeed icon="wind"
@@ -414,8 +414,8 @@ Frame {
Text item=ForecastLondonPlus01MidnightVisibility icon="sun_clouds"
Text item=ForecastLondonPlus01MiddayRelativeHumidity icon="humidity"
Text item=ForecastLondonPlus01MidnightRelativeHumidity icon="humidity"
- Text item=ForecastLondonPlus01Middaypressure icon="pressure"
- Text item=ForecastLondonPlus01Midnightpressure icon="pressure"
+ Text item=ForecastLondonPlus01MiddayPressure icon="pressure"
+ Text item=ForecastLondonPlus01MidnightPressure icon="pressure"
Text item=ForecastLondonPlus01MaxUvIndex icon="pressure"
Text item=ForecastLondonPlus01NightUpperBoundMinTemp icon="temperature"
Text item=ForecastLondonPlus01DayLowerBoundMaxTemp icon="temperature"
diff --git a/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/socket/XiaomiSocket.java b/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/socket/XiaomiSocket.java
index 1119057d5bd0d..503f38e4d4eb7 100644
--- a/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/socket/XiaomiSocket.java
+++ b/bundles/org.openhab.binding.mihome/src/main/java/org/openhab/binding/mihome/internal/socket/XiaomiSocket.java
@@ -12,6 +12,8 @@
*/
package org.openhab.binding.mihome.internal.socket;
+import static org.openhab.binding.mihome.internal.XiaomiGatewayBindingConstants.BINDING_ID;
+
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
@@ -71,7 +73,7 @@ public XiaomiSocket(String owner) {
*/
public XiaomiSocket(int port, String owner) {
this.port = port;
- socketReceiveThread.setName("XiaomiSocketReceiveThread(" + port + ", " + owner + ")");
+ socketReceiveThread.setName("OH-binding-" + BINDING_ID + "-XiaomiSocket(" + port + ", " + owner + ")");
}
public void initialize() {
diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/discovery/MiIoDiscovery.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/discovery/MiIoDiscovery.java
index a62af789566bf..a74c2d73de8c1 100644
--- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/discovery/MiIoDiscovery.java
+++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/discovery/MiIoDiscovery.java
@@ -32,6 +32,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.miio.internal.Message;
+import org.openhab.binding.miio.internal.MiIoBindingConstants;
import org.openhab.binding.miio.internal.MiIoDevices;
import org.openhab.binding.miio.internal.Utils;
import org.openhab.binding.miio.internal.cloud.CloudConnector;
@@ -352,6 +353,11 @@ private synchronized void stopReceiverThreat() {
*
*/
private class ReceiverThread extends Thread {
+
+ public ReceiverThread() {
+ super(String.format("OH-binding-%s-%s", MiIoBindingConstants.BINDING_ID, "Receiver"));
+ }
+
@Override
public void run() {
DatagramSocket socket = getSocket();
diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/transport/MiIoAsyncCommunication.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/transport/MiIoAsyncCommunication.java
index 4c0baa0baa6af..09b4d7e858be3 100644
--- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/transport/MiIoAsyncCommunication.java
+++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/transport/MiIoAsyncCommunication.java
@@ -260,7 +260,7 @@ private class MessageSenderThread extends Thread {
private final String deviceId;
public MessageSenderThread(String deviceId) {
- super("OH-binding-miio-MessageSenderThread-" + deviceId);
+ super(String.format("OH-binding-%s-%s-%s", MiIoBindingConstants.BINDING_ID, "Sender", deviceId));
setDaemon(true);
this.deviceId = deviceId;
}
diff --git a/bundles/org.openhab.binding.milight/src/main/java/org/openhab/binding/milight/internal/protocol/MilightV6SessionManager.java b/bundles/org.openhab.binding.milight/src/main/java/org/openhab/binding/milight/internal/protocol/MilightV6SessionManager.java
index 0d04f8bc4c879..8713451fba7e7 100644
--- a/bundles/org.openhab.binding.milight/src/main/java/org/openhab/binding/milight/internal/protocol/MilightV6SessionManager.java
+++ b/bundles/org.openhab.binding.milight/src/main/java/org/openhab/binding/milight/internal/protocol/MilightV6SessionManager.java
@@ -32,6 +32,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.milight.internal.MilightBindingConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -185,7 +186,7 @@ public MilightV6SessionManager(String bridgeId, ISessionState observer, @Nullabl
throw new IllegalArgumentException("keepAliveInterval not within given limits!");
}
- sessionThread = new Thread(this, "SessionThread");
+ sessionThread = new Thread(this, "OH-binding-" + MilightBindingConstants.BINDING_ID + "-SessionThread");
}
/**
diff --git a/bundles/org.openhab.binding.milight/src/main/java/org/openhab/binding/milight/internal/protocol/QueuedSend.java b/bundles/org.openhab.binding.milight/src/main/java/org/openhab/binding/milight/internal/protocol/QueuedSend.java
index 8666591e45d92..b968f3bb24652 100644
--- a/bundles/org.openhab.binding.milight/src/main/java/org/openhab/binding/milight/internal/protocol/QueuedSend.java
+++ b/bundles/org.openhab.binding.milight/src/main/java/org/openhab/binding/milight/internal/protocol/QueuedSend.java
@@ -21,6 +21,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.milight.internal.MilightBindingConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -48,7 +49,7 @@ public class QueuedSend implements Runnable, Closeable {
*/
public void start() {
willbeclosed = false;
- thread = new Thread(this);
+ thread = new Thread(this, "OH-binding-" + MilightBindingConstants.BINDING_ID + "-QueuedSend");
thread.start();
}
diff --git a/bundles/org.openhab.binding.milight/src/main/java/org/openhab/binding/milight/internal/test/EmulatedV6Bridge.java b/bundles/org.openhab.binding.milight/src/main/java/org/openhab/binding/milight/internal/test/EmulatedV6Bridge.java
index 243a912d726c1..75e5872a55df1 100644
--- a/bundles/org.openhab.binding.milight/src/main/java/org/openhab/binding/milight/internal/test/EmulatedV6Bridge.java
+++ b/bundles/org.openhab.binding.milight/src/main/java/org/openhab/binding/milight/internal/test/EmulatedV6Bridge.java
@@ -92,8 +92,8 @@ public class EmulatedV6Bridge {
FAKE_MAC[2], FAKE_MAC[3], FAKE_MAC[4], FAKE_MAC[5], 1 };
EmulatedV6Bridge() {
- new Thread(this::runDiscovery).start();
- new Thread(this::runBrigde).start();
+ new Thread(this::runDiscovery, "OH-binding-" + MilightBindingConstants.BINDING_ID + "-runDiscovery").start();
+ new Thread(this::runBridge, "OH-binding-" + MilightBindingConstants.BINDING_ID + "-runBridge").start();
}
private void replaceWithMac(byte[] data, int offset) {
@@ -144,7 +144,7 @@ public void runDiscovery() {
}
}
- public void runBrigde() {
+ public void runBridge() {
try {
byte[] a = new byte[0];
DatagramPacket sPacket = new DatagramPacket(a, a.length);
diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/MPDBindingConstants.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/MPDBindingConstants.java
index b6b2b8ed89258..2259da9f55a6a 100644
--- a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/MPDBindingConstants.java
+++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/MPDBindingConstants.java
@@ -24,7 +24,7 @@
@NonNullByDefault
public class MPDBindingConstants {
- private static final String BINDING_ID = "mpd";
+ public static final String BINDING_ID = "mpd";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_MPD = new ThingTypeUID(BINDING_ID, "mpd");
diff --git a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDConnectionThread.java b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDConnectionThread.java
index ec57db499466b..740dbca52f698 100644
--- a/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDConnectionThread.java
+++ b/bundles/org.openhab.binding.mpd/src/main/java/org/openhab/binding/mpd/internal/protocol/MPDConnectionThread.java
@@ -24,6 +24,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mpd.internal.MPDBindingConstants;
import org.openhab.binding.mpd.internal.MPDException;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
@@ -58,6 +59,7 @@ public class MPDConnectionThread extends Thread {
private AtomicBoolean disposed = new AtomicBoolean(false);
public MPDConnectionThread(MPDResponseListener listener, String address, Integer port, String password) {
+ super(String.format("OH-binding-%s-%s", MPDBindingConstants.BINDING_ID, "Connection"));
this.listener = listener;
this.address = address;
this.port = port;
diff --git a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/PercentageValue.java b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/PercentageValue.java
index e2d37a7b506a0..6a521fbe54752 100644
--- a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/PercentageValue.java
+++ b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/PercentageValue.java
@@ -54,9 +54,10 @@ public class PercentageValue extends Value {
private final BigDecimal stepPercent;
private final @Nullable String onValue;
private final @Nullable String offValue;
+ private final @Nullable String formatOverride;
public PercentageValue(@Nullable BigDecimal min, @Nullable BigDecimal max, @Nullable BigDecimal step,
- @Nullable String onValue, @Nullable String offValue) {
+ @Nullable String onValue, @Nullable String offValue, @Nullable String formatOverride) {
super(CoreItemFactory.DIMMER, List.of(DecimalType.class, QuantityType.class, IncreaseDecreaseType.class,
OnOffType.class, UpDownType.class, StringType.class));
this.onValue = onValue;
@@ -69,6 +70,7 @@ public PercentageValue(@Nullable BigDecimal min, @Nullable BigDecimal max, @Null
this.span = this.max.subtract(this.min);
this.step = step == null ? BigDecimal.ONE : step;
this.stepPercent = this.step.multiply(HUNDRED).divide(this.span, MathContext.DECIMAL128);
+ this.formatOverride = formatOverride;
}
@Override
@@ -135,7 +137,10 @@ public Command parseCommand(Command command) throws IllegalArgumentException {
@Override
public String getMQTTpublishValue(Command command, @Nullable String pattern) {
- String formatPattern = pattern;
+ String formatPattern = this.formatOverride;
+ if (formatPattern == null) {
+ formatPattern = pattern;
+ }
if (formatPattern == null) {
formatPattern = "%s";
}
@@ -170,7 +175,7 @@ public String getMQTTpublishValue(Command command, @Nullable String pattern) {
@Override
public StateDescriptionFragmentBuilder createStateDescription(boolean readOnly) {
- return super.createStateDescription(readOnly).withMaximum(HUNDRED).withMinimum(BigDecimal.ZERO).withStep(step)
- .withPattern("%.0f %%");
+ return super.createStateDescription(readOnly).withMaximum(HUNDRED).withMinimum(BigDecimal.ZERO)
+ .withStep(stepPercent).withPattern("%.0f %%");
}
}
diff --git a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/ValueFactory.java b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/ValueFactory.java
index 2a361d84276b0..32b2826e9ea06 100644
--- a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/ValueFactory.java
+++ b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/ValueFactory.java
@@ -54,7 +54,7 @@ public static Value createValueState(ChannelConfig config, String channelTypeID)
value = new NumberValue(config.min, config.max, config.step, UnitUtils.parseUnit(config.unit));
break;
case MqttBindingConstants.DIMMER:
- value = new PercentageValue(config.min, config.max, config.step, config.on, config.off);
+ value = new PercentageValue(config.min, config.max, config.step, config.on, config.off, null);
break;
case MqttBindingConstants.COLOR_HSB:
value = new ColorValue(ColorMode.HSB, config.on, config.off, config.onBrightness);
diff --git a/bundles/org.openhab.binding.mqtt.generic/src/test/java/org/openhab/binding/mqtt/generic/ChannelStateTests.java b/bundles/org.openhab.binding.mqtt.generic/src/test/java/org/openhab/binding/mqtt/generic/ChannelStateTests.java
index 2fe0a9f611200..5f960f691d05a 100644
--- a/bundles/org.openhab.binding.mqtt.generic/src/test/java/org/openhab/binding/mqtt/generic/ChannelStateTests.java
+++ b/bundles/org.openhab.binding.mqtt.generic/src/test/java/org/openhab/binding/mqtt/generic/ChannelStateTests.java
@@ -267,7 +267,7 @@ public void receiveDecimalAsPercentageUnitTest() {
@Test
public void receivePercentageTest() {
PercentageValue value = new PercentageValue(new BigDecimal(-100), new BigDecimal(100), new BigDecimal(10), null,
- null);
+ null, null);
ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
diff --git a/bundles/org.openhab.binding.mqtt.generic/src/test/java/org/openhab/binding/mqtt/generic/values/ValueTests.java b/bundles/org.openhab.binding.mqtt.generic/src/test/java/org/openhab/binding/mqtt/generic/values/ValueTests.java
index dd95a8e784b45..b602e16897c4a 100644
--- a/bundles/org.openhab.binding.mqtt.generic/src/test/java/org/openhab/binding/mqtt/generic/values/ValueTests.java
+++ b/bundles/org.openhab.binding.mqtt.generic/src/test/java/org/openhab/binding/mqtt/generic/values/ValueTests.java
@@ -17,6 +17,7 @@
import static org.junit.jupiter.api.Assertions.*;
import java.math.BigDecimal;
+import java.math.MathContext;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -99,7 +100,7 @@ public void illegalNumberCommand() {
@Test
public void illegalPercentCommand() {
- PercentageValue v = new PercentageValue(null, null, null, null, null);
+ PercentageValue v = new PercentageValue(null, null, null, null, null, null);
assertThrows(IllegalStateException.class, () -> v.parseCommand(new StringType("demo")));
}
@@ -111,7 +112,7 @@ public void illegalOnOffCommand() {
@Test
public void illegalPercentUpdate() {
- PercentageValue v = new PercentageValue(null, null, null, null, null);
+ PercentageValue v = new PercentageValue(null, null, null, null, null, null);
assertThrows(IllegalArgumentException.class, () -> v.parseCommand(new DecimalType(101.0)));
}
@@ -304,7 +305,9 @@ public void rollershutterUpdateWithOutStrings() {
@Test
public void percentCalc() {
PercentageValue v = new PercentageValue(new BigDecimal(10.0), new BigDecimal(110.0), new BigDecimal(1.0), null,
- null);
+ null, null);
+ assertThat(v.createStateDescription(false).build().getStep(), is(new BigDecimal(1)));
+
assertThat(v.parseCommand(new DecimalType("110.0")), is(PercentType.HUNDRED));
assertThat(v.getMQTTpublishValue(PercentType.HUNDRED, null), is("110"));
assertThat(v.parseCommand(new DecimalType(10.0)), is(PercentType.ZERO));
@@ -316,9 +319,20 @@ public void percentCalc() {
assertThat(v.getMQTTpublishValue(OnOffType.OFF, null), is("10"));
}
+ @Test
+ public void percentFormatOverride() {
+ PercentageValue v = new PercentageValue(BigDecimal.ZERO, new BigDecimal(3.0), null, null, null, "%.0f");
+ assertThat(v.createStateDescription(false).build().getStep(),
+ is(new BigDecimal(100).divide(new BigDecimal(3), MathContext.DECIMAL128)));
+ assertThat(v.getMQTTpublishValue(PercentType.HUNDRED, null), is("3"));
+ assertThat(v.getMQTTpublishValue(PercentType.valueOf("67"), null), is("2"));
+ assertThat(v.getMQTTpublishValue(PercentType.valueOf("33"), null), is("1"));
+ assertThat(v.getMQTTpublishValue(PercentType.ZERO, null), is("0"));
+ }
+
@Test
public void percentMQTTValue() {
- PercentageValue v = new PercentageValue(null, null, null, null, null);
+ PercentageValue v = new PercentageValue(null, null, null, null, null, null);
assertThat(v.parseCommand(new DecimalType("10.10000")), is(new PercentType("10.1")));
assertThat(v.getMQTTpublishValue(new PercentType("10.1"), null), is("10.1"));
Command command;
@@ -333,7 +347,7 @@ public void percentMQTTValue() {
@Test
public void percentCustomOnOff() {
PercentageValue v = new PercentageValue(new BigDecimal("0.0"), new BigDecimal("100.0"), new BigDecimal("1.0"),
- "on", "off");
+ "on", "off", null);
assertThat(v.parseCommand(new StringType("on")), is(OnOffType.ON));
assertThat(v.getMQTTpublishValue(OnOffType.ON, "%s"), is("on"));
assertThat(v.parseCommand(new StringType("off")), is(OnOffType.OFF));
@@ -343,7 +357,7 @@ public void percentCustomOnOff() {
@Test
public void decimalCalc() {
PercentageValue v = new PercentageValue(new BigDecimal("0.1"), new BigDecimal("1.0"), new BigDecimal("0.1"),
- null, null);
+ null, null, null);
assertThat(v.parseCommand(new DecimalType(1.0)), is(PercentType.HUNDRED));
assertThat(v.parseCommand(new DecimalType(0.1)), is(PercentType.ZERO));
PercentType command = (PercentType) v.parseCommand(new DecimalType(0.2));
@@ -353,7 +367,7 @@ public void decimalCalc() {
@Test
public void increaseDecreaseCalc() {
PercentageValue v = new PercentageValue(new BigDecimal("1.0"), new BigDecimal("11.0"), new BigDecimal("0.5"),
- null, null);
+ null, null, null);
// Normal operation.
PercentType command = (PercentType) v.parseCommand(new DecimalType("6.0"));
@@ -382,7 +396,7 @@ public void increaseDecreaseCalc() {
@Test
public void upDownCalc() {
PercentageValue v = new PercentageValue(new BigDecimal("1.0"), new BigDecimal("11.0"), new BigDecimal("0.5"),
- null, null);
+ null, null, null);
// Normal operation.
PercentType command = (PercentType) v.parseCommand(new DecimalType("6.0"));
@@ -411,7 +425,7 @@ public void upDownCalc() {
@Test
public void percentCalcInvalid() {
PercentageValue v = new PercentageValue(new BigDecimal(10.0), new BigDecimal(110.0), new BigDecimal(1.0), null,
- null);
+ null, null);
assertThrows(IllegalArgumentException.class, () -> v.parseCommand(new DecimalType(9.0)));
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/README.md b/bundles/org.openhab.binding.mqtt.homeassistant/README.md
index 355045262e589..b3e343f11d20d 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/README.md
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/README.md
@@ -72,6 +72,17 @@ Base64 encoding is not supported
| state | String | RO | The current state of the cover, possibly including opening, closing, or stopped. |
| json-attributes | String | RO | Additional attributes, as a serialized JSON string. |
+### [Device Tracker](https://www.home-assistant.io/integrations/device_tracker.mqtt/)
+
+| Channel ID | Type | R/W | Description |
+|-----------------|---------------|-----|------------------------------------------------------------------------------------------------------------------------------------|
+| home | Switch | RO | If the tracker reports itself as home or not home. |
+| location-name | String | RO | The arbitrary location the tracker reports itself as at (can often be "home" or "not_home"). |
+| location | Location | RO | The GPS location, if the tracker can report it. |
+| gps-accuracy | Number:Length | RO | The accuracy of a GPS fix. Even if a tracker can provide GPS location, it may not be able to determine and/or report its accuracy. |
+| source-type | String | RO | The source of the data, if the tracker reports it. May be "gps", "router", "bluetooth", or "bluetooth_le". |
+| json-attributes | String | RO | Additional attributes, as a serialized JSON string. |
+
### [Device Trigger](https://www.home-assistant.io/integrations/device_trigger.mqtt/)
If a device has multiple device triggers for the same subtype (the particular button), they will only show up as a single channel, and all events for that button will be delivered to that channel.
@@ -98,6 +109,18 @@ If a device has multiple device triggers for the same subtype (the particular bu
| direction | String | R/W | `forward` or `backward` |
| json-attributes | String | RO | Additional attributes, as a serialized JSON string. |
+### [Humidifier](https://www.home-assistant.io/integrations/humidifier.mqtt/)
+
+| Channel ID | Type | R/W | Description |
+|------------------|----------------------|-----|------------------------------------------------------------------------------------------|
+| state | Switch | R/W | If the humidifier should be on or off. |
+| action | String | RO | What the humidifier is actively doing. One of `off`, `humidifying`, `drying`, or `idle`. |
+| mode | String | R/W | Inspect the state description for valid values. |
+| current-humidity | Number:Dimensionless | RO | The current detected relative humidity, in %. |
+| target-humidity | Number:Dimensionless | R/W | The desired relative humidity, in %. |
+| device-class | String | RO | `humidifier` or `dehumidifier` |
+| json-attributes | String | RO | Additional attributes, as a serialized JSON string. |
+
### [Light](https://www.home-assistant.io/integrations/light.mqtt/)
| Channel ID | Type | R/W | Description |
@@ -153,6 +176,12 @@ If a device has multiple device triggers for the same subtype (the particular bu
| switch | Switch | R/W | If the device is on or off. |
| json-attributes | String | RO | Additional attributes, as a serialized JSON string. |
+### [Tag Scanner](https://www.home-assistant.io/integrations/tag.mqtt/)
+
+| Channel ID | Type | R/W | Description |
+|-----------------|---------|-----|---------------------------------|
+| tag | Trigger | N/A | The value of the "scanned" tag. |
+
### [Text](https://www.home-assistant.io/integrations/text.mqtt/)
| Channel ID | Type | R/W | Description |
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml b/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml
index 849eeb0d3bf57..45f343227ced9 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/pom.xml
@@ -37,7 +37,7 @@
org.openhab.osgiify
com.hubspot.jinjava.jinjava
- 2.7.3
+ 2.7.4
compile
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/feature/feature.xml b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/feature/feature.xml
index 770886a76adbc..59a216a0bd900 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/feature/feature.xml
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/feature/feature.xml
@@ -6,7 +6,7 @@
openhab-runtime-base
openhab-transport-mqtt
openhab.tp-commons-net
- mvn:org.openhab.osgiify/com.hubspot.jinjava.jinjava/2.7.3
+ mvn:org.openhab.osgiify/com.hubspot.jinjava.jinjava/2.7.4
mvn:org.openhab.osgiify/com.google.re2j.re2j/1.2
mvn:ch.obermuhlner/big-math/2.3.2
mvn:com.fasterxml.jackson.datatype/jackson-datatype-jdk8/${jackson.version}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java
index 5b2c8e9fdc96b..ce613e6886172 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java
@@ -22,6 +22,7 @@
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantJinjaFunctionLibrary;
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
+import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
@@ -47,6 +48,7 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
private final MqttChannelStateDescriptionProvider stateDescriptionProvider;
private final ChannelTypeRegistry channelTypeRegistry;
private final Jinjava jinjava = new Jinjava();
+ private final UnitProvider unitProvider;
private static final Set SUPPORTED_THING_TYPES_UIDS = Stream
.of(MqttBindingConstants.HOMEASSISTANT_MQTT_THING).collect(Collectors.toSet());
@@ -54,10 +56,11 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
@Activate
public MqttThingHandlerFactory(final @Reference MqttChannelTypeProvider typeProvider,
final @Reference MqttChannelStateDescriptionProvider stateDescriptionProvider,
- final @Reference ChannelTypeRegistry channelTypeRegistry) {
+ final @Reference ChannelTypeRegistry channelTypeRegistry, final @Reference UnitProvider unitProvider) {
this.typeProvider = typeProvider;
this.stateDescriptionProvider = stateDescriptionProvider;
this.channelTypeRegistry = channelTypeRegistry;
+ this.unitProvider = unitProvider;
HomeAssistantJinjaFunctionLibrary.register(jinjava.getGlobalContext());
}
@@ -78,7 +81,7 @@ private boolean isHomeassistantDynamicType(ThingTypeUID thingTypeUID) {
if (supportsThingType(thingTypeUID)) {
return new HomeAssistantThingHandler(thing, typeProvider, stateDescriptionProvider, channelTypeRegistry,
- jinjava, 10000, 2000);
+ jinjava, unitProvider, 10000, 2000);
}
return null;
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannelType.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannelType.java
index b99f8683ec115..2d410fe43bb63 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannelType.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannelType.java
@@ -26,11 +26,15 @@ public enum ComponentChannelType {
COLOR("ha-color"),
DIMMER("ha-dimmer"),
IMAGE("ha-image"),
+ LOCATION("ha-location"),
NUMBER("ha-number"),
ROLLERSHUTTER("ha-rollershutter"),
STRING("ha-string"),
SWITCH("ha-switch"),
- TRIGGER("ha-trigger");
+ TRIGGER("ha-trigger"),
+ HUMIDITY("ha-humidity"),
+ GPS_ACCURACY("ha-gps-accuracy"),
+ TEMPERATURE("ha-temperature");
final ChannelTypeUID channelTypeUID;
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java
index 810d99d988d78..3046847e94756 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java
@@ -30,6 +30,7 @@
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
+import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
import org.openhab.core.thing.ThingUID;
@@ -57,6 +58,7 @@ public class DiscoverComponents implements MqttMessageSubscriber {
protected final CompletableFuture<@Nullable Void> discoverFinishedFuture = new CompletableFuture<>();
private final Gson gson;
private final Jinjava jinjava;
+ private final UnitProvider unitProvider;
private @Nullable ScheduledFuture> stopDiscoveryFuture;
private WeakReference<@Nullable MqttBrokerConnection> connectionRef = new WeakReference<>(null);
@@ -69,6 +71,8 @@ public class DiscoverComponents implements MqttMessageSubscriber {
*/
public static interface ComponentDiscovered {
void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent> component);
+
+ void componentRemoved(HaID homeAssistantTopicID);
}
/**
@@ -80,12 +84,13 @@ public static interface ComponentDiscovered {
*/
public DiscoverComponents(ThingUID thingUID, ScheduledExecutorService scheduler,
ChannelStateUpdateListener channelStateUpdateListener, AvailabilityTracker tracker, Gson gson,
- Jinjava jinjava, boolean newStyleChannels) {
+ Jinjava jinjava, UnitProvider unitProvider, boolean newStyleChannels) {
this.thingUID = thingUID;
this.scheduler = scheduler;
this.updateListener = channelStateUpdateListener;
this.gson = gson;
this.jinjava = jinjava;
+ this.unitProvider = unitProvider;
this.tracker = tracker;
this.newStyleChannels = newStyleChannels;
}
@@ -103,7 +108,7 @@ public void processMessage(String topic, byte[] payload) {
if (config.length() > 0) {
try {
component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, tracker, scheduler,
- gson, jinjava, newStyleChannels);
+ gson, jinjava, unitProvider, newStyleChannels);
component.setConfigSeen();
logger.trace("Found HomeAssistant component {}", haID);
@@ -117,11 +122,11 @@ public void processMessage(String topic, byte[] payload) {
} catch (ConfigurationException e) {
logger.warn("HomeAssistant discover error: invalid configuration of thing {} component {}: {}",
haID.objectID, haID.component, e.getMessage());
- } catch (Exception e) {
- logger.warn("HomeAssistant discover error: {}", e.getMessage());
}
} else {
- logger.warn("Configuration of HomeAssistant thing {} is empty", haID.objectID);
+ if (discoveredListener != null) {
+ discoveredListener.componentRemoved(haID);
+ }
}
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponent.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponent.java
index 28e17c01a1ffc..80fbb958a7d35 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponent.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponent.java
@@ -12,6 +12,7 @@
*/
package org.openhab.binding.mqtt.homeassistant.internal.component;
+import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -21,6 +22,9 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Stream;
+import javax.measure.Unit;
+import javax.measure.quantity.Temperature;
+
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.AvailabilityTracker;
@@ -40,6 +44,8 @@
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AvailabilityMode;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Device;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.generic.ChannelTransformation;
@@ -53,6 +59,7 @@
import org.openhab.core.types.StateDescription;
import com.google.gson.Gson;
+import com.google.gson.annotations.SerializedName;
import com.hubspot.jinjava.Jinjava;
/**
@@ -64,6 +71,29 @@
*/
@NonNullByDefault
public abstract class AbstractComponent {
+ public enum TemperatureUnit {
+ @SerializedName("C")
+ CELSIUS(SIUnits.CELSIUS, new BigDecimal("0.1")),
+ @SerializedName("F")
+ FAHRENHEIT(ImperialUnits.FAHRENHEIT, BigDecimal.ONE);
+
+ private final Unit unit;
+ private final BigDecimal defaultPrecision;
+
+ TemperatureUnit(Unit unit, BigDecimal defaultPrecision) {
+ this.unit = unit;
+ this.defaultPrecision = defaultPrecision;
+ }
+
+ public Unit getUnit() {
+ return unit;
+ }
+
+ public BigDecimal getDefaultPrecision() {
+ return defaultPrecision;
+ }
+ }
+
public static final String JSON_ATTRIBUTES_CHANNEL_ID = "json-attributes";
// Component location fields
@@ -157,8 +187,10 @@ public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfig
protected void addJsonAttributesChannel() {
if (channelConfiguration.getJsonAttributesTopic() != null) {
+ ChannelStateUpdateListener listener = (this instanceof ChannelStateUpdateListener localThis) ? localThis
+ : componentConfiguration.getUpdateListener();
buildChannel(JSON_ATTRIBUTES_CHANNEL_ID, ComponentChannelType.STRING, new TextValue(), "JSON Attributes",
- componentConfiguration.getUpdateListener())
+ listener)
.stateTopic(channelConfiguration.getJsonAttributesTopic(),
channelConfiguration.getJsonAttributesTemplate())
.withAutoUpdatePolicy(AutoUpdatePolicy.VETO).isAdvanced(true).build();
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java
index a3c1572c80949..6396b1e44b0a0 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java
@@ -17,7 +17,6 @@
import java.util.List;
import java.util.function.Predicate;
-import javax.measure.Unit;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -32,7 +31,6 @@
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.ImperialUnits;
-import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
@@ -69,29 +67,6 @@ public class Climate extends AbstractComponent {
public static final String TEMPERATURE_LOW_CH_ID_DEPRECATED = "temperatureLow";
public static final String POWER_CH_ID = "power";
- public enum TemperatureUnit {
- @SerializedName("C")
- CELSIUS(SIUnits.CELSIUS, new BigDecimal("0.1")),
- @SerializedName("F")
- FAHRENHEIT(ImperialUnits.FAHRENHEIT, BigDecimal.ONE);
-
- private final Unit unit;
- private final BigDecimal defaultPrecision;
-
- TemperatureUnit(Unit unit, BigDecimal defaultPrecision) {
- this.unit = unit;
- this.defaultPrecision = defaultPrecision;
- }
-
- public Unit getUnit() {
- return unit;
- }
-
- public BigDecimal getDefaultPrecision() {
- return defaultPrecision;
- }
- }
-
private static final String ACTION_OFF = "off";
private static final State ACTION_OFF_STATE = new StringType(ACTION_OFF);
private static final List ACTION_MODES = List.of(ACTION_OFF, "heating", "cooling", "drying", "idle", "fan");
@@ -241,7 +216,7 @@ static class ChannelConfiguration extends AbstractChannelConfiguration {
@SerializedName("min_temp")
protected @Nullable BigDecimal minTemp;
@SerializedName("temperature_unit")
- protected TemperatureUnit temperatureUnit = TemperatureUnit.CELSIUS; // System unit by default
+ protected @Nullable TemperatureUnit temperatureUnit;
@SerializedName("temp_step")
protected BigDecimal tempStep = BigDecimal.ONE;
protected @Nullable BigDecimal precision;
@@ -252,8 +227,16 @@ static class ChannelConfiguration extends AbstractChannelConfiguration {
public Climate(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
+ TemperatureUnit temperatureUnit = channelConfiguration.temperatureUnit;
+ if (channelConfiguration.temperatureUnit == null) {
+ if (ImperialUnits.FAHRENHEIT.equals(componentConfiguration.getUnitProvider().getUnit(Temperature.class))) {
+ temperatureUnit = TemperatureUnit.FAHRENHEIT;
+ } else {
+ temperatureUnit = TemperatureUnit.CELSIUS;
+ }
+ }
BigDecimal precision = channelConfiguration.precision != null ? channelConfiguration.precision
- : channelConfiguration.temperatureUnit.getDefaultPrecision();
+ : temperatureUnit.getDefaultPrecision();
final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
ComponentChannel actionChannel = buildOptionalChannel(ACTION_CH_ID, ComponentChannelType.STRING,
@@ -272,14 +255,13 @@ ComponentChannelType.SWITCH, new OnOffValue(), updateListener, null,
channelConfiguration.awayModeCommandTopic, channelConfiguration.awayModeStateTemplate,
channelConfiguration.awayModeStateTopic, commandFilter);
- buildOptionalChannel(CURRENT_HUMIDITY_CH_ID, ComponentChannelType.NUMBER,
+ buildOptionalChannel(CURRENT_HUMIDITY_CH_ID, ComponentChannelType.HUMIDITY,
new NumberValue(new BigDecimal(0), new BigDecimal(100), null, Units.PERCENT), updateListener, null,
null, channelConfiguration.currentHumidityTemplate, channelConfiguration.currentHumidityTopic, null);
buildOptionalChannel(newStyleChannels ? CURRENT_TEMPERATURE_CH_ID : CURRENT_TEMPERATURE_CH_ID_DEPRECATED,
- ComponentChannelType.NUMBER,
- new NumberValue(null, null, precision, channelConfiguration.temperatureUnit.getUnit()), updateListener,
- null, null, channelConfiguration.currentTemperatureTemplate,
+ ComponentChannelType.TEMPERATURE, new NumberValue(null, null, precision, temperatureUnit.getUnit()),
+ updateListener, null, null, channelConfiguration.currentTemperatureTemplate,
channelConfiguration.currentTemperatureTopic, commandFilter);
buildOptionalChannel(newStyleChannels ? FAN_MODE_CH_ID : FAN_MODE_CH_ID_DEPRECATED, ComponentChannelType.STRING,
@@ -310,32 +292,32 @@ ComponentChannelType.SWITCH, new OnOffValue(), updateListener, null,
channelConfiguration.swingCommandTemplate, channelConfiguration.swingCommandTopic,
channelConfiguration.swingStateTemplate, channelConfiguration.swingStateTopic, commandFilter);
- buildOptionalChannel(TARGET_HUMIDITY_CH_ID, ComponentChannelType.NUMBER,
+ buildOptionalChannel(TARGET_HUMIDITY_CH_ID, ComponentChannelType.HUMIDITY,
new NumberValue(channelConfiguration.minHumidity, channelConfiguration.maxHumidity, null,
Units.PERCENT),
updateListener, channelConfiguration.targetHumidityCommandTemplate,
channelConfiguration.targetHumidityCommandTopic, channelConfiguration.targetHumidityStateTemplate,
channelConfiguration.targetHumidityStateTopic, commandFilter);
- buildOptionalChannel(TEMPERATURE_CH_ID, ComponentChannelType.NUMBER,
+ buildOptionalChannel(TEMPERATURE_CH_ID, ComponentChannelType.TEMPERATURE,
new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
- channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
+ channelConfiguration.tempStep, temperatureUnit.getUnit()),
updateListener, channelConfiguration.temperatureCommandTemplate,
channelConfiguration.temperatureCommandTopic, channelConfiguration.temperatureStateTemplate,
channelConfiguration.temperatureStateTopic, commandFilter);
buildOptionalChannel(newStyleChannels ? TEMPERATURE_HIGH_CH_ID : TEMPERATURE_HIGH_CH_ID_DEPRECATED,
- ComponentChannelType.NUMBER,
+ ComponentChannelType.TEMPERATURE,
new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
- channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
+ channelConfiguration.tempStep, temperatureUnit.getUnit()),
updateListener, channelConfiguration.temperatureHighCommandTemplate,
channelConfiguration.temperatureHighCommandTopic, channelConfiguration.temperatureHighStateTemplate,
channelConfiguration.temperatureHighStateTopic, commandFilter);
buildOptionalChannel(newStyleChannels ? TEMPERATURE_LOW_CH_ID : TEMPERATURE_LOW_CH_ID_DEPRECATED,
- ComponentChannelType.NUMBER,
+ ComponentChannelType.TEMPERATURE,
new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
- channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
+ channelConfiguration.tempStep, temperatureUnit.getUnit()),
updateListener, channelConfiguration.temperatureLowCommandTemplate,
channelConfiguration.temperatureLowCommandTopic, channelConfiguration.temperatureLowStateTemplate,
channelConfiguration.temperatureLowStateTopic, commandFilter);
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java
index fe864be0635c8..a6d4e8194872f 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java
@@ -21,6 +21,7 @@
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
+import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.thing.ThingUID;
import com.google.gson.Gson;
@@ -47,9 +48,10 @@ public class ComponentFactory {
*/
public static AbstractComponent> createComponent(ThingUID thingUID, HaID haID, String channelConfigurationJSON,
ChannelStateUpdateListener updateListener, AvailabilityTracker tracker, ScheduledExecutorService scheduler,
- Gson gson, Jinjava jinjava, boolean newStyleChannels) throws ConfigurationException {
+ Gson gson, Jinjava jinjava, UnitProvider unitProvider, boolean newStyleChannels)
+ throws ConfigurationException {
ComponentConfiguration componentConfiguration = new ComponentConfiguration(thingUID, haID,
- channelConfigurationJSON, gson, jinjava, updateListener, tracker, scheduler);
+ channelConfigurationJSON, gson, jinjava, updateListener, tracker, scheduler, unitProvider);
switch (haID.component) {
case "alarm_control_panel":
return new AlarmControlPanel(componentConfiguration, newStyleChannels);
@@ -65,10 +67,14 @@ public static AbstractComponent> createComponent(ThingUID thingUID, HaID haID,
return new Cover(componentConfiguration, newStyleChannels);
case "device_automation":
return new DeviceTrigger(componentConfiguration, newStyleChannels);
+ case "device_tracker":
+ return new DeviceTracker(componentConfiguration, newStyleChannels);
case "event":
return new Event(componentConfiguration, newStyleChannels);
case "fan":
return new Fan(componentConfiguration, newStyleChannels);
+ case "humidifier":
+ return new Humidifier(componentConfiguration, newStyleChannels);
case "light":
return Light.create(componentConfiguration, newStyleChannels);
case "lock":
@@ -83,6 +89,8 @@ public static AbstractComponent> createComponent(ThingUID thingUID, HaID haID,
return new Sensor(componentConfiguration, newStyleChannels);
case "switch":
return new Switch(componentConfiguration, newStyleChannels);
+ case "tag":
+ return new Tag(componentConfiguration, newStyleChannels);
case "text":
return new Text(componentConfiguration, newStyleChannels);
case "update":
@@ -91,6 +99,8 @@ public static AbstractComponent> createComponent(ThingUID thingUID, HaID haID,
return new Vacuum(componentConfiguration, newStyleChannels);
case "valve":
return new Valve(componentConfiguration, newStyleChannels);
+ case "water_heater":
+ return new WaterHeater(componentConfiguration, newStyleChannels);
default:
throw new UnsupportedComponentException("Component '" + haID + "' is unsupported!");
}
@@ -105,6 +115,7 @@ protected static class ComponentConfiguration {
private final Gson gson;
private final Jinjava jinjava;
private final ScheduledExecutorService scheduler;
+ private final UnitProvider unitProvider;
/**
* Provide a thingUID and HomeAssistant topic ID to determine the channel group UID and type.
@@ -116,7 +127,7 @@ protected static class ComponentConfiguration {
*/
protected ComponentConfiguration(ThingUID thingUID, HaID haID, String configJSON, Gson gson, Jinjava jinjava,
ChannelStateUpdateListener updateListener, AvailabilityTracker tracker,
- ScheduledExecutorService scheduler) {
+ ScheduledExecutorService scheduler, UnitProvider unitProvider) {
this.thingUID = thingUID;
this.haID = haID;
this.configJSON = configJSON;
@@ -125,6 +136,7 @@ protected ComponentConfiguration(ThingUID thingUID, HaID haID, String configJSON
this.updateListener = updateListener;
this.tracker = tracker;
this.scheduler = scheduler;
+ this.unitProvider = unitProvider;
}
public ThingUID getThingUID() {
@@ -151,6 +163,10 @@ public Jinjava getJinjava() {
return jinjava;
}
+ public UnitProvider getUnitProvider() {
+ return unitProvider;
+ }
+
public AvailabilityTracker getTracker() {
return tracker;
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/DeviceTracker.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/DeviceTracker.java
new file mode 100644
index 0000000000000..70b47010ba89a
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/DeviceTracker.java
@@ -0,0 +1,232 @@
+/**
+ * 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.mqtt.homeassistant.internal.component;
+
+import java.math.BigDecimal;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
+import org.openhab.binding.mqtt.generic.values.LocationValue;
+import org.openhab.binding.mqtt.generic.values.NumberValue;
+import org.openhab.binding.mqtt.generic.values.OnOffValue;
+import org.openhab.binding.mqtt.generic.values.TextValue;
+import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
+import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
+import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PointType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.type.AutoUpdatePolicy;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * A MQTT Device Tracker, following the https://www.home-assistant.io/integrations/device_tracker.mqtt/specification.
+ *
+ * @author Cody Cutrer - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceTracker extends AbstractComponent
+ implements ChannelStateUpdateListener {
+ public static final String HOME_CHANNEL_ID = "home";
+ public static final String LOCATION_CHANNEL_ID = "location";
+ public static final String GPS_ACCURACY_CHANNEL_ID = "gps-accuracy"; // Always in meters
+ public static final String LOCATION_NAME_CHANNEL_ID = "location-name";
+ public static final String SOURCE_TYPE_CHANNEL_ID = "source-type";
+
+ public static final String[] SOURCE_TYPE_OPTIONS = new String[] { "gps", "router", "bluetooth", "bluetooth_le" };
+
+ /**
+ * Configuration class for MQTT component
+ */
+ static class ChannelConfiguration extends AbstractChannelConfiguration {
+ ChannelConfiguration() {
+ super("MQTT Binary Sensor");
+ }
+
+ @SerializedName("source_type")
+ protected @Nullable String sourceType;
+ @SerializedName("state_topic")
+ protected @Nullable String stateTopic;
+ @SerializedName("payload_home")
+ protected String payloadHome = "home";
+ @SerializedName("payload_not_home")
+ protected String payloadNotHome = "not_home";
+ @SerializedName("payload_reset")
+ protected String payloadReset = "None";
+ }
+
+ /**
+ * DTO for JSON Attributes providing location data
+ */
+ static class JSONAttributes {
+ protected @Nullable BigDecimal latitude;
+ protected @Nullable BigDecimal longitude;
+ @SerializedName("gps_accuracy")
+ protected @Nullable BigDecimal gpsAccuracy;
+ }
+
+ private final Logger logger = LoggerFactory.getLogger(DeviceTracker.class);
+
+ private final ChannelStateUpdateListener channelStateUpdateListener;
+ private final OnOffValue homeValue = new OnOffValue();
+ private final NumberValue accuracyValue = new NumberValue(BigDecimal.ZERO, null, null, SIUnits.METRE);
+ private final TextValue locationNameValue = new TextValue();
+ private final LocationValue locationValue = new LocationValue();
+ private final @Nullable ComponentChannel homeChannel, locationChannel, accuracyChannel;
+
+ public DeviceTracker(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
+ super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
+ this.channelStateUpdateListener = componentConfiguration.getUpdateListener();
+
+ if (channelConfiguration.stateTopic == null && channelConfiguration.getJsonAttributesTopic() == null) {
+ throw new ConfigurationException("Device trackers must define either state_topic or json_attributes_topic");
+ }
+ homeValue.update(UnDefType.NULL);
+ locationNameValue.update(UnDefType.NULL);
+ accuracyValue.update(UnDefType.NULL);
+ locationValue.update(UnDefType.NULL);
+
+ if (channelConfiguration.stateTopic != null) {
+ homeChannel = buildChannel(HOME_CHANNEL_ID, ComponentChannelType.SWITCH, homeValue, "At Home",
+ componentConfiguration.getUpdateListener()).withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
+
+ buildChannel(LOCATION_NAME_CHANNEL_ID, ComponentChannelType.STRING, locationNameValue, "Location Name",
+ this).stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
+ .withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
+ } else {
+ homeChannel = null;
+ }
+
+ String sourceType = channelConfiguration.sourceType;
+ if (sourceType != null) {
+ TextValue sourceTypeValue = new TextValue(SOURCE_TYPE_OPTIONS);
+ sourceTypeValue.update(new StringType(sourceType));
+ buildChannel(SOURCE_TYPE_CHANNEL_ID, ComponentChannelType.STRING, sourceTypeValue, "Source Type", this)
+ .isAdvanced(true).withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
+ }
+
+ if (channelConfiguration.getJsonAttributesTopic() != null) {
+ locationChannel = buildChannel(LOCATION_CHANNEL_ID, ComponentChannelType.LOCATION, locationValue,
+ "Location", this).withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
+
+ accuracyChannel = buildChannel(GPS_ACCURACY_CHANNEL_ID, ComponentChannelType.GPS_ACCURACY, accuracyValue,
+ "GPS Accuracy", this).isAdvanced(true).withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
+ } else {
+ locationChannel = accuracyChannel = null;
+ }
+
+ finalizeChannels();
+ }
+
+ // Override to set ourselves as listener
+ protected void addJsonAttributesChannel() {
+ if (channelConfiguration.getJsonAttributesTopic() != null) {
+ buildChannel(JSON_ATTRIBUTES_CHANNEL_ID, ComponentChannelType.STRING, new TextValue(), "JSON Attributes",
+ this)
+ .stateTopic(channelConfiguration.getJsonAttributesTopic(),
+ channelConfiguration.getJsonAttributesTemplate())
+ .withAutoUpdatePolicy(AutoUpdatePolicy.VETO).isAdvanced(true).build();
+ }
+ }
+
+ @Override
+ public void updateChannelState(ChannelUID channel, State state) {
+ if (channel.getIdWithoutGroup().equals(LOCATION_NAME_CHANNEL_ID)) {
+ String stateString = state.toString();
+ if (stateString.isEmpty()) {
+ return;
+ }
+
+ State homeState;
+ if (channelConfiguration.payloadHome.equals(stateString)) {
+ homeState = OnOffType.ON;
+ } else if (channelConfiguration.payloadNotHome.equals(stateString)) {
+ homeState = OnOffType.OFF;
+ } else {
+ homeState = UnDefType.UNDEF;
+ }
+
+ if (channelConfiguration.payloadReset.equals(stateString)) {
+ state = UnDefType.NULL;
+ locationNameValue.update(state);
+ homeState = UnDefType.NULL;
+ ComponentChannel locationChannel = this.locationChannel;
+ if (locationChannel != null) {
+ locationValue.update(UnDefType.NULL);
+ accuracyValue.update(UnDefType.NULL);
+ channelStateUpdateListener.updateChannelState(locationChannel.getChannel().getUID(),
+ locationValue.getChannelState());
+ channelStateUpdateListener.updateChannelState(
+ Objects.requireNonNull(accuracyChannel).getChannel().getUID(),
+ accuracyValue.getChannelState());
+ }
+ }
+ channelStateUpdateListener.updateChannelState(channel, state);
+ homeValue.update(homeState);
+ channelStateUpdateListener.updateChannelState(Objects.requireNonNull(homeChannel).getChannel().getUID(),
+ homeState);
+ } else if (channel.getIdWithoutGroup().equals(JSON_ATTRIBUTES_CHANNEL_ID)) {
+ // First forward JSON attributes channel as-is
+ channelStateUpdateListener.updateChannelState(channel, state);
+
+ JSONAttributes jsonAttributes;
+ try {
+ jsonAttributes = Objects.requireNonNull(getGson().fromJson(state.toString(), JSONAttributes.class));
+ } catch (JsonSyntaxException e) {
+ logger.warn("Cannot parse JSON attributes '{}' for '{}'.", state, getHaID());
+ return;
+ }
+ BigDecimal latitude = jsonAttributes.latitude;
+ BigDecimal longitude = jsonAttributes.longitude;
+ BigDecimal gpsAccuracy = jsonAttributes.gpsAccuracy;
+ if (latitude != null && longitude != null) {
+ locationValue.update(new PointType(new DecimalType(latitude), new DecimalType(longitude)));
+ } else {
+ locationValue.update(UnDefType.NULL);
+ }
+ if (gpsAccuracy != null) {
+ accuracyValue.update(new QuantityType<>(gpsAccuracy, SIUnits.METRE));
+ } else {
+ accuracyValue.update(UnDefType.NULL);
+ }
+ channelStateUpdateListener.updateChannelState(Objects.requireNonNull(locationChannel).getChannel().getUID(),
+ locationValue.getChannelState());
+ channelStateUpdateListener.updateChannelState(Objects.requireNonNull(accuracyChannel).getChannel().getUID(),
+ accuracyValue.getChannelState());
+ }
+ }
+
+ @Override
+ public void postChannelCommand(ChannelUID channelUID, Command value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void triggerChannel(ChannelUID channelUID, String eventPayload) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Fan.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Fan.java
index 8f5132b79e6c0..99394e0886003 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Fan.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Fan.java
@@ -50,6 +50,9 @@ public class Fan extends AbstractComponent implements
public static final String OSCILLATION_CHANNEL_ID = "oscillation";
public static final String DIRECTION_CHANNEL_ID = "direction";
+ private static final BigDecimal BIG_DECIMAL_HUNDRED = new BigDecimal(100);
+ private static final String FORMAT_INTEGER = "%.0f";
+
/**
* Configuration class for MQTT component
*/
@@ -60,6 +63,8 @@ static class ChannelConfiguration extends AbstractChannelConfiguration {
protected @Nullable Boolean optimistic;
+ @SerializedName("state_value_template")
+ protected @Nullable String stateValueTemplate;
@SerializedName("state_topic")
protected @Nullable String stateTopic;
@SerializedName("command_template")
@@ -136,7 +141,7 @@ public Fan(ComponentFactory.ComponentConfiguration componentConfiguration, boole
: this;
onOffChannel = buildChannel(newStyleChannels ? SWITCH_CHANNEL_ID : SWITCH_CHANNEL_ID_DEPRECATED,
ComponentChannelType.SWITCH, onOffValue, "On/Off State", onOffListener)
- .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
+ .stateTopic(channelConfiguration.stateTopic, channelConfiguration.stateValueTemplate)
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.commandTemplate)
.inferOptimistic(channelConfiguration.optimistic)
@@ -144,10 +149,9 @@ public Fan(ComponentFactory.ComponentConfiguration componentConfiguration, boole
rawSpeedState = UnDefType.NULL;
- int speeds = Math.min(channelConfiguration.speedRangeMax, 100) - Math.max(channelConfiguration.speedRangeMin, 1)
- + 1;
- speedValue = new PercentageValue(BigDecimal.ZERO, BigDecimal.valueOf(100), BigDecimal.valueOf(100.0d / speeds),
- channelConfiguration.payloadOn, channelConfiguration.payloadOff);
+ speedValue = new PercentageValue(BigDecimal.valueOf(channelConfiguration.speedRangeMin - 1),
+ BigDecimal.valueOf(channelConfiguration.speedRangeMax), null, channelConfiguration.payloadOn,
+ channelConfiguration.payloadOff, FORMAT_INTEGER);
if (channelConfiguration.percentageCommandTopic != null) {
hiddenChannels.add(onOffChannel);
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Humidifier.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Humidifier.java
new file mode 100644
index 0000000000000..cedbbde975a39
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Humidifier.java
@@ -0,0 +1,174 @@
+/**
+ * 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.mqtt.homeassistant.internal.component;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mqtt.generic.values.NumberValue;
+import org.openhab.binding.mqtt.generic.values.OnOffValue;
+import org.openhab.binding.mqtt.generic.values.TextValue;
+import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
+import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.Units;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * A MQTT Humidifier, following the https://www.home-assistant.io/integrations/humidifier.mqtt/ specification.
+ *
+ * @author Cody Cutrer - Initial contribution
+ */
+@NonNullByDefault
+public class Humidifier extends AbstractComponent {
+ public static final String ACTION_CHANNEL_ID = "action";
+ public static final String CURRENT_HUMIDITY_CHANNEL_ID = "current-humidity";
+ public static final String DEVICE_CLASS_CHANNEL_ID = "device-class";
+ public static final String MODE_CHANNEL_ID = "mode";
+ public static final String STATE_CHANNEL_ID = "state";
+ public static final String TARGET_HUMIDITY_CHANNEL_ID = "target-humidity";
+
+ public static final String PLATFORM_HUMIDIFIER = "humidifier";
+ public static final String[] ACTIONS = new String[] { "off", "humidifying", "drying", "idle" };
+
+ /**
+ * Configuration class for MQTT component
+ */
+ static class ChannelConfiguration extends AbstractChannelConfiguration {
+ ChannelConfiguration() {
+ super("MQTT Humidifier");
+ }
+
+ protected @Nullable Boolean optimistic;
+
+ @SerializedName("action_topic")
+ protected @Nullable String actionTopic;
+ @SerializedName("action_template")
+ protected @Nullable String actionTemplate;
+ @SerializedName("command_topic")
+ protected String commandTopic = "";
+ @SerializedName("command_template")
+ protected @Nullable String commandTemplate;
+ @SerializedName("state_topic")
+ protected @Nullable String stateTopic;
+ @SerializedName("state_value_template")
+ protected @Nullable String stateValueTemplate;
+ @SerializedName("current_humidity_topic")
+ protected @Nullable String currentHumidityTopic;
+ @SerializedName("current_humidity_template")
+ protected @Nullable String currentHumidityTemplate;
+ @SerializedName("target_humidity_command_topic")
+ protected @Nullable String targetHumidityCommandTopic;
+ @SerializedName("target_humidity_command_template")
+ protected @Nullable String targetHumidityCommandTemplate;
+ @SerializedName("target_humidity_state_topic")
+ protected @Nullable String targetHumidityStateTopic;
+ @SerializedName("target_humidity_state_template")
+ protected @Nullable String targetHumidityStateTemplate;
+ @SerializedName("mode_command_topic")
+ protected @Nullable String modeCommandTopic;
+ @SerializedName("mode_command_template")
+ protected @Nullable String modeCommandTemplate;
+ @SerializedName("mode_state_topic")
+ protected @Nullable String modeStateTopic;
+ @SerializedName("mode_state_template")
+ protected @Nullable String modeStateTemplate;
+
+ @SerializedName("device_class")
+ protected @Nullable String deviceClass;
+ protected String platform = "";
+
+ @SerializedName("min_humidity")
+ protected BigDecimal minHumidity = BigDecimal.ZERO;
+ @SerializedName("max_humidity")
+ protected BigDecimal maxHumidity = new BigDecimal(100);
+
+ @SerializedName("payload_on")
+ protected String payloadOn = "ON";
+ @SerializedName("payload_off")
+ protected String payloadOff = "OFF";
+ @SerializedName("payload_reset_humidity")
+ protected String payloadResetHumidity = "None";
+ @SerializedName("payload_reset_mode")
+ protected String payloadResetMode = "None";
+ protected @Nullable List modes;
+ }
+
+ public Humidifier(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
+ super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
+
+ if (!PLATFORM_HUMIDIFIER.equals(channelConfiguration.platform)) {
+ throw new ConfigurationException("platform must be " + PLATFORM_HUMIDIFIER);
+ }
+
+ buildChannel(STATE_CHANNEL_ID, ComponentChannelType.SWITCH,
+ new OnOffValue(channelConfiguration.payloadOn, channelConfiguration.payloadOff), "State",
+ componentConfiguration.getUpdateListener())
+ .stateTopic(channelConfiguration.stateTopic, channelConfiguration.stateValueTemplate)
+ .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
+ channelConfiguration.getQos(), channelConfiguration.commandTemplate)
+ .inferOptimistic(channelConfiguration.optimistic).build();
+
+ buildChannel(TARGET_HUMIDITY_CHANNEL_ID, ComponentChannelType.HUMIDITY,
+ new NumberValue(channelConfiguration.minHumidity, channelConfiguration.maxHumidity, null,
+ Units.PERCENT),
+ "Target Humidity", componentConfiguration.getUpdateListener())
+ .stateTopic(channelConfiguration.targetHumidityStateTopic,
+ channelConfiguration.targetHumidityStateTemplate)
+ .commandTopic(channelConfiguration.targetHumidityCommandTopic, channelConfiguration.isRetain(),
+ channelConfiguration.getQos(), channelConfiguration.targetHumidityCommandTemplate)
+ .inferOptimistic(channelConfiguration.optimistic).build();
+
+ if (channelConfiguration.actionTopic != null) {
+ buildChannel(ACTION_CHANNEL_ID, ComponentChannelType.STRING, new TextValue(ACTIONS), "Action",
+ componentConfiguration.getUpdateListener())
+ .stateTopic(channelConfiguration.actionTopic, channelConfiguration.actionTemplate).build();
+ }
+
+ if (channelConfiguration.modeCommandTopic != null) {
+ List modes = channelConfiguration.modes;
+ if (modes == null) {
+ throw new ConfigurationException("modes cannot be null if mode_command_topic is specified");
+ }
+ TextValue modeValue = new TextValue(modes.toArray(new String[0]));
+ modeValue.setNullValue(channelConfiguration.payloadResetMode);
+ buildChannel(MODE_CHANNEL_ID, ComponentChannelType.STRING, modeValue, "Mode",
+ componentConfiguration.getUpdateListener())
+ .stateTopic(channelConfiguration.modeStateTopic, channelConfiguration.modeStateTemplate)
+ .commandTopic(channelConfiguration.modeCommandTopic, channelConfiguration.isRetain(),
+ channelConfiguration.getQos(), channelConfiguration.modeCommandTemplate)
+ .inferOptimistic(channelConfiguration.optimistic).build();
+ }
+
+ if (channelConfiguration.currentHumidityTopic != null) {
+ buildChannel(CURRENT_HUMIDITY_CHANNEL_ID, ComponentChannelType.HUMIDITY,
+ new NumberValue(null, null, null, Units.PERCENT), "Current Humidity",
+ componentConfiguration.getUpdateListener())
+ .stateTopic(channelConfiguration.currentHumidityTopic, channelConfiguration.currentHumidityTemplate)
+ .build();
+ }
+
+ if (channelConfiguration.deviceClass != null) {
+ TextValue deviceClassValue = new TextValue();
+ deviceClassValue.update(new StringType(channelConfiguration.deviceClass));
+ buildChannel(DEVICE_CLASS_CHANNEL_ID, ComponentChannelType.STRING, deviceClassValue, "Device Class",
+ componentConfiguration.getUpdateListener()).build();
+ }
+
+ finalizeChannels();
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Light.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Light.java
index 443d06797f9a7..d704679adef2b 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Light.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Light.java
@@ -73,6 +73,8 @@ public abstract class Light extends AbstractComponent effectList = channelConfiguration.effectList;
if (effectList != null) {
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Tag.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Tag.java
new file mode 100644
index 0000000000000..758ac6c8a6d63
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Tag.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.mqtt.homeassistant.internal.component;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mqtt.generic.values.TextValue;
+import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
+
+/**
+ * A MQTT Tag scanner, following the https://www.home-assistant.io/integrations/tag.mqtt/ specification.
+ *
+ * @author Cody Cutrer - Initial contribution
+ */
+@NonNullByDefault
+public class Tag extends AbstractComponent {
+ public static final String TAG_CHANNEL_ID = "tag";
+
+ /**
+ * Configuration class for MQTT component
+ */
+ public static class ChannelConfiguration extends AbstractChannelConfiguration {
+ ChannelConfiguration() {
+ super("MQTT Tag Scanner");
+ }
+
+ protected String topic = "";
+ }
+
+ public Tag(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
+ super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
+
+ buildChannel(TAG_CHANNEL_ID, ComponentChannelType.TRIGGER, new TextValue(), getName(),
+ componentConfiguration.getUpdateListener())
+ .stateTopic(channelConfiguration.topic, channelConfiguration.getValueTemplate()).trigger(true).build();
+ finalizeChannels();
+ }
+
+ @Override
+ protected void addJsonAttributesChannel() {
+ // json_attributes are not supported
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/TemplateSchemaLight.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/TemplateSchemaLight.java
index 8a1abfe2418de..e1d1035a26402 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/TemplateSchemaLight.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/TemplateSchemaLight.java
@@ -84,7 +84,7 @@ protected void buildChannels() {
}
onOffValue = new OnOffValue("on", "off");
- brightnessValue = new PercentageValue(null, new BigDecimal(255), null, null, null);
+ brightnessValue = new PercentageValue(null, new BigDecimal(255), null, null, null, FORMAT_INTEGER);
if (channelConfiguration.redTemplate != null && channelConfiguration.greenTemplate != null
&& channelConfiguration.blueTemplate != null) {
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Vacuum.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Vacuum.java
index f7cdde2ba07fe..502601c0e006e 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Vacuum.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Vacuum.java
@@ -173,7 +173,7 @@ ComponentChannelType.STRING, new TextValue(), updateListener, null,
if (supportedFeatures.contains(FEATURE_BATTERY)) {
buildOptionalChannel(newStyleChannels ? BATTERY_LEVEL_CH_ID : BATTERY_LEVEL_CH_ID_DEPRECATED,
ComponentChannelType.DIMMER,
- new PercentageValue(BigDecimal.ZERO, BigDecimal.valueOf(100), BigDecimal.ONE, null, null),
+ new PercentageValue(BigDecimal.ZERO, BigDecimal.valueOf(100), BigDecimal.ONE, null, null, null),
updateListener, null, null, "{{ value_json.battery_level }}", channelConfiguration.stateTopic);
}
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Valve.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Valve.java
index 6c5f8e54f278b..5a0cc23cf7320 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Valve.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Valve.java
@@ -61,6 +61,8 @@ public class Valve extends AbstractComponent impleme
private static final String POSITION_KEY = "position";
private static final String STATE_KEY = "state";
+ private static final String FORMAT_INTEGER = "%.0f";
+
private final Logger logger = LoggerFactory.getLogger(Valve.class);
/**
@@ -121,7 +123,7 @@ public Valve(ComponentFactory.ComponentConfiguration componentConfiguration, boo
onOffValue = new OnOffValue(channelConfiguration.stateOpen, channelConfiguration.stateClosed,
channelConfiguration.payloadOpen, channelConfiguration.payloadClose);
positionValue = new PercentageValue(BigDecimal.valueOf(channelConfiguration.positionClosed),
- BigDecimal.valueOf(channelConfiguration.positionOpen), null, null, null);
+ BigDecimal.valueOf(channelConfiguration.positionOpen), null, null, null, FORMAT_INTEGER);
if (channelConfiguration.reportsPosition) {
buildChannel(VALVE_CHANNEL_ID, ComponentChannelType.DIMMER, positionValue, getName(), this)
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/WaterHeater.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/WaterHeater.java
new file mode 100644
index 0000000000000..3e79c5761aaff
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/WaterHeater.java
@@ -0,0 +1,190 @@
+/**
+ * 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.mqtt.homeassistant.internal.component;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.measure.quantity.Temperature;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mqtt.generic.values.NumberValue;
+import org.openhab.binding.mqtt.generic.values.OnOffValue;
+import org.openhab.binding.mqtt.generic.values.TextValue;
+import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
+import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
+import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
+import org.openhab.core.library.unit.ImperialUnits;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * A MQTT Humidifier, following the https://www.home-assistant.io/integrations/water_heater.mqtt/ specification.
+ *
+ * @author Cody Cutrer - Initial contribution
+ */
+@NonNullByDefault
+public class WaterHeater extends AbstractComponent {
+ public static final String CURRENT_TEMPERATURE_CHANNEL_ID = "current-temperature";
+ public static final String MODE_CHANNEL_ID = "mode";
+ public static final String STATE_CHANNEL_ID = "state";
+ public static final String TARGET_TEMPERATURE_CHANNEL_ID = "target-temperature";
+
+ public static final String PLATFORM_WATER_HEATER = "water_heater";
+
+ public static final String MODE_OFF = "off";
+ public static final String MODE_ECO = "eco";
+ public static final String MODE_ELECTRIC = "electric";
+ public static final String MODE_GAS = "gas";
+ public static final String MODE_HEAT_PUMP = "heat_pump";
+ public static final String MODE_HIGH_DEMAND = "high_demand";
+ public static final String MODE_PERFORMANCE = "performance";
+ public static final List DEFAULT_MODES = List.of(MODE_OFF, MODE_ECO, MODE_ELECTRIC, MODE_GAS,
+ MODE_HEAT_PUMP, MODE_HIGH_DEMAND, MODE_PERFORMANCE);
+
+ public static final String TEMPERATURE_UNIT_C = "C";
+ public static final String TEMPERATURE_UNIT_F = "F";
+
+ /**
+ * Configuration class for MQTT component
+ */
+ static class ChannelConfiguration extends AbstractChannelConfiguration {
+ ChannelConfiguration() {
+ super("MQTT Humidifier");
+ }
+
+ protected @Nullable Boolean optimistic;
+
+ @SerializedName("power_command_topic")
+ protected @Nullable String powerCommandTopic;
+ @SerializedName("power_command_template")
+ protected @Nullable String powerCommandTemplate;
+ @SerializedName("current_temperature_topic")
+ protected @Nullable String currentTemperatureTopic;
+ @SerializedName("current_temperature_template")
+ protected @Nullable String currentTemperatureTemplate;
+ @SerializedName("temperature_command_topic")
+ protected @Nullable String temperatureCommandTopic;
+ @SerializedName("temperature_command_template")
+ protected @Nullable String temperatureCommandTemplate;
+ @SerializedName("temperature_state_topic")
+ protected @Nullable String temperatureStateTopic;
+ @SerializedName("temperature_state_template")
+ protected @Nullable String temperatureStateTemplate;
+ @SerializedName("mode_command_topic")
+ protected @Nullable String modeCommandTopic;
+ @SerializedName("mode_command_template")
+ protected @Nullable String modeCommandTemplate;
+ @SerializedName("mode_state_topic")
+ protected @Nullable String modeStateTopic;
+ @SerializedName("mode_state_template")
+ protected @Nullable String modeStateTemplate;
+
+ @SerializedName("device_class")
+ protected @Nullable String deviceClass;
+ protected String platform = "";
+
+ protected @Nullable Integer initial;
+ @SerializedName("min_temp")
+ protected @Nullable BigDecimal minTemp;
+ @SerializedName("max_temp")
+ protected @Nullable BigDecimal maxTemp;
+ protected @Nullable BigDecimal precision;
+ @SerializedName("temperature_unit")
+ protected @Nullable TemperatureUnit temperatureUnit;
+
+ @SerializedName("payload_on")
+ protected String payloadOn = "ON";
+ @SerializedName("payload_off")
+ protected String payloadOff = "OFF";
+ protected List modes = DEFAULT_MODES;
+ }
+
+ public WaterHeater(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
+ super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
+
+ if (!PLATFORM_WATER_HEATER.equals(channelConfiguration.platform)) {
+ throw new ConfigurationException("platform must be " + PLATFORM_WATER_HEATER);
+ }
+
+ TemperatureUnit temperatureUnit = channelConfiguration.temperatureUnit;
+ if (channelConfiguration.temperatureUnit == null) {
+ if (ImperialUnits.FAHRENHEIT.equals(componentConfiguration.getUnitProvider().getUnit(Temperature.class))) {
+ temperatureUnit = TemperatureUnit.FAHRENHEIT;
+ } else {
+ temperatureUnit = TemperatureUnit.CELSIUS;
+ }
+ }
+ BigDecimal precision = channelConfiguration.precision != null ? channelConfiguration.precision
+ : temperatureUnit.getDefaultPrecision();
+
+ List onStates = new ArrayList<>(channelConfiguration.modes);
+ onStates.remove(MODE_OFF);
+
+ List unsupportedModes = onStates.stream().filter(mode -> !DEFAULT_MODES.contains(mode))
+ .collect(Collectors.toList());
+ if (!unsupportedModes.isEmpty()) {
+ throw new ConfigurationException("unsupported modes: " + unsupportedModes.toString());
+ }
+
+ if (channelConfiguration.powerCommandTopic != null) {
+ buildChannel(STATE_CHANNEL_ID, ComponentChannelType.SWITCH,
+ new OnOffValue(onStates.toArray(new String[0]), new String[] { MODE_OFF },
+ channelConfiguration.payloadOn, channelConfiguration.payloadOff),
+ "State", componentConfiguration.getUpdateListener())
+ .stateTopic(channelConfiguration.modeStateTopic, channelConfiguration.modeStateTemplate,
+ channelConfiguration.getValueTemplate())
+ .commandTopic(channelConfiguration.powerCommandTopic, channelConfiguration.isRetain(),
+ channelConfiguration.getQos(), channelConfiguration.powerCommandTemplate)
+ .inferOptimistic(channelConfiguration.optimistic).build();
+ }
+
+ if (channelConfiguration.modeCommandTopic != null | channelConfiguration.modeStateTopic != null) {
+ buildChannel(MODE_CHANNEL_ID, ComponentChannelType.STRING,
+ new TextValue(channelConfiguration.modes.toArray(new String[0])), "Mode",
+ componentConfiguration.getUpdateListener())
+ .stateTopic(channelConfiguration.modeStateTopic, channelConfiguration.modeStateTemplate,
+ channelConfiguration.getValueTemplate())
+ .commandTopic(channelConfiguration.modeCommandTopic, channelConfiguration.isRetain(),
+ channelConfiguration.getQos(), channelConfiguration.modeCommandTemplate)
+ .inferOptimistic(channelConfiguration.optimistic).build();
+ }
+
+ if (channelConfiguration.currentTemperatureTopic != null) {
+ buildChannel(CURRENT_TEMPERATURE_CHANNEL_ID, ComponentChannelType.TEMPERATURE,
+ new NumberValue(null, null, null, temperatureUnit.getUnit()), "Current Temperature",
+ componentConfiguration.getUpdateListener())
+ .stateTopic(channelConfiguration.currentTemperatureTopic,
+ channelConfiguration.currentTemperatureTemplate, channelConfiguration.getValueTemplate())
+ .build();
+ }
+
+ if (channelConfiguration.temperatureStateTopic != null
+ || channelConfiguration.temperatureCommandTopic != null) {
+ buildChannel(TARGET_TEMPERATURE_CHANNEL_ID, ComponentChannelType.TEMPERATURE,
+ new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp, precision,
+ temperatureUnit.getUnit()),
+ "Target Temperature", componentConfiguration.getUpdateListener())
+ .stateTopic(channelConfiguration.temperatureStateTopic,
+ channelConfiguration.temperatureStateTemplate, channelConfiguration.getValueTemplate())
+ .commandTopic(channelConfiguration.temperatureCommandTopic, channelConfiguration.isRetain(),
+ channelConfiguration.getQos(), channelConfiguration.temperatureCommandTemplate)
+ .inferOptimistic(channelConfiguration.optimistic).build();
+ }
+
+ finalizeChannels();
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscovery.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscovery.java
index ca675d346f8a1..0d8d99b29ff77 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscovery.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscovery.java
@@ -18,18 +18,14 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
-import java.util.TreeMap;
-import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
-import java.util.function.Function;
-import java.util.stream.Collector;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -76,27 +72,14 @@
public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
private final Logger logger = LoggerFactory.getLogger(HomeAssistantDiscovery.class);
private HomeAssistantConfiguration configuration;
- protected final Map> componentsPerThingID = new TreeMap<>();
- protected final Map thingIDPerTopic = new TreeMap<>();
- protected final Map results = new ConcurrentHashMap<>();
+ protected final Map> componentsPerThingID = new HashMap<>();
+ protected final Map thingIDPerTopic = new HashMap<>();
+ protected final Map results = new HashMap<>();
+ protected final Map allResults = new HashMap<>();
private @Nullable ScheduledFuture> future;
private final Gson gson;
- public static final Map HA_COMP_TO_NAME = new TreeMap<>();
- {
- HA_COMP_TO_NAME.put("alarm_control_panel", "Alarm Control Panel");
- HA_COMP_TO_NAME.put("binary_sensor", "Sensor");
- HA_COMP_TO_NAME.put("camera", "Camera");
- HA_COMP_TO_NAME.put("cover", "Blind");
- HA_COMP_TO_NAME.put("fan", "Fan");
- HA_COMP_TO_NAME.put("climate", "Climate Control");
- HA_COMP_TO_NAME.put("light", "Light");
- HA_COMP_TO_NAME.put("lock", "Lock");
- HA_COMP_TO_NAME.put("sensor", "Sensor");
- HA_COMP_TO_NAME.put("switch", "Switch");
- }
-
static final String BASE_TOPIC = "homeassistant";
static final String BIRTH_TOPIC = "homeassistant/status";
static final String ONLINE_STATUS = "online";
@@ -148,36 +131,8 @@ public Set getSupportedThingTypes() {
return typeProvider.getThingTypes(null).stream().map(ThingType::getUID).collect(Collectors.toSet());
}
- /**
- * Summarize components such as {Switch, Switch, Sensor} into string "Sensor, 2x Switch"
- *
- * @param componentNames stream of component names
- * @return summary string of component names and their counts
- */
- static String getComponentNamesSummary(Stream componentNames) {
- StringBuilder summary = new StringBuilder();
- Collector countingCollector = Collectors.counting();
- Map componentCounts = componentNames
- .collect(Collectors.groupingBy(Function.identity(), countingCollector));
- componentCounts.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry -> {
- String componentName = entry.getKey();
- long count = entry.getValue();
- if (summary.length() > 0) {
- // not the first entry, so let's add the separating comma
- summary.append(", ");
- }
- if (count > 1) {
- summary.append(count);
- summary.append("x ");
- }
- summary.append(componentName);
- });
- return summary.toString();
- }
-
@Override
- public void receivedMessage(ThingUID connectionBridge, MqttBrokerConnection connection, String topic,
- byte[] payload) {
+ public void receivedMessage(ThingUID bridgeUID, MqttBrokerConnection connection, String topic, byte[] payload) {
resetTimeout();
// For HomeAssistant we need to subscribe to a wildcard topic, because topics can either be:
@@ -188,13 +143,7 @@ public void receivedMessage(ThingUID connectionBridge, MqttBrokerConnection conn
return;
}
- // Reset the found-component timer.
- // We will collect components for the thing label description for another 2 seconds.
- final ScheduledFuture> future = this.future;
- if (future != null) {
- future.cancel(false);
- }
- this.future = scheduler.schedule(this::publishResults, 2, TimeUnit.SECONDS);
+ resetPublishTimer();
// We will of course find multiple of the same unique Thing IDs, for each different component another one.
// Therefore the components are assembled into a list and given to the DiscoveryResult label for the user to
@@ -206,45 +155,18 @@ public void receivedMessage(ThingUID connectionBridge, MqttBrokerConnection conn
.fromString(new String(payload, StandardCharsets.UTF_8), gson);
final String thingID = config.getThingId(haID.objectID);
- final ThingUID thingUID = new ThingUID(MqttBindingConstants.HOMEASSISTANT_MQTT_THING, connectionBridge,
- thingID);
-
- thingIDPerTopic.put(topic, thingUID);
-
- // We need to keep track of already found component topics for a specific thing
- final List components;
- {
- Set componentsUnordered = componentsPerThingID.computeIfAbsent(thingID,
- key -> ConcurrentHashMap.newKeySet());
-
- // Invariant. For compiler, computeIfAbsent above returns always
- // non-null
- Objects.requireNonNull(componentsUnordered);
- componentsUnordered.add(haID);
-
- components = componentsUnordered.stream().collect(Collectors.toList());
- // We sort the components for consistent jsondb serialization order of 'topics' thing property
- // Sorting key is HaID::toString, i.e. using the full topic string
- components.sort(Comparator.comparing(HaID::toString));
- }
+ final ThingUID thingUID = new ThingUID(MqttBindingConstants.HOMEASSISTANT_MQTT_THING, bridgeUID, thingID);
- final String componentNames = getComponentNamesSummary(
- components.stream().map(id -> id.component).map(c -> HA_COMP_TO_NAME.getOrDefault(c, c)));
+ synchronized (results) {
+ thingIDPerTopic.put(topic, thingUID);
- final List topics = components.stream().map(HaID::toShortTopic).collect(Collectors.toList());
+ Map properties = new HashMap<>();
+ properties = config.appendToProperties(properties);
+ properties.put("deviceId", thingID);
+ properties.put("newStyleChannels", "true");
- Map properties = new HashMap<>();
- HandlerConfiguration handlerConfig = new HandlerConfiguration(haID.baseTopic, topics);
- properties = handlerConfig.appendToProperties(properties);
- properties = config.appendToProperties(properties);
- properties.put("deviceId", thingID);
- properties.put("newStyleChannels", "true");
-
- // Because we need the new properties map with the updated "components" list
- results.put(thingUID.getAsString(),
- DiscoveryResultBuilder.create(thingUID).withProperties(properties)
- .withRepresentationProperty("deviceId").withBridge(connectionBridge)
- .withLabel(config.getThingName() + " (" + componentNames + ")").build());
+ buildResult(thingID, thingUID, config.getThingName(), haID, properties, bridgeUID);
+ }
} catch (ConfigurationException e) {
logger.warn("HomeAssistant discover error: invalid configuration of thing {} component {}: {}",
haID.objectID, haID.component, e.getMessage());
@@ -273,23 +195,64 @@ private void triggerDeviceDiscovery() {
getDiscoveryService().publish(BIRTH_TOPIC, ONLINE_STATUS.getBytes(), 1, false);
}
+ private void resetPublishTimer() {
+ // Reset the found-component timer.
+ // We will collect components for the thing label description for another 2 seconds.
+ final ScheduledFuture> future = this.future;
+ if (future != null) {
+ future.cancel(false);
+ }
+ this.future = scheduler.schedule(this::publishResults, 2, TimeUnit.SECONDS);
+ }
+
+ private void buildResult(String thingID, ThingUID thingUID, String thingName, HaID haID,
+ Map properties, ThingUID bridgeUID) {
+ // We need to keep track of already found component topics for a specific thing
+ final List components;
+ {
+ Set componentsUnordered = componentsPerThingID.computeIfAbsent(thingID, key -> new HashSet<>());
+
+ // Invariant. For compiler, computeIfAbsent above returns always
+ // non-null
+ Objects.requireNonNull(componentsUnordered);
+ componentsUnordered.add(haID);
+
+ components = componentsUnordered.stream().collect(Collectors.toList());
+ // We sort the components for consistent jsondb serialization order of 'topics' thing property
+ // Sorting key is HaID::toString, i.e. using the full topic string
+ components.sort(Comparator.comparing(HaID::toString));
+ }
+
+ final List topics = components.stream().map(HaID::toShortTopic).collect(Collectors.toList());
+
+ HandlerConfiguration handlerConfig = new HandlerConfiguration(haID.baseTopic, topics);
+ properties = handlerConfig.appendToProperties(properties);
+
+ DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
+ .withRepresentationProperty("deviceId").withBridge(bridgeUID).withLabel(thingName).build();
+ // Because we need the new properties map with the updated "components" list
+ results.put(thingUID.toString(), result);
+ allResults.put(thingUID.toString(), result);
+ }
+
protected void publishResults() {
Collection localResults;
- localResults = new ArrayList<>(results.values());
- results.clear();
- componentsPerThingID.clear();
+ synchronized (results) {
+ localResults = new ArrayList<>(results.values());
+ results.clear();
+ }
for (DiscoveryResult result : localResults) {
thingDiscovered(result);
}
}
@Override
- public void topicVanished(ThingUID connectionBridge, MqttBrokerConnection connection, String topic) {
+ public void topicVanished(ThingUID bridgeUID, MqttBrokerConnection connection, String topic) {
if (!topic.endsWith("/config")) {
return;
}
- if (thingIDPerTopic.containsKey(topic)) {
+ synchronized (results) {
ThingUID thingUID = thingIDPerTopic.remove(topic);
if (thingUID != null) {
final String thingID = thingUID.getId();
@@ -299,7 +262,20 @@ public void topicVanished(ThingUID connectionBridge, MqttBrokerConnection connec
Set components = componentsPerThingID.getOrDefault(thingID, Collections.emptySet());
components.remove(haID);
if (components.isEmpty()) {
+ allResults.remove(thingUID.toString());
+ results.remove(thingUID.toString());
thingRemoved(thingUID);
+ } else {
+ resetPublishTimer();
+
+ DiscoveryResult existingThing = allResults.get(thingUID.toString());
+ if (existingThing == null) {
+ logger.warn("Could not find discovery result for removed component {}; this is a bug",
+ thingUID);
+ return;
+ }
+ Map properties = new HashMap<>(existingThing.getProperties());
+ buildResult(thingID, thingUID, existingThing.getLabel(), haID, properties, bridgeUID);
}
}
}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java
index 11ce67c84f7ae..2321641849616 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java
@@ -47,6 +47,7 @@
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.core.validation.ConfigValidationException;
+import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
@@ -82,7 +83,7 @@
*/
@NonNullByDefault
public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
- implements ComponentDiscovered, Consumer>> {
+ implements ComponentDiscovered, Consumer> {
public static final String AVAILABILITY_CHANNEL = "availability";
private static final Comparator> COMPONENT_COMPARATOR = Comparator
.comparing((AbstractComponent> component) -> component.hasGroup())
@@ -95,13 +96,15 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
protected final MqttChannelStateDescriptionProvider stateDescriptionProvider;
protected final ChannelTypeRegistry channelTypeRegistry;
protected final Jinjava jinjava;
+ protected final UnitProvider unitProvider;
public final int attributeReceiveTimeout;
- protected final DelayedBatchProcessing> delayedProcessing;
+ protected final DelayedBatchProcessing delayedProcessing;
protected final DiscoverComponents discoverComponents;
private final Gson gson;
protected final Map<@Nullable String, AbstractComponent>> haComponents = new HashMap<>();
protected final Map<@Nullable String, AbstractComponent>> haComponentsByUniqueId = new HashMap<>();
+ protected final Map> haComponentsByHaId = new HashMap<>();
protected final Map channelStates = new HashMap<>();
protected HandlerConfiguration config = new HandlerConfiguration();
@@ -122,20 +125,21 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
*/
public HomeAssistantThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
- Jinjava jinjava, int subscribeTimeout, int attributeReceiveTimeout) {
+ Jinjava jinjava, UnitProvider unitProvider, int subscribeTimeout, int attributeReceiveTimeout) {
super(thing, subscribeTimeout);
this.gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
this.channelTypeProvider = channelTypeProvider;
this.stateDescriptionProvider = stateDescriptionProvider;
this.channelTypeRegistry = channelTypeRegistry;
this.jinjava = jinjava;
+ this.unitProvider = unitProvider;
this.attributeReceiveTimeout = attributeReceiveTimeout;
this.delayedProcessing = new DelayedBatchProcessing<>(attributeReceiveTimeout, this, scheduler);
newStyleChannels = "true".equals(thing.getProperties().get("newStyleChannels"));
this.discoverComponents = new DiscoverComponents(thing.getUID(), scheduler, this, this, gson, jinjava,
- newStyleChannels);
+ unitProvider, newStyleChannels);
}
@Override
@@ -183,7 +187,8 @@ public void initialize() {
String channelConfigurationJSON = (String) channelConfig.get("config");
try {
AbstractComponent> component = ComponentFactory.createComponent(thingUID, haID,
- channelConfigurationJSON, this, this, scheduler, gson, jinjava, newStyleChannels);
+ channelConfigurationJSON, this, this, scheduler, gson, jinjava, unitProvider,
+ newStyleChannels);
if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
typeID = calculateThingTypeUID(component);
}
@@ -267,12 +272,38 @@ public void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent>
delayedProcessing.accept(component);
}
+ @Override
+ public void componentRemoved(HaID haID) {
+ delayedProcessing.accept(haID);
+ }
+
/**
* Callback of {@link DelayedBatchProcessing}.
- * Add all newly discovered components to the Thing and start the components.
+ * Add all newly discovered and removed components to the Thing and start the components.
*/
@Override
- public void accept(List> discoveredComponentsList) {
+ public void accept(List actions) {
+ List> discoveredComponents = new ArrayList<>();
+ List removedComponents = new ArrayList<>();
+ for (Object item : actions) {
+ if (item instanceof AbstractComponent> component) {
+ discoveredComponents.add(component);
+ } else if (item instanceof HaID removedComponent) {
+ removedComponents.add(removedComponent);
+ }
+ }
+ if (!discoveredComponents.isEmpty()) {
+ addComponents(discoveredComponents);
+ }
+ if (!removedComponents.isEmpty()) {
+ removeComponents(removedComponents);
+ }
+ }
+
+ /**
+ * Add all newly discovered components to the Thing and start the components.
+ */
+ private void addComponents(List> discoveredComponentsList) {
MqttBrokerConnection connection = this.connection;
if (connection == null) {
return;
@@ -293,6 +324,7 @@ public void accept(List> discoveredComponentsList) {
// The component will be replaced in a moment.
known.stop();
haComponentsByUniqueId.remove(discovered.getUniqueId());
+ haComponentsByHaId.remove(known.getHaID());
haComponents.remove(known.getComponentId());
if (!known.getComponentId().equals(discovered.getComponentId())) {
discovered.resolveConflict();
@@ -321,6 +353,29 @@ public void accept(List> discoveredComponentsList) {
}
}
+ /**
+ * Remove all matching deleted components.
+ */
+ private void removeComponents(List removedComponentsList) {
+ synchronized (haComponents) {
+ boolean componentActuallyRemoved = false;
+ for (HaID removed : removedComponentsList) {
+ AbstractComponent> known = haComponentsByHaId.get(removed);
+ if (known != null) {
+ // Don't wait for the future to complete. We are also not interested in failures.
+ known.stop();
+ haComponentsByUniqueId.remove(known.getUniqueId());
+ haComponents.remove(known.getComponentId());
+ haComponentsByHaId.remove(removed);
+ componentActuallyRemoved = true;
+ }
+ }
+ if (componentActuallyRemoved) {
+ updateThingType(getThing().getThingTypeUID());
+ }
+ }
+ }
+
@Override
protected void updateThingStatus(boolean messageReceived, Optional availabilityTopicsSeen) {
if (availabilityTopicsSeen.orElse(messageReceived)) {
@@ -402,7 +457,7 @@ private boolean updateThingType(ThingTypeUID typeID) {
return true;
}
- private ThingTypeUID calculateThingTypeUID(AbstractComponent component) {
+ private ThingTypeUID calculateThingTypeUID(AbstractComponent> component) {
return new ThingTypeUID(MqttBindingConstants.BINDING_ID, MqttBindingConstants.HOMEASSISTANT_MQTT_THING.getId()
+ "_" + component.getChannelConfiguration().getThingId(component.getHaID().objectID));
}
@@ -428,8 +483,8 @@ private void releaseStateUpdated(Update.ReleaseState state) {
}
// should only be called when it's safe to access haComponents
- private boolean addComponent(AbstractComponent component) {
- AbstractComponent existing = haComponents.get(component.getComponentId());
+ private boolean addComponent(AbstractComponent> component) {
+ AbstractComponent> existing = haComponents.get(component.getComponentId());
if (existing != null) {
// DeviceTriggers that are for the same subtype, topic, and value template
// can be coalesced together
@@ -455,6 +510,7 @@ private boolean addComponent(AbstractComponent component) {
});
}
haComponentsByUniqueId.put(component.getUniqueId(), component);
+ haComponentsByHaId.put(component.getHaID(), component);
return false;
}
}
@@ -467,6 +523,7 @@ private boolean addComponent(AbstractComponent component) {
}
haComponents.put(component.getComponentId(), component);
haComponentsByUniqueId.put(component.getUniqueId(), component);
+ haComponentsByHaId.put(component.getHaID(), component);
return true;
}
@@ -478,16 +535,16 @@ private List flattenChannelConfiguration(Configuration multiCompo
ChannelUID channelUID) {
Object component = multiComponentChannelConfig.get("component");
Object nodeid = multiComponentChannelConfig.get("nodeid");
- if ((multiComponentChannelConfig.get("objectid") instanceof List objectIds)
- && (multiComponentChannelConfig.get("config") instanceof List configurations)) {
+ if ((multiComponentChannelConfig.get("objectid") instanceof List> objectIds)
+ && (multiComponentChannelConfig.get("config") instanceof List> configurations)) {
if (objectIds.size() != configurations.size()) {
logger.warn("objectid and config for channel {} do not have the same number of items; ignoring",
channelUID);
return List.of();
}
- List result = new ArrayList();
- Iterator objectIdIterator = objectIds.iterator();
- Iterator configIterator = configurations.iterator();
+ List result = new ArrayList<>();
+ Iterator> objectIdIterator = objectIds.iterator();
+ Iterator> configIterator = configurations.iterator();
while (objectIdIterator.hasNext()) {
Configuration componentConfiguration = new Configuration();
componentConfiguration.put("component", component);
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/i18n/mqtt.properties b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/i18n/mqtt.properties
index 22cb9fb44fb43..659b0d8bc6e47 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/i18n/mqtt.properties
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/i18n/mqtt.properties
@@ -16,8 +16,12 @@ channel-type.mqtt.ha-color-advanced.label = Color
channel-type.mqtt.ha-color.label = Color
channel-type.mqtt.ha-dimmer-advanced.label = Dimmer
channel-type.mqtt.ha-dimmer.label = Dimmer
+channel-type.mqtt.ha-gps-accuracy.label = GPS Accuracy
+channel-type.mqtt.ha-gps-accuracy.description = The accuracy of the GPS fix, in meters.
+channel-type.mqtt.ha-humidity.label = Humidity
channel-type.mqtt.ha-image-advanced.label = Image
channel-type.mqtt.ha-image.label = Image
+channel-type.mqtt.ha-location.label = Location
channel-type.mqtt.ha-number-advanced.label = Number
channel-type.mqtt.ha-number.label = Number
channel-type.mqtt.ha-rollershutter-advanced.label = Rollershutter
@@ -26,6 +30,7 @@ channel-type.mqtt.ha-string-advanced.label = String
channel-type.mqtt.ha-string.label = String
channel-type.mqtt.ha-switch-advanced.label = Switch
channel-type.mqtt.ha-switch.label = Switch
+channel-type.mqtt.ha-temperature.label = Temperature
channel-type.mqtt.ha-trigger-advanced.label = Trigger
channel-type.mqtt.ha-trigger.label = Trigger
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/i18n/mqtt_it.properties b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/i18n/mqtt_it.properties
index d0a3c2f683fb0..f3f305f8980fe 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/i18n/mqtt_it.properties
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/i18n/mqtt_it.properties
@@ -16,8 +16,12 @@ channel-type.mqtt.ha-color-advanced.label = Colore
channel-type.mqtt.ha-color.label = Colore
channel-type.mqtt.ha-dimmer-advanced.label = Varialuce
channel-type.mqtt.ha-dimmer.label = Varialuce
+channel-type.mqtt.ha-gps-accuracy.label = Precisione GPS
+channel-type.mqtt.ha-gps-accuracy.description = La precisione della correzione GPS, in metri.
+channel-type.mqtt.ha-humidity.label = Umidità
channel-type.mqtt.ha-image-advanced.label = Immagine
channel-type.mqtt.ha-image.label = Immagine
+channel-type.mqtt.ha-location.label = Località
channel-type.mqtt.ha-number-advanced.label = Numero
channel-type.mqtt.ha-number.label = Numero
channel-type.mqtt.ha-rollershutter-advanced.label = Tapparella
@@ -26,6 +30,7 @@ channel-type.mqtt.ha-string-advanced.label = Stringa
channel-type.mqtt.ha-string.label = Stringa
channel-type.mqtt.ha-switch-advanced.label = Interruttore
channel-type.mqtt.ha-switch.label = Interruttore
+channel-type.mqtt.ha-temperature.label = Temperatura
channel-type.mqtt.ha-trigger-advanced.label = Pulsante
channel-type.mqtt.ha-trigger.label = Pulsante
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/thing/homeassistant-channels.xml b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/thing/homeassistant-channels.xml
index cd4679398b1dc..a949792f5017f 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/thing/homeassistant-channels.xml
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/thing/homeassistant-channels.xml
@@ -16,18 +16,38 @@
+
+ Number:Dimensionless
+ Humidity
+
+
+
Image
Image
+
+ Location
+ Location
+
+
+
Number
Number
+
+ Number:Length
+ GPS Accuracy
+ The accuracy of the GPS fix, in meters.
+ veto
+
+
+
Rollershutter
Rollershutter
@@ -46,6 +66,12 @@
+
+ Number:Temperature
+ Temperature
+
+
+
trigger
Trigger
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformationTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformationTests.java
index 7ef078cc53422..f8341fbb1c6c9 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformationTests.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformationTests.java
@@ -32,6 +32,7 @@
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttThingHandlerFactory;
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
+import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.test.storage.VolatileStorageService;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.thing.type.ThingTypeRegistry;
@@ -44,6 +45,7 @@
@NonNullByDefault
public class HomeAssistantChannelTransformationTests {
protected @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistry;
+ protected @Mock @NonNullByDefault({}) UnitProvider unitProvider;
protected @NonNullByDefault({}) HomeAssistantChannelTransformation transformation;
@@ -54,7 +56,7 @@ public void beforeEachChannelTransformationTest() {
MqttChannelStateDescriptionProvider stateDescriptionProvider = new MqttChannelStateDescriptionProvider();
ChannelTypeRegistry channelTypeRegistry = new ChannelTypeRegistry();
MqttThingHandlerFactory thingHandlerFactory = new MqttThingHandlerFactory(channelTypeProvider,
- stateDescriptionProvider, channelTypeRegistry);
+ stateDescriptionProvider, channelTypeRegistry, unitProvider);
AbstractComponent component = Mockito.mock(AbstractComponent.class);
HaID haID = new HaID("homeassistant/light/pool/light/config");
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java
index cb9c4fe5a306d..febda9cf5b8ca 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java
@@ -41,6 +41,7 @@
import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
+import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatusInfo;
@@ -64,6 +65,7 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
private @NonNullByDefault({}) LatchThingHandler thingHandler;
+ protected @Mock @NonNullByDefault({}) UnitProvider unitProvider;
@BeforeEach
public void setupThingHandler() {
@@ -84,7 +86,7 @@ public void setupThingHandler() {
haThing.setProperty("newStyleChannels", "true");
}
thingHandler = new LatchThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
- channelTypeRegistry, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
+ channelTypeRegistry, unitProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock);
thingHandler = spy(thingHandler);
@@ -341,9 +343,9 @@ protected static class LatchThingHandler extends HomeAssistantThingHandler {
public LatchThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
- int subscribeTimeout, int attributeReceiveTimeout) {
+ UnitProvider unitProvider, int subscribeTimeout, int attributeReceiveTimeout) {
super(thing, channelTypeProvider, stateDescriptionProvider, channelTypeRegistry, new Jinjava(),
- subscribeTimeout, attributeReceiveTimeout);
+ unitProvider, subscribeTimeout, attributeReceiveTimeout);
}
@Override
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/DeviceTrackerTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/DeviceTrackerTests.java
new file mode 100644
index 0000000000000..8df94e88717f3
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/DeviceTrackerTests.java
@@ -0,0 +1,274 @@
+/**
+ * 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.mqtt.homeassistant.internal.component;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mqtt.generic.values.LocationValue;
+import org.openhab.binding.mqtt.generic.values.NumberValue;
+import org.openhab.binding.mqtt.generic.values.OnOffValue;
+import org.openhab.binding.mqtt.generic.values.TextValue;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PointType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * Tests for {@link DeviceTracker}
+ *
+ * @author Cody Cutrer - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceTrackerTests extends AbstractComponentTests {
+ public static final String CONFIG_TOPIC = "device_tracker/112233445566-tracker";
+
+ @Test
+ public void testIPhone() throws InterruptedException {
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
+ {
+ "stat_t": "home/TheengsGateway/BTtoMQTT/112233445566",
+ "name": "APPLEDEVICE-tracker",
+ "uniq_id": "112233445566-tracker",
+ "val_tpl": "{% if value_json.get('rssi') -%}home{%- else -%}not_home{%- endif %}",
+ "source_type": "bluetooth_le",
+ "device": {
+ "ids": ["112233445566"],
+ "cns": [["mac", "112233445566"]],
+ "mf": "Apple",
+ "mdl": "APPLEDEVICE",
+ "name": "Apple iPhone/iPad-123456",
+ "via_device": "TheengsGateway"
+ }
+ }
+ """);
+
+ assertThat(component.channels.size(), is(3));
+ assertThat(component.getName(), is("APPLEDEVICE-tracker"));
+
+ assertChannel(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, "home/TheengsGateway/BTtoMQTT/112233445566",
+ "", "Location Name", TextValue.class);
+ assertChannel(component, DeviceTracker.HOME_CHANNEL_ID, "", "", "At Home", OnOffValue.class);
+ assertChannel(component, DeviceTracker.SOURCE_TYPE_CHANNEL_ID, "", "", "Source Type", TextValue.class);
+ assertState(component, DeviceTracker.SOURCE_TYPE_CHANNEL_ID, new StringType("bluetooth_le"));
+
+ publishMessage("home/TheengsGateway/BTtoMQTT/112233445566", """
+ {
+ "id": "11:22:33:44:55:66",
+ "rssi": -55,
+ "brand": "Apple",
+ "model": "Apple iPhone/iPad",
+ "model_id": "APPLEDEVICE",
+ "type": "TRACK",
+ "unlocked": false
+ }
+ """);
+ assertState(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, new StringType("home"));
+ assertState(component, DeviceTracker.HOME_CHANNEL_ID, OnOffType.ON);
+ publishMessage("home/TheengsGateway/BTtoMQTT/112233445566", """
+ {"id": "11:22:33:44:55:66", "presence": "absent"}
+ """);
+ assertState(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, new StringType("not_home"));
+ assertState(component, DeviceTracker.HOME_CHANNEL_ID, OnOffType.OFF);
+ }
+
+ @Test
+ public void testGeneric() throws InterruptedException {
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
+ {
+ "stat_t": "devices/112233445566",
+ "name": "tracker"
+ }
+ """);
+
+ assertThat(component.channels.size(), is(2));
+ assertThat(component.getName(), is("tracker"));
+
+ assertChannel(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, "devices/112233445566", "", "Location Name",
+ TextValue.class);
+ assertChannel(component, DeviceTracker.HOME_CHANNEL_ID, "", "", "At Home", OnOffValue.class);
+
+ publishMessage("devices/112233445566", "home");
+ assertState(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, new StringType("home"));
+ assertState(component, DeviceTracker.HOME_CHANNEL_ID, OnOffType.ON);
+ publishMessage("devices/112233445566", "not_home");
+ assertState(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, new StringType("not_home"));
+ assertState(component, DeviceTracker.HOME_CHANNEL_ID, OnOffType.OFF);
+ publishMessage("devices/112233445566", "work");
+ assertState(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, new StringType("work"));
+ assertState(component, DeviceTracker.HOME_CHANNEL_ID, UnDefType.UNDEF);
+ publishMessage("devices/112233445566", "None");
+ assertState(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, UnDefType.NULL);
+ assertState(component, DeviceTracker.HOME_CHANNEL_ID, UnDefType.NULL);
+ }
+
+ @Test
+ public void testGPS() throws InterruptedException {
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
+ {
+ "stat_t": "devices/112233445566",
+ "name": "tracker",
+ "json_attributes_topic": "devices/112233445566/json"
+ }
+ """);
+
+ assertThat(component.channels.size(), is(5));
+ assertThat(component.getName(), is("tracker"));
+
+ assertChannel(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, "devices/112233445566", "", "Location Name",
+ TextValue.class);
+ assertChannel(component, DeviceTracker.HOME_CHANNEL_ID, "", "", "At Home", OnOffValue.class);
+ assertChannel(component, DeviceTracker.LOCATION_CHANNEL_ID, "", "", "Location", LocationValue.class);
+ assertChannel(component, DeviceTracker.GPS_ACCURACY_CHANNEL_ID, "", "", "GPS Accuracy", NumberValue.class);
+ assertChannel(component, DeviceTracker.JSON_ATTRIBUTES_CHANNEL_ID, "devices/112233445566/json", "",
+ "JSON Attributes", TextValue.class);
+
+ publishMessage("devices/112233445566", "home");
+ assertState(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, new StringType("home"));
+ assertState(component, DeviceTracker.HOME_CHANNEL_ID, OnOffType.ON);
+ assertState(component, DeviceTracker.LOCATION_CHANNEL_ID, UnDefType.NULL);
+ assertState(component, DeviceTracker.GPS_ACCURACY_CHANNEL_ID, UnDefType.NULL);
+ publishMessage("devices/112233445566", "not_home");
+ assertState(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, new StringType("not_home"));
+ assertState(component, DeviceTracker.HOME_CHANNEL_ID, OnOffType.OFF);
+ assertState(component, DeviceTracker.LOCATION_CHANNEL_ID, UnDefType.NULL);
+ assertState(component, DeviceTracker.GPS_ACCURACY_CHANNEL_ID, UnDefType.NULL);
+ publishMessage("devices/112233445566", "work");
+ assertState(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, new StringType("work"));
+ assertState(component, DeviceTracker.HOME_CHANNEL_ID, UnDefType.UNDEF);
+ assertState(component, DeviceTracker.LOCATION_CHANNEL_ID, UnDefType.NULL);
+ assertState(component, DeviceTracker.GPS_ACCURACY_CHANNEL_ID, UnDefType.NULL);
+ publishMessage("devices/112233445566/json", "not JSON");
+ assertState(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, new StringType("work"));
+ assertState(component, DeviceTracker.HOME_CHANNEL_ID, UnDefType.UNDEF);
+ assertState(component, DeviceTracker.LOCATION_CHANNEL_ID, UnDefType.NULL);
+ assertState(component, DeviceTracker.GPS_ACCURACY_CHANNEL_ID, UnDefType.NULL);
+ publishMessage("devices/112233445566/json", """
+ {
+ "nothing": 1
+ }
+ """);
+ assertState(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, new StringType("work"));
+ assertState(component, DeviceTracker.HOME_CHANNEL_ID, UnDefType.UNDEF);
+ assertState(component, DeviceTracker.LOCATION_CHANNEL_ID, UnDefType.NULL);
+ assertState(component, DeviceTracker.GPS_ACCURACY_CHANNEL_ID, UnDefType.NULL);
+ publishMessage("devices/112233445566/json", """
+ {
+ "latitude": 45.5,
+ "longitude": 91.1
+ }
+ """);
+ assertState(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, new StringType("work"));
+ assertState(component, DeviceTracker.HOME_CHANNEL_ID, UnDefType.UNDEF);
+ assertState(component, DeviceTracker.LOCATION_CHANNEL_ID,
+ new PointType(new DecimalType(45.5), new DecimalType(91.1)));
+ assertState(component, DeviceTracker.GPS_ACCURACY_CHANNEL_ID, UnDefType.NULL);
+ publishMessage("devices/112233445566/json", """
+ {
+ "latitude": 45.6,
+ "longitude": 91.2,
+ "gps_accuracy": 5.5
+ }
+ """);
+ assertState(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, new StringType("work"));
+ assertState(component, DeviceTracker.HOME_CHANNEL_ID, UnDefType.UNDEF);
+ assertState(component, DeviceTracker.LOCATION_CHANNEL_ID,
+ new PointType(new DecimalType(45.6), new DecimalType(91.2)));
+ assertState(component, DeviceTracker.GPS_ACCURACY_CHANNEL_ID, new QuantityType<>(5.5, SIUnits.METRE));
+ publishMessage("devices/112233445566/json", """
+ {
+ "latitude": 45.7,
+ "longitude": 91.3
+ }
+ """);
+ assertState(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, new StringType("work"));
+ assertState(component, DeviceTracker.HOME_CHANNEL_ID, UnDefType.UNDEF);
+ assertState(component, DeviceTracker.LOCATION_CHANNEL_ID,
+ new PointType(new DecimalType(45.7), new DecimalType(91.3)));
+ assertState(component, DeviceTracker.GPS_ACCURACY_CHANNEL_ID, UnDefType.NULL);
+ publishMessage("devices/112233445566", "None");
+ assertState(component, DeviceTracker.LOCATION_NAME_CHANNEL_ID, UnDefType.NULL);
+ assertState(component, DeviceTracker.HOME_CHANNEL_ID, UnDefType.NULL);
+ assertState(component, DeviceTracker.LOCATION_CHANNEL_ID, UnDefType.NULL);
+ assertState(component, DeviceTracker.GPS_ACCURACY_CHANNEL_ID, UnDefType.NULL);
+ }
+
+ @Test
+ public void testGPSOnly() throws InterruptedException {
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
+ {
+ "name": "tracker",
+ "json_attributes_topic": "devices/112233445566/json"
+ }
+ """);
+
+ assertThat(component.channels.size(), is(3));
+ assertThat(component.getName(), is("tracker"));
+
+ assertChannel(component, DeviceTracker.LOCATION_CHANNEL_ID, "", "", "Location", LocationValue.class);
+ assertChannel(component, DeviceTracker.GPS_ACCURACY_CHANNEL_ID, "", "", "GPS Accuracy", NumberValue.class);
+ assertChannel(component, DeviceTracker.JSON_ATTRIBUTES_CHANNEL_ID, "devices/112233445566/json", "",
+ "JSON Attributes", TextValue.class);
+
+ publishMessage("devices/112233445566/json", "not JSON");
+ assertState(component, DeviceTracker.LOCATION_CHANNEL_ID, UnDefType.NULL);
+ assertState(component, DeviceTracker.GPS_ACCURACY_CHANNEL_ID, UnDefType.NULL);
+ publishMessage("devices/112233445566/json", """
+ {
+ "nothing": 1
+ }
+ """);
+ assertState(component, DeviceTracker.LOCATION_CHANNEL_ID, UnDefType.NULL);
+ assertState(component, DeviceTracker.GPS_ACCURACY_CHANNEL_ID, UnDefType.NULL);
+ publishMessage("devices/112233445566/json", """
+ {
+ "latitude": 45.5,
+ "longitude": 91.1
+ }
+ """);
+ assertState(component, DeviceTracker.LOCATION_CHANNEL_ID,
+ new PointType(new DecimalType(45.5), new DecimalType(91.1)));
+ assertState(component, DeviceTracker.GPS_ACCURACY_CHANNEL_ID, UnDefType.NULL);
+ publishMessage("devices/112233445566/json", """
+ {
+ "latitude": 45.6,
+ "longitude": 91.2,
+ "gps_accuracy": 5.5
+ }
+ """);
+ assertState(component, DeviceTracker.LOCATION_CHANNEL_ID,
+ new PointType(new DecimalType(45.6), new DecimalType(91.2)));
+ assertState(component, DeviceTracker.GPS_ACCURACY_CHANNEL_ID, new QuantityType<>(5.5, SIUnits.METRE));
+ publishMessage("devices/112233445566/json", """
+ {
+ "latitude": 45.7,
+ "longitude": 91.3
+ }
+ """);
+ assertState(component, DeviceTracker.LOCATION_CHANNEL_ID,
+ new PointType(new DecimalType(45.7), new DecimalType(91.3)));
+ assertState(component, DeviceTracker.GPS_ACCURACY_CHANNEL_ID, UnDefType.NULL);
+ }
+
+ @Override
+ protected Set getConfigTopics() {
+ return Set.of(CONFIG_TOPIC);
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/FanTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/FanTests.java
index 7869752548a2f..7978ff0d13717 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/FanTests.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/FanTests.java
@@ -16,6 +16,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import java.math.BigDecimal;
+import java.math.MathContext;
import java.util.Objects;
import java.util.Set;
@@ -90,6 +91,70 @@ public void test() throws InterruptedException {
assertPublished("zigbee2mqtt/fan/set/state", "ON_");
}
+ @SuppressWarnings("null")
+ @Test
+ public void testPercentageWithTemplates() throws InterruptedException {
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+ """
+ {
+ "availability": [
+ {
+ "topic": "zigbee2mqtt/bridge/state"
+ }
+ ],
+ "device": {
+ "identifiers": [
+ "zigbee2mqtt_0x0000000000000000"
+ ],
+ "manufacturer": "Fans inc",
+ "model": "Fan",
+ "name": "FanBlower",
+ "sw_version": "Zigbee2MQTT 1.18.2"
+ },
+ "name": "fan",
+ "state_topic": "zigbee2mqtt/fan",
+ "state_value_template": "{{ value_json.fan_state }}",
+ "command_topic": "zigbee2mqtt/fan/set/fan_state",
+ "percentage_command_template": "{{ {0:'off', 1:'low', 2:'medium', 3:'high'}[value] | default('') }}",
+ "percentage_command_topic": "zigbee2mqtt/fan/set/fan_mode",
+ "percentage_state_topic": "zigbee2mqtt/fan",
+ "percentage_value_template": "{{ {'off':0, 'low':1, 'medium':2, 'high':3}[value_json.fan_mode] | default('None') }}",
+ "speed_range_max": 3,
+ "speed_range_min": 1
+ }
+ """);
+
+ assertThat(component.channels.size(), is(1));
+ assertThat(component.getName(), is("fan"));
+
+ assertChannel(component, Fan.SPEED_CHANNEL_ID, "zigbee2mqtt/fan", "zigbee2mqtt/fan/set/fan_mode", "Speed",
+ PercentageValue.class, null);
+
+ publishMessage("zigbee2mqtt/fan", "{ \"fan_state\": \"OFF\", \"fan_mode\": \"high\"}");
+ assertState(component, Fan.SPEED_CHANNEL_ID, PercentType.ZERO);
+ publishMessage("zigbee2mqtt/fan", "{ \"fan_state\": \"ON\", \"fan_mode\": \"high\"}");
+ assertState(component, Fan.SPEED_CHANNEL_ID, PercentType.HUNDRED);
+ publishMessage("zigbee2mqtt/fan", "{ \"fan_state\": \"ON\", \"fan_mode\": \"medium\"}");
+ assertState(component, Fan.SPEED_CHANNEL_ID,
+ new PercentType(new BigDecimal(200).divide(new BigDecimal(3), MathContext.DECIMAL128)));
+ publishMessage("zigbee2mqtt/fan", "{ \"fan_state\": \"ON\", \"fan_mode\": \"low\"}");
+ assertState(component, Fan.SPEED_CHANNEL_ID,
+ new PercentType(new BigDecimal(100).divide(new BigDecimal(3), MathContext.DECIMAL128)));
+
+ component.getChannel(Fan.SPEED_CHANNEL_ID).getState().publishValue(OnOffType.OFF);
+ assertPublished("zigbee2mqtt/fan/set/fan_state", "OFF");
+ component.getChannel(Fan.SPEED_CHANNEL_ID).getState().publishValue(OnOffType.ON);
+ assertPublished("zigbee2mqtt/fan/set/fan_state", "ON");
+ component.getChannel(Fan.SPEED_CHANNEL_ID).getState().publishValue(PercentType.HUNDRED);
+ assertPublished("zigbee2mqtt/fan/set/fan_mode", "high");
+ component.getChannel(Fan.SPEED_CHANNEL_ID).getState().publishValue(PercentType.ZERO);
+ assertPublished("zigbee2mqtt/fan/set/fan_mode", "off");
+ component.getChannel(Fan.SPEED_CHANNEL_ID).getState().publishValue(new PercentType(33));
+ assertPublished("zigbee2mqtt/fan/set/fan_mode", "low");
+ component.getChannel(Fan.SPEED_CHANNEL_ID).getState().publishValue(new PercentType(66));
+ assertPublished("zigbee2mqtt/fan/set/fan_mode", "medium");
+ }
+
@SuppressWarnings("null")
@Test
public void testInferredOptimistic() throws InterruptedException {
@@ -287,7 +352,7 @@ public void testComplex() throws InterruptedException {
assertChannel(component, Fan.SPEED_CHANNEL_ID, "bedroom_fan/speed/percentage_state",
"bedroom_fan/speed/percentage", "Speed", PercentageValue.class);
var channel = Objects.requireNonNull(component.getChannel(Fan.SPEED_CHANNEL_ID));
- assertThat(channel.getStateDescription().getStep(), is(BigDecimal.valueOf(10.0d)));
+ assertThat(channel.getStateDescription().getStep(), is(BigDecimal.valueOf(10)));
assertChannel(component, Fan.OSCILLATION_CHANNEL_ID, "bedroom_fan/oscillation/state",
"bedroom_fan/oscillation/set", "Oscillation", OnOffValue.class);
assertChannel(component, Fan.DIRECTION_CHANNEL_ID, "bedroom_fan/direction/state", "bedroom_fan/direction/set",
@@ -300,16 +365,16 @@ public void testComplex() throws InterruptedException {
publishMessage("bedroom_fan/on/state", "false");
assertState(component, Fan.SPEED_CHANNEL_ID, PercentType.ZERO);
publishMessage("bedroom_fan/on/state", "true");
- publishMessage("bedroom_fan/speed/percentage_state", "50");
+ publishMessage("bedroom_fan/speed/percentage_state", "5");
assertState(component, Fan.SPEED_CHANNEL_ID, new PercentType(50));
publishMessage("bedroom_fan/on/state", "false");
// Off, even though we got an updated speed
assertState(component, Fan.SPEED_CHANNEL_ID, PercentType.ZERO);
- publishMessage("bedroom_fan/speed/percentage_state", "25");
+ publishMessage("bedroom_fan/speed/percentage_state", "2");
assertState(component, Fan.SPEED_CHANNEL_ID, PercentType.ZERO);
publishMessage("bedroom_fan/on/state", "true");
// Now that it's on, the channel reflects the proper speed
- assertState(component, Fan.SPEED_CHANNEL_ID, new PercentType(25));
+ assertState(component, Fan.SPEED_CHANNEL_ID, new PercentType(20));
publishMessage("bedroom_fan/oscillation/state", "true");
assertState(component, Fan.OSCILLATION_CHANNEL_ID, OnOffType.ON);
@@ -333,7 +398,7 @@ public void testComplex() throws InterruptedException {
// Setting to a specific speed turns it on first
component.getChannel(Fan.SPEED_CHANNEL_ID).getState().publishValue(PercentType.HUNDRED);
assertPublished("bedroom_fan/on/set", "true");
- assertPublished("bedroom_fan/speed/percentage", "100");
+ assertPublished("bedroom_fan/speed/percentage", "10");
component.getChannel(Fan.OSCILLATION_CHANNEL_ID).getState().publishValue(OnOffType.ON);
assertPublished("bedroom_fan/oscillation/set", "true");
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/HumidifierTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/HumidifierTests.java
new file mode 100644
index 0000000000000..7942af65e1708
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/HumidifierTests.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.mqtt.homeassistant.internal.component;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mqtt.generic.values.NumberValue;
+import org.openhab.binding.mqtt.generic.values.OnOffValue;
+import org.openhab.binding.mqtt.generic.values.TextValue;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * Tests for {@link Humidifier}
+ *
+ * @author Cody Cutrer - Initial contribution
+ */
+@NonNullByDefault
+public class HumidifierTests extends AbstractComponentTests {
+ public static final String CONFIG_TOPIC = "humidifier/bedroom_humidifier";
+
+ @SuppressWarnings("null")
+ @Test
+ public void test() {
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
+ {
+ "platform": "humidifier",
+ "name": "Bedroom humidifier",
+ "device_class": "humidifier",
+ "state_topic": "bedroom_humidifier/on/state",
+ "action_topic": "bedroom_humidifier/action",
+ "command_topic": "bedroom_humidifier/on/set",
+ "current_humidity_topic": "bedroom_humidifier/humidity/current",
+ "target_humidity_command_topic": "bedroom_humidifier/humidity/set",
+ "target_humidity_state_topic": "bedroom_humidifier/humidity/state",
+ "mode_state_topic": "bedroom_humidifier/mode/state",
+ "mode_command_topic": "bedroom_humidifier/preset/preset_mode",
+ "modes": [
+ "normal",
+ "eco",
+ "away",
+ "boost",
+ "comfort",
+ "home",
+ "sleep",
+ "auto",
+ "baby"],
+ "qos": 0,
+ "payload_on": "true",
+ "payload_off": "false",
+ "min_humidity": 30,
+ "max_humidity": 80
+ }
+ """);
+
+ assertThat(component.channels.size(), is(6));
+ assertThat(component.getName(), is("Bedroom humidifier"));
+
+ assertChannel(component, Humidifier.STATE_CHANNEL_ID, "bedroom_humidifier/on/state",
+ "bedroom_humidifier/on/set", "State", OnOffValue.class);
+ assertChannel(component, Humidifier.ACTION_CHANNEL_ID, "bedroom_humidifier/action", "", "Action",
+ TextValue.class);
+ assertChannel(component, Humidifier.MODE_CHANNEL_ID, "bedroom_humidifier/mode/state",
+ "bedroom_humidifier/preset/preset_mode", "Mode", TextValue.class);
+ assertChannel(component, Humidifier.DEVICE_CLASS_CHANNEL_ID, "", "", "Device Class", TextValue.class);
+ assertChannel(component, Humidifier.CURRENT_HUMIDITY_CHANNEL_ID, "bedroom_humidifier/humidity/current", "",
+ "Current Humidity", NumberValue.class);
+ assertChannel(component, Humidifier.TARGET_HUMIDITY_CHANNEL_ID, "bedroom_humidifier/humidity/state",
+ "bedroom_humidifier/humidity/set", "Target Humidity", NumberValue.class);
+
+ publishMessage("bedroom_humidifier/on/state", "true");
+ assertState(component, Humidifier.STATE_CHANNEL_ID, OnOffType.ON);
+ publishMessage("bedroom_humidifier/on/state", "false");
+ assertState(component, Humidifier.STATE_CHANNEL_ID, OnOffType.OFF);
+
+ publishMessage("bedroom_humidifier/action", "off");
+ assertState(component, Humidifier.ACTION_CHANNEL_ID, new StringType("off"));
+ publishMessage("bedroom_humidifier/action", "idle");
+ assertState(component, Humidifier.ACTION_CHANNEL_ID, new StringType("idle"));
+ publishMessage("bedroom_humidifier/action", "invalid");
+ assertState(component, Humidifier.ACTION_CHANNEL_ID, new StringType("idle"));
+
+ publishMessage("bedroom_humidifier/mode/state", "eco");
+ assertState(component, Humidifier.MODE_CHANNEL_ID, new StringType("eco"));
+ publishMessage("bedroom_humidifier/mode/state", "invalid");
+ assertState(component, Humidifier.MODE_CHANNEL_ID, new StringType("eco"));
+ publishMessage("bedroom_humidifier/mode/state", "None");
+ assertState(component, Humidifier.MODE_CHANNEL_ID, UnDefType.NULL);
+
+ publishMessage("bedroom_humidifier/humidity/current", "35");
+ assertState(component, Humidifier.CURRENT_HUMIDITY_CHANNEL_ID, new QuantityType<>(35, Units.PERCENT));
+ publishMessage("bedroom_humidifier/humidity/state", "40");
+ assertState(component, Humidifier.TARGET_HUMIDITY_CHANNEL_ID, new QuantityType<>(40, Units.PERCENT));
+
+ component.getChannel(Humidifier.STATE_CHANNEL_ID).getState().publishValue(OnOffType.OFF);
+ assertPublished("bedroom_humidifier/on/set", "false");
+ component.getChannel(Humidifier.STATE_CHANNEL_ID).getState().publishValue(OnOffType.ON);
+ assertPublished("bedroom_humidifier/on/set", "true");
+
+ component.getChannel(Humidifier.MODE_CHANNEL_ID).getState().publishValue(new StringType("eco"));
+ assertPublished("bedroom_humidifier/preset/preset_mode", "eco");
+
+ component.getChannel(Humidifier.TARGET_HUMIDITY_CHANNEL_ID).getState().publishValue(new DecimalType(45));
+ assertPublished("bedroom_humidifier/humidity/set", "45");
+ }
+
+ @Override
+ protected Set getConfigTopics() {
+ return Set.of(CONFIG_TOPIC);
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/TagTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/TagTests.java
new file mode 100644
index 0000000000000..b9d688eb6c7c1
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/TagTests.java
@@ -0,0 +1,64 @@
+/**
+ * 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.mqtt.homeassistant.internal.component;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mqtt.generic.values.TextValue;
+
+/**
+ * Tests for {@link Tag}
+ *
+ * @author Cody Cutrer - Initial contribution
+ */
+@NonNullByDefault
+public class TagTests extends AbstractComponentTests {
+ public static final String CONFIG_TOPIC = "tag/0AFFD2";
+
+ @SuppressWarnings("null")
+ @Test
+ public void test() throws InterruptedException {
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
+ {
+ "topic": "0AFFD2/tag_scanned",
+ "value_template": "{{ value_json.PN532.UID }}"
+ }
+ """);
+
+ assertThat(component.channels.size(), is(1));
+ assertThat(component.getName(), is("MQTT Tag Scanner"));
+
+ assertChannel(component, Tag.TAG_CHANNEL_ID, "0AFFD2/tag_scanned", "", "MQTT Tag Scanner", TextValue.class);
+
+ publishMessage("0AFFD2/tag_scanned", """
+ {
+ "Time": "2020-09-28T17:02:10",
+ "PN532": {
+ "UID": "E9F35959",
+ "DATA":"ILOVETASMOTA"
+ }
+ }
+ """);
+ assertTriggered(component, Tag.TAG_CHANNEL_ID, "E9F35959");
+ }
+
+ @Override
+ protected Set getConfigTopics() {
+ return Set.of(CONFIG_TOPIC);
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/WaterHeaterTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/WaterHeaterTests.java
new file mode 100644
index 0000000000000..9cb1c9481e39b
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/WaterHeaterTests.java
@@ -0,0 +1,159 @@
+/**
+ * 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.mqtt.homeassistant.internal.component;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.*;
+
+import java.util.Set;
+
+import javax.measure.quantity.Temperature;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.mqtt.generic.values.NumberValue;
+import org.openhab.binding.mqtt.generic.values.OnOffValue;
+import org.openhab.binding.mqtt.generic.values.TextValue;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.ImperialUnits;
+
+/**
+ * Tests for {@link WaterHeater}
+ *
+ * @author Cody Cutrer - Initial contribution
+ */
+@NonNullByDefault
+public class WaterHeaterTests extends AbstractComponentTests {
+ public static final String CONFIG_TOPIC = "water_heater/boiler";
+
+ @SuppressWarnings("null")
+ @Test
+ public void test() {
+ when(unitProvider.getUnit(Temperature.class)).thenReturn(ImperialUnits.FAHRENHEIT);
+
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
+ {
+ "platform": "water_heater",
+ "name": "Boiler",
+ "modes": [
+ "off",
+ "eco",
+ "performance"
+ ],
+ "mode_state_topic": "basement/boiler/mode",
+ "mode_command_topic": "basement/boiler/mode/set",
+ "mode_command_template": "{{ value if value==\\"off\\" else \\"on\\" }}",
+ "temperature_state_topic": "basement/boiler/temperature",
+ "temperature_command_topic": "basement/boiler/temperature/set",
+ "current_temperature_topic": "basement/boiler/current_temperature",
+ "precision": 1.0
+ }
+ """);
+
+ assertThat(component.channels.size(), is(3));
+ assertThat(component.getName(), is("Boiler"));
+
+ assertChannel(component, WaterHeater.MODE_CHANNEL_ID, "basement/boiler/mode", "basement/boiler/mode/set",
+ "Mode", TextValue.class);
+ assertChannel(component, WaterHeater.CURRENT_TEMPERATURE_CHANNEL_ID, "basement/boiler/current_temperature", "",
+ "Current Temperature", NumberValue.class);
+ assertChannel(component, WaterHeater.TARGET_TEMPERATURE_CHANNEL_ID, "basement/boiler/temperature",
+ "basement/boiler/temperature/set", "Target Temperature", NumberValue.class);
+
+ publishMessage("basement/boiler/mode", "eco");
+ assertState(component, WaterHeater.MODE_CHANNEL_ID, new StringType("eco"));
+ publishMessage("basement/boiler/mode", "invalid");
+ assertState(component, WaterHeater.MODE_CHANNEL_ID, new StringType("eco"));
+
+ publishMessage("basement/boiler/current_temperature", "120");
+ assertState(component, WaterHeater.CURRENT_TEMPERATURE_CHANNEL_ID,
+ new QuantityType<>(120, ImperialUnits.FAHRENHEIT));
+ publishMessage("basement/boiler/temperature", "125");
+ assertState(component, WaterHeater.TARGET_TEMPERATURE_CHANNEL_ID,
+ new QuantityType<>(125, ImperialUnits.FAHRENHEIT));
+
+ component.getChannel(WaterHeater.MODE_CHANNEL_ID).getState().publishValue(new StringType("eco"));
+ assertPublished("basement/boiler/mode/set", "on");
+ component.getChannel(WaterHeater.MODE_CHANNEL_ID).getState().publishValue(new StringType("off"));
+ assertPublished("basement/boiler/mode/set", "off");
+
+ component.getChannel(WaterHeater.TARGET_TEMPERATURE_CHANNEL_ID).getState().publishValue(new DecimalType(130));
+ assertPublished("basement/boiler/temperature/set", "130");
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testSynthesizedPowerState() {
+ when(unitProvider.getUnit(Temperature.class)).thenReturn(ImperialUnits.FAHRENHEIT);
+
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
+ {
+ "platform": "water_heater",
+ "name": "Boiler",
+ "modes": [
+ "off",
+ "eco",
+ "performance"
+ ],
+ "mode_state_topic": "basement/boiler/mode",
+ "mode_command_topic": "basement/boiler/mode/set",
+ "temperature_state_topic": "basement/boiler/temperature",
+ "temperature_command_topic": "basement/boiler/temperature/set",
+ "current_temperature_topic": "basement/boiler/current_temperature",
+ "precision": 1.0,
+ "power_command_topic": "basement/boiler/power/set"
+ }
+ """);
+
+ assertThat(component.channels.size(), is(4));
+ assertThat(component.getName(), is("Boiler"));
+
+ assertChannel(component, WaterHeater.STATE_CHANNEL_ID, "basement/boiler/mode", "basement/boiler/power/set",
+ "State", OnOffValue.class);
+ assertChannel(component, WaterHeater.MODE_CHANNEL_ID, "basement/boiler/mode", "basement/boiler/mode/set",
+ "Mode", TextValue.class);
+ assertChannel(component, WaterHeater.CURRENT_TEMPERATURE_CHANNEL_ID, "basement/boiler/current_temperature", "",
+ "Current Temperature", NumberValue.class);
+ assertChannel(component, WaterHeater.TARGET_TEMPERATURE_CHANNEL_ID, "basement/boiler/temperature",
+ "basement/boiler/temperature/set", "Target Temperature", NumberValue.class);
+
+ publishMessage("basement/boiler/mode", "eco");
+ assertState(component, WaterHeater.MODE_CHANNEL_ID, new StringType("eco"));
+ assertState(component, WaterHeater.STATE_CHANNEL_ID, OnOffType.ON);
+ publishMessage("basement/boiler/mode", "invalid");
+ assertState(component, WaterHeater.MODE_CHANNEL_ID, new StringType("eco"));
+ assertState(component, WaterHeater.STATE_CHANNEL_ID, OnOffType.ON);
+ publishMessage("basement/boiler/mode", "off");
+ assertState(component, WaterHeater.MODE_CHANNEL_ID, new StringType("off"));
+ assertState(component, WaterHeater.STATE_CHANNEL_ID, OnOffType.OFF);
+
+ component.getChannel(WaterHeater.MODE_CHANNEL_ID).getState().publishValue(new StringType("eco"));
+ assertPublished("basement/boiler/mode/set", "eco");
+ component.getChannel(WaterHeater.MODE_CHANNEL_ID).getState().publishValue(new StringType("off"));
+ assertPublished("basement/boiler/mode/set", "off");
+
+ component.getChannel(WaterHeater.STATE_CHANNEL_ID).getState().publishValue(OnOffType.ON);
+ assertPublished("basement/boiler/power/set", "ON");
+ component.getChannel(WaterHeater.STATE_CHANNEL_ID).getState().publishValue(OnOffType.OFF);
+ assertPublished("basement/boiler/power/set", "OFF");
+ }
+
+ @Override
+ protected Set getConfigTopics() {
+ return Set.of(CONFIG_TOPIC);
+ }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscoveryTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscoveryTests.java
index 15999087f4bef..d44e5a9763f53 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscoveryTests.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscoveryTests.java
@@ -15,13 +15,13 @@
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -56,15 +56,84 @@ public void beforeEach() {
}
@Test
- public void testComponentNameSummary() {
- assertThat(
- HomeAssistantDiscovery.getComponentNamesSummary(
- Stream.of("Sensor", "Switch", "Sensor", "Foobar", "Foobar", "Foobar")), //
- is("3x Foobar, 2x Sensor, Switch"));
+ public void testOneThingDiscovery() throws Exception {
+ var discoveryListener = new LatchDiscoveryListener();
+ var latch = discoveryListener.createWaitForThingsDiscoveredLatch(1);
+
+ // When discover one thing with two channels
+ discovery.addDiscoveryListener(discoveryListener);
+ discovery.receivedMessage(HA_UID, bridgeConnection,
+ "homeassistant/climate/0x847127fffe11dd6a_climate_zigbee2mqtt/config",
+ getResourceAsByteArray("component/configTS0601ClimateThermostat.json"));
+ discovery.receivedMessage(HA_UID, bridgeConnection,
+ "homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config",
+ getResourceAsByteArray("component/configTS0601AutoLock.json"));
+
+ // Then one thing found
+ assert latch.await(3, TimeUnit.SECONDS);
+ var discoveryResults = discoveryListener.getDiscoveryResults();
+ assertThat(discoveryResults.size(), is(1));
+ var result = discoveryResults.get(0);
+ assertThat(result.getBridgeUID(), is(HA_UID));
+ assertThat(result.getProperties().get(Thing.PROPERTY_MODEL_ID),
+ is("Radiator valve with thermostat (TS0601_thermostat)"));
+ assertThat(result.getProperties().get(Thing.PROPERTY_VENDOR), is("TuYa"));
+ assertThat(result.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is("Zigbee2MQTT 1.18.2"));
+ assertThat(result.getProperties().get(HandlerConfiguration.PROPERTY_BASETOPIC), is("homeassistant"));
+ assertThat(result.getLabel(), is("th1"));
+ assertThat((List) result.getProperties().get(HandlerConfiguration.PROPERTY_TOPICS), hasItems(
+ "climate/0x847127fffe11dd6a_climate_zigbee2mqtt", "switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt"));
}
@Test
- public void testOneThingDiscovery() throws Exception {
+ public void testComponentAddedToExistingThing() throws Exception {
+ var discoveryListener = new LatchDiscoveryListener();
+ var latch = discoveryListener.createWaitForThingsDiscoveredLatch(1);
+
+ // When discover one thing with two channels
+ discovery.addDiscoveryListener(discoveryListener);
+ discovery.receivedMessage(HA_UID, bridgeConnection,
+ "homeassistant/climate/0x847127fffe11dd6a_climate_zigbee2mqtt/config",
+ getResourceAsByteArray("component/configTS0601ClimateThermostat.json"));
+
+ // Then one thing found
+ assert latch.await(3, TimeUnit.SECONDS);
+ var discoveryResults = discoveryListener.getDiscoveryResults();
+ assertThat(discoveryResults.size(), is(1));
+ var result = discoveryResults.get(0);
+ assertThat(result.getBridgeUID(), is(HA_UID));
+ assertThat(result.getProperties().get(Thing.PROPERTY_MODEL_ID),
+ is("Radiator valve with thermostat (TS0601_thermostat)"));
+ assertThat(result.getProperties().get(Thing.PROPERTY_VENDOR), is("TuYa"));
+ assertThat(result.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is("Zigbee2MQTT 1.18.2"));
+ assertThat(result.getProperties().get(HandlerConfiguration.PROPERTY_BASETOPIC), is("homeassistant"));
+ assertThat(result.getLabel(), is("th1"));
+ assertThat((List) result.getProperties().get(HandlerConfiguration.PROPERTY_TOPICS),
+ hasItems("climate/0x847127fffe11dd6a_climate_zigbee2mqtt"));
+
+ // Now another component added to the same thing
+ latch = discoveryListener.createWaitForThingsDiscoveredLatch(1);
+ discovery.receivedMessage(HA_UID, bridgeConnection,
+ "homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config",
+ getResourceAsByteArray("component/configTS0601AutoLock.json"));
+
+ assert latch.await(3, TimeUnit.SECONDS);
+ discoveryResults = discoveryListener.getDiscoveryResults();
+ assertThat(discoveryResults.size(), is(1));
+ result = discoveryResults.get(0);
+ assertThat(result.getBridgeUID(), is(HA_UID));
+ assertThat(result.getProperties().get(Thing.PROPERTY_MODEL_ID),
+ is("Radiator valve with thermostat (TS0601_thermostat)"));
+ assertThat(result.getProperties().get(Thing.PROPERTY_VENDOR), is("TuYa"));
+ assertThat(result.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is("Zigbee2MQTT 1.18.2"));
+ assertThat(result.getProperties().get(HandlerConfiguration.PROPERTY_BASETOPIC), is("homeassistant"));
+ assertThat(result.getLabel(), is("th1"));
+ assertThat((List) result.getProperties().get(HandlerConfiguration.PROPERTY_TOPICS), hasItems(
+ "climate/0x847127fffe11dd6a_climate_zigbee2mqtt", "switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt"));
+ }
+
+ @Test
+ public void testComponentRemovedFromExistingThing() throws Exception {
var discoveryListener = new LatchDiscoveryListener();
var latch = discoveryListener.createWaitForThingsDiscoveredLatch(1);
@@ -88,9 +157,28 @@ public void testOneThingDiscovery() throws Exception {
assertThat(result.getProperties().get(Thing.PROPERTY_VENDOR), is("TuYa"));
assertThat(result.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is("Zigbee2MQTT 1.18.2"));
assertThat(result.getProperties().get(HandlerConfiguration.PROPERTY_BASETOPIC), is("homeassistant"));
- assertThat(result.getLabel(), is("th1 (Climate Control, Switch)"));
+ assertThat(result.getLabel(), is("th1"));
assertThat((List) result.getProperties().get(HandlerConfiguration.PROPERTY_TOPICS), hasItems(
"climate/0x847127fffe11dd6a_climate_zigbee2mqtt", "switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt"));
+
+ // Now remove the second component
+ latch = discoveryListener.createWaitForThingsDiscoveredLatch(1);
+ discovery.topicVanished(HA_UID, bridgeConnection,
+ "homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config");
+
+ assert latch.await(3, TimeUnit.SECONDS);
+ discoveryResults = discoveryListener.getDiscoveryResults();
+ assertThat(discoveryResults.size(), is(1));
+ result = discoveryResults.get(0);
+ assertThat(result.getBridgeUID(), is(HA_UID));
+ assertThat(result.getProperties().get(Thing.PROPERTY_MODEL_ID),
+ is("Radiator valve with thermostat (TS0601_thermostat)"));
+ assertThat(result.getProperties().get(Thing.PROPERTY_VENDOR), is("TuYa"));
+ assertThat(result.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), is("Zigbee2MQTT 1.18.2"));
+ assertThat(result.getProperties().get(HandlerConfiguration.PROPERTY_BASETOPIC), is("homeassistant"));
+ assertThat(result.getLabel(), is("th1"));
+ assertThat((List) result.getProperties().get(HandlerConfiguration.PROPERTY_TOPICS),
+ hasItems("climate/0x847127fffe11dd6a_climate_zigbee2mqtt"));
}
private static class TestHomeAssistantDiscovery extends HomeAssistantDiscovery {
@@ -122,8 +210,10 @@ public void thingRemoved(DiscoveryService source, ThingUID thingUID) {
return Collections.emptyList();
}
- public CopyOnWriteArrayList getDiscoveryResults() {
- return discoveryResults;
+ public List getDiscoveryResults() {
+ ArrayList localResults = new ArrayList<>(discoveryResults);
+ discoveryResults.clear();
+ return localResults;
}
public CountDownLatch createWaitForThingsDiscoveredLatch(int count) {
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java
index 3f4c066239a56..964bc63879685 100644
--- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java
+++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java
@@ -38,6 +38,7 @@
import org.openhab.binding.mqtt.homeassistant.internal.component.Sensor;
import org.openhab.binding.mqtt.homeassistant.internal.component.Switch;
import org.openhab.core.config.core.Configuration;
+import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
@@ -72,6 +73,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
private @NonNullByDefault({}) HomeAssistantThingHandler thingHandler;
private @NonNullByDefault({}) HomeAssistantThingHandler nonSpyThingHandler;
+ private @Mock @NonNullByDefault({}) UnitProvider unitProvider;
@BeforeEach
public void setup() {
@@ -87,7 +89,7 @@ public void setup() {
protected void setupThingHandler() {
thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
- channelTypeRegistry, new Jinjava(), SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
+ channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock);
nonSpyThingHandler = thingHandler;
@@ -409,7 +411,7 @@ public void testDuplicateChannelId() {
public void testDuplicateChannelIdNewStyleChannels() {
haThing.setProperty("newStyleChannels", "true");
thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
- channelTypeRegistry, new Jinjava(), SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
+ channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock);
nonSpyThingHandler = thingHandler;
@@ -466,7 +468,7 @@ public void testDuplicateChannelIdNewStyleChannels() {
public void testDuplicateChannelIdNewStyleChannelsComplex() {
haThing.setProperty("newStyleChannels", "true");
thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
- channelTypeRegistry, new Jinjava(), SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
+ channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock);
nonSpyThingHandler = thingHandler;
diff --git a/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/homie300/Property.java b/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/homie300/Property.java
index 3b676dccd55ec..2ca0342366fa1 100644
--- a/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/homie300/Property.java
+++ b/bundles/org.openhab.binding.mqtt.homie/src/main/java/org/openhab/binding/mqtt/homie/internal/homie300/Property.java
@@ -191,7 +191,7 @@ private void createChannelTypeFromAttributes() {
step = new BigDecimal(1);
}
if (attributes.unit.contains("%") && attributes.settable) {
- value = new PercentageValue(min, max, step, null, null);
+ value = new PercentageValue(min, max, step, null, null, null);
} else {
value = new NumberValue(min, max, step, unit);
}
diff --git a/bundles/org.openhab.binding.mqtt.ruuvigateway/README.md b/bundles/org.openhab.binding.mqtt.ruuvigateway/README.md
index f688c42d7987c..c1fa59d3f9a46 100644
--- a/bundles/org.openhab.binding.mqtt.ruuvigateway/README.md
+++ b/bundles/org.openhab.binding.mqtt.ruuvigateway/README.md
@@ -48,7 +48,7 @@ For users that prefer manual configuration, we list here the configurable parame
| dataFormat | Number | Data format version |
| measurementSequenceNumber | Number:Dimensionless | Measurement sequence number |
| movementCounter | Number:Dimensionless | Movement counter |
-| rssi | Number | Received signal (between the Gateway and the sensor) strength indicator |
+| rssi | Number:Power | Received signal (between the Gateway and the sensor) strength indicator |
| ts | DateTime | Timestamp when the message from Bluetooth-sensor was received by Gateway |
| gwts | DateTime | Timestamp when the message from Bluetooth-sensor was relayed by Gateway |
| gwmac | String | MAC-address of Ruuvi Gateway |
diff --git a/bundles/org.openhab.binding.mqtt.ruuvigateway/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mqtt.ruuvigateway/src/main/resources/OH-INF/thing/thing-types.xml
index 0bd2bb9c3a917..d9b615ddb3733 100644
--- a/bundles/org.openhab.binding.mqtt.ruuvigateway/src/main/resources/OH-INF/thing/thing-types.xml
+++ b/bundles/org.openhab.binding.mqtt.ruuvigateway/src/main/resources/OH-INF/thing/thing-types.xml
@@ -30,6 +30,10 @@
+
+ 1
+
+
MQTT Topic
@@ -40,11 +44,11 @@
DateTime
diff --git a/bundles/org.openhab.binding.mqtt.ruuvigateway/src/main/resources/OH-INF/update/update.xml b/bundles/org.openhab.binding.mqtt.ruuvigateway/src/main/resources/OH-INF/update/update.xml
new file mode 100644
index 0000000000000..ef5acb1800f6c
--- /dev/null
+++ b/bundles/org.openhab.binding.mqtt.ruuvigateway/src/main/resources/OH-INF/update/update.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceBindingConstants.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceBindingConstants.java
index 772319c0b6541..f79558fce75e0 100644
--- a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceBindingConstants.java
+++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/MyNiceBindingConstants.java
@@ -22,7 +22,7 @@
*/
@NonNullByDefault
public class MyNiceBindingConstants {
- private static final String BINDING_ID = "mynice";
+ public static final String BINDING_ID = "mynice";
// List of all Channel ids
public static final String CHANNEL_STATUS = "status";
diff --git a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiConnector.java b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiConnector.java
index 1e460369e1a4d..ff3e76d7cb697 100644
--- a/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiConnector.java
+++ b/bundles/org.openhab.binding.mynice/src/main/java/org/openhab/binding/mynice/internal/handler/It4WifiConnector.java
@@ -20,6 +20,7 @@
import javax.net.ssl.SSLSocket;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mynice.internal.MyNiceBindingConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,7 +40,7 @@ public class It4WifiConnector extends Thread {
private final OutputStreamWriter out;
public It4WifiConnector(It4WifiHandler handler, SSLSocket sslSocket) throws IOException {
- super(It4WifiConnector.class.getName());
+ super(String.format("OH-binding-%s-%s", MyNiceBindingConstants.BINDING_ID, "WifiConnector"));
this.handler = handler;
this.in = new InputStreamReader(sslSocket.getInputStream());
this.out = new OutputStreamWriter(sslSocket.getOutputStream());
diff --git a/bundles/org.openhab.binding.myuplink/NOTICE b/bundles/org.openhab.binding.myuplink/NOTICE
new file mode 100644
index 0000000000000..edfd204e5d248
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/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.myuplink/README.md b/bundles/org.openhab.binding.myuplink/README.md
new file mode 100644
index 0000000000000..98f6fa7c04b8c
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/README.md
@@ -0,0 +1,78 @@
+# myUplink Binding
+
+The myUplink binding is used to get "live data" from from Nibe heat pumps without plugging any custom devices into your heat pump.
+This avoids the risk of losing your warranty.
+Instead data is retrieved from myUplink.
+The myUplink API is the successor of the Nibe Uplink API.
+This binding should in general be compatible with all heat pump models that support myUplink.
+Read or write access is supported by all channels as exposed by the API.
+Write access might only be available with a paid subscription for myUplink.
+You will need to create credentials at in order to use this binding.
+
+## Supported Things
+
+This binding provides two thing types:
+
+| Thing/Bridge | Thing Type | Description |
+|---------------------|---------------------|-------------------------------------------------------------------|
+| bridge | account | cloud connection to a myUplink user account |
+| thing | generic-device | the physical heatpump which is connected to myUplink |
+## Discovery
+
+When the `account` bridge is setup, the binding will discover all heatpumps within that account and also detect the specific channels supported by the model.
+
+## Bridge Configuration
+
+The following configuration parameters are available for the bridge:
+
+| Configuration Parameter | Required | Description |
+|-------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| clientId | yes | The clientId to login at myUplink cloud service. This is some kind of UUID. Visit to generate login credentials. |
+| clientSecret | yes | The secret which belongs to the clientId. |
+| dataPollingInterval | no | Interval (seconds) in which live data values are retrieved from the Easee Cloud API. (default = 60) |
+
+## Thing Configuration
+
+It is recommended to use auto discovery which does not require further configuration.
+If manual configuration is preferred you need to specify configuration as below.
+
+| Configuration Parameter | Required | Description |
+|-------------------------|----------|------------------------------------------------------------------------------------------------------------------------|
+| deviceId | yes | The id of the heatpump that will be represented by this thing. Can be retrieved via API call or autodiscovery. |
+| systemId | no | The systemId of the heatpump. Only needed for "SmartHomeMode". Can be retrieved via API call or autodiscovery. |
+
+## Channels
+
+The binding only supports channels which are explicitely exposed by the myUplink API.
+
+Depending on your model and additional hardware the channels might be different.
+Thus no list is provided here.
+
+## Full Example
+
+The configuration below is an example which could easily be adopted to your actual model.
+Thing configuration (account and generic-device) is the same for all models.
+Item configuration depends on your specific model and thus channels will have different IDs and/or channels might not exist for all models.
+
+### `demo.things` Example
+
+```java
+Bridge myuplink:account:myAccount "myUplink" [
+ clientId="c7c2f9a4-b960-448f-b00d-b8f30aff3324",
+ clientSecret="471147114711ABCDEF133713371337AB",
+ dataPollingInterval=55
+ ] {
+ Thing generic-device vvm320 "VVM320" [ deviceId="id taken from automatic discovery", systemId="id taken from automatic discovery" ]
+ }
+```
+
+### `demo.items` Example
+
+```java
+Number NIBE_ADD_STATUS "Status ZH [%s]" { channel="myuplink:generic-device:myAccount:vvm320:49993" }
+Number NIBE_COMP_STATUS "Status Compr. [%s]" { channel="myuplink:generic-device:myAccount:vvm320:44064" }
+Number:Temperature NIBE_SUPPLY "Supply line" { unit="°C", channel="myuplink:generic-device:myAccount:vvm320:40008" }
+Number:Temperature NIBE_RETURN "Return line" { unit="°C", channel="myuplink:generic-device:myAccount:vvm320:40012" }
+Number:Energy NIBE_HM_HEAT "HM heating" { unit="kWh", channel="myuplink:generic-device:myAccount:vvm320:44308" }
+Number:Energy NIBE_HM_HW "HM hot water" { unit="kWh", channel="myuplink:generic-device:myAccount:vvm320:44306" }
+```
diff --git a/bundles/org.openhab.binding.myuplink/pom.xml b/bundles/org.openhab.binding.myuplink/pom.xml
new file mode 100644
index 0000000000000..7ff5b539382cd
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 4.3.0-SNAPSHOT
+
+
+ org.openhab.binding.myuplink
+
+ openHAB Add-ons :: Bundles :: myUplink Binding
+
+
diff --git a/bundles/org.openhab.binding.myuplink/src/main/feature/feature.xml b/bundles/org.openhab.binding.myuplink/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..753c27ef2ce63
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/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.myuplink/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/AtomicReferenceTrait.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/AtomicReferenceTrait.java
new file mode 100644
index 0000000000000..1a7a9a10798d9
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/AtomicReferenceTrait.java
@@ -0,0 +1,59 @@
+/**
+ * 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.myuplink.internal;
+
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * trait class which contains useful helper methods. Thus, the interface can be implemented and methods are available
+ * within the class.
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public interface AtomicReferenceTrait {
+
+ /**
+ * this should usually not called directly. use updateJobReference or cancelJobReference instead
+ *
+ * @param job job to cancel.
+ */
+ default void cancelJob(@Nullable Future> job) {
+ if (job != null) {
+ job.cancel(true);
+ }
+ }
+
+ /**
+ * updates a job reference with a new job. the old job will be cancelled if there is one.
+ *
+ * @param jobReference reference to be updated
+ * @param newJob job to be assigned
+ */
+ default void updateJobReference(AtomicReference<@Nullable Future>> jobReference, Future> newJob) {
+ cancelJob(jobReference.getAndSet(newJob));
+ }
+
+ /**
+ * updates a job reference to null and cancels any existing job which might be assigned to the reference.
+ *
+ * @param jobReference to be updated to null.
+ */
+ default void cancelJobReference(AtomicReference<@Nullable Future>> jobReference) {
+ cancelJob(jobReference.getAndSet(null));
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/MyUplinkBindingConstants.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/MyUplinkBindingConstants.java
new file mode 100644
index 0000000000000..6f4ac2b24bcf2
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/MyUplinkBindingConstants.java
@@ -0,0 +1,181 @@
+/**
+ * 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.myuplink.internal;
+
+import java.time.Instant;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link MyUplinkBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Alexander Friese - Initial contribution
+ */
+@NonNullByDefault
+public class MyUplinkBindingConstants {
+
+ public static final String BINDING_ID = "myuplink";
+
+ // List of main device types
+ public static final String DEVICE_ACCOUNT = "account";
+ public static final String DEVICE_GENERIC_DEVICE = "generic-device";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, DEVICE_ACCOUNT);
+ public static final ThingTypeUID THING_TYPE_GENERIC_DEVICE = new ThingTypeUID(BINDING_ID, DEVICE_GENERIC_DEVICE);
+
+ public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT,
+ THING_TYPE_GENERIC_DEVICE);
+
+ // Channel types
+ public static final String CHANNEL_TYPE_UNIT_NONE = "NO_UNIT";
+ public static final String CHANNEL_TYPE_PREFIX_RW = "rw";
+ public static final String CHANNEL_TYPE_ENUM_PRFIX = "type-enum-";
+ public static final String CHANNEL_TYPE_NUMERIC_PRFIX = "type-numeric-";
+ public static final String CHANNEL_TYPE_DEFAULT_DATATYPE = "Number";
+
+ public static final String CHANNEL_TYPE_ENERGY = "type-energy";
+ public static final String CHANNEL_TYPE_ENERGY_UNIT = "kWh";
+ public static final String CHANNEL_TYPE_PRESSURE = "type-pressure";
+ public static final String CHANNEL_TYPE_PRESSURE_UNIT = "bar";
+ public static final String CHANNEL_TYPE_PERCENT = "type-percent";
+ public static final String CHANNEL_TYPE_PERCENT_UNIT = "%";
+ public static final String CHANNEL_TYPE_TEMPERATURE = "type-temperature";
+ public static final String CHANNEL_TYPE_TEMPERATURE_UNIT = "°C";
+ public static final String CHANNEL_TYPE_FREQUENCY = "type-frequency";
+ public static final String CHANNEL_TYPE_FREQUENCY_UNIT = "Hz";
+ public static final String CHANNEL_TYPE_FLOW = "type-flow";
+ public static final String CHANNEL_TYPE_FLOW_UNIT = "l/m";
+ public static final String CHANNEL_TYPE_ELECTRIC_CURRENT = "type-electric-current";
+ public static final String CHANNEL_TYPE_ELECTRIC_CURRENT_UNIT = "A";
+ public static final String CHANNEL_TYPE_TIME = "type-time";
+ public static final String CHANNEL_TYPE_TIME_UNIT = "h";
+ public static final String CHANNEL_TYPE_INTEGER = "type-number-integer";
+ public static final String CHANNEL_TYPE_DOUBLE = "type-number-double";
+ public static final String CHANNEL_TYPE_ON_OFF = "type-on-off";
+ public static final String CHANNEL_TYPE_RW_SWITCH = "rwtype-switch";
+ public static final String CHANNEL_TYPE_RW_COMMAND = "rwtype-command";
+ public static final String CHANNEL_TYPE_RW_MODE = "rwtype-mode";
+
+ public static final String CHANNEL_ID_COMMAND = "command";
+ public static final String CHANNEL_ID_SMART_HOME_MODE = "smart-home-mode";
+
+ // JSON Keys
+ public static final String JSON_KEY_ROOT_DATA = "data";
+ public static final String JSON_KEY_CHANNEL_STR_VAL = "strVal";
+ public static final String JSON_KEY_CHANNEL_VALUE = "value";
+ public static final String JSON_KEY_CHANNEL_WRITABLE = "writable";
+ public static final String JSON_KEY_CHANNEL_ENUM_VALUES = "enumValues";
+ public static final String JSON_KEY_CHANNEL_ID = "parameterId";
+ public static final String JSON_KEY_CHANNEL_LABEL = "parameterName";
+ public static final String JSON_KEY_CHANNEL_UNIT = "parameterUnit";
+ public static final String JSON_KEY_CHANNEL_SCALE = "scaleValue";
+ public static final String JSON_KEY_CHANNEL_MIN = "minValue";
+ public static final String JSON_KEY_CHANNEL_MAX = "maxValue";
+ public static final String JSON_KEY_CHANNEL_STEP = "stepValue";
+ public static final String JSON_KEY_SYSTEMS = "systems";
+ public static final String JSON_KEY_SYSTEM_ID = "systemId";
+ public static final String JSON_KEY_DEVICES = "devices";
+ public static final String JSON_KEY_GENERIC_ID = "id";
+ public static final String JSON_KEY_PRODUCT = "product";
+ public static final String JSON_KEY_SERIAL = "serialNumber";
+ public static final String JSON_KEY_NAME = "name";
+ public static final String JSON_KEY_CURRENT_FW_VERSION = "currentFwVersion";
+ public static final String JSON_KEY_CONNECTION_STATE = "connectionState";
+ public static final String JSON_KEY_ERROR = "error";
+ public static final String JSON_KEY_SMART_HOME_MODE = "smartHomeMode";
+
+ public static final String JSON_KEY_AUTH_ACCESS_TOKEN = "access_token";
+ public static final String JSON_KEY_AUTH_EXPIRES_IN = "expires_in";
+
+ public static final String JSON_ENUM_KEY_TEXT = "text";
+ public static final String JSON_ENUM_ORD_0 = "0";
+ public static final String JSON_ENUM_ORD_1 = "1";
+ public static final String JSON_ENUM_ORD_4 = "4";
+ public static final String JSON_ENUM_ORD_6 = "6";
+ public static final String JSON_ENUM_ORD_10 = "10";
+ public static final String JSON_ENUM_ORD_20 = "20";
+ public static final String JSON_ENUM_ORD_30 = "30";
+ public static final String JSON_ENUM_ORD_40 = "40";
+ public static final String JSON_ENUM_ORD_60 = "60";
+ public static final String JSON_ENUM_ORD_100 = "100";
+ public static final String JSON_ENUM_VAL_OFF = "off";
+ public static final String JSON_ENUM_VAL_ON = "on";
+ public static final String JSON_ENUM_VAL_HOT_WATER = "hot water";
+ public static final String JSON_ENUM_VAL_HEATING = "heating";
+ public static final String JSON_ENUM_VAL_POOL = "pool";
+ public static final String JSON_ENUM_VAL_STARTS = "starts";
+ public static final String JSON_ENUM_VAL_RUNS = "runs";
+ public static final String JSON_ENUM_VAL_ALARM = "alarm";
+ public static final String JSON_ENUM_VAL_BLOCKED = "blocked";
+ public static final String JSON_ENUM_VAL_ACTIVE = "active";
+
+ public static final String JSON_VAL_CONNECTION_CONNECTED = "Connected";
+ public static final String JSON_VAL_DECIMAL_SEPARATOR = ".";
+
+ // web request constants
+ public static final long WEB_REQUEST_INITIAL_DELAY = 10;
+ public static final long WEB_REQUEST_INTERVAL = 5;
+ public static final int WEB_REQUEST_QUEUE_MAX_SIZE = 20;
+ public static final int WEB_REQUEST_TOKEN_EXPIRY_BUFFER_MINUTES = 5;
+ public static final int WEB_REQUEST_TOKEN_MAX_AGE_MINUTES = 45;
+ public static final String WEB_REQUEST_PARAM_PAGE_KEY = "page";
+ public static final String WEB_REQUEST_PARAM_PAGE_SIZE_KEY = "itemsPerPage";
+ public static final String WEB_REQUEST_PATCH_CONTENT_TYPE = "application/json-patch+json";
+ public static final int WEB_REQUEST_PARAM_PAGE_SIZE_VALUE = 100;
+ public static final String WEB_REQUEST_BEARER_TOKEN_PREFIX = "Bearer ";
+ public static final String LOGIN_BASIC_AUTH_PREFIX = "Basic ";
+ public static final String LOGIN_FIELD_SCOPE_KEY = "scope";
+ public static final String LOGIN_FIELD_SCOPE_VALUE = "READSYSTEM WRITESYSTEM";
+ public static final String LOGIN_FIELD_GRANT_TYPE_KEY = "grant_type";
+ public static final String LOGIN_FIELD_GRANT_TYPE_VALUE = "client_credentials";
+
+ // URLs
+ private static final String API_BASE_URL = "https://api.myuplink.com";
+ public static final String LOGIN_URL = API_BASE_URL + "/oauth/token";
+ public static final String GET_SYSTEMS_URL = API_BASE_URL + "/v2/systems/me";
+ public static final String GET_SMART_HOME_MODE_URL = API_BASE_URL + "/v2/systems/{systemId}/smart-home-mode";
+ public static final String SET_SMART_HOME_MODE_URL = GET_SMART_HOME_MODE_URL;
+ public static final String GET_DEVICE_POINTS = API_BASE_URL + "/v2/devices/{deviceId}/points";
+ public static final String SET_DEVICE_POINTS = GET_DEVICE_POINTS;
+
+ // Status Keys
+ public static final String STATUS_TOKEN_VALIDATED = "@text/status.token.validated";
+ public static final String STATUS_WAITING_FOR_BRIDGE = "@text/status.waiting.for.bridge";
+ public static final String STATUS_WAITING_FOR_LOGIN = "@text/status.waiting.for.login";
+ public static final String STATUS_NO_VALID_DATA = "@text/status.no.valid.data";
+ public static final String STATUS_NO_CONNECTION = "@text/status.no.connection";
+ public static final String STATUS_DEVICE_NOT_FOUND = "@text/status.device.not.found";
+ public static final String STATUS_CONFIG_ERROR_NO_CLIENT_ID = "@text/status.config.error.no.client.id";
+ public static final String STATUS_CONFIG_ERROR_NO_CLIENT_SECRET = "@text/status.config.error.no.client.secret";
+
+ // other
+ public static final long POLLING_INITIAL_DELAY = 5;
+
+ public static final String GENERIC_NO_VAL = "---";
+ public static final String EMPTY = "";
+
+ public static final String THING_CONFIG_ID = "deviceId";
+ public static final String THING_CONFIG_SYSTEM_ID = "systemId";
+ public static final String THING_CONFIG_SERIAL = "serial";
+ public static final String THING_CONFIG_CURRENT_FW_VERSION = "currentFwVersion";
+
+ public static final Instant OUTDATED_DATE = Instant.EPOCH;
+
+ public static final String PARAMETER_NAME_WRITE_COMMAND = "writeCommand";
+ public static final String PARAMETER_NAME_VALIDATION_REGEXP = "validationExpression";
+ public static final String DEFAULT_VALIDATION_EXPRESSION = "[0-9]+";
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/MyUplinkHandlerFactory.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/MyUplinkHandlerFactory.java
new file mode 100644
index 0000000000000..c9bbdd0fe8450
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/MyUplinkHandlerFactory.java
@@ -0,0 +1,81 @@
+/**
+ * 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.myuplink.internal;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.myuplink.internal.handler.MyUplinkAccountHandler;
+import org.openhab.binding.myuplink.internal.handler.MyUplinkGenericDeviceHandler;
+import org.openhab.binding.myuplink.internal.provider.ChannelFactory;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link myUplinkHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Alexander Friese - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.myuplink", service = ThingHandlerFactory.class)
+public class MyUplinkHandlerFactory extends BaseThingHandlerFactory {
+
+ private final Logger logger = LoggerFactory.getLogger(MyUplinkHandlerFactory.class);
+
+ /**
+ * the shared http client
+ */
+ private final HttpClient httpClient;
+
+ private final ChannelFactory channelFactory;
+
+ @Activate
+ public MyUplinkHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
+ final @Reference ChannelFactory channelFactory) {
+ this.httpClient = httpClientFactory.getCommonHttpClient();
+ this.channelFactory = channelFactory;
+ }
+
+ @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_ACCOUNT.equals(thingTypeUID)) {
+ return new MyUplinkAccountHandler((Bridge) thing, httpClient);
+ } else if (THING_TYPE_GENERIC_DEVICE.equals(thingTypeUID)) {
+ return new MyUplinkGenericDeviceHandler(thing, channelFactory);
+ } else {
+ logger.warn("Unsupported Thing-Type: {}", thingTypeUID.getAsString());
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/Utils.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/Utils.java
new file mode 100644
index 0000000000000..cbcac58dbf8cd
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/Utils.java
@@ -0,0 +1,237 @@
+/**
+ * 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.myuplink.internal;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import java.math.BigDecimal;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.myuplink.internal.model.ConfigurationException;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.type.ChannelTypeUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+
+/**
+ * some helper methods.
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public final class Utils {
+ private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class);
+
+ /**
+ * only static methods no instance needed
+ */
+ private Utils() {
+ }
+
+ /**
+ * parses a date string in api source format to ZonedDateTime which is used by openHAB.
+ *
+ * @param date
+ * @return
+ */
+ public static ZonedDateTime parseDate(String date) throws DateTimeParseException {
+ DateTimeFormatter formatter;
+ if (date.length() == 24) {
+ formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX");
+ } else {
+ formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssX");
+ }
+ LOGGER.trace("parsing: {}", date);
+ return ZonedDateTime.parse(date, formatter);
+ }
+
+ /**
+ * get element as JsonObject.
+ *
+ * @param jsonObject
+ * @param key
+ * @return
+ */
+ public static @Nullable JsonObject getAsJsonObject(@Nullable JsonObject jsonObject, String key) {
+ JsonElement element = jsonObject == null ? null : jsonObject.get(key);
+ return (element instanceof JsonObject) ? element.getAsJsonObject() : null;
+ }
+
+ /**
+ * get element as JsonArray.
+ *
+ * @param jsonObject
+ * @param key
+ * @return
+ */
+ public static JsonArray getAsJsonArray(@Nullable JsonObject jsonObject, String key) {
+ JsonElement element = jsonObject == null ? null : jsonObject.get(key);
+ return (element instanceof JsonArray) ? element.getAsJsonArray() : new JsonArray();
+ }
+
+ /**
+ * get element as String.
+ *
+ * @param jsonObject
+ * @param key
+ * @return
+ */
+ public static @Nullable String getAsString(@Nullable JsonObject jsonObject, String key) {
+ JsonElement element = jsonObject == null ? null : jsonObject.get(key);
+ String text = null;
+ if (element != null) {
+ if (element instanceof JsonPrimitive) {
+ text = element.getAsString();
+ } else if (element instanceof JsonObject || element instanceof JsonArray) {
+ text = element.toString();
+ }
+ }
+ return text;
+ }
+
+ /**
+ * null safe version of getAsString with default value.
+ *
+ * @param jsonObject
+ * @param key
+ * @param defaultVal
+ * @return
+ */
+ public static String getAsString(@Nullable JsonObject jsonObject, String key, String defaultVal) {
+ String text = getAsString(jsonObject, key);
+ return text == null ? defaultVal : text;
+ }
+
+ /**
+ * get element as int.
+ *
+ * @param jsonObject
+ * @param key
+ * @return
+ */
+ public static int getAsInt(@Nullable JsonObject jsonObject, String key) {
+ JsonElement element = jsonObject == null ? null : jsonObject.get(key);
+ return (element instanceof JsonPrimitive) ? element.getAsInt() : 0;
+ }
+
+ /**
+ * get element as BigDecimal.
+ *
+ * @param jsonObject
+ * @param key
+ * @return
+ */
+ public static @Nullable BigDecimal getAsBigDecimal(@Nullable JsonObject jsonObject, String key) {
+ JsonElement element = jsonObject == null ? null : jsonObject.get(key);
+ return (element == null || element instanceof JsonNull) ? null : element.getAsBigDecimal();
+ }
+
+ /**
+ * get element as boolean.
+ *
+ * @param jsonObject
+ * @param key
+ * @return
+ */
+ public static @Nullable Boolean getAsBool(@Nullable JsonObject jsonObject, String key) {
+ JsonElement json = jsonObject == null ? null : jsonObject.get(key);
+ return (json == null || json instanceof JsonNull) ? null : json.getAsBoolean();
+ }
+
+ /**
+ * null safe version of getAsBool with default value.
+ *
+ * @param jsonObject
+ * @param key
+ * @return
+ */
+ public static boolean getAsBool(@Nullable JsonObject jsonObject, String key, Boolean defaultValue) {
+ Boolean result = getAsBool(jsonObject, key);
+ return result == null ? defaultValue : result;
+ }
+
+ /**
+ * retrieves typeID of a channel.
+ *
+ * @param channel
+ * @return typeID or empty string if typeUID is null.
+ */
+ public static String getChannelTypeId(Channel channel) {
+ ChannelTypeUID typeUID = channel.getChannelTypeUID();
+ if (typeUID == null) {
+ return "";
+ }
+ return typeUID.getId();
+ }
+
+ /**
+ * retrieves the validation expression which is assigned to this channel, fallback to a public static, if no
+ * validation
+ * is
+ * defined.
+ *
+ * @param channel
+ * @return the validation expression
+ */
+ public static String getValidationExpression(Channel channel) {
+ String expr = getPropertyOrParameter(channel, PARAMETER_NAME_VALIDATION_REGEXP);
+ if (expr == null) {
+ throw new ConfigurationException(
+ "channel (" + channel.getUID().getId() + ") does not have a validation expression configured");
+ }
+ return expr;
+ }
+
+ /**
+ * internal utiliy method which returns a property (if found) or a config parameter (if found) otherwise null
+ *
+ * @param channel
+ * @param name
+ * @return
+ */
+ public static @Nullable String getPropertyOrParameter(Channel channel, String name) {
+ String value = channel.getProperties().get(name);
+ // also eclipse says this cannot be null, it definitely can!
+ if (value == null || value.isEmpty()) {
+ Object obj = channel.getConfiguration().get(name);
+ value = obj == null ? null : obj.toString();
+ }
+ return value;
+ }
+
+ /**
+ * converts units received from myUplink API into UoM compliant strings.
+ *
+ * @param originalUnit
+ * @return UoM compliant unit
+ */
+ public static String fixUnit(String originalUnit) {
+ return switch (originalUnit) {
+ case "l/m" -> "l/min";
+ case "hrs" -> "h";
+ case "m3/h" -> "m³/h";
+ case "days" -> "d";
+ default -> originalUnit;
+ };
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/AbstractCommand.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/AbstractCommand.java
new file mode 100644
index 0000000000000..30bf10d2dc87b
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/AbstractCommand.java
@@ -0,0 +1,339 @@
+/**
+ * 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.myuplink.internal.command;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+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.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.util.BufferingResponseListener;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpStatus.Code;
+import org.openhab.binding.myuplink.internal.connector.CommunicationStatus;
+import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
+import org.openhab.binding.myuplink.internal.model.GenericResponseTransformer;
+import org.openhab.binding.myuplink.internal.model.ResponseTransformer;
+import org.openhab.binding.myuplink.internal.model.ValidationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.ToNumberPolicy;
+
+/**
+ * base class for all commands. common logic should be implemented here
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public abstract class AbstractCommand extends BufferingResponseListener implements MyUplinkCommand {
+
+ public enum RetryOnFailure {
+ YES,
+ NO
+ }
+
+ public enum ProcessFailureResponse {
+ YES,
+ NO
+ }
+
+ /**
+ * logger
+ */
+ private final Logger logger = LoggerFactory.getLogger(AbstractCommand.class);
+
+ /**
+ * the configuration
+ */
+ protected final MyUplinkThingHandler handler;
+
+ /**
+ * JSON deserializer
+ */
+ protected final Gson gson;
+
+ /**
+ * status code of fulfilled request
+ */
+ private final CommunicationStatus communicationStatus;
+
+ /**
+ * generic transformer which just transfers all values in a plain map.
+ */
+ protected final ResponseTransformer transformer;
+
+ /**
+ * retry counter.
+ */
+ private int retries = 0;
+
+ /**
+ * retry active
+ */
+ private final RetryOnFailure retryOnFailure;
+
+ /**
+ * process error response, e.g. set handler offline on error
+ */
+ private final ProcessFailureResponse processFailureResponse;
+
+ /**
+ * allows further processing of the json result data, if set.
+ */
+ private final JsonResultProcessor resultProcessor;
+
+ /**
+ * the constructor
+ */
+ public AbstractCommand(MyUplinkThingHandler handler, RetryOnFailure retryOnFailure,
+ ProcessFailureResponse processFailureResponse, JsonResultProcessor resultProcessor) {
+ this(handler, new GenericResponseTransformer(handler), retryOnFailure, processFailureResponse, resultProcessor);
+ }
+
+ public AbstractCommand(MyUplinkThingHandler handler, ResponseTransformer responseTransformer,
+ RetryOnFailure retryOnFailure, ProcessFailureResponse processFailureResponse,
+ JsonResultProcessor resultProcessor) {
+ this.gson = new GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create();
+ this.communicationStatus = new CommunicationStatus();
+ this.transformer = responseTransformer;
+ this.handler = handler;
+ this.processFailureResponse = processFailureResponse;
+ this.retryOnFailure = retryOnFailure;
+ this.resultProcessor = resultProcessor;
+ }
+
+ /**
+ * Log request success
+ */
+ @Override
+ public final void onSuccess(@Nullable Response response) {
+ if (response != null) {
+ super.onSuccess(response);
+ communicationStatus.setHttpCode(HttpStatus.getCode(response.getStatus()));
+ logger.debug("[{}] HTTP response {}", getClass().getSimpleName(), response.getStatus());
+ }
+ }
+
+ /**
+ * Log request failure
+ */
+ @Override
+ public final void onFailure(@Nullable Response response, @Nullable Throwable failure) {
+ if (failure != null && response != null) {
+ super.onFailure(response, failure);
+ }
+ if (failure != null) {
+ logger.info("[{}] Request failed: {}", getClass().getSimpleName(), failure.toString());
+ communicationStatus.setError((Exception) failure);
+ if (failure instanceof SocketTimeoutException || failure instanceof TimeoutException) {
+ communicationStatus.setHttpCode(Code.REQUEST_TIMEOUT);
+ } else if (failure instanceof UnknownHostException) {
+ communicationStatus.setHttpCode(Code.BAD_GATEWAY);
+ } else {
+ communicationStatus.setHttpCode(Code.INTERNAL_SERVER_ERROR);
+ }
+ } else {
+ logger.warn("[{}] Request failed", getClass().getSimpleName());
+ }
+ if (response != null && response.getStatus() > 0) {
+ communicationStatus.setHttpCode(HttpStatus.getCode(response.getStatus()));
+ }
+ }
+
+ /**
+ * just for logging of content
+ */
+ @Override
+ public void onContent(@Nullable Response response, @Nullable ByteBuffer content) {
+ if (response != null && content != null) {
+ super.onContent(response, content);
+ }
+ var contentAsString = getContentAsString();
+ var contentLength = contentAsString == null ? 0 : contentAsString.length();
+ logger.debug("[{}] received content, length: {}, encoding: {}", getClass().getSimpleName(), contentLength,
+ this.getEncoding());
+ }
+
+ /**
+ * default handling of successful requests.
+ */
+ @Override
+ public void onComplete(@Nullable Result result) {
+ String json = getContentAsString(StandardCharsets.UTF_8);
+
+ logger.debug("[{}] JSON String: {}", getClass().getSimpleName(), json);
+ switch (getCommunicationStatus().getHttpCode()) {
+ case OK:
+ case ACCEPTED:
+ onCompleteCodeOk(json);
+ break;
+ default:
+ onCompleteCodeDefault(json);
+ }
+ }
+
+ /**
+ * handling of result in case of HTTP response OK.
+ *
+ * @param json
+ */
+ protected void onCompleteCodeOk(@Nullable String json) {
+ JsonObject jsonObject = transform(json);
+ if (jsonObject != null) {
+ logger.debug("[{}] success", getClass().getSimpleName());
+ handler.updateChannelStatus(transformer.transform(jsonObject, getChannelGroup()));
+ processResult(jsonObject);
+ }
+ }
+
+ /**
+ * handling of result in default case, this means error handling of http codes where no specific handling applies.
+ *
+ * @param json
+ */
+ protected void onCompleteCodeDefault(@Nullable String json) {
+ JsonObject jsonObject = transform(json);
+ if (jsonObject == null) {
+ jsonObject = new JsonObject();
+ }
+ if (processFailureResponse == ProcessFailureResponse.YES) {
+ processResult(jsonObject);
+ } else {
+ logger.warn("command failed, url: {} - code: {} - result: {}", getURL(),
+ getCommunicationStatus().getHttpCode(), jsonObject.toString());
+ }
+
+ if (retryOnFailure == RetryOnFailure.YES && retries++ < MAX_RETRIES) {
+ handler.enqueueCommand(this);
+ }
+ }
+
+ /**
+ * error safe json transformer.
+ *
+ * @param json
+ * @return
+ */
+ protected @Nullable JsonObject transform(@Nullable String json) {
+ if (json != null) {
+ try {
+ JsonElement jsonElement = gson.fromJson(json, JsonElement.class);
+ JsonObject jsonObject;
+ if (jsonElement instanceof JsonObject) {
+ jsonObject = jsonElement.getAsJsonObject();
+ } else {
+ jsonObject = new JsonObject();
+ jsonObject.add(JSON_KEY_ROOT_DATA, jsonElement);
+ }
+ return jsonObject;
+ } catch (Exception ex) {
+ logger.debug("[{}] JSON could not be parsed: {}\nError: {}", getClass().getSimpleName(), json,
+ ex.getMessage());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * preparation of the request. will call a hook (prepareRequest) that has to be implemented in the subclass to add
+ * content to the request.
+ *
+ * @throws ValidationException
+ */
+ @Override
+ public void performAction(HttpClient asyncclient, String accessToken) throws ValidationException {
+ Request request = asyncclient.newRequest(getURL()).timeout(handler.getBridgeConfiguration().getAsyncTimeout(),
+ TimeUnit.SECONDS);
+ logger.debug("[{}] running command", getClass().getSimpleName());
+
+ // we want to receive json only, so explicitely set this!
+ request.header(HttpHeader.ACCEPT, "application/json");
+ request.header(HttpHeader.ACCEPT_ENCODING, StandardCharsets.UTF_8.name());
+
+ // this should be the default for myUplink Cloud API
+ request.followRedirects(false);
+
+ // add authentication data for every request. Handling this here makes it obsolete to implement for each and
+ // every command
+ if (!accessToken.isBlank()) {
+ request.header(HttpHeader.AUTHORIZATION, WEB_REQUEST_BEARER_TOKEN_PREFIX + accessToken);
+ }
+
+ prepareRequest(request).send(this);
+ }
+
+ /**
+ * @return returns Http Status Code
+ */
+ public CommunicationStatus getCommunicationStatus() {
+ return communicationStatus;
+ }
+
+ /**
+ * calls the registered resultProcessor.
+ *
+ * @param jsonObject
+ */
+ protected final void processResult(JsonObject jsonObject) {
+ try {
+ resultProcessor.processResult(getCommunicationStatus(), jsonObject);
+ } catch (Exception ex) {
+ // this should not happen
+ logger.warn("[{}] Exception caught: {}", getClass().getSimpleName(), ex.getMessage(), ex);
+ }
+ }
+
+ /**
+ * default implementation just assumes that we want to retrieve data via GET.
+ * can be overridden for any special case and has to prepare the requests with additional parameters, etc
+ *
+ * @param requestToPrepare the request to prepare
+ * @return prepared Request object
+ * @throws ValidationException
+ */
+ protected Request prepareRequest(Request requestToPrepare) throws ValidationException {
+ requestToPrepare.method(HttpMethod.GET);
+ return requestToPrepare;
+ }
+
+ /**
+ * concrete implementation has to provide the channel group.
+ *
+ * @return
+ */
+ protected abstract String getChannelGroup();
+
+ /**
+ * concrete implementation has to provide the URL
+ *
+ * @return Url
+ */
+ protected abstract String getURL();
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/AbstractPagingCommand.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/AbstractPagingCommand.java
new file mode 100644
index 0000000000000..4a7a8ec24f81d
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/AbstractPagingCommand.java
@@ -0,0 +1,42 @@
+/**
+ * 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.myuplink.internal.command;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
+import org.openhab.binding.myuplink.internal.model.ValidationException;
+
+/**
+ * base class for all commands that support paging. common logic should be implemented here
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public abstract class AbstractPagingCommand extends AbstractCommand {
+
+ public AbstractPagingCommand(MyUplinkThingHandler handler, RetryOnFailure retryOnFailure,
+ ProcessFailureResponse processFailureResponse, JsonResultProcessor resultProcessor) {
+ super(handler, retryOnFailure, processFailureResponse, resultProcessor);
+ }
+
+ @Override
+ protected Request prepareRequest(Request requestToPrepare) throws ValidationException {
+ requestToPrepare.param(WEB_REQUEST_PARAM_PAGE_SIZE_KEY, String.valueOf(WEB_REQUEST_PARAM_PAGE_SIZE_VALUE));
+ requestToPrepare.method(HttpMethod.GET);
+ return requestToPrepare;
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/AbstractWriteCommand.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/AbstractWriteCommand.java
new file mode 100644
index 0000000000000..0632bf0c1fa0f
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/AbstractWriteCommand.java
@@ -0,0 +1,151 @@
+/**
+ * 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.myuplink.internal.command;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.EMPTY;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.measure.MetricPrefix;
+import javax.measure.Unit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.api.Request;
+import org.openhab.binding.myuplink.internal.Utils;
+import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
+import org.openhab.binding.myuplink.internal.model.ValidationException;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * base class for all write commands. common logic should be implemented here
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public abstract class AbstractWriteCommand extends AbstractCommand {
+ private final Logger logger = LoggerFactory.getLogger(AbstractWriteCommand.class);
+
+ protected final Channel channel;
+ protected final Command command;
+
+ /**
+ * the constructor
+ */
+ public AbstractWriteCommand(MyUplinkThingHandler handler, Channel channel, Command command,
+ RetryOnFailure retryOnFailure, ProcessFailureResponse processFailureResponse,
+ JsonResultProcessor resultProcessor) {
+ super(handler, retryOnFailure, processFailureResponse, resultProcessor);
+ this.channel = channel;
+ this.command = command;
+ }
+
+ /**
+ * helper method for write commands that extracts value from command.
+ *
+ * @return value as String without unit.
+ */
+ protected String getCommandValue() {
+ if (command instanceof QuantityType> quantityCommand) {
+ // this is necessary because we must not send the unit to the backend
+ Unit> unit = quantityCommand.getUnit();
+ QuantityType> convertedType;
+ if (unit.isCompatible(SIUnits.CELSIUS)) {
+ convertedType = quantityCommand.toUnit(SIUnits.CELSIUS);
+ } else if (unit.isCompatible(Units.KILOWATT_HOUR)) {
+ convertedType = quantityCommand.toUnit(Units.KILOWATT_HOUR);
+ } else if (unit.isCompatible(Units.LITRE_PER_MINUTE)) {
+ convertedType = quantityCommand.toUnit(Units.LITRE_PER_MINUTE);
+ } else if (unit.isCompatible(tech.units.indriya.unit.Units.WATT)) {
+ convertedType = quantityCommand.toUnit(MetricPrefix.KILO(tech.units.indriya.unit.Units.WATT));
+ } else {
+ logger.warn("automatic conversion of unit '{}' to myUplink expected unit not supported.",
+ unit.getName());
+ convertedType = quantityCommand;
+ }
+ return String.valueOf(convertedType != null ? convertedType.doubleValue() : UnDefType.NULL);
+ } else if (command instanceof OnOffType onOffType) {
+ // this is necessary because we must send 0/1 and not ON/OFF to the backend
+ return OnOffType.ON.equals(onOffType) ? "1" : "0";
+ } else {
+ return command.toString();
+ }
+ }
+
+ /**
+ * helper that transforms channelId + commandvalue in a JSON string that can be added as content to a POST request.
+ *
+ * @return converted JSON string
+ */
+ protected String getJsonContent() {
+ return buildJsonObject(channel.getUID().getIdWithoutGroup(), getCommandValue());
+ }
+
+ /**
+ * helper that creates a simple json object as string.
+ *
+ * @param key identifier of the value
+ * @param value the value to assign to the key
+ *
+ * @return converted JSON string
+ */
+ protected String buildJsonObject(String key, String value) {
+ Map content = new HashMap<>(1);
+ content.put(key, value);
+
+ return gson.toJson(content);
+ }
+
+ @Override
+ protected Request prepareRequest(Request requestToPrepare) throws ValidationException {
+ String channelId = channel.getUID().getIdWithoutGroup();
+ String expr = Utils.getValidationExpression(channel);
+ String value = getCommandValue();
+
+ // quantity types are transformed to double and thus we might have decimals which could cause validation error.
+ // So we will shorten here in case no decimals are needed.
+ if (value.endsWith(".0")) {
+ value = value.substring(0, value.length() - 2);
+ }
+
+ if (value.matches(expr)) {
+ return prepareWriteRequest(requestToPrepare);
+ } else {
+ logger.debug("channel '{}' does not allow value '{}' - validation rule '{}'", channelId, value, expr);
+ throw new ValidationException("channel (" + channelId + ") could not be updated due to a validation error");
+ }
+ }
+
+ @Override
+ protected String getChannelGroup() {
+ // this is a pure write command, thus no channel group needed.
+ return EMPTY;
+ }
+
+ /**
+ * concrete implementation has to prepare the write requests with additional parameters, etc
+ *
+ * @param requestToPrepare the request to prepare
+ * @return prepared Request object
+ * @throws ValidationException
+ */
+ protected abstract Request prepareWriteRequest(Request requestToPrepare) throws ValidationException;
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/JsonResultProcessor.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/JsonResultProcessor.java
new file mode 100644
index 0000000000000..ccf51e7e39ca3
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/JsonResultProcessor.java
@@ -0,0 +1,39 @@
+/**
+ * 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.myuplink.internal.command;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.myuplink.internal.connector.CommunicationStatus;
+
+import com.google.gson.JsonObject;
+
+/**
+ * functional interface that is intended to provide a function for further result processing of json data retrieved by a
+ * command.
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+@FunctionalInterface
+public interface JsonResultProcessor {
+
+ /**
+ * this method processes the result of the myUplink API call.
+ *
+ * @param status
+ * technical communication status of the http call.
+ * @param jsonObject
+ * json response of the http call
+ */
+ void processResult(CommunicationStatus status, JsonObject jsonObject);
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/MyUplinkCommand.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/MyUplinkCommand.java
new file mode 100644
index 0000000000000..1719826622364
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/MyUplinkCommand.java
@@ -0,0 +1,40 @@
+/**
+ * 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.myuplink.internal.command;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.Response.CompleteListener;
+import org.eclipse.jetty.client.api.Response.ContentListener;
+import org.eclipse.jetty.client.api.Response.FailureListener;
+import org.eclipse.jetty.client.api.Response.SuccessListener;
+import org.openhab.binding.myuplink.internal.model.ValidationException;
+
+/**
+ * public interface for all commands
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public interface MyUplinkCommand extends SuccessListener, FailureListener, ContentListener, CompleteListener {
+
+ static final int MAX_RETRIES = 5;
+
+ /**
+ * this method is to be called by the UplinkWebinterface class
+ *
+ * @param asyncclient
+ * @throws ValidationException
+ */
+ void performAction(HttpClient asyncclient, String token) throws ValidationException;
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/account/GetSystems.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/account/GetSystems.java
new file mode 100644
index 0000000000000..7cff5d5541d41
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/account/GetSystems.java
@@ -0,0 +1,62 @@
+/**
+ * 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.myuplink.internal.command.account;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.Result;
+import org.openhab.binding.myuplink.internal.command.AbstractPagingCommand;
+import org.openhab.binding.myuplink.internal.command.JsonResultProcessor;
+import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
+
+import com.google.gson.JsonObject;
+
+/**
+ * implements the get sites api call of the site.
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public class GetSystems extends AbstractPagingCommand {
+
+ public GetSystems(MyUplinkThingHandler handler, JsonResultProcessor resultProcessor) {
+ // retry does not make much sense as it is a polling command, command should always succeed therefore update
+ // handler on failure.
+ super(handler, RetryOnFailure.NO, ProcessFailureResponse.YES, resultProcessor);
+ }
+
+ @Override
+ protected String getURL() {
+ String url = GET_SYSTEMS_URL;
+ return url;
+ }
+
+ @Override
+ public void onComplete(@Nullable Result result) {
+ String json = getContentAsString(StandardCharsets.UTF_8);
+ JsonObject jsonObject = gson.fromJson(json, JsonObject.class);
+
+ if (jsonObject != null) {
+ processResult(jsonObject);
+ }
+ }
+
+ @Override
+ protected String getChannelGroup() {
+ return EMPTY;
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/account/Login.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/account/Login.java
new file mode 100644
index 0000000000000..e205e5c4c4252
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/account/Login.java
@@ -0,0 +1,86 @@
+/**
+ * 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.myuplink.internal.command.account;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.util.FormContentProvider;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.util.Fields;
+import org.openhab.binding.myuplink.internal.command.AbstractCommand;
+import org.openhab.binding.myuplink.internal.command.JsonResultProcessor;
+import org.openhab.binding.myuplink.internal.handler.MyUplinkBridgeHandler;
+
+import com.google.gson.JsonObject;
+
+/**
+ * implements the login to the webinterface
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public class Login extends AbstractCommand {
+
+ private final String encodedLogin;
+
+ public Login(MyUplinkBridgeHandler handler, JsonResultProcessor resultProcessor) {
+ // flags do not matter as "onComplete" is overwritten in this class.
+ super(handler, RetryOnFailure.NO, ProcessFailureResponse.NO, resultProcessor);
+
+ String login = handler.getBridgeConfiguration().getClientId() + ":"
+ + handler.getBridgeConfiguration().getClientSecret();
+ encodedLogin = Base64.getEncoder().encodeToString(login.getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Override
+ protected Request prepareRequest(Request requestToPrepare) {
+ Fields fields = new Fields();
+ fields.add(LOGIN_FIELD_GRANT_TYPE_KEY, LOGIN_FIELD_GRANT_TYPE_VALUE);
+ fields.add(LOGIN_FIELD_SCOPE_KEY, LOGIN_FIELD_SCOPE_VALUE);
+ FormContentProvider cp = new FormContentProvider(fields, StandardCharsets.UTF_8);
+
+ requestToPrepare.header(HttpHeader.AUTHORIZATION, LOGIN_BASIC_AUTH_PREFIX + encodedLogin);
+ requestToPrepare.content(cp);
+ requestToPrepare.method(HttpMethod.POST);
+
+ return requestToPrepare;
+ }
+
+ @Override
+ protected String getURL() {
+ return LOGIN_URL;
+ }
+
+ @Override
+ public void onComplete(@Nullable Result result) {
+ String json = getContentAsString(StandardCharsets.UTF_8);
+ JsonObject jsonObject = gson.fromJson(json, JsonObject.class);
+
+ if (jsonObject != null) {
+ processResult(jsonObject);
+ }
+ }
+
+ @Override
+ protected String getChannelGroup() {
+ return EMPTY;
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/device/GetPoints.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/device/GetPoints.java
new file mode 100644
index 0000000000000..e1dc7562d35e5
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/device/GetPoints.java
@@ -0,0 +1,47 @@
+/**
+ * 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.myuplink.internal.command.device;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.myuplink.internal.command.AbstractCommand;
+import org.openhab.binding.myuplink.internal.command.JsonResultProcessor;
+import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
+
+/**
+ * implements the get points api call of the myUplink API.
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public class GetPoints extends AbstractCommand {
+ private final String url;
+
+ public GetPoints(MyUplinkThingHandler handler, String deviceId, JsonResultProcessor resultProcessor) {
+ // retry does not make much sense as it is a polling command, command should always succeed therefore update
+ // handler on failure.
+ super(handler, RetryOnFailure.NO, ProcessFailureResponse.YES, resultProcessor);
+ this.url = GET_DEVICE_POINTS.replaceAll("\\{deviceId\\}", deviceId);
+ }
+
+ @Override
+ protected String getURL() {
+ return url;
+ }
+
+ @Override
+ protected String getChannelGroup() {
+ return EMPTY;
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/device/GetSmartHomeMode.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/device/GetSmartHomeMode.java
new file mode 100644
index 0000000000000..46c716f83f706
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/device/GetSmartHomeMode.java
@@ -0,0 +1,49 @@
+/**
+ * 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.myuplink.internal.command.device;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.myuplink.internal.command.AbstractCommand;
+import org.openhab.binding.myuplink.internal.command.JsonResultProcessor;
+import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
+import org.openhab.binding.myuplink.internal.model.SmartHomeModeResponseTransformer;
+
+/**
+ * implements the get sites api call of the site.
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public class GetSmartHomeMode extends AbstractCommand {
+ private String url;
+
+ public GetSmartHomeMode(MyUplinkThingHandler handler, String systemId, JsonResultProcessor resultProcessor) {
+ // retry does not make much sense as it is a polling command, command should always succeed therefore update
+ // handler on failure.
+ super(handler, new SmartHomeModeResponseTransformer(handler), RetryOnFailure.NO, ProcessFailureResponse.YES,
+ resultProcessor);
+ this.url = GET_SMART_HOME_MODE_URL.replaceAll("\\{systemId\\}", systemId);
+ }
+
+ @Override
+ protected String getURL() {
+ return this.url;
+ }
+
+ @Override
+ protected String getChannelGroup() {
+ return EMPTY;
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/device/SetPoints.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/device/SetPoints.java
new file mode 100644
index 0000000000000..16ca6bc013d45
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/device/SetPoints.java
@@ -0,0 +1,61 @@
+/**
+ * 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.myuplink.internal.command.device;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.myuplink.internal.command.AbstractWriteCommand;
+import org.openhab.binding.myuplink.internal.command.JsonResultProcessor;
+import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
+import org.openhab.binding.myuplink.internal.model.ValidationException;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.types.Command;
+
+/**
+ * implements the set points api call of the myUplink API.
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public class SetPoints extends AbstractWriteCommand {
+ private final String url;
+
+ public SetPoints(MyUplinkThingHandler handler, Channel channel, Command command, String deviceId,
+ JsonResultProcessor resultProcessor) {
+ super(handler, channel, command, RetryOnFailure.YES, ProcessFailureResponse.YES, resultProcessor);
+ this.url = SET_DEVICE_POINTS.replaceAll("\\{deviceId\\}", deviceId);
+ }
+
+ @Override
+ protected String getURL() {
+ return url;
+ }
+
+ @Override
+ protected Request prepareWriteRequest(Request requestToPrepare) throws ValidationException {
+ requestToPrepare.method(HttpMethod.PATCH);
+
+ StringContentProvider cp = new StringContentProvider(WEB_REQUEST_PATCH_CONTENT_TYPE, getJsonContent(),
+ StandardCharsets.UTF_8);
+
+ requestToPrepare.content(cp);
+
+ return requestToPrepare;
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/device/SetPointsAdvanced.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/device/SetPointsAdvanced.java
new file mode 100644
index 0000000000000..4bee2688f0ca6
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/device/SetPointsAdvanced.java
@@ -0,0 +1,105 @@
+/**
+ * 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.myuplink.internal.command.device;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.myuplink.internal.command.AbstractWriteCommand;
+import org.openhab.binding.myuplink.internal.command.JsonResultProcessor;
+import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
+import org.openhab.binding.myuplink.internal.model.ValidationException;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.types.Command;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+
+/**
+ * implements the set points api call of the API. Extracts channel ID and value from the command string. Needed by the
+ * "generic command" channel.
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public class SetPointsAdvanced extends AbstractWriteCommand {
+ private final String url;
+
+ public SetPointsAdvanced(MyUplinkThingHandler handler, Channel channel, Command command, String deviceId,
+ JsonResultProcessor resultProcessor) {
+ super(handler, channel, command, RetryOnFailure.YES, ProcessFailureResponse.YES, resultProcessor);
+ this.url = SET_DEVICE_POINTS.replaceAll("\\{deviceId\\}", deviceId);
+ }
+
+ @Override
+ protected String getURL() {
+ return url;
+ }
+
+ @Override
+ protected Request prepareWriteRequest(Request requestToPrepare) throws ValidationException {
+ requestToPrepare.method(HttpMethod.PATCH);
+
+ StringContentProvider cp = new StringContentProvider(WEB_REQUEST_PATCH_CONTENT_TYPE,
+ buildJsonObject(getChannelId(), getChannelValue()), StandardCharsets.UTF_8);
+ requestToPrepare.content(cp);
+
+ return requestToPrepare;
+ }
+
+ private String getChannelId() {
+ if (command instanceof StringType stringCommand) {
+ String[] tokens = stringCommand.toString().split(":");
+ return tokens.length == 2 ? tokens[0] : command.toString();
+ } else {
+ return command.toString();
+ }
+ }
+
+ private String getChannelValue() {
+ if (command instanceof StringType stringCommand) {
+ String[] tokens = stringCommand.toString().split(":");
+ return tokens.length == 2 ? tokens[1] : command.toString();
+ } else {
+ return command.toString();
+ }
+ }
+
+ /**
+ * handling of result in case of HTTP response OK.
+ *
+ * @param json
+ */
+ protected void onCompleteCodeOk(@Nullable String json) {
+ Map content = new HashMap<>(2);
+ content.put(JSON_KEY_CHANNEL_ID, CHANNEL_ID_COMMAND);
+ content.put(JSON_KEY_CHANNEL_VALUE, getCommunicationStatus().getHttpCode().name());
+ content.put(JSON_KEY_ROOT_DATA, json == null ? EMPTY : json);
+
+ var jsonObjectString = gson.toJson(content);
+ var jsonObject = gson.fromJson(jsonObjectString, JsonObject.class);
+
+ var jsonArray = new JsonArray();
+ jsonArray.add(jsonObject);
+ super.onCompleteCodeOk(gson.toJson(jsonArray));
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/device/SetSmartHomeMode.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/device/SetSmartHomeMode.java
new file mode 100644
index 0000000000000..65732fc13fa7d
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/command/device/SetSmartHomeMode.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.myuplink.internal.command.device;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.myuplink.internal.command.AbstractWriteCommand;
+import org.openhab.binding.myuplink.internal.command.JsonResultProcessor;
+import org.openhab.binding.myuplink.internal.handler.MyUplinkThingHandler;
+import org.openhab.binding.myuplink.internal.model.ValidationException;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.types.Command;
+
+/**
+ * implements the set smart home mode api call of the site.
+ *
+ * @author Anders Alfredsson - initial contribution
+ */
+@NonNullByDefault
+public class SetSmartHomeMode extends AbstractWriteCommand {
+ private String url;
+
+ public SetSmartHomeMode(MyUplinkThingHandler handler, Channel channel, Command command, String systemId,
+ JsonResultProcessor resultProcessor) {
+ // retry does not make much sense as it is a polling command, command should always succeed therefore update
+ // handler on failure.
+ super(handler, channel, command, RetryOnFailure.NO, ProcessFailureResponse.YES, resultProcessor);
+ this.url = SET_SMART_HOME_MODE_URL.replaceAll("\\{systemId\\}", systemId);
+ }
+
+ @Override
+ protected String getURL() {
+ return this.url;
+ }
+
+ @Override
+ protected Request prepareWriteRequest(Request requestToPrepare) throws ValidationException {
+ requestToPrepare.method(HttpMethod.PUT);
+
+ String body = buildJsonObject(JSON_KEY_SMART_HOME_MODE, command.toString());
+
+ StringContentProvider cp = new StringContentProvider(WEB_REQUEST_PATCH_CONTENT_TYPE, body,
+ StandardCharsets.UTF_8);
+
+ requestToPrepare.content(cp);
+
+ return requestToPrepare;
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/config/MyUplinkConfiguration.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/config/MyUplinkConfiguration.java
new file mode 100644
index 0000000000000..8f8ce78c01e98
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/config/MyUplinkConfiguration.java
@@ -0,0 +1,95 @@
+/**
+ * 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.myuplink.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link MyUplinkConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Alexander Friese - Initial contribution
+ */
+@NonNullByDefault
+public class MyUplinkConfiguration {
+
+ private String clientId = "";
+ private String clientSecret = "";
+
+ private int dataPollingInterval = 60;
+ private static final int ASYNC_TIMEOUT = 120;
+ private static final int SYNC_TIMEOUT = 120;
+
+ /**
+ * @return the clientId
+ */
+ public String getClientId() {
+ return clientId;
+ }
+
+ /**
+ * @param clientId the clientId to set
+ */
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ /**
+ * @return the clientSecret
+ */
+ public String getClientSecret() {
+ return clientSecret;
+ }
+
+ /**
+ * @param clientSecret the clientSecret to set
+ */
+ public void setClientSecret(String clientSecret) {
+ this.clientSecret = clientSecret;
+ }
+
+ /**
+ * @return the asyncTimeout
+ */
+ public Integer getAsyncTimeout() {
+ return ASYNC_TIMEOUT;
+ }
+
+ /**
+ * @return the syncTimeout
+ */
+ public Integer getSyncTimeout() {
+ return SYNC_TIMEOUT;
+ }
+
+ /**
+ * @return the dataPollingInterval
+ */
+ public Integer getDataPollingInterval() {
+ return dataPollingInterval;
+ }
+
+ /**
+ * @param dataPollingInterval the dataPollingInterval to set
+ */
+ public void setDataPollingInterval(Integer dataPollingInterval) {
+ this.dataPollingInterval = dataPollingInterval;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("MyUplinkConfiguration [clientId=").append(clientId).append(", clientSecret=")
+ .append(clientSecret).append(", dataPollingInterval=").append(dataPollingInterval).append("]");
+ return builder.toString();
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/connector/CommunicationStatus.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/connector/CommunicationStatus.java
new file mode 100644
index 0000000000000..4e95a4b813bec
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/connector/CommunicationStatus.java
@@ -0,0 +1,59 @@
+/**
+ * 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.myuplink.internal.connector;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.http.HttpStatus.Code;
+
+/**
+ * this class contains the HTTP status of the communication and an optional exception that might occoured during
+ * communication
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public class CommunicationStatus {
+
+ private @Nullable Code httpCode;
+ private @Nullable Exception error;
+
+ public final Code getHttpCode() {
+ Code code = httpCode;
+ return code == null ? Code.INTERNAL_SERVER_ERROR : code;
+ }
+
+ public final void setHttpCode(Code httpCode) {
+ this.httpCode = httpCode;
+ }
+
+ public final @Nullable Exception getError() {
+ return error;
+ }
+
+ public final void setError(Exception error) {
+ this.error = error;
+ }
+
+ public final String getMessage() {
+ Exception err = error;
+ String errMsg = err == null ? null : err.getMessage();
+ String msg = getHttpCode().getMessage();
+ if (errMsg != null && !errMsg.isEmpty()) {
+ return errMsg;
+ } else if (msg != null && !msg.isEmpty()) {
+ return msg;
+ }
+ return "";
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/connector/WebInterface.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/connector/WebInterface.java
new file mode 100644
index 0000000000000..23c7c7fc3dc6c
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/connector/WebInterface.java
@@ -0,0 +1,316 @@
+/**
+ * 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.myuplink.internal.connector;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Queue;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.util.BlockingArrayQueue;
+import org.openhab.binding.myuplink.internal.AtomicReferenceTrait;
+import org.openhab.binding.myuplink.internal.Utils;
+import org.openhab.binding.myuplink.internal.command.MyUplinkCommand;
+import org.openhab.binding.myuplink.internal.command.account.Login;
+import org.openhab.binding.myuplink.internal.handler.MyUplinkBridgeHandler;
+import org.openhab.binding.myuplink.internal.handler.StatusHandler;
+import org.openhab.binding.myuplink.internal.model.ValidationException;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonObject;
+
+/**
+ * The connector is responsible for communication with the NIBE myUplink Cloud API
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public class WebInterface implements AtomicReferenceTrait {
+
+ private final Logger logger = LoggerFactory.getLogger(WebInterface.class);
+
+ /**
+ * bridge handler
+ */
+ private final MyUplinkBridgeHandler handler;
+
+ /**
+ * handler for updating bridge status
+ */
+ private final StatusHandler bridgeStatusHandler;
+
+ /**
+ * holds authentication status
+ */
+ private boolean authenticated = false;
+
+ /**
+ * access token returned by login, needed to authenticate all requests send to API.
+ */
+ private String accessToken;
+
+ /**
+ * expiry of the access token.
+ */
+ private Instant tokenExpiry;
+
+ /**
+ * last refresh of the access token.
+ */
+ private Instant tokenRefreshDate;
+
+ /**
+ * HTTP client for asynchronous calls
+ */
+ private final HttpClient httpClient;
+
+ /**
+ * the scheduler which periodically sends web requests to the NIBE myUplink Cloud API. Should be initiated with the
+ * thing's
+ * existing scheduler instance.
+ */
+ private final ScheduledExecutorService scheduler;
+
+ /**
+ * request executor
+ */
+ private final WebRequestExecutor requestExecutor;
+
+ /**
+ * periodic request executor job
+ */
+ private final AtomicReference<@Nullable Future>> requestExecutorJobReference;
+
+ /**
+ * this class is responsible for executing periodic web requests. This ensures that only one request is executed
+ * at the same time and there will be a guaranteed minimum delay between subsequent requests.
+ *
+ * @author afriese - initial contribution
+ */
+ private class WebRequestExecutor implements Runnable {
+
+ /**
+ * queue which holds the commands to execute
+ */
+ private final Queue commandQueue;
+
+ /**
+ * constructor
+ */
+ WebRequestExecutor() {
+ this.commandQueue = new BlockingArrayQueue<>(WEB_REQUEST_QUEUE_MAX_SIZE);
+ }
+
+ private void processAuthenticationResult(CommunicationStatus status, JsonObject jsonObject) {
+ String msg = Utils.getAsString(jsonObject, JSON_KEY_ERROR);
+ if (msg == null || msg.isBlank()) {
+ msg = status.getMessage();
+ }
+
+ switch (status.getHttpCode()) {
+ case BAD_REQUEST:
+ bridgeStatusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ msg);
+ setAuthenticated(false);
+ break;
+ case OK:
+ String accessToken = Utils.getAsString(jsonObject, JSON_KEY_AUTH_ACCESS_TOKEN);
+ int expiresInSeconds = Utils.getAsInt(jsonObject, JSON_KEY_AUTH_EXPIRES_IN);
+ if (accessToken != null && expiresInSeconds != 0) {
+ WebInterface.this.accessToken = accessToken;
+ tokenRefreshDate = Instant.now();
+ tokenExpiry = tokenRefreshDate.plusSeconds(expiresInSeconds);
+
+ logger.debug("access token refreshed: {}, expiry: {}", tokenRefreshDate.toString(),
+ tokenExpiry.toString());
+
+ bridgeStatusHandler.updateStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE,
+ STATUS_TOKEN_VALIDATED);
+ setAuthenticated(true);
+ handler.startDiscovery();
+ break;
+ }
+ default:
+ bridgeStatusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ msg);
+ setAuthenticated(false);
+ }
+ }
+
+ /**
+ * puts a command into the queue
+ *
+ * @param command
+ */
+ void enqueue(MyUplinkCommand command) {
+ try {
+ commandQueue.add(command);
+ } catch (IllegalStateException ex) {
+ if (commandQueue.size() >= WEB_REQUEST_QUEUE_MAX_SIZE) {
+ logger.debug(
+ "Could not add command to command queue because queue is already full. Maybe NIBE myUplink is down?");
+ } else {
+ logger.warn("Could not add command to queue - IllegalStateException");
+ }
+ }
+ }
+
+ /**
+ * executes the web request
+ */
+ @Override
+ public void run() {
+ logger.debug("run queued commands, queue size is {}", commandQueue.size());
+ if (!isAuthenticated()) {
+ authenticate();
+ } else {
+ refreshAccessToken();
+
+ if (isAuthenticated() && !commandQueue.isEmpty()) {
+ try {
+ executeCommand();
+ } catch (Exception ex) {
+ logger.warn("command execution ended with exception:", ex);
+ }
+ }
+ }
+ }
+
+ /**
+ * authenticates with the NIBE myUplink Cloud interface.
+ */
+ private synchronized void authenticate() {
+ setAuthenticated(false);
+ MyUplinkCommand loginCommand = new Login(handler, this::processAuthenticationResult);
+ try {
+ loginCommand.performAction(httpClient, "");
+ } catch (ValidationException e) {
+ // this cannot happen
+ }
+ }
+
+ /**
+ * periodically refreshed the access token.
+ */
+ private synchronized void refreshAccessToken() {
+ Instant now = Instant.now();
+
+ if (now.plus(WEB_REQUEST_TOKEN_EXPIRY_BUFFER_MINUTES, ChronoUnit.MINUTES).isAfter(tokenExpiry)
+ || now.isAfter(tokenRefreshDate.plus(WEB_REQUEST_TOKEN_MAX_AGE_MINUTES, ChronoUnit.MINUTES))) {
+ logger.debug("access token needs to be refreshed, last refresh: {}, expiry: {}",
+ tokenRefreshDate.toString(), tokenExpiry.toString());
+
+ MyUplinkCommand refreshCommand = new Login(handler, this::processAuthenticationResult);
+ try {
+ refreshCommand.performAction(httpClient, "");
+ } catch (ValidationException e) {
+ // this cannot happen
+ }
+ }
+ }
+
+ /**
+ * executes the next command in the queue. requires authenticated session.
+ *
+ * @throws ValidationException
+ */
+ private void executeCommand() throws ValidationException {
+ MyUplinkCommand command = commandQueue.poll();
+ if (command != null) {
+ command.performAction(httpClient, accessToken);
+ }
+ }
+ }
+
+ /**
+ * Constructor to set up interface
+ */
+ public WebInterface(ScheduledExecutorService scheduler, MyUplinkBridgeHandler handler, HttpClient httpClient,
+ StatusHandler bridgeStatusHandler) {
+ this.handler = handler;
+ this.bridgeStatusHandler = bridgeStatusHandler;
+ this.scheduler = scheduler;
+ this.httpClient = httpClient;
+ this.tokenExpiry = OUTDATED_DATE;
+ this.tokenRefreshDate = OUTDATED_DATE;
+ this.accessToken = "";
+ this.requestExecutor = new WebRequestExecutor();
+ this.requestExecutorJobReference = new AtomicReference<>(null);
+ }
+
+ public void start() {
+ setAuthenticated(false);
+ updateJobReference(requestExecutorJobReference, scheduler.scheduleWithFixedDelay(requestExecutor,
+ WEB_REQUEST_INITIAL_DELAY, WEB_REQUEST_INTERVAL, TimeUnit.SECONDS));
+ }
+
+ /**
+ * queues any command for execution
+ *
+ * @param command
+ */
+ public void enqueueCommand(MyUplinkCommand command) {
+ requestExecutor.enqueue(command);
+ }
+
+ /**
+ * will be called by the ThingHandler to abort periodic jobs.
+ */
+ public void dispose() {
+ logger.debug("Webinterface disposed.");
+ cancelJobReference(requestExecutorJobReference);
+ setAuthenticated(false);
+ }
+
+ /**
+ * returns authentication status.
+ *
+ * @return
+ */
+ private boolean isAuthenticated() {
+ return authenticated;
+ }
+
+ /**
+ * update the authentication status, also resets token data.
+ *
+ * @param authenticated
+ */
+ private void setAuthenticated(boolean authenticated) {
+ this.authenticated = authenticated;
+ if (!authenticated) {
+ this.tokenExpiry = OUTDATED_DATE;
+ this.accessToken = "";
+ }
+ }
+
+ /**
+ * returns the current access token.
+ *
+ * @return
+ */
+ public String getAccessToken() {
+ return accessToken;
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/discovery/MyUplinkDiscoveryService.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/discovery/MyUplinkDiscoveryService.java
new file mode 100644
index 0000000000000..f2c2bd8f7931b
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/discovery/MyUplinkDiscoveryService.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.myuplink.internal.discovery;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.myuplink.internal.MyUplinkBindingConstants;
+import org.openhab.binding.myuplink.internal.Utils;
+import org.openhab.binding.myuplink.internal.command.account.GetSystems;
+import org.openhab.binding.myuplink.internal.connector.CommunicationStatus;
+import org.openhab.binding.myuplink.internal.handler.MyUplinkAccountHandler;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+/**
+ * this class will handle discovery of wallboxes and circuits within the site configured.
+ *
+ * @author Alexander Friese - initial contribution
+ *
+ */
+@NonNullByDefault
+public class MyUplinkDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
+
+ private final Logger logger = LoggerFactory.getLogger(MyUplinkDiscoveryService.class);
+ private @NonNullByDefault({}) MyUplinkAccountHandler bridgeHandler;
+
+ public MyUplinkDiscoveryService() throws IllegalArgumentException {
+ super(Set.of(MyUplinkBindingConstants.THING_TYPE_ACCOUNT), 300, false);
+ }
+
+ @Override
+ public void setThingHandler(ThingHandler handler) {
+ if (handler instanceof MyUplinkAccountHandler accountHandler) {
+ this.bridgeHandler = accountHandler;
+ this.bridgeHandler.setDiscoveryService(this);
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return bridgeHandler;
+ }
+
+ // method is defined in both implemented interface and inherited class, thus we must define a behaviour here.
+ @Override
+ public void deactivate() {
+ super.deactivate();
+ }
+
+ @Override
+ protected void startScan() {
+ bridgeHandler.enqueueCommand(new GetSystems(bridgeHandler, this::processMyUplinkDiscoveryResult));
+ }
+
+ /**
+ * callback that handles json result data to provide discovery result.
+ *
+ * @param site
+ */
+ void processMyUplinkDiscoveryResult(CommunicationStatus status, JsonObject json) {
+ logger.debug("processMyUplinkDiscoveryResult {}", json);
+
+ JsonArray systems = json.getAsJsonArray(JSON_KEY_SYSTEMS);
+ if (systems == null || systems.isEmpty()) {
+ logger.debug("System discovery finished, no systems found.");
+ } else {
+ systems.forEach(this::handleSystemDiscovery);
+ }
+ }
+
+ void handleSystemDiscovery(JsonElement json) {
+ logger.debug("handleSystemDiscovery {}", json);
+
+ JsonObject system = json.getAsJsonObject();
+ String systemId = Utils.getAsString(system, JSON_KEY_SYSTEM_ID);
+ JsonArray devices = system.getAsJsonArray(JSON_KEY_DEVICES);
+ if (devices == null || devices.isEmpty()) {
+ logger.debug("System discovery finished, no devices found.");
+ } else {
+ devices.forEach(device -> handleDeviceDiscovery(device, systemId));
+ }
+ }
+
+ void handleDeviceDiscovery(JsonElement json, @Nullable String systemId) {
+ logger.debug("handleDeviceDiscovery {}", json);
+
+ JsonObject device = json.getAsJsonObject();
+ String id = Utils.getAsString(device, JSON_KEY_GENERIC_ID);
+ String serial = Utils.getAsString(device.getAsJsonObject(JSON_KEY_PRODUCT), JSON_KEY_SERIAL);
+ String name = Utils.getAsString(device.getAsJsonObject(JSON_KEY_PRODUCT), JSON_KEY_NAME);
+
+ if (id != null && serial != null) {
+ DiscoveryResultBuilder builder;
+ builder = initDiscoveryResultBuilder(DEVICE_GENERIC_DEVICE, id, name);
+ builder.withProperty(THING_CONFIG_SERIAL, serial);
+ if (systemId != null) {
+ builder.withProperty(THING_CONFIG_SYSTEM_ID, systemId);
+ }
+ thingDiscovered(builder.build());
+ }
+ }
+
+ /**
+ * sends discovery notification to the framework.
+ *
+ * @param deviceType
+ * @param deviceId
+ * @param deviceName
+ */
+ DiscoveryResultBuilder initDiscoveryResultBuilder(String deviceType, String deviceId, @Nullable String deviceName) {
+ ThingUID bridgeUID = bridgeHandler.getThing().getUID();
+ ThingTypeUID typeUid = new ThingTypeUID(BINDING_ID, deviceType);
+
+ ThingUID thingUID = new ThingUID(typeUid, bridgeUID, deviceId);
+ String label = deviceName != null ? deviceName : deviceId;
+
+ return DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID).withLabel(label)
+ .withProperty(THING_CONFIG_ID, deviceId).withRepresentationProperty(THING_CONFIG_ID);
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/ChannelProvider.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/ChannelProvider.java
new file mode 100644
index 0000000000000..6b1c9b52885be
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/ChannelProvider.java
@@ -0,0 +1,38 @@
+/**
+ * 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.myuplink.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.Channel;
+
+/**
+ * this interface provides all methods which deal with channels
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public interface ChannelProvider {
+
+ /**
+ * returns the channel with given channelId and groupId. If no channel matches, null is returned.
+ *
+ * @param groupId
+ * group ID of the channel
+ * @param channelId
+ * channel ID of the channel
+ * @return
+ */
+ @Nullable
+ Channel getChannel(String groupId, String channelId);
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/DynamicChannelProvider.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/DynamicChannelProvider.java
new file mode 100644
index 0000000000000..4e07427d6c8c9
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/DynamicChannelProvider.java
@@ -0,0 +1,51 @@
+/**
+ * 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.myuplink.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.myuplink.internal.provider.ChannelFactory;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * this interface provides all methods which deal with channels
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public interface DynamicChannelProvider {
+
+ /**
+ * registers a channel with the thing.
+ *
+ * @param channel
+ * the channel to be registered.
+ */
+ void registerChannel(Channel channel);
+
+ /**
+ * Simple Getter to retrieve the Channelfactory of this thing.
+ *
+ * @return
+ * the ChannelFactory
+ */
+ ChannelFactory getChannelFactory();
+
+ /**
+ * Simple Getter to retrieve the ThingUid
+ *
+ * @return
+ * the ThingUid
+ */
+ ThingUID getThingUid();
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/MyUplinkAccountHandler.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/MyUplinkAccountHandler.java
new file mode 100644
index 0000000000000..2b6c0192065a9
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/MyUplinkAccountHandler.java
@@ -0,0 +1,172 @@
+/**
+ * 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.myuplink.internal.handler;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.myuplink.internal.AtomicReferenceTrait;
+import org.openhab.binding.myuplink.internal.Utils;
+import org.openhab.binding.myuplink.internal.command.MyUplinkCommand;
+import org.openhab.binding.myuplink.internal.command.account.GetSystems;
+import org.openhab.binding.myuplink.internal.config.MyUplinkConfiguration;
+import org.openhab.binding.myuplink.internal.connector.CommunicationStatus;
+import org.openhab.binding.myuplink.internal.connector.WebInterface;
+import org.openhab.binding.myuplink.internal.discovery.MyUplinkDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonObject;
+
+/**
+ * The {@link myUplinkHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Alexander Friese - Initial contribution
+ */
+@NonNullByDefault
+public class MyUplinkAccountHandler extends BaseBridgeHandler implements MyUplinkBridgeHandler, AtomicReferenceTrait {
+
+ private final Logger logger = LoggerFactory.getLogger(MyUplinkAccountHandler.class);
+
+ /**
+ * Schedule for polling live data
+ */
+ private final AtomicReference<@Nullable Future>> dataPollingJobReference;
+
+ private @Nullable DiscoveryService discoveryService;
+
+ /**
+ * Interface object for querying the NIBE myUplink API.
+ */
+ private WebInterface webInterface;
+
+ public MyUplinkAccountHandler(Bridge bridge, HttpClient httpClient) {
+ super(bridge);
+ this.dataPollingJobReference = new AtomicReference<>(null);
+ this.webInterface = new WebInterface(scheduler, this, httpClient, super::updateStatus);
+ }
+
+ @Override
+ public void initialize() {
+ logger.debug("About to initialize myUplink Account");
+ MyUplinkConfiguration config = getBridgeConfiguration();
+ logger.debug("myUplink Account initialized with configuration: {}", config.toString());
+
+ updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, STATUS_WAITING_FOR_LOGIN);
+
+ if (config.getClientId().isBlank()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, STATUS_CONFIG_ERROR_NO_CLIENT_ID);
+ } else if (config.getClientSecret().isBlank()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ STATUS_CONFIG_ERROR_NO_CLIENT_SECRET);
+ } else {
+ webInterface.start();
+ startPolling();
+ }
+ }
+
+ /**
+ * Start the polling.
+ */
+ private void startPolling() {
+ updateJobReference(dataPollingJobReference, scheduler.scheduleWithFixedDelay(this::pollingRun,
+ POLLING_INITIAL_DELAY, getBridgeConfiguration().getDataPollingInterval(), TimeUnit.SECONDS));
+ }
+
+ /**
+ * Poll the Easee Cloud API one time.
+ */
+ void pollingRun() {
+ GetSystems state = new GetSystems(this, this::updateOnlineStatus);
+ enqueueCommand(state);
+ }
+
+ /**
+ * result processor to handle online status updates
+ *
+ * @param status of command execution
+ * @param jsonObject json respone result
+ */
+ protected final void updateOnlineStatus(CommunicationStatus status, JsonObject jsonObject) {
+ String msg = Utils.getAsString(jsonObject, JSON_KEY_ERROR);
+ if (msg == null || msg.isBlank()) {
+ msg = status.getMessage();
+ }
+
+ switch (status.getHttpCode()) {
+ case OK:
+ case ACCEPTED:
+ super.updateStatus(ThingStatus.ONLINE);
+ break;
+ default:
+ super.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
+ }
+ }
+
+ /**
+ * Disposes the bridge.
+ */
+ @Override
+ public void dispose() {
+ logger.debug("Handler disposing");
+ cancelJobReference(dataPollingJobReference);
+ webInterface.dispose();
+ }
+
+ @Override
+ public MyUplinkConfiguration getBridgeConfiguration() {
+ return this.getConfigAs(MyUplinkConfiguration.class);
+ }
+
+ @Override
+ public Collection> getServices() {
+ return Set.of(MyUplinkDiscoveryService.class);
+ }
+
+ public void setDiscoveryService(MyUplinkDiscoveryService discoveryService) {
+ this.discoveryService = discoveryService;
+ }
+
+ @Override
+ public void startDiscovery() {
+ DiscoveryService discoveryService = this.discoveryService;
+ if (discoveryService != null) {
+ discoveryService.startScan(null);
+ }
+ }
+
+ @Override
+ public void enqueueCommand(MyUplinkCommand command) {
+ webInterface.enqueueCommand(command);
+ }
+
+ @Override
+ public Logger getLogger() {
+ return logger;
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/MyUplinkBridgeHandler.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/MyUplinkBridgeHandler.java
new file mode 100644
index 0000000000000..34a2cf8c2f55e
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/MyUplinkBridgeHandler.java
@@ -0,0 +1,30 @@
+/**
+ * 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.myuplink.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.binding.BridgeHandler;
+
+/**
+ * public interface of the {@link MyUplinkBridgeHandler}
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public interface MyUplinkBridgeHandler extends BridgeHandler, MyUplinkThingHandler {
+
+ /**
+ * starts discovery of Nibe devices
+ */
+ void startDiscovery();
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/MyUplinkGenericDeviceHandler.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/MyUplinkGenericDeviceHandler.java
new file mode 100644
index 0000000000000..a30a2b7f1a28e
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/MyUplinkGenericDeviceHandler.java
@@ -0,0 +1,296 @@
+/**
+ * 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.myuplink.internal.handler;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import java.util.Map;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.myuplink.internal.AtomicReferenceTrait;
+import org.openhab.binding.myuplink.internal.MyUplinkBindingConstants;
+import org.openhab.binding.myuplink.internal.Utils;
+import org.openhab.binding.myuplink.internal.command.MyUplinkCommand;
+import org.openhab.binding.myuplink.internal.command.account.GetSystems;
+import org.openhab.binding.myuplink.internal.command.device.GetPoints;
+import org.openhab.binding.myuplink.internal.command.device.GetSmartHomeMode;
+import org.openhab.binding.myuplink.internal.command.device.SetPoints;
+import org.openhab.binding.myuplink.internal.command.device.SetPointsAdvanced;
+import org.openhab.binding.myuplink.internal.command.device.SetSmartHomeMode;
+import org.openhab.binding.myuplink.internal.config.MyUplinkConfiguration;
+import org.openhab.binding.myuplink.internal.connector.CommunicationStatus;
+import org.openhab.binding.myuplink.internal.provider.ChannelFactory;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Channel;
+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.ThingBuilder;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+/**
+ * The {@link MyUplinkGenericDeviceHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public class MyUplinkGenericDeviceHandler extends BaseThingHandler
+ implements MyUplinkThingHandler, DynamicChannelProvider, AtomicReferenceTrait {
+ private final Logger logger = LoggerFactory.getLogger(MyUplinkGenericDeviceHandler.class);
+
+ /**
+ * Schedule for polling live data
+ */
+ private final AtomicReference<@Nullable Future>> dataPollingJobReference;
+
+ private final ChannelFactory channelFactory;
+
+ private final Configuration config;
+
+ public MyUplinkGenericDeviceHandler(Thing thing, ChannelFactory channelFactory) {
+ super(thing);
+ this.dataPollingJobReference = new AtomicReference<>(null);
+ this.channelFactory = channelFactory;
+ this.config = getConfig();
+ }
+
+ @Override
+ public void initialize() {
+ logger.debug("About to initialize myUplink Generic Device with id: {}", getDeviceId());
+
+ updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, STATUS_WAITING_FOR_BRIDGE);
+ startPolling();
+ }
+
+ public String getDeviceId() {
+ return config.get(THING_CONFIG_ID).toString();
+ }
+
+ private void updatePropertiesAndOnlineStatus(CommunicationStatus status, JsonObject systemsJson) {
+ JsonObject deviceFound = extractDevice(systemsJson);
+
+ if (deviceFound != null) {
+ Map properties = editProperties();
+ String currentFwVersion = Utils.getAsString(deviceFound, JSON_KEY_CURRENT_FW_VERSION, GENERIC_NO_VAL);
+ properties.put(THING_CONFIG_CURRENT_FW_VERSION, currentFwVersion);
+ updateProperties(properties);
+
+ String connectionStatus = Utils.getAsString(deviceFound, JSON_KEY_CONNECTION_STATE, GENERIC_NO_VAL);
+ if (connectionStatus.equals(JSON_VAL_CONNECTION_CONNECTED)) {
+ super.updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
+ } else {
+ super.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, STATUS_NO_CONNECTION);
+ }
+ } else {
+ super.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, STATUS_DEVICE_NOT_FOUND);
+ }
+ }
+
+ private final @Nullable JsonObject extractDevice(JsonObject systemsJson) {
+ JsonArray systems = systemsJson.getAsJsonArray(JSON_KEY_SYSTEMS);
+ if (systems != null && !systems.isEmpty()) {
+ for (JsonElement systemJson : systems) {
+ JsonObject system = systemJson.getAsJsonObject();
+ JsonArray devices = system.getAsJsonArray(JSON_KEY_DEVICES);
+ if (devices != null && !devices.isEmpty()) {
+ for (JsonElement deviceJson : devices) {
+ JsonObject device = deviceJson.getAsJsonObject();
+ String deviceId = Utils.getAsString(device, JSON_KEY_GENERIC_ID, GENERIC_NO_VAL);
+ if (deviceId.equals(getDeviceId())) {
+ return device;
+ }
+
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public MyUplinkCommand buildMyUplinkCommand(Command command, Channel channel) {
+ var deviceId = config.get(MyUplinkBindingConstants.THING_CONFIG_ID).toString();
+ String systemId = "";
+ if (config.containsKey(THING_CONFIG_SYSTEM_ID)) {
+ systemId = config.get(THING_CONFIG_SYSTEM_ID).toString();
+ }
+
+ var channelTypeId = Utils.getChannelTypeId(channel);
+ return switch (channelTypeId) {
+ case CHANNEL_TYPE_RW_COMMAND ->
+ new SetPointsAdvanced(this, channel, command, deviceId, this::updateOnlineStatus);
+ case CHANNEL_TYPE_RW_MODE -> {
+ if (systemId.isBlank()) {
+ throw new UnsupportedOperationException("systemId not configured");
+ }
+ yield new SetSmartHomeMode(this, channel, command, systemId, this::updateOnlineStatus);
+ }
+ default -> new SetPoints(this, channel, command, deviceId, this::updateOnlineStatus);
+ };
+ }
+
+ /**
+ * Start the polling.
+ */
+ private void startPolling() {
+ updateJobReference(dataPollingJobReference, scheduler.scheduleWithFixedDelay(this::pollingRun,
+ POLLING_INITIAL_DELAY, getBridgeConfiguration().getDataPollingInterval(), TimeUnit.SECONDS));
+ }
+
+ /**
+ * Poll the myUplink Cloud API one time.
+ */
+ void pollingRun() {
+ String deviceId = config.get(THING_CONFIG_ID).toString();
+ String systemId = "";
+ if (config.containsKey(THING_CONFIG_SYSTEM_ID)) {
+ systemId = config.get(THING_CONFIG_SYSTEM_ID).toString();
+ }
+ logger.debug("polling device data for {}", deviceId);
+
+ // proceed if device is online
+ if (getThing().getStatus() == ThingStatus.ONLINE) {
+ enqueueCommand(new GetPoints(this, deviceId, this::updateOnlineStatus));
+ if (!systemId.isBlank()) {
+ enqueueCommand(new GetSmartHomeMode(this, systemId, this::updateOnlineStatus));
+ }
+ }
+ enqueueCommand(new GetSystems(this, this::updatePropertiesAndOnlineStatus));
+ }
+
+ /**
+ * result processor to handle online status updates
+ *
+ * @param status of command execution
+ * @param jsonObject json respone result
+ */
+ protected final void updateOnlineStatus(CommunicationStatus status, JsonObject jsonObject) {
+ String msg = Utils.getAsString(jsonObject, JSON_KEY_ERROR);
+ if (msg == null || msg.isBlank()) {
+ msg = status.getMessage();
+ }
+
+ switch (status.getHttpCode()) {
+ case OK:
+ case ACCEPTED:
+ super.updateStatus(ThingStatus.ONLINE);
+ break;
+ case BAD_REQUEST:
+ case UNAUTHORIZED:
+ case FORBIDDEN:
+ super.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
+ break;
+ default:
+ super.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg);
+ }
+ }
+
+ /**
+ * Disposes the thing.
+ */
+ @Override
+ public void dispose() {
+ logger.debug("Handler disposing.");
+ cancelJobReference(dataPollingJobReference);
+ }
+
+ /**
+ * adds a channel.
+ */
+ @Override
+ public void registerChannel(Channel channel) {
+ ThingBuilder thingBuilder = editThing();
+ thingBuilder.withChannel(channel);
+ updateThing(thingBuilder.build());
+ }
+
+ /**
+ * will update all channels provided in the map
+ */
+ @Override
+ public void updateChannelStatus(Map values) {
+ logger.debug("Handling heatpump channel update.");
+
+ for (Channel channel : values.keySet()) {
+ if (getThing().getChannels().contains(channel)) {
+ if (isLinked(channel.getUID())) {
+ State value = values.get(channel);
+ if (value != null) {
+ logger.debug("Channel is to be updated: {}: {}", channel.getUID().getAsString(), value);
+ updateState(channel.getUID(), value);
+ } else {
+ logger.debug("Value is null or not provided by myUplink Cloud (channel: {})",
+ channel.getUID().getAsString());
+ updateState(channel.getUID(), UnDefType.UNDEF);
+ }
+ }
+ } else {
+ logger.debug("Could not identify channel: {} for model {}", channel.getUID().getAsString(),
+ getThing().getThingTypeUID().getAsString());
+ }
+ }
+ }
+
+ @Override
+ public void enqueueCommand(MyUplinkCommand command) {
+ MyUplinkBridgeHandler bridgeHandler = getBridgeHandler();
+ if (bridgeHandler != null) {
+ bridgeHandler.enqueueCommand(command);
+ } else {
+ // this should not happen
+ logger.warn("no bridge handler found");
+ }
+ }
+
+ private @Nullable MyUplinkBridgeHandler getBridgeHandler() {
+ Bridge bridge = getBridge();
+ return bridge != null && bridge.getHandler() instanceof MyUplinkBridgeHandler handler ? handler : null;
+ }
+
+ @Override
+ public MyUplinkConfiguration getBridgeConfiguration() {
+ MyUplinkBridgeHandler bridgeHandler = getBridgeHandler();
+ return bridgeHandler == null ? new MyUplinkConfiguration() : bridgeHandler.getBridgeConfiguration();
+ }
+
+ @Override
+ public Logger getLogger() {
+ return logger;
+ }
+
+ @Override
+ public ThingUID getThingUid() {
+ return getThing().getUID();
+ }
+
+ @Override
+ public ChannelFactory getChannelFactory() {
+ return channelFactory;
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/MyUplinkThingHandler.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/MyUplinkThingHandler.java
new file mode 100644
index 0000000000000..88d0c4ed790fe
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/MyUplinkThingHandler.java
@@ -0,0 +1,146 @@
+/**
+ * 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.myuplink.internal.handler;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.myuplink.internal.Utils;
+import org.openhab.binding.myuplink.internal.command.MyUplinkCommand;
+import org.openhab.binding.myuplink.internal.config.MyUplinkConfiguration;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelGroupUID;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+
+/**
+ * public interface of the {@link MyUplinkThingHandler}. provides some default implementations which can be used by both
+ * BridgeHandlers and ThingHandlers.
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public interface MyUplinkThingHandler extends ThingHandler, ChannelProvider {
+
+ /**
+ * just to avoid usage of static loggers.
+ *
+ * @return
+ */
+ Logger getLogger();
+
+ /**
+ * method which updates the channels. needs to be implemented by thing types which have channels.
+ *
+ * @param values key-value list where key is the channel
+ */
+ default void updateChannelStatus(Map values) {
+ getLogger().debug("updateChannelStatus not implemented/supported by this thing type");
+ }
+
+ /**
+ * return the bridge's configuration
+ *
+ * @return
+ */
+ MyUplinkConfiguration getBridgeConfiguration();
+
+ /**
+ * passes a new command o the command queue
+ *
+ * @param command to be queued/executed
+ */
+ void enqueueCommand(MyUplinkCommand command);
+
+ /**
+ * default implementation to handle commands
+ *
+ * @param channelUID
+ * @param command
+ */
+ @Override
+ default void handleCommand(ChannelUID channelUID, Command command) {
+ getLogger().debug("command for {}: {}", channelUID, command);
+
+ if (command instanceof RefreshType) {
+ return;
+ }
+
+ String group = channelUID.getGroupId();
+ group = group == null ? "" : group;
+ Channel channel = getChannel(group, channelUID.getIdWithoutGroup());
+ if (channel == null) {
+ // this should not happen
+ getLogger().warn("channel not found: {}", channelUID);
+ return;
+ }
+
+ String channelType = Utils.getChannelTypeId(channel);
+ if (!channelType.startsWith(CHANNEL_TYPE_PREFIX_RW)) {
+ getLogger().warn("channel '{}', type '{}' does not support write access - value to set '{}'",
+ channelUID.getIdWithoutGroup(), channelType, command);
+ throw new UnsupportedOperationException(
+ "channel (" + channelUID.getIdWithoutGroup() + ") does not support write access");
+ }
+
+ if (getThing().getStatus() != ThingStatus.ONLINE) {
+ getLogger().debug("Thing is not online, thus no commands will be handled");
+ return;
+ }
+
+ try {
+ enqueueCommand(buildMyUplinkCommand(command, channel));
+ } catch (UnsupportedOperationException e) {
+ getLogger().warn("Unsupported command: {}", e.getMessage());
+ }
+ }
+
+ /**
+ * builds the MyUplinkCommand which can be send to the webinterface.
+ *
+ * @param command the openhab raw command received from the framework
+ * @param channel the channel which belongs to the command.
+ * @return
+ */
+ default MyUplinkCommand buildMyUplinkCommand(Command command, Channel channel) {
+ throw new UnsupportedOperationException("buildMyUplinkCommand not implemented/supported by this thing type");
+ }
+
+ /**
+ * determines the channel for a given groupId and channelId.
+ *
+ * @param groupId groupId of the channel
+ * @param channelId channelId of the channel
+ */
+ @Override
+ default @Nullable Channel getChannel(String groupId, String channelId) {
+ ThingUID thingUID = this.getThing().getUID();
+ ChannelUID channelUID;
+ if (!groupId.isEmpty()) {
+ ChannelGroupUID channelGroupUID = new ChannelGroupUID(thingUID, groupId);
+ channelUID = new ChannelUID(channelGroupUID, channelId);
+ } else {
+ channelUID = new ChannelUID(thingUID, channelId);
+ }
+ return getThing().getChannel(channelUID);
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/StatusHandler.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/StatusHandler.java
new file mode 100644
index 0000000000000..a3841897f4111
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/handler/StatusHandler.java
@@ -0,0 +1,36 @@
+/**
+ * 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.myuplink.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+
+/**
+ * functional interface to provide a function to update status of a thing or bridge.
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@FunctionalInterface
+@NonNullByDefault
+public interface StatusHandler {
+ /**
+ * Called from WebInterface#authenticate() to update
+ * the thing status because updateStatus is protected.
+ *
+ * @param status Thing status
+ * @param statusDetail Thing status detail
+ * @param description Thing status description
+ */
+ void updateStatusInfo(ThingStatus status, ThingStatusDetail statusDetail, String description);
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/model/ConfigurationException.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/model/ConfigurationException.java
new file mode 100644
index 0000000000000..95a0abcf60fb5
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/model/ConfigurationException.java
@@ -0,0 +1,41 @@
+/**
+ * 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.myuplink.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * exception whichs is used to state a validation error
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public class ConfigurationException extends RuntimeException {
+ private static final long serialVersionUID = 5736865225953884234L;
+
+ public ConfigurationException() {
+ super();
+ }
+
+ public ConfigurationException(String message) {
+ super(message);
+ }
+
+ public ConfigurationException(Throwable cause) {
+ super(cause);
+ }
+
+ public ConfigurationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/model/GenericResponseTransformer.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/model/GenericResponseTransformer.java
new file mode 100644
index 0000000000000..c0d6f79f99e67
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/model/GenericResponseTransformer.java
@@ -0,0 +1,132 @@
+/**
+ * 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.myuplink.internal.model;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import java.time.format.DateTimeParseException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.myuplink.internal.Utils;
+import org.openhab.binding.myuplink.internal.handler.ChannelProvider;
+import org.openhab.binding.myuplink.internal.handler.DynamicChannelProvider;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.openhab.core.types.util.UnitUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+/**
+ * transforms the http response into the openhab datamodel (instances of State)
+ * this is a generic trnasformer which tries to map json fields 1:1 to channels.
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public class GenericResponseTransformer implements ResponseTransformer {
+ private final Logger logger = LoggerFactory.getLogger(GenericResponseTransformer.class);
+ private final ChannelProvider channelProvider;
+ private final @Nullable DynamicChannelProvider dynamicChannelProvider;
+
+ public GenericResponseTransformer(ChannelProvider channelProvider) {
+ this.channelProvider = channelProvider;
+ this.dynamicChannelProvider = channelProvider instanceof DynamicChannelProvider
+ ? (DynamicChannelProvider) channelProvider
+ : null;
+ }
+
+ public Map transform(JsonObject jsonData, String group) {
+ Map result = new HashMap<>(20);
+
+ for (JsonElement channelData : Utils.getAsJsonArray(jsonData, JSON_KEY_ROOT_DATA)) {
+
+ logger.debug("received channel data: {}", channelData.toString());
+
+ var value = Utils.getAsBigDecimal(channelData.getAsJsonObject(), JSON_KEY_CHANNEL_VALUE);
+ var unit = UnitUtils.parseUnit(
+ Utils.fixUnit(Utils.getAsString(channelData.getAsJsonObject(), JSON_KEY_CHANNEL_UNIT, "")));
+ var channelId = Utils.getAsString(channelData.getAsJsonObject(), JSON_KEY_CHANNEL_ID, GENERIC_NO_VAL);
+
+ Channel channel;
+ var dcp = dynamicChannelProvider;
+ if (dcp != null) {
+ channel = getOrCreateChannel(dcp, channelId, channelData.getAsJsonObject());
+ } else {
+ channel = channelProvider.getChannel(group, channelId);
+ }
+
+ if (channel == null) {
+ logger.debug("Channel not found: {}#{}, dynamic channels not support by thing.", group, channelId);
+ } else {
+ logger.debug("mapping value '{}' to channel {}", value, channel.getUID().getId());
+
+ if (value == null) {
+ result.put(channel, UnDefType.NULL);
+ } else {
+ try {
+ var channelTypeId = Utils.getChannelTypeId(channel);
+ State newState;
+ if (channelTypeId.equals(CHANNEL_TYPE_RW_SWITCH)) {
+ newState = convertToOnOffType(value.stripTrailingZeros().toString());
+ } else if (channelTypeId.equals(CHANNEL_TYPE_RW_COMMAND)) {
+ newState = new StringType(value.toString());
+ } else if (unit != null) {
+ newState = new QuantityType<>(value, unit);
+ } else {
+ newState = new DecimalType(value.stripTrailingZeros());
+ }
+
+ if (newState == UnDefType.NULL) {
+ logger.warn("no mapping implemented for channel type '{}'", channelTypeId);
+ } else {
+ result.put(channel, newState);
+ }
+ } catch (NumberFormatException | DateTimeParseException ex) {
+ logger.warn("caught exception while parsing data for channel {} (value '{}'). Exception: {}",
+ channel.getUID().getId(), value, ex.getMessage());
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ private Channel getOrCreateChannel(DynamicChannelProvider dcp, String channelId, JsonObject channelData) {
+ var result = channelProvider.getChannel(EMPTY, channelId);
+ if (result == null) {
+ result = dcp.getChannelFactory().createChannel(dcp.getThingUid(), channelData);
+ dcp.registerChannel(result);
+
+ }
+ return result;
+ }
+
+ private OnOffType convertToOnOffType(String value) {
+ return switch (value) {
+ case "1" -> OnOffType.ON;
+ case "1.0" -> OnOffType.ON;
+ default -> OnOffType.OFF;
+ };
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/model/ResponseTransformer.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/model/ResponseTransformer.java
new file mode 100644
index 0000000000000..fecac3eb5c2d2
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/model/ResponseTransformer.java
@@ -0,0 +1,40 @@
+/**
+ * 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.myuplink.internal.model;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.types.State;
+
+import com.google.gson.JsonObject;
+
+/**
+ * transforms the http response into the openhab datamodel (instances of State)
+ * this is an interface which can be implemented by different transformer classes
+ *
+ * @author Anders Alfredsson - initial contribution
+ */
+@NonNullByDefault
+public interface ResponseTransformer {
+
+ /**
+ * Transform the received data into a Map of channels and the State they should be updated to
+ *
+ * @param jsonData The input json data
+ * @param group The channel group
+ * @return
+ */
+ Map transform(JsonObject jsonData, String group);
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/model/SmartHomeModeResponseTransformer.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/model/SmartHomeModeResponseTransformer.java
new file mode 100644
index 0000000000000..10281ec3171f3
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/model/SmartHomeModeResponseTransformer.java
@@ -0,0 +1,66 @@
+/**
+ * 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.myuplink.internal.model;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.myuplink.internal.Utils;
+import org.openhab.binding.myuplink.internal.handler.ChannelProvider;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonObject;
+
+/**
+ * transforms the http response into the openhab datamodel (instances of State)
+ * this is a transformer for the smart home mode data received from the api.
+ *
+ * @author Anders Alfredsson - initial contribution
+ */
+@NonNullByDefault
+public class SmartHomeModeResponseTransformer implements ResponseTransformer {
+ private final Logger logger = LoggerFactory.getLogger(SmartHomeModeResponseTransformer.class);
+ private final ChannelProvider channelProvider;
+
+ public SmartHomeModeResponseTransformer(ChannelProvider channelProvider) {
+ this.channelProvider = channelProvider;
+ }
+
+ public Map transform(JsonObject jsonData, String group) {
+ String mode = Utils.getAsString(jsonData, JSON_KEY_SMART_HOME_MODE);
+ Channel channel = channelProvider.getChannel(group, CHANNEL_ID_SMART_HOME_MODE);
+
+ if (channel == null) {
+ logger.warn(
+ "Smart home mode channel not found. This is likely because of a bug. Please report to the developers.");
+ return Map.of();
+ } else {
+ State newState;
+
+ if (mode == null) {
+ newState = UnDefType.UNDEF;
+ } else {
+ newState = new StringType(mode);
+ }
+
+ return Map.of(channel, newState);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/model/ValidationException.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/model/ValidationException.java
new file mode 100644
index 0000000000000..019f5ccfca835
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/model/ValidationException.java
@@ -0,0 +1,41 @@
+/**
+ * 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.myuplink.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * exception whichs is used to state a validation error
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public class ValidationException extends Exception {
+ private static final long serialVersionUID = -6479556472780307224L;
+
+ public ValidationException() {
+ super();
+ }
+
+ public ValidationException(String message) {
+ super(message);
+ }
+
+ public ValidationException(Throwable cause) {
+ super(cause);
+ }
+
+ public ValidationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/provider/ChannelFactory.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/provider/ChannelFactory.java
new file mode 100644
index 0000000000000..7ec39e78e4c98
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/provider/ChannelFactory.java
@@ -0,0 +1,237 @@
+/**
+ * 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.myuplink.internal.provider;
+
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import javax.measure.Unit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.myuplink.internal.Utils;
+import org.openhab.core.library.CoreItemFactory;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.builder.ChannelBuilder;
+import org.openhab.core.thing.type.ChannelTypeBuilder;
+import org.openhab.core.thing.type.ChannelTypeRegistry;
+import org.openhab.core.thing.type.ChannelTypeUID;
+import org.openhab.core.types.StateDescriptionFragmentBuilder;
+import org.openhab.core.types.StateOption;
+import org.openhab.core.types.util.UnitUtils;
+import org.openhab.core.util.StringUtils;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+
+/**
+ * Factory that contains logic to create dynamic channels.
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@Component(service = { ChannelFactory.class })
+@NonNullByDefault
+public class ChannelFactory {
+ private final Logger logger = LoggerFactory.getLogger(ChannelFactory.class);
+
+ private final MyUplinkChannelTypeProvider channelTypeProvider;
+ private final ChannelTypeRegistry channelTypeRegistry;
+
+ @Activate
+ public ChannelFactory(@Reference MyUplinkChannelTypeProvider channelTypeProvider,
+ @Reference ChannelTypeRegistry channelTypeRegistry) {
+ this.channelTypeProvider = channelTypeProvider;
+ this.channelTypeRegistry = channelTypeRegistry;
+ }
+
+ public Channel createChannel(ThingUID thingUID, JsonObject channelData) {
+ final var channelId = Utils.getAsString(channelData, JSON_KEY_CHANNEL_ID, GENERIC_NO_VAL);
+ final var label = Utils.getAsString(channelData, JSON_KEY_CHANNEL_LABEL, GENERIC_NO_VAL);
+ final var unit = Utils.fixUnit(Utils.getAsString(channelData, JSON_KEY_CHANNEL_UNIT, ""));
+ final var strVal = Utils.getAsString(channelData, JSON_KEY_CHANNEL_STR_VAL, GENERIC_NO_VAL);
+ final var writable = Utils.getAsBool(channelData, JSON_KEY_CHANNEL_WRITABLE, Boolean.FALSE);
+ final var enumValues = Utils.getAsJsonArray(channelData, JSON_KEY_CHANNEL_ENUM_VALUES);
+ final var minValue = Utils.getAsBigDecimal(channelData, JSON_KEY_CHANNEL_MIN);
+ final var maxValue = Utils.getAsBigDecimal(channelData, JSON_KEY_CHANNEL_MAX);
+ final var stepValue = Utils.getAsBigDecimal(channelData, JSON_KEY_CHANNEL_STEP);
+
+ ChannelTypeUID channelTypeUID = null;
+ if (enumValues.isEmpty()) {
+ if (!writable) {
+ channelTypeUID = determineStaticChannelTypeUID(unit, strVal.contains(JSON_VAL_DECIMAL_SEPARATOR));
+ } else {
+ channelTypeUID = getOrBuildNumberChannelType(channelId, unit, minValue, maxValue, stepValue);
+ }
+ } else {
+ channelTypeUID = determineEnumChannelTypeUID(channelId, enumValues, writable);
+ }
+
+ final var channelUID = new ChannelUID(thingUID, channelId);
+ final var acceptedType = determineAcceptedType(channelTypeUID, unit);
+ final var builder = ChannelBuilder.create(channelUID).withLabel(label).withDescription(label)
+ .withType(channelTypeUID).withAcceptedItemType(acceptedType);
+
+ if (writable) {
+ var props = new HashMap();
+ props.put(PARAMETER_NAME_VALIDATION_REGEXP, DEFAULT_VALIDATION_EXPRESSION);
+ builder.withProperties(props);
+ }
+
+ return builder.build();
+ }
+
+ String determineAcceptedType(ChannelTypeUID channelTypeUID, String unit) {
+ if (channelTypeUID.getId().equals(CHANNEL_TYPE_RW_SWITCH)) {
+ return CoreItemFactory.SWITCH;
+ } else if (unit.isEmpty()) {
+ return CoreItemFactory.NUMBER;
+ } else {
+ Unit> parsedUnit = UnitUtils.parseUnit(unit);
+ String dimension = parsedUnit == null ? null : UnitUtils.getDimensionName(parsedUnit);
+
+ if (dimension == null || dimension.isEmpty()) {
+ logger.warn("Could not parse unit: '{}'", unit);
+ return CoreItemFactory.NUMBER;
+ } else {
+ return CoreItemFactory.NUMBER + ":" + dimension;
+ }
+ }
+ }
+
+ private ChannelTypeUID determineStaticChannelTypeUID(String unit, boolean isDouble) {
+ String typeName = switch (unit) {
+ case CHANNEL_TYPE_ENERGY_UNIT -> CHANNEL_TYPE_ENERGY;
+ case CHANNEL_TYPE_PRESSURE_UNIT -> CHANNEL_TYPE_PRESSURE;
+ case CHANNEL_TYPE_PERCENT_UNIT -> CHANNEL_TYPE_PERCENT;
+ case CHANNEL_TYPE_TEMPERATURE_UNIT -> CHANNEL_TYPE_TEMPERATURE;
+ case CHANNEL_TYPE_FREQUENCY_UNIT -> CHANNEL_TYPE_FREQUENCY;
+ case CHANNEL_TYPE_FLOW_UNIT -> CHANNEL_TYPE_FLOW;
+ case CHANNEL_TYPE_ELECTRIC_CURRENT_UNIT -> CHANNEL_TYPE_ELECTRIC_CURRENT;
+ case CHANNEL_TYPE_TIME_UNIT -> CHANNEL_TYPE_TIME;
+ default -> isDouble ? CHANNEL_TYPE_DOUBLE : CHANNEL_TYPE_INTEGER;
+ };
+ return new ChannelTypeUID(BINDING_ID, typeName);
+ }
+
+ private ChannelTypeUID determineEnumChannelTypeUID(String channelId, JsonArray enumValues, boolean writable) {
+ var channelTypeUID = determineStaticEnumType(enumValues, writable);
+ if (channelTypeUID == null) {
+ channelTypeUID = getOrBuildDynamicEnumType(channelId, enumValues, writable);
+ }
+ return channelTypeUID;
+ }
+
+ private ChannelTypeUID getOrBuildDynamicEnumType(String channelId, JsonArray enumValues, boolean writable) {
+ final var prefix = writable ? CHANNEL_TYPE_PREFIX_RW + CHANNEL_TYPE_ENUM_PRFIX : CHANNEL_TYPE_ENUM_PRFIX;
+ final var channelTypeUID = new ChannelTypeUID(BINDING_ID, prefix + channelId);
+ var type = channelTypeRegistry.getChannelType(channelTypeUID);
+
+ if (type == null) {
+ var stateBuilder = StateDescriptionFragmentBuilder.create();
+ stateBuilder.withReadOnly(!writable).withOptions(extractEnumValues(enumValues));
+
+ var typeBuilder = ChannelTypeBuilder.state(channelTypeUID, channelId, CoreItemFactory.NUMBER)
+ .withStateDescriptionFragment(stateBuilder.build());
+
+ type = typeBuilder.build();
+ channelTypeProvider.putChannelType(type);
+ }
+
+ return channelTypeUID;
+ }
+
+ private ChannelTypeUID getOrBuildNumberChannelType(String channelId, String unit, @Nullable BigDecimal min,
+ @Nullable BigDecimal max, @Nullable BigDecimal step) {
+ final var channelTypeUID = new ChannelTypeUID(BINDING_ID,
+ CHANNEL_TYPE_PREFIX_RW + CHANNEL_TYPE_NUMERIC_PRFIX + channelId);
+ var type = channelTypeRegistry.getChannelType(channelTypeUID);
+
+ if (type == null) {
+ var stateBuilder = StateDescriptionFragmentBuilder.create().withReadOnly(false);
+
+ if (min != null) {
+ stateBuilder.withMinimum(min);
+ }
+ if (max != null) {
+ stateBuilder.withMaximum(max);
+ }
+ if (step != null) {
+ stateBuilder.withStep(step);
+ }
+
+ var itemType = determineAcceptedType(channelTypeUID, unit);
+ var typeBuilder = ChannelTypeBuilder.state(channelTypeUID, channelId, itemType)
+ .withStateDescriptionFragment(stateBuilder.build());
+ if (!itemType.equals(CoreItemFactory.NUMBER)) {
+ typeBuilder.withUnitHint(unit);
+ }
+
+ channelTypeProvider.putChannelType(typeBuilder.build());
+ }
+
+ return channelTypeUID;
+ }
+
+ List extractEnumValues(JsonArray enumValues) {
+ List list = new ArrayList<>();
+ for (var element : enumValues) {
+ var enumText = Utils.getAsString(element.getAsJsonObject(), JSON_ENUM_KEY_TEXT, EMPTY);
+ var enumOrdinal = Utils.getAsString(element.getAsJsonObject(), JSON_KEY_CHANNEL_VALUE, GENERIC_NO_VAL);
+ list.add(new StateOption(enumOrdinal, StringUtils.capitalizeByWhitespace(enumText.toLowerCase())));
+ }
+ return list;
+ }
+
+ /**
+ * internal method to dertermine the enum type.
+ *
+ * @param enumValues enum data from myuplink API
+ * @param writable flag to determine writable capability
+ * @return
+ */
+ @Nullable
+ private ChannelTypeUID determineStaticEnumType(JsonArray enumValues, boolean writable) {
+ boolean containsOffAt0 = false;
+ boolean containsOnAt1 = false;
+
+ for (var element : enumValues) {
+ var enumText = Utils.getAsString(element.getAsJsonObject(), JSON_ENUM_KEY_TEXT, "").toLowerCase();
+ var enumOrdinal = Utils.getAsString(element.getAsJsonObject(), JSON_KEY_CHANNEL_VALUE, GENERIC_NO_VAL);
+
+ switch (enumText) {
+ case JSON_ENUM_VAL_OFF -> containsOffAt0 = enumOrdinal.equals(JSON_ENUM_ORD_0);
+ case JSON_ENUM_VAL_ON -> containsOnAt1 = enumOrdinal.equals(JSON_ENUM_ORD_1);
+ }
+ }
+
+ if (enumValues.size() == 2 && containsOffAt0 && containsOnAt1) {
+ if (writable) {
+ return new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_RW_SWITCH);
+ } else {
+ return new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_ON_OFF);
+ }
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/provider/MyUplinkChannelTypeProvider.java b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/provider/MyUplinkChannelTypeProvider.java
new file mode 100644
index 0000000000000..db3604267a754
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/java/org/openhab/binding/myuplink/internal/provider/MyUplinkChannelTypeProvider.java
@@ -0,0 +1,36 @@
+/**
+ * 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.myuplink.internal.provider;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.storage.StorageService;
+import org.openhab.core.thing.binding.AbstractStorageBasedTypeProvider;
+import org.openhab.core.thing.type.ChannelTypeProvider;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * Provides generated channel-types to the framework
+ *
+ * @author Alexander Friese - Initial contribution
+ */
+@Component(service = { ChannelTypeProvider.class, MyUplinkChannelTypeProvider.class })
+@NonNullByDefault
+public class MyUplinkChannelTypeProvider extends AbstractStorageBasedTypeProvider {
+
+ @Activate
+ public MyUplinkChannelTypeProvider(@Reference StorageService storageService) {
+ super(storageService);
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644
index 0000000000000..43e9cbdafdbcf
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/addon/addon.xml
@@ -0,0 +1,9 @@
+
+
+ binding
+ myUplink Binding
+ This is the binding for NIBE myUplink.
+ cloud
+
diff --git a/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/config/config.xml
new file mode 100644
index 0000000000000..cb50a4c74bea1
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/config/config.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+ Authentication
+ Authentication settings.
+
+
+ Connection
+ Connection settings.
+
+
+
+ Username
+ The Client Id to login at myUplink.
+
+
+ Password
+ password
+ The Client Secret to login at myUplink.
+
+
+ Polling Interval
+ Interval in which data is polled from myUplink (in seconds).
+ 60
+
+
+
+
+
+ Device Id
+ The Id to identify the device.
+
+
+ System Id
+ The Id of the system the device belongs to.
+
+
+
+
diff --git a/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/i18n/myuplink.properties b/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/i18n/myuplink.properties
new file mode 100644
index 0000000000000..e1b581d892e52
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/i18n/myuplink.properties
@@ -0,0 +1,80 @@
+# add-on
+
+addon.myuplink.name = myUplink Binding
+addon.myuplink.description = This is the binding for NIBE myUplink.
+
+# thing types
+
+thing-type.myuplink.account.label = myUplink Account
+thing-type.myuplink.account.description = Cloud connection to a myUplink account.
+thing-type.myuplink.generic-device.label = myUplink Generic Device
+thing-type.myuplink.generic-device.description = Cloud connection to a myUplink device.
+thing-type.myuplink.generic-device.channel.command.label = Generic Command
+thing-type.myuplink.generic-device.channel.command.description = Allows to send commands to any channel. Format 'channel:value'
+thing-type.myuplink.generic-device.channel.smart-home-mode.label = Smart Home Mode
+thing-type.myuplink.generic-device.channel.smart-home-mode.description = Controls the smart home mode
+
+# thing types config
+
+thing-type.config.myuplink.account.clientId.label = Username
+thing-type.config.myuplink.account.clientId.description = The Client Id to login at myUplink.
+thing-type.config.myuplink.account.clientSecret.label = Password
+thing-type.config.myuplink.account.clientSecret.description = The Client Secret to login at myUplink.
+thing-type.config.myuplink.account.dataPollingInterval.label = Polling Interval
+thing-type.config.myuplink.account.dataPollingInterval.description = Interval in which data is polled from myUplink (in seconds).
+thing-type.config.myuplink.account.group.authentication.label = Authentication
+thing-type.config.myuplink.account.group.authentication.description = Authentication settings.
+thing-type.config.myuplink.account.group.connection.label = Connection
+thing-type.config.myuplink.account.group.connection.description = Connection settings.
+thing-type.config.myuplink.generic-device.deviceId.label = Device Id
+thing-type.config.myuplink.generic-device.deviceId.description = The Id to identify the device.
+thing-type.config.myuplink.generic-device.systemId.label = System Id
+thing-type.config.myuplink.generic-device.systemId.description = The Id of the system the device belongs to.
+
+# channel types
+
+channel-type.myuplink.rwtype-command.label = Generic Command
+channel-type.myuplink.rwtype-mode.label = Generic Command
+channel-type.myuplink.rwtype-mode.state.option.Default = Default
+channel-type.myuplink.rwtype-mode.state.option.Normal = Normal
+channel-type.myuplink.rwtype-mode.state.option.Away = Away
+channel-type.myuplink.rwtype-mode.state.option.Vacation = Vacation
+channel-type.myuplink.rwtype-mode.state.option.Home = Home
+channel-type.myuplink.rwtype-mode.command.option.Default = Default
+channel-type.myuplink.rwtype-mode.command.option.Normal = Normal
+channel-type.myuplink.rwtype-mode.command.option.Away = Away
+channel-type.myuplink.rwtype-mode.command.option.Vacation = Vacation
+channel-type.myuplink.rwtype-mode.command.option.Home = Home
+channel-type.myuplink.rwtype-switch.label = Generic Switch
+channel-type.myuplink.type-electric-current.label = Generic Current
+channel-type.myuplink.type-energy.label = Generic Energy
+channel-type.myuplink.type-flow.label = Generic Flow
+channel-type.myuplink.type-frequency.label = Generic Frequency
+channel-type.myuplink.type-number-double.label = Generic Number (0.1)
+channel-type.myuplink.type-number-integer.label = Generic Number
+channel-type.myuplink.type-on-off.label = Generic OnOff
+channel-type.myuplink.type-on-off.state.option.0 = Off
+channel-type.myuplink.type-on-off.state.option.1 = On
+channel-type.myuplink.type-percent.label = Generic Percentage
+channel-type.myuplink.type-pressure.label = Generic Pressure
+channel-type.myuplink.type-temperature.label = Generic Temperature
+channel-type.myuplink.type-time.label = Generic Time
+
+# thing types config
+
+thing-type.config.myuplink.account.group.customChannels.label = Custom Channels
+thing-type.config.myuplink.account.group.customChannels.description = Custom Channel configuration
+thing-type.config.myuplink.account.group.general.label = General
+thing-type.config.myuplink.account.group.general.description = General settings.
+
+
+# status translations
+
+status.token.validated = "Access token validated"
+status.waiting.for.bridge = "Waiting for bridge to go online"
+status.waiting.for.login = "Waiting for web api login"
+status.no.valid.data = "No valid data received. This is most likely a configuration error"
+status.no.connection = "No connection could be established"
+status.device.not.found = "Device could not be found. Most likely a configuration error"
+status.config.error.no.client.id = "No/Empty client id. Please fix the configuration."
+status.config.error.no.client.secret = "No/Empty client secret. Please fix the configuration."
diff --git a/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/thing/readonly-channel-types.xml b/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/thing/readonly-channel-types.xml
new file mode 100644
index 0000000000000..0ad15c7512a17
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/thing/readonly-channel-types.xml
@@ -0,0 +1,77 @@
+
+
+
+
+ Number
+ Generic Number
+
+
+
+
+ Number
+ Generic Number (0.1)
+
+
+
+
+ Number:Temperature
+ Generic Temperature
+
+
+
+
+ Number:VolumetricFlowRate
+ Generic Flow
+
+
+
+
+ Number:ElectricCurrent
+ Generic Current
+
+
+
+
+ Number:Time
+ Generic Time
+
+
+
+
+ Number:Frequency
+ Generic Frequency
+
+
+
+
+ Number:Energy
+ Generic Energy
+
+
+
+
+ Number:Pressure
+ Generic Pressure
+
+
+
+
+ Number:Dimensionless
+ Generic Percentage
+
+
+
+
+ Number
+ Generic OnOff
+
+
+ Off
+ On
+
+
+
+
diff --git a/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/thing/readwrite-channel-types.xml b/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/thing/readwrite-channel-types.xml
new file mode 100644
index 0000000000000..df46da34c0924
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/thing/readwrite-channel-types.xml
@@ -0,0 +1,39 @@
+
+
+
+
+ Switch
+ Generic Switch
+
+
+
+ String
+ Generic Command
+
+
+
+ String
+ Generic Command
+
+
+ Default
+ Normal
+ Away
+ Vacation
+ Home
+
+
+
+
+ Default
+ Normal
+ Away
+ Vacation
+ Home
+
+
+
+
diff --git a/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 0000000000000..796d2bafc251e
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,35 @@
+
+
+
+ myUplink Account
+ Cloud connection to a myUplink account.
+
+
+
+
+
+
+ myUplink Generic Device
+ Cloud connection to a myUplink device.
+
+
+ Generic Command
+ Allows to send commands to any channel. Format 'channel:value'
+
+ [0-9]+:-?[0-9,]+
+
+
+
+ Smart Home Mode
+ Controls the smart home mode
+
+ \w+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.myuplink/src/test/java/org/openhab/binding/myuplink/internal/discovery/MyUplinkDiscoveryServiceTest.java b/bundles/org.openhab.binding.myuplink/src/test/java/org/openhab/binding/myuplink/internal/discovery/MyUplinkDiscoveryServiceTest.java
new file mode 100644
index 0000000000000..761eb5ff29836
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/test/java/org/openhab/binding/myuplink/internal/discovery/MyUplinkDiscoveryServiceTest.java
@@ -0,0 +1,135 @@
+/**
+ * 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.myuplink.internal.discovery;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.openhab.binding.myuplink.internal.connector.CommunicationStatus;
+import org.openhab.binding.myuplink.internal.handler.MyUplinkAccountHandler;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ThingUID;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+/**
+ * Unit Tests to verify behaviour of DiscoveryService implementation.
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+@ExtendWith(MockitoExtension.class)
+public class MyUplinkDiscoveryServiceTest {
+
+ private MyUplinkAccountHandler bridgeHandler = mock(MyUplinkAccountHandler.class);
+
+ private CommunicationStatus communicationStatus = mock(CommunicationStatus.class);
+
+ private MyUplinkDiscoveryService discoveryService = spy(MyUplinkDiscoveryService.class);
+
+ private final String emptyResponseString = """
+ {"page":1,"itemsPerPage":100,"numItems":0,"systems":[]}
+ """;
+
+ private JsonObject emptyResponse = new JsonObject();
+
+ private final String testResponseString = """
+ {
+ "page": 0,
+ "itemsPerPage": 0,
+ "numItems": 0,
+ "systems": [
+ {
+ "systemId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "name": "string",
+ "securityLevel": "admin",
+ "hasAlarm": true,
+ "country": "string",
+ "devices": [
+ {
+ "id": "Dev-1337",
+ "connectionState": "Disconnected",
+ "currentFwVersion": "string",
+ "product": {
+ "serialNumber": "1337",
+ "name": "My Device 1337"
+ }
+ },
+ {
+ "id": "Dev-4712",
+ "connectionState": "Disconnected",
+ "currentFwVersion": "string",
+ "product": {
+ "serialNumber": "4712",
+ "name": "My Device 4712"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ """;
+
+ private static JsonObject testResponse = new JsonObject();
+
+ @BeforeEach
+ public void prepareTestData() {
+ emptyResponse = JsonParser.parseString(emptyResponseString).getAsJsonObject();
+ testResponse = JsonParser.parseString(testResponseString).getAsJsonObject();
+
+ discoveryService.setThingHandler(bridgeHandler);
+ }
+
+ @Test
+ public void testEmptyResponse() {
+ discoveryService.processMyUplinkDiscoveryResult(communicationStatus, emptyResponse);
+
+ // testdata contains no systems -> no further processing
+ verify(discoveryService, never()).handleSystemDiscovery(any());
+ verify(discoveryService, never()).handleDeviceDiscovery(any(), any());
+ verify(discoveryService, never()).initDiscoveryResultBuilder(any(), any(), any());
+ }
+
+ @Test
+ public void testSampleResponse() {
+ // mocking of bridgehandler needed to get an UID.
+ Bridge mockThing = mock(Bridge.class);
+ when(mockThing.getUID()).thenReturn(new ThingUID(THING_TYPE_ACCOUNT, "testAccount4711"));
+ when(bridgeHandler.getThing()).thenReturn(mockThing);
+
+ discoveryService.processMyUplinkDiscoveryResult(communicationStatus, testResponse);
+
+ // testdata contains one system
+ verify(discoveryService, times(1)).handleSystemDiscovery(any());
+ // testdata contains two devices
+ verify(discoveryService, times(2)).handleDeviceDiscovery(any(), any());
+ // builder should be called once for each device
+ verify(discoveryService, times(2)).initDiscoveryResultBuilder(any(), any(), any());
+
+ // verify that correct values are extracted from data
+ verify(discoveryService).initDiscoveryResultBuilder(DEVICE_GENERIC_DEVICE, "Dev-4712", "My Device 4712");
+ verify(discoveryService).initDiscoveryResultBuilder(DEVICE_GENERIC_DEVICE, "Dev-1337", "My Device 1337");
+ }
+}
diff --git a/bundles/org.openhab.binding.myuplink/src/test/java/org/openhab/binding/myuplink/internal/provider/ChannelFactoryTest.java b/bundles/org.openhab.binding.myuplink/src/test/java/org/openhab/binding/myuplink/internal/provider/ChannelFactoryTest.java
new file mode 100644
index 0000000000000..f6d6a5faa47ae
--- /dev/null
+++ b/bundles/org.openhab.binding.myuplink/src/test/java/org/openhab/binding/myuplink/internal/provider/ChannelFactoryTest.java
@@ -0,0 +1,205 @@
+/**
+ * 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.myuplink.internal.provider;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.openhab.binding.myuplink.internal.MyUplinkBindingConstants.*;
+
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.myuplink.internal.MyUplinkBindingConstants;
+import org.openhab.binding.myuplink.internal.Utils;
+import org.openhab.core.library.CoreItemFactory;
+import org.openhab.core.test.storage.VolatileStorageService;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.type.ChannelTypeRegistry;
+import org.openhab.core.thing.type.ChannelTypeUID;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+/**
+ * Unit Tests to verify behaviour of ChannelFactory implementation.
+ *
+ * @author Alexander Friese - initial contribution
+ */
+@NonNullByDefault
+public class ChannelFactoryTest {
+
+ private final MyUplinkChannelTypeProvider channelTypeProvider = new MyUplinkChannelTypeProvider(
+ new VolatileStorageService());
+ private final ChannelFactory channelFactory = new ChannelFactory(channelTypeProvider, new ChannelTypeRegistry());
+
+ private static final ThingUID TEST_THING_UID = new ThingUID(MyUplinkBindingConstants.BINDING_ID, "genericThing",
+ "myUnit");
+
+ private final String testChannelDataTemperature = """
+ {"category":"NIBEF VVM 320 E","parameterId":"40121","parameterName":"Add. heat (BT63)","parameterUnit":"°C","writable":false,"timestamp":"2024-05-10T05:35:50+00:00","value":39.0,"strVal":"39°C","smartHomeCategories":[],"minValue":null,"maxValue":null,"stepValue":1.0,"enumValues":[],"scaleValue":"0.1","zoneId":null}
+ """;
+
+ private final String testChannelEnumWritableSwitch = """
+ {"category":"NIBEF VVM 320 E","parameterId":"50004","parameterName":"Temporary lux","parameterUnit":"","writable":true,"timestamp":"2024-05-05T13:41:09+00:00","value":0.0,"strVal":"off","smartHomeCategories":[],"minValue":null,"maxValue":null,"stepValue":1.0,"enumValues":[{"value":"0","text":"off","icon":""},{"value":"1","text":"on","icon":""}],"scaleValue":"1","zoneId":null}
+ """;
+
+ private final String testChannelEnumSwitch = """
+ {"category":"NIBEF VVM 320 E","parameterId":"49992","parameterName":"Pump: Heating medium (GP6)","parameterUnit":"","writable":false,"timestamp":"2024-05-05T13:41:09+00:00","value":0.0,"strVal":"Off","smartHomeCategories":[],"minValue":null,"maxValue":null,"stepValue":1.0,"enumValues":[{"value":"0","text":"Off","icon":""},{"value":"1","text":"On","icon":""}],"scaleValue":"1","zoneId":null}
+ """;
+
+ private final String testChannelEnumPriority = """
+ {"category":"NIBEF VVM 320 E","parameterId":"49994","parameterName":"Priority","parameterUnit":"","writable":false,"timestamp":"2024-05-10T03:31:23+00:00","value":10.0,"strVal":"Off","smartHomeCategories":[],"minValue":null,"maxValue":null,"stepValue":1.0,"enumValues":[{"value":"10","text":"Off","icon":""},{"value":"20","text":"Hot water","icon":""},{"value":"30","text":"Heating","icon":""},{"value":"40","text":"Pool","icon":""},{"value":"41","text":"Pool 2","icon":""},{"value":"50","text":"TransÂfer","icon":""},{"value":"60","text":"Cooling","icon":""}],"scaleValue":"1","zoneId":null}
+ """;
+
+ private final String testChannelEnumCompressorStatus = """
+ {"category":"Slave 1 (EB101)","parameterId":"44064","parameterName":"Status compressor (EB101)","parameterUnit":"","writable":false,"timestamp":"2024-05-10T03:31:28+00:00","value":20.0,"strVal":"off","smartHomeCategories":[],"minValue":null,"maxValue":null,"stepValue":1.0,"enumValues":[{"value":"20","text":"off","icon":""},{"value":"40","text":"starts","icon":""},{"value":"60","text":"runs","icon":""},{"value":"100","text":"stops","icon":""}],"scaleValue":"1","zoneId":null}
+ """;
+
+ private final String testChannelEnumAddHeatStatus = """
+ {"category":"NIBEF VVM 320 E","parameterId":"49993","parameterName":"Int elec add heat","parameterUnit":"","writable":false,"timestamp":"2024-05-05T13:41:27+00:00","value":4.0,"strVal":"Blocked","smartHomeCategories":[],"minValue":null,"maxValue":null,"stepValue":1.0,"enumValues":[{"value":"0","text":"Alarm","icon":""},{"value":"1","text":"Alarm","icon":""},{"value":"2","text":"Active","icon":""},{"value":"3","text":"Off","icon":""},{"value":"4","text":"Blocked","icon":""},{"value":"5","text":"Off","icon":""},{"value":"6","text":"Active","icon":""}],"scaleValue":"1","zoneId":null}
+ """;
+
+ private final String testChannelEnumHeatPumpStatusWithLowerCaseTexts = """
+ { "category": "Heat pump 1", "parameterId": "62017", "parameterName": "Status", "parameterUnit": "", "writable": false, "timestamp": "2024-05-21T16:22:21+00:00", "value": 1, "strVal": "Off, ready to start", "smartHomeCategories": [], "minValue": null, "maxValue": null, "stepValue": 1, "enumValues": [ { "value": "0", "text": "Off, start delay", "icon": "" }, { "value": "1", "text": "OFF, ready to start", "icon": "" }, { "value": "2", "text": "Wait until flow", "icon": "" }, { "value": "3", "text": "On", "icon": "" }, { "value": "4", "text": "Defrost", "icon": "" }, { "value": "5", "text": "Cooling", "icon": "" }, { "value": "6", "text": "Blocked", "icon": "" }, { "value": "7", "text": "Off, alarm", "icon": "" }, { "value": "8", "text": "Function test", "icon": "" }, { "value": "30", "text": "not defined", "icon": "" }, { "value": "31", "text": "Comp. disabled", "icon": "" }, { "value": "32", "text": "Comm. error", "icon": "" }, { "value": "33", "text": "Hot Water", "icon": "" } ], "scaleValue": "1", "zoneId": null }
+ """;
+
+ @Test
+ public void testFromJsonDataTemperature() {
+ var gson = new Gson();
+ var json = gson.fromJson(testChannelDataTemperature, JsonObject.class);
+ json = json == null ? new JsonObject() : json;
+
+ var result = channelFactory.createChannel(TEST_THING_UID, json);
+ assertThat(result.getAcceptedItemType(), is("Number:Temperature"));
+ assertThat(Objects.requireNonNull(result.getChannelTypeUID()).getId(), is("type-temperature"));
+ assertThat(result.getUID().getThingUID(), is(TEST_THING_UID));
+ assertThat(result.getUID().getId(), is("40121"));
+ assertThat(result.getDescription(), is("Add. heat (BT63)"));
+ assertThat(result.getLabel(), is("Add. heat (BT63)"));
+ }
+
+ @Test
+ public void testFromJsonDataEnumWritableSwitch() {
+ var gson = new Gson();
+ var json = gson.fromJson(testChannelEnumWritableSwitch, JsonObject.class);
+ json = json == null ? new JsonObject() : json;
+
+ var result = channelFactory.createChannel(TEST_THING_UID, json);
+ assertThat(result.getAcceptedItemType(), is(CoreItemFactory.SWITCH));
+ assertThat(Objects.requireNonNull(result.getChannelTypeUID()).getId(), is("rwtype-switch"));
+ assertThat(result.getUID().getThingUID(), is(TEST_THING_UID));
+ assertThat(result.getUID().getId(), is("50004"));
+ assertThat(result.getDescription(), is("Temporary lux"));
+ assertThat(result.getLabel(), is("Temporary lux"));
+ }
+
+ @Test
+ public void testFromJsonDataEnumSwitch() {
+ var gson = new Gson();
+ var json = gson.fromJson(testChannelEnumSwitch, JsonObject.class);
+ json = json == null ? new JsonObject() : json;
+
+ var result = channelFactory.createChannel(TEST_THING_UID, json);
+ assertThat(result.getAcceptedItemType(), is(CoreItemFactory.NUMBER));
+ assertThat(Objects.requireNonNull(result.getChannelTypeUID()).getId(), is("type-on-off"));
+ assertThat(result.getUID().getThingUID(), is(TEST_THING_UID));
+ assertThat(result.getUID().getId(), is("49992"));
+ assertThat(result.getDescription(), is("Pump: Heating medium (GP6)"));
+ assertThat(result.getLabel(), is("Pump: Heating medium (GP6)"));
+ }
+
+ @Test
+ public void testFromJsonDataEnumPriority() {
+ var gson = new Gson();
+ var json = gson.fromJson(testChannelEnumPriority, JsonObject.class);
+ json = json == null ? new JsonObject() : json;
+
+ var result = channelFactory.createChannel(TEST_THING_UID, json);
+ assertThat(result.getAcceptedItemType(), is(CoreItemFactory.NUMBER));
+ assertThat(Objects.requireNonNull(result.getChannelTypeUID()).getId(), is("type-enum-49994"));
+ }
+
+ @Test
+ public void testFromJsonDataEnumCompressorStatus() {
+ var gson = new Gson();
+ var json = gson.fromJson(testChannelEnumCompressorStatus, JsonObject.class);
+ json = json == null ? new JsonObject() : json;
+
+ var result = channelFactory.createChannel(TEST_THING_UID, json);
+ assertThat(result.getAcceptedItemType(), is(CoreItemFactory.NUMBER));
+ assertThat(Objects.requireNonNull(result.getChannelTypeUID()).getId(), is("type-enum-44064"));
+ }
+
+ @Test
+ public void testFromJsonDataEnumAddHeatStatus() {
+ var gson = new Gson();
+ var json = gson.fromJson(testChannelEnumAddHeatStatus, JsonObject.class);
+ json = json == null ? new JsonObject() : json;
+
+ var result = channelFactory.createChannel(TEST_THING_UID, json);
+ assertThat(result.getAcceptedItemType(), is(CoreItemFactory.NUMBER));
+ assertThat(Objects.requireNonNull(result.getChannelTypeUID()).getId(), is("type-enum-49993"));
+
+ var type = channelTypeProvider.getChannelType(new ChannelTypeUID(BINDING_ID, "type-enum-49993"), null);
+ assertNotNull(type);
+ assertThat(Objects.requireNonNull(type.getState()).getOptions().size(), is(7));
+ }
+
+ @Test
+ public void testFromJsonDataEnumHeatPumpStatus() {
+ var gson = new Gson();
+ var json = gson.fromJson(testChannelEnumHeatPumpStatusWithLowerCaseTexts, JsonObject.class);
+ json = json == null ? new JsonObject() : json;
+
+ var result = channelFactory.createChannel(TEST_THING_UID, json);
+ assertThat(result.getAcceptedItemType(), is(CoreItemFactory.NUMBER));
+ assertThat(Objects.requireNonNull(result.getChannelTypeUID()).getId(), is("type-enum-62017"));
+
+ var type = channelTypeProvider.getChannelType(new ChannelTypeUID(BINDING_ID, "type-enum-62017"), null);
+ assertNotNull(type);
+ assertThat(Objects.requireNonNull(type.getState()).getOptions().size(), is(13));
+ }
+
+ @Test
+ public void testHeatPumpStatusEnumValues() {
+ var gson = new Gson();
+ var json = gson.fromJson(testChannelEnumHeatPumpStatusWithLowerCaseTexts, JsonObject.class);
+ json = json == null ? new JsonObject() : json;
+
+ var enumValues = Utils.getAsJsonArray(json, JSON_KEY_CHANNEL_ENUM_VALUES);
+ var list = channelFactory.extractEnumValues(enumValues);
+
+ assertThat(list.size(), is(13));
+
+ list.forEach(enumMapping -> {
+ switch (enumMapping.getValue()) {
+ case "0" -> assertThat(enumMapping.getLabel(), is("Off, Start Delay"));
+ case "1" -> assertThat(enumMapping.getLabel(), is("Off, Ready To Start"));
+ case "2" -> assertThat(enumMapping.getLabel(), is("Wait Until Flow"));
+ case "3" -> assertThat(enumMapping.getLabel(), is("On"));
+ case "4" -> assertThat(enumMapping.getLabel(), is("Defrost"));
+ case "5" -> assertThat(enumMapping.getLabel(), is("Cooling"));
+ case "6" -> assertThat(enumMapping.getLabel(), is("Blocked"));
+ case "7" -> assertThat(enumMapping.getLabel(), is("Off, Alarm"));
+ case "8" -> assertThat(enumMapping.getLabel(), is("Function Test"));
+ case "30" -> assertThat(enumMapping.getLabel(), is("Not Defined"));
+ case "31" -> assertThat(enumMapping.getLabel(), is("Comp. Disabled"));
+ case "32" -> assertThat(enumMapping.getLabel(), is("Comm. Error"));
+ case "33" -> assertThat(enumMapping.getLabel(), is("Hot Water"));
+ default -> assertNotNull(null, "unknown enum value");
+ }
+ });
+ }
+}
diff --git a/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/i18n/netatmo_fr.properties b/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/i18n/netatmo_fr.properties
index d86af4d587e4a..3e1226872d617 100644
--- a/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/i18n/netatmo_fr.properties
+++ b/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/i18n/netatmo_fr.properties
@@ -16,7 +16,7 @@ channel-group-type.netatmo.battery-extended.label = Batterie
channel-group-type.netatmo.battery.label = Batterie
channel-group-type.netatmo.energy.label = Énergie de la maison
channel-group-type.netatmo.energy.channel.end.label = Fin Mode
-channel-group-type.netatmo.energy.channel.end.description = Heure de fin de la consigne actuellement appliquée.
+channel-group-type.netatmo.energy.channel.end.description = Heure de fin du mode de chauffage actuellement appliqué.
channel-group-type.netatmo.humidity.label = Humidité
channel-group-type.netatmo.last-event-alarm.label = Dernier événement
channel-group-type.netatmo.last-event-alarm.channel.time.label = Horodatage Événement
@@ -465,6 +465,7 @@ device-not-connected = L'objet n'est pas accessible
data-over-limit = Les données semblent assez anciennes
request-time-out = La requête a expiré - va essayer de se reconnecter plus tard
deserialization-unknown = La désérialisation renvoie un code inconnu
+maximum-usage-reached = Utilisation maximale atteinte. Nouvel essai de reconnexion dans `reconnectInterval` secondes.
homestatus-unknown-error = Erreur inconnue
homestatus-internal-error = Erreur interne
diff --git a/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/thing/channels.xml
index 957b374150752..7744fc4403bca 100644
--- a/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/thing/channels.xml
+++ b/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/thing/channels.xml
@@ -507,7 +507,7 @@
diff --git a/bundles/org.openhab.binding.surepetcare/src/main/resources/OH-INF/update/update.xml b/bundles/org.openhab.binding.surepetcare/src/main/resources/OH-INF/update/update.xml
new file mode 100644
index 0000000000000..40ededf7cd2db
--- /dev/null
+++ b/bundles/org.openhab.binding.surepetcare/src/main/resources/OH-INF/update/update.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.tasmotaplug/src/main/resources/OH-INF/i18n/tasmotaplug_fr.properties b/bundles/org.openhab.binding.tasmotaplug/src/main/resources/OH-INF/i18n/tasmotaplug_fr.properties
index cb1d2ba0ed206..23b2b6d5ddc5d 100644
--- a/bundles/org.openhab.binding.tasmotaplug/src/main/resources/OH-INF/i18n/tasmotaplug_fr.properties
+++ b/bundles/org.openhab.binding.tasmotaplug/src/main/resources/OH-INF/i18n/tasmotaplug_fr.properties
@@ -7,6 +7,12 @@ addon.tasmotaplug.description = Contrôle les prises intelligentes Wi-Fi flashé
thing-type.tasmotaplug.plug.label = Prise
thing-type.tasmotaplug.plug.description = Prise intelligente Tasmota
+thing-type.tasmotaplug.plug.channel.energy-today.label = Énergie Aujourd'hui
+thing-type.tasmotaplug.plug.channel.energy-today.description = Énergie consommée aujourd'hui (kWh)
+thing-type.tasmotaplug.plug.channel.energy-total.label = Énergie Totale
+thing-type.tasmotaplug.plug.channel.energy-total.description = Énergie totale consommée (kWh)
+thing-type.tasmotaplug.plug.channel.energy-yesterday.label = Énergie Hier
+thing-type.tasmotaplug.plug.channel.energy-yesterday.description = Énergie consommée hier (kWh)
thing-type.tasmotaplug.plug.channel.power.label = Alimentation
thing-type.tasmotaplug.plug.channel.power.description = Contrôle le relais de prise intelligente pour le 1er canal
thing-type.tasmotaplug.plug.channel.power2.label = Alimentation 2
@@ -15,6 +21,8 @@ thing-type.tasmotaplug.plug.channel.power3.label = Alimentation 3
thing-type.tasmotaplug.plug.channel.power3.description = Contrôle le relais de prise intelligente pour le 3ème canal
thing-type.tasmotaplug.plug.channel.power4.label = Alimentation 4
thing-type.tasmotaplug.plug.channel.power4.description = Contrôle le relais de prise intelligente pour le 4ème canal
+thing-type.tasmotaplug.plug.channel.watts.label = Puissance Active
+thing-type.tasmotaplug.plug.channel.watts.description = Puissance active (W)
# thing types config
@@ -29,6 +37,17 @@ thing-type.config.tasmotaplug.plug.refresh.description = Définit l'intervalle d
thing-type.config.tasmotaplug.plug.username.label = Nom d'utilisateur
thing-type.config.tasmotaplug.plug.username.description = Nom d'utilisateur Tasmota
+# channel types
+
+channel-type.tasmotaplug.energy-total-start.label = Début Énergie Totale
+channel-type.tasmotaplug.energy-total-start.description = La date/heure où la mesure totale de l'énergie a commencé
+channel-type.tasmotaplug.power-factor.label = Facteur Puissance
+channel-type.tasmotaplug.power-factor.description = Facteur de puissance (PF)
+channel-type.tasmotaplug.volt-ampere-reactive.label = Puissance Réactive
+channel-type.tasmotaplug.volt-ampere-reactive.description = Puissance réactive (VAr)
+channel-type.tasmotaplug.volt-ampere.label = Puissance Apparente
+channel-type.tasmotaplug.volt-ampere.description = Puissance apparente (VA)
+
# thing status descriptions
offline.communication-error.http-failure = Le code de réponse http de Tasmota était \: {0}
diff --git a/bundles/org.openhab.binding.teleinfo/src/main/java/org/openhab/binding/teleinfo/internal/serial/TeleinfoReceiveThread.java b/bundles/org.openhab.binding.teleinfo/src/main/java/org/openhab/binding/teleinfo/internal/serial/TeleinfoReceiveThread.java
index 26801ecf19454..4565b9dbbfa6f 100644
--- a/bundles/org.openhab.binding.teleinfo/src/main/java/org/openhab/binding/teleinfo/internal/serial/TeleinfoReceiveThread.java
+++ b/bundles/org.openhab.binding.teleinfo/src/main/java/org/openhab/binding/teleinfo/internal/serial/TeleinfoReceiveThread.java
@@ -41,7 +41,7 @@ public class TeleinfoReceiveThread extends Thread {
public TeleinfoReceiveThread(SerialPort serialPort, final TeleinfoSerialControllerHandler listener,
boolean autoRepairInvalidADPSgroupLine, TeleinfoTicMode ticMode, boolean verifyChecksum) {
- super("OH-binding-TeleinfoReceiveThread-" + listener.getThing().getUID().getId());
+ super("OH-binding-" + listener.getThing().getUID());
setDaemon(true);
this.serialPort = serialPort;
this.listener = listener;
diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/core/TelldusCoreBridgeHandler.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/core/TelldusCoreBridgeHandler.java
index f5b90334de079..97b3f701be171 100644
--- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/core/TelldusCoreBridgeHandler.java
+++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/core/TelldusCoreBridgeHandler.java
@@ -129,7 +129,8 @@ public void initialize() {
}
private void setupDeviceController(TellstickBridgeConfiguration configuration) {
- deviceController = new TelldusCoreDeviceController(configuration.resendInterval);
+ deviceController = new TelldusCoreDeviceController(configuration.resendInterval,
+ "OH-binding-" + getThing().getUID() + "-worker");
eventHandler.addListener((TelldusCoreDeviceController) deviceController);
}
diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/core/TelldusCoreDeviceController.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/core/TelldusCoreDeviceController.java
index ecff75e061d9b..a37e21352a248 100644
--- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/core/TelldusCoreDeviceController.java
+++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/core/TelldusCoreDeviceController.java
@@ -55,11 +55,11 @@ public class TelldusCoreDeviceController implements DeviceChangeListener, Sensor
private Thread workerThread;
private SortedMap messageQue;
- public TelldusCoreDeviceController(long resendInterval) {
+ public TelldusCoreDeviceController(long resendInterval, final String threadName) {
this.resendInterval = resendInterval;
messageQue = Collections.synchronizedSortedMap(new TreeMap<>());
telldusCoreWorker = new TelldusCoreWorker(messageQue);
- workerThread = new Thread(telldusCoreWorker);
+ workerThread = new Thread(telldusCoreWorker, threadName);
}
@Override
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaChannelSelectorProxy.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaChannelSelectorProxy.java
index 8bcef86df0539..aab9c92498ca8 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaChannelSelectorProxy.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaChannelSelectorProxy.java
@@ -19,6 +19,8 @@
import java.util.Date;
import java.util.Map;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
@@ -33,6 +35,7 @@
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.State;
import org.openhab.core.types.Type;
+import org.openhab.core.types.UnDefType;
/**
* The {@link TeslaChannelSelectorProxy} class is a helper class to instantiate
@@ -40,6 +43,7 @@
*
* @author Karel Goderis - Initial contribution
*/
+@NonNullByDefault
public class TeslaChannelSelectorProxy {
public enum TeslaChannelSelector {
@@ -939,11 +943,11 @@ public State getState(String s, TeslaChannelSelectorProxy proxy, Map properties) {
State someState = super.getState(s);
- if (someState != null) {
+ if (someState != UnDefType.UNDEF) {
BigDecimal value = ((DecimalType) someState).toBigDecimal();
return new QuantityType<>(value, ImperialUnits.MILES_PER_HOUR);
} else {
- return null;
+ return UnDefType.UNDEF;
}
}
},
@@ -1062,12 +1066,12 @@ public State getState(String s, TeslaChannelSelectorProxy proxy, Map typeClass;
private final boolean isProperty;
- private TeslaChannelSelector(String restID, String channelID, Class extends Type> typeClass,
+ private TeslaChannelSelector(@Nullable String restID, String channelID, Class extends Type> typeClass,
boolean isProperty) {
this.restID = restID;
this.channelID = channelID;
@@ -1077,7 +1081,8 @@ private TeslaChannelSelector(String restID, String channelID, Class extends Ty
@Override
public String toString() {
- return restID;
+ String restID = this.restID;
+ return restID != null ? restID : "null";
}
public String getChannelID() {
@@ -1107,7 +1112,7 @@ public State getState(String s) {
| InvocationTargetException e) {
}
- return null;
+ return UnDefType.UNDEF;
}
public static TeslaChannelSelector getValueSelectorFromChannelID(String valueSelectorText)
@@ -1124,7 +1129,8 @@ public static TeslaChannelSelector getValueSelectorFromChannelID(String valueSel
public static TeslaChannelSelector getValueSelectorFromRESTID(String valueSelectorText)
throws IllegalArgumentException {
for (TeslaChannelSelector c : TeslaChannelSelector.values()) {
- if (c.restID != null && c.restID.equals(valueSelectorText)) {
+ String restID = c.restID;
+ if (restID != null && restID.equals(valueSelectorText)) {
return c;
}
}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/discovery/TeslaVehicleDiscoveryService.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/discovery/TeslaVehicleDiscoveryService.java
index 74c10193e40e4..bd07045388759 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/discovery/TeslaVehicleDiscoveryService.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/discovery/TeslaVehicleDiscoveryService.java
@@ -12,13 +12,14 @@
*/
package org.openhab.binding.tesla.internal.discovery;
-import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tesla.internal.TeslaBindingConstants;
import org.openhab.binding.tesla.internal.TeslaHandlerFactory;
import org.openhab.binding.tesla.internal.handler.TeslaAccountHandler;
import org.openhab.binding.tesla.internal.handler.VehicleListener;
-import org.openhab.binding.tesla.internal.protocol.Vehicle;
-import org.openhab.binding.tesla.internal.protocol.VehicleConfig;
+import org.openhab.binding.tesla.internal.protocol.dto.Vehicle;
+import org.openhab.binding.tesla.internal.protocol.dto.VehicleConfig;
import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
@@ -36,8 +37,9 @@
* @author Kai Kreuzer - Initial contribution
*
*/
+@NonNullByDefault
@Component(scope = ServiceScope.PROTOTYPE, service = TeslaVehicleDiscoveryService.class)
-public class TeslaVehicleDiscoveryService extends AbstractThingHandlerDiscoveryService<@NonNull TeslaAccountHandler>
+public class TeslaVehicleDiscoveryService extends AbstractThingHandlerDiscoveryService
implements VehicleListener {
private final Logger logger = LoggerFactory.getLogger(TeslaVehicleDiscoveryService.class);
@@ -63,13 +65,13 @@ public void dispose() {
}
@Override
- public void vehicleFound(Vehicle vehicle, VehicleConfig vehicleConfig) {
+ public void vehicleFound(Vehicle vehicle, @Nullable VehicleConfig vehicleConfig) {
ThingTypeUID type = vehicleConfig == null ? TeslaBindingConstants.THING_TYPE_VEHICLE
: vehicleConfig.identifyModel();
if (type != null) {
logger.debug("Found a {} vehicle", type.getId());
ThingUID thingUID = new ThingUID(type, thingHandler.getThing().getUID(), vehicle.vin);
- DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withLabel(vehicle.display_name)
+ DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withLabel(vehicle.displayName)
.withBridge(thingHandler.getThing().getUID()).withProperty(TeslaBindingConstants.VIN, vehicle.vin)
.build();
thingDiscovered(discoveryResult);
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java
index 59e92f8c61cf8..898339f9940a6 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java
@@ -31,12 +31,14 @@
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tesla.internal.TeslaBindingConstants;
import org.openhab.binding.tesla.internal.discovery.TeslaVehicleDiscoveryService;
-import org.openhab.binding.tesla.internal.protocol.Vehicle;
-import org.openhab.binding.tesla.internal.protocol.VehicleConfig;
-import org.openhab.binding.tesla.internal.protocol.VehicleData;
-import org.openhab.binding.tesla.internal.protocol.sso.TokenResponse;
+import org.openhab.binding.tesla.internal.protocol.dto.Vehicle;
+import org.openhab.binding.tesla.internal.protocol.dto.VehicleConfig;
+import org.openhab.binding.tesla.internal.protocol.dto.VehicleData;
+import org.openhab.binding.tesla.internal.protocol.dto.sso.TokenResponse;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
@@ -63,6 +65,7 @@
* @author Nicolai Grødum - Adding token based auth
* @author Kai Kreuzer - refactored to use separate vehicle handlers
*/
+@NonNullByDefault
public class TeslaAccountHandler extends BaseBridgeHandler {
public static final int API_MAXIMUM_ERRORS_IN_INTERVAL = 3;
@@ -86,6 +89,7 @@ public class TeslaAccountHandler extends BaseBridgeHandler {
private final ThingTypeMigrationService thingTypeMigrationService;
// Threading and Job related variables
+ @Nullable
protected ScheduledFuture> connectJob;
protected long lastTimeStamp;
@@ -93,10 +97,12 @@ public class TeslaAccountHandler extends BaseBridgeHandler {
protected int apiIntervalErrors;
protected long eventIntervalTimestamp;
protected int eventIntervalErrors;
- protected ReentrantLock lock;
+
+ protected ReentrantLock lock = new ReentrantLock();
private final Gson gson = new Gson();
+ @Nullable
private TokenResponse logonToken;
private final Set vehicleListeners = new HashSet<>();
@@ -122,31 +128,17 @@ public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
- lock = new ReentrantLock();
- lock.lock();
-
- try {
- if (connectJob == null || connectJob.isCancelled()) {
- connectJob = scheduler.scheduleWithFixedDelay(connectRunnable, 0, CONNECT_RETRY_INTERVAL,
- TimeUnit.MILLISECONDS);
- }
- } finally {
- lock.unlock();
- }
+ connectJob = scheduler.scheduleWithFixedDelay(connectRunnable, 0, CONNECT_RETRY_INTERVAL,
+ TimeUnit.MILLISECONDS);
}
@Override
public void dispose() {
logger.debug("Disposing the Tesla account handler for {}", getThing().getUID());
-
- lock.lock();
- try {
- if (connectJob != null && !connectJob.isCancelled()) {
- connectJob.cancel(true);
- connectJob = null;
- }
- } finally {
- lock.unlock();
+ ScheduledFuture> connectJob = this.connectJob;
+ if (connectJob != null && !connectJob.isCancelled()) {
+ connectJob.cancel(true);
+ this.connectJob = null;
}
}
@@ -167,19 +159,25 @@ public void handleCommand(ChannelUID channelUID, Command command) {
// we do not have any channels -> nothing to do here
}
- public String getAuthHeader() {
- if (logonToken != null) {
- return "Bearer " + logonToken.access_token;
+ public @Nullable String getAuthHeader() {
+ String accessToken = getAccessToken();
+ if (accessToken != null) {
+ return "Bearer " + accessToken;
} else {
return null;
}
}
- public String getAccessToken() {
- return logonToken.access_token;
+ public @Nullable String getAccessToken() {
+ TokenResponse logonToken = this.logonToken;
+ if (logonToken != null) {
+ return logonToken.accessToken;
+ } else {
+ return null;
+ }
}
- protected boolean checkResponse(Response response, boolean immediatelyFail) {
+ protected boolean checkResponse(@Nullable Response response, boolean immediatelyFail) {
if (response != null && response.getStatus() == 200) {
return true;
} else if (response != null && response.getStatus() == 401) {
@@ -221,17 +219,23 @@ protected Vehicle[] queryVehicles() {
if (!checkResponse(response, true)) {
logger.debug("An error occurred while querying the vehicle");
- return null;
+ return new Vehicle[0];
}
JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject();
Vehicle[] vehicleArray = gson.fromJson(jsonObject.getAsJsonArray("response"), Vehicle[].class);
-
+ if (vehicleArray == null) {
+ logger.debug("Response resulted in unexpected null array");
+ return new Vehicle[0];
+ }
for (Vehicle vehicle : vehicleArray) {
String responseString = invokeAndParse(vehicle.id, null, null, dataRequestTarget, 0);
VehicleConfig vehicleConfig = null;
if (responseString != null && !responseString.isBlank()) {
- vehicleConfig = gson.fromJson(responseString, VehicleData.class).vehicle_config;
+ VehicleData vehicleData = gson.fromJson(responseString, VehicleData.class);
+ if (vehicleData != null) {
+ vehicleConfig = vehicleData.vehicleConfig;
+ }
}
for (VehicleListener listener : vehicleListeners) {
listener.vehicleFound(vehicle, vehicleConfig);
@@ -251,7 +255,7 @@ protected Vehicle[] queryVehicles() {
logger.debug("Querying the vehicle: VIN {}", vehicle.vin);
String vehicleJSON = gson.toJson(vehicle);
vehicleHandler.parseAndUpdate("queryVehicle", null, vehicleJSON);
- logger.trace("Vehicle is id {}/vehicle_id {}/tokens {}", vehicle.id, vehicle.vehicle_id,
+ logger.trace("Vehicle is id {}/vehicle_id {}/tokens {}", vehicle.id, vehicle.vehicleId,
vehicle.tokens);
}
}
@@ -274,8 +278,8 @@ ThingStatusInfo authenticate() {
logger.debug("Current authentication time {}", DATE_FORMATTER.format(Instant.now()));
if (token != null) {
- Instant tokenCreationInstant = Instant.ofEpochMilli(token.created_at * 1000);
- Instant tokenExpiresInstant = Instant.ofEpochMilli((token.created_at + token.expires_in) * 1000);
+ Instant tokenCreationInstant = Instant.ofEpochMilli(token.createdAt * 1000);
+ Instant tokenExpiresInstant = Instant.ofEpochMilli((token.createdAt + token.expiresIn) * 1000);
logger.debug("Found a request token from {}", DATE_FORMATTER.format(tokenCreationInstant));
logger.debug("Access token expiration time {}", DATE_FORMATTER.format(tokenExpiresInstant));
@@ -306,8 +310,8 @@ ThingStatusInfo authenticate() {
return new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
}
- protected String invokeAndParse(String vehicleId, String command, String payLoad, WebTarget target,
- int noOfretries) {
+ protected @Nullable String invokeAndParse(@Nullable String vehicleId, @Nullable String command,
+ @Nullable String payLoad, WebTarget target, int noOfretries) {
logger.debug("Invoking: {}", command);
if (vehicleId != null) {
@@ -316,26 +320,29 @@ protected String invokeAndParse(String vehicleId, String command, String payLoad
if (payLoad != null) {
if (command != null) {
response = target.resolveTemplate("cmd", command).resolveTemplate("vid", vehicleId).request()
- .header("Authorization", "Bearer " + logonToken.access_token)
+ .header("Authorization", getAuthHeader())
.post(Entity.entity(payLoad, MediaType.APPLICATION_JSON_TYPE));
} else {
response = target.resolveTemplate("vid", vehicleId).request()
- .header("Authorization", "Bearer " + logonToken.access_token)
+ .header("Authorization", getAuthHeader())
.post(Entity.entity(payLoad, MediaType.APPLICATION_JSON_TYPE));
}
} else if (command != null) {
response = target.resolveTemplate("cmd", command).resolveTemplate("vid", vehicleId)
- .request(MediaType.APPLICATION_JSON_TYPE)
- .header("Authorization", "Bearer " + logonToken.access_token).get();
+ .request(MediaType.APPLICATION_JSON_TYPE).header("Authorization", getAuthHeader()).get();
} else {
response = target.resolveTemplate("vid", vehicleId).request(MediaType.APPLICATION_JSON_TYPE)
- .header("Authorization", "Bearer " + logonToken.access_token).get();
+ .header("Authorization", getAuthHeader()).get();
}
if (!checkResponse(response, false)) {
+ if (response == null) {
+ logger.debug(
+ "An error occurred while communicating with the vehicle during request, the response was null");
+ return null;
+ }
logger.debug("An error occurred while communicating with the vehicle during request {}: {}: {}",
- command, (response != null) ? response.getStatus() : "",
- (response != null) ? response.getStatusInfo().getReasonPhrase() : "No Response");
+ command, response.getStatus(), response.getStatusInfo().getReasonPhrase());
if (response.getStatus() == 408 && noOfretries > 0) {
try {
// we give the vehicle a moment to wake up and try the request again
@@ -377,7 +384,7 @@ protected String invokeAndParse(String vehicleId, String command, String payLoad
if (authenticationResult.getStatus() == ThingStatus.ONLINE) {
// get a list of vehicles
Response response = productsTarget.request(MediaType.APPLICATION_JSON_TYPE)
- .header("Authorization", "Bearer " + logonToken.access_token).get();
+ .header("Authorization", getAuthHeader()).get();
if (response != null && response.getStatus() == 200 && response.hasEntity()) {
updateStatus(ThingStatus.ONLINE);
@@ -436,11 +443,12 @@ protected class Request implements Runnable {
private TeslaVehicleHandler handler;
private String request;
+ @Nullable
private String payLoad;
private WebTarget target;
private boolean allowWakeUpForCommands;
- public Request(TeslaVehicleHandler handler, String request, String payLoad, WebTarget target,
+ public Request(TeslaVehicleHandler handler, String request, @Nullable String payLoad, WebTarget target,
boolean allowWakeUpForCommands) {
this.handler = handler;
this.request = request;
@@ -467,8 +475,8 @@ public void run() {
}
}
- public Request newRequest(TeslaVehicleHandler teslaVehicleHandler, String command, String payLoad, WebTarget target,
- boolean allowWakeUpForCommands) {
+ public Request newRequest(TeslaVehicleHandler teslaVehicleHandler, String command, @Nullable String payLoad,
+ WebTarget target, boolean allowWakeUpForCommands) {
return new Request(teslaVehicleHandler, command, payLoad, target, allowWakeUpForCommands);
}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaEventEndpoint.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaEventEndpoint.java
index 54fe6ac721bda..d026f7ea92c9b 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaEventEndpoint.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaEventEndpoint.java
@@ -29,7 +29,7 @@
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.eclipse.jetty.websocket.api.WebSocketPingPongListener;
import org.eclipse.jetty.websocket.client.WebSocketClient;
-import org.openhab.binding.tesla.internal.protocol.Event;
+import org.openhab.binding.tesla.internal.protocol.dto.Event;
import org.openhab.core.io.net.http.WebSocketFactory;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.util.ThingWebClientUtil;
@@ -54,25 +54,20 @@ public class TeslaEventEndpoint implements WebSocketListener, WebSocketPingPongL
private final Logger logger = LoggerFactory.getLogger(TeslaEventEndpoint.class);
private String endpointId;
- protected WebSocketFactory webSocketFactory;
private WebSocketClient client;
private ConnectionState connectionState = ConnectionState.CLOSED;
private @Nullable Session session;
- private EventHandler eventHandler;
+ private @Nullable EventHandler eventHandler;
private final Gson gson = new Gson();
public TeslaEventEndpoint(ThingUID uid, WebSocketFactory webSocketFactory) {
- try {
- this.endpointId = "TeslaEventEndpoint-" + uid.getAsString();
+ this.endpointId = "TeslaEventEndpoint-" + uid.getAsString();
- String name = ThingWebClientUtil.buildWebClientConsumerName(uid, null);
- client = webSocketFactory.createWebSocketClient(name);
- this.client.setConnectTimeout(TIMEOUT_MILLISECONDS);
- this.client.setMaxIdleTimeout(IDLE_TIMEOUT_MILLISECONDS);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
+ String name = ThingWebClientUtil.buildWebClientConsumerName(uid, null);
+ client = webSocketFactory.createWebSocketClient(name);
+ this.client.setConnectTimeout(TIMEOUT_MILLISECONDS);
+ this.client.setMaxIdleTimeout(IDLE_TIMEOUT_MILLISECONDS);
}
public void close() {
@@ -117,19 +112,22 @@ public void connect(URI endpointURI) {
}
@Override
- public void onWebSocketConnect(Session session) {
- logger.debug("{} : Connected to {} with hash {}", endpointId, session.getRemoteAddress().getAddress(),
- session.hashCode());
+ public void onWebSocketConnect(@Nullable Session session) {
+ logger.debug("{} : Connected to {} with hash {}", endpointId,
+ (session != null) ? session.getRemoteAddress().getAddress() : "Unknown",
+ (session != null) ? session.hashCode() : -1);
connectionState = ConnectionState.CONNECTED;
this.session = session;
}
public void closeConnection() {
+ Session session = this.session;
try {
connectionState = ConnectionState.CLOSING;
if (session != null && session.isOpen()) {
logger.debug("{} : Closing the session", endpointId);
session.close(StatusCode.NORMAL, "bye");
+ this.session = session;
}
} catch (Exception e) {
logger.error("{} : An exception occurred while closing the session : {}", endpointId, e.getMessage());
@@ -138,14 +136,14 @@ public void closeConnection() {
}
@Override
- public void onWebSocketClose(int statusCode, String reason) {
+ public void onWebSocketClose(int statusCode, @Nullable String reason) {
logger.debug("{} : Closed the session with status {} for reason {}", endpointId, statusCode, reason);
connectionState = ConnectionState.CLOSED;
this.session = null;
}
@Override
- public void onWebSocketText(String message) {
+ public void onWebSocketText(@Nullable String message) {
// NoOp
}
@@ -158,10 +156,13 @@ public void onWebSocketBinary(byte[] payload, int offset, int length) {
try {
while ((str = in.readLine()) != null) {
logger.trace("{} : Received raw data '{}'", endpointId, str);
- if (this.eventHandler != null) {
+ EventHandler eventHandler = this.eventHandler;
+ if (eventHandler != null) {
try {
Event event = gson.fromJson(str, Event.class);
- this.eventHandler.handleEvent(event);
+ if (event != null) {
+ eventHandler.handleEvent(event);
+ }
} catch (RuntimeException e) {
logger.error("{} : An exception occurred while processing raw data : {}", endpointId,
e.getMessage());
@@ -176,12 +177,14 @@ public void onWebSocketBinary(byte[] payload, int offset, int length) {
@Override
public void onWebSocketError(Throwable cause) {
logger.error("{} : An error occurred in the session : {}", endpointId, cause.getMessage());
+ Session session = this.session;
if (session != null && session.isOpen()) {
session.close(StatusCode.ABNORMAL, "Session Error");
}
}
public void sendMessage(String message) throws IOException {
+ Session session = this.session;
try {
if (session != null) {
logger.debug("{} : Sending raw data '{}'", endpointId, message);
@@ -198,6 +201,7 @@ public void sendMessage(String message) throws IOException {
}
public void ping() {
+ Session session = this.session;
try {
if (session != null) {
ByteBuffer buffer = ByteBuffer.allocate(8).putLong(System.nanoTime()).flip();
@@ -209,8 +213,9 @@ public void ping() {
}
@Override
- public void onWebSocketPing(ByteBuffer payload) {
+ public void onWebSocketPing(@Nullable ByteBuffer payload) {
ByteBuffer buffer = ByteBuffer.allocate(8).putLong(System.nanoTime()).flip();
+ Session session = this.session;
try {
if (session != null) {
session.getRemote().sendPing(buffer);
@@ -221,7 +226,10 @@ public void onWebSocketPing(ByteBuffer payload) {
}
@Override
- public void onWebSocketPong(ByteBuffer payload) {
+ public void onWebSocketPong(@Nullable ByteBuffer payload) {
+ if (payload == null) {
+ return;
+ }
long start = payload.getLong();
long roundTrip = System.nanoTime() - start;
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaSSOHandler.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaSSOHandler.java
index 1ba2bbe69b1a4..7fa6953b633e8 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaSSOHandler.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaSSOHandler.java
@@ -28,8 +28,8 @@
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
-import org.openhab.binding.tesla.internal.protocol.sso.RefreshTokenRequest;
-import org.openhab.binding.tesla.internal.protocol.sso.TokenResponse;
+import org.openhab.binding.tesla.internal.protocol.dto.sso.RefreshTokenRequest;
+import org.openhab.binding.tesla.internal.protocol.dto.sso.TokenResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -72,10 +72,10 @@ public TokenResponse getAccessToken(String refreshToken) {
String refreshTokenResponse = refreshResponse.getContentAsString();
TokenResponse tokenResponse = gson.fromJson(refreshTokenResponse.trim(), TokenResponse.class);
- if (tokenResponse != null && tokenResponse.access_token != null && !tokenResponse.access_token.isEmpty()) {
- tokenResponse.created_at = Instant.now().getEpochSecond();
- logger.debug("Access token expires in {} seconds at {}", tokenResponse.expires_in, DATE_FORMATTER
- .format(Instant.ofEpochMilli((tokenResponse.created_at + tokenResponse.expires_in) * 1000)));
+ if (tokenResponse != null && tokenResponse.accessToken != null && !tokenResponse.accessToken.isEmpty()) {
+ tokenResponse.createdAt = Instant.now().getEpochSecond();
+ logger.debug("Access token expires in {} seconds at {}", tokenResponse.expiresIn, DATE_FORMATTER
+ .format(Instant.ofEpochMilli((tokenResponse.createdAt + tokenResponse.expiresIn) * 1000)));
return tokenResponse;
} else {
logger.debug("An error occurred while exchanging SSO auth token for API access token.");
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaVehicleHandler.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaVehicleHandler.java
index b4af61a80b250..7ef596e6b90cb 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaVehicleHandler.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaVehicleHandler.java
@@ -25,6 +25,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -37,21 +38,22 @@
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
+import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tesla.internal.TeslaBindingConstants;
import org.openhab.binding.tesla.internal.TeslaBindingConstants.EventKeys;
import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy;
import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy.TeslaChannelSelector;
import org.openhab.binding.tesla.internal.handler.TeslaAccountHandler.Request;
-import org.openhab.binding.tesla.internal.protocol.ChargeState;
-import org.openhab.binding.tesla.internal.protocol.ClimateState;
-import org.openhab.binding.tesla.internal.protocol.DriveState;
-import org.openhab.binding.tesla.internal.protocol.Event;
-import org.openhab.binding.tesla.internal.protocol.GUIState;
-import org.openhab.binding.tesla.internal.protocol.SoftwareUpdate;
-import org.openhab.binding.tesla.internal.protocol.Vehicle;
-import org.openhab.binding.tesla.internal.protocol.VehicleData;
-import org.openhab.binding.tesla.internal.protocol.VehicleState;
+import org.openhab.binding.tesla.internal.protocol.dto.ChargeState;
+import org.openhab.binding.tesla.internal.protocol.dto.ClimateState;
+import org.openhab.binding.tesla.internal.protocol.dto.DriveState;
+import org.openhab.binding.tesla.internal.protocol.dto.Event;
+import org.openhab.binding.tesla.internal.protocol.dto.GUIState;
+import org.openhab.binding.tesla.internal.protocol.dto.SoftwareUpdate;
+import org.openhab.binding.tesla.internal.protocol.dto.Vehicle;
+import org.openhab.binding.tesla.internal.protocol.dto.VehicleData;
+import org.openhab.binding.tesla.internal.protocol.dto.VehicleState;
import org.openhab.binding.tesla.internal.throttler.QueueChannelThrottler;
import org.openhab.binding.tesla.internal.throttler.Rate;
import org.openhab.core.io.net.http.WebSocketFactory;
@@ -64,6 +66,7 @@
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
@@ -88,9 +91,9 @@
* @author Karel Goderis - Initial contribution
* @author Kai Kreuzer - Refactored to use separate account handler and improved configuration options
*/
+@NonNullByDefault
public class TeslaVehicleHandler extends BaseThingHandler {
- private static final int FAST_STATUS_REFRESH_INTERVAL = 15000;
private static final int SLOW_STATUS_REFRESH_INTERVAL = 60000;
private static final int API_SLEEP_INTERVAL_MINUTES = 20;
private static final int MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT = 5;
@@ -105,13 +108,21 @@ public class TeslaVehicleHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(TeslaVehicleHandler.class);
// Vehicle state variables
+ @Nullable
protected Vehicle vehicle;
+ @Nullable
protected String vehicleJSON;
+ @Nullable
protected DriveState driveState;
+ @Nullable
protected GUIState guiState;
+ @Nullable
protected VehicleState vehicleState;
+ @Nullable
protected ChargeState chargeState;
+ @Nullable
protected ClimateState climateState;
+ @Nullable
protected SoftwareUpdate softwareUpdate;
protected boolean allowWakeUp;
@@ -127,7 +138,7 @@ public class TeslaVehicleHandler extends BaseThingHandler {
protected long eventIntervalTimestamp;
protected int eventIntervalErrors;
protected int inactivity = MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT;
- protected ReentrantLock lock;
+ protected ReentrantLock lock = new ReentrantLock();
protected double lastLongitude;
protected double lastLatitude;
@@ -140,13 +151,13 @@ public class TeslaVehicleHandler extends BaseThingHandler {
protected String lastState = "";
protected boolean isInactive = false;
- protected TeslaAccountHandler account;
+ protected @NonNullByDefault({}) TeslaAccountHandler account;
- protected QueueChannelThrottler stateThrottler;
- protected TeslaChannelSelectorProxy teslaChannelSelectorProxy = new TeslaChannelSelectorProxy();
- protected Thread eventThread;
- protected ScheduledFuture> stateJob;
+ protected @Nullable QueueChannelThrottler stateThrottler;
+ protected @Nullable Thread eventThread;
+ protected @Nullable ScheduledFuture> stateJob;
protected WebSocketFactory webSocketFactory;
+ protected TeslaChannelSelectorProxy teslaChannelSelectorProxy = new TeslaChannelSelectorProxy();
private final Gson gson = new Gson();
@@ -155,68 +166,62 @@ public TeslaVehicleHandler(Thing thing, WebSocketFactory webSocketFactory) {
this.webSocketFactory = webSocketFactory;
}
- @SuppressWarnings("null")
@Override
public void initialize() {
logger.trace("Initializing the Tesla handler for {}", getThing().getUID());
updateStatus(ThingStatus.UNKNOWN);
+
allowWakeUp = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUP);
allowWakeUpForCommands = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUPFORCOMMANDS);
enableEvents = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ENABLEEVENTS);
- Number inactivityParam = (Number) getConfig().get(TeslaBindingConstants.CONFIG_INACTIVITY);
- inactivity = inactivityParam == null ? MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT : inactivityParam.intValue();
- Boolean useDriveStateParam = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_USEDRIVESTATE);
- useDriveState = useDriveStateParam == null ? false : useDriveStateParam;
- Boolean useAdvancedStatesParam = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_USEDADVANCEDSTATES);
- useAdvancedStates = useAdvancedStatesParam == null ? false : useAdvancedStatesParam;
-
- account = (TeslaAccountHandler) getBridge().getHandler();
+
+ inactivity = Objects.requireNonNullElse((Number) getConfig().get(TeslaBindingConstants.CONFIG_INACTIVITY),
+ MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT).intValue();
+ useDriveState = Objects
+ .requireNonNullElse((boolean) getConfig().get(TeslaBindingConstants.CONFIG_USEDRIVESTATE), false);
+ useAdvancedStates = Objects
+ .requireNonNullElse((boolean) getConfig().get(TeslaBindingConstants.CONFIG_USEDADVANCEDSTATES), false);
+ Bridge bridge = getBridge();
+ if (bridge == null || !(bridge.getHandler() instanceof TeslaAccountHandler teslaAccountHandler)) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
+ return;
+ }
+ account = teslaAccountHandler;
lock = new ReentrantLock();
scheduler.execute(this::queryVehicleAndUpdate);
- lock.lock();
- try {
- Map channels = new HashMap<>();
- channels.put(DATA_THROTTLE, new Rate(1, 1, TimeUnit.SECONDS));
- channels.put(COMMAND_THROTTLE, new Rate(20, 1, TimeUnit.MINUTES));
-
- Rate firstRate = new Rate(20, 1, TimeUnit.MINUTES);
- Rate secondRate = new Rate(200, 10, TimeUnit.MINUTES);
- stateThrottler = new QueueChannelThrottler(firstRate, scheduler, channels);
- stateThrottler.addRate(secondRate);
-
- if (stateJob == null || stateJob.isCancelled()) {
- stateJob = scheduler.scheduleWithFixedDelay(stateRunnable, 0, SLOW_STATUS_REFRESH_INTERVAL,
- TimeUnit.MILLISECONDS);
- }
-
- if (enableEvents) {
- if (eventThread == null) {
- eventThread = new Thread(eventRunnable, "OH-binding-" + getThing().getUID() + "-events");
- eventThread.start();
- }
- }
- } finally {
- lock.unlock();
+ Map channels = new HashMap<>();
+ channels.put(DATA_THROTTLE, new Rate(1, 1, TimeUnit.SECONDS));
+ channels.put(COMMAND_THROTTLE, new Rate(20, 1, TimeUnit.MINUTES));
+
+ Rate firstRate = new Rate(20, 1, TimeUnit.MINUTES);
+ Rate secondRate = new Rate(200, 10, TimeUnit.MINUTES);
+ QueueChannelThrottler stateThrottler = new QueueChannelThrottler(firstRate, scheduler, channels);
+ stateThrottler.addRate(secondRate);
+ this.stateThrottler = stateThrottler;
+ stateJob = scheduler.scheduleWithFixedDelay(stateRunnable, 0, SLOW_STATUS_REFRESH_INTERVAL,
+ TimeUnit.MILLISECONDS);
+
+ if (enableEvents) {
+ Thread eventThread = new Thread(eventRunnable, "OH-binding-" + getThing().getUID() + "-events");
+ eventThread.start();
+ this.eventThread = eventThread;
}
}
@Override
public void dispose() {
logger.trace("Disposing the Tesla handler for {}", getThing().getUID());
- lock.lock();
- try {
- if (stateJob != null && !stateJob.isCancelled()) {
- stateJob.cancel(true);
- stateJob = null;
- }
- if (eventThread != null && !eventThread.isInterrupted()) {
- eventThread.interrupt();
- eventThread = null;
- }
- } finally {
- lock.unlock();
+ ScheduledFuture> stateJob = this.stateJob;
+ if (stateJob != null && !stateJob.isCancelled()) {
+ stateJob.cancel(true);
+ this.stateJob = null;
+ }
+ Thread eventThread = this.eventThread;
+ if (eventThread != null && !eventThread.isInterrupted()) {
+ eventThread.interrupt();
+ this.eventThread = null;
}
}
@@ -225,7 +230,8 @@ public void dispose() {
*
* @return the vehicle id
*/
- public String getVehicleId() {
+ public @Nullable String getVehicleId() {
+ Vehicle vehicle = this.vehicle;
if (vehicle != null) {
return vehicle.id;
} else {
@@ -248,9 +254,9 @@ public void handleCommand(ChannelUID channelUID, Command command) {
setActive();
// Request the state of all known variables. This is sub-optimal, but the requests get scheduled and
- // throttled so we are safe not to break the Tesla SLA
+ // throbridgettled so we are safe not to break the Tesla SLA
requestAllData();
- } else if (selector != null) {
+ } else {
if (!isAwake() && allowWakeUpForCommands) {
logger.debug("Waking vehicle to send command.");
wakeUp();
@@ -267,10 +273,16 @@ public void handleCommand(ChannelUID channelUID, Command command) {
setChargeLimit(0);
} else if (command instanceof IncreaseDecreaseType
&& command == IncreaseDecreaseType.INCREASE) {
- setChargeLimit(Math.min(chargeState.charge_limit_soc + 1, 100));
+ ChargeState chargeState = this.chargeState;
+ if (chargeState != null) {
+ setChargeLimit(Math.min(chargeState.chargeLimitSoc + 1, 100));
+ }
} else if (command instanceof IncreaseDecreaseType
&& command == IncreaseDecreaseType.DECREASE) {
- setChargeLimit(Math.max(chargeState.charge_limit_soc - 1, 0));
+ ChargeState chargeState = this.chargeState;
+ if (chargeState != null) {
+ setChargeLimit(Math.max(chargeState.chargeLimitSoc - 1, 0));
+ }
}
break;
}
@@ -298,23 +310,17 @@ public void handleCommand(ChannelUID channelUID, Command command) {
break;
case COMBINED_TEMP: {
QuantityType quantity = commandToQuantityType(command);
- if (quantity != null) {
- setCombinedTemperature(quanityToRoundedFloat(quantity));
- }
+ setCombinedTemperature(quanityToRoundedFloat(quantity));
break;
}
case DRIVER_TEMP: {
QuantityType quantity = commandToQuantityType(command);
- if (quantity != null) {
- setDriverTemperature(quanityToRoundedFloat(quantity));
- }
+ setDriverTemperature(quanityToRoundedFloat(quantity));
break;
}
case PASSENGER_TEMP: {
QuantityType quantity = commandToQuantityType(command);
- if (quantity != null) {
- setPassengerTemperature(quanityToRoundedFloat(quantity));
- }
+ setPassengerTemperature(quanityToRoundedFloat(quantity));
break;
}
case SENTRY_MODE: {
@@ -411,11 +417,12 @@ public void handleCommand(ChannelUID channelUID, Command command) {
}
case RT: {
if (command instanceof OnOffType onOffCommand) {
+ VehicleState vehicleState = this.vehicleState;
if (onOffCommand == OnOffType.ON) {
- if (vehicleState.rt == 0) {
+ if (vehicleState != null && vehicleState.rt == 0) {
openTrunk();
}
- } else if (vehicleState.rt == 1) {
+ } else if (vehicleState != null && vehicleState.rt == 1) {
closeTrunk();
}
}
@@ -459,9 +466,10 @@ public void handleCommand(ChannelUID channelUID, Command command) {
}
}
- public void sendCommand(String command, String payLoad, WebTarget target) {
+ public void sendCommand(String command, @Nullable String payLoad, WebTarget target) {
if (COMMAND_WAKE_UP.equals(command) || isAwake() || allowWakeUpForCommands) {
Request request = account.newRequest(this, command, payLoad, target, allowWakeUpForCommands);
+ QueueChannelThrottler stateThrottler = this.stateThrottler;
if (stateThrottler != null) {
stateThrottler.submit(COMMAND_THROTTLE, request);
}
@@ -475,6 +483,7 @@ public void sendCommand(String command) {
public void sendCommand(String command, String payLoad) {
if (COMMAND_WAKE_UP.equals(command) || isAwake() || allowWakeUpForCommands) {
Request request = account.newRequest(this, command, payLoad, account.commandTarget, allowWakeUpForCommands);
+ QueueChannelThrottler stateThrottler = this.stateThrottler;
if (stateThrottler != null) {
stateThrottler.submit(COMMAND_THROTTLE, request);
}
@@ -484,16 +493,18 @@ public void sendCommand(String command, String payLoad) {
public void sendCommand(String command, WebTarget target) {
if (COMMAND_WAKE_UP.equals(command) || isAwake() || allowWakeUpForCommands) {
Request request = account.newRequest(this, command, "{}", target, allowWakeUpForCommands);
+ QueueChannelThrottler stateThrottler = this.stateThrottler;
if (stateThrottler != null) {
stateThrottler.submit(COMMAND_THROTTLE, request);
}
}
}
- public void requestData(String command, String payLoad) {
+ public void requestData(String command, @Nullable String payLoad) {
if (COMMAND_WAKE_UP.equals(command) || isAwake()
|| (!"vehicleData".equals(command) && allowWakeUpForCommands)) {
Request request = account.newRequest(this, command, payLoad, account.dataRequestTarget, false);
+ QueueChannelThrottler stateThrottler = this.stateThrottler;
if (stateThrottler != null) {
stateThrottler.submit(DATA_THROTTLE, request);
}
@@ -529,14 +540,16 @@ public void requestAllData() {
}
protected boolean isAwake() {
- return vehicle != null && "online".equals(vehicle.state) && vehicle.vehicle_id != null;
+ Vehicle vehicle = this.vehicle;
+ return vehicle != null && "online".equals(vehicle.state) && vehicle.vehicleId != null;
}
protected boolean isInMotion() {
+ DriveState driveState = this.driveState;
if (driveState != null) {
- if (driveState.speed != null && driveState.shift_state != null) {
+ if (driveState.speed != null && driveState.shiftState != null) {
return !"Undefined".equals(driveState.speed)
- && (!"P".equals(driveState.shift_state) || !"Undefined".equals(driveState.shift_state));
+ && (!"P".equals(driveState.shiftState) || !"Undefined".equals(driveState.shiftState));
}
}
return false;
@@ -551,15 +564,16 @@ protected boolean isInactive() {
}
protected boolean isCharging() {
- return chargeState != null && "Charging".equals(chargeState.charging_state);
+ ChargeState chargeState = this.chargeState;
+ return chargeState != null && "Charging".equals(chargeState.chargingState);
}
protected boolean notReadyForSleep() {
boolean status;
int computedInactivityPeriod = inactivity;
-
+ VehicleState vehicleState = this.vehicleState;
if (useAdvancedStates) {
- if (vehicleState.is_user_present && !isInMotion()) {
+ if (vehicleState != null && vehicleState.isUserPresent && !isInMotion()) {
logger.debug("Car is occupied but stationary.");
if (lastAdvModesTimestamp < (System.currentTimeMillis()
- (THRESHOLD_INTERVAL_FOR_ADVANCED_MINUTES * 60 * 1000))) {
@@ -568,7 +582,7 @@ protected boolean notReadyForSleep() {
return (backOffCounter++ % 6 == 0); // using 6 should make sure 1 out of 5 pollers get serviced,
// about every min.
}
- } else if (vehicleState.sentry_mode) {
+ } else if (vehicleState != null && vehicleState.sentryMode) {
logger.debug("Car is in sentry mode.");
if (lastAdvModesTimestamp < (System.currentTimeMillis()
- (THRESHOLD_INTERVAL_FOR_ADVANCED_MINUTES * 60 * 1000))) {
@@ -576,23 +590,23 @@ protected boolean notReadyForSleep() {
} else {
return (backOffCounter++ % 6 == 0);
}
- } else if ((vehicleState.center_display_state != 0) && (!isInMotion())) {
+ } else if (vehicleState != null && (vehicleState.centerDisplayState != 0) && (!isInMotion())) {
logger.debug("Car is in camp, climate keep, dog, or other mode preventing sleep. Mode {}",
- vehicleState.center_display_state);
+ vehicleState.centerDisplayState);
return (backOffCounter++ % 6 == 0);
} else {
lastAdvModesTimestamp = System.currentTimeMillis();
}
}
- if (vehicleState != null && vehicleState.homelink_nearby) {
+ if (vehicleState != null && vehicleState.homelinkNearby) {
computedInactivityPeriod = MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT;
logger.debug("Car is at home. Movement or drive state threshold is {} min.",
MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT);
}
-
+ DriveState driveState = this.driveState;
if (useDriveState) {
- if (driveState.shift_state != null) {
+ if (driveState != null && driveState.shiftState != null) {
logger.debug("Car drive state not null and not ready to sleep.");
return true;
} else {
@@ -632,7 +646,7 @@ protected void setActive() {
lastLongitude = 0;
}
- protected boolean checkResponse(Response response, boolean immediatelyFail) {
+ protected boolean checkResponse(@Nullable Response response, boolean immediatelyFail) {
if (response != null && response.getStatus() == 200) {
return true;
} else if (response != null && response.getStatus() == 401) {
@@ -711,11 +725,13 @@ public void setCombinedTemperature(float temperature) {
}
public void setDriverTemperature(float temperature) {
- setTemperature(temperature, climateState != null ? climateState.passenger_temp_setting : temperature);
+ ClimateState climateState = this.climateState;
+ setTemperature(temperature, climateState != null ? climateState.passengerTempSetting : temperature);
}
public void setPassengerTemperature(float temperature) {
- setTemperature(climateState != null ? climateState.driver_temp_setting : temperature, temperature);
+ ClimateState climateState = this.climateState;
+ setTemperature(climateState != null ? climateState.passengerTempSetting : temperature, temperature);
}
public void openFrunk() {
@@ -734,7 +750,7 @@ public void closeTrunk() {
openTrunk();
}
- public void setValetMode(boolean b, Integer pin) {
+ public void setValetMode(boolean b, @Nullable Integer pin) {
JsonObject payloadObject = new JsonObject();
payloadObject.addProperty("on", b);
if (pin != null) {
@@ -785,7 +801,7 @@ public void setSteeringWheelHeater(boolean isOn) {
sendCommand(COMMAND_STEERING_WHEEL_HEATER, gson.toJson(payloadObject), account.commandTarget);
}
- protected Vehicle queryVehicle() {
+ protected @Nullable Vehicle queryVehicle() {
String authHeader = account.getAuthHeader();
if (authHeader != null) {
@@ -805,14 +821,17 @@ protected Vehicle queryVehicle() {
JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject();
Vehicle[] vehicleArray = gson.fromJson(jsonObject.getAsJsonArray("response"), Vehicle[].class);
-
+ if (vehicleArray == null) {
+ logger.debug("Response resulted in unexpected null array");
+ return null;
+ }
for (Vehicle vehicle : vehicleArray) {
logger.debug("Querying the vehicle: VIN {}", vehicle.vin);
if (vehicle.vin.equals(getConfig().get(VIN))) {
vehicleJSON = gson.toJson(vehicle);
parseAndUpdate("queryVehicle", null, vehicleJSON);
if (logger.isTraceEnabled()) {
- logger.trace("Vehicle is id {}/vehicle_id {}/tokens {}", vehicle.id, vehicle.vehicle_id,
+ logger.trace("Vehicle is id {}/vehicle_id {}/tokens {}", vehicle.id, vehicle.vehicleId,
vehicle.tokens);
}
return vehicle;
@@ -831,9 +850,9 @@ protected void queryVehicleAndUpdate() {
vehicle = queryVehicle();
}
- public void parseAndUpdate(String request, String payLoad, String result) {
+ public void parseAndUpdate(@Nullable String request, @Nullable String payLoad, @Nullable String result) {
final double locationThreshold = .0000001;
-
+ Vehicle vehicle = this.vehicle;
try {
if (request != null && result != null && !"null".equals(result)) {
updateStatus(ThingStatus.ONLINE);
@@ -848,12 +867,12 @@ public void parseAndUpdate(String request, String payLoad, String result) {
return;
}
- if (vehicle != null && ("asleep".equals(vehicle.state) || "offline".equals(vehicle.state))) {
+ if ("asleep".equals(vehicle.state) || "offline".equals(vehicle.state)) {
logger.debug("Vehicle is {}", vehicle.state);
return;
}
- if (vehicle != null && !lastState.equals(vehicle.state)) {
+ if (!lastState.equals(vehicle.state)) {
lastState = vehicle.state;
// in case vehicle changed to awake, refresh all data
@@ -888,43 +907,44 @@ public void parseAndUpdate(String request, String payLoad, String result) {
return;
}
- driveState = vehicleData.drive_state;
- if (Math.abs(lastLatitude - driveState.latitude) > locationThreshold
- || Math.abs(lastLongitude - driveState.longitude) > locationThreshold) {
+ DriveState driveState = this.driveState = vehicleData.driveState;
+
+ if (driveState != null && (Math.abs(lastLatitude - driveState.latitude) > locationThreshold
+ || Math.abs(lastLongitude - driveState.longitude) > locationThreshold)) {
logger.debug("Vehicle moved, resetting last location timestamp");
lastLatitude = driveState.latitude;
lastLongitude = driveState.longitude;
lastLocationChangeTimestamp = System.currentTimeMillis();
}
- logger.trace("Drive state: {}", driveState.shift_state);
+ logger.trace("Drive state: {}", driveState != null ? driveState.shiftState : "null");
- if ((driveState.shift_state == null) && (lastValidDriveStateNotNull)) {
+ if ((driveState != null && driveState.shiftState == null) && (lastValidDriveStateNotNull)) {
logger.debug("Set NULL shiftstate time");
lastValidDriveStateNotNull = false;
lastDriveStateChangeToNullTimestamp = System.currentTimeMillis();
- } else if (driveState.shift_state != null) {
+ } else if (driveState != null && driveState.shiftState != null) {
logger.trace("Clear NULL shiftstate time");
lastValidDriveStateNotNull = true;
}
- guiState = vehicleData.gui_settings;
+ guiState = vehicleData.guiSettings;
- vehicleState = vehicleData.vehicle_state;
+ VehicleState vehicleState = this.vehicleState = vehicleData.vehicleState;
- chargeState = vehicleData.charge_state;
+ chargeState = vehicleData.chargeState;
if (isCharging()) {
updateState(CHANNEL_CHARGE, OnOffType.ON);
} else {
updateState(CHANNEL_CHARGE, OnOffType.OFF);
}
- climateState = vehicleData.climate_state;
+ ClimateState climateState = this.climateState = vehicleData.climateState;
BigDecimal avgtemp = roundBigDecimal(new BigDecimal(
- (climateState.driver_temp_setting + climateState.passenger_temp_setting) / 2.0f));
+ (climateState.passengerTempSetting + climateState.passengerTempSetting) / 2.0f));
updateState(CHANNEL_COMBINED_TEMP, new QuantityType<>(avgtemp, SIUnits.CELSIUS));
- softwareUpdate = vehicleState.software_update;
+ SoftwareUpdate softwareUpdate = this.softwareUpdate = vehicleState.softwareUpdate;
try {
lock.lock();
@@ -990,8 +1010,13 @@ public void parseAndUpdate(String request, String payLoad, String result) {
@SuppressWarnings("unchecked")
protected QuantityType commandToQuantityType(Command command) {
- if (command instanceof QuantityType) {
- return ((QuantityType) command).toUnit(SIUnits.CELSIUS);
+ if (command instanceof QuantityType quantityCommand) {
+ QuantityType commandInCelsius = quantityCommand.toUnit(SIUnits.CELSIUS);
+ if (commandInCelsius == null) {
+ logger.warn("Unable to convert command {} to CELSIUS", command);
+ } else {
+ return commandInCelsius;
+ }
}
return new QuantityType<>(new BigDecimal(command.toString()), SIUnits.CELSIUS);
}
@@ -1024,18 +1049,19 @@ protected BigDecimal roundBigDecimal(BigDecimal value) {
};
protected Runnable eventRunnable = new Runnable() {
+ @Nullable
TeslaEventEndpoint eventEndpoint;
boolean isAuthenticated = false;
long lastPingTimestamp = 0;
@Override
public void run() {
- eventEndpoint = new TeslaEventEndpoint(getThing().getUID(), webSocketFactory);
+ TeslaEventEndpoint eventEndpoint = new TeslaEventEndpoint(getThing().getUID(), webSocketFactory);
eventEndpoint.addEventHandler(new TeslaEventEndpoint.EventHandler() {
@Override
- public void handleEvent(Event event) {
+ public void handleEvent(@Nullable Event event) {
if (event != null) {
- switch (event.msg_type) {
+ switch (event.msgType) {
case "control:hello":
logger.debug("Event : Received hello");
break;
@@ -1068,7 +1094,7 @@ public void handleEvent(Event event) {
if (!selector.isProperty()) {
State newState = teslaChannelSelectorProxy.getState(vals[i], selector,
editProperties());
- if (newState != null && !"".equals(vals[i])) {
+ if (!"".equals(vals[i])) {
updateState(selector.getChannelID(), newState);
} else {
updateState(selector.getChannelID(), UnDefType.UNDEF);
@@ -1115,29 +1141,28 @@ public void handleEvent(Event event) {
}
break;
case "data:error":
- logger.debug("Event : Received an error: '{}'/'{}'", event.value, event.error_type);
+ logger.debug("Event : Received an error: '{}'/'{}'", event.value, event.errorType);
eventEndpoint.closeConnection();
break;
}
}
}
});
-
+ this.eventEndpoint = eventEndpoint;
while (true) {
try {
if (getThing().getStatus() == ThingStatus.ONLINE) {
if (isAwake()) {
eventEndpoint.connect(new URI(URI_EVENT));
-
if (eventEndpoint.isConnected()) {
if (!isAuthenticated) {
- logger.debug("Event : Authenticating vehicle {}", vehicle.vehicle_id);
+ logger.debug("Event : Authenticating vehicle {}", vehicle.vehicleId);
JsonObject payloadObject = new JsonObject();
payloadObject.addProperty("msg_type", "data:subscribe_oauth");
payloadObject.addProperty("token", account.getAccessToken());
payloadObject.addProperty("value", Arrays.asList(EventKeys.values()).stream()
.skip(1).map(Enum::toString).collect(Collectors.joining(",")));
- payloadObject.addProperty("tag", vehicle.vehicle_id);
+ payloadObject.addProperty("tag", vehicle.vehicleId);
eventEndpoint.sendMessage(gson.toJson(payloadObject));
isAuthenticated = true;
@@ -1200,6 +1225,7 @@ public void handleEvent(Event event) {
if (Thread.interrupted()) {
logger.debug("Event : The event thread was interrupted");
eventEndpoint.close();
+ this.eventEndpoint = eventEndpoint;
return;
}
}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/VehicleListener.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/VehicleListener.java
index 4008bd4f4ebdb..0a1c030676c95 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/VehicleListener.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/VehicleListener.java
@@ -14,8 +14,8 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.tesla.internal.protocol.Vehicle;
-import org.openhab.binding.tesla.internal.protocol.VehicleConfig;
+import org.openhab.binding.tesla.internal.protocol.dto.Vehicle;
+import org.openhab.binding.tesla.internal.protocol.dto.VehicleConfig;
/**
* The {@link VehicleListener} interface can be implemented by classes that want to be informed about
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/ChargeState.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/ChargeState.java
deleted file mode 100644
index 69018cbe2070d..0000000000000
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/ChargeState.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/**
- * 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.tesla.internal.protocol;
-
-/**
- * The {@link ChargeState} is a datastructure to capture
- * variables sent by the Tesla Vehicle
- *
- * @author Karel Goderis - Initial contribution
- */
-public class ChargeState {
- public boolean battery_heater_on;
- public boolean charge_enable_request;
- public boolean charge_port_door_open;
- public boolean charge_to_max_range;
- public boolean eu_vehicle;
- public boolean fast_charger_present;
- public boolean managed_charging_active;
- public boolean managed_charging_user_canceled;
- public boolean motorized_charge_port;
- public boolean not_enough_power_to_heat;
- public boolean scheduled_charging_pending;
- public boolean trip_charging;
- public float battery_current;
- public float battery_range;
- public float charge_energy_added;
- public float charge_miles_added_ideal;
- public float charge_miles_added_rated;
- public float charge_rate;
- public float est_battery_range;
- public float ideal_battery_range;
- public float time_to_full_charge;
- public int battery_level;
- public int charge_amps;
- public int charge_current_request;
- public int charge_current_request_max;
- public int charge_limit_soc;
- public int charge_limit_soc_max;
- public int charge_limit_soc_min;
- public int charge_limit_soc_std;
- public int charger_actual_current;
- public int charger_phases;
- public int charger_pilot_current;
- public int charger_power;
- public int charger_voltage;
- public int max_range_charge_counter;
- public int usable_battery_level;
- public String charge_port_latch;
- public String charging_state;
- public String conn_charge_cable;
- public String fast_charger_brand;
- public String fast_charger_type;
- public String managed_charging_start_time;
- public String scheduled_charging_start_time;
- public String user_charge_enable_request;
-
- ChargeState() {
- }
-}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/ClimateState.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/ClimateState.java
deleted file mode 100644
index 9eec75556b006..0000000000000
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/ClimateState.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * 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.tesla.internal.protocol;
-
-/**
- * The {@link ClimateState} is a datastructure to capture
- * variables sent by the Tesla Vehicle
- *
- * @author Karel Goderis - Initial contribution
- */
-public class ClimateState {
-
- public boolean battery_heater;
- public boolean battery_heater_no_power;
- public boolean is_auto_conditioning_on;
- public boolean is_climate_on;
- public boolean is_front_defroster_on;
- public boolean is_preconditioning;
- public boolean is_rear_defroster_on;
- public int seat_heater_left;
- public int seat_heater_rear_center;
- public int seat_heater_rear_left;
- public int seat_heater_rear_right;
- public int seat_heater_right;
- public boolean side_mirror_heaters;
- public boolean smart_preconditioning;
- public boolean steering_wheel_heater;
- public boolean wiper_blade_heater;
- public float driver_temp_setting;
- public float inside_temp;
- public float outside_temp;
- public float passenger_temp_setting;
- public int fan_status;
- public int left_temp_direction;
- public int max_avail_temp;
- public int min_avail_temp;
- public int right_temp_direction;
- public int seat_heater_rear_left_back;
- public int seat_heater_rear_right_back;
-
- ClimateState() {
- }
-}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/DriveState.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/DriveState.java
deleted file mode 100644
index 1403cc15e2755..0000000000000
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/DriveState.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * 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.tesla.internal.protocol;
-
-/**
- * The {@link DriveState} is a datastructure to capture
- * variables sent by the Tesla Vehicle
- *
- * @author Karel Goderis - Initial contribution
- */
-public class DriveState {
-
- public String active_route_destination;
- public double active_route_latitude;
- public double active_route_longitude;
- public double active_route_miles_to_arrival;
- public double active_route_minutes_to_arrival;
- public double active_route_traffic_minutes_delay;
- public double latitude;
- public double longitude;
- public double native_latitude;
- public double native_longitude;
- public int gps_as_of;
- public int heading;
- public int native_location_supported;
- public String native_type;
- public String shift_state;
- public String speed;
-
- DriveState() {
- }
-}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/GUIState.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/GUIState.java
deleted file mode 100644
index ebb73c9d57598..0000000000000
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/GUIState.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * 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.tesla.internal.protocol;
-
-/**
- * The {@link GUIState} is a datastructure to capture
- * variables sent by the Tesla Vehicle
- *
- * @author Karel Goderis - Initial contribution
- */
-public class GUIState {
-
- public String gui_distance_units;
- public String gui_temperature_units;
- public String gui_charge_rate_units;
- public String gui_24_hour_time;
- public String gui_range_display;
-
- public GUIState() {
- }
-}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/Vehicle.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/Vehicle.java
deleted file mode 100644
index a3e37f36a8cc0..0000000000000
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/Vehicle.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * 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.tesla.internal.protocol;
-
-/**
- * The {@link Vehicle} is a datastructure to capture
- * variables sent by the Tesla Vehicle
- *
- * @author Karel Goderis - Initial contribution
- */
-public class Vehicle {
-
- public String color;
- public String display_name;
- public String id;
- public String option_codes;
- public String vehicle_id;
- public String vin;
- public String[] tokens;
- public String state;
- public boolean remote_start_enabled;
- public boolean calendar_enabled;
- public boolean notifications_enabled;
- public String backseat_token;
- public String backseat_token_updated_at;
-
- Vehicle() {
- }
-}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/VehicleConfig.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/VehicleConfig.java
deleted file mode 100644
index 85ea588a75662..0000000000000
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/VehicleConfig.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * 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.tesla.internal.protocol;
-
-import org.openhab.binding.tesla.internal.TeslaBindingConstants;
-import org.openhab.core.thing.ThingTypeUID;
-
-/**
- * The {@link VehicleConfig} is a data structure to capture
- * vehicle configuration variables sent by the Tesla Vehicle
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class VehicleConfig {
- public boolean can_accept_navigation_requests;
- public boolean can_actuate_trunks;
- public boolean eu_vehicle;
- public boolean has_air_suspension;
- public boolean has_ludicrous_mode;
- public boolean motorized_charge_port;
- public boolean plg;
- public boolean rhd;
- public boolean use_range_badging;
- public int rear_seat_heaters;
- public int rear_seat_type;
- public int sun_roof_installed;
- public long timestamp;
- public String car_special_type;
- public String car_type;
- public String charge_port_type;
- public String exterior_color;
- public String roof_color;
- public String spoiler_type;
- public String third_row_seats;
- public String trim_badging;
- public String wheel_type;
-
- public ThingTypeUID identifyModel() {
- switch (car_type) {
- case "models":
- case "models2":
- return TeslaBindingConstants.THING_TYPE_MODELS;
- case "modelx":
- return TeslaBindingConstants.THING_TYPE_MODELX;
- case "model3":
- return TeslaBindingConstants.THING_TYPE_MODEL3;
- case "modely":
- return TeslaBindingConstants.THING_TYPE_MODELY;
- default:
- return TeslaBindingConstants.THING_TYPE_VEHICLE;
- }
- }
-}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/VehicleData.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/VehicleData.java
deleted file mode 100644
index 172ea20188cdc..0000000000000
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/VehicleData.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * 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.tesla.internal.protocol;
-
-/**
- * The {@link VehicleData} is a data structure to capture
- * variables sent by the Tesla API about a vehicle.
- *
- * @author Kai Kreuzer - Initial contribution
- */
-public class VehicleData {
-
- public String color;
- public String display_name;
- public String id;
- public String option_codes;
- public String vehicle_id;
- public String vin;
- public String[] tokens;
- public String state;
- public boolean remote_start_enabled;
- public boolean calendar_enabled;
- public boolean notifications_enabled;
- public String backseat_token;
- public String backseat_token_updated_at;
-
- public ChargeState charge_state;
- public ClimateState climate_state;
- public DriveState drive_state;
- public GUIState gui_settings;
- public VehicleConfig vehicle_config;
- public VehicleState vehicle_state;
-
- VehicleData() {
- }
-}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/VehicleState.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/VehicleState.java
deleted file mode 100644
index 38c7c175aa80e..0000000000000
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/VehicleState.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * 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.tesla.internal.protocol;
-
-/**
- * The {@link VehicleState} is a datastructure to capture
- * variables sent by the Tesla Vehicle
- *
- * @author Karel Goderis - Initial contribution
- */
-public class VehicleState {
-
- public boolean dark_rims;
- public boolean has_spoiler;
- public boolean homelink_nearby;
- public boolean is_user_present;
- public boolean locked;
- public boolean notifications_supported;
- public boolean parsed_calendar_supported;
- public boolean remote_start;
- public boolean remote_start_supported;
- public boolean rhd;
- public boolean sentry_mode;
- public boolean valet_mode;
- public boolean valet_pin_needed;
- public float odometer;
- public int center_display_state;
- public int df;
- public int dr;
- public int ft;
- public int pf;
- public int pr;
- public int rear_seat_heaters;
- public int rt;
- public int seat_type;
- public int sun_roof_installed;
- public int sun_roof_percent_open;
- public String autopark_state;
- public String autopark_state_v2;
- public String autopark_style;
- public String car_version;
- public String exterior_color;
- public String last_autopark_error;
- public String perf_config;
- public String roof_color;
- public String sun_roof_state;
- public String vehicle_name;
- public String wheel_type;
-
- public SoftwareUpdate software_update;
-
- VehicleState() {
- }
-}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/ChargeState.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/ChargeState.java
new file mode 100644
index 0000000000000..6cb5fa49312dc
--- /dev/null
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/ChargeState.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.tesla.internal.protocol.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link ChargeState} is a datastructure to capture
+ * variables sent by the Tesla Vehicle
+ *
+ * @author Karel Goderis - Initial contribution
+ */
+public class ChargeState {
+ @SerializedName("battery_heater_on")
+ public boolean batteryHeaterOn;
+ @SerializedName("charge_enable_request")
+ public boolean chargeEnableRequest;
+ @SerializedName("charge_port_door_open")
+ public boolean chargePortDoorOpen;
+ @SerializedName("charge_to_max_range")
+ public boolean chargeToMaxRange;
+ @SerializedName("eu_vehicle")
+ public boolean euVehicle;
+ @SerializedName("fast_charger_present")
+ public boolean fastChargerPresent;
+ @SerializedName("managed_charging_active")
+ public boolean managedChargingActive;
+ @SerializedName("managed_charging_user_canceled")
+ public boolean managedChargingUserCanceled;
+ @SerializedName("motorized_charge_port")
+ public boolean motorizedChargePort;
+ @SerializedName("not_enough_power_to_heat")
+ public boolean notEnoughPowerToHeat;
+ @SerializedName("scheduled_charging_pending")
+ public boolean scheduledChargingPending;
+ @SerializedName("trip_charging")
+ public boolean tripCharging;
+ @SerializedName("battery_current")
+ public float batteryCurrent;
+ @SerializedName("battery_range")
+ public float batteryRange;
+ @SerializedName("charge_energy_added")
+ public float chargeEnergyAdded;
+ @SerializedName("charge_miles_added_ideal")
+ public float chargeMilesAddedIdeal;
+ @SerializedName("charge_miles_added_rated")
+ public float chargeMilesAddedRated;
+ @SerializedName("aaacharge_rateaa")
+ public float chargeRate;
+ @SerializedName("est_battery_range")
+ public float estBatteryRange;
+ @SerializedName("ideal_battery_range")
+ public float idealBatteryRange;
+ @SerializedName("time_to_full_charge")
+ public float timeToFullCharge;
+ @SerializedName("battery_level")
+ public int batteryLevel;
+ @SerializedName("charge_amps")
+ public int chargeAmps;
+ @SerializedName("charge_current_request")
+ public int chargeCurrentRequest;
+ @SerializedName("charge_current_request_max")
+ public int chargeCurrentRequestMax;
+ @SerializedName("charge_limit_soc")
+ public int chargeLimitSoc;
+ @SerializedName("charge_limit_soc_max")
+ public int chargeLimitSocMax;
+ @SerializedName("charge_limit_soc_min")
+ public int chargeLimitSocMin;
+ @SerializedName("charge_limit_soc_std")
+ public int chargeLimitSocStd;
+ @SerializedName("charger_actual_current")
+ public int chargerActualCurrent;
+ @SerializedName("charger_phases")
+ public int chargerPhases;
+ @SerializedName("charger_pilot_current")
+ public int chargerPilotCurrent;
+ @SerializedName("charger_power")
+ public int chargerPower;
+ @SerializedName("charger_voltage")
+ public int chargerVoltage;
+ @SerializedName("max_range_charge_counter")
+ public int maxRangeChargeCounter;
+ @SerializedName("usable_battery_level")
+ public int usableBatteryLevel;
+ @SerializedName("charge_port_latch")
+ public String chargePortLatch;
+ @SerializedName("charging_state")
+ public String chargingState;
+ @SerializedName("conn_charge_cable")
+ public String connChargeCable;
+ @SerializedName("fast_charger_brand")
+ public String fastChargerBrand;
+ @SerializedName("fast_charger_type")
+ public String fastChargerType;
+ @SerializedName("managed_charging_start_time")
+ public String managedChargingStartTime;
+ @SerializedName("scheduled_charging_start_time")
+ public String scheduledChargingStartTime;
+ @SerializedName("user_charge_enable_request")
+ public String userChargeEnableRequest;
+
+ ChargeState() {
+ }
+}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/ClimateState.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/ClimateState.java
new file mode 100644
index 0000000000000..c8bea92971018
--- /dev/null
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/ClimateState.java
@@ -0,0 +1,92 @@
+/**
+ * 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.tesla.internal.protocol.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link ClimateState} is a datastructure to capture
+ * variables sent by the Tesla Vehicle
+ *
+ * @author Karel Goderis - Initial contribution
+ */
+public class ClimateState {
+
+ @SerializedName("allow_cabin_overheat_protection")
+ public boolean allowCabinOverheatProtection;
+ @SerializedName("auto_seat_climate_left")
+ public boolean autoSeatClimateLeft;
+ @SerializedName("auto_seat_climate_right")
+ public boolean autoSeatClimateRight;
+ @SerializedName("battery_heater")
+ public boolean batteryHeater;
+ @SerializedName("battery_heater_no_power")
+ public boolean batteryHeaterNoPower;
+ @SerializedName("cabin_overheat_protection")
+ public String cabinOverheatProtection;
+ @SerializedName("cabin_overheat_protection_actively_cooling")
+ public boolean cabinOverheatProtectionActivelyCooling;
+ @SerializedName("climate_keeper_mode")
+ public String climateKeeperMode;
+ @SerializedName("cop_activation_temperature")
+ public String copActivationTemperature;
+ @SerializedName("defrost_mode")
+ public int defrostMode;
+ @SerializedName("driver_temp_setting")
+ public float driverTempSetting;
+ @SerializedName("fan_status")
+ public int fanStatus;
+ @SerializedName("hvac_auto_request")
+ public String hvacAutoRequest;
+ @SerializedName("inside_temp")
+ public float insideTemp;
+ @SerializedName("is_auto_conditioning_on")
+ public boolean isAutoConditioningOn;
+ @SerializedName("is_climate_on")
+ public boolean isClimateOn;
+ @SerializedName("is_front_defroster_on")
+ public boolean isFrontDefrosterOn;
+ @SerializedName("is_preconditioning")
+ public boolean isPreconditioning;
+ @SerializedName("is_rear_defroster_on")
+ public boolean isRearDefrosterOn;
+ @SerializedName("left_temp_direction")
+ public int leftTempDirection;
+ @SerializedName("max_avail_temp")
+ public float maxAvailTemp;
+ @SerializedName("min_avail_temp")
+ public float minAvailTemp;
+ @SerializedName("outside_temp")
+ public float outsideTemp;
+ @SerializedName("passenger_temp_setting")
+ public float passengerTempSetting;
+ @SerializedName("remote_heater_control_enabled")
+ public boolean remoteHeaterControlEnabled;
+ @SerializedName("right_temp_direction")
+ public int rightTempDirection;
+ @SerializedName("seat_heater_left")
+ public int seatHeaterLeft;
+ @SerializedName("seat_heater_right")
+ public int seatHeaterRight;
+ @SerializedName("side_mirror_heaters")
+ public boolean sideMirrorHeaters;
+ @SerializedName("supports_fan_only_cabin_overheat_protection")
+ public boolean supportsFanOnlyCabinOverheatProtection;
+ @SerializedName("timestamp")
+ public long timestamp;
+ @SerializedName("wiper_blade_heater")
+ public boolean wiperBladeHeater;
+
+ ClimateState() {
+ }
+}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/DriveState.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/DriveState.java
new file mode 100644
index 0000000000000..0c452195b3e3e
--- /dev/null
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/DriveState.java
@@ -0,0 +1,52 @@
+/**
+ * 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.tesla.internal.protocol.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link DriveState} is a datastructure to capture
+ * variables sent by the Tesla Vehicle
+ *
+ * @author Karel Goderis - Initial contribution
+ */
+public class DriveState {
+
+ @SerializedName("gps_as_of")
+ public int gpsAsOf;
+ @SerializedName("heading")
+ public int heading;
+ @SerializedName("latitude")
+ public double latitude;
+ @SerializedName("longitude")
+ public double longitude;
+ @SerializedName("native_latitude")
+ public double nativeLatitude;
+ @SerializedName("native_location_supported")
+ public int nativeLocationSupported;
+ @SerializedName("native_longitude")
+ public double nativeLongitude;
+ @SerializedName("native_type")
+ public String nativeType;
+ @SerializedName("power")
+ public int power;
+ @SerializedName("shift_state")
+ public String shiftState;
+ @SerializedName("speed")
+ public String speed;
+ @SerializedName("timestamp")
+ public long timestamp;
+
+ DriveState() {
+ }
+}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/Event.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/Event.java
similarity index 73%
rename from bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/Event.java
rename to bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/Event.java
index 02e6fbf1802b6..bdb0bef47b62d 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/Event.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/Event.java
@@ -10,7 +10,9 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.tesla.internal.protocol;
+package org.openhab.binding.tesla.internal.protocol.dto;
+
+import com.google.gson.annotations.SerializedName;
/**
* The {@link Event} is a datastructure to capture
@@ -19,9 +21,11 @@
* @author Karel Goderis - Initial contribution
*/
public class Event {
- public String msg_type;
+ @SerializedName("msg_type")
+ public String msgType;
public String value;
public String tag;
- public String error_type;
+ @SerializedName("error_type")
+ public String errorType;
public int connectionTimeout;
}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/GUIState.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/GUIState.java
new file mode 100644
index 0000000000000..dea7ebf585286
--- /dev/null
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/GUIState.java
@@ -0,0 +1,43 @@
+/**
+ * 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.tesla.internal.protocol.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link GUIState} is a datastructure to capture
+ * variables sent by the Tesla Vehicle
+ *
+ * @author Karel Goderis - Initial contribution
+ */
+public class GUIState {
+
+ @SerializedName("gui_24_hour_time")
+ public boolean gui24HourTime;
+ @SerializedName("gui_charge_rate_units")
+ public String guiChargeRateUnits;
+ @SerializedName("gui_distance_units")
+ public String guiDistanceUnits;
+ @SerializedName("gui_range_display")
+ public String guiRangeDisplay;
+ @SerializedName("gui_temperature_units")
+ public String guiTemperatureUnits;
+ @SerializedName("show_range_units")
+ public boolean showRangeUnits;
+ @SerializedName("timestamp")
+
+ public Long timestamp;
+
+ public GUIState() {
+ }
+}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/SoftwareUpdate.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/SoftwareUpdate.java
similarity index 67%
rename from bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/SoftwareUpdate.java
rename to bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/SoftwareUpdate.java
index 8a58aada2a8ec..6e0103f627a3d 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/SoftwareUpdate.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/SoftwareUpdate.java
@@ -10,7 +10,9 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.tesla.internal.protocol;
+package org.openhab.binding.tesla.internal.protocol.dto;
+
+import com.google.gson.annotations.SerializedName;
/**
* The {@link SoftwareUpdate} is a datastructure to capture
@@ -20,9 +22,12 @@
*/
public class SoftwareUpdate {
- public int download_perc;
- public int expected_duration_sec;
- public int install_perc;
+ @SerializedName("download_perc")
+ public int downloadPerc;
+ @SerializedName("expected_duration_sec")
+ public int expectedDurationSec;
+ @SerializedName("install_perc")
+ public int installPerc;
public String status;
public String version;
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/Vehicle.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/Vehicle.java
new file mode 100644
index 0000000000000..5d6908a7388ae
--- /dev/null
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/Vehicle.java
@@ -0,0 +1,49 @@
+/**
+ * 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.tesla.internal.protocol.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link Vehicle} is a datastructure to capture
+ * variables sent by the Tesla Vehicle
+ *
+ * @author Karel Goderis - Initial contribution
+ */
+public class Vehicle {
+
+ public String color;
+ @SerializedName("display_name")
+ public String displayName;
+ public String id;
+ @SerializedName("option_codes")
+ public String optionCodes;
+ @SerializedName("vehicle_id")
+ public String vehicleId;
+ public String vin;
+ public String[] tokens;
+ public String state;
+ @SerializedName("remote_start_enabled")
+ public boolean remoteStartEnabled;
+ @SerializedName("calendar_enabled")
+ public boolean calendarEnabled;
+ @SerializedName("notifications_enabled")
+ public boolean notificationsEnabled;
+ @SerializedName("backseat_token")
+ public String backseatToken;
+ @SerializedName("backseat_token_updated_at")
+ public String backseatTokenUpdatedAt;
+
+ Vehicle() {
+ }
+}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/VehicleConfig.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/VehicleConfig.java
new file mode 100644
index 0000000000000..e1fdf9ecf1d6c
--- /dev/null
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/VehicleConfig.java
@@ -0,0 +1,92 @@
+/**
+ * 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.tesla.internal.protocol.dto;
+
+import org.openhab.binding.tesla.internal.TeslaBindingConstants;
+import org.openhab.core.thing.ThingTypeUID;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link VehicleConfig} is a data structure to capture
+ * vehicle configuration variables sent by the Tesla Vehicle
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class VehicleConfig {
+
+ @SerializedName("can_accept_navigation_requests")
+ public boolean canAcceptNavigationRequests;
+ @SerializedName("can_actuate_trunks")
+ public boolean canActuateTrunks;
+ @SerializedName("car_special_type")
+ public String carSpecialType;
+ @SerializedName("car_type")
+ public String carType;
+ @SerializedName("charge_port_type")
+ public String chargePortType;
+ @SerializedName("ece_restrictions")
+ public boolean eceRestrictions;
+ @SerializedName("eu_vehicle")
+ public boolean euVehicle;
+ @SerializedName("exterior_color")
+ public String exteriorColor;
+ @SerializedName("has_air_suspension")
+ public boolean hasAirSuspension;
+ @SerializedName("has_ludicrous_mode")
+ public boolean hasLudicrousMode;
+ @SerializedName("motorized_charge_port")
+ public boolean motorizedChargePort;
+ @SerializedName("plg")
+ public boolean plg;
+ @SerializedName("rear_seat_heaters")
+ public int rearSeatHeaters;
+ @SerializedName("rear_seat_type")
+ public int rearSeatType;
+ @SerializedName("rhd")
+ public boolean rhd;
+ @SerializedName("roof_color")
+ public String roofColor;
+ @SerializedName("seat_type")
+ public int seatType;
+ @SerializedName("spoiler_type")
+ public String spoilerType;
+ @SerializedName("sun_roof_installed")
+ public int sunRoofInstalled;
+ @SerializedName("third_row_seats")
+ public String thirdRowSeats;
+ @SerializedName("timestamp")
+ public Long timestamp;
+ @SerializedName("trim_badging")
+ public String trimBadging;
+ @SerializedName("use_range_badging")
+ public boolean useRangeBadging;
+ @SerializedName("wheel_type")
+ public String wheelType;
+
+ public ThingTypeUID identifyModel() {
+ switch (carType) {
+ case "models":
+ case "models2":
+ return TeslaBindingConstants.THING_TYPE_MODELS;
+ case "modelx":
+ return TeslaBindingConstants.THING_TYPE_MODELX;
+ case "model3":
+ return TeslaBindingConstants.THING_TYPE_MODEL3;
+ case "modely":
+ return TeslaBindingConstants.THING_TYPE_MODELY;
+ default:
+ return TeslaBindingConstants.THING_TYPE_VEHICLE;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/VehicleData.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/VehicleData.java
new file mode 100644
index 0000000000000..1c941ad86f594
--- /dev/null
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/VehicleData.java
@@ -0,0 +1,72 @@
+/**
+ * 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.tesla.internal.protocol.dto;
+
+import java.util.List;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link VehicleData} is a data structure to capture
+ * variables sent by the Tesla API about a vehicle.
+ *
+ * @author Kai Kreuzer - Initial contribution
+ */
+public class VehicleData {
+
+ @SerializedName("id")
+ public Long id;
+ @SerializedName("user_id")
+ public int userId;
+ @SerializedName("vehicle_id")
+ public String vehicleId;
+ @SerializedName("vin")
+ public String vin;
+ @SerializedName("display_name")
+ public String displayName;
+ @SerializedName("color")
+ public Object color;
+ @SerializedName("access_type")
+ public String accessType;
+ @SerializedName("tokens")
+ public List tokens;
+ @SerializedName("state")
+ public String state;
+ @SerializedName("in_service")
+ public boolean inService;
+ @SerializedName("id_s")
+ public String idS;
+ @SerializedName("calendar_enabled")
+ public boolean calendarEnabled;
+ @SerializedName("api_version")
+ public int apiVersion;
+ @SerializedName("backseat_token")
+ public Object backseatToken;
+ @SerializedName("backseat_token_updated_at")
+ public Object backseatTokenUpdatedAt;
+ @SerializedName("charge_state")
+ public ChargeState chargeState;
+ @SerializedName("climate_state")
+ public ClimateState climateState;
+ @SerializedName("drive_state")
+ public DriveState driveState;
+ @SerializedName("gui_settings")
+ public GUIState guiSettings;
+ @SerializedName("vehicle_config")
+ public VehicleConfig vehicleConfig;
+ @SerializedName("vehicle_state")
+ public VehicleState vehicleState;
+
+ VehicleData() {
+ }
+}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/VehicleState.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/VehicleState.java
new file mode 100644
index 0000000000000..7a5e66b61289c
--- /dev/null
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/VehicleState.java
@@ -0,0 +1,94 @@
+/**
+ * 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.tesla.internal.protocol.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link VehicleState} is a datastructure to capture
+ * variables sent by the Tesla Vehicle
+ *
+ * @author Karel Goderis - Initial contribution
+ */
+public class VehicleState {
+
+ @SerializedName("dark_rims")
+ public boolean darkRims;
+ @SerializedName("has_spoiler")
+ public boolean hasSpoiler;
+ @SerializedName("homelink_nearby")
+ public boolean homelinkNearby;
+ @SerializedName("is_user_present")
+ public boolean isUserPresent;
+ public boolean locked;
+ @SerializedName("notifications_supported")
+ public boolean notificationsSupported;
+ @SerializedName("parsed_calendar_supported")
+ public boolean parsedCalendarSupported;
+ @SerializedName("remote_start")
+ public boolean remoteStart;
+ @SerializedName("remote_start_supported")
+ public boolean remoteStartSupported;
+ public boolean rhd;
+ @SerializedName("sentry_mode")
+ public boolean sentryMode;
+ @SerializedName("valet_mode")
+ public boolean valetMode;
+ @SerializedName("valet_pin_needed")
+ public boolean valetPinNeeded;
+ public float odometer;
+ @SerializedName("center_display_state")
+ public int centerDisplayState;
+ public int df;
+ public int dr;
+ public int ft;
+ public int pf;
+ public int pr;
+ @SerializedName("rear_seat_heaters")
+ public int rearSeatHeaters;
+ public int rt;
+ @SerializedName("seat_type")
+ public int seatType;
+ @SerializedName("sun_roof_installed")
+ public int sunRoofInstalled;
+ @SerializedName("sun_roof_percent_open")
+ public int sunRoofPercentOpen;
+ @SerializedName("autopark_state")
+ public String autoparkState;
+ @SerializedName("autopark_state_v2")
+ public String autoparkStateV2;
+ @SerializedName("autopark_style")
+ public String autoparkStyle;
+ @SerializedName("car_version")
+ public String carVersion;
+ @SerializedName("exterior_color")
+ public String exteriorColor;
+ @SerializedName("last_autopark_error")
+ public String lastAutoparkError;
+ @SerializedName("perf_config")
+ public String perfConfig;
+ @SerializedName("roof_color")
+ public String roofColor;
+ @SerializedName("sun_roof_state")
+ public String sunRoofState;
+ @SerializedName("vehicle_name")
+ public String vehicleName;
+ @SerializedName("wheel_type")
+ public String wheelType;
+
+ @SerializedName("software_update")
+ public SoftwareUpdate softwareUpdate;
+
+ VehicleState() {
+ }
+}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/AuthorizationCodeExchangeRequest.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/sso/AuthorizationCodeExchangeRequest.java
similarity index 67%
rename from bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/AuthorizationCodeExchangeRequest.java
rename to bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/sso/AuthorizationCodeExchangeRequest.java
index 07ac2d4e5b0cc..eba92c637f083 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/AuthorizationCodeExchangeRequest.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/sso/AuthorizationCodeExchangeRequest.java
@@ -10,10 +10,12 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.tesla.internal.protocol.sso;
+package org.openhab.binding.tesla.internal.protocol.dto.sso;
import static org.openhab.binding.tesla.internal.TeslaBindingConstants.*;
+import com.google.gson.annotations.SerializedName;
+
/**
* The {@link AuthorizationCodeExchangeRequest} is a datastructure to exchange
* the authorization code for an access token on the SSO endpoint
@@ -22,14 +24,18 @@
*/
@SuppressWarnings("unused") // Unused fields must not be removed since they are used for serialization to JSON
public class AuthorizationCodeExchangeRequest {
- private String grant_type = "authorization_code";
- private String client_id = CLIENT_ID;
+ @SerializedName("grant_type")
+ private String grantType = "authorization_code";
+ @SerializedName("client_id")
+ private String clientId = CLIENT_ID;
private String code;
- private String code_verifier;
- private String redirect_uri = URI_CALLBACK;
+ @SerializedName("code_verifier")
+ private String codeVerifier;
+ @SerializedName("redirect_uri")
+ private String redirectUri = URI_CALLBACK;
public AuthorizationCodeExchangeRequest(String code, String codeVerifier) {
this.code = code;
- this.code_verifier = codeVerifier;
+ this.codeVerifier = codeVerifier;
}
}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/AuthorizationCodeExchangeResponse.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/sso/AuthorizationCodeExchangeResponse.java
similarity index 63%
rename from bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/AuthorizationCodeExchangeResponse.java
rename to bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/sso/AuthorizationCodeExchangeResponse.java
index ff767af29c2aa..3eaee54176227 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/AuthorizationCodeExchangeResponse.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/sso/AuthorizationCodeExchangeResponse.java
@@ -10,7 +10,9 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.tesla.internal.protocol.sso;
+package org.openhab.binding.tesla.internal.protocol.dto.sso;
+
+import com.google.gson.annotations.SerializedName;
/**
* The {@link AuthorizationCodeExchangeResponse} is a datastructure to capture
@@ -19,9 +21,13 @@
* @author Christian Güdel - Initial contribution
*/
public class AuthorizationCodeExchangeResponse {
- public String access_token;
- public String refresh_token;
- public String expires_in;
+ @SerializedName("access_token")
+ public String accessToken;
+ @SerializedName("refresh_token")
+ public String refreshToken;
+ @SerializedName("expires_in")
+ public String expiresIn;
public String state;
- public String token_type;
+ @SerializedName("token_type")
+ public String tokenType;
}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/RefreshTokenRequest.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/sso/RefreshTokenRequest.java
similarity index 62%
rename from bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/RefreshTokenRequest.java
rename to bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/sso/RefreshTokenRequest.java
index 47bb477588573..b307caba78f15 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/RefreshTokenRequest.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/sso/RefreshTokenRequest.java
@@ -10,10 +10,12 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.tesla.internal.protocol.sso;
+package org.openhab.binding.tesla.internal.protocol.dto.sso;
import static org.openhab.binding.tesla.internal.TeslaBindingConstants.*;
+import com.google.gson.annotations.SerializedName;
+
/**
* The {@link RefreshTokenRequest} is a datastructure to refresh
* the access token for the SSO endpoint
@@ -21,12 +23,15 @@
* @author Christian Güdel - Initial contribution
*/
public class RefreshTokenRequest {
- public String grant_type = "refresh_token";
- public String client_id = CLIENT_ID;
- public String refresh_token;
+ @SerializedName("grant_type")
+ public String grantType = "refresh_token";
+ @SerializedName("client_id")
+ public String clientId = CLIENT_ID;
+ @SerializedName("refresh_token")
+ public String refreshToken;
public String scope = SSO_SCOPES;
- public RefreshTokenRequest(String refresh_token) {
- this.refresh_token = refresh_token;
+ public RefreshTokenRequest(String refreshToken) {
+ this.refreshToken = refreshToken;
}
}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/TokenExchangeRequest.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/sso/TokenExchangeRequest.java
similarity index 55%
rename from bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/TokenExchangeRequest.java
rename to bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/sso/TokenExchangeRequest.java
index 1835d9d9766b9..23e8f8c68fa5e 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/TokenExchangeRequest.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/sso/TokenExchangeRequest.java
@@ -10,7 +10,9 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.tesla.internal.protocol.sso;
+package org.openhab.binding.tesla.internal.protocol.dto.sso;
+
+import com.google.gson.annotations.SerializedName;
/**
* The {@link TokenExchangeRequest} is a datastructure to exchange
@@ -19,7 +21,10 @@
* @author Christian Güdel - Initial contribution
*/
public class TokenExchangeRequest {
- public String grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer";
- public String client_id = "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384";
- public String client_secret = "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3";
+ @SerializedName("grant_type")
+ public String grantType = "urn:ietf:params:oauth:grant-type:jwt-bearer";
+ @SerializedName("client_id")
+ public String clientId = "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384";
+ @SerializedName("client_secret")
+ public String clientSecret = "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3";
}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/TokenResponse.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/sso/TokenResponse.java
similarity index 58%
rename from bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/TokenResponse.java
rename to bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/sso/TokenResponse.java
index 7fa98aa7534b6..1948c9265ca88 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/TokenResponse.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/dto/sso/TokenResponse.java
@@ -10,7 +10,9 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.binding.tesla.internal.protocol.sso;
+package org.openhab.binding.tesla.internal.protocol.dto.sso;
+
+import com.google.gson.annotations.SerializedName;
/**
* The {@link TokenResponse} is a datastructure to capture
@@ -19,12 +21,16 @@
* @author Nicolai Grødum - Initial contribution
*/
public class TokenResponse {
-
- public String access_token;
- public String token_type;
- public Long expires_in;
- public Long created_at;
- public String refresh_token;
+ @SerializedName("access_token")
+ public String accessToken;
+ @SerializedName("token_type")
+ public String tokenType;
+ @SerializedName("expires_in")
+ public Long expiresIn;
+ @SerializedName("created_at")
+ public Long createdAt;
+ @SerializedName("refresh_token")
+ public String refreshToken;
public TokenResponse() {
}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/AbstractChannelThrottler.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/AbstractChannelThrottler.java
index fe477366b6a5d..72c5ef1d50bef 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/AbstractChannelThrottler.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/AbstractChannelThrottler.java
@@ -16,12 +16,16 @@
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
/**
* The {@link AbstractChannelThrottler} is abstract class implementing a
* throttler with one global execution rate, or rate limiter
*
* @author Karel Goderis - Initial contribution
*/
+@NonNullByDefault
abstract class AbstractChannelThrottler implements ChannelThrottler {
protected final Rate totalRate;
@@ -37,7 +41,7 @@ protected AbstractChannelThrottler(Rate totalRate, ScheduledExecutorService sche
this.timeProvider = timeProvider;
}
- protected synchronized long callTime(Rate channel) {
+ protected synchronized long callTime(@Nullable Rate channel) {
long now = timeProvider.getCurrentTimeInMillis();
long callTime = totalRate.callTime(now);
if (channel != null) {
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/AbstractMultiRateChannelThrottler.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/AbstractMultiRateChannelThrottler.java
index 2798dbaad469c..90835d730254b 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/AbstractMultiRateChannelThrottler.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/AbstractMultiRateChannelThrottler.java
@@ -18,12 +18,16 @@
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
/**
* The {@link AbstractMultiRateChannelThrottler} is abstract class implementing
* a throttler with multiple global execution rates, or rate limiters
*
* @author Karel Goderis - Initial contribution
*/
+@NonNullByDefault
abstract class AbstractMultiRateChannelThrottler implements ChannelThrottler {
protected final TimeProvider timeProvider;
@@ -43,7 +47,7 @@ public synchronized void addRate(Rate rate) {
this.rates.add(rate);
}
- protected synchronized long callTime(Rate channel) {
+ protected synchronized long callTime(@Nullable Rate channel) {
long maxCallTime = 0;
long finalCallTime = 0;
long now = timeProvider.getCurrentTimeInMillis();
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/ChannelThrottler.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/ChannelThrottler.java
index 8e26c2e747878..f5e8e62751854 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/ChannelThrottler.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/ChannelThrottler.java
@@ -14,14 +14,20 @@
import java.util.concurrent.Future;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
/**
* The {@link ChannelThrottler} defines the interface for to submit tasks to a
* throttler
*
* @author Karel Goderis - Initial contribution
*/
+@NonNullByDefault
public interface ChannelThrottler {
+ @Nullable
Future> submit(Runnable task);
+ @Nullable
Future> submit(Object channelKey, Runnable task);
}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/QueueChannelThrottler.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/QueueChannelThrottler.java
index 14ab02885f280..766c620187087 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/QueueChannelThrottler.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/QueueChannelThrottler.java
@@ -22,6 +22,8 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -31,6 +33,7 @@
*
* @author Karel Goderis - Initial contribution
*/
+@NonNullByDefault
public final class QueueChannelThrottler extends AbstractMultiRateChannelThrottler {
private final Logger logger = LoggerFactory.getLogger(QueueChannelThrottler.class);
@@ -71,13 +74,13 @@ public QueueChannelThrottler(Rate someRate, ScheduledExecutorService scheduler,
}
@Override
- public Future> submit(Runnable task) {
+ public @Nullable Future> submit(Runnable task) {
return submit(null, task);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
- public Future> submit(Object channelKey, Runnable task) {
+ public @Nullable Future> submit(@Nullable Object channelKey, Runnable task) {
FutureTask runTask = new FutureTask(task, null);
try {
if (tasks.offer(runTask, overallRate.timeInMillis(), TimeUnit.MILLISECONDS)) {
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/Rate.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/Rate.java
index 8ccf681f18418..c45866430ccba 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/Rate.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/Rate.java
@@ -17,6 +17,8 @@
import java.util.ListIterator;
import java.util.concurrent.TimeUnit;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
/**
* The {@link Rate} defines a rate limiter that accepts a number of calls to be
* executed in a given time length. If the quota of calls is used, then calls
@@ -24,6 +26,7 @@
*
* @author Karel Goderis - Initial contribution
*/
+@NonNullByDefault
public final class Rate {
private final int numberCalls;
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/ScheduledChannelThrottler.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/ScheduledChannelThrottler.java
index 6e357a225f31a..6873cc59efdb6 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/ScheduledChannelThrottler.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/ScheduledChannelThrottler.java
@@ -19,6 +19,9 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
/**
* The {@link ScheduledChannelThrottler} implements a throttler that maintains a
* single execution rates, and does not maintain order of calls (thus has to
@@ -26,6 +29,7 @@
*
* @author Karel Goderis - Initial contribution
*/
+@NonNullByDefault
public final class ScheduledChannelThrottler extends AbstractChannelThrottler {
public ScheduledChannelThrottler(Rate totalRate) {
@@ -53,13 +57,13 @@ public void submitSync(Runnable task) throws InterruptedException {
}
@Override
- public Future> submit(Runnable task) {
+ public @Nullable Future> submit(Runnable task) {
long delay = callTime(null) - timeProvider.getCurrentTimeInMillis();
return scheduler.schedule(task, delay < 0 ? 0 : delay, TimeUnit.MILLISECONDS);
}
@Override
- public Future> submit(Object channelKey, Runnable task) {
+ public @Nullable Future> submit(Object channelKey, Runnable task) {
return scheduler.schedule(task, getThrottleDelay(channelKey), TimeUnit.MILLISECONDS);
}
}
diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/TimeProvider.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/TimeProvider.java
index bf4dbc28fa41f..386fbcb34b7c8 100644
--- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/TimeProvider.java
+++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/throttler/TimeProvider.java
@@ -12,11 +12,14 @@
*/
package org.openhab.binding.tesla.internal.throttler;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
/**
* The {@link TimeProvider} provides time stamps
*
* @author Karel Goderis - Initial contribution
*/
+@NonNullByDefault
public interface TimeProvider {
static final TimeProvider SYSTEM_PROVIDER = new TimeProvider() {
@Override
diff --git a/bundles/org.openhab.binding.touchwand/src/main/java/org/openhab/binding/touchwand/internal/discovery/TouchWandControllerDiscoveryService.java b/bundles/org.openhab.binding.touchwand/src/main/java/org/openhab/binding/touchwand/internal/discovery/TouchWandControllerDiscoveryService.java
index ffd4adfde3dba..6e7cc9d2c390b 100644
--- a/bundles/org.openhab.binding.touchwand/src/main/java/org/openhab/binding/touchwand/internal/discovery/TouchWandControllerDiscoveryService.java
+++ b/bundles/org.openhab.binding.touchwand/src/main/java/org/openhab/binding/touchwand/internal/discovery/TouchWandControllerDiscoveryService.java
@@ -122,6 +122,7 @@ private class ReceiverThread extends Thread {
private DatagramSocket mySocket;
public ReceiverThread(DatagramSocket socket) {
+ super(String.format("OH-binding-%s-%s", TouchWandBindingConstants.BINDING_ID, "Receiver"));
mySocket = socket;
}
diff --git a/bundles/org.openhab.binding.tplinksmarthome/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.tplinksmarthome/src/main/resources/OH-INF/thing/channels.xml
index 5a132e769788f..5b819d00e3893 100644
--- a/bundles/org.openhab.binding.tplinksmarthome/src/main/resources/OH-INF/thing/channels.xml
+++ b/bundles/org.openhab.binding.tplinksmarthome/src/main/resources/OH-INF/thing/channels.xml
@@ -51,7 +51,7 @@
- Number:Dimensionless
+ Number:Dimensionless
Filter Life Remaining
Indicator of the remaining filter life
-
+
@@ -169,6 +175,7 @@
Auto
Manual Fan Control
Sleeping Auto
+ Pet Auto
@@ -188,14 +195,14 @@
- Number:Dimensionless
+ Number:Dimensionless
Fan Speed
Indicator of the current fan speed
- Number:Dimensionless
+ Number:Dimensionless
Device Error Code
Indicator of the current error code of the device
@@ -209,7 +216,7 @@
- Number:Density
+ Number:Density
Air Quality PPM2.5
Indicator of current air quality
@@ -221,6 +228,20 @@
Configuration: If the devices display is enabled forever
+
+ Switch
+ Light Detection
+ If the devices light detection is enabled
+
+
+
+
+ Switch
+ Light Detected
+ Indicator if the device detects light
+
+
+
String
Config: Auto Mode
@@ -242,14 +263,14 @@
- Number:Dimensionless
+ Number:Area
Config: Room size
- Room size (foot sq) for efficient auto mode
-
+ Room size for efficient auto mode
+
- Number:Dimensionless
+ Number:Dimensionless
Config: Schedules Count
The current number of schedules configured
@@ -299,7 +320,7 @@
- Number:Dimensionless
+ Number:Dimensionless
Mist Level
System representation of mist level
@@ -326,11 +347,10 @@
- Number:Dimensionless
+ Number:Dimensionless
Warm Level
Warm Level
-
diff --git a/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/update/air-humidifier-instructions.xml b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/update/air-humidifier-instructions.xml
new file mode 100644
index 0000000000000..fa054fa01ec75
--- /dev/null
+++ b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/update/air-humidifier-instructions.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ vesync:deviceAFConfigAutoScheduleCountType
+
+
+ vesync:deviceAFTimerExpiry
+
+
+
+
diff --git a/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/update/air-purifier-instructions.xml b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/update/air-purifier-instructions.xml
new file mode 100644
index 0000000000000..377a7b310d90d
--- /dev/null
+++ b/bundles/org.openhab.binding.vesync/src/main/resources/OH-INF/update/air-purifier-instructions.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+ vesync:deviceAFLightDetection
+
+
+ vesync:deviceAFLightDetected
+
+
+ vesync:airPurifierModeType
+
+
+
+
diff --git a/bundles/org.openhab.binding.vitotronic/src/main/java/org/openhab/binding/vitotronic/internal/handler/VitotronicBridgeHandler.java b/bundles/org.openhab.binding.vitotronic/src/main/java/org/openhab/binding/vitotronic/internal/handler/VitotronicBridgeHandler.java
index 27536b376a6ea..5c18d79855aa6 100644
--- a/bundles/org.openhab.binding.vitotronic/src/main/java/org/openhab/binding/vitotronic/internal/handler/VitotronicBridgeHandler.java
+++ b/bundles/org.openhab.binding.vitotronic/src/main/java/org/openhab/binding/vitotronic/internal/handler/VitotronicBridgeHandler.java
@@ -276,8 +276,7 @@ private void startSocketReceiver() {
if (!isConnect) {
openSocket();
- Thread thread = new Thread(socketReceiverRunnable);
- thread.setName("VitotronicSocketThread");
+ Thread thread = new Thread(socketReceiverRunnable, "OH-binding-" + getThing().getUID() + "-SocketThread");
thread.start();
}
}
diff --git a/bundles/org.openhab.binding.wiz/NOTICE b/bundles/org.openhab.binding.wiz/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/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.wiz/README.md b/bundles/org.openhab.binding.wiz/README.md
new file mode 100644
index 0000000000000..55d2816f61d3d
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/README.md
@@ -0,0 +1,150 @@
+# WiZ Binding
+
+This binding integrates [WiZ Connected](https://www.wizconnected.com/en-US/) smart devices.
+These inexpensive devices, typically smart bulbs, are available online and in most Home Depot stores.
+They come in a variety of bulb shapes and sizes with options of full color with tunable white, tunable white, and dimmable white.
+This binding has been tested with various bulbs and switchable plugs.
+They are sold under the Philips brand name.
+(Wiz is owned by Signify (formerly Philips Lighting).)
+*Note* that while both are sold by Philips, WiZ bulbs are *not* part of the Hue ecosystem.
+
+This binding operates completely within the local network - the discovery, control, and status monitoring is entirely over UDP in the local network.
+The binding never attempts to contact the WiZ servers in any way but does not stop them from doing so independently.
+It should not interfere in any way with control of the bulbs via the WiZ app or any other service integrated with the WiZ app (e.g. Alexa, IFTTT, SmartThings).
+Any changes made to the bulb state outside of openHAB should be detected by the binding and vice-versa.
+Before using the binding, the bulbs must be set up using the WiZ iOS or Android app.
+Local control must also be enabled with-in the WiZ app in the app settings.
+(This is the default.)
+
+## Supported Things
+
+- WiZ Full Color with Tunable White Bulbs
+- WiZ Tunable White Bulbs
+- WiZ Dimmable single-color bulbs
+- WiZ Smart Plugs
+- Smart fans (with or without a dimmable light)
+
+**NOTE:** This binding was created for and tested on the full color with tunable white bulbs, however, users have reported success with other bulb types and plugs.
+
+## Discovery
+
+New devices can be discovered by scanning and may also be discovered by background discovery.
+All discovered devices will default to 'Full Color' bulbs if unable to automatically detect the specific device type.
+You may need to create devices manually if desired.
+
+Devices must first have been set up using the WiZ iOS or Android app.
+If the binding cannot discover your device, try unplugging it, wait several seconds, and plug it back in.
+
+## Binding Configuration
+
+The binding does not require any special configuration.
+You can optionally manually set the IP and MAC address of the openHAB instance; if you do not set them, the binding will use the system defaults.
+
+## Thing Configuration
+
+To create or configure a device manually you need its IP address and MAC address.
+These can be quickly found in the iOS or Android app by entering the settings for device in question and clicking on the model name.
+The refresh interval may also be set; if unset it defaults to 30 seconds.
+If you desire instant updates, you may also enable "heart-beat" synchronization with the bulbs.
+Heart-beats are not used by default.
+When heart-beats are enabled, the binding will continuously re-register with the bulbs to receive sync packets on every state change and on every 5 seconds.
+Enabling heart-beats causes the refresh-interval to be ignored.
+If heart-beats are not enabled, the channels are only updated when polled at the set interval and thus will be slightly delayed with regard to changes made to the bulb state outside of the binding (e.g. via the WiZ app).
+
+**NOTE:** While the bulb's IP address is needed for initial manual configuration, this binding _does not_ require you to use a static IP for each bulb.
+After initial discovery or setup, the binding will automatically search for and re-match bulbs with changed IP addresses by MAC address once every hour.
+
+Thing parameters:
+
+| Parameter ID | Parameter Type | Mandatory | Description | Default |
+|-------------------|----------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
+| macAddress | text | true | The MAC address of the bulb | |
+| ipAddress | text | true | The IP of the bulb | |
+| updateInterval | integer | false | Update time interval in seconds to request the status of the bulb. | 60 |
+| useHeartBeats | boolean | false | Whether to register for continuous 5s heart-beats | false |
+| reconnectInterval | integer | false | Interval in minutes between attempts to reconnect with a bulb that is no longer responding to status queries. When the bulb first connects to the network, it should send out a firstBeat message allowing openHAB to immediately detect it. This is only as a back-up to re-find the bulb. | 15 |
+
+Example Thing:
+
+```java
+Thing wiz:bulb:lamp "My Lamp" @ "Living Room" [ macAddress="accf23343cxx", ipAddress="192.168.0.xx" ]
+```
+
+## Channels
+
+The binding supports the following channels. If a device is only a light or only a fan, the channels will
+not be in a group.
+
+| Channel ID | Item Type | Description | Access |
+|------------------------|----------------------|-------------------------------------------------------|--------|
+| light#color | Color | State, intensity, and color of the LEDs | R/W |
+| light#temperature | Dimmer | Color temperature of the bulb | R/W |
+| light#temperature-abs | Number:Temperature | Color temperature of the bulb in Kelvin | R/W |
+| light#brightness | Dimmer | The brightness of the bulb | R/W |
+| light#state | Switch | Whether the bulb is on or off | R/W |
+| light#light-mode | Number | Preset light mode name to run | R/W |
+| light#speed | Dimmer | Speed of the color changes in dynamic light modes | R/W |
+| fan#state | Switch | Whether the fan is on or off | R/W |
+| fan#speed | Number | Speed of the fan, in arbitrary steps | R/W |
+| fan#reverse | Switch | Whether the fan direction is reversed | R/W |
+| fan#mode | Number | Special fan modes (Breeze) | R/W |
+| device#last-update | Time | The last time an an update was received from the bulb | R |
+| device#signal-strength | Number | Quality of the bulb's WiFi connection | R |
+| device#rssi | Number:Dimensionless | WiFi Received Signal Strength Indicator (in dB) | R |
+
+## Light Modes
+
+The binding supports the following Light Modes
+
+| ID | Scene Name |
+|----|---------------|
+| 1 | Ocean |
+| 2 | Romance |
+| 3 | Sunset |
+| 4 | Party |
+| 5 | Fireplace |
+| 6 | Cozy White |
+| 7 | Forest |
+| 8 | Pastel Colors |
+| 9 | Wakeup |
+| 10 | Bed Time |
+| 11 | Warm White |
+| 12 | Daylight |
+| 13 | Cool White |
+| 14 | Night Light |
+| 15 | Focus |
+| 16 | Relax |
+| 17 | True Colors |
+| 18 | TV Time |
+| 19 | Plant Growth |
+| 20 | Spring |
+| 21 | Summer |
+| 22 | Fall |
+| 23 | Deep Dive |
+| 24 | Jungle |
+| 25 | Mojito |
+| 26 | Club |
+| 27 | Christmas |
+| 28 | Halloween |
+| 29 | Candlelight |
+| 30 | Golden White |
+| 31 | Pulse |
+| 32 | Steampunk |
+
+## Bulb Limitations
+
+- Full-color bulbs operate in either color mode OR tunable white/color temperature mode.
+The RGB LED's are NOT used to control temperature - separate warm and cool white LED's are used.
+Sending a command on the color channel or the temperature channel will cause the bulb to switch the relevant mode.
+- Dimmable bulbs do not dim below 10%.
+- The binding attempts to immediately retrieve the actual state from the device after each command is acknowledged, sometimes this means your settings don't 'stick' this is because the device itself did not accept the command or setting.
+- Parameters can not be changed while the bulbs are off, sending any commands to change any settings will cause the bulbs to turn on.
+- Power on behavior is configured in the app.
+- Fade in/out times are configured in the app.
+- Sending too many commands to the bulbs too quickly can cause them to stop responding for a period of time.
+
+## Example Item Linked To a Channel
+
+```java
+Color LivingRoom_Light_Color "Living Room Lamp" (gLivingroom) {channel="wiz:color-bulb:accf23343cxx:color"}
+```
diff --git a/bundles/org.openhab.binding.wiz/pom.xml b/bundles/org.openhab.binding.wiz/pom.xml
new file mode 100644
index 0000000000000..f1940b89af96c
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 4.3.0-SNAPSHOT
+
+
+ org.openhab.binding.wiz
+
+ openHAB Add-ons :: Bundles :: WiZ Binding
+
+
diff --git a/bundles/org.openhab.binding.wiz/src/main/feature/feature.xml b/bundles/org.openhab.binding.wiz/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..7f4ccfbea2b4c
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/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.wiz/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizBindingConstants.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizBindingConstants.java
new file mode 100644
index 0000000000000..d3ffd29364a27
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizBindingConstants.java
@@ -0,0 +1,140 @@
+/**
+ * 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.wiz.internal;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link WizBindingConstants} class defines common constants, which
+ * are used across the whole binding.
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ * @author Joshua Freeman - update version
+ */
+@NonNullByDefault
+public class WizBindingConstants {
+
+ /**
+ * The binding id.
+ */
+ public static final String BINDING_ID = "wiz";
+
+ /**
+ * List of all Thing Type UIDs.
+ */
+ public static final ThingTypeUID THING_TYPE_COLOR_BULB = new ThingTypeUID(BINDING_ID, "color-bulb");
+ public static final ThingTypeUID THING_TYPE_TUNABLE_BULB = new ThingTypeUID(BINDING_ID, "tunable-bulb");
+ public static final ThingTypeUID THING_TYPE_DIMMABLE_BULB = new ThingTypeUID(BINDING_ID, "dimmable-bulb");
+ public static final ThingTypeUID THING_TYPE_SMART_PLUG = new ThingTypeUID(BINDING_ID, "plug");
+ public static final ThingTypeUID THING_TYPE_FAN = new ThingTypeUID(BINDING_ID, "fan");
+ public static final ThingTypeUID THING_TYPE_FAN_WITH_DIMMABLE_BULB = new ThingTypeUID(BINDING_ID,
+ "fan-with-dimmable-bulb");
+
+ /**
+ * The supported thing types.
+ */
+ public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_COLOR_BULB, THING_TYPE_TUNABLE_BULB,
+ THING_TYPE_DIMMABLE_BULB, THING_TYPE_SMART_PLUG, THING_TYPE_FAN, THING_TYPE_FAN_WITH_DIMMABLE_BULB);
+
+ /**
+ * List of all Channel ids
+ */
+ public static final String CHANNEL_BRIGHTNESS = "brightness";
+ public static final String CHANNEL_COLOR = "color";
+ public static final String CHANNEL_LAST_UPDATE = "last-update";
+ public static final String CHANNEL_MODE = "mode";
+ public static final String CHANNEL_REVERSE = "reverse";
+ public static final String CHANNEL_RSSI = "rssi";
+ public static final String CHANNEL_SIGNAL_STRENGTH = "signal-strength";
+ public static final String CHANNEL_SPEED = "speed";
+ public static final String CHANNEL_STATE = "state";
+ public static final String CHANNEL_TEMPERATURE = "temperature";
+ public static final String CHANNEL_TEMPERATURE_ABS = "temperature-abs";
+
+ public static final String CHANNEL_GROUP_DEVICE = "device";
+ public static final String CHANNEL_GROUP_LIGHT = "light";
+ public static final String CHANNEL_GROUP_FAN = "fan";
+
+ // -------------- Configuration arguments ----------------
+ /**
+ * Mac address configuration argument key.
+ */
+ public static final String CONFIG_MAC_ADDRESS = "macAddress";
+
+ /**
+ * Host address configuration argument key.
+ */
+ public static final String CONFIG_IP_ADDRESS = "ipAddress";
+
+ /**
+ * Wifi socket update interval configuration argument key.
+ */
+ public static final String CONFIG_UPDATE_INTERVAL = "updateInterval";
+ public static final long DEFAULT_REFRESH_INTERVAL_SEC = 60;
+
+ /**
+ * Wifi socket update interval configuration argument key.
+ */
+ public static final String CONFIG_RECONNECT_INTERVAL = "reconnectInterval";
+ public static final long DEFAULT_RECONNECT_INTERVAL_MIN = 15;
+
+ // -------------- Default values ----------------
+
+ /**
+ * The number of refresh intervals without a response before a bulb is marked
+ * offline
+ */
+ public static final int MARK_OFFLINE_AFTER_SEC = 5 * 60;
+
+ /**
+ * Default Wifi socket default UDP port.
+ */
+ public static final int DEFAULT_UDP_PORT = 38899;
+
+ /**
+ * Default listener socket default UDP port.
+ */
+ public static final int DEFAULT_LISTENER_UDP_PORT = 38900;
+
+ /**
+ * How long before active discovery times out.
+ */
+ public static final int DISCOVERY_TIMEOUT_SECONDS = 2;
+
+ // -------------- Constants Used ----------------
+
+ /**
+ * The color temperature range of the WiZ bulbs
+ */
+ public static final int MIN_COLOR_TEMPERATURE = 2200;
+ public static final int MAX_COLOR_TEMPERATURE = 6500;
+
+ // -------------- Bulb Properties ----------------
+
+ public static final String PROPERTY_IP_ADDRESS = "ipAddress";
+
+ public static final String PROPERTY_HOME_ID = "homeId";
+ public static final String PROPERTY_ROOM_ID = "roomId";
+ public static final String PROPERTY_HOME_LOCK = "homeLock";
+ public static final String PROPERTY_PAIRING_LOCK = "pairingLock";
+ public static final String PROPERTY_TYPE_ID = "typeId";
+ public static final String PROPERTY_MODULE_NAME = "moduleName";
+ public static final String PROPERTY_GROUP_ID = "groupId";
+
+ public static final String EXPECTED_MODULE_NAME = "ESP01_SHRGB1C_31";
+ public static final String LAST_KNOWN_FIRMWARE_VERSION = "1.18.0";
+ public static final String MODEL_CONFIG_MINIMUM_FIRMWARE_VERSION = "1.22";
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizHandlerFactory.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizHandlerFactory.java
new file mode 100644
index 0000000000000..d15abedfb0075
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizHandlerFactory.java
@@ -0,0 +1,83 @@
+/**
+ * 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.wiz.internal;
+
+import static org.openhab.binding.wiz.internal.WizBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.wiz.internal.handler.WizHandler;
+import org.openhab.binding.wiz.internal.handler.WizMediator;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link WizHandlerFactory} is responsible for creating things and
+ * thing handlers.
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.wiz", service = ThingHandlerFactory.class)
+public class WizHandlerFactory extends BaseThingHandlerFactory {
+ private final Logger logger = LoggerFactory.getLogger(WizHandlerFactory.class);
+
+ private final WizMediator mediator;
+ private final WizStateDescriptionProvider stateDescriptionProvider;
+ private final TimeZoneProvider timeZoneProvider;
+
+ @Activate
+ public WizHandlerFactory(@Reference WizMediator mediator,
+ @Reference WizStateDescriptionProvider stateDescriptionProvider,
+ @Reference TimeZoneProvider timeZoneProvider) {
+ this.mediator = mediator;
+ this.stateDescriptionProvider = stateDescriptionProvider;
+ this.timeZoneProvider = timeZoneProvider;
+ }
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(final Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (supportsThingType(thing.getThingTypeUID())) {
+ WizHandler handler;
+
+ handler = new WizHandler(thing, mediator, stateDescriptionProvider, timeZoneProvider);
+
+ mediator.registerThingAndWizBulbHandler(thing, handler);
+ return handler;
+ } else {
+ logger.warn("Thing type {} not supported.", thingTypeUID);
+ }
+ return null;
+ }
+
+ @Override
+ public void unregisterHandler(final Thing thing) {
+ mediator.unregisterWizBulbHandlerByThing(thing);
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizStateDescriptionProvider.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizStateDescriptionProvider.java
new file mode 100644
index 0000000000000..9d462a7982c82
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizStateDescriptionProvider.java
@@ -0,0 +1,73 @@
+/**
+ * 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.wiz.internal;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
+import org.openhab.core.types.StateDescription;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provide a dynamic state description for color temp to define the min/max as provided by the
+ * actual bulb.
+ * This service is started on-demand only, as soon as {@link WizThingHandlerFactory} requires it.
+ *
+ * @author Cody Cutrer - Initial contribution
+ */
+@Component(service = { DynamicStateDescriptionProvider.class, WizStateDescriptionProvider.class })
+@NonNullByDefault
+public class WizStateDescriptionProvider implements DynamicStateDescriptionProvider {
+
+ private final Map stateDescriptions = new ConcurrentHashMap<>();
+ private final Logger logger = LoggerFactory.getLogger(WizStateDescriptionProvider.class);
+
+ /**
+ * Set a state description for a channel. This description will be used when preparing the channel state by
+ * the framework for presentation. A previous description, if existed, will be replaced.
+ *
+ * @param channelUID channel UID
+ * @param description state description for the channel
+ */
+ public void setDescription(ChannelUID channelUID, StateDescription description) {
+ logger.debug("Adding state description for channel {}: {}", channelUID, description);
+ stateDescriptions.put(channelUID, description);
+ }
+
+ @Override
+ public @Nullable StateDescription getStateDescription(Channel channel,
+ @Nullable StateDescription originalStateDescription, @Nullable Locale locale) {
+ StateDescription description = stateDescriptions.get(channel.getUID());
+ if (description != null) {
+ logger.trace("Providing state description for channel {}", channel.getUID());
+ }
+ return description;
+ }
+
+ /**
+ * Removes the given channel description.
+ *
+ * @param channel The channel
+ */
+ public void remove(ChannelUID channel) {
+ stateDescriptions.remove(channel);
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/config/WizDeviceConfiguration.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/config/WizDeviceConfiguration.java
new file mode 100644
index 0000000000000..abf108fdd42cd
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/config/WizDeviceConfiguration.java
@@ -0,0 +1,36 @@
+/**
+ * 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.wiz.internal.config;
+
+import static org.openhab.binding.wiz.internal.WizBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link BondHomeConfiguration} class contains fields mapping thing
+ * configuration parameters.
+ *
+ * @author Sara Geleskie Damiano - Initial contribution
+ */
+@NonNullByDefault
+public class WizDeviceConfiguration {
+
+ /**
+ * Configuration for a WiZ Device
+ */
+ public String macAddress = "";
+ public String ipAddress = "";
+ public long updateInterval = DEFAULT_REFRESH_INTERVAL_SEC;
+ public boolean useHeartBeats = false; // true: register to get 5s heart-beats
+ public long reconnectInterval = DEFAULT_RECONNECT_INTERVAL_MIN;
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/discovery/WizDiscoveryService.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/discovery/WizDiscoveryService.java
new file mode 100644
index 0000000000000..6f980523be433
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/discovery/WizDiscoveryService.java
@@ -0,0 +1,271 @@
+/**
+ * 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.wiz.internal.discovery;
+
+import static org.openhab.binding.wiz.internal.WizBindingConstants.*;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.SocketTimeoutException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+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.wiz.internal.entities.RegistrationRequestParam;
+import org.openhab.binding.wiz.internal.entities.SystemConfigResult;
+import org.openhab.binding.wiz.internal.entities.WizRequest;
+import org.openhab.binding.wiz.internal.entities.WizResponse;
+import org.openhab.binding.wiz.internal.enums.WizMethodType;
+import org.openhab.binding.wiz.internal.handler.WizMediator;
+import org.openhab.binding.wiz.internal.utils.WizPacketConverter;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is the {@link DiscoveryService} for the WiZ Things.
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ * @author Joshua Freeman - use configured Broadcast address instead of guessing, discovery of plugs
+ *
+ */
+@Component(configurationPid = "discovery.wiz", service = DiscoveryService.class, immediate = true)
+@NonNullByDefault
+public class WizDiscoveryService extends AbstractDiscoveryService {
+
+ private final Logger logger = LoggerFactory.getLogger(WizDiscoveryService.class);
+
+ private final WizMediator mediator;
+
+ private final WizPacketConverter converter = new WizPacketConverter();
+
+ private @Nullable ScheduledFuture> backgroundDiscovery;
+
+ /**
+ * Constructor of the discovery service.
+ *
+ * @throws IllegalArgumentException if the timeout < 0
+ */
+ @Activate
+ public WizDiscoveryService(
+ @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.STATIC) WizMediator mediator)
+ throws IllegalArgumentException {
+ super(SUPPORTED_THING_TYPES, DISCOVERY_TIMEOUT_SECONDS, true);
+ this.mediator = mediator;
+ mediator.setDiscoveryService(this);
+ }
+
+ @Override
+ public Set getSupportedThingTypes() {
+ return SUPPORTED_THING_TYPES;
+ }
+
+ /**
+ * This method is called when {@link AbstractDiscoveryService#setBackgroundDiscoveryEnabled(boolean)}
+ * is called with true as parameter and when the component is being activated
+ * (see {@link AbstractDiscoveryService#activate()}.
+ *
+ * This will also serve to "re-discover" any devices that have changed to a new IP address.
+ */
+ @Override
+ protected void startBackgroundDiscovery() {
+ ScheduledFuture> backgroundDiscovery = this.backgroundDiscovery;
+ if (backgroundDiscovery == null || backgroundDiscovery.isCancelled()) {
+ this.backgroundDiscovery = scheduler.scheduleWithFixedDelay(this::startScan, 1, 60, TimeUnit.MINUTES);
+ }
+ }
+
+ @Override
+ protected void stopBackgroundDiscovery() {
+ ScheduledFuture> backgroundDiscovery = this.backgroundDiscovery;
+ if (backgroundDiscovery != null && !backgroundDiscovery.isCancelled()) {
+ backgroundDiscovery.cancel(true);
+ this.backgroundDiscovery = null;
+ }
+ }
+
+ @Override
+ protected void startScan() {
+ DatagramSocket dsocket = null;
+ try {
+ String broadcastIp = this.mediator.getNetworkAddressService().getConfiguredBroadcastAddress();
+ if (broadcastIp != null) {
+ InetAddress address = InetAddress.getByName(broadcastIp);
+ RegistrationRequestParam registrationRequestParam = mediator.getRegistrationParams();
+ WizRequest request = new WizRequest(WizMethodType.Registration, registrationRequestParam);
+ request.setId(0);
+
+ byte[] message = this.converter.transformToByteMessage(request);
+
+ // Initialize a datagram packet with data and address
+ DatagramPacket packet = new DatagramPacket(message, message.length, address, DEFAULT_UDP_PORT);
+
+ // Create a datagram socket, send the packet through it, close it.
+ // For discovery we will "fire and forget" and let the mediator take care of the
+ // responses
+ dsocket = new DatagramSocket();
+ dsocket.send(packet);
+ logger.debug("Broadcast packet to address: {} and port {}", address, DEFAULT_UDP_PORT);
+ } else {
+ logger.warn("No broadcast address was configured or discovered! No broadcast sent.");
+ }
+ } catch (IllegalStateException e) {
+ logger.debug("Unable to start background scan: {}", e.getMessage());
+ } catch (IOException exception) {
+ logger.debug("Something wrong happened when broadcasting the packet to port {}... msg: {}",
+ DEFAULT_UDP_PORT, exception.getMessage());
+ } finally {
+ if (dsocket != null) {
+ dsocket.close();
+ }
+ }
+ }
+
+ /**
+ * Method called by mediator, after receiving a packet from an unknown WiZ device
+ *
+ * @param macAddress the mac address from the device.
+ * @param ipAddress the host address from the device.
+ */
+ public void discoveredLight(final String macAddress, final String ipAddress) {
+ Map properties = new HashMap<>(2);
+ properties.put(CONFIG_MAC_ADDRESS, macAddress);
+ properties.put(CONFIG_IP_ADDRESS, ipAddress);
+ logger.trace("New device discovered at {} with MAC {}. Requesting configuration info from it.", ipAddress,
+ macAddress);
+
+ // Assume it is a full color bulb, unless we get confirmation otherwise.
+ // This will ensure the maximum number of channels will be created so there's no
+ // missing functionality.
+ // There's nothing a simple dimmable bulb can do that a full color bulb can't.
+ // It's easy for a user to ignore or not link anything to a non-working channel,
+ // but impossible to add a new channel if it's wanted.
+ // The bulbs will merely ignore or return an error for specific commands they
+ // cannot carry-out (ie, setting color on a non-color bulb) and continue to
+ // function as they were before the bad command.
+ ThingTypeUID thisDeviceType = THING_TYPE_COLOR_BULB;
+ String thisDeviceLabel = "WiZ Full Color Bulb at " + ipAddress;
+ ThingUID newThingId = new ThingUID(thisDeviceType, macAddress);
+
+ WizResponse configResponse = getDiscoveredDeviceConfig(ipAddress);
+ if (configResponse != null) {
+ SystemConfigResult discoveredDeviceConfig = configResponse.getSystemConfigResults();
+ if (discoveredDeviceConfig != null) {
+ String discoveredModel = discoveredDeviceConfig.moduleName.toUpperCase();
+ logger.trace("Returned model from discovered device at {}: {}", ipAddress, discoveredModel);
+
+ // “moduleName”:“ESP10_SOCKET_06” confirmed example module name for Wiz Smart Plug
+ // Check for "SOCKET" this seems safe based on other naming conventions observed
+ if (discoveredModel.contains("SOCKET")) {
+ thisDeviceType = THING_TYPE_SMART_PLUG;
+ thisDeviceLabel = "WiZ Smart Plug at " + ipAddress;
+ newThingId = new ThingUID(thisDeviceType, macAddress);
+ logger.trace("New device appears to be a smart plug and will be given the UUID: {}", newThingId);
+
+ // We'll try to key off "TW" for tunable white
+ } else if (discoveredModel.contains("TW")) {
+ thisDeviceType = THING_TYPE_TUNABLE_BULB;
+ thisDeviceLabel = "WiZ Tunable White Bulb at " + ipAddress;
+ newThingId = new ThingUID(thisDeviceType, macAddress);
+ logger.trace("New device appears to be a tunable white bulb and will be given the UUID: {}",
+ newThingId);
+
+ // Check for "FANDIMS" as in confirmed example ESP03_FANDIMS_31 for Faro Barcelona Smart Fan
+ } else if (discoveredModel.contains("FANDIMS")) {
+ thisDeviceType = THING_TYPE_FAN_WITH_DIMMABLE_BULB;
+ thisDeviceLabel = "WiZ Smart Fan at " + ipAddress;
+ newThingId = new ThingUID(thisDeviceType, macAddress);
+ logger.trace("New device appears to be a smart fan and will be given the UUID: {}", newThingId);
+
+ // We key off "RGB" for color bulbs
+ } else if (!discoveredModel.contains("RGB")) {
+ thisDeviceType = THING_TYPE_DIMMABLE_BULB;
+ thisDeviceLabel = "WiZ Dimmable White Bulb at " + ipAddress;
+ newThingId = new ThingUID(thisDeviceType, macAddress);
+ logger.trace(
+ "New device appears not to be either tunable white bulb or full color and will be called a dimmable only bulb and given the UUID: {}",
+ newThingId);
+ }
+
+ DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(newThingId).withProperties(properties)
+ .withLabel(thisDeviceLabel).withRepresentationProperty(CONFIG_MAC_ADDRESS).build();
+
+ this.thingDiscovered(discoveryResult);
+ }
+ } else {
+ logger.trace(
+ "Couldn't get or couldn't parse configuration information from discovered device. Discovery result will not be created.");
+ }
+ }
+
+ private synchronized @Nullable WizResponse getDiscoveredDeviceConfig(final String lightIpAddress) {
+ DatagramSocket dsocket = null;
+ try {
+ WizRequest request = new WizRequest(WizMethodType.GetSystemConfig, null);
+ request.setId(1);
+
+ byte[] message = this.converter.transformToByteMessage(request);
+
+ // Initialize a datagram packet with data and address
+ InetAddress address = InetAddress.getByName(lightIpAddress);
+ DatagramPacket packet = new DatagramPacket(message, message.length, address, DEFAULT_UDP_PORT);
+
+ // Create a datagram socket, send the packet through it, close it.
+ dsocket = new DatagramSocket();
+ dsocket.send(packet);
+ logger.debug("Sent packet to address: {} and port {}", address, DEFAULT_UDP_PORT);
+
+ byte[] responseMessage = new byte[1024];
+ packet = new DatagramPacket(responseMessage, responseMessage.length);
+ dsocket.receive(packet);
+
+ return converter.transformResponsePacket(packet);
+ } catch (SocketTimeoutException e) {
+ logger.trace("Socket timeout after sending command; no response from {} within 500ms", lightIpAddress);
+ } catch (IOException exception) {
+ logger.debug("Something wrong happened when sending the packet to address: {} and port {}... msg: {}",
+ lightIpAddress, DEFAULT_UDP_PORT, exception.getMessage());
+ } finally {
+ if (dsocket != null) {
+ dsocket.close();
+ }
+ }
+ return null;
+ }
+
+ // SETTERS AND GETTERS
+ /**
+ * Gets the {@link WizMediator} of this binding.
+ *
+ * @return {@link WizMediator}.
+ */
+ public WizMediator getMediator() {
+ return this.mediator;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ColorRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ColorRequestParam.java
new file mode 100644
index 0000000000000..b2f7bb41e4af3
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ColorRequestParam.java
@@ -0,0 +1,105 @@
+/**
+ * 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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.wiz.internal.utils.WizColorConverter;
+import org.openhab.core.library.types.HSBType;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents Color Request Param
+ *
+ * Outgoing JSON should look like this:
+ *
+ * {"id": 24, "method": "setPilot", "params": {"r": 0, "g": 230, "b": 80, "w":
+ * 130, "c": 0, "dimming": 12}}
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class ColorRequestParam extends DimmingRequestParam {
+ @Expose
+ private int r; // red 0-255
+ @Expose
+ private int g; // green 0-255
+ @Expose
+ private int b; // blue 0-255
+ @Expose
+ private int w; // warm white LED's 0-255
+ @Expose
+ private int c; // cool white LED's 0-255
+ @Expose(serialize = false, deserialize = false)
+ private WizColorConverter colorConverter = new WizColorConverter();
+
+ public ColorRequestParam(int r, int g, int b, int w, int c, int dimming) {
+ super(dimming);
+ this.r = r;
+ this.g = g;
+ this.b = b;
+ this.w = w;
+ this.c = c;
+ }
+
+ public ColorRequestParam(HSBType hsb) {
+ super(hsb.getBrightness().intValue());
+ int rgbw[] = colorConverter.hsbToRgbw(hsb);
+ this.r = rgbw[0];
+ this.g = rgbw[1];
+ this.b = rgbw[2];
+ this.w = rgbw[3];
+ this.c = 0;
+ }
+
+ public int getB() {
+ return b;
+ }
+
+ public void setB(int b) {
+ this.b = b;
+ }
+
+ public int getG() {
+ return g;
+ }
+
+ public void setG(int g) {
+ this.g = g;
+ }
+
+ public int getR() {
+ return r;
+ }
+
+ public void setR(int r) {
+ this.r = r;
+ }
+
+ public int getW() {
+ return w;
+ }
+
+ public void setW(int w) {
+ this.w = w;
+ }
+
+ public int getC() {
+ return c;
+ }
+
+ public void setC(int c) {
+ this.c = c;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ColorTemperatureRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ColorTemperatureRequestParam.java
new file mode 100644
index 0000000000000..ba0dd4faf5063
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ColorTemperatureRequestParam.java
@@ -0,0 +1,45 @@
+/**
+ * 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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents Color Request Param
+ *
+ * The outgoing JSON should look like this:
+ *
+ * {"id": 24, "method": "setPilot", "params": {"temp": 3000}}
+ *
+ * @author Alexander Seeliger - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class ColorTemperatureRequestParam implements Param {
+ @Expose
+ private int temp;
+
+ public ColorTemperatureRequestParam(int temp) {
+ this.temp = temp;
+ }
+
+ public int getColorTemperature() {
+ return temp;
+ }
+
+ public void setColorTemperature(int temp) {
+ this.temp = temp;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/DimmingRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/DimmingRequestParam.java
new file mode 100644
index 0000000000000..64359cf059cb4
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/DimmingRequestParam.java
@@ -0,0 +1,55 @@
+/**
+ * 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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents Dimming Request Param
+ *
+ * The outgoing JSON should look like this:
+ *
+ * {"id": 24, "method": "setPilot", "params": {"dimming": 10}}
+ *
+ * NOTE: Dimming cannot be set below 10%. Sending a command with a value of less
+ * than 10 will cause the bulb to reply with an error.
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class DimmingRequestParam extends StateRequestParam {
+ @Expose
+ private int dimming;
+
+ public DimmingRequestParam(int dimming) {
+ super(true);
+ setDimming(dimming);
+ }
+
+ public int getDimming() {
+ return dimming;
+ }
+
+ public void setDimming(int dimming) {
+ if (dimming <= 10) {
+ dimming = 10;
+ }
+ if (dimming >= 100) {
+ dimming = 100;
+ }
+ this.dimming = dimming;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ErrorResponseResult.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ErrorResponseResult.java
new file mode 100644
index 0000000000000..d7d355ab4fdd8
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ErrorResponseResult.java
@@ -0,0 +1,38 @@
+/**
+ * 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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents the "result" of one WiZ Response "results" are
+ * returned from registration, pulse, setPilot, and (presumably) setSysConfig
+ * commands
+ *
+ * Incoming JSON might look like this:
+ *
+ * {"env":"pro","error":{"code":-32700,"message":"Parse error"}}
+ *
+ * @author Sara Geleskie - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class ErrorResponseResult {
+ @Expose
+ public int code;
+ @Expose
+ public @Nullable String message;
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanModeRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanModeRequestParam.java
new file mode 100644
index 0000000000000..b7f3185c05994
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanModeRequestParam.java
@@ -0,0 +1,40 @@
+/**
+ * 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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents Fan Reverse Request Param
+ *
+ * @author Cody Cutrer - Initial Contribution
+ */
+@NonNullByDefault
+public class FanModeRequestParam implements Param {
+ @Expose
+ private int fanMode; // true = 1, false = 0
+
+ public FanModeRequestParam(int fanMode) {
+ this.fanMode = fanMode;
+ }
+
+ public int getFanMode() {
+ return fanMode;
+ }
+
+ public void setFanMode(int fanMode) {
+ this.fanMode = fanMode;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanReverseRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanReverseRequestParam.java
new file mode 100644
index 0000000000000..18788f33f1f26
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanReverseRequestParam.java
@@ -0,0 +1,40 @@
+/**
+ * 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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents Fan Reverse Request Param
+ *
+ * @author Cody Cutrer - Initial Contribution
+ */
+@NonNullByDefault
+public class FanReverseRequestParam implements Param {
+ @Expose
+ private int fanRevrs; // true = 1, false = 0
+
+ public FanReverseRequestParam(int reverse) {
+ this.fanRevrs = reverse;
+ }
+
+ public int getReverse() {
+ return fanRevrs;
+ }
+
+ public void setReverse(int reverse) {
+ this.fanRevrs = reverse;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanSpeedRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanSpeedRequestParam.java
new file mode 100644
index 0000000000000..1c090420a09b5
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanSpeedRequestParam.java
@@ -0,0 +1,40 @@
+/**
+ * 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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents Fan Speed Request Param
+ *
+ * @author Cody Cutrer - Initial Contribution
+ */
+@NonNullByDefault
+public class FanSpeedRequestParam implements Param {
+ @Expose
+ private int fanSpeed; // 0-6
+
+ public FanSpeedRequestParam(int fanSpeed) {
+ this.fanSpeed = fanSpeed;
+ }
+
+ public int getFanSpeed() {
+ return fanSpeed;
+ }
+
+ public void setFanSpeed(int fanSpeed) {
+ this.fanSpeed = fanSpeed;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanStateRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanStateRequestParam.java
new file mode 100644
index 0000000000000..049e48a9564db
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanStateRequestParam.java
@@ -0,0 +1,40 @@
+/**
+ * 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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents Fan State Request Param
+ *
+ * @author Stefan Fussenegger - Initial Contribution
+ */
+@NonNullByDefault
+public class FanStateRequestParam implements Param {
+ @Expose
+ private int fanState; // true = 1, false = 0
+
+ public FanStateRequestParam(int fanState) {
+ this.fanState = fanState;
+ }
+
+ public int getFanState() {
+ return fanState;
+ }
+
+ public void setFanState(int fanState) {
+ this.fanState = fanState;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FirstBeatResponseParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FirstBeatResponseParam.java
new file mode 100644
index 0000000000000..1631dc626253e
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FirstBeatResponseParam.java
@@ -0,0 +1,42 @@
+/**
+ * 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.wiz.internal.entities;
+
+import static org.openhab.binding.wiz.internal.WizBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents the "params" returned in a "firstBeat"
+ *
+ * The incoming JSON looks like this:
+ *
+ * {"method": "firstBeat", "id": 0, "env": "pro", "params": {"mac": "theBulbMacAddress",
+ * "homeId": xxxxxx, "fwVersion": "1.15.2"}}
+ *
+ * @author Sara Geleskie Damiano - Initial contribution
+ */
+@NonNullByDefault
+public class FirstBeatResponseParam {
+ // The MAC address the response is coming from
+ @Expose
+ public String mac = "";
+ // Home ID of the bulb
+ @Expose(serialize = false)
+ public int homeId;
+ // Firmware version of the bulb
+ @Expose
+ public String fwVersion = LAST_KNOWN_FIRMWARE_VERSION;
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ModelConfigResult.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ModelConfigResult.java
new file mode 100644
index 0000000000000..d52114d54c622
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ModelConfigResult.java
@@ -0,0 +1,69 @@
+/**
+ * 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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents the "result" of one request for a bulb's model
+ * configuration
+ *
+ * {
+ * "method": "getModelConfig", "id": 1, "env":"pro",
+ * "result": {
+ * "ps":2, "pwmFreq":1000, "pwmRes":11, "pwmRange":[0,100],
+ * "wcr":20, "nowc":1, "cctRange": [1800,2100,2100,2100],
+ * "renderFactor": [120,255,255,255,0,0,20,90,255,255], "hasCctTable": 6,
+ * "wizc1": {
+ * "mode": [0,0,0,0,0,0,2100],
+ * "opts": { "dim": 100 }
+ * },
+ * "wizc2": {
+ * "mode": [0,0,0,0,0,0,2100],
+ * "opts": { "dim": 50 }
+ * },
+ * "drvIface":4,
+ * "i2cDrv": [
+ * {
+ * "chip": "BP5758D",
+ * "addr": 255,
+ * "freq": 200,
+ * "curr": [30,30,30,36,36],
+ * "output":[3,2,1,4,5]
+ * }, {
+ * "chip": "NONE",
+ * "addr": 0,
+ * "freq": 0,
+ * "curr": [0,0,0,0,0],
+ * "output": [0,0,0,0,0]
+ * }, {
+ * "chip": "NONE",
+ * "addr": 0,
+ * "freq": 0,
+ * "curr": [0,0,0,0,0],
+ * "output":[0,0,0,0,0]
+ * }
+ * ]
+ * }
+ * }
+ *
+ * @author Cody Cutrer - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class ModelConfigResult {
+ @Expose
+ public int[] cctRange = {};
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/Param.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/Param.java
new file mode 100644
index 0000000000000..f086955880119
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/Param.java
@@ -0,0 +1,26 @@
+/**
+ * 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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This POJO represents an abstract Request Param
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ *
+ */
+@NonNullByDefault
+public interface Param {
+
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/PulseRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/PulseRequestParam.java
new file mode 100644
index 0000000000000..aa146a7554366
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/PulseRequestParam.java
@@ -0,0 +1,56 @@
+/**
+ * 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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents Pulse Request Param
+ *
+ * The outgoing JSON should look like this:
+ *
+ * {"id": 22, "method": "pulse", "params": {"delta": 30, "duration": 900}}
+ *
+ * @author Sara Geleskie Damiano - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class PulseRequestParam implements Param {
+ @Expose
+ private int delta;
+ @Expose
+ private int duration;
+
+ public PulseRequestParam(int delta, int duration) {
+ this.delta = delta;
+ this.duration = duration;
+ }
+
+ public int getDelta() {
+ return delta;
+ }
+
+ public void setDelta(int delta) {
+ this.delta = delta;
+ }
+
+ public int getDuration() {
+ return duration;
+ }
+
+ public void setDuration(int duration) {
+ this.delta = duration;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/RegistrationRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/RegistrationRequestParam.java
new file mode 100644
index 0000000000000..e7dd90b7b8b86
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/RegistrationRequestParam.java
@@ -0,0 +1,84 @@
+/**
+ * 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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents Registration request param
+ *
+ * The outgoing JSON should look like this:
+ *
+ * {"id": 22, "method": "registration", "params": {"phoneIp": "10.0.0.xx",
+ * "register": true, "homeId": xxx, "phoneMac": "xxx"}}
+ *
+ * NOTE: This can be sent directly to a single bulb or as a UDP broadcast. When
+ * sent as a broadcast, all bulbs in the network should respond.
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class RegistrationRequestParam implements Param {
+ @Expose
+ private String phoneIp;
+ @Expose
+ private boolean register;
+ // NOTE: We are NOT exposing the Home id for serialization because it's not
+ // necessary and it's a PITA to find it
+ @Expose(serialize = false)
+ private int homeId;
+ @Expose
+ private String phoneMac;
+
+ public RegistrationRequestParam(String phoneIp, boolean register, int homeId, String phoneMac) {
+ this.phoneIp = phoneIp;
+ this.register = register;
+ this.homeId = homeId;
+ this.phoneMac = phoneMac;
+ }
+
+ public String getPhoneIp() {
+ return phoneIp;
+ }
+
+ public void setPhoneIp(String phoneIp) {
+ this.phoneIp = phoneIp;
+ }
+
+ public boolean getRegister() {
+ return register;
+ }
+
+ public void setRegister(boolean register) {
+ this.register = register;
+ }
+
+ public int getHomeId() {
+ return homeId;
+ }
+
+ public void setHomeId(int homeId) {
+ this.homeId = homeId;
+ }
+
+ public String getPhoneMac() {
+ return phoneMac;
+ }
+
+ public void setPhoneMac(String phoneMac) {
+ this.phoneMac = phoneMac;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SceneRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SceneRequestParam.java
new file mode 100644
index 0000000000000..d0c5aa7301188
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SceneRequestParam.java
@@ -0,0 +1,46 @@
+/**
+ * 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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents one Scene Request Param
+ *
+ * The outgoing JSON should look like this:
+ *
+ * {"id": 22, "method": "setPilot", "params": {"sceneId": 3}} *
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class SceneRequestParam extends StateRequestParam {
+ @Expose
+ private int sceneId;
+
+ public SceneRequestParam(int sceneId) {
+ super(true);
+ this.sceneId = sceneId;
+ }
+
+ public int getSceneId() {
+ return sceneId;
+ }
+
+ public void setSceneId(int sceneId) {
+ this.sceneId = sceneId;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SpeedRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SpeedRequestParam.java
new file mode 100644
index 0000000000000..568b9307a0367
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SpeedRequestParam.java
@@ -0,0 +1,49 @@
+/**
+ * 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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents Speed Request Param
+ *
+ * The outgoing JSON should look like this:
+ *
+ * {"id": 23, "method": "setPilot", "params": {"sceneId":3,"speed": 20}}
+ *
+ * NOTE: A sceneId MUST also be specified in the request or the bulb will reply
+ * with an error.
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class SpeedRequestParam extends SceneRequestParam {
+ @Expose
+ private int speed;
+
+ public SpeedRequestParam(int sceneId, int speed) {
+ super(sceneId);
+ this.speed = speed;
+ }
+
+ public int getSpeed() {
+ return speed;
+ }
+
+ public void setSpeed(int speed) {
+ this.speed = speed;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/StateRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/StateRequestParam.java
new file mode 100644
index 0000000000000..390bbc2626e09
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/StateRequestParam.java
@@ -0,0 +1,41 @@
+/**
+ * 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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents State Request Param
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class StateRequestParam implements Param {
+ @Expose
+ private boolean state; // true = ON, false = OFF
+
+ public StateRequestParam(boolean state) {
+ this.state = state;
+ }
+
+ public boolean getState() {
+ return state;
+ }
+
+ public void setState(boolean state) {
+ this.state = state;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SystemConfigResult.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SystemConfigResult.java
new file mode 100644
index 0000000000000..638063cba044a
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SystemConfigResult.java
@@ -0,0 +1,74 @@
+/**
+ * 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.wiz.internal.entities;
+
+import static org.openhab.binding.wiz.internal.WizBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents the "result" of one request for a bulb's system
+ * configuration I assume the same packet could be used as the param of a
+ * 'setSystemConfig' request, but I'm not willing to risk ruining my bulbs by
+ * trying it.
+ *
+ * The incoming JSON looks like this:
+ *
+ * {"method": "getSystemConfig", "id": 22, "env": "pro", "result": {"mac":
+ * "theBulbMacAddress", "homeId": xxxxxx, "roomId": xxxxxx, "homeLock": false,
+ * "pairingLock": false, "typeId": 0, "moduleName": "ESP01_SHRGB1C_31",
+ * "fwVersion": "1.15.2", "groupId": 0, "drvConf":[33,1]}}
+ *
+ * @author Sara Geleskie Damiano - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class SystemConfigResult {
+ // The MAC address the response is coming from
+ @Expose
+ public String mac = "";
+ // Home ID of the bulb
+ @Expose
+ public int homeId;
+ // The ID of room the bulb is assigned to
+ @Expose
+ public int roomId;
+ // Not sure what the home lock is
+ @Expose
+ public boolean homeLock;
+ // Also not sure about the pairing lock
+ @Expose
+ public boolean pairingLock;
+ // Obviously a type ID
+ // The value is 0 for both BR30 and A19 full color bulbs
+ @Expose
+ public int typeId;
+ // The module name
+ // The value is "ESP01_SHRGB1C_31" for both BR30 and A19 full color bulbs
+ @Expose
+ public String moduleName = EXPECTED_MODULE_NAME;
+ // Firmware version of the bulb
+ @Expose
+ public String fwVersion = LAST_KNOWN_FIRMWARE_VERSION;
+ // The ID of group the bulb is assigned to
+ // I don't know how to group bulbs, all of mine return 0
+ @Expose
+ public int groupId;
+ // Not sure what the numbers mean
+ // For a full color A19 I get [33,1]
+ // For a full coloer BR30 I get [37,1]
+ @Expose
+ public int[] drvConf = {};
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizRequest.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizRequest.java
new file mode 100644
index 0000000000000..c98d76fba16c4
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizRequest.java
@@ -0,0 +1,77 @@
+/**
+ * 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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.wiz.internal.enums.WizMethodType;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents one WiZ UDP Request.
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class WizRequest {
+ @Expose
+ private int id;
+
+ @Expose
+ private WizMethodType method;
+
+ @Expose(serialize = false, deserialize = false)
+ private String methodName;
+
+ @Expose(deserialize = false)
+ private @Nullable Param params;
+
+ /**
+ * Default constructor.
+ *
+ * @param type the {@link WizMethodType}
+ * @param params {@link Param}
+ */
+ public WizRequest(final WizMethodType method, final @Nullable Param params) {
+ this.method = method;
+ this.methodName = method.getMethodName();
+ this.params = params;
+ }
+
+ public @Nullable Param getParams() {
+ return this.params;
+ }
+
+ public void setParams(final Param params) {
+ this.params = params;
+ }
+
+ public WizMethodType getMethod() {
+ return this.method;
+ }
+
+ public void setMethod(final WizMethodType method) {
+ this.method = method;
+ this.methodName = method.getMethodName();
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizResponse.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizResponse.java
new file mode 100644
index 0000000000000..1801c8b2989b6
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizResponse.java
@@ -0,0 +1,159 @@
+/**
+ * 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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.wiz.internal.enums.WizMethodType;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents one WiZ Response
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class WizResponse {
+
+ // The IP address we're coming from
+ @Expose(deserialize = true)
+ private String wizResponseIpAddress = "";
+
+ // Increasing numeric value.
+ // Bulb doesn't seem to care if it receives the same id multiple time
+ // or commands with lower numbers after higher ones.
+ @Expose
+ private int id;
+ // Not sure what env is - value always seems to be "pro"
+ @Expose
+ private @Nullable String env;
+
+ // An error response
+ @Expose
+ private @Nullable ErrorResponseResult methodError;
+
+ // The method being used - see the enum for details
+ // We're setting this to "unknown"
+ @Expose
+ private WizMethodType method = WizMethodType.UnknownMethod;
+
+ // The MAC address the response is coming from
+ @Expose
+ private String mac = "";
+
+ // Whether or not a command succeeded (if the response is from a command)
+ @Expose
+ private boolean success = false;
+
+ // The system configuration result, if present
+ @Expose
+ private @Nullable SystemConfigResult systemConfigResult;
+
+ // The modeul configuration result, if present
+ @Expose
+ private @Nullable ModelConfigResult modelConfigResult;
+
+ // The parameters or result of a command/response
+ // A "result" is generally returned when solicited using a set/get method and a
+ // "params" is retuned with an unsolicited sync/heartbeat. The result returned
+ // from a get method is generally identical to the params returned in the
+ // heartbeat.
+ @Expose
+ private @Nullable WizSyncState params;
+
+ /**
+ * Setters and Getters
+ */
+
+ public @Nullable SystemConfigResult getSystemConfigResults() {
+ return this.systemConfigResult;
+ }
+
+ public void setSystemConfigResult(final SystemConfigResult configResult) {
+ this.systemConfigResult = configResult;
+ }
+
+ public @Nullable ModelConfigResult getModelConfigResults() {
+ return this.modelConfigResult;
+ }
+
+ public void setModelConfigResult(final ModelConfigResult configResult) {
+ this.modelConfigResult = configResult;
+ }
+
+ public boolean getResultSuccess() {
+ return this.success;
+ }
+
+ public void setResultSucess(final boolean success) {
+ this.success = success;
+ }
+
+ public @Nullable WizSyncState getSyncState() {
+ return this.params;
+ }
+
+ public void setSyncParams(final WizSyncState params) {
+ this.params = params;
+ }
+
+ public String getWizResponseMacAddress() {
+ return this.mac;
+ }
+
+ public void setWizResponseMacAddress(final String wizResponseMacAddress) {
+ this.mac = wizResponseMacAddress;
+ }
+
+ public String getWizResponseIpAddress() {
+ return this.wizResponseIpAddress;
+ }
+
+ public void setWizResponseIpAddress(final String wizResponseIpAddress) {
+ this.wizResponseIpAddress = wizResponseIpAddress;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(final int id) {
+ this.id = id;
+ }
+
+ public @Nullable WizMethodType getMethod() {
+ return method;
+ }
+
+ public void setMethod(final WizMethodType method) {
+ this.method = method;
+ }
+
+ public @Nullable String getEnv() {
+ return env;
+ }
+
+ public void setEnv(final String env) {
+ this.env = env;
+ }
+
+ public @Nullable ErrorResponseResult getError() {
+ return methodError;
+ }
+
+ public void setError(ErrorResponseResult error) {
+ this.methodError = error;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizSyncState.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizSyncState.java
new file mode 100644
index 0000000000000..de6421b8dfe0a
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizSyncState.java
@@ -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.wiz.internal.entities;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.wiz.internal.enums.WizColorMode;
+import org.openhab.binding.wiz.internal.utils.WizColorConverter;
+import org.openhab.core.library.types.HSBType;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * This POJO represents the "params" of the current state of a WiZ bulb.
+ * These are retruned as the "params" in getPilot, sync, and heartbeat packets
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class WizSyncState {
+ // The MAC address the response is coming from
+ @Expose
+ public String mac = "";
+
+ // The current color mode of the bulb
+ // We will assume by default that it's a single color bulb
+ @Expose(serialize = false, deserialize = false)
+ public WizColorMode colorMode = WizColorMode.SingleColorMode;
+ @Expose(serialize = false, deserialize = false)
+ private WizColorConverter colorConverter = new WizColorConverter();
+
+ /*
+ * Extra Information only in 'hb' params
+ */
+ // Not sure exactly what this means, seems to be a boolean
+ // I believe the bulb communicates with the WiZ servers via MQTT
+ @Expose
+ public int mqttCd;
+
+ /*
+ * Bulb state information - not all fields are populated
+ */
+
+ // The bulb's WiFi signal strength
+ @Expose
+ public int rssi;
+ // The overall state of the bulb - on/off
+ @Expose
+ public boolean state;
+ // The numeric identifier for a preset lighting mode
+ @Expose
+ public int sceneId;
+ // Unknown - not seen by SRGD
+ @Expose
+ public boolean play;
+ // The speed of color changes in dynamic lighting modes
+ @Expose
+ public int speed;
+ // Strength of the red channel (0-255)
+ @Expose
+ public int r;
+ // Strength of the green channel (0-255)
+ @Expose
+ public int g;
+ // Strength of the blue channel (0-255)
+ @Expose
+ public int b;
+ // Intensity of the cool white channel (0-255)
+ @Expose
+ public int c;
+ // Intensity of the warm white channel (0-255)
+ @Expose
+ public int w;
+ // Dimming percent (10-100)
+ @Expose
+ public int dimming;
+ // Color temperature - sent in place of r/g/b/c/w
+ // If temperatures are sent, color LED's are not in use
+ @Expose
+ public int temp;
+ // Indicates if the light mode is applied following a pre-set "rhythm"
+ @Expose
+ public int schdPsetId;
+
+ @Expose
+ public int fanState;
+ @Expose
+ public int fanSpeed;
+ @Expose
+ public int fanMode;
+ @Expose
+ public int fanRevrs;
+
+ public WizColorMode getColorMode() {
+ if (r != 0 || g != 0 || b != 0) {
+ return WizColorMode.RGBMode;
+ } else if (temp != 0) {
+ return WizColorMode.CTMode;
+ } else {
+ return WizColorMode.SingleColorMode;
+ }
+ }
+
+ public HSBType getHSBColor() {
+ if (getColorMode() == WizColorMode.RGBMode) {
+ HSBType newColor = colorConverter.rgbwDimmingToHSB(r, g, b, w, dimming);
+ // NOTE: The WiZ bulbs do not use the cool white LED's in full color mode.
+ return newColor;
+ } else {
+ // If a rgb color isn't returned, simply call it simply white.
+ // Do not attempt any conversions given a color temperature.
+ return HSBType.WHITE;
+ }
+ }
+
+ public void setHSBColor(HSBType hsb) {
+ this.dimming = hsb.getBrightness().intValue();
+ int rgbw[] = colorConverter.hsbToRgbw(hsb);
+ this.r = rgbw[0];
+ this.g = rgbw[1];
+ this.b = rgbw[2];
+ this.w = rgbw[3];
+ this.c = 0;
+ }
+
+ public int getTemperature() {
+ return temp;
+ }
+
+ public void setTemperature(int temp) {
+ this.temp = temp;
+ }
+
+ public int getDimming() {
+ return this.dimming;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizColorMode.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizColorMode.java
new file mode 100644
index 0000000000000..7ce62bd743829
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizColorMode.java
@@ -0,0 +1,50 @@
+/**
+ * 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.wiz.internal.enums;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This enum represents the possible color modes for WiZ bulbs.
+ * The bulbs come in three types - full color with tunable white,
+ * tunable white, and dimmable with set white. The full color and
+ * tunable white bulbs operate EITHER in color mode OR in tunable
+ * white mode.
+ *
+ * @author Sara Geleskie Damiano - Initial contribution
+ *
+ */
+@NonNullByDefault
+public enum WizColorMode {
+ // Full color mode
+ RGBMode("Full Color"),
+ // Tunable white (color temperature) mode
+ CTMode("Tunable White"),
+ // Dimming only
+ SingleColorMode("Dimming Only");
+
+ private String colorMode;
+
+ private WizColorMode(final String colorMode) {
+ this.colorMode = colorMode;
+ }
+
+ /**
+ * Gets the colorMode name for request colorMode
+ *
+ * @return the colorMode name
+ */
+ public String getColorMode() {
+ return colorMode;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizLightMode.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizLightMode.java
new file mode 100644
index 0000000000000..a5f29fb379dbd
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizLightMode.java
@@ -0,0 +1,111 @@
+/**
+ * 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.wiz.internal.enums;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * This enum represents the possible scene modes.
+ *
+ * @author Sara Geleskie Damiano - Initial contribution
+ *
+ */
+@NonNullByDefault
+public enum WizLightMode {
+ Ocean("Ocean", 1),
+ Romance("Romance", 2),
+ Sunset("Sunset", 3),
+ Party("Party", 4),
+ Fireplace("Fireplace", 5),
+ CozyWhite("Cozy White", 6),
+ Forest("Forest", 7),
+ PastelColors("Pastel Colors", 8),
+ Wakeup("Wakeup", 9),
+ BedTime("Bed Time", 10),
+ WarmWhite("Warm White", 11),
+ Daylight("Daylight", 12),
+ CoolWhite("Cool White", 13),
+ NightLight("Night Light", 14),
+ Focus("Focus", 15),
+ Relax("Relax", 16),
+ TrueColors("True Colors", 17),
+ TVTime("TV Time", 18),
+ PlantGrowth("Plant Growth", 19),
+ Spring("Spring", 20),
+ Summer("Summer", 21),
+ Fall("Fall", 22),
+ DeepDive("Deep Dive", 23),
+ Jungle("Jungle", 24),
+ Mojito("Mojito", 25),
+ Club("Club", 26),
+ Christmas("Christmas", 27),
+ Halloween("Halloween", 28),
+ Candlelight("Candlelight", 29),
+ GoldenWhite("Golden White", 30),
+ Pulse("Pulse", 31),
+ Steampunk("Steampunk", 32);
+
+ private String colorModeName;
+ private int sceneId;
+
+ private WizLightMode(final String colorModeName, final int sceneId) {
+ this.colorModeName = colorModeName;
+ this.sceneId = sceneId;
+ }
+
+ /**
+ * Gets the colorMode name for request colorMode
+ *
+ * @return the colorMode name
+ */
+ public String getColorMode() {
+ return colorModeName;
+ }
+
+ public int getSceneId() {
+ return sceneId;
+ }
+
+ private static final Map LIGHT_MODE_MAP_BY_ID;
+ private static final Map LIGHT_MODE_MAP_BY_NAME;
+
+ static {
+ LIGHT_MODE_MAP_BY_ID = new HashMap();
+ LIGHT_MODE_MAP_BY_NAME = new HashMap();
+
+ for (WizLightMode v : WizLightMode.values()) {
+ LIGHT_MODE_MAP_BY_ID.put(v.sceneId, v);
+ LIGHT_MODE_MAP_BY_NAME.put(v.colorModeName.toLowerCase().replaceAll("\\W+", ""), v);
+ }
+ }
+
+ public static @Nullable WizLightMode fromSceneId(int id) {
+ WizLightMode r = null;
+ if (id > 0 && id < 33) {
+ r = LIGHT_MODE_MAP_BY_ID.get(id);
+ }
+ return r;
+ }
+
+ public static @Nullable WizLightMode fromSceneName(String name) {
+ WizLightMode r = null;
+ if (!name.isEmpty()) {
+ r = LIGHT_MODE_MAP_BY_NAME.get(name.toLowerCase().replaceAll("\\W+", ""));
+ }
+ return r;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizMethodType.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizMethodType.java
new file mode 100644
index 0000000000000..e8b05a1d56916
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizMethodType.java
@@ -0,0 +1,150 @@
+/**
+ * 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.wiz.internal.enums;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This enum represents the available WiZ Request Methods
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ *
+ */
+@NonNullByDefault
+public enum WizMethodType {
+ /**
+ * Registration - used to "register" with the bulb: This notifies the bult that
+ * it you want it to send you heartbeat sync packets.
+ * NOTE: The homeId value is optional, other values are required
+ * NOTE: There is no need to register before calling other methods.
+ * Example Request:
+ * {"method": "registration", "id": 1, "params":
+ * {"phoneIp": "10.0.0.xxx", "register": true, "homeId": xxxxxx, "phoneMac": "macOfopenHAB"}}
+ * Example Response:
+ * {"method": "registration", "id": 1, "env": "pro", "result":
+ * {"mac": "macOfopenHAB", "success": true}}
+ */
+ @SerializedName("registration")
+ Registration("registration"),
+ /**
+ * Pulse - tells the bulb to briely change brightness (by delta % for duration ms)
+ * Example Request:
+ * {"method": "pulse", "id": 22, "params": {"delta": -30, "duration": 900}}
+ * Example Response:
+ * {"method": "pulse", "id": 22, "env": "pro", "result": {"success": true}}
+ */
+ @SerializedName("pulse")
+ Pulse("pulse"),
+ /**
+ * setPilot - used to tell the bulb to change color/temp/state
+ * Example Request:
+ * {"method": "setPilot", "id": 24, "params": {"state": 1}}
+ * Example Response:
+ * {"method": "setPilot", "id": 24, "env": "pro", "result": {"success": true}}
+ */
+ @SerializedName("setPilot")
+ SetPilot("setPilot"),
+ /**
+ * getPilot - gets the current bulb state - no paramters need to be included
+ * Example Request:
+ * {"method": "getPilot", "id": 24}
+ * Example Response:
+ * {"method": "getPilot", "id": 22, "env": "pro", "result": {"mac":
+ * "a8bb508f570a", "rssi":-76, "state": true, "sceneId": 0, "temp": 2700,
+ * "dimming": 42, "schdPsetId": 5}}
+ */
+ @SerializedName("getPilot")
+ GetPilot("getPilot"),
+ /**
+ * syncPilot - sent by the bulb as heart-beats
+ * Example:
+ * {"method": "syncPilot", "id": 218, "env": "pro", "params":
+ * { "mac": "theBulbMacAddress", "rssi": -72, "src": "udp", "state": true, "sceneId": 0,
+ * "temp": 3362, "dimming": 69, "schdPsetId": 5}}
+ * Another Example:
+ * {"method": "syncPilot", "id": 219, "env": "pro", "params":
+ * { "mac": "theBulbMacAddress", "rssi": -72, "src": "hb", "mqttCd": 0, "state": true,
+ * "sceneId": 0, "temp": 3362, "dimming": 69, "schdPsetId": 5}}
+ */
+ @SerializedName("syncPilot")
+ SyncPilot("syncPilot"),
+ /**
+ * getModelConfig - gets more details on the bulb
+ */
+ @SerializedName("getModelConfig")
+ GetModelConfig("getModelConfig"),
+ /**
+ * getSystemConfig - gets the current system configuration - no paramters need
+ * to be included
+ * Example Request:
+ * {"method": "getSystemConfig", "id": 24}
+ * Example Response:
+ * {"method": "getSystemConfig", "id": 22, "env": "pro",
+ * "result": {"mac": "theBulbMacAddress", "homeId": xxxxxx, "roomId": xxxxxx,
+ * "homeLock": false, "pairingLock": false, "typeId": 0, "moduleName":
+ * "ESP01_SHRGB1C_31", "fwVersion": "1.15.2", "groupId": 0, "drvConf":[33,1]}}
+ */
+ @SerializedName("getSystemConfig")
+ GetSystemConfig("getSystemConfig"),
+ /**
+ * setSystemConfig - presumably sets up the system
+ * I have NOT attempted to call this method
+ */
+ @SerializedName("setSystemConfig")
+ SetSystemConfig("setSystemConfig"),
+ /**
+ * getWifiConfig - gets the current wifi configuration - no paramters need to be
+ * included
+ * Example Request:
+ * {"id": 22, "method": "getWifiConfig"}
+ * Example Response:
+ * {"method": "getWifiConfig", "id": 22, "env": "pro", "result":
+ * {:["encryptedString"]}
+ */
+ @SerializedName("getWifiConfig")
+ GetWifiConfig("getWifiConfig"),
+ /**
+ * setWifiConfig - presumably sets up the system I have NOT attempted to use this method
+ */
+ @SerializedName("setWifiConfig")
+ SetWifiConfig("setWifiConfig"),
+ /**
+ * firstBeat - set by a bulb upon power up
+ * Example:
+ * {"method": "firstBeat", "id": 0, "env": "pro", "params":
+ * {"mac": "theBulbMacAddress", "homeId": xxxxxx, "fwVersion": "1.15.2"}}
+ */
+ @SerializedName("firstBeat")
+ FirstBeat("firstBeat"),
+ /**
+ * Unknown - using as a default for inproperly received responses
+ */
+ UnknownMethod("unknownMethod");
+
+ private final String methodName;
+
+ private WizMethodType(final String methodName) {
+ this.methodName = methodName;
+ }
+
+ /**
+ * Gets the method name for request method
+ *
+ * @return the method name
+ */
+ public String getMethodName() {
+ return methodName;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizModuleType.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizModuleType.java
new file mode 100644
index 0000000000000..294d151b6ffc0
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizModuleType.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.wiz.internal.enums;
+
+import static org.openhab.binding.wiz.internal.WizBindingConstants.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * This enum represents the possible scene modes.
+ *
+ * @author Sara Geleskie Damiano - Initial contribution
+ *
+ */
+@NonNullByDefault
+public enum WizModuleType {
+ FullColorWifi("ESP01_SHRGB1C_31", THING_TYPE_COLOR_BULB),
+ TunableWhiteWifi("ESP56_SHTW3_01", THING_TYPE_TUNABLE_BULB),
+ DimmableWifi("TBD", THING_TYPE_DIMMABLE_BULB),
+ SmartPlug("TBD", THING_TYPE_SMART_PLUG);
+
+ private final String moduleName;
+ private final ThingTypeUID thingTypeUID;
+
+ private WizModuleType(final String moduleName, final ThingTypeUID thingTypeUID) {
+ this.moduleName = moduleName;
+ this.thingTypeUID = thingTypeUID;
+ }
+
+ /**
+ * Gets the colorMode name for request colorMode
+ *
+ * @return the colorMode name
+ */
+ public String getModuleName() {
+ return moduleName;
+ }
+
+ private static final Map MODULE_NAME_MAP;
+ static {
+ MODULE_NAME_MAP = new HashMap();
+ for (WizModuleType v : WizModuleType.values()) {
+ MODULE_NAME_MAP.put(v.moduleName, v.thingTypeUID);
+ }
+ }
+
+ public static @Nullable ThingTypeUID getThingTypeUIDFromModuleName(String moduleName) {
+ return MODULE_NAME_MAP.get(moduleName);
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizHandler.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizHandler.java
new file mode 100644
index 0000000000000..f6108051c2f1f
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizHandler.java
@@ -0,0 +1,882 @@
+/**
+ * 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.wiz.internal.handler;
+
+import static org.openhab.binding.wiz.internal.WizBindingConstants.*;
+import static org.openhab.core.thing.Thing.*;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.SocketTimeoutException;
+import java.time.ZonedDateTime;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+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.wiz.internal.WizStateDescriptionProvider;
+import org.openhab.binding.wiz.internal.config.WizDeviceConfiguration;
+import org.openhab.binding.wiz.internal.entities.ColorRequestParam;
+import org.openhab.binding.wiz.internal.entities.ColorTemperatureRequestParam;
+import org.openhab.binding.wiz.internal.entities.DimmingRequestParam;
+import org.openhab.binding.wiz.internal.entities.FanModeRequestParam;
+import org.openhab.binding.wiz.internal.entities.FanReverseRequestParam;
+import org.openhab.binding.wiz.internal.entities.FanSpeedRequestParam;
+import org.openhab.binding.wiz.internal.entities.FanStateRequestParam;
+import org.openhab.binding.wiz.internal.entities.ModelConfigResult;
+import org.openhab.binding.wiz.internal.entities.Param;
+import org.openhab.binding.wiz.internal.entities.RegistrationRequestParam;
+import org.openhab.binding.wiz.internal.entities.SceneRequestParam;
+import org.openhab.binding.wiz.internal.entities.SpeedRequestParam;
+import org.openhab.binding.wiz.internal.entities.StateRequestParam;
+import org.openhab.binding.wiz.internal.entities.SystemConfigResult;
+import org.openhab.binding.wiz.internal.entities.WizRequest;
+import org.openhab.binding.wiz.internal.entities.WizResponse;
+import org.openhab.binding.wiz.internal.entities.WizSyncState;
+import org.openhab.binding.wiz.internal.enums.WizLightMode;
+import org.openhab.binding.wiz.internal.enums.WizMethodType;
+import org.openhab.binding.wiz.internal.utils.ValidationUtils;
+import org.openhab.binding.wiz.internal.utils.WizPacketConverter;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.IncreaseDecreaseType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.StateDescription;
+import org.openhab.core.types.StateDescriptionFragmentBuilder;
+import org.openhab.core.types.UnDefType;
+import org.openhab.core.util.ColorUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link WizHandler} is responsible for handling commands, which
+ * are sent to one of the channels.
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ */
+@NonNullByDefault
+public class WizHandler extends BaseThingHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(WizHandler.class);
+
+ private @NonNullByDefault({}) WizDeviceConfiguration config;
+ private @Nullable RegistrationRequestParam registrationRequestParam;
+ private int homeId;
+
+ private WizSyncState mostRecentState;
+
+ private final WizPacketConverter converter = new WizPacketConverter();
+ private final WizStateDescriptionProvider stateDescriptionProvider;
+ private final TimeZoneProvider timeZoneProvider;
+ private final ChannelUID colorTempChannelUID;
+ private @Nullable ScheduledFuture> keepAliveJob;
+ private long latestUpdate = -1;
+ private long latestOfflineRefresh = -1;
+ private int requestId = 0;
+ private final boolean isFan;
+ private final boolean isFanOnly;
+ private int minColorTemp = MIN_COLOR_TEMPERATURE;
+ private int maxColorTemp = MAX_COLOR_TEMPERATURE;
+
+ private volatile boolean disposed;
+ private volatile boolean fullyInitialized;
+
+ /**
+ * Default constructor.
+ *
+ * @param thing the thing of the handler.
+ * @param stateDescriptionProvider A state description provider
+ */
+ public WizHandler(final Thing thing, final WizMediator mediator,
+ WizStateDescriptionProvider stateDescriptionProvider, TimeZoneProvider timeZoneProvider) {
+ super(thing);
+ try {
+ registrationRequestParam = mediator.getRegistrationParams();
+ } catch (IllegalStateException e) {
+ registrationRequestParam = null;
+ }
+ this.stateDescriptionProvider = stateDescriptionProvider;
+ this.timeZoneProvider = timeZoneProvider;
+ this.mostRecentState = new WizSyncState();
+ this.isFan = thing.getThingTypeUID().equals(THING_TYPE_FAN)
+ || thing.getThingTypeUID().equals(THING_TYPE_FAN_WITH_DIMMABLE_BULB);
+ this.isFanOnly = thing.getThingTypeUID().equals(THING_TYPE_FAN);
+ colorTempChannelUID = new ChannelUID(getThing().getUID(), CHANNEL_TEMPERATURE_ABS);
+ fullyInitialized = false;
+ }
+
+ @Override
+ public void handleCommand(final ChannelUID channelUID, final Command command) {
+ if (hasConfigurationError() || disposed || !fullyInitialized) {
+ logger.debug(
+ "[{}] WiZ handler for device {} received command {} on channel {} but is not yet prepared to handle it.",
+ config.ipAddress, config.macAddress, command, channelUID);
+ return;
+ }
+
+ if (command instanceof RefreshType) {
+ long now = System.currentTimeMillis();
+ long timePassedFromLastUpdateInSeconds = (now - latestUpdate) / 1000;
+ // Be patient...
+ if (latestUpdate < 0 || timePassedFromLastUpdateInSeconds > 5) {
+ getPilot();
+ }
+ return;
+ }
+
+ if (isFanOnly || (isFan && CHANNEL_GROUP_FAN.equals(channelUID.getGroupId()))) {
+ handleFanCommand(channelUID.getIdWithoutGroup(), command);
+ } else if (!isFan || (isFan && CHANNEL_GROUP_LIGHT.equals(channelUID.getGroupId()))) {
+ handleLightCommand(channelUID.getIdWithoutGroup(), command);
+ }
+ }
+
+ private void handleLightCommand(final String channelId, final Command command) {
+ switch (channelId) {
+ case CHANNEL_COLOR:
+ if (command instanceof HSBType hsbCommand) {
+ handleHSBCommand(hsbCommand);
+ } else if (command instanceof PercentType percentCommand) {
+ handlePercentCommand(percentCommand);
+ } else if (command instanceof OnOffType onOffCommand) {
+ handleOnOffCommand(onOffCommand);
+ } else if (command instanceof IncreaseDecreaseType) {
+ handleIncreaseDecreaseCommand(command == IncreaseDecreaseType.INCREASE);
+ }
+ break;
+ case CHANNEL_TEMPERATURE:
+ if (command instanceof PercentType percentCommand) {
+ handleTemperatureCommand(percentToColorTemp(percentCommand));
+ } else if (command instanceof OnOffType onOffCommand) {
+ handleTemperatureCommand(
+ percentToColorTemp(onOffCommand == OnOffType.ON ? PercentType.HUNDRED : PercentType.ZERO));
+ } else if (command instanceof IncreaseDecreaseType) {
+ handleIncreaseDecreaseTemperatureCommand(command == IncreaseDecreaseType.INCREASE);
+ }
+ break;
+ case CHANNEL_TEMPERATURE_ABS:
+ QuantityType> kelvinQt;
+ if (command instanceof QuantityType> commandQt
+ && (kelvinQt = commandQt.toInvertibleUnit(Units.KELVIN)) != null) {
+ handleTemperatureCommand(kelvinQt.intValue());
+ } else {
+ handleTemperatureCommand(Integer.valueOf(command.toString()));
+ }
+ case CHANNEL_BRIGHTNESS:
+ if (command instanceof PercentType percentCommand) {
+ handlePercentCommand(percentCommand);
+ } else if (command instanceof OnOffType onOffCommand) {
+ handleOnOffCommand(onOffCommand);
+ } else if (command instanceof IncreaseDecreaseType) {
+ handleIncreaseDecreaseCommand(command == IncreaseDecreaseType.INCREASE);
+ }
+ break;
+ case CHANNEL_STATE:
+ if (command instanceof OnOffType onOffCommand) {
+ handleOnOffCommand(onOffCommand);
+ }
+ break;
+ case CHANNEL_MODE:
+ handleLightModeCommand(command);
+ break;
+ case CHANNEL_SPEED:
+ if (command instanceof PercentType percentCommand) {
+ handleSpeedCommand(percentCommand);
+ } else if (command instanceof OnOffType onOffCommand) {
+ handleSpeedCommand(onOffCommand == OnOffType.ON ? PercentType.HUNDRED : PercentType.ZERO);
+ } else if (command instanceof IncreaseDecreaseType) {
+ handleIncreaseDecreaseSpeedCommand(command == IncreaseDecreaseType.INCREASE);
+ }
+ break;
+ }
+ }
+
+ private void handleFanCommand(final String channelId, final Command command) {
+ switch (channelId) {
+ case CHANNEL_STATE:
+ if (command instanceof OnOffType onOffCommand) {
+ handleFanOnOffCommand(onOffCommand);
+ }
+ break;
+ case CHANNEL_MODE:
+ if (command instanceof DecimalType decimalCommand) {
+ handleFanModeCommand(decimalCommand);
+ }
+ break;
+ case CHANNEL_SPEED:
+ if (command instanceof DecimalType numberCommand) {
+ if (numberCommand.equals(DecimalType.ZERO)) {
+ handleFanOnOffCommand(OnOffType.OFF);
+ } else {
+ handleFanSpeedCommand(numberCommand);
+ }
+ }
+ break;
+ case CHANNEL_REVERSE:
+ if (command instanceof OnOffType onOffCommand) {
+ handleFanReverseCommand(onOffCommand);
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void handleRemoval() {
+ disposed = true;
+ fullyInitialized = false;
+ // stop update thread
+ ScheduledFuture> keepAliveJob = this.keepAliveJob;
+ if (keepAliveJob != null) {
+ keepAliveJob.cancel(true);
+ this.keepAliveJob = null;
+ }
+ super.handleRemoval();
+ }
+
+ private void handleLightModeCommand(Command command) {
+ String commandAsString = command.toString();
+
+ Integer commandAsInt = Integer.MIN_VALUE;
+ WizLightMode commandAsLightMode = null;
+
+ try {
+ commandAsInt = Integer.parseInt(commandAsString);
+ } catch (Exception ex) {
+ }
+
+ if (commandAsInt > 0) {
+ commandAsLightMode = WizLightMode.fromSceneId(commandAsInt);
+ }
+
+ if (commandAsLightMode == null) {
+ commandAsLightMode = WizLightMode.fromSceneName(commandAsString);
+ }
+
+ if (commandAsLightMode != null) {
+ mostRecentState.sceneId = commandAsLightMode.getSceneId();
+ setPilotCommand(new SceneRequestParam(commandAsLightMode.getSceneId()));
+ } else {
+ logger.warn("[{}] Command [{}] not a recognized Light Mode!", config.ipAddress, command);
+ }
+ }
+
+ private void handleHSBCommand(HSBType hsb) {
+ if (hsb.getBrightness().intValue() == 0) {
+ logger.debug("[{}] Zero intensity requested, turning bulb off.", config.ipAddress);
+ setPilotCommand(new StateRequestParam(false));
+ } else {
+ setPilotCommand(new ColorRequestParam(hsb));
+ }
+ mostRecentState.setHSBColor(hsb);
+ }
+
+ private void handlePercentCommand(PercentType brightness) {
+ if (brightness.equals(PercentType.ZERO)) {
+ logger.debug("[{}] Zero brightness requested, turning bulb off.", config.ipAddress);
+ setPilotCommand(new StateRequestParam(false));
+ } else {
+ setPilotCommand(new DimmingRequestParam(brightness.intValue()));
+ }
+ mostRecentState.dimming = brightness.intValue();
+ }
+
+ private void handleOnOffCommand(OnOffType onOff) {
+ setPilotCommand(new StateRequestParam(onOff == OnOffType.ON));
+ mostRecentState.state = onOff == OnOffType.ON;
+ }
+
+ private void handleFanOnOffCommand(OnOffType onOff) {
+ int value = onOff == OnOffType.ON ? 1 : 0;
+ setPilotCommand(new FanStateRequestParam(value));
+ mostRecentState.fanState = value;
+ }
+
+ private void handleFanSpeedCommand(DecimalType speed) {
+ setPilotCommand(new FanSpeedRequestParam(speed.intValue()));
+ mostRecentState.fanSpeed = speed.intValue();
+ }
+
+ private void handleFanReverseCommand(OnOffType onOff) {
+ int value = onOff == OnOffType.ON ? 1 : 0;
+ setPilotCommand(new FanReverseRequestParam(value));
+ mostRecentState.fanRevrs = value;
+ }
+
+ private void handleFanModeCommand(DecimalType mode) {
+ setPilotCommand(new FanModeRequestParam(mode.intValue()));
+ mostRecentState.fanMode = mode.intValue();
+ }
+
+ private void handleIncreaseDecreaseCommand(boolean isIncrease) {
+ int oldDimming = mostRecentState.dimming;
+ int newDimming;
+ if (isIncrease) {
+ newDimming = Math.min(100, oldDimming + 5);
+ } else {
+ newDimming = Math.max(10, oldDimming - 5);
+ }
+ logger.debug("[{}] Changing bulb brightness from {}% to {}%.", config.ipAddress, oldDimming, newDimming);
+ handlePercentCommand(new PercentType(newDimming));
+ }
+
+ private void handleTemperatureCommand(int temperature) {
+ setPilotCommand(new ColorTemperatureRequestParam(temperature));
+ mostRecentState.setTemperature(temperature);
+ }
+
+ private void handleIncreaseDecreaseTemperatureCommand(boolean isIncrease) {
+ float oldTempPct = colorTempToPercent(mostRecentState.getTemperature()).floatValue();
+ float newTempPct;
+ if (isIncrease) {
+ newTempPct = Math.min(100, oldTempPct + 5);
+ } else {
+ newTempPct = Math.max(0, oldTempPct - 5);
+ }
+ logger.debug("[{}] Changing color temperature from {}% to {}%.", config.ipAddress, oldTempPct, newTempPct);
+ handleTemperatureCommand(percentToColorTemp(new PercentType(BigDecimal.valueOf(newTempPct))));
+ }
+
+ private void handleSpeedCommand(PercentType speed) {
+ // NOTE: We cannot set the speed without also setting the scene
+ int currentScene = mostRecentState.sceneId;
+ setPilotCommand(new SpeedRequestParam(currentScene, speed.intValue()));
+ mostRecentState.speed = speed.intValue();
+ }
+
+ private void handleIncreaseDecreaseSpeedCommand(boolean isIncrease) {
+ int oldSpeed = mostRecentState.speed;
+ int newSpeed;
+ if (isIncrease) {
+ newSpeed = Math.min(100, oldSpeed + 5);
+ } else {
+ newSpeed = Math.max(10, oldSpeed - 5);
+ }
+ handleSpeedCommand(new PercentType(newSpeed));
+ }
+
+ /**
+ * Starts one thread that querys the state of the socket, after the defined
+ * refresh interval.
+ */
+ private synchronized void initGetStatusAndKeepAliveThread() {
+ ScheduledFuture> keepAliveJob = this.keepAliveJob;
+ if (keepAliveJob != null) {
+ keepAliveJob.cancel(true);
+ }
+
+ Runnable runnable = () -> {
+ long now = System.currentTimeMillis();
+ long timePassedFromLastUpdateInSeconds = (now - latestUpdate) / 1000;
+ long timePassedFromLastRefreshInSeconds = (now - latestOfflineRefresh) / 1000;
+
+ // If the device has an online status, check if we it's been too long since the
+ // last response and re-set offline accordingly
+ if (getThing().getStatus() == ThingStatus.ONLINE) {
+ logger.trace("[{}] MAC address: {} Latest Update: {} Now: {} Delta: {} seconds", config.ipAddress,
+ config.macAddress, latestUpdate, now, timePassedFromLastUpdateInSeconds);
+
+ boolean considerThingOffline = (latestUpdate < 0)
+ || (timePassedFromLastUpdateInSeconds > MARK_OFFLINE_AFTER_SEC);
+ if (considerThingOffline) {
+ logger.debug(
+ "[{}] Since no updates have been received from mac address {} in {} seconds, setting its status to OFFLINE and discontinuing polling.",
+ config.ipAddress, config.macAddress, MARK_OFFLINE_AFTER_SEC);
+ updateStatus(ThingStatus.OFFLINE);
+
+ }
+ }
+
+ // If we're not offline ither re-register for heart-beats or request status
+ if (getThing().getStatus() != ThingStatus.OFFLINE) {
+ if (config.useHeartBeats) {
+ // If we're using 5s heart-beats, we must re-register every 30s to maintain
+ // connection
+ logger.debug("[{}] Re-registering for heart-beats.", config.ipAddress);
+ registerWithDevice();
+ } else {
+ // If we're not using heart-beats, just request the current status
+ logger.debug("[{}] Polling for status from device at {}.", config.ipAddress, config.macAddress);
+ getPilot();
+ }
+
+ // Else if we are offline, but it's been a while, re-check if the device re-appeared
+ } else if (timePassedFromLastRefreshInSeconds > config.reconnectInterval * 60) {
+ // Request the current status
+ logger.debug("[{}] Checking for reappearance of offline device at {}.", config.ipAddress,
+ config.macAddress);
+ latestOfflineRefresh = now;
+ getPilot();
+ }
+ };
+ /**
+ * Schedule the keep-alive job.
+ *
+ * The scheduling inteval is:
+ * - every 30 seconds for online devices receiving heart-beats
+ * - every config.updateInterval for other online devices
+ */
+ long updateIntervalInUse = config.useHeartBeats ? 30 : config.updateInterval;
+ logger.debug("[{}] Scheduling reoccuring keep alive for every {} seconds for device at {}.", config.ipAddress,
+ updateIntervalInUse, config.macAddress);
+ this.keepAliveJob = scheduler.scheduleWithFixedDelay(runnable, 1, updateIntervalInUse, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void initialize() {
+ this.config = getConfigAs(WizDeviceConfiguration.class);
+ fullyInitialized = false;
+ disposed = false;
+
+ if (registrationRequestParam == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "Unable to determine openHAB's IP or MAC address");
+ return;
+ }
+ if (!ValidationUtils.isMacValid(config.macAddress)) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "MAC address is not valid");
+ return;
+ }
+
+ // set the thing status to UNKNOWN temporarily
+ updateStatus(ThingStatus.UNKNOWN);
+ updateDeviceProperties();
+ initGetStatusAndKeepAliveThread();
+ fullyInitialized = true;
+ }
+
+ @Override
+ public void dispose() {
+ disposed = true;
+ fullyInitialized = false;
+ // stop update thread
+ ScheduledFuture> keepAliveJob = this.keepAliveJob;
+ if (keepAliveJob != null) {
+ keepAliveJob.cancel(true);
+ this.keepAliveJob = null;
+ }
+ stateDescriptionProvider.remove(colorTempChannelUID);
+ super.dispose();
+ }
+
+ private synchronized void getPilot() {
+ WizResponse response = sendRequestPacket(WizMethodType.GetPilot, null);
+ if (response != null) {
+ WizSyncState rParam = response.getSyncState();
+ if (rParam != null) {
+ updateTimestamps();
+ updateStatesFromParams(rParam);
+ } else {
+ logger.trace("[{}] No parameters in getPilot response!", config.ipAddress);
+ }
+ } else {
+ logger.trace("[{}] No response from getPilot request!", config.ipAddress);
+ }
+ }
+
+ /**
+ * Method called by {@link WizMediator} when any "unsolicited" messages
+ * come in on the listening socket and appear to be a WiZ device. "Unsolicited"
+ * messages from the device could be:
+ * - a "firstBeat" broadcast to the subnet by the device on first powering up
+ * - an "hb" (heartbeat) specifically directed to openHAB within 30 seconds of registration
+ * - or a response to a registration request broadcast by this binding to all devices on the subnet
+ *
+ * @note The mediator finds the correct handler for the device based on the (unchanging) device
+ * MAC address. If the mediator matches a message to the handler by MAC address, but the IP address
+ * the message came from doesn't match the device's configured IP address, this will update the
+ * device's configuration to reflect whatever the current IP is.
+ *
+ * @param receivedMessage the received {@link WizResponse}.
+ */
+ public synchronized void newReceivedResponseMessage(final WizResponse receivedMessage) {
+ Boolean updatePropertiesAfterParams = false;
+
+ // Check if the device still has the same IP address it had previously
+ // If not, we need to update the configuration for the thing.
+ if (!receivedMessage.getWizResponseIpAddress().isEmpty()
+ && !receivedMessage.getWizResponseIpAddress().equals(this.getIpAddress())) {
+ // get the old config
+ Configuration priorConfig = getConfig();
+ // change the ip address property
+ priorConfig.put(CONFIG_IP_ADDRESS, receivedMessage.getWizResponseIpAddress());
+ // save the changes to the thing
+ updateConfiguration(priorConfig);
+ // and then refresh the config within the handler
+ this.config = getConfigAs(WizDeviceConfiguration.class);
+ // finally, make note that we want to update properties
+ updatePropertiesAfterParams = true;
+ }
+
+ // Grab the ID number and mark the device online
+ requestId = receivedMessage.getId();
+ updateTimestamps();
+
+ // Update the state from the parameters, if possible
+ WizSyncState params = receivedMessage.getSyncState();
+ if (params != null) {
+ updateStatesFromParams(params);
+ }
+
+ // After updating state, we'll update all other device parameters from devices that
+ // presented with a new IP address.
+ if (updatePropertiesAfterParams) {
+ updateDeviceProperties();
+ }
+ }
+
+ /**
+ * Updates the channel states based on incoming parameters
+ *
+ * @param receivedParam The received {@link WizSyncState}
+ */
+ private synchronized void updateStatesFromParams(final WizSyncState receivedParam) {
+ // Save the current state
+ this.mostRecentState = receivedParam;
+
+ if (hasConfigurationError() || disposed) {
+ return;
+ }
+
+ if (isFan) {
+ updateFanStatesFromParams(receivedParam);
+ }
+ if (!isFanOnly) {
+ updateLightStatesFromParams(receivedParam);
+ }
+
+ // update signal strength
+ if (receivedParam.rssi != 0) {
+ int strength = -1;
+ if (receivedParam.rssi < -90) {
+ strength = 0;
+ } else if (receivedParam.rssi < -80) {
+ strength = 1;
+ } else if (receivedParam.rssi < -70) {
+ strength = 2;
+ } else if (receivedParam.rssi < -67) {
+ strength = 3;
+ } else {
+ strength = 4;
+ }
+ updateDeviceState(CHANNEL_SIGNAL_STRENGTH, new DecimalType(strength));
+ updateDeviceState(CHANNEL_RSSI, new QuantityType<>(receivedParam.rssi, Units.DECIBEL_MILLIWATTS));
+ }
+ }
+
+ /**
+ * Updates the channel states for a light based on incoming parameters
+ *
+ * @param receivedParam The received {@link WizSyncState}
+ */
+ private void updateLightStatesFromParams(final WizSyncState receivedParam) {
+ if (!receivedParam.state) {
+ updateLightState(CHANNEL_COLOR, HSBType.BLACK);
+ updateLightState(CHANNEL_BRIGHTNESS, PercentType.ZERO);
+ updateLightState(CHANNEL_STATE, OnOffType.OFF);
+ updateLightState(CHANNEL_TEMPERATURE, UnDefType.UNDEF);
+ updateLightState(CHANNEL_TEMPERATURE_ABS, UnDefType.UNDEF);
+ } else {
+ updateLightState(CHANNEL_BRIGHTNESS, new PercentType(receivedParam.dimming));
+ updateLightState(CHANNEL_STATE, OnOffType.ON);
+ switch (receivedParam.getColorMode()) {
+ case RGBMode:
+ logger.trace(
+ "[{}] Received color values - R: {} G: {} B: {} W: {} C: {} Dimming: {}; translate to HSBType: {}",
+ config.ipAddress, receivedParam.r, receivedParam.g, receivedParam.b, receivedParam.w,
+ receivedParam.c, receivedParam.dimming, receivedParam.getHSBColor());
+
+ updateLightState(CHANNEL_COLOR, receivedParam.getHSBColor());
+ updateLightState(CHANNEL_TEMPERATURE, UnDefType.UNDEF);
+ updateLightState(CHANNEL_TEMPERATURE_ABS, UnDefType.UNDEF);
+ break;
+ case CTMode:
+ double[] xy = ColorUtil.kelvinToXY(receivedParam.getTemperature());
+ HSBType color = ColorUtil.xyToHsb(xy);
+ updateLightState(CHANNEL_COLOR, new HSBType(color.getHue(), color.getSaturation(),
+ new PercentType(receivedParam.getDimming())));
+ updateLightState(CHANNEL_TEMPERATURE, colorTempToPercent(receivedParam.getTemperature()));
+ updateLightState(CHANNEL_TEMPERATURE_ABS,
+ new QuantityType<>(receivedParam.getTemperature(), Units.KELVIN));
+ break;
+ case SingleColorMode:
+ updateLightState(CHANNEL_COLOR, new HSBType(DecimalType.ZERO, PercentType.ZERO,
+ new PercentType(receivedParam.getDimming())));
+ updateLightState(CHANNEL_TEMPERATURE, UnDefType.UNDEF);
+ updateLightState(CHANNEL_TEMPERATURE_ABS, UnDefType.UNDEF);
+ break;
+ }
+ }
+
+ updateLightState(CHANNEL_MODE, new StringType(String.valueOf(receivedParam.sceneId)));
+ updateLightState(CHANNEL_SPEED, new PercentType(receivedParam.speed));
+ }
+
+ /**
+ * Updates the channel states for a fan based on incoming parameters
+ *
+ * @param receivedParam The received {@link WizSyncState}
+ */
+ private void updateFanStatesFromParams(final WizSyncState receivedParam) {
+ updateFanState(CHANNEL_STATE, receivedParam.fanState == 0 ? OnOffType.OFF : OnOffType.ON);
+ updateFanState(CHANNEL_SPEED, new DecimalType(receivedParam.fanSpeed));
+ updateFanState(CHANNEL_REVERSE, receivedParam.fanRevrs == 0 ? OnOffType.OFF : OnOffType.ON);
+ updateFanState(CHANNEL_MODE, new DecimalType(receivedParam.fanMode));
+ }
+
+ /**
+ * Sends {@link WizRequest} to the passed {@link InetAddress}.
+ *
+ * @param requestPacket the {@link WizRequest}.
+ * @param address the {@link InetAddress}.
+ */
+ private synchronized @Nullable WizResponse sendRequestPacket(final WizMethodType method,
+ final @Nullable Param param) {
+ DatagramSocket dsocket = null;
+ try {
+ InetAddress address = InetAddress.getByName(config.ipAddress);
+ if (address != null) {
+ WizRequest request = new WizRequest(method, param);
+ request.setId(requestId++);
+
+ byte[] message = this.converter.transformToByteMessage(request);
+ logger.trace("Raw packet to send: {}", message);
+
+ // Initialize a datagram packet with data and address
+ DatagramPacket packet = new DatagramPacket(message, message.length, address, DEFAULT_UDP_PORT);
+
+ // Create a datagram socket, send the packet through it, close it.
+ dsocket = new DatagramSocket(null);
+ dsocket.setReuseAddress(true);
+ dsocket.setBroadcast(true);
+ dsocket.setSoTimeout(500); // Timeout in 500ms
+ dsocket.send(packet);
+ logger.debug("[{}] Sent packet to address: {} and port {}", config.ipAddress, address,
+ DEFAULT_UDP_PORT);
+
+ byte[] responseMessage = new byte[1024];
+ packet = new DatagramPacket(responseMessage, responseMessage.length);
+ dsocket.receive(packet);
+
+ return converter.transformResponsePacket(packet);
+ }
+ } catch (SocketTimeoutException e) {
+ logger.trace("[{}] Socket timeout after sending command; no response from {} within 500ms",
+ config.ipAddress, config.macAddress);
+ } catch (IOException exception) {
+ logger.debug("[{}] Something wrong happened when sending the packet to port {}... msg: {}",
+ config.ipAddress, DEFAULT_UDP_PORT, exception.getMessage());
+ } finally {
+ if (dsocket != null) {
+ dsocket.close();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Sends a setPilot request and checks for success
+ */
+ private synchronized boolean setPilotCommand(final @Nullable Param param) {
+ WizResponse response = sendRequestPacket(WizMethodType.SetPilot, param);
+ if (response != null) {
+ boolean setSucceeded = response.getResultSuccess();
+ if (setSucceeded) {
+ // can't process this response it doens't have a syncstate, so request updated state
+ // let the getPilot response update the timestamps
+ try {
+ // wait for state change to apply
+ Thread.sleep(1000L);
+ } catch (InterruptedException e) {
+ }
+ getPilot();
+ return setSucceeded;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Makes note of the latest timestamps and sets the device online
+ */
+ private synchronized void updateTimestamps() {
+ if (hasConfigurationError() || disposed) {
+ return;
+ }
+ updateStatus(ThingStatus.ONLINE);
+ latestUpdate = System.currentTimeMillis();
+ latestOfflineRefresh = System.currentTimeMillis();
+ final ZonedDateTime zonedDateTime = ZonedDateTime.now(timeZoneProvider.getTimeZone());
+ updateDeviceState(CHANNEL_LAST_UPDATE, new DateTimeType(zonedDateTime));
+ }
+
+ /**
+ * Asks the device for its current system configuration
+ */
+ private synchronized void updateDeviceProperties() {
+ if (hasConfigurationError() || disposed) {
+ return;
+ }
+ WizResponse registrationResponse = sendRequestPacket(WizMethodType.GetSystemConfig, null);
+ if (registrationResponse != null) {
+ SystemConfigResult systemConfigResult = registrationResponse.getSystemConfigResults();
+ if (systemConfigResult != null) {
+ // Update all the thing properties based on the result
+ Map thingProperties = new HashMap();
+ thingProperties.put(PROPERTY_VENDOR, "WiZ Connected");
+ thingProperties.put(PROPERTY_FIRMWARE_VERSION, systemConfigResult.fwVersion);
+ thingProperties.put(PROPERTY_MAC_ADDRESS, systemConfigResult.mac);
+ thingProperties.put(PROPERTY_IP_ADDRESS, registrationResponse.getWizResponseIpAddress());
+ thingProperties.put(PROPERTY_HOME_ID, String.valueOf(systemConfigResult.homeId));
+ thingProperties.put(PROPERTY_ROOM_ID, String.valueOf(systemConfigResult.roomId));
+ thingProperties.put(PROPERTY_HOME_LOCK, String.valueOf(systemConfigResult.homeLock));
+ thingProperties.put(PROPERTY_PAIRING_LOCK, String.valueOf(systemConfigResult.pairingLock));
+ thingProperties.put(PROPERTY_TYPE_ID, String.valueOf(systemConfigResult.typeId));
+ thingProperties.put(PROPERTY_MODULE_NAME, systemConfigResult.moduleName);
+ thingProperties.put(PROPERTY_GROUP_ID, String.valueOf(systemConfigResult.groupId));
+ updateProperties(thingProperties);
+ updateTimestamps();
+ } else {
+ logger.debug(
+ "[{}] Received response to getConfigRequest from device at {}, but it did not contain device configuration information.",
+ config.ipAddress, config.macAddress);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+ }
+
+ // Firmware versions > 1.22 support more details
+ registrationResponse = sendRequestPacket(WizMethodType.GetModelConfig, null);
+ if (registrationResponse != null) {
+ ModelConfigResult modelConfigResult = registrationResponse.getModelConfigResults();
+ if (modelConfigResult != null && modelConfigResult.cctRange.length > 0) {
+ minColorTemp = Arrays.stream(modelConfigResult.cctRange).min().getAsInt();
+ maxColorTemp = Arrays.stream(modelConfigResult.cctRange).max().getAsInt();
+ StateDescription stateDescription = StateDescriptionFragmentBuilder.create()
+ .withMinimum(BigDecimal.valueOf(minColorTemp)).withMaximum(BigDecimal.valueOf(maxColorTemp))
+ .withPattern("%.0f K").build().toStateDescription();
+ stateDescriptionProvider.setDescription(colorTempChannelUID,
+ Objects.requireNonNull(stateDescription));
+ }
+ } else {
+ // Not a big deal; probably just an older device
+ logger.warn("[{}] No response to getModelConfig request from device", config.ipAddress);
+ }
+ } else {
+ logger.debug("[{}] No response to getSystemConfig request from device at {}", config.ipAddress,
+ config.macAddress);
+ // Not calling it "gone" because it's probably just been powered off and will beback any time
+ updateStatus(ThingStatus.OFFLINE);
+ }
+ }
+
+ /**
+ * Registers with the device - this tells the device to begin sending 5-second
+ * heartbeat (hb) status updates. Status updates are sent by the device every 5
+ * sec and on any state change for 30s after registration. For continuous
+ * heart-beats the registration must be re-sent after 30s.
+ */
+ private synchronized void registerWithDevice() {
+ WizResponse registrationResponse = sendRequestPacket(WizMethodType.Registration,
+ Objects.requireNonNull(registrationRequestParam));
+ if (registrationResponse != null) {
+ if (registrationResponse.getResultSuccess()) {
+ updateTimestamps();
+ } else {
+ logger.debug(
+ "[{}] Received response to getConfigRequest from device at {}, but it did not contain device configuration information.",
+ config.ipAddress, config.macAddress);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+ }
+ } else {
+ logger.debug("[{}] No response to registration request from device at {}", config.ipAddress,
+ config.macAddress);
+ // Not calling it "gone" because it's probably just been powered off and will be
+ // back any time
+ updateStatus(ThingStatus.OFFLINE);
+ }
+ }
+
+ private boolean hasConfigurationError() {
+ ThingStatusInfo statusInfo = getThing().getStatusInfo();
+ return statusInfo.getStatus() == ThingStatus.OFFLINE
+ && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
+ }
+
+ private int percentToColorTemp(PercentType command) {
+ int range = maxColorTemp - minColorTemp;
+ // NOTE: 0% is cold (highest K) and 100% is warm (lowest K)
+ return maxColorTemp - Math.round((range * command.floatValue()) / 100);
+ }
+
+ private PercentType colorTempToPercent(int temp) {
+ return new PercentType(BigDecimal.valueOf(((float) temp - minColorTemp) / (maxColorTemp - minColorTemp) * 100));
+ }
+
+ // SETTERS AND GETTERS
+ public String getIpAddress() {
+ return config.ipAddress;
+ }
+
+ public String getMacAddress() {
+ return config.macAddress;
+ }
+
+ public int getHomeId() {
+ return homeId;
+ }
+
+ private void updateLightState(String channelId, State state) {
+ if (isFan) {
+ updateState(new ChannelUID(this.getThing().getUID(), CHANNEL_GROUP_LIGHT, channelId), state);
+ } else {
+ updateState(channelId, state);
+ }
+ }
+
+ private void updateFanState(String channelId, State state) {
+ if (isFanOnly) {
+ updateState(channelId, state);
+ } else {
+ updateState(new ChannelUID(this.getThing().getUID(), CHANNEL_GROUP_FAN, channelId), state);
+ }
+ }
+
+ private void updateDeviceState(String channelId, State state) {
+ if (isFan && !isFanOnly) {
+ updateState(new ChannelUID(this.getThing().getUID(), CHANNEL_GROUP_DEVICE, channelId), state);
+ } else {
+ updateState(channelId, state);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizMediator.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizMediator.java
new file mode 100644
index 0000000000000..29ee6ff6fc774
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizMediator.java
@@ -0,0 +1,87 @@
+/**
+ * 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.wiz.internal.handler;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.wiz.internal.discovery.WizDiscoveryService;
+import org.openhab.binding.wiz.internal.entities.RegistrationRequestParam;
+import org.openhab.binding.wiz.internal.entities.WizResponse;
+import org.openhab.core.net.NetworkAddressService;
+import org.openhab.core.thing.Thing;
+
+/**
+ * The {@link WizMediator} is responsible for receiving all the sync
+ * packets and route correctly to each handler.
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ * @author Joshua Freeman - pass through NetworkAddressService
+ */
+@NonNullByDefault
+public interface WizMediator {
+
+ /**
+ * This method is called by the {@link WizUpdateReceiverRunnable}, when
+ * one new message has been received.
+ *
+ * @param receivedMessage the {@link WizResponse} message.
+ */
+ void processReceivedPacket(final WizResponse receivedMessage);
+
+ /**
+ * Returns a {@link RegistrationRequestParam} based on the current OpenHAB
+ * connection.
+ *
+ */
+ RegistrationRequestParam getRegistrationParams() throws IllegalStateException;
+
+ /**
+ * Registers a new {@link Thing} and the corresponding
+ * {@link WizHandler}.
+ *
+ * @param thing the {@link Thing}.
+ * @param handler the {@link WizHandler}.
+ */
+ void registerThingAndWizBulbHandler(final Thing thing, final WizHandler handler);
+
+ /**
+ * Unregisters a {@link WizHandler} by the corresponding {@link Thing}.
+ *
+ * @param thing the {@link Thing}.
+ */
+ void unregisterWizBulbHandlerByThing(final Thing thing);
+
+ /**
+ * Returns all the {@link Thing} registered.
+ *
+ * @returns all the {@link Thing}.
+ */
+ Set getAllThingsRegistered();
+
+ /**
+ * Sets the discovery service to inform the user when one new thing has been
+ * found.
+ *
+ * @param discoveryService the discovery service.
+ */
+ void setDiscoveryService(final @Nullable WizDiscoveryService discoveryService);
+
+ /**
+ * Gets the NetworkAddressService used to configure the mediator instance.
+ *
+ * @return networkAddressService
+ */
+ NetworkAddressService getNetworkAddressService();
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizMediatorImpl.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizMediatorImpl.java
new file mode 100644
index 0000000000000..c354df448f779
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizMediatorImpl.java
@@ -0,0 +1,232 @@
+/**
+ * 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.wiz.internal.handler;
+
+import static org.openhab.binding.wiz.internal.WizBindingConstants.*;
+
+import java.net.SocketException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.wiz.internal.WizBindingConstants;
+import org.openhab.binding.wiz.internal.discovery.WizDiscoveryService;
+import org.openhab.binding.wiz.internal.entities.RegistrationRequestParam;
+import org.openhab.binding.wiz.internal.entities.WizResponse;
+import org.openhab.binding.wiz.internal.runnable.WizUpdateReceiverRunnable;
+import org.openhab.binding.wiz.internal.utils.NetworkUtils;
+import org.openhab.core.net.NetworkAddressService;
+import org.openhab.core.thing.Thing;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link WizMediatorImpl} is responsible for receiving all the sync
+ * packets and route correctly to each handler.
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ * @author Joshua Freeman - pass through NetworkAddressService
+ */
+@Component(configurationPid = "WizMediator", service = WizMediator.class)
+@NonNullByDefault
+public class WizMediatorImpl implements WizMediator {
+
+ private final Logger logger = LoggerFactory.getLogger(WizMediatorImpl.class);
+
+ private final Map handlersRegisteredByThing = new HashMap<>();
+
+ private @Nullable WizUpdateReceiverRunnable receiver;
+ private @Nullable Thread receiverThread;
+
+ private @Nullable WizDiscoveryService wizDiscoveryService;
+
+ private final NetworkAddressService networkAddressService;
+
+ /**
+ * Constructor for the mediator implementation.
+ *
+ * @param IllegalArgumentException if the timeout < 0
+ */
+ @Activate
+ public WizMediatorImpl(
+ @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.STATIC) NetworkAddressService networkAddressService) {
+ this.networkAddressService = networkAddressService;
+ this.initMediatorWizBulbUpdateReceiverRunnable();
+ }
+
+ /**
+ * Called at the service deactivation.
+ *
+ * @param componentContext the componentContext
+ */
+ protected void deactivate(final ComponentContext componentContext) {
+ WizUpdateReceiverRunnable receiver = this.receiver;
+ if (receiver != null) {
+ receiver.shutdown();
+ }
+ }
+
+ /**
+ * This method is called by the {@link WizUpdateReceiverRunnable}, when
+ * one new message has been received.
+ *
+ * @param receivedMessage the {@link WizResponse} message.
+ */
+ @Override
+ public void processReceivedPacket(final WizResponse receivedMessage) {
+ logger.debug("Received packet from: {} - {} with method: [{}]", receivedMessage.getWizResponseIpAddress(),
+ receivedMessage.getWizResponseMacAddress(), receivedMessage.getMethod());
+
+ String bulbIp = receivedMessage.getWizResponseIpAddress();
+ String bulbMac = receivedMessage.getWizResponseMacAddress();
+
+ if (!bulbMac.isEmpty()) {
+ @Nullable
+ WizHandler handler = this.getHandlerRegisteredByMac(bulbMac);
+
+ if (handler != null) {
+ // deliver message to handler.
+ handler.newReceivedResponseMessage(receivedMessage);
+ } else if (!bulbIp.isEmpty()) {
+ logger.debug("There is no handler registered for mac address: {}",
+ receivedMessage.getWizResponseMacAddress());
+ WizDiscoveryService discoveryServe = this.wizDiscoveryService;
+ if (discoveryServe != null) {
+ discoveryServe.discoveredLight(bulbMac, bulbIp);
+ logger.trace("Sending a new thing to the discovery service. MAC: {} IP: {}", bulbMac, bulbIp);
+ } else {
+ logger.trace("There is no discovery service registered to receive the new bulb!");
+ }
+ }
+ } else {
+ logger.warn("The sync response did not contain a valid mac address, it cannot be processed.");
+ }
+ }
+
+ /**
+ * Register one new {@link Thing} and the corresponding
+ * {@link WizHandler}.
+ *
+ * @param thing the {@link Thing}.
+ * @param handler the {@link WizHandler}.
+ */
+ @Override
+ public void registerThingAndWizBulbHandler(final Thing thing, final WizHandler handler) {
+ this.handlersRegisteredByThing.put(thing, handler);
+ }
+
+ /**
+ * Unregister one {@link WizHandler} by the corresponding {@link Thing}.
+ *
+ * @param thing the {@link Thing}.
+ */
+ @Override
+ public void unregisterWizBulbHandlerByThing(final Thing thing) {
+ this.handlersRegisteredByThing.remove(thing);
+ }
+
+ /**
+ * Utility method to get the registered thing handler in mediator by the mac
+ * address.
+ *
+ * @param macAddress the mac address of the thing of the handler.
+ * @return {@link WizHandler} if found.
+ */
+ private @Nullable WizHandler getHandlerRegisteredByMac(final String macAddress) {
+ WizHandler searchedHandler = null;
+ for (WizHandler handler : this.handlersRegisteredByThing.values()) {
+ if (macAddress.equalsIgnoreCase(handler.getMacAddress())) {
+ searchedHandler = handler;
+ // don't spend more computation. Found the handler.
+ break;
+ }
+ }
+ return searchedHandler;
+ }
+
+ /**
+ * Inits the mediator WizBulbUpdateReceiverRunnable thread. This thread is
+ * responsible to receive all packets from Wiz Bulbs, and redirect the messages
+ * to mediator.
+ */
+ private void initMediatorWizBulbUpdateReceiverRunnable() {
+ WizUpdateReceiverRunnable receiver = this.receiver;
+ Thread receiverThread = this.receiverThread;
+ // try with handler port if is null
+ if ((receiver == null)
+ || ((receiverThread != null) && (receiverThread.isInterrupted() || !receiverThread.isAlive()))) {
+ try {
+ logger.trace("Receiver thread is either null, interrupted, or dead.");
+ WizUpdateReceiverRunnable newReceiver = new WizUpdateReceiverRunnable(this, DEFAULT_LISTENER_UDP_PORT);
+ Thread newThread = new Thread(newReceiver,
+ "OH-binding-" + WizBindingConstants.BINDING_ID + "-ReceiverThread");
+ newThread.setDaemon(true);
+ newThread.start();
+ this.receiver = newReceiver;
+ this.receiverThread = newThread;
+ } catch (SocketException e) {
+ logger.debug("Cannot start the socket with default port {}...", e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Returns all the {@link Thing} registered.
+ *
+ * @returns all the {@link Thing}.
+ */
+ @Override
+ public Set getAllThingsRegistered() {
+ return this.handlersRegisteredByThing.keySet();
+ }
+
+ /**
+ * Returns a {@link RegistrationRequestParam} based on the current openHAB
+ * connection.
+ *
+ * @throws IllegalStateException
+ */
+ public RegistrationRequestParam getRegistrationParams() throws IllegalStateException {
+ String ipAddress = networkAddressService.getPrimaryIpv4HostAddress();
+ String macAddress = null;
+ if (ipAddress != null) {
+ macAddress = NetworkUtils.getMacAddress(ipAddress);
+ }
+ if (ipAddress == null || macAddress == null) {
+ throw new IllegalStateException("Unable to determine openHAB's IP and/or MAC address");
+ }
+ return new RegistrationRequestParam(ipAddress, true, 0, macAddress);
+ }
+
+ @Override
+ public void setDiscoveryService(final @Nullable WizDiscoveryService discoveryService) {
+ this.wizDiscoveryService = discoveryService;
+ }
+
+ public @Nullable WizDiscoveryService getDiscoveryService() {
+ return this.wizDiscoveryService;
+ }
+
+ @Override
+ public NetworkAddressService getNetworkAddressService() {
+ return this.networkAddressService;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/runnable/WizUpdateReceiverRunnable.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/runnable/WizUpdateReceiverRunnable.java
new file mode 100644
index 0000000000000..0946a5168b392
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/runnable/WizUpdateReceiverRunnable.java
@@ -0,0 +1,141 @@
+/**
+ * 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.wiz.internal.runnable;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.wiz.internal.entities.WizResponse;
+import org.openhab.binding.wiz.internal.handler.WizMediator;
+import org.openhab.binding.wiz.internal.utils.WizPacketConverter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This Thread is responsible for receiving all sync messages and redirecting them to
+ * {@link WizMediator}.
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class WizUpdateReceiverRunnable implements Runnable {
+
+ private static final int TIMEOUT_TO_DATAGRAM_RECEPTION_MILLISECONDS = 15000;
+
+ private final Logger logger = LoggerFactory.getLogger(WizUpdateReceiverRunnable.class);
+
+ private DatagramSocket datagramSocket;
+ private final WizMediator mediator;
+ private final WizPacketConverter packetConverter = new WizPacketConverter();
+
+ private boolean shutdown;
+ private int listeningPort;
+
+ /**
+ * Constructor of the receiver runnable thread.
+ *
+ * @param mediator the {@link WizMediator}
+ * @param listeningPort the listening UDP port
+ * @throws SocketException is some problem occurs opening the socket.
+ */
+ public WizUpdateReceiverRunnable(final WizMediator mediator, final int listeningPort) throws SocketException {
+ this.listeningPort = listeningPort;
+ this.mediator = mediator;
+
+ // Create a socket to listen on the port.
+ logger.debug("Opening socket and start listening UDP port: {}", listeningPort);
+ DatagramSocket dsocket = new DatagramSocket(null);
+ dsocket.setReuseAddress(true);
+ dsocket.setBroadcast(true);
+ dsocket.setSoTimeout(TIMEOUT_TO_DATAGRAM_RECEPTION_MILLISECONDS);
+ dsocket.bind(new InetSocketAddress(listeningPort));
+ this.datagramSocket = dsocket;
+
+ this.shutdown = false;
+ }
+
+ @Override
+ public void run() {
+ try {
+ // Now loop forever, waiting to receive packets and redirect them to mediator.
+ while (!this.shutdown) {
+ datagramSocketHealthRoutine();
+
+ // Create a buffer to read datagrams into. If a
+ // packet is larger than this buffer, the
+ // excess will simply be discarded!
+ byte[] buffer = new byte[2048];
+
+ // Create a packet to receive data into the buffer
+ DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
+
+ // Wait to receive a datagram
+ try {
+ datagramSocket.receive(packet);
+
+ // Redirect packet to the mediator
+ WizResponse response = this.packetConverter.transformResponsePacket(packet);
+ if (response != null) {
+ this.mediator.processReceivedPacket(response);
+ } else {
+ logger.debug("No WizResponse was parsed from returned packet");
+ }
+ } catch (SocketTimeoutException e) {
+ logger.trace("No incoming data on port {} during {} ms socket was listening.", listeningPort,
+ TIMEOUT_TO_DATAGRAM_RECEPTION_MILLISECONDS);
+ } catch (IOException e) {
+ logger.debug("One exception has occurred: {} ", e.getMessage());
+ }
+ }
+ } finally {
+ // close the socket
+ datagramSocket.close();
+ }
+ }
+
+ private void datagramSocketHealthRoutine() {
+ DatagramSocket datagramSocket = this.datagramSocket;
+ if (datagramSocket.isClosed() || !datagramSocket.isConnected()) {
+ logger.trace("Datagram Socket is disconnected or has been closed (probably timed out), reconnecting...");
+ try {
+ // close the socket before trying to reopen
+ this.datagramSocket.close();
+ logger.trace("Old socket closed.");
+ DatagramSocket dsocket = new DatagramSocket(null);
+ dsocket.setReuseAddress(true);
+ dsocket.setBroadcast(true);
+ dsocket.setSoTimeout(TIMEOUT_TO_DATAGRAM_RECEPTION_MILLISECONDS);
+ dsocket.bind(new InetSocketAddress(listeningPort));
+ this.datagramSocket = dsocket;
+ logger.trace("Datagram Socket reconnected.");
+ } catch (SocketException exception) {
+ logger.debug("Problem creating one new socket on port {}. Error: {}", listeningPort,
+ exception.getLocalizedMessage());
+ }
+ }
+ }
+
+ /**
+ * Gracefully shutdown thread. Worst case takes TIMEOUT_TO_DATAGRAM_RECEPTION_MILLISECONDS to
+ * shutdown.
+ */
+ public void shutdown() {
+ this.shutdown = true;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/NetworkUtils.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/NetworkUtils.java
new file mode 100644
index 0000000000000..1987c5c24eecc
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/NetworkUtils.java
@@ -0,0 +1,75 @@
+/**
+ * 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.wiz.internal.utils;
+
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Enumeration;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Utility class to perform some network routines.
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ * @author Joshua Freeman - Modified to get MAC matching IP
+ *
+ */
+@NonNullByDefault
+public final class NetworkUtils {
+ /**
+ * Returns the MAC address of the openHAB first network device.
+ *
+ * @return The MAC address of the openHAB network device.
+ */
+ public static @Nullable String getMacAddress(String matchIP) {
+ try {
+ Enumeration networks = NetworkInterface.getNetworkInterfaces();
+ while (networks.hasMoreElements()) {
+ NetworkInterface network = networks.nextElement();
+
+ if (networkMatchesIP(network, matchIP)) {
+ byte[] hardwareAddress = network.getHardwareAddress();
+ if (hardwareAddress == null) {
+ continue;
+ }
+ return convertBytesToMACString(hardwareAddress);
+ }
+ }
+ } catch (SocketException e) {
+ }
+ return null;
+ }
+
+ private static boolean networkMatchesIP(NetworkInterface network, String ip) {
+ for (InterfaceAddress interfaceAddress : network.getInterfaceAddresses()) {
+ String hostAddress = interfaceAddress.getAddress().getHostAddress();
+ if (ip.equals(hostAddress)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static String convertBytesToMACString(byte[] hardwareAddress) {
+ StringBuilder macAddressBuilder = new StringBuilder();
+ for (int macAddressByteIndex = 0; macAddressByteIndex < hardwareAddress.length; macAddressByteIndex++) {
+ String macAddressHexByte = String.format("%02X", hardwareAddress[macAddressByteIndex]);
+ macAddressBuilder.append(macAddressHexByte);
+ }
+ return macAddressBuilder.toString();
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/ValidationUtils.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/ValidationUtils.java
new file mode 100644
index 0000000000000..5ae40af4d3183
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/ValidationUtils.java
@@ -0,0 +1,46 @@
+/**
+ * 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.wiz.internal.utils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Utility static class to perform some validations.
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ *
+ */
+@NonNullByDefault
+public final class ValidationUtils {
+
+ private ValidationUtils() {
+ // avoid instantiation.
+ }
+
+ public static final String MAC_PATTERN = "^([0-9A-Fa-f]{2}[:-]*){5}([0-9A-Fa-f]{2})$";
+ private static final Pattern VALID_PATTERN = Pattern.compile(ValidationUtils.MAC_PATTERN);
+
+ /**
+ * Validates if one Mac address is valid.
+ *
+ * @param mac the mac, with or without :
+ * @return true if is valid.
+ */
+ public static boolean isMacValid(final String mac) {
+ Matcher matcher = VALID_PATTERN.matcher(mac);
+ return matcher.matches();
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizColorConverter.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizColorConverter.java
new file mode 100644
index 0000000000000..75423fad37e3f
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizColorConverter.java
@@ -0,0 +1,118 @@
+/**
+ * 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.wiz.internal.utils;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.util.ColorUtil;
+
+/**
+ * Utilities for converting colors and color temperatures
+ *
+ * The full color WiZ bulbs can produce colors and various temperatures of
+ * "whites" by mixing any of the available LEDs: RGBWwarm = RGBWWCwarm = Red,
+ * Green, Blue, Warm White, Cool White. When operating in full color mode, the
+ * warm whites are used to increase saturation (RGBW style). Temperatures of
+ * white can also be called directly as K instead of mixing cw/ww (c/w) The
+ * colors and temperatures need to be converted to the HSBType/PercentType
+ * supported by openHAB.
+ *
+ * @author Sara Geleskie Damiano - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class WizColorConverter {
+ /**
+ * Converts an {@link DecimalType} hue and a {@link PercentType} saturation to
+ * red, green, blue, and white (RGBW) components. Because the WiZ bulbs keep
+ * dimming in a separate channel, we only take account hue and saturation for
+ * the color channels. When creating colors, the WiZ bulbs only use the warm
+ * white channel, the cool white channel is ignored.
+ *
+ * Taken from Tasmota HsToRGB
+ *
+ * @param hsbColor the {@link HSBType}.
+ *
+ * @return an interger array of the color components
+ */
+ public int[] hsbToRgbw(HSBType hsb) {
+ // Since we're going to use the white lights to control saturation, recalculate what
+ // the HSBvalue would be if the color was at full brightness and saturation
+ HSBType hsbFullBrightness = new HSBType(hsb.getHue(), hsb.getSaturation(), PercentType.HUNDRED);
+ PercentType[] rgbPercent = ColorUtil.hsbToRgbPercent(hsbFullBrightness);
+ double redD = rgbPercent[0].doubleValue();
+ double greenD = rgbPercent[1].doubleValue();
+ double blueD = rgbPercent[2].doubleValue();
+
+ double saturationPercent = hsb.getSaturation().doubleValue() / 100;
+
+ int red;
+ int green;
+ int blue;
+ int white;
+
+ // Calculate the white intensity from saturation and adjust down the other colors
+ // This is approximately what the WiZ app does. Personally, I think it undersaturates everything
+ if (saturationPercent < 0.5) {
+ // At less than 50% saturation, maximize white and lower the other intensities by 2x of the saturation
+ // percent. (2x to give us full range between 0-50%)
+ // white = 255;
+ // ^^ WiZ does this.. I think it's very undersaturated that way
+ white = 255 / 2; // Divide by two to not undersaturate
+ red = (int) (redD * (2 * saturationPercent));
+ green = (int) (greenD * (2 * saturationPercent));
+ blue = (int) (blueD * (2 * saturationPercent));
+ } else {
+ // At >50% saturation, colors are at full and increase saturation by decreasing the white intensity.
+ // white = (int) (255 * 2 * (1 - saturationPercent));
+ // ^^ WiZ does this.. I think it's very undersaturated that way
+ white = (int) ((255 / 2) * 2 * (1 - saturationPercent));
+ red = (int) redD;
+ green = (int) greenD;
+ blue = (int) blueD;
+ }
+
+ // Note: We're keeping the brightness in a totally separate channel
+ return new int[] { red, green, blue, white };
+ }
+
+ /**
+ * Converts Red/Green/Blue/White components to Hue and saturation.
+ *
+ * @param int red - the value of the red component (0-255)
+ * @param int green - the value of the green component (0-255)
+ * @param int blue - the value of the blue component (0-255)
+ * @param int white - the value of the white component (0-255)
+ * @param int dimming - the brightness of the bulb, independent of the RGB color (0-100)
+ *
+ * Totally made this up.
+ *
+ * @return a {@link HSBType} with the color components
+ */
+ public HSBType rgbwDimmingToHSB(int red, int green, int blue, int white, int dimming) {
+ // Can get hue from the ratios of the colors.
+ // The calculated *hue* component of the HSB should be correct regardless of the
+ // state of the white lights because it's strictly based on the ratio of the colors
+ DecimalType hue = HSBType.fromRGB(red, green, blue).getHue();
+ double saturationPercent;
+ if (white < 255) {
+ saturationPercent = (int) (1 - (white / (255 * 2)));
+ } else {
+ saturationPercent = Math.max(red, Math.max(green, blue)) / (255 * 2);
+ }
+ HSBType out = new HSBType(hue, new PercentType((int) saturationPercent * 100), new PercentType(dimming));
+ return out;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizPacketConverter.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizPacketConverter.java
new file mode 100644
index 0000000000000..bfe78eeabfeb1
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizPacketConverter.java
@@ -0,0 +1,94 @@
+/**
+ * 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.wiz.internal.utils;
+
+import static java.nio.charset.StandardCharsets.*;
+
+import java.net.DatagramPacket;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.wiz.internal.entities.WizRequest;
+import org.openhab.binding.wiz.internal.entities.WizResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+
+/**
+ * Transforms the datagram packet to request/response
+ *
+ * @author Sriram Balakrishnan - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class WizPacketConverter {
+
+ private final Logger logger = LoggerFactory.getLogger(WizPacketConverter.class);
+
+ private Gson wizGsonBuilder;
+
+ /**
+ * Default constructor of the packet converter.
+ */
+ public WizPacketConverter() {
+ GsonBuilder gsonBuilder = new GsonBuilder();
+ gsonBuilder.registerTypeAdapter(WizResponse.class, new WizResponseDeserializer());
+ gsonBuilder.excludeFieldsWithoutExposeAnnotation();
+ Gson gson = gsonBuilder.create();
+ this.wizGsonBuilder = gson;
+ }
+
+ /**
+ * Method that transforms one {@link WizRequest} to json requst
+ *
+ * @param requestPacket the {@link WizRequest}.
+ * @return the byte array with the message.
+ */
+ public byte[] transformToByteMessage(final WizRequest requestPacket) {
+ byte[] requestDatagram = null;
+
+ // {"id":20,"method":"setPilot","params":{"sceneId":18}}
+ String jsonCmd = this.wizGsonBuilder.toJson(requestPacket);
+
+ requestDatagram = jsonCmd.getBytes(UTF_8);
+ return requestDatagram;
+ }
+
+ /**
+ * Method that transforms {@link DatagramPacket} to a
+ * {@link WizResponse} Object
+ *
+ * @param packet the {@link DatagramPacket}
+ * @return the {@link WizResponse}
+ */
+ public @Nullable WizResponse transformResponsePacket(final DatagramPacket packet) {
+ String responseJson = new String(packet.getData(), 0, packet.getLength(), UTF_8);
+ logger.debug("Incoming packet from {} to convert -> {}", packet.getAddress().getHostAddress(), responseJson);
+
+ @Nullable
+ WizResponse response = null;
+ try {
+ response = this.wizGsonBuilder.fromJson(responseJson, WizResponse.class);
+ if (response == null) {
+ throw new JsonParseException("JSON is empty");
+ }
+ response.setWizResponseIpAddress(packet.getAddress().getHostAddress());
+ } catch (JsonParseException e) {
+ logger.debug("Error parsing json! {}", e.getMessage());
+ }
+ return response;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizResponseDeserializer.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizResponseDeserializer.java
new file mode 100644
index 0000000000000..6cfc24ad85d34
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizResponseDeserializer.java
@@ -0,0 +1,228 @@
+/**
+ * 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.wiz.internal.utils;
+
+import java.lang.reflect.Type;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.wiz.internal.entities.ErrorResponseResult;
+import org.openhab.binding.wiz.internal.entities.ModelConfigResult;
+import org.openhab.binding.wiz.internal.entities.SystemConfigResult;
+import org.openhab.binding.wiz.internal.entities.WizResponse;
+import org.openhab.binding.wiz.internal.entities.WizSyncState;
+import org.openhab.binding.wiz.internal.enums.WizMethodType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+
+/**
+ * Deserializes incoming json
+ *
+ * @author Sara Geleskie Damiano - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class WizResponseDeserializer implements JsonDeserializer {
+ private final Logger logger = LoggerFactory.getLogger(WizResponseDeserializer.class);
+
+ @Override
+ @Nullable
+ public WizResponse deserialize(@Nullable JsonElement json, @Nullable Type typeOfT,
+ @Nullable JsonDeserializationContext context) throws JsonParseException {
+ // The outgoing response
+ WizResponse deserializedResponse = new WizResponse();
+
+ // The incoming JSON
+ JsonObject jobject;
+ if (json == null) {
+ logger.trace("No json provided to parse.");
+ } else if (context == null) {
+ logger.trace("No context available for parsing sub-objects.");
+ } else {
+ jobject = json.getAsJsonObject();
+
+ // Parse the ID
+ if (jobject.has("id")) {
+ deserializedResponse.setId(jobject.get("id").getAsInt());
+ }
+ // Parse the environment - I think this is always sent, but I'm checking anyway
+ if (jobject.has("env")) {
+ deserializedResponse.setEnv(jobject.get("env").getAsString());
+ }
+
+ // Check if the response contains an error
+ // Return without completing parsing if there's an error
+ if (jobject.has("error")) {
+ ErrorResponseResult error = context.deserialize(jobject.getAsJsonObject("error"),
+ ErrorResponseResult.class);
+ deserializedResponse.setError(error);
+ if (jobject.has("method")) {
+ logger.debug("Bulb returned an error on method {}: {}, {}", jobject.get("method"), error.code,
+ error.message);
+ } else {
+ logger.debug("Bulb returned an error: {}", error.code);
+ }
+ return deserializedResponse;
+ }
+
+ // Parse the method. We will use the method to decide how to continue to parse
+ // Bail out of everything if we cannot understand the method.
+ WizMethodType method;
+ if (jobject.has("method")) {
+ try {
+ String inMethod = jobject.get("method").getAsString();
+ String properCaseMethod = inMethod.substring(0, 1).toUpperCase() + inMethod.substring(1);
+ method = WizMethodType.valueOf(properCaseMethod);
+ deserializedResponse.setMethod(method);
+ } catch (IllegalArgumentException e) {
+ logger.debug("Bulb returned an invalid method: {}", jobject.get("method"));
+ return deserializedResponse;
+ }
+ } else {
+ throw new JsonParseException("Incoming message did not contain a method and cannot be parsed!");
+ }
+
+ switch (method) {
+ case Registration:
+ // {"method": "registration", "id": 1, "env": "pro", "result": {"mac":
+ // "macOfopenHAB", "success": true}}
+ if (!jobject.has("result")) {
+ throw new JsonParseException("registration received, but no result object present");
+ }
+ JsonObject registrationResult = jobject.getAsJsonObject("result");
+ if (!registrationResult.has("mac")) {
+ throw new JsonParseException("registration received, but no MAC address present");
+ }
+ String mac = registrationResult.get("mac").getAsString();
+ deserializedResponse.setWizResponseMacAddress(mac);
+ deserializedResponse.setResultSucess(registrationResult.get("success").getAsBoolean());
+ logger.trace("Registration result deserialized with mac {} and success {}", mac,
+ registrationResult.get("success").getAsBoolean());
+ break;
+
+ case Pulse:
+ // {"method":"pulse","id":22,"env":"pro","result":{"success":true}}
+ case SetPilot:
+ // {"method":"setPilot","id":24,"env":"pro","result":{"success":true}}
+ if (!jobject.has("result")) {
+ throw new JsonParseException("pulse or setPilot method received, but no result object present");
+ }
+ JsonObject setResult = jobject.getAsJsonObject("result");
+ deserializedResponse.setResultSucess(setResult.get("success").getAsBoolean());
+ logger.trace("Result deserialized - command success {}", setResult.get("success").getAsBoolean());
+ break;
+
+ case FirstBeat:
+ // {"method": "firstBeat", "id": 0, "env": "pro", "params": {"mac": "theBulbMacAddress",
+ // "homeId": xxxxxx, "fwVersion": "1.15.2"}}
+ if (!jobject.has("params")) {
+ throw new JsonParseException("firstBeat received, but no params object present");
+ }
+ SystemConfigResult parsedFBParams = context.deserialize(jobject.getAsJsonObject("params"),
+ SystemConfigResult.class);
+ if (parsedFBParams.mac.isEmpty()) {
+ throw new JsonParseException("firstBeat received, but no MAC address present");
+ }
+ deserializedResponse.setWizResponseMacAddress(parsedFBParams.mac);
+ deserializedResponse.setResultSucess(true);
+ deserializedResponse.setSystemConfigResult(parsedFBParams);
+ logger.trace("firstBeat result deserialized with mac {}", parsedFBParams.mac);
+ break;
+
+ case GetModelConfig:
+ if (!jobject.has("result")) {
+ throw new JsonParseException("getModelConfig received, but no result object present");
+ }
+ ModelConfigResult parsedMResult = context.deserialize(jobject.getAsJsonObject("result"),
+ ModelConfigResult.class);
+ deserializedResponse.setResultSucess(true);
+ deserializedResponse.setModelConfigResult(parsedMResult);
+ break;
+
+ case GetSystemConfig:
+ // {"method": "getSystemConfig", "id": 22, "env": "pro",
+ // "result": {"mac": "theBulbMacAddress", "homeId": xxxxxx, "roomId": xxxxxx,
+ // "homeLock": false, "pairingLock": false, "typeId": 0, "moduleName":
+ // "ESP01_SHRGB1C_31", "fwVersion": "1.15.2", "groupId": 0, "drvConf":[33,1]}}
+ if (!jobject.has("result")) {
+ throw new JsonParseException("getSystemConfig received, but no result object present");
+ }
+ SystemConfigResult parsedCResult = context.deserialize(jobject.getAsJsonObject("result"),
+ SystemConfigResult.class);
+ if (parsedCResult.mac.isEmpty()) {
+ throw new JsonParseException("getSystemConfig received, but no MAC address present");
+ }
+ deserializedResponse.setWizResponseMacAddress(parsedCResult.mac);
+ deserializedResponse.setResultSucess(true);
+ deserializedResponse.setSystemConfigResult(parsedCResult);
+ logger.trace("systemConfig result deserialized with mac {}", parsedCResult.mac);
+ break;
+
+ case GetPilot:
+ // {"method": "getPilot", "id": 22, "env": "pro", "result": {"mac":
+ // "theBulbMacAddress", "rssi":-76, "state": true, "sceneId": 0, "temp": 2700,
+ // "dimming": 42, "schdPsetId": 5}}
+ if (!jobject.has("result")) {
+ throw new JsonParseException("getPilot received, but no result object present");
+ }
+ WizSyncState parsedPResult = context.deserialize(jobject.getAsJsonObject("result"),
+ WizSyncState.class);
+ if (parsedPResult.mac.isEmpty()) {
+ throw new JsonParseException("getPilot received, but no MAC address present");
+ }
+ deserializedResponse.setWizResponseMacAddress(parsedPResult.mac);
+ deserializedResponse.setResultSucess(true);
+ deserializedResponse.setSyncParams(parsedPResult);
+ logger.trace("getPilot result deserialized with mac {}", parsedPResult.mac);
+ break;
+
+ case SyncPilot:
+ // {"method": "syncPilot", "id": 219, "env": "pro", "params": { "mac":
+ // "theBulbMacAddress", "rssi": -72, "src": "hb", "mqttCd": 0, "state": true, "sceneId":
+ // 0, "temp": 3362, "dimming": 69, "schdPsetId": 5}}
+ if (!jobject.has("params")) {
+ throw new JsonParseException("syncPilot received, but no params object present");
+ }
+ WizSyncState parsedPParam = context.deserialize(jobject.getAsJsonObject("params"),
+ WizSyncState.class);
+ if (parsedPParam.mac.isEmpty()) {
+ throw new JsonParseException("syncPilot received, but no MAC address present");
+ }
+ deserializedResponse.setWizResponseMacAddress(parsedPParam.mac);
+ deserializedResponse.setResultSucess(true);
+ deserializedResponse.setSyncParams(parsedPParam);
+ logger.trace("syncPilot result deserialized with mac {}", parsedPParam.mac);
+ break;
+
+ case SetSystemConfig:
+ // ?? I'm not trying this at home!
+ case SetWifiConfig:
+ // ?? I'm not trying this at home!
+ case GetWifiConfig:
+ // The returns an encrypted string and I'm not using it so I'm not bothering to parse it
+ // {"method":"getWifiConfig","id":22,"env":"pro","result":{:["longStringInEncryptedUnicode"]}}
+ case UnknownMethod:
+ // This should just never happen
+ break;
+ }
+ }
+
+ return deserializedResponse;
+ }
+}
diff --git a/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644
index 0000000000000..620c84d014882
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/addon/addon.xml
@@ -0,0 +1,45 @@
+
+
+
+ binding
+ WiZ Binding
+ Binding for WiZ smart devices.
+ local
+
+
+
+ ip
+
+
+ type
+ ipBroadcast
+
+
+ destPort
+ 38899
+
+
+ requestPlain
+ {"method":"registration","id":1,"params":{"phoneIp":"$srcIp","register":false,"phoneMac":"$srcMac"}}
+
+
+ fmtMac
+ %02X
+
+
+ timeoutMs
+ 5000
+
+
+
+
+ response
+ .*
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/config/config.xml
new file mode 100644
index 0000000000000..bcea53ad47031
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/config/config.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+ MAC Address
+ MAC address of the device
+ true
+
+
+ IP Address
+ network-address
+ IP address of the device
+ true
+
+
+ Update Interval
+ Update time interval in seconds to request the status of the device while it is connected to the
+ network.
+ 60
+ true
+
+
+ Use Heartbeats
+ True to request continuous 5s heartbeats from device. Update interval is ignored when using heartbeats.
+ true
+ false
+
+
+ Reconnect Interval
+ Interval in minutes between attempts to reconnect with a device that is no longer responding to status
+ queries. When the device first connects to the network, it should send out a firstBeat message allowing openHAB to
+ immediately detect it. This is only as a back-up to re-find the device.
+ 15
+ true
+
+
+
+
diff --git a/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/i18n/wiz.properties b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/i18n/wiz.properties
new file mode 100644
index 0000000000000..620005071a551
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/i18n/wiz.properties
@@ -0,0 +1,116 @@
+# add-on
+
+addon.wiz.name = WiZ Binding
+addon.wiz.description = Binding for WiZ smart devices.
+
+# thing types
+
+thing-type.wiz.color-bulb.label = WiZ Color Bulb
+thing-type.wiz.color-bulb.description = Supports WiZ Full Color with Tunable White Bulbs
+thing-type.wiz.dimmable-bulb.label = WiZ Dimmable Bulb
+thing-type.wiz.dimmable-bulb.description = Supports WiZ Single Color Dimmable Bulbs
+thing-type.wiz.fan-with-dimmable-bulb.label = WiZ Ceiling Fan With Dimmable Bulb
+thing-type.wiz.fan-with-dimmable-bulb.description = Supports WiZ Ceiling Fans With a Dimmable Bulb
+thing-type.wiz.fan.label = WiZ Ceiling Fan
+thing-type.wiz.fan.description = Supports WiZ Ceiling Fans
+thing-type.wiz.plug.label = WiZ Smart Plug
+thing-type.wiz.plug.description = Supports WiZ Smart Plugs
+thing-type.wiz.tunable-bulb.label = WiZ Tunable Bulb
+thing-type.wiz.tunable-bulb.description = Supports WiZ Tunable White Bulbs
+
+# thing types config
+
+thing-type.config.wiz.device.ipAddress.label = IP Address
+thing-type.config.wiz.device.ipAddress.description = IP address of the device
+thing-type.config.wiz.device.macAddress.label = MAC Address
+thing-type.config.wiz.device.macAddress.description = MAC address of the device
+thing-type.config.wiz.device.reconnectInterval.label = Reconnect Interval
+thing-type.config.wiz.device.reconnectInterval.description = Interval in minutes between attempts to reconnect with a device that is no longer responding to status queries. When the device first connects to the network, it should send out a firstBeat message allowing openHAB to immediately detect it. This is only as a back-up to re-find the device.
+thing-type.config.wiz.device.updateInterval.label = Update Interval
+thing-type.config.wiz.device.updateInterval.description = Update time interval in seconds to request the status of the device while it is connected to the network.
+thing-type.config.wiz.device.useHeartBeats.label = Use Heartbeats
+thing-type.config.wiz.device.useHeartBeats.description = True to request continuous 5s heartbeats from device. Update interval is ignored when using heartbeats.
+
+# channel group types
+
+channel-group-type.wiz.device-channels.label = Device
+channel-group-type.wiz.dimmable-light.label = Light
+channel-group-type.wiz.fan-group.label = Fan
+
+# channel types
+
+channel-type.wiz.fan-mode.label = Mode
+channel-type.wiz.fan-mode.state.option.1 = Normal
+channel-type.wiz.fan-mode.state.option.2 = Breeze
+channel-type.wiz.fan-reverse.label = Reverse
+channel-type.wiz.fan-reverse.state.option.OFF = Forward
+channel-type.wiz.fan-reverse.state.option.ON = Reverse
+channel-type.wiz.fan-speed.label = Fan Speed
+channel-type.wiz.fan-speed.description = Speed of the fan, in arbitrary steps
+channel-type.wiz.last-update.label = Last Update
+channel-type.wiz.last-update.description = Timestamp of last status update
+channel-type.wiz.light-mode-speed.label = Dynamic Light Mode Speed
+channel-type.wiz.light-mode-speed.description = Speed of color/intensity changes in dynamic light modes
+channel-type.wiz.light-mode.label = Light Mode
+channel-type.wiz.light-mode.state.option.1 = Ocean
+channel-type.wiz.light-mode.state.option.2 = Romance
+channel-type.wiz.light-mode.state.option.3 = Sunset
+channel-type.wiz.light-mode.state.option.4 = Party
+channel-type.wiz.light-mode.state.option.5 = Fireplace
+channel-type.wiz.light-mode.state.option.6 = Cozy White
+channel-type.wiz.light-mode.state.option.7 = Forest
+channel-type.wiz.light-mode.state.option.8 = Pastel Colors
+channel-type.wiz.light-mode.state.option.9 = Wakeup
+channel-type.wiz.light-mode.state.option.10 = Bed Time
+channel-type.wiz.light-mode.state.option.11 = Warm White
+channel-type.wiz.light-mode.state.option.12 = Daylight
+channel-type.wiz.light-mode.state.option.13 = Cool White
+channel-type.wiz.light-mode.state.option.14 = Night Light
+channel-type.wiz.light-mode.state.option.15 = Focus
+channel-type.wiz.light-mode.state.option.16 = Relax
+channel-type.wiz.light-mode.state.option.17 = True Colors
+channel-type.wiz.light-mode.state.option.18 = TV Time
+channel-type.wiz.light-mode.state.option.19 = Plant Growth
+channel-type.wiz.light-mode.state.option.20 = Spring
+channel-type.wiz.light-mode.state.option.21 = Summer
+channel-type.wiz.light-mode.state.option.22 = Fall
+channel-type.wiz.light-mode.state.option.23 = Deep Dive
+channel-type.wiz.light-mode.state.option.24 = Jungle
+channel-type.wiz.light-mode.state.option.25 = Mojito
+channel-type.wiz.light-mode.state.option.26 = Club
+channel-type.wiz.light-mode.state.option.27 = Christmas
+channel-type.wiz.light-mode.state.option.28 = Halloween
+channel-type.wiz.light-mode.state.option.29 = Candlelight
+channel-type.wiz.light-mode.state.option.30 = Golden White
+channel-type.wiz.light-mode.state.option.31 = Pulse
+channel-type.wiz.light-mode.state.option.32 = Steampunk
+channel-type.wiz.rssi.label = RSSI
+channel-type.wiz.rssi.description = WiFi Received Signal Strength Indicator
+
+# thing types config
+
+thing-type.config.wiz.light.ipAddress.label = Bulb IP Address
+thing-type.config.wiz.light.ipAddress.description = IP address of the bulb
+thing-type.config.wiz.light.bulbMacAddress.label = Bulb MAC Address
+thing-type.config.wiz.light.bulbMacAddress.description = MAC address of the bulb
+thing-type.config.wiz.light.reconnectInterval.label = Reconnect Interval
+thing-type.config.wiz.light.reconnectInterval.description = Interval in minutes between attempts to reconnect with a bulb that is no longer responding to status queries. When the bulb first connects to the network, it should send out a firstBeat message allowing OpenHab to immediately detect it. This is only as a back-up to re-find the bulb.
+thing-type.config.wiz.light.updateInterval.label = Update Interval
+thing-type.config.wiz.light.updateInterval.description = Update time interval in seconds to request the status of the bulb while it is connected to the network.
+thing-type.config.wiz.light.useHeartBeats.label = Use Heartbeats
+thing-type.config.wiz.light.useHeartBeats.description = True to request continuous 5s heartbeats from bulb. Update interval is ignored when using heartbeats.
+
+# channel types
+
+channel-type.wiz.speed.label = Dynamic Light Mode Speed
+channel-type.wiz.speed.description = Speed of color/intensity changes in dynamic light modes
+
+# thing types
+
+thing-type.wiz.ceiling-fan.label = WiZ Ceiling Fan
+thing-type.wiz.ceiling-fan.description = Supports WiZ Ceiling Fans
+
+# channel types
+
+channel-type.wiz.color-temperature-abs.label = Color Temperature
+channel-type.wiz.color-temperature-abs.description = Controls the color temperature of the light in Kelvin
diff --git a/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 0000000000000..a5e1c78a64025
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,240 @@
+
+
+
+
+ WiZ Color Bulb
+ Supports WiZ Full Color with Tunable White Bulbs
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+ WiZ Tunable Bulb
+ Supports WiZ Tunable White Bulbs
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+ WiZ Dimmable Bulb
+ Supports WiZ Single Color Dimmable Bulbs
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+ WiZ Smart Plug
+ Supports WiZ Smart Plugs
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+ WiZ Ceiling Fan
+ Supports WiZ Ceiling Fans
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+ WiZ Ceiling Fan With Dimmable Bulb
+ Supports WiZ Ceiling Fans With a Dimmable Bulb
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+ String
+ Light Mode
+
+ Lighting
+
+
+
+ Ocean
+ Romance
+ Sunset
+ Party
+ Fireplace
+ Cozy White
+ Forest
+ Pastel Colors
+ Wakeup
+ Bed Time
+ Warm White
+ Daylight
+ Cool White
+ Night Light
+ Focus
+ Relax
+ True Colors
+ TV Time
+ Plant Growth
+ Spring
+ Summer
+ Fall
+ Deep Dive
+ Jungle
+ Mojito
+ Club
+ Christmas
+ Halloween
+ Candlelight
+ Golden White
+ Pulse
+ Steampunk
+
+
+
+
+
+ Dimmer
+ Dynamic Light Mode Speed
+ Speed of color/intensity changes in dynamic light modes
+
+ Lighting
+
+
+
+
+ Number
+ Fan Speed
+ Speed of the fan, in arbitrary steps
+ Fan
+
+
+
+
+ Switch
+ Reverse
+
+
+ Forward
+ Reverse
+
+
+
+
+
+ Number
+ Mode
+
+
+ Normal
+ Breeze
+
+
+
+
+
+ DateTime
+ Last Update
+ Timestamp of last status update
+ Time
+
+
+
+
+
+
+ Device
+
+
+
+
+
+
+
+
+ Light
+
+
+
+
+
+
+
+
+ Fan
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/update/instructions.xml b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/update/instructions.xml
new file mode 100644
index 0000000000000..febf38265f659
--- /dev/null
+++ b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/update/instructions.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.wlanthermo/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.wlanthermo/src/main/resources/OH-INF/thing/channel-types.xml
index b75a202535277..e90faaa70d2d7 100644
--- a/bundles/org.openhab.binding.wlanthermo/src/main/resources/OH-INF/thing/channel-types.xml
+++ b/bundles/org.openhab.binding.wlanthermo/src/main/resources/OH-INF/thing/channel-types.xml
@@ -251,7 +251,7 @@