diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 3a3e20519ae..22b4e2b1f2f 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -956,6 +956,11 @@
org.openhab.binding.lifx${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.linktap
+ ${project.version}
+ org.openhab.addons.bundlesorg.openhab.binding.linky
diff --git a/bundles/org.openhab.binding.linktap/NOTICE b/bundles/org.openhab.binding.linktap/NOTICE
new file mode 100644
index 00000000000..3e2c49e0050
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/NOTICE
@@ -0,0 +1,20 @@
+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
+
+== Third-party Content
+
+jsoup
+* License: MIT License
+* Project: https://jsoup.org/
+* Source: https://github.com/jhy/jsoup
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.linktap/README.md b/bundles/org.openhab.binding.linktap/README.md
new file mode 100644
index 00000000000..2e0099e3a2f
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/README.md
@@ -0,0 +1,234 @@
+# LinkTap Binding
+
+This binding is for [Link-Tap](https://www.link-tap.com/) devices.
+
+**This is for communication over a local area network, that prevents direct access to your gateway / openHAB instance from the internet.
+E.g. behind a router**
+
+The method of interaction this binding supports is:
+
+**Program and execution of the watering plan within the application**
+
+The currently supported capabilities include where supported by the gateway / device:
+
+- Time synchronisation to openHAB
+- Child lock controls
+- Monitoring and dismissal for device alarms (Water Cut, etc.)
+- Monitoring of sensor states (Battery, Zigbee Signal, Flow Meters Statistics, etc.)
+- Enable watering based on time duration / volume limits
+- Shutdown of active watering
+
+## Requirements
+
+A LinkTap gateway device such as the GW_02, in order for openHAB to connect to the system, as a gateway.
+Older GW_01 gateway devices have not been tested but should work with a static IP setup.
+
+The recommended minimum version of the firmware is:
+
+- **GW_01** is to start with **at least S609**
+- **GW_02** is to start with **at least G609**
+
+## Connection Options
+
+LinkTap supports MQTT and a direct interaction via HTTP.
+This binding directly interacts with LinkTap's gateway devices using the Local HTTP API (HTTP).
+The binding connects to the gateway's directly, and the Gateway is configured automatically to push updates to openHAB if it has a HTTP configured server.
+(Note HTTPS is not supported).
+
+Should the Gateway device's not be able to connect to the binding it automatically falls-back to a polling implementation (15 second cycle).
+The gateway supports 1 Local HTTP API, for an ideal behavior the Gateway should be able to connect to openHAB on a HTTP port by its IP, and only a single openHAB instance should be connected to a Gateway.
+It is recommended that you use **static IP's** for this binding, **for both openHAB and the Gateway device(s)** regardless of the gateway's model.
+
+If dynamic IPs are used for the gateway, the mDNS address is recommended to be used.
+This can be found when running a manual scan, for LinkTap Gateways.
+This will remove any DNS caching related issues, depending on your setup.
+
+## Supported Things
+
+This binding supports the follow thing types:
+
+| Thing Type | Thing Type UID | Discovery | Description |
+|------------|----------------|--------------------|------------------------------------------------------------------|
+| Bridge | gateway | Manual / Automatic | A connection to a LinkTap Gateway device |
+| Thing | device | Automatic | A end device such as one of the four controlled values on the Q1 |
+
+**NOTE** This binding was developed and tested using a GW-02 gateway with a Q1 device.
+
+## Discovery
+
+### Gateways
+
+If mDNS has been enabled on the Gateway device via it's webpage, then the gateway(s) will be discovered, and appear in the inbox when a manual scan is run when adding a LinkTap Gateway.
+It is however recommended to use **static IP addresses** and add the gateways directly using the IP address.
+
+### Devices
+
+Once connected to a LinkTap gateway, the binding will listen for updates of new devices and add them, to the inbox.
+If the gateway cannot publish to openHAB, then the gateway is checked every 2 minutes for new devices, and they are added to the inbox when discovered.
+
+## Binding Configuration
+
+### Gateway Configuration
+
+| Name | Type | Description | Recommended Values | Required | Advanced |
+|-----------------------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|----------|----------|
+| host | String | The hostname / IP address of the gateway device | | Yes | No |
+| username | String | The username if set for the gateway device | | No | No |
+| password | String | The password if set for the gateway device | | No | No |
+| enableMDNS | Switch | On connection whether the mDNS responder should be enabled on the gateway device | true | No | Yes |
+| enforceProtocolLimits | Switch | If true data outside of the allowed ranges against the protocol will be logged and not sent | true | No | Yes |
+| enableJSONComms | Switch | false by default for backwards compatibility, if using up to date firmware with no other local network applications set this to true, for more efficient communications | true | No | Yes |
+
+**NOTE** When enableMDNS is enabled, upon connection to the gateway option "Enable mDNS responder" is switched on.
+
+### Device Configuration
+
+| Name | Type | Description | Recommended Values | Required | Advanced |
+|--------------|---------|-----------------------------------------------------------------------|--------------------|----------|----------|
+| deviceId | String | The Device Id for the device under the gateway | | No (A,B) | No |
+| deviceName | String | The name allocated to the device by the app. (Must be unique if used) | | No (B) | No |
+| enableAlerts | boolean | On connection whether the device should be configured to send alerts | true | No | Yes |
+
+**NOTE:**
+
+(A) It is recommended to use the Device Id, for locating devices.
+This can be found in the LinkTap mobile application under Settings->TapLinker / ValveLinker, e.g.
+
+- ValueLinker_1 (D71BC52F004B1200_1-xxxx)
+ - has Device Id "ValveLinker_1"
+ - has Device Name D71BC52F004B1200_1
+
+(B) Either a **deviceId or deviceName is required** for the device to be located and used.
+
+## Channels
+
+| Name | Type | Description | Representation | Write Action | Note |
+|-------------------|---------------------------|-------------------------------------------------------------------------------------|----------------|------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
+| water-cut | Switch | Water cut-off alert | Alert | Dismiss alert | |
+| shutdown-failure | Switch | The device has failed to close the valve | Alert | Dismiss alert | |
+| high-flow | Switch | Unusually high flow rate detected alert | Alert | Dismiss alert | |
+| low-flow | Switch | Unusually low flow rate detected alert | Alert | Dismiss alert | |
+| fall-status | Switch | The device has fallen | Alert | Dismiss alert | |
+| mode | Text | The current watering plan mode | R | | |
+| flm-linked | Switch | The device has a included flow meter | R | | |
+| rf-linked | Switch | Is the device RF linked | R | | |
+| signal | Number:Dimensionless | Reception Signal Strength | R | | |
+| battery | Number:Dimensionless | Battery Remaining Level | R | | |
+| flow-rate | Number:VolumetricFlowRate | Current water flow rate | R | | |
+| volume | Number:Volume | Accumulated volume of current watering cycle | R | | |
+| eco-final | Switch | In ECO mode this is true when the final ON watering on segment is running | R | | |
+| remaining | Number:Time | Remaining duration of the current watering cycle | R | | |
+| duration | Number:Time | Total duration of current watering cycle | R | | |
+| watering | Switch | Active watering status | RW | True - Start immediate watering, False - Stops the current watering process, the next planned watering will run as scheduled | |
+| manual-watering | Switch | Manual watering mode status | R | | |
+| child-lock | Text | The child lock mode | RW | Unlocked - Button enabled, Partially locked -> 3 second push required, Completely locked -> Button disabled | If the GW has internet connectivity settings will be reset when it sync's to TapLink's servers. |
+| oh-dur-limit | Number:Time | Max duration allowed for the immediate watering | W | Max Time duration for "Start immediate watering" | |
+| oh-vol-limit | Number:Volume | Max Volume limit for immediate watering | W | Max Volume for "Start immediate watering" | |
+| plan-pause-enable | Switch | When ON will pause the current watering plan for an hour every 55 minutes | RW | Pause current watering plan every 55 mins for one hour | This disables the TapLink Watering Plan from being run, it is not reflected in the mobile app. |
+| plan-resume-time | DateTime | Displays when the last pause issued will expiry, resuming the current watering plan | R | | |
+| watering-plan-id | Text | Displays the current watering plan id | R | | |
+
+**NOTE:**
+There are 4 different areas of channels:
+
+- R (Read Only Data)
+ - These represent data published by the device
+- Alerts
+ - These are switches that are set to ON by the device when an alert condition is detected, such as a Water Cut.
+ - The alert can be dismissed by setting the switch to OFF
+- RW (Read Write Data)
+ - Provides the ability to read data
+ - Provides the ability to set a relevant state to the data
+- W Data
+ - Provides parameter values for the named action, it is stored within openHAB is not read from the device
+ - E.g. Start Immediate Watering
+ - Can be limited by a time duration - ohDurLimit
+ - If a flow meter is attached can be limited by a volume limit - ohVolLimit
+
+## Full Example
+
+### Thing Configuration
+
+- **Gateway Model**: GW_02
+- **Device Model**: Q1
+
+```java
+Bridge linktap:gateway:home "LinkTap GW02" [ host="192.168.0.21", enableMDNS=true, enableJSONComms=false, enforceProtocolLimits=true ] {
+ Thing device TapValve1 "Outdoor Tap 1" [ id="D71BC52E985B1200_1", name="ValveLinker_1", enableAlerts=true ]
+ Thing device TapValve2 "Outdoor Tap 2" [ id="D71BC52E985B1200_2", name="ValveLinker_2", enableAlerts=true ]
+ Thing device TapValve3 "Outdoor Tap 3" [ id="D71BC52E985B1200_3", name="ValveLinker_3", enableAlerts=true ]
+ Thing device TapValve4 "Outdoor Tap 4" [ id="D71BC52E985B1200_4", name="ValveLinker_4", enableAlerts=true ]
+}
+```
+
+### Item Configuration
+
+```java
+Number:Dimensionless Tap1BatteryLevel "Tap 1 - Battery Level" ["Point"] { channel="linktap:device:home:tapValve1:battery",unit="%%" }
+Number:Dimensionless Tap1SignalLevel "Tap 1 - Signal Level" ["Point"] { channel="linktap:device:home:tapValve1:signal",unit="%%" }
+Switch Tap1RfLinked "Tap 1 - RF Linked" ["Point"] { channel="linktap:device:home:tapValve1:rf-linked"}
+Switch Tap1FlmLinked "Tap 1 - FLM Linked" ["Point"] { channel="linktap:device:home:tapValve1:flm-linked"}
+Switch Tap1WaterCutAlert "Tap 1 - Water Cut Alert" ["Point"] { channel="linktap:device:home:tapValve1:water-cut" }
+Switch Tap1WaterFallAlert "Tap 1 - Fallen Alert" ["Point"] { channel="linktap:device:home:tapValve1:fall-status" }
+Switch Tap1WaterValveAlert "Tap 1 - Shutdown Failure Alert" ["Point"] { channel="linktap:device:home:tapValve1:shutdown-failure" }
+Switch Tap1WaterLowFlowAlert "Tap 1 - Low Flow Alert" ["Point"] { channel="linktap:device:home:tapValve1:low-flow" }
+Switch Tap1WaterHighFlowAlert "Tap 1 - High Flow Alert" ["Point"] { channel="linktap:device:home:tapValve1:high-flow" }
+String Tap1ChildLockMode "Tap 1 - Child Lock Mode" ["Point"] { channel="linktap:device:home:tapValve1:child-lock" }
+Number:VolumetricFlowRate Tap1FlowRate "Tap 1 - Flow Rate" ["Point"] { channel="linktap:device:home:tapValve1:flow-rate",unit="l/min" }
+Number:Volume Tap1WateringVolume "Tap 1 - Watering Volume" ["Point"] { channel="linktap:device:home:tapValve1:volume",unit="l" }
+Switch Tap1FinalEcoSegment "Tap 1 - Final ECO Segment" ["Point"] { channel="linktap:device:home:tapValve1:eco-final" }
+Switch Tap1Watering "Tap 1 - Watering" ["Point"] { channel="linktap:device:home:tapValve1:watering" }
+Switch Tap1ManualWatering "Tap 1 - Manual Watering" ["Point"] { channel="linktap:device:home:tapValve1:manual-watering" }
+String Tap1WateringMode "Tap 1 - Watering Mode"
+ * Gateway acquires its local time base from third-party application, and sends its
+ * end devices ID list to third-party application, through this message.
+ */
+ public static final int CMD_HANDSHAKE = 0;
+
+ /**
+ * Command - 1. Add / Register End Device
+ *
+ * @direction App->Broker->GW
+ * @description Add's / Registers a new device to the Gateway
+ * (e.g., water timer) to the Gateway.
+ */
+ public static final int CMD_ADD_END_DEVICE = 1;
+
+ /**
+ * Command - 2. Delete End Device
+ *
+ * @direction App->Broker->GW
+ * @description Removes / De-registers a device (e.g., water timer) from the Gateway.
+ */
+ public static final int CMD_REMOVE_END_DEVICE = 2;
+
+ /**
+ * Command - 3. Update Water Timer Status
+ *
+ * @direction App->Broker->GW
+ * @description Update water timer’s status
+ */
+ public static final int CMD_UPDATE_WATER_TIMER_STATUS = 3;
+
+ /**
+ * Command - 103. Update Water Timer Status Unsolicited
+ *
+ * @direction GW->Broker->App
+ * @description Update water timer’s status
+ */
+ public static final int CMD_UPDATE_WATER_TIMER_STATUS_UNSOLICITED = 103;
+
+ /**
+ * Command - 4. Send / Setup Water Plan
+ *
+ * @direction App->Broker->GW
+ * @description Send / set up watering plan
+ * (The prerequisite for the correct execution of the watering plan is that
+ * the Gateway’s local time base has been properly set through
+ * CMD:0 (CMD_HANDSHAKE) or
+ * CMD:13)
+ */
+ public static final int CMD_SETUP_WATER_PLAN = 4;
+
+ /**
+ * Command - 5. Delete Water Plan
+ *
+ * @direction App->Broker->GW
+ * @description Deletes the existing water plan
+ */
+ public static final int CMD_REMOVE_WATER_PLAN = 5;
+
+ /**
+ * Command - 6. Start Watering Immediately
+ *
+ * @direction App->Broker->GW
+ * @description Start watering for the immediate duration irrelevant
+ * of water plan. (Gateway local time base is not required
+ * for the operation of this mode).
+ */
+ public static final int CMD_IMMEDIATE_WATER_START = 6;
+
+ /**
+ * Command - 7. Stop Watering Immediately
+ *
+ * @direction App->Broker->GW
+ * @description Stop's watering immediately. The water plan will resume at
+ * the next point as setup.
+ */
+ public static final int CMD_IMMEDIATE_WATER_STOP = 7;
+
+ /**
+ * Command - 8. Fetch / Push Rainfall Data
+ *
+ * @direction GW->Broker->App
+ * @description Request for Rainfall data
+ * @direction App->Broker->GW
+ * @description Push of Rainfall data
+ */
+ public static final int CMD_RAINFALL_DATA = 8;
+
+ /**
+ * Command - 9. Notificaiton of watering has been skipped
+ *
+ * @direction GW->Broker->App
+ * @description Notification that a watering cycle has been skipped due to rainfall
+ */
+ public static final int CMD_NOTIFICATION_WATERING_SKIPPED = 9;
+
+ /**
+ * Command - 10. Alert Enablement / Disablement
+ *
+ * @direction App->Broker->GW
+ * @description Enable or disablement of particular monitoring alerts
+ */
+ public static final int CMD_ALERT_ENABLEMENT = 10;
+
+ /**
+ * Command - 11. Dismiss Alert
+ *
+ * @direction App->Broker->GW
+ * @description Dismisses the given alert
+ */
+ public static final int CMD_ALERT_DISMISS = 11;
+
+ /**
+ * Command - 12 Lockout state setup
+ *
+ * @direction App->Broker->GW
+ * @description Setup lockout state for manual On/Off button (for G15 and G25 models only)
+ */
+ public static final int CMD_LOCKOUT_STATE = 12;
+
+ /**
+ * Command - 13 Gateways Date & Time Sync Request
+ *
+ * @direction GW->Broker->App
+ * @description Request for the current date and time, for the Gateway to apply
+ */
+ public static final int CMD_DATETIME_SYNC = 13;
+
+ /**
+ * Command - 14 Read the Gateways Date & Time
+ *
+ * @direction App->Broker->Gw
+ * @description Fetch Gateway's local datetime
+ */
+ public static final int CMD_DATETIME_READ = 14;
+
+ /**
+ * Command - 15 Test wireless performance of end device
+ *
+ * @direction App->Broker->Gw
+ * @description Request a communications test between the Gateway and End Device
+ */
+ public static final int CMD_WIRELESS_CHECK = 15;
+
+ /**
+ * Command - 16 Get Gateway's configuration
+ *
+ * @direction App->Broker->Gw
+ * @description Request the current Gateway's configuration
+ */
+ public static final int CMD_GET_CONFIGURATION = 16;
+
+ /**
+ * Command - 17 Set Gateway's configuration
+ *
+ * @direction App->Broker->Gw
+ * @description Update the configuration for a device in the Gateway
+ */
+ public static final int CMD_SET_CONFIGURATION = 17;
+
+ /**
+ * Command - 18 Pause Water Plan
+ *
+ * @direction App->Broker->Gw
+ * @description Pause the Water Plan for the given duration
+ * (0.1 to 240 hours)
+ */
+ public static final int CMD_PAUSE_WATER_PLAN = 18;
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/TimeDataResp.java b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/TimeDataResp.java
new file mode 100644
index 00000000000..5d8ea7f8ab7
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/TimeDataResp.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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link TimeDataResp} defines a response that defines the time, date and weekday
+ * from the gateway.
+ *
+ * @author David Goodyear - Initial contribution
+ */
+@NonNullByDefault
+public class TimeDataResp extends HandshakeResp {
+
+ public TimeDataResp() {
+ }
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/ValidationError.java b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/ValidationError.java
new file mode 100644
index 00000000000..8870a928082
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/ValidationError.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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ValidationError} represents a data payload validation error.
+ *
+ * @author David Goodyear - Initial contribution
+ */
+@NonNullByDefault
+public class ValidationError {
+ String variable = "";
+ String error = "";
+ Cause cause = Cause.BUG;
+
+ public ValidationError(String var, String err) {
+ this.variable = var;
+ this.error = err;
+ }
+
+ public ValidationError(String var, String err, Cause cause) {
+ this.variable = var;
+ this.error = err;
+ this.cause = cause;
+ }
+
+ public static enum Cause {
+ BUG,
+ USER
+ }
+
+ public Cause getCause() {
+ return this.cause;
+ }
+
+ public String toString() {
+ return this.variable + " " + this.cause;
+ }
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/WaterMeterStatus.java b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/WaterMeterStatus.java
new file mode 100644
index 00000000000..8481e71833c
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/WaterMeterStatus.java
@@ -0,0 +1,279 @@
+/**
+ * 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.linktap.protocol.frames;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link DeviceCmdReq} is a payload representing the current status of the
+ * water timer.
+ *
+ * @author David Goodyear - Initial contribution
+ */
+@NonNullByDefault
+public class WaterMeterStatus extends GatewayDeviceResponse {
+
+ public WaterMeterStatus() {
+ }
+
+ @Override
+ public ResultStatus getRes() {
+ if (super.getRes() == ResultStatus.INVALID) {
+ return ResultStatus.RET_SUCCESS;
+ }
+ return super.getRes();
+ }
+
+ public static class DeviceStatusClassTypeAdapter implements JsonDeserializer> {
+ public @Nullable List deserialize(JsonElement json, Type typeOfT,
+ JsonDeserializationContext ctx) {
+ List vals = new ArrayList<>();
+ if (json.isJsonArray()) {
+ for (JsonElement e : json.getAsJsonArray()) {
+ vals.add(ctx.deserialize(e, DeviceStatus.class));
+ }
+ } else if (json.isJsonObject()) {
+ vals.add(ctx.deserialize(json, DeviceStatus.class));
+ }
+ return vals;
+ }
+ }
+
+ /**
+ * Defines the device stat's for each device
+ */
+ @SerializedName("dev_stat")
+ @Expose
+ public List deviceStatuses = new ArrayList();
+
+ public static class DeviceStatus implements IPayloadValidator {
+ /**
+ * Defines the targetted device ID
+ */
+ @SerializedName("dev_id")
+ @Expose
+ public String deviceId = EMPTY_STRING;
+
+ /**
+ * Defines the currently active plan (Operating Mode)
+ */
+ @SerializedName("plan_mode")
+ @Expose
+ public @Nullable Integer planMode;
+
+ /**
+ * Defines the serial number of the currently active plan
+ */
+ @SerializedName("plan_sn")
+ @Expose
+ public int planSerialNo = DEFAULT_INT;
+
+ /**
+ * Defines if the water timer is connected to the Gateway
+ */
+ @SerializedName("is_rf_linked")
+ @Expose
+ public @Nullable Boolean isRfLinked;
+
+ /**
+ * Defines whether the flow meter is plugin
+ */
+ @SerializedName("is_flm_plugin")
+ @Expose
+ public @Nullable Boolean isFlmPlugin;
+
+ /**
+ * Water timer fall alert status
+ */
+ @SerializedName("is_fall")
+ @Expose
+ public @Nullable Boolean isFall;
+
+ /**
+ * Valve shut-down failure alert status
+ */
+ @SerializedName("is_broken")
+ @Expose
+ public @Nullable Boolean isBroken;
+
+ /**
+ * Water cut-off alert status
+ */
+ @SerializedName("is_cutoff")
+ @Expose
+ public @Nullable Boolean isCutoff;
+
+ /**
+ * Unusually high flow alert status
+ */
+ @SerializedName("is_leak")
+ @Expose
+ public @Nullable Boolean isLeak;
+
+ /**
+ * Unusually low flow alert status
+ */
+ @SerializedName("is_clog")
+ @Expose
+ public @Nullable Boolean isClog;
+
+ /**
+ * Water timer signal reception level
+ */
+ @SerializedName("signal")
+ @Expose
+ public @Nullable Integer signal;
+
+ /**
+ * Water timer battery level
+ */
+ @SerializedName("battery")
+ @Expose
+ public @Nullable Integer battery;
+
+ /**
+ * Defines the lock in operation
+ */
+ @SerializedName("child_lock")
+ @Expose
+ public @Nullable Integer childLock;
+
+ /**
+ * Is manual watering currently on
+ */
+ @SerializedName("is_manual_mode")
+ @Expose
+ public @Nullable Boolean isManualMode;
+
+ /**
+ * Is watering currently on
+ */
+ @SerializedName("is_watering")
+ @Expose
+ public @Nullable Boolean isWatering = false;
+
+ /**
+ * When the ECO mode is enabled, the watering duration is divided into multiple "on-off-on-off" segments.
+ * If is_final is true,it means current watering belongs to the last segment. If both is_watering and is_final
+ * are false,it means that the watering is currently suspended (i.e. in midst of the segments), and there are
+ * subsequent watering seqments to be executed.
+ */
+ @SerializedName("is_final")
+ @Expose
+ public @Nullable Boolean isFinal;
+
+ /**
+ * The duration of the current watering cycle in seconds
+ */
+ @SerializedName("total_duration")
+ @Expose
+ public @Nullable Integer totalDuration;
+
+ /**
+ * The remaining duration of the current watering cycle in seconds
+ */
+ @SerializedName("remain_duration")
+ @Expose
+ public @Nullable Integer remainDuration;
+
+ /**
+ * The failsafe duration of the current watering cycle in seconds
+ */
+ @SerializedName("failsafe_duration")
+ @Expose
+ public @Nullable Integer failsafeDuration;
+
+ /**
+ * The current water flow rate (LPN or GPM)
+ */
+ @SerializedName("speed")
+ @Expose
+ public @Nullable Double speed = 0.0d;
+
+ /**
+ * The accumulated volume of the current watering cycle (Litre or Gallon)
+ */
+ @SerializedName("volume")
+ @Expose
+ public @Nullable Double volume = 0.0d;
+
+ /**
+ * The volume limit of the current watering cycle (Litre or Gallon)
+ */
+ @SerializedName("volume_limit")
+ @Expose
+ public @Nullable Double volumeLimit = 0.0d;
+
+ public Collection getValidationErrors() {
+ final Collection errors = new ArrayList<>(0);
+
+ final Integer planModeRaw = planMode;
+ if (planModeRaw == null || planModeRaw < 1 || planModeRaw > OP_MODE_DESC.length) {
+ errors.add(new ValidationError("planMode", "is not in range 1 -> " + OP_MODE_DESC.length));
+ }
+
+ final Integer signalRaw = signal;
+ if (signalRaw == null || signalRaw < 0 || signalRaw > 100) {
+ errors.add(new ValidationError("signal", "is not in range 0 -> 100"));
+ }
+ final Integer batteryRaw = battery;
+ if (batteryRaw == null || batteryRaw < 0 || batteryRaw > 100) {
+ errors.add(new ValidationError("battery", "is not in range 0 -> 100"));
+ }
+ if (planSerialNo == DEFAULT_INT) {
+ errors.add(new ValidationError("plan_sn", "is invalid"));
+ }
+ final Integer childLockRaw = childLock;
+ if (childLockRaw == null || childLockRaw < LockReq.LOCK_UNLOCKED || childLockRaw > LockReq.LOCK_FULL) {
+ errors.add(new ValidationError("child_lock",
+ "is not in range " + LockReq.LOCK_UNLOCKED + " -> " + LockReq.LOCK_FULL));
+ }
+ if (!DEVICE_ID_PATTERN.matcher(deviceId).matches()) {
+ errors.add(new ValidationError("dev_id", "is not in the expected format"));
+ }
+
+ return errors;
+ }
+ }
+
+ public Collection getValidationErrors() {
+ return EMPTY_COLLECTION;
+ }
+
+ public static final int OP_MODE_INSTANT = 1;
+
+ public static final int OP_MODE_CALENDAR = 2;
+
+ public static final int OP_MODE_WEEK_TIMER = 3;
+
+ public static final int OP_MODE_ODD_EVEN = 4;
+
+ public static final int OP_MODE_INTERVAL = 5;
+
+ public static final int OP_MODE_MONTH = 6;
+
+ public static final String[] OP_MODE_DESC = new String[] { "Instant Mode", "Calendar Mode", "7 Day Mode",
+ "Odd-Even Mode", "Interval Mode", "Month Mode" };
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/WateringSkippedNotification.java b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/WateringSkippedNotification.java
new file mode 100644
index 00000000000..f688e9f9abb
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/WateringSkippedNotification.java
@@ -0,0 +1,67 @@
+/**
+ * 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.linktap.protocol.frames;
+
+import static org.openhab.binding.linktap.protocol.frames.ValidationError.Cause.BUG;
+
+import java.util.Collection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link WateringSkippedNotification} defines the request to dismiss alerts from a given device.
+ *
+ * @author David Goodyear - Initial contribution
+ */
+@NonNullByDefault
+public class WateringSkippedNotification extends DeviceCmdReq {
+
+ public WateringSkippedNotification() {
+ }
+
+ /**
+ * Defines the past rainfall [0] and future rainfull [1] measurements in mm
+ */
+ @SerializedName("rain")
+ @Expose
+ public double[] rainfallData = new double[] { 0.0, 0.0 };
+
+ public void setPastRainfall(final double pastRainMM) {
+ rainfallData[0] = pastRainMM;
+ }
+
+ public double getPastRainfall() {
+ return rainfallData[0];
+ }
+
+ public void setFutureRainfall(final double futureRainMM) {
+ rainfallData[1] = futureRainMM;
+ }
+
+ public double getFutureRainfall() {
+ return rainfallData[1];
+ }
+
+ public Collection getValidationErrors() {
+ final Collection errors = super.getValidationErrors();
+
+ if (rainfallData.length != 2) {
+ errors.add(new ValidationError("rain", "invalid number of entries", BUG));
+ }
+
+ return errors;
+ }
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/WirelessTestResp.java b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/WirelessTestResp.java
new file mode 100644
index 00000000000..5df7a91df96
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/WirelessTestResp.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.linktap.protocol.frames;
+
+import java.util.Collection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link WirelessTestResp} defines the wireless test result data response
+ *
+ * @author David Goodyear - Initial contribution
+ */
+@NonNullByDefault
+public class WirelessTestResp extends EndpointDeviceResponse {
+
+ public WirelessTestResp() {
+ }
+
+ /**
+ * Defines true if the last packet has been transmitted and
+ * therefore the test is complete
+ */
+ @SerializedName("final")
+ @Expose
+ public boolean testComplete = false;
+
+ /**
+ * Defines how many pings have been sent to the endpoint device
+ */
+ @SerializedName("ping")
+ @Expose
+ public int pingCount = DEFAULT_INT;
+
+ /**
+ * Defines how many pongs have been received from the endpoint device
+ */
+ @SerializedName("pong")
+ @Expose
+ public int pongCount = DEFAULT_INT;
+
+ public Collection getValidationErrors() {
+ final Collection errors = super.getValidationErrors();
+
+ if (pingCount == DEFAULT_INT) {
+ errors.add(new ValidationError("ping", "count is missing"));
+ }
+
+ if (pongCount == DEFAULT_INT) {
+ errors.add(new ValidationError("pong", "count is missing"));
+ }
+
+ if (!DEVICE_ID_PATTERN.matcher(deviceId).matches()) {
+ errors.add(new ValidationError("dev_id", "is not in the expected format"));
+ }
+
+ return errors;
+ }
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/CommandNotSupportedException.java b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/CommandNotSupportedException.java
new file mode 100644
index 00000000000..e29b078fc09
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/CommandNotSupportedException.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.linktap.protocol.http;
+
+import java.io.Serial;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.linktap.protocol.frames.GatewayDeviceResponse;
+
+/**
+ * The {@link CommandNotSupportedException} should be thrown when the endpoint being communicated with
+ * does not appear to be a Tap Link Gateway device.
+ *
+ * @author David Goodyear - Initial contribution
+ */
+@NonNullByDefault
+public class CommandNotSupportedException extends I18Exception {
+ @Serial
+ private static final long serialVersionUID = -7784829325604153947L;
+
+ public CommandNotSupportedException() {
+ super();
+ }
+
+ public CommandNotSupportedException(final String message) {
+ super(message);
+ }
+
+ public CommandNotSupportedException(final Throwable cause) {
+ super(cause);
+ }
+
+ public CommandNotSupportedException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ public CommandNotSupportedException(final GatewayDeviceResponse.ResultStatus rs) {
+ super(rs.getDesc());
+ this.i18Key = rs.getI18Key();
+ }
+
+ public String getI18Key() {
+ return getI18Key("exception-cmd-not-supported-exception");
+ }
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/DeviceIdException.java b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/DeviceIdException.java
new file mode 100644
index 00000000000..4d702f08e47
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/DeviceIdException.java
@@ -0,0 +1,58 @@
+/**
+ * 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.linktap.protocol.http;
+
+import java.io.Serial;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.linktap.protocol.frames.GatewayDeviceResponse;
+
+/**
+ * The {@link DeviceIdException} should be thrown when the endpoint being communicated with
+ * does not appear to be a Tap Link Gateway device.
+ *
+ * @author David Goodyear - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceIdException extends I18Exception {
+ @Serial
+ private static final long serialVersionUID = -7786449325604153947L;
+
+ // case RET_DEVICE_ID_ERROR:
+ // case RET_DEVICE_NOT_FOUND:
+
+ public DeviceIdException() {
+ super();
+ }
+
+ public DeviceIdException(final String message) {
+ super(message);
+ }
+
+ public DeviceIdException(final Throwable cause) {
+ super(cause);
+ }
+
+ public DeviceIdException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ public DeviceIdException(final GatewayDeviceResponse.ResultStatus rs) {
+ super(rs.getDesc());
+ this.i18Key = rs.getI18Key();
+ }
+
+ public String getI18Key() {
+ return getI18Key("exception.device-id-exception");
+ }
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/GatewayIdException.java b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/GatewayIdException.java
new file mode 100644
index 00000000000..eb3260c0eb7
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/GatewayIdException.java
@@ -0,0 +1,58 @@
+/**
+ * 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.linktap.protocol.http;
+
+import java.io.Serial;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.linktap.protocol.frames.GatewayDeviceResponse;
+
+/**
+ * The {@link GatewayIdException} should be thrown when the endpoint being communicated with
+ * does not appear to be a Tap Link Gateway device.
+ *
+ * @author David Goodyear - Initial contribution
+ */
+@NonNullByDefault
+public class GatewayIdException extends I18Exception {
+ @Serial
+ private static final long serialVersionUID = -7786449325604153947L;
+
+ // case RET_DEVICE_ID_ERROR:
+ // case RET_DEVICE_NOT_FOUND:
+
+ public GatewayIdException() {
+ super();
+ }
+
+ public GatewayIdException(final String message) {
+ super(message);
+ }
+
+ public GatewayIdException(final Throwable cause) {
+ super(cause);
+ }
+
+ public GatewayIdException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ public GatewayIdException(final GatewayDeviceResponse.ResultStatus rs) {
+ super(rs.getDesc());
+ this.i18Key = rs.getI18Key();
+ }
+
+ public String getI18Key() {
+ return getI18Key("exception.gw-id-exception");
+ }
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/I18Exception.java b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/I18Exception.java
new file mode 100644
index 00000000000..a11dd59fe89
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/I18Exception.java
@@ -0,0 +1,58 @@
+/**
+ * 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.linktap.protocol.http;
+
+import java.io.Serial;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link I18Exception} is a abstract class for exceptions that support
+ * i18key functionality.
+ *
+ * @author David Goodyear - Initial contribution
+ */
+@NonNullByDefault
+public abstract class I18Exception extends Exception {
+
+ @Serial
+ private static final long serialVersionUID = -7784829349743963947L;
+
+ protected String i18Key = "";
+
+ public I18Exception() {
+ super();
+ }
+
+ public I18Exception(final String message) {
+ super(message);
+ }
+
+ public I18Exception(final Throwable cause) {
+ super(cause);
+ }
+
+ public I18Exception(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ public abstract String getI18Key();
+
+ public String getI18Key(final String defaultI18) {
+ if (!i18Key.isBlank()) {
+ return i18Key;
+ }
+ return Objects.requireNonNullElse(getMessage(), defaultI18);
+ }
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/InvalidParameterException.java b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/InvalidParameterException.java
new file mode 100644
index 00000000000..c93f46b98a4
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/InvalidParameterException.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.linktap.protocol.http;
+
+import java.io.Serial;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.linktap.protocol.frames.GatewayDeviceResponse;
+
+/**
+ * The {@link InvalidParameterException} should be thrown when the endpoint being communicated with
+ * does not appear to be a Tap Link Gateway device.
+ *
+ * @author David Goodyear - Initial contribution
+ */
+@NonNullByDefault
+public class InvalidParameterException extends I18Exception {
+ @Serial
+ private static final long serialVersionUID = -7784829499604153947L;
+
+ public InvalidParameterException() {
+ super();
+ }
+
+ public InvalidParameterException(final String message) {
+ super(message);
+ }
+
+ public InvalidParameterException(final Throwable cause) {
+ super(cause);
+ }
+
+ public InvalidParameterException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ public InvalidParameterException(final GatewayDeviceResponse.ResultStatus rs) {
+ super(rs.getDesc());
+ this.i18Key = rs.getI18Key();
+ }
+
+ public String getI18Key() {
+ return getI18Key("exception.invalid-parameter-exception");
+ }
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/LinkTapException.java b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/LinkTapException.java
new file mode 100644
index 00000000000..a25981b1b30
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/LinkTapException.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.linktap.protocol.http;
+
+import java.io.Serial;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link LinkTapException} is a class for general exceptions that support
+ * i18key functionality.
+ *
+ * @author David Goodyear - Initial contribution
+ */
+@NonNullByDefault
+public class LinkTapException extends I18Exception {
+
+ @Serial
+ private static final long serialVersionUID = -7739358310944502365L;
+
+ protected String i18Key = "";
+
+ public LinkTapException() {
+ super();
+ }
+
+ public LinkTapException(final String message) {
+ super(message);
+ }
+
+ public LinkTapException(final Throwable cause) {
+ super(cause);
+ }
+
+ public LinkTapException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ @Override
+ public String getI18Key() {
+ return getI18Key("exception.unexpected-exception");
+ }
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/NotTapLinkGatewayException.java b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/NotTapLinkGatewayException.java
new file mode 100644
index 00000000000..a17c8e89003
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/NotTapLinkGatewayException.java
@@ -0,0 +1,107 @@
+/**
+ * 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.linktap.protocol.http;
+
+import java.io.Serial;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link NotTapLinkGatewayException} should be thrown when the endpoint being communicated with
+ * does not appear to be a Tap Link Gateway device.
+ *
+ * @author David Goodyear - Initial contribution
+ */
+@NonNullByDefault
+public class NotTapLinkGatewayException extends I18Exception {
+ @Serial
+ private static final long serialVersionUID = -7786449325604153487L;
+
+ public NotTapLinkGatewayException() {
+ super();
+ }
+
+ public NotTapLinkGatewayException(final String message) {
+ super(message);
+ }
+
+ public NotTapLinkGatewayException(final Throwable cause) {
+ super(cause);
+ }
+
+ public NotTapLinkGatewayException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ public NotTapLinkGatewayException(final String message, final String i18key) {
+ super(message);
+ this.i18Key = i18key;
+ }
+
+ public NotTapLinkGatewayException(final NotTapLinkGatewapExecptionDefinitions definition) {
+ this(definition.description, definition.i18Key);
+ }
+
+ public enum NotTapLinkGatewapExecptionDefinitions {
+
+ /**
+ * HEADERS_MISSING
+ */
+ HEADERS_MISSING("Missing header markers", "exception.not-gw.missing-headers"),
+
+ /**
+ * MISSING_API_TITLE
+ */
+ MISSING_API_TITLE("Not a LinkTap API response", "exception.not-gw.missing-api-title"),
+
+ /**
+ * MISSING_SERVER_TITLE
+ */
+ MISSING_SERVER_TITLE("Not a LinkTap response", "exception.not-gw.missing-server-title"),
+
+ /**
+ * UNEXPECTED_STATUS_CODE
+ */
+ UNEXPECTED_STATUS_CODE("Unexpected status code response", "exception.not-gw.unexpected-status-code"),
+
+ /**
+ * UNEXPECTED_HTTPS
+ */
+ UNEXPECTED_HTTPS("Unexpected protocol", "exception.not-gw.unexpected-protocol");
+
+ private final String description;
+ private final String i18Key;
+
+ private NotTapLinkGatewapExecptionDefinitions(final String description, final String i18key) {
+ this.description = description;
+ this.i18Key = i18key;
+ }
+
+ public String getI18Key() {
+ return i18Key;
+ }
+
+ public String getDesc() {
+ return description;
+ }
+
+ @Override
+ public String toString() {
+ return description;
+ }
+ }
+
+ public String getI18Key() {
+ return getI18Key("exception.not-tap-link-gw");
+ }
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/TransientCommunicationIssueException.java b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/TransientCommunicationIssueException.java
new file mode 100644
index 00000000000..e634ad776e7
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/TransientCommunicationIssueException.java
@@ -0,0 +1,93 @@
+/**
+ * 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.linktap.protocol.http;
+
+import java.io.Serial;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link TransientCommunicationIssueException} should be thrown when the endpoint being communicated with
+ * does not appear to be a Tap Link Gateway device.
+ *
+ * @author David Goodyear - Initial contribution
+ */
+@NonNullByDefault
+public class TransientCommunicationIssueException extends I18Exception {
+ @Serial
+ private static final long serialVersionUID = -7786449325604143287L;
+
+ public TransientCommunicationIssueException() {
+ super();
+ }
+
+ public TransientCommunicationIssueException(final String message, final String i18key) {
+ super(message);
+ this.i18Key = i18key;
+ }
+
+ public TransientCommunicationIssueException(final TransientExecptionDefinitions definition) {
+ this(definition.description, definition.i18Key);
+ }
+
+ public TransientCommunicationIssueException(final Throwable cause) {
+ super(cause);
+ }
+
+ public TransientCommunicationIssueException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ public enum TransientExecptionDefinitions {
+
+ /**
+ * HOST_UNREACHABLE
+ */
+ HOST_UNREACHABLE("Could not connect", "exception.could-not-connect"),
+
+ /**
+ * HOST_NOT_RESOLVED
+ */
+ HOST_NOT_RESOLVED("Could not resolve IP address", "exception.could-not-resolve"),
+
+ /**
+ * COMMUNICATIONS_LOST
+ */
+ COMMUNICATIONS_LOST("Communications Lost", "exception.communications-lost");
+
+ private final String description;
+ private final String i18Key;
+
+ private TransientExecptionDefinitions(final String description, final String i18key) {
+ this.description = description;
+ this.i18Key = i18key;
+ }
+
+ public String getI18Key() {
+ return i18Key;
+ }
+
+ public String getDesc() {
+ return description;
+ }
+
+ @Override
+ public String toString() {
+ return description;
+ }
+ }
+
+ public String getI18Key() {
+ return getI18Key("exception.gw-id-exception");
+ }
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/WebServerApi.java b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/WebServerApi.java
new file mode 100644
index 00000000000..35b38620c81
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/WebServerApi.java
@@ -0,0 +1,572 @@
+/**
+ * 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.linktap.protocol.http;
+
+import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.*;
+import static org.openhab.binding.linktap.protocol.http.NotTapLinkGatewayException.NotTapLinkGatewapExecptionDefinitions.*;
+import static org.openhab.binding.linktap.protocol.http.TransientCommunicationIssueException.TransientExecptionDefinitions.*;
+
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.SocketTimeoutException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.net.ssl.SSLHandshakeException;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.core.MediaType;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.FormContentProvider;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import org.openhab.binding.linktap.internal.Firmware;
+import org.openhab.binding.linktap.internal.Utils;
+import org.openhab.core.i18n.LocaleProvider;
+import org.openhab.core.i18n.TranslationProvider;
+import org.osgi.framework.Bundle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link WebServerApi} defines interactions with the web server interface.
+ *
+ * @author David Goodyear - Initial contribution
+ */
+@NonNullByDefault
+public final class WebServerApi {
+
+ public static final String URI_SCHEME = "http";
+ public static final String URI_HOST_PREFIX = URI_SCHEME + "://";
+
+ /**
+ * Headers
+ */
+ public static final String HEADER_SERVER = "Server";
+ public static final String HEADER_GW_SERVER_NAME = "LinkTap Gateway";
+
+ /**
+ * HTML title field mappings to use cases
+ */
+ private static final String TITLE_API_RESPONSE = "api";
+ private static final String TITLE_API_CONFIG_PAGE = "LinkTap Gateway";
+ private static final String TITLE_API_LOGIN_PAGE = "LinkTap Gateway Login";
+
+ /**
+ * Field names for form submission API's
+ */
+ private static final String FIELD_ADMIN_USER = "admin";
+ private static final String FIELD_ADMIN_USER_PWD = "adminpwd";
+
+ private static final int REQ_TIMEOUT_SECONDS = 3;
+ private static final WebServerApi INSTANCE = new WebServerApi();
+ private static final String REQ_HDR_APPLICATION_JSON = new MediaType("application", "json", "UTF-8").toString();
+ private final Logger logger = LoggerFactory.getLogger(WebServerApi.class);
+
+ private @NonNullByDefault({}) HttpClient httpClient;
+ private @Nullable TranslationProvider translationProvider;
+ private @Nullable LocaleProvider localeProvider;
+ private @Nullable Bundle bundle;
+
+ private WebServerApi() {
+ }
+
+ public static WebServerApi getInstance() {
+ return INSTANCE;
+ }
+
+ public void setTranslationProviderInfo(TranslationProvider translationProvider, LocaleProvider localeProvider,
+ Bundle bundle) {
+ this.bundle = bundle;
+ this.localeProvider = localeProvider;
+ this.translationProvider = translationProvider;
+ }
+
+ public String getLocalizedText(String key, @Nullable Object @Nullable... arguments) {
+ TranslationProvider translationProv = translationProvider;
+ LocaleProvider localeProv = localeProvider;
+ if (translationProv == null || localeProv == null) {
+ return key;
+ }
+ String result = translationProv.getText(bundle, key, key, localeProv.getLocale(), arguments);
+ return Objects.nonNull(result) ? result : key;
+ }
+
+ /**
+ * Sets the httpClient object to be used for API calls to LinkTap.
+ *
+ * @param httpClient the client to be used.
+ */
+ public void setHttpClient(@Nullable HttpClient httpClient) {
+ if (httpClient != null) {
+ this.httpClient = httpClient;
+ }
+ }
+
+ public Map getBridgeProperities(final String hostname)
+ throws LinkTapException, NotTapLinkGatewayException, TransientCommunicationIssueException {
+ try {
+ final Request request = httpClient.newRequest(URI_HOST_PREFIX + hostname).method(HttpMethod.GET);
+ final ContentResponse cr = request.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
+ if (HttpURLConnection.HTTP_OK != cr.getStatus()) {
+ throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
+ }
+ validateHeaders(cr.getHeaders());
+ final String responseData = cr.getContentAsString();
+ final Document doc = Jsoup.parse(responseData);
+
+ switch (doc.title()) {
+ case TITLE_API_CONFIG_PAGE:
+ break;
+ case TITLE_API_LOGIN_PAGE:
+ return Map.of();
+ default:
+ throw new NotTapLinkGatewayException(MISSING_SERVER_TITLE);
+ }
+ final Map deviceProps = getMetadataProperties(doc);
+ Firmware firmware = new Firmware(deviceProps.get(BRIDGE_PROP_GW_VER));
+ if (firmware.supportsMDNS()) {
+ getMdnsEnableArgs(doc);
+ } else {
+ logger.debug("Firmware revision does not include mDNS support");
+ }
+ return deviceProps;
+
+ } catch (InterruptedException e) {
+ return Map.of();
+ } catch (TimeoutException e) {
+ throw new TransientCommunicationIssueException(COMMUNICATIONS_LOST);
+ } catch (ExecutionException e) {
+ final Throwable t = e.getCause();
+ if (t instanceof UnknownHostException || t instanceof SocketTimeoutException) {
+ throw new TransientCommunicationIssueException(HOST_UNREACHABLE);
+ } else if (t instanceof SSLHandshakeException) {
+ throw new NotTapLinkGatewayException(UNEXPECTED_HTTPS);
+ } else {
+ logger.warn("{}", getLocalizedText("ExecutionException -> {}", Utils.getMessage(e)));
+ }
+ throw new LinkTapException(getLocalizedText("exception.unexpected-failure", Utils.getMessage(e)));
+ }
+ }
+
+ /**
+ * Extract the common properties for all devices, from the given meta-data of a device.
+ *
+ * @param doc the html document returns from the potential Gateway device
+ * @return Map of common props
+ */
+ private Map getMetadataProperties(final Document doc) {
+ final Map newProps = new HashMap<>(7);
+
+ /*
+ * Extract elements based on td location using the text markers
+ */
+ String firmwareVer = "?";
+ String hwModel = "?";
+ String id = "?";
+ String macAddr = "?";
+
+ final org.jsoup.select.Elements tdEntries = doc.getElementsByTag("td");
+ for (int i = 0; i < tdEntries.size(); ++i) {
+ if (tdEntries.get(i).hasText()) {
+ switch (tdEntries.get(i).text()) {
+ case "Firmware version":
+ firmwareVer = tdEntries.get(i + 1).text();
+ i++;
+ break;
+ case "Model":
+ hwModel = tdEntries.get(i + 1).text();
+ i++;
+ break;
+ case "ID":
+ id = tdEntries.get(i + 1).text();
+ i++;
+ break;
+ case "MAC address":
+ macAddr = tdEntries.get(i + 1).text();
+ i++;
+ break;
+ }
+ }
+ }
+
+ newProps.put(BRIDGE_PROP_GW_ID, id.split("[-]")[0]);
+ newProps.put(BRIDGE_PROP_GW_VER, firmwareVer.split("[_]")[0]);
+ newProps.put(BRIDGE_PROP_MAC_ADDR, macAddr);
+ newProps.put(BRIDGE_PROP_HW_MODEL, hwModel);
+
+ /*
+ * Extract elements based on name markers and attributes
+ */
+ final boolean httpApiEnabled = doc.getElementsByAttributeValue("name", "htapi").hasAttr("checked");
+ final String httpApiEndpoint = doc.getElementsByAttributeValue("name", "URL").attr("value");
+
+ newProps.put(BRIDGE_PROP_HTTP_API_ENABLED, String.valueOf(httpApiEnabled));
+ newProps.put(BRIDGE_PROP_HTTP_API_EP, httpApiEndpoint);
+
+ Optional vunitSelections = doc.getElementsByAttributeValue("name", "vunit").stream()
+ .filter(x -> x.hasAttr("checked")).findFirst();
+ if (vunitSelections.isPresent()) {
+ switch (vunitSelections.get().attr("value")) {
+ case "0":
+ newProps.put(BRIDGE_PROP_VOL_UNIT, "L");
+ break;
+ case "1":
+ newProps.put(BRIDGE_PROP_VOL_UNIT, "gal");
+ break;
+ }
+ }
+
+ return newProps;
+ }
+
+ public Optional getSection(final Document doc, final String title) {
+ final Elements thead = doc.getElementsByTag("thead");
+ Optional element = thead.stream()
+ .filter(x -> x.hasText() && x.text().toLowerCase().contains(title.toLowerCase())).findFirst();
+ if (element.isPresent()) {
+ return Optional.of(element.get().parent());
+ }
+ return Optional.empty();
+ }
+
+ public String getUriInputArg(final Element el) {
+ final StringBuilder sb = new StringBuilder();
+ switch (el.attr("type")) {
+ case "checkbox":
+ sb.append(el.attr("name"));
+ sb.append("=");
+ if (el.hasAttr("checked")) {
+ sb.append(el.attr("value"));
+ } else {
+ sb.append("0");
+ }
+ break;
+ case "radio":
+ if (el.hasAttr("checked")) {
+ sb.append(el.attr("name"));
+ sb.append("=");
+ sb.append(el.attr("value"));
+ }
+ break;
+ case "text":
+ sb.append(el.attr("name"));
+ sb.append("=");
+ sb.append(URLDecoder.decode(el.attr("value"), StandardCharsets.UTF_8));
+ }
+ return sb.toString();
+ }
+
+ public String getMdnsEnableArgs(final Document doc) {
+ final Optional miscSection = getSection(doc, "Misc settings");
+ StringBuilder sb = new StringBuilder();
+
+ if (!miscSection.isPresent()) {
+ return sb.toString();
+ }
+ final Elements inputs = miscSection.get().getElementsByTag("input");
+ for (int i = 0; i < inputs.size(); ++i) {
+ final String val = getUriInputArg(inputs.get(i));
+ if (!val.isEmpty() && !sb.isEmpty()) {
+ sb.append("&");
+ }
+ sb.append(val);
+ }
+ // Change the mdns flag to true
+ {
+ final int mdnsIdx = sb.indexOf("mdns=0");
+ if (mdnsIdx != -1) {
+ sb.replace(mdnsIdx, mdnsIdx + 6, "mdns=1");
+ return sb.toString();
+ }
+ }
+
+ return "";
+ }
+
+ public String getLocalHttpApiArgs(final Document doc, final Optional targetServerOpt,
+ final Optional wrapHtmlDisable) {
+ final Optional localHttpApiSection = getSection(doc, "Local HTTP API settings");
+ StringBuilder sb = new StringBuilder();
+
+ if (!localHttpApiSection.isPresent()) {
+ return sb.toString();
+ }
+ final Elements inputs = localHttpApiSection.get().getElementsByTag("input");
+ for (int i = 0; i < inputs.size(); ++i) {
+ final String val = getUriInputArg(inputs.get(i));
+ if (!val.isEmpty() && !sb.isEmpty()) {
+ sb.append("&");
+ }
+ sb.append(val);
+ }
+
+ boolean updatedUri = false;
+ int enableApiIdx = -1;
+
+ if (targetServerOpt.isPresent()) {
+ String targetServer = targetServerOpt.get();
+ // Change the enable Local HTTP API flag to true
+ enableApiIdx = sb.indexOf("htapi=0");
+ if (enableApiIdx != -1) {
+ sb.replace(enableApiIdx, enableApiIdx + 7, "htapi=1");
+ }
+
+ final int urlApiMarker = sb.indexOf("URL=");
+ if (urlApiMarker != -1) {
+ final int nextArg = sb.indexOf("&", urlApiMarker);
+ String urlArg = (nextArg == -1) ? sb.substring(urlApiMarker + 4)
+ : sb.substring(urlApiMarker + 4, nextArg);
+ logger.trace("Found existing HTTP URL Server : {}", urlArg);
+ if (!urlArg.equals(targetServer)) {
+ updatedUri = true;
+ sb.replace(urlApiMarker, urlApiMarker + urlArg.length() + 4,
+ "URL=" + URLEncoder.encode(targetServer, StandardCharsets.UTF_8));
+ }
+ }
+ }
+
+ int wgrhIdx = -1;
+ if (wrapHtmlDisable.isPresent() && wrapHtmlDisable.get()) {
+ // Change the wgrhIdx flag to true
+ {
+ wgrhIdx = sb.indexOf("wgrh=1");
+ if (wgrhIdx != -1) {
+ sb.replace(wgrhIdx, wgrhIdx + 6, "wgrh=0");
+ return sb.toString();
+ }
+ }
+ }
+
+ if (wgrhIdx != -1 || enableApiIdx != -1 || updatedUri) {
+ return sb.toString();
+ }
+
+ return "";
+ }
+
+ public boolean configureBridge(final @Nullable String hostname, final Optional mdnsEnable,
+ final Optional nonHtmlEnable, final Optional localServer)
+ throws InterruptedException, NotTapLinkGatewayException, TransientCommunicationIssueException {
+ try {
+ if (hostname == null) {
+ throw new TransientCommunicationIssueException(HOST_NOT_RESOLVED);
+ }
+ final String targetHost = URI_HOST_PREFIX + hostname;
+ final Request request = httpClient.newRequest(targetHost).method(HttpMethod.GET);
+ final ContentResponse cr = request.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
+ if (HttpURLConnection.HTTP_OK != cr.getStatus()) {
+ throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
+ }
+ validateHeaders(cr.getHeaders());
+ final String responseData = cr.getContentAsString();
+ final Document doc = Jsoup.parse(responseData);
+
+ switch (doc.title()) {
+ case TITLE_API_CONFIG_PAGE:
+ break;
+ case TITLE_API_LOGIN_PAGE:
+ return false;
+ default:
+ throw new NotTapLinkGatewayException(MISSING_SERVER_TITLE);
+ }
+ // Send the GET request to configure mdns if it's not enabled
+ boolean rebootReq = false;
+ if (mdnsEnable.isPresent() && mdnsEnable.get()) {
+ logger.trace("Enabling mdns server on gateway");
+ String mdnsEnableReqStr = getMdnsEnableArgs(doc);
+ if (!mdnsEnableReqStr.isEmpty()) {
+ logger.debug("Updating mdns server settings on gateway");
+ final Request mdnsRequest = httpClient
+ .newRequest(targetHost + "/index.shtml?flag=4&" + mdnsEnableReqStr).method(HttpMethod.GET);
+ final ContentResponse mdnsCr = mdnsRequest.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
+ if (HttpURLConnection.HTTP_OK != mdnsCr.getStatus()) {
+ throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
+ }
+ rebootReq = true;
+ }
+ }
+
+ if (localServer.isPresent() && !localServer.get().isBlank()
+ || nonHtmlEnable.isPresent() && nonHtmlEnable.get()) {
+ if (localServer.isPresent() && !localServer.get().isBlank()) {
+ logger.trace("Setting Local HTTP Api on gateway");
+ }
+ if (nonHtmlEnable.isPresent() && nonHtmlEnable.get()) {
+ logger.trace("Enabling efficient non HTML communications on gateway");
+ }
+
+ String localHttpApiReqStr = this.getLocalHttpApiArgs(doc, localServer, nonHtmlEnable);
+ if (!localHttpApiReqStr.isEmpty()) {
+ logger.debug("Updating Local HTTP API server settings on gateway");
+ final Request lhttpApiRequest = httpClient
+ .newRequest(targetHost + "/index.shtml?flag=5&" + localHttpApiReqStr)
+ .method(HttpMethod.GET);
+ final ContentResponse mdnsCr = lhttpApiRequest.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .send();
+ if (HttpURLConnection.HTTP_OK != mdnsCr.getStatus()) {
+ throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
+ }
+ rebootReq = true;
+ }
+ }
+
+ if (rebootReq) {
+ logger.debug("Rebooting gateway to apply new settings");
+ final Request restartReq = httpClient.newRequest(targetHost + "/index.shtml?flag=0")
+ .method(HttpMethod.GET);
+ final ContentResponse mdnsCr = restartReq.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
+ if (HttpURLConnection.HTTP_OK != mdnsCr.getStatus()) {
+ throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
+ }
+ }
+
+ return rebootReq;
+
+ } catch (TimeoutException e) {
+ throw new TransientCommunicationIssueException(HOST_UNREACHABLE);
+ } catch (ExecutionException e) {
+ final Throwable t = e.getCause();
+ if (t instanceof UnknownHostException) {
+ throw new TransientCommunicationIssueException(HOST_NOT_RESOLVED);
+ } else if (t instanceof SocketTimeoutException) {
+ throw new TransientCommunicationIssueException(HOST_UNREACHABLE);
+ } else if (t instanceof SSLHandshakeException) {
+ throw new NotTapLinkGatewayException(UNEXPECTED_HTTPS);
+ } else {
+ logger.warn("{}", getLocalizedText("ExecutionException -> {}", Utils.getMessage(e)));
+ }
+ throw new NotTapLinkGatewayException(getLocalizedText("exception.unexpected-failure", Utils.getMessage(t)));
+ }
+ }
+
+ public boolean unlockWebInterface(final String hostname, final String username, final String password)
+ throws LinkTapException, NotTapLinkGatewayException, TransientCommunicationIssueException {
+ try {
+ org.eclipse.jetty.util.Fields fields = new org.eclipse.jetty.util.Fields();
+ fields.put(FIELD_ADMIN_USER, username);
+ fields.put(FIELD_ADMIN_USER_PWD, password);
+ final Request request = httpClient.newRequest(URI_HOST_PREFIX + hostname + "/login.shtml")
+ .method(HttpMethod.POST).content(new FormContentProvider(fields));
+ final ContentResponse cr = request.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
+ if (HttpURLConnection.HTTP_OK != cr.getStatus()) {
+ throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
+ }
+ validateHeaders(cr.getHeaders());
+ return !getBridgeProperities(hostname).isEmpty();
+ } catch (InterruptedException e) {
+ return false;
+ } catch (TimeoutException e) {
+ throw new TransientCommunicationIssueException(HOST_UNREACHABLE);
+ } catch (ExecutionException e) {
+ final Throwable t = e.getCause();
+ if (t instanceof UnknownHostException) {
+ throw new TransientCommunicationIssueException(HOST_NOT_RESOLVED);
+ } else if (t instanceof SocketTimeoutException) {
+ throw new TransientCommunicationIssueException(HOST_UNREACHABLE);
+ } else if (t instanceof SSLHandshakeException) {
+ throw new NotTapLinkGatewayException(UNEXPECTED_HTTPS);
+ } else {
+ logger.warn("{}", getLocalizedText("ExecutionException -> {}", Utils.getMessage(e)));
+ }
+ throw new NotTapLinkGatewayException(getLocalizedText("exception.unexpected-failure", Utils.getMessage(e)));
+ }
+ }
+
+ /**
+ * Returns whether a response from the HTTP endpoint reached, appears to have the correct
+ * header markers for a Link Tap Gateway device.
+ *
+ * @param headers the http headers from the response to be checked
+ * @throws NotTapLinkGatewayException if the response does not appear to be from a Link Tap Gateway
+ */
+ private void validateHeaders(final HttpFields headers) throws NotTapLinkGatewayException {
+ if (!headers.contains(HEADER_SERVER, HEADER_GW_SERVER_NAME)) {
+ throw new NotTapLinkGatewayException(HEADERS_MISSING);
+ }
+ }
+
+ public String sendRequest(final String hostname, final String requestBody)
+ throws NotTapLinkGatewayException, TransientCommunicationIssueException {
+ try {
+ final InetAddress address = InetAddress.getByName(hostname);
+ logger.trace("API Endpoint: {}", URI_HOST_PREFIX + address.getHostAddress() + "/api.shtml");
+ final Request request = httpClient.POST(URI_HOST_PREFIX + address.getHostAddress() + "/api.shtml");
+ request.content(new StringContentProvider(requestBody), REQ_HDR_APPLICATION_JSON);
+
+ final ContentResponse cr = request.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
+ if (HttpURLConnection.HTTP_OK != cr.getStatus()) {
+ throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
+ }
+
+ final HttpFields headers = cr.getHeaders();
+ validateHeaders(headers);
+
+ String responseData = cr.getContentAsString();
+ final String contentType = headers.get(HttpHeader.CONTENT_TYPE);
+
+ // If content type is test/html its wrapped in HTML (Old standard)
+ // If content type is application/json it's a raw compact response (More efficient new standard)
+ switch (contentType) {
+ case "text/html":
+ final Document doc = Jsoup.parse(responseData);
+ final String docTitle = doc.title();
+ if (!docTitle.equals(TITLE_API_RESPONSE)) {
+ throw new NotTapLinkGatewayException(MISSING_API_TITLE);
+ }
+ responseData = doc.body().text();
+ break;
+ case "application/json":
+ // Do nothing - the raw content is the response
+ break;
+ default:
+ responseData = "";
+ }
+
+ return responseData;
+ } catch (InterruptedException e) {
+ return "";
+ } catch (TimeoutException e) {
+ throw new TransientCommunicationIssueException(HOST_UNREACHABLE);
+ } catch (UnknownHostException e) {
+ throw new TransientCommunicationIssueException(HOST_NOT_RESOLVED);
+ } catch (ExecutionException e) {
+ final Throwable t = e.getCause();
+ if (t instanceof UnknownHostException) {
+ throw new TransientCommunicationIssueException(HOST_NOT_RESOLVED);
+ } else if (t instanceof SSLHandshakeException) {
+ throw new NotTapLinkGatewayException(UNEXPECTED_HTTPS);
+ } else {
+ throw new TransientCommunicationIssueException(HOST_UNREACHABLE);
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/servers/BindingServlet.java b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/servers/BindingServlet.java
new file mode 100644
index 00000000000..b768b04d297
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/servers/BindingServlet.java
@@ -0,0 +1,163 @@
+/**
+ * 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.linktap.protocol.servers;
+
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.EMPTY_STRING;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.core.MediaType;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+import org.openhab.binding.linktap.internal.TransactionProcessor;
+import org.openhab.binding.linktap.internal.Utils;
+import org.openhab.binding.linktap.protocol.frames.TLGatewayFrame;
+import org.openhab.core.i18n.LocaleProvider;
+import org.openhab.core.i18n.TranslationProvider;
+import org.osgi.framework.Bundle;
+import org.osgi.service.http.HttpService;
+import org.osgi.service.http.NamespaceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link BindingServlet} defines the request to enable or disable alerts from a given device.
+ *
+ * @author David Goodyear - Initial contribution
+ */
+@NonNullByDefault
+public class BindingServlet extends HttpServlet {
+
+ public static final BindingServlet INSTANCE = new BindingServlet();
+ public static final String SERVLET_URL_WITHOUT_ROOT = "linktap";
+ private static final String SERVLET_URL = "/" + SERVLET_URL_WITHOUT_ROOT;
+ private static final long serialVersionUID = -23L;
+
+ private final Logger logger = LoggerFactory.getLogger(BindingServlet.class);
+ private final Object registerLock = new Object();
+
+ private volatile boolean registered;
+
+ @Nullable
+ HttpService httpService;
+ private @Nullable TranslationProvider translationProvider;
+ private @Nullable LocaleProvider localeProvider;
+ private @Nullable Bundle bundle;
+
+ public void setTranslationProviderInfo(TranslationProvider translationProvider, LocaleProvider localeProvider,
+ Bundle bundle) {
+ this.bundle = bundle;
+ this.localeProvider = localeProvider;
+ this.translationProvider = translationProvider;
+ }
+
+ public static final BindingServlet getInstance() {
+ return INSTANCE;
+ }
+
+ public String getLocalizedText(String key, @Nullable Object @Nullable... arguments) {
+ TranslationProvider translationProv = translationProvider;
+ LocaleProvider localeProv = localeProvider;
+ if (translationProv == null || localeProv == null) {
+ return key;
+ }
+ String result = translationProv.getText(bundle, key, key, localeProv.getLocale(), arguments);
+ return Objects.nonNull(result) ? result : key;
+ }
+
+ public void setHttpService(final HttpService httpService) {
+ this.httpService = httpService;
+ }
+
+ public static String getServletAddress(final String hostname, final String localizedWarning) {
+ final String httpPortStr = System.getProperty("org.osgi.service.http.port");
+ final Logger logger = LoggerFactory.getLogger(BindingServlet.class);
+ if (httpPortStr == null || httpPortStr.isEmpty()) {
+ logger.warn("{}", localizedWarning);
+ return EMPTY_STRING;
+ }
+ return "http://" + hostname + ":" + httpPortStr + SERVLET_URL;
+ }
+
+ public void registerServlet() {
+ final HttpService srv = httpService;
+ synchronized (registerLock) {
+ if (!registered && srv != null) {
+ try {
+ srv.registerServlet(SERVLET_URL, this, null, srv.createDefaultHttpContext());
+ registered = true;
+ logger.trace("Registered servlet " + SERVLET_URL);
+ } catch (NamespaceException | ServletException e) {
+ logger.warn("{}",
+ getLocalizedText("exception.fail-servlet-registration", SERVLET_URL, Utils.getMessage(e)));
+ }
+ }
+ }
+ }
+
+ public void unregisterServlet() {
+ final HttpService srv = httpService;
+ synchronized (registerLock) {
+ if (registered && srv != null) {
+ srv.unregister(SERVLET_URL);
+ registered = false;
+ logger.trace("Unregistered servlet");
+ }
+ }
+ }
+
+ @Override
+ protected void doPost(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
+ throws ServletException, IOException {
+ if (req == null) {
+ return;
+ }
+
+ int bufferSize = 1000; // The payload string is technically limited to 768 characters - this should be enough
+ // a single read to fully fit
+ char[] buffer = new char[bufferSize];
+ StringBuilder out = new StringBuilder();
+ try (Reader in = new InputStreamReader(req.getInputStream(), StandardCharsets.UTF_8)) {
+ for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0;) {
+ out.append(buffer, 0, numRead);
+ }
+ }
+
+ String payload = out.toString();
+ final TLGatewayFrame tlFrame = LinkTapBindingConstants.GSON.fromJson(payload, TLGatewayFrame.class);
+ String result = "";
+ if (tlFrame != null) {
+ TransactionProcessor tp = TransactionProcessor.getInstance();
+ result = tp.processGwRequest(req.getRemoteAddr(), tlFrame.command, payload);
+ }
+ if (resp == null) {
+ return;
+ }
+
+ resp.setContentType(MediaType.APPLICATION_JSON);
+ resp.setStatus(HttpStatus.OK_200);
+ resp.getWriter().append(result);
+ resp.getWriter().close();
+ }
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/servers/IHttpClientProvider.java b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/servers/IHttpClientProvider.java
new file mode 100644
index 00000000000..7be84b310cb
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/servers/IHttpClientProvider.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.linktap.protocol.servers;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+
+/**
+ * Implementations of this interface, allow access to a HttpClient which can be used
+ * for communication requests to LinkTap Gateways.
+ *
+ * @author David Goodyear - Initial contribution
+ */
+@NonNullByDefault
+public interface IHttpClientProvider {
+
+ /**
+ * This returns a HttpClient reference which can be used for communication requests.
+ *
+ * @return instance of HttpClient to use for requests
+ */
+ HttpClient getHttpClient();
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644
index 00000000000..7dad0e1ad70
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/addon/addon.xml
@@ -0,0 +1,29 @@
+
+
+
+ binding
+ LinkTap Binding
+ This is the binding for LinkTap.
+ local
+
+
+
+ mdns
+
+
+ mdnsServiceType
+ _http._tcp.local.
+
+
+
+
+ name
+ ^(LinkTapGw_)
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/i18n/linktap.properties b/bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/i18n/linktap.properties
new file mode 100644
index 00000000000..29cd79bf376
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/i18n/linktap.properties
@@ -0,0 +1,313 @@
+# add-on
+
+addon.linktap.name = LinkTap Binding
+addon.linktap.description = This is the binding for LinkTap.
+
+# thing types
+
+thing-type.linktap.device.label = LinkTap Binding Thing
+thing-type.linktap.device.description = LinkTap Binding Device
+thing-type.linktap.gateway.label = LinkTap Gateway
+thing-type.linktap.gateway.description = This represents a LinkTap gateway
+
+# thing types config
+
+thing-type.config.linktap.device.enableAlerts.label = Auto Enable Alerts
+thing-type.config.linktap.device.enableAlerts.description = If enabled, during device initialisation all alerts are enabled
+thing-type.config.linktap.device.id.label = Device Id
+thing-type.config.linktap.device.id.description = The Device Id for the device under the gateway
+thing-type.config.linktap.device.name.label = Device Name
+thing-type.config.linktap.device.name.description = The name allocated to the device by the app. (Must be unique if used)
+thing-type.config.linktap.gateway.enableJSONComms.label = Enable non HTML responses
+thing-type.config.linktap.gateway.enableJSONComms.description = Enable only if openHAB is directly using the Gateway, to allow more efficient communications
+thing-type.config.linktap.gateway.enableMDNS.label = Enable mDNS Responder
+thing-type.config.linktap.gateway.enableMDNS.description = On connection whether the mDNS responder should be enabled on the gateway device
+thing-type.config.linktap.gateway.enforceProtocolLimits.label = Enforce protocol limits
+thing-type.config.linktap.gateway.enforceProtocolLimits.description = If parameters outside the limits acceptable to the device's are sent they will be blocked and logged
+thing-type.config.linktap.gateway.host.label = Hostname / IP
+thing-type.config.linktap.gateway.host.description = The hostname / IP address of the gateway device
+thing-type.config.linktap.gateway.password.label = Device Password
+thing-type.config.linktap.gateway.password.description = The password if set for the gateway device
+thing-type.config.linktap.gateway.username.label = Device Username
+thing-type.config.linktap.gateway.username.description = The username if set for the gateway device
+
+# channel types
+
+channel-type.linktap.battery-level-type.label = Battery Level
+channel-type.linktap.battery-level-type.description = Battery Remaining Level
+channel-type.linktap.child-lock-type.label = Child Lock Mode
+channel-type.linktap.child-lock-type.description = The child lock mode
+channel-type.linktap.child-lock-type.state.option.0 = Unlocked
+channel-type.linktap.child-lock-type.state.option.1 = Partially locked
+channel-type.linktap.child-lock-type.state.option.2 = Completely locked
+channel-type.linktap.fail-status-type.label = Shutdown Value Failed
+channel-type.linktap.fail-status-type.description = The device has failed to close the valve
+channel-type.linktap.failsafe-duration-type.label = Watering Cycle Failsafe
+channel-type.linktap.failsafe-duration-type.description = Failsafe duration of the current watering cycle
+channel-type.linktap.fall-status-type.label = Fallen Status
+channel-type.linktap.fall-status-type.description = The device has fallen
+channel-type.linktap.final-segment-type.label = Final ECO Segment
+channel-type.linktap.final-segment-type.description = In ECO mode this is true when the final ON watering on segment is running
+channel-type.linktap.flm-linked-type.label = FLM Linked
+channel-type.linktap.flm-linked-type.description = The device has a included flow meter
+channel-type.linktap.flow-rate-type.label = Flow Rate
+channel-type.linktap.flow-rate-type.description = Current water flow rate
+channel-type.linktap.instant-duration-type.label = Instant Duration Limit
+channel-type.linktap.instant-duration-type.description = Max duration allowed for the immediate watering
+channel-type.linktap.instant-limit-type.label = Instant Volume Limit
+channel-type.linktap.instant-limit-type.description = Max Volume limit for immediate watering
+channel-type.linktap.is-clog-type.label = Low Flow Detected
+channel-type.linktap.is-clog-type.description = Unusually low flow rate detected alert
+channel-type.linktap.is-leak-type.label = High Flow Detected
+channel-type.linktap.is-leak-type.description = Unusually high flow rate detected alert
+channel-type.linktap.man-mode-type.label = Manual Watering
+channel-type.linktap.man-mode-type.description = Manual watering mode status
+channel-type.linktap.mode-type.label = Watering Mode
+channel-type.linktap.mode-type.description = The watering mode
+channel-type.linktap.mode-type.state.option.0 = Off
+channel-type.linktap.mode-type.state.option.1 = Instant
+channel-type.linktap.mode-type.state.option.2 = Calendar
+channel-type.linktap.mode-type.state.option.3 = Day
+channel-type.linktap.mode-type.state.option.4 = Odd-even
+channel-type.linktap.mode-type.state.option.5 = Interval
+channel-type.linktap.mode-type.state.option.6 = Month
+channel-type.linktap.pause-enable-type.label = Pause plan schedule
+channel-type.linktap.pause-enable-type.description = When ON will pause the current watering plan for an hour every 55 minutes
+channel-type.linktap.pause-until-type.label = Plan Paused Until
+channel-type.linktap.pause-until-type.description = Displays when the last pause issued will expiry, resuming the current watering plan
+channel-type.linktap.plan-id-type.label = Watering Plan Id
+channel-type.linktap.plan-id-type.description = Displays the current watering plan id
+channel-type.linktap.remaining-duration-type.label = Watering Cycle Remaining
+channel-type.linktap.remaining-duration-type.description = Remaining duration of the current watering cycle
+channel-type.linktap.rf-linked-type.label = RF Linked
+channel-type.linktap.rf-linked-type.description = Is the device RF linked
+channel-type.linktap.signal-level-type.label = Signal Level
+channel-type.linktap.signal-level-type.description = Reception Signal Strength
+channel-type.linktap.total-duration-type.label = Watering Cycle Duration
+channel-type.linktap.total-duration-type.description = Total duration of current watering cycle
+channel-type.linktap.volume-limit-type.label = Current Watering Limit
+channel-type.linktap.volume-limit-type.description = Volume limit for the current watering cycle
+channel-type.linktap.volume-type.label = Current Watering Volume
+channel-type.linktap.volume-type.description = Accumulated volume of current watering cycle
+channel-type.linktap.water-cut-type.label = Water Cutoff
+channel-type.linktap.water-cut-type.description = Water cut-off alert
+channel-type.linktap.watering-type.label = Watering
+channel-type.linktap.watering-type.description = Active watering status
+
+# thing types
+
+thing-type.linktap.bridge.label = LinkTap Bridge
+thing-type.linktap.bridge.description = The LinkTap bridge represents a LinkTap gateway device
+
+# thing types config
+
+thing-type.config.linktap.bridge.enableMDNS.label = Enable mDNS Responder
+thing-type.config.linktap.bridge.enableMDNS.description = On connection whether the mDNS responder should be enabled on the gateway device
+thing-type.config.linktap.bridge.host.label = Hostname / IP
+thing-type.config.linktap.bridge.host.description = The hostname / IP address of the gateway device
+thing-type.config.linktap.bridge.password.label = Device Password
+thing-type.config.linktap.bridge.password.description = The password if set for the gateway device
+thing-type.config.linktap.bridge.username.label = Device Username
+thing-type.config.linktap.bridge.username.description = The username if set for the gateway device
+
+# channel types
+
+channel-type.linktap.future-rain-type.label = Watering Skipped Future
+channel-type.linktap.future-rain-type.description = Future rainfall calculated when watering was skipped
+channel-type.linktap.prev-rain-type.label = Watering Skipped Previous
+channel-type.linktap.prev-rain-type.description = Previous rainfall calculated when watering was skipped
+channel-type.linktap.rain-timestamp-type.label = Watering Skipped Timestamp
+channel-type.linktap.rain-timestamp-type.description = Time when watering was skipped
+
+# channel types
+
+channel-type.linktap.instant-volume-limit-type.label = OH Instance On Watering Limit
+channel-type.linktap.instant-volume-limit-type.description = Max Volume limit for immediate watering
+
+# channel types
+
+channel-type.linktap.battery-level.label = Battery Level
+channel-type.linktap.battery-level.description = Battery Remaining Level
+channel-type.linktap.child-lock-mode.label = Child-lock mode
+channel-type.linktap.child-lock-mode.description = The child lock mode
+channel-type.linktap.child-lock-mode.state.option.0 = Unlocked
+channel-type.linktap.child-lock-mode.state.option.1 = Partially locked
+channel-type.linktap.child-lock-mode.state.option.2 = Completely locked
+channel-type.linktap.fail-status.label = Shutdown value failed
+channel-type.linktap.fail-status.description = The device has failed to close the valve
+channel-type.linktap.failsafe-duration.label = Watering Cycle Failsafe
+channel-type.linktap.failsafe-duration.description = Failsafe duration of the current watering cycle
+channel-type.linktap.fall-status.label = Fallen status
+channel-type.linktap.fall-status.description = The device has fallen
+channel-type.linktap.final-segment.label = Final ECO Segment
+channel-type.linktap.final-segment.description = In ECO mode this is true when the final ON watering on segment is running
+channel-type.linktap.flm-linked.label = FLM Linked
+channel-type.linktap.flm-linked.description = The device has a included flow meter
+channel-type.linktap.flow-rate.label = Flow Rate
+channel-type.linktap.flow-rate.description = Current water flow rate
+channel-type.linktap.instant-duration.label = OH Instance On Duration
+channel-type.linktap.instant-duration.description = Max duration allowed for the immediate watering
+channel-type.linktap.instant-volume-limit.label = OH Instance On Watering Limit
+channel-type.linktap.instant-volume-limit.description = Max Volume limit for immediate watering
+channel-type.linktap.is-clog.label = Low Flow Detected
+channel-type.linktap.is-clog.description = Unusually low flow rate detected alert
+channel-type.linktap.is-leak.label = High Flow Detected
+channel-type.linktap.is-leak.description = Unusually high flow rate detected alert
+channel-type.linktap.man-mode.label = Manual watering
+channel-type.linktap.man-mode.description = Manual watering mode status
+channel-type.linktap.mode.label = Watering mode
+channel-type.linktap.mode.description = The watering mode
+channel-type.linktap.mode.state.option.0 = Off
+channel-type.linktap.mode.state.option.1 = Instant
+channel-type.linktap.mode.state.option.2 = Calendar
+channel-type.linktap.mode.state.option.3 = Day
+channel-type.linktap.mode.state.option.4 = Odd-even
+channel-type.linktap.mode.state.option.5 = Interval
+channel-type.linktap.mode.state.option.6 = Month
+channel-type.linktap.remaining-duration.label = Watering Cycle Remaining
+channel-type.linktap.remaining-duration.description = Remaining duration of the current watering cycle
+channel-type.linktap.rf-inked.label = RF Linked
+channel-type.linktap.rf-inked.description = Is the device RF linked
+channel-type.linktap.signal-level.label = Signal Level
+channel-type.linktap.signal-level.description = Reception Signal Strength
+channel-type.linktap.skip-future-rain.label = Watering Skipped Future Rain
+channel-type.linktap.skip-future-rain.description = Future rainfall calculated when watering was skipped
+channel-type.linktap.skip-prev-rain.label = Watering Skipped Previous Rain
+channel-type.linktap.skip-prev-rain.description = Previous rainfall calculated when watering was skipped
+channel-type.linktap.skip-timestamp.label = Watering Skipped Timestamp
+channel-type.linktap.skip-timestamp.description = Time when watering was skipped
+channel-type.linktap.total-duration.label = Watering Cycle Duration
+channel-type.linktap.total-duration.description = Total duration of current watering cycle
+channel-type.linktap.volume-limit.label = Current Watering Limit
+channel-type.linktap.volume-limit.description = Volume limit for the current watering cycle
+channel-type.linktap.volume.label = Current Watering Volume
+channel-type.linktap.volume.description = Accumulated volume of current watering cycle
+channel-type.linktap.water-cut-off.label = Water Cutoff
+channel-type.linktap.water-cut-off.description = Water cut-off alert
+channel-type.linktap.watering.label = Watering
+channel-type.linktap.watering.description = Active watering status
+
+# channel types
+
+channel-type.linktap.deviceBatteryLevel.label = Battery Level
+channel-type.linktap.deviceBatteryLevel.description = Battery Remaining Level
+channel-type.linktap.deviceChildLockMode.label = Child-lock mode
+channel-type.linktap.deviceChildLockMode.description = The child lock mode
+channel-type.linktap.deviceChildLockMode.state.option.0 = Unlocked
+channel-type.linktap.deviceChildLockMode.state.option.1 = Partially locked
+channel-type.linktap.deviceChildLockMode.state.option.2 = Completely locked
+channel-type.linktap.deviceFailStatus.label = Shutdown value failed
+channel-type.linktap.deviceFailStatus.description = The device has failed to close the valve
+channel-type.linktap.deviceFailsafeDuration.label = Watering Cycle Failsafe
+channel-type.linktap.deviceFailsafeDuration.description = Failsafe duration of the current watering cycle
+channel-type.linktap.deviceFallStatus.label = Fallen status
+channel-type.linktap.deviceFallStatus.description = The device has fallen
+channel-type.linktap.deviceFinalSegment.label = Final ECO Segment
+channel-type.linktap.deviceFinalSegment.description = In ECO mode this is true when the final ON watering on segment is running
+channel-type.linktap.deviceFlmLinked.label = FLM Linked
+channel-type.linktap.deviceFlmLinked.description = The device has a included flow meter
+channel-type.linktap.deviceFlowRate.label = Flow Rate
+channel-type.linktap.deviceFlowRate.description = Current water flow rate
+channel-type.linktap.deviceIsClog.label = Low Flow Detected
+channel-type.linktap.deviceIsClog.description = Unusually low flow rate detected alert
+channel-type.linktap.deviceIsLeak.label = High Flow Detected
+channel-type.linktap.deviceIsLeak.description = Unusually high flow rate detected alert
+channel-type.linktap.deviceManMode.label = Manual watering
+channel-type.linktap.deviceManMode.description = Manual watering mode status
+channel-type.linktap.deviceMode.label = Watering mode
+channel-type.linktap.deviceMode.description = The watering mode
+channel-type.linktap.deviceMode.state.option.0 = Off
+channel-type.linktap.deviceMode.state.option.1 = Instant
+channel-type.linktap.deviceMode.state.option.2 = Calendar
+channel-type.linktap.deviceMode.state.option.3 = Day
+channel-type.linktap.deviceMode.state.option.4 = Odd-even
+channel-type.linktap.deviceMode.state.option.5 = Interval
+channel-type.linktap.deviceMode.state.option.6 = Month
+channel-type.linktap.deviceRemainingDuration.label = Watering Cycle Remaining
+channel-type.linktap.deviceRemainingDuration.description = Remaining duration of the current watering cycle
+channel-type.linktap.deviceRfLinked.label = RF Linked
+channel-type.linktap.deviceRfLinked.description = Is the device RF linked
+channel-type.linktap.deviceSignalLevel.label = Signal Level
+channel-type.linktap.deviceSignalLevel.description = Reception Signal Strength
+channel-type.linktap.deviceTotalDuration.label = Watering Cycle Duration
+channel-type.linktap.deviceTotalDuration.description = Total duration of current watering cycle
+channel-type.linktap.deviceVolume.label = Current Watering Volume
+channel-type.linktap.deviceVolume.description = Accumulated volume of current watering cycle
+channel-type.linktap.deviceVolumeLimit.label = Current Watering Limit
+channel-type.linktap.deviceVolumeLimit.description = Volume limit for the current watering cycle
+channel-type.linktap.deviceWaterCutoff.label = Water Cutoff
+channel-type.linktap.deviceWaterCutoff.description = Water cut-off alert
+channel-type.linktap.deviceWatering.label = Watering
+channel-type.linktap.deviceWatering.description = Active watering status
+channel-type.linktap.ohInstantDuration.label = OH Instance On Duration
+channel-type.linktap.ohInstantDuration.description = Max duration allowed for the immediate watering
+channel-type.linktap.ohInstantVolumeLimit.label = OH Instance On Watering Limit
+channel-type.linktap.ohInstantVolumeLimit.description = Max Volume limit for immediate watering
+channel-type.linktap.waterSkipDateTime.label = Watering Skipped Timestamp
+channel-type.linktap.waterSkipDateTime.description = Time when watering was skipped
+channel-type.linktap.waterSkipFutureRain.label = Watering Skipped Future Rain
+channel-type.linktap.waterSkipFutureRain.description = Future rainfall calculated when watering was skipped
+channel-type.linktap.waterSkipPrevRain.label = Watering Skipped Previous Rain
+channel-type.linktap.waterSkipPrevRain.description = Previous rainfall calculated when watering was skipped
+
+# errors
+
+bridge.error.host-not-found = Hostname / IP cannot be found
+bridge.error.check-credentials = Check credentials provided
+bridge.error.cannot-connect = Cannot connect to LinkTap Gateway
+bridge.error.unknown-host = Unknown host
+bridge.error.target-is-not-gateway = Target Host is not a LinkTap Gateway
+polling-device.error.bridge-unset = Bridge is not selected / set
+polling-device.error.device-unknown-in-bridge = Device not found in bridges known devices
+polling-device.error.unknown-device-id = Bridge does not recognise device id
+polling-device.error.unknown-device = Check device setup - device is unknown
+protocol.ret.success = Success
+protocol.ret.format-error = Message format error
+protocol.ret.cmd-unsupported = CMD message not supported
+protocol.ret.gw-id-unmatched = Gateway ID not matched
+protocol.ret.end-device-id-error = End device ID error
+protocol.ret.end-device-id-not-found = End device ID not found
+protocol.ret.gw-internal-error = Gateway internal error
+protocol.ret.conflict-watering-plan = Conflict with watering plan
+protocol.ret.gw-busy = Gateway busy
+protocol.ret.bad-parameter-in-msg = Bad parameter in message
+protocol.ret.invalid = Missing ret response
+warning.failed-local-address-detection = Failed to determine local openHab address due to connection failure with exception {}
+warning.no-http-server-port = HTTP Server port is not running, cannot use API callbacks
+warning.fw-update-local-config = {0} -> Local configuration support requires newer firmware it should be >= {1}
+warning.user-data-payload-failure = Device {0} payload validation failed - will not send due to bad data -> {1}
+warning.parameter-not-accepted = Parameter not accepted by device {0} for command {1}
+warning.response-from-wrong-gw-id = {0} = Response from incorrect Gateway "{1}" != "{2}"
+warning.incorrect-cmd-resp = {0} = Received incorrect CMD response {1} != {2}
+warning.non-gw = Communicating with non TapLink Gateway detected
+warning.not-taplink-gw = {0} = {1} is not a Link Tap Gateway!
+warning.comms-issue-auto-retry = {0} = Possible communications issue (auto retry): {1}
+warning.device-no-accept = Device {0} did not accept command {1}
+warning.error-with-gw-id = Error with gateway ID
+warning.host-gw-unknown-for-cmd = Request when host "{0}" or gateway "{1}" id is unknown for command {2}
+warning.discovery-charset-missing = Missing character set to decode MDNS Text
+warning.unexpected-response-frame = Unexpected response frame {0} -> {1}
+warning.unexpected-cmd-result = Unexpected command result
+bug-report.failed-alert-enable = Raise Bug Report: {0} - Failed to enable all alerts - invalid parameter exception
+bug-report.poll-failure = Raise Bug Report: {0} - Poll failure - invalid parameter exception
+bug-report.pause-plan-failure = Raise Bug Report: {0} - Pause plan failure - invalid parameter exception
+bug-report.unexpected-payload-failure = Potential Bug: Device {0} payload validation failed - will not send -> {1}
+bug-report.gw-unsupported-command = Raise Bug Report: Command {0} not supported by gateway
+exception.device-id-exception = Device ID Exception
+exception.gw-id-exception = Gateway ID Exception
+exception-cmd-not-supported-exception = Command Not Supported Exception
+exception.invalid-parameter-exception = Invalid Parameter Exception
+exception.could-not-connect = Could not connect
+exception.could-not-resolve = Could not resolve IP address
+exception.communications-lost = Communications Lost
+exception.local-addr-lookup-failure = Local address lookup failure
+exception.exec-exception = ExecutionException -> {0}
+exception.not-gw.missing-headers = Missing header markers
+exception.not-gw.missing-title = Not a LinkTap API response
+exception.not-gw.missing-server-title = Not a LinkTap response
+exception.not-gw.unexpected-status-code = Unexpected status code response
+exception.not-gw.unexpected-protocol = Unexpected protocol
+exception.not-tap-link-gw = Not a TapLink GW
+exception.fail-servlet-registration = Register servlet failed for {0}
+exception.unexpected-failure = Unexpected failure -> {0}
+exception.unexpected-exception = Unexpected exception
diff --git a/bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/thing/channel-types.xml
new file mode 100644
index 00000000000..1e6c9f78014
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/thing/channel-types.xml
@@ -0,0 +1,223 @@
+
+
+
+
+ String
+
+ The watering mode
+ Time
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Switch
+
+ Manual watering mode status
+ Water
+
+
+
+
+ Switch
+
+ Active watering status
+ Water
+
+
+
+
+ Switch
+
+ Is the device RF linked
+ Switch
+
+
+
+
+ Switch
+
+ The device has a included flow meter
+ Switch
+
+
+
+
+ Switch
+
+ Unusually high flow rate detected alert
+ Alarm
+
+
+
+
+ Switch
+
+ Unusually low flow rate detected alert
+ Alarm
+
+
+
+
+ Switch
+
+ The device has fallen
+ Alarm
+
+
+
+
+ Switch
+
+ The device has failed to close the valve
+ Alarm
+
+
+
+
+ Switch
+
+ In ECO mode this is true when the final ON watering on segment is running
+ Switch
+
+
+
+
+ Number:Dimensionless
+
+ Reception Signal Strength
+ QualityOfService
+
+
+
+
+ Number:Dimensionless
+
+ Battery Remaining Level
+ BatteryLevel
+
+
+
+
+ Switch
+
+ Water cut-off alert
+ Alarm
+
+
+
+
+ Number:VolumetricFlowRate
+
+ Current water flow rate
+ Flow
+
+
+
+
+ Number:Volume
+
+ Accumulated volume of current watering cycle
+ Water
+
+
+
+
+ Number:Volume
+
+ Volume limit for the current watering cycle
+ Water
+
+
+
+
+ Number:Time
+
+ Total duration of current watering cycle
+ Time
+
+
+
+
+ Number:Time
+
+ Remaining duration of the current watering cycle
+ Time
+
+
+
+
+ Number:Time
+
+ Failsafe duration of the current watering cycle
+ Time
+
+
+
+
+ String
+
+ The child lock mode
+ Lock
+
+
+
+
+
+
+
+
+
+
+ Number:Time
+
+ Max duration allowed for the immediate watering
+ Time
+
+
+
+
+ Number:Volume
+
+ Max Volume limit for immediate watering
+ Water
+
+
+
+
+ Switch
+
+ When ON will pause the current watering plan for an hour every 55 minutes
+ Time
+
+
+
+
+ DateTime
+
+ Displays when the last pause issued will expiry, resuming the current watering plan
+ Calendar
+
+
+
+
+ String
+
+ Displays the current watering plan id
+ Calendar
+
+
+
+
diff --git a/bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 00000000000..7da700be2bb
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,119 @@
+
+
+
+
+
+ This represents a LinkTap gateway
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ network-address
+
+ The hostname / IP address of the gateway device
+
+
+
+ The username if set for the gateway device
+
+
+ password
+
+ The password if set for the gateway device
+
+
+
+ On connection whether the mDNS responder should be enabled on the gateway device
+ true
+ true
+
+
+
+ Enable only if openHAB is directly using the Gateway, to allow more efficient communications
+ false
+ true
+
+
+
+ If parameters outside the limits acceptable to the device's are sent they will be blocked and logged
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+ LinkTap Binding Device
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Device Id
+ Device Name
+
+ deviceId
+
+
+
+
+ The Device Id for the device under the gateway
+
+
+
+ The name allocated to the device by the app. (Must be unique if used)
+
+
+
+ If enabled, during device initialisation all alerts are enabled
+ true
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command0Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command0Test.java
new file mode 100644
index 00000000000..ed55b558385
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command0Test.java
@@ -0,0 +1,71 @@
+/**
+ * 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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+import java.util.Arrays;
+import java.util.Properties;
+import java.util.Vector;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_HANDSHAKE;
+
+/**
+ * Command 0: Handshake
+ * Flow 1 --> GW->Broker->App: First message after connection with system device mappings
+ */
+@NonNullByDefault
+public class Command0Test {
+
+ /**
+ * Command 0:
+ * Flow 1 --> GW->Broker->App: First handshake message decoding test
+ */
+ @Test
+ public void HandshakeRequestDecoding() {
+ final HandshakeReq decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":0, \"gw_id\":\"CCCCDDDDEEEEFFFF\", \"ver\":\"G0404172103261024C\", \"end_dev\":[ \"1111222233334444\", \"7777888933336666\", \"2245222233334444\", \"3333999993333555\"\n]\n}",HandshakeReq.class);
+
+ assertEquals(CMD_HANDSHAKE,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ assertEquals("G0404172103261024C",decoded.version);
+ assertEquals(4,decoded.endDevices.length);
+ assertTrue(Arrays.asList(decoded.endDevices).contains("1111222233334444"));
+ assertTrue(Arrays.asList(decoded.endDevices).contains("7777888933336666"));
+ assertTrue(Arrays.asList(decoded.endDevices).contains("2245222233334444"));
+ assertTrue(Arrays.asList(decoded.endDevices).contains("3333999993333555"));
+ }
+
+ /**
+ * Command 0:
+ * Flow 1 --> GW->Broker->App: First handshake message response encoding test
+ */
+ @Test
+ public void HandshakeResponseEncoding() {
+ HandshakeResp reply = new HandshakeResp();
+ reply.command = CMD_HANDSHAKE;
+ reply.gatewayId = "CCCCDDDDEEEEFFFF";
+ reply.date = "20210501";
+ reply.time = "123055";
+ reply.wday = 6;
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(reply);
+
+ assertEquals("{\"date\":\"20210501\",\"time\":\"123055\",\"wday\":6,\"cmd\":0,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command10Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command10Test.java
new file mode 100644
index 00000000000..09512f81caa
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command10Test.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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_ALERT_DISMISS;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_ALERT_ENABLEMENT;
+
+/**
+ * Command 10: Enable or Disable alert type
+ * Flow 1 --> App->Broker->GW: Enable or Disable the given alert type
+ */
+@NonNullByDefault
+public class Command10Test {
+
+ /**
+ * Command 10: Enable or Disable alert type
+ * Flow 1 --> App->Broker->GW: Enable or Disable the given alert type
+ */
+ @Test
+ public void ChangeAlertStateGenerationTest() {
+ AlertStateReq req = new AlertStateReq();
+ req.command = CMD_ALERT_ENABLEMENT;
+ req.gatewayId = "CCCCDDDDEEEEFFFF";
+ req.deviceId = "1111222233334444";
+ req.alert = 0;
+ req.enable = true;
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(req);
+
+ assertEquals("{\"enable\":true,\"alert\":0,\"dev_id\":\"1111222233334444\",\"cmd\":10,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+ /**
+ * Command 11: Dismiss Alert
+ * Flow 1 --> App->Broker->GW: Dismiss the specified alert type reply
+ */
+ @Test
+ public void ChangeAlertStateResponseDecoding() {
+ final EndpointDeviceResponse decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":10, \"gw_id\":\"CCCCDDDDEEEEFFFF\", \"dev_id\":\"1111222233334444\", \"ret\":0\n}",EndpointDeviceResponse.class);
+
+ assertEquals(CMD_ALERT_ENABLEMENT,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ assertEquals("1111222233334444",decoded.deviceId );
+ assertEquals(GatewayDeviceResponse.ResultStatus.RET_SUCCESS,decoded.getRes());
+ }
+
+
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command11Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command11Test.java
new file mode 100644
index 00000000000..29a67970a1e
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command11Test.java
@@ -0,0 +1,63 @@
+/**
+ * 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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_ALERT_DISMISS;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_RAINFALL_DATA;
+
+/**
+ * Command 11: Dismiss Alert
+ * Flow 1 --> App->Broker->GW: Dismiss the specified alert type
+ */
+@NonNullByDefault
+public class Command11Test {
+
+ /**
+ * Command 11: Dismiss Alert
+ * Flow 1 --> App->Broker->GW: Dismiss the specified alert type
+ */
+ @Test
+ public void DismissAlertGenerationTest() {
+ DismissAlertReq req = new DismissAlertReq();
+ req.command = CMD_ALERT_DISMISS;
+ req.gatewayId = "CCCCDDDDEEEEFFFF";
+ req.deviceId = "1111222233334444";
+ req.alert = 0;
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(req);
+
+ assertEquals("{\"alert\":0,\"dev_id\":\"1111222233334444\",\"cmd\":11,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+ /**
+ * Command 11: Dismiss Alert
+ * Flow 1 --> App->Broker->GW: Dismiss the specified alert type reply
+ */
+ @Test
+ public void DismissAlertResponseDecoding() {
+ final EndpointDeviceResponse decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":11, \"gw_id\":\"CCCCDDDDEEEEFFFF\", \"dev_id\":\"1111222233334444\", \"ret\":0\n}",EndpointDeviceResponse.class);
+
+ assertEquals(CMD_ALERT_DISMISS,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ assertEquals("1111222233334444",decoded.deviceId );
+ assertEquals(GatewayDeviceResponse.ResultStatus.RET_SUCCESS,decoded.getRes());
+ }
+
+
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command12Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command12Test.java
new file mode 100644
index 00000000000..8a4b1e9299b
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command12Test.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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_ALERT_DISMISS;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_LOCKOUT_STATE;
+
+/**
+ * Command 12: Set lock state
+ * Flow 1 --> App->Broker->GW: Request lock status is changed - G15 and G25 models only
+ */
+@NonNullByDefault
+public class Command12Test {
+
+ /**
+ * Command 12: Set lock state
+ * Flow 1 --> App->Broker->GW: Request lock status is changed - G15 and G25 models only
+ */
+ @Test
+ public void SetLockStateGenerationTest() {
+ LockReq req = new LockReq();
+ req.command = CMD_LOCKOUT_STATE;
+ req.gatewayId = "CCCCDDDDEEEEFFFF";
+ req.deviceId = "1111222233334444";
+ req.lock = 0;
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(req);
+
+ assertEquals("{\"lock\":0,\"dev_id\":\"1111222233334444\",\"cmd\":12,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+ /**
+ * Command 12: Set lock state
+ * Flow 1 --> App->Broker->GW: Request lock status is changed response decoding
+ */
+ @Test
+ public void SetLockStateResponseDecoding() {
+ final EndpointDeviceResponse decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":12, \"gw_id\":\"CCCCDDDDEEEEFFFF\", \"dev_id\":\"1111222233334444\", \"ret\":0\n}",EndpointDeviceResponse.class);
+
+ assertEquals(CMD_LOCKOUT_STATE,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ assertEquals("1111222233334444",decoded.deviceId );
+ assertEquals(GatewayDeviceResponse.ResultStatus.RET_SUCCESS,decoded.getRes());
+ }
+
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command13Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command13Test.java
new file mode 100644
index 00000000000..9dbe24dcb16
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command13Test.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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.*;
+
+/**
+ * Command 13: Sync Gateway time
+ * Flow 1 --> GW->Broker->App: Request from Gateway for the current system time
+ */
+@NonNullByDefault
+public class Command13Test {
+
+ /**
+ * Command 13: Sync Gateway time
+ * Flow 1 --> GW->Broker->App: Response sent from the App to the Gateway with the time information
+ */
+ @Test
+ public void ResponseForAppTimeRequestGenerationTest() {
+ HandshakeResp req = new HandshakeResp();
+ req.gatewayId = "CCCCDDDDEEEEFFFF";
+ req.command = CMD_DATETIME_SYNC;
+ req.date = "20210501";
+ req.time = "123055";
+ req.wday = 6;
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(req);
+
+ assertEquals("{\"date\":\"20210501\",\"time\":\"123055\",\"wday\":6,\"cmd\":13,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+ /**
+ * Command 13: Sync Gateway time
+ * Flow 1 --> GW->Broker->App: Request from Gateway for the current system time
+ */
+ @Test
+ public void RequestForAppTimeDecoding() {
+ final TLGatewayFrame decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":13, \"gw_id\":\"CCCCDDDDEEEEFFFF\"\n}",EndpointDeviceResponse.class);
+ assertEquals(CMD_DATETIME_SYNC,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ }
+
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command14Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command14Test.java
new file mode 100644
index 00000000000..b1ea9670b9f
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command14Test.java
@@ -0,0 +1,60 @@
+/**
+ * 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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_DATETIME_READ;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_DATETIME_SYNC;
+
+/**
+ * Command 14: Read Gateway time
+ * Flow 1 --> App->Broker->GW: Request from Gateway its current system time
+ */
+@NonNullByDefault
+public class Command14Test {
+
+ /**
+ * Command 14: Read Gateway time
+ * Flow 1 --> App->Broker->GW: Request from Gateway its current system time
+ */
+ @Test
+ public void ResponseForGwTimeRequestGenerationTest() {
+ HandshakeResp req = new HandshakeResp();
+ req.gatewayId = "CCCCDDDDEEEEFFFF";
+ req.command = CMD_DATETIME_READ;
+ req.date = "20210501";
+ req.time = "123055";
+ req.wday = 6;
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(req);
+
+ assertEquals("{\"date\":\"20210501\",\"time\":\"123055\",\"wday\":6,\"cmd\":14,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+ /**
+ * Command 14: Read Gateway time
+ * Flow 1 --> App->Broker->GW: Request from Gateway its current system time
+ */
+ @Test
+ public void RequestForGwTimeDecoding() {
+ final TLGatewayFrame decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":14, \"gw_id\":\"CCCCDDDDEEEEFFFF\"\n}",EndpointDeviceResponse.class);
+ assertEquals(CMD_DATETIME_READ,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ }
+
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command15Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command15Test.java
new file mode 100644
index 00000000000..e4a4bfa8c6d
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command15Test.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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_DATETIME_SYNC;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_WIRELESS_CHECK;
+
+/**
+ * Command 15: Test wireless performance of end device
+ * Flow 1 --> App->Broker->GW: Request a ping pong test is done to measure wireless performance for a end device
+ */
+@NonNullByDefault
+public class Command15Test {
+
+ /**
+ * Command 15: Test wireless performance of end device
+ * Flow 1 --> App->Broker->GW: Request a ping pong test is done to measure wireless performance for a end device
+ */
+ @Test
+ public void RequestWirelessCheckGenerationTest() {
+ DeviceCmdReq req = new DeviceCmdReq();
+ req.gatewayId = "CCCCDDDDEEEEFFFF";
+ req.deviceId = "1111222233334444";
+ req.command = CMD_WIRELESS_CHECK;
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(req);
+
+ assertEquals("{\"dev_id\":\"1111222233334444\",\"cmd\":15,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+ /**
+ * Command 15: Test wireless performance of end device
+ * Flow 1 --> App->Broker->GW: Request a ping pong test is done to measure wireless performance for a end device response
+ */
+ @Test
+ public void RequestWirelessCheckResponseDecoding() {
+ final WirelessTestResp decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":15, \"gw_id\":\"CCCCDDDDEEEEFFFF\", \"dev_id\":\"1111222233334444\", \"ret\":0, \"final\":false, \"ping\":5, \"pong\":4\n}",WirelessTestResp.class);
+ assertEquals(CMD_WIRELESS_CHECK,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ assertEquals("1111222233334444",decoded.deviceId );
+ assertEquals(GatewayDeviceResponse.ResultStatus.RET_SUCCESS,decoded.getRes());
+ assertFalse(decoded.testComplete);
+ assertEquals(5, decoded.pingCount);
+ assertEquals(4, decoded.pongCount);
+ }
+
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command16Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command16Test.java
new file mode 100644
index 00000000000..61b2950c1e6
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command16Test.java
@@ -0,0 +1,71 @@
+/**
+ * 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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+import java.util.Arrays;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_GET_CONFIGURATION;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_WIRELESS_CHECK;
+
+/**
+ * Command 16: Get gateway configuration
+ * Flow 1 --> App->Broker->GW: Request the configuration of the Gateway
+ */
+@NonNullByDefault
+public class Command16Test {
+
+ /**
+ * Command 16: Get gateway configuration
+ * Flow 1 --> App->Broker->GW: Request the configuration of the Gateway
+ */
+ @Test
+ public void RequestGatewayConfigGenerationTest() {
+ TLGatewayFrame req = new TLGatewayFrame();
+ req.gatewayId = "CCCCDDDDEEEEFFFF";
+ req.command = CMD_GET_CONFIGURATION;
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(req);
+
+ assertEquals("{\"cmd\":16,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+ /**
+ * Command 16: Get gateway configuration
+ * Flow 1 --> App->Broker->GW: Request the configuration of the Gateway
+ */
+ @Test
+ public void RequestGatewayConfigResponseDecoding() {
+ final GatewayConfigResp decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":16, \"gw_id\":\"1234E607004B1200\", \"ver\":\"G0608062305191832I\", \"vol_unit\":\"gal\", \"end_dev\":[ \"1234A923004B1200\", \"56787022004B1200\", \"ABCD6D13004B1200\"], \"dev_name\":[ \"Name_Of_Device_1234A923004B1200\", \"Name_Of_Device_56787022004B1200\", \"Name_Of_Device_ABCD6D13004B1200\"] }",GatewayConfigResp.class);
+ assertEquals(CMD_GET_CONFIGURATION,decoded.command);
+ assertEquals("1234E607004B1200",decoded.gatewayId );
+ assertEquals("G0608062305191832I",decoded.version );
+ assertEquals("gal", decoded.volumeUnit);
+ assertEquals(3, decoded.endDevices.length);
+ assertTrue(Arrays.asList(decoded.endDevices).contains("1234A923004B1200"));
+ assertTrue(Arrays.asList(decoded.endDevices).contains("56787022004B1200"));
+ assertTrue(Arrays.asList(decoded.endDevices).contains("ABCD6D13004B1200"));
+ assertEquals(3, decoded.deviceNames.length);
+ assertTrue(Arrays.asList(decoded.deviceNames).contains("Name_Of_Device_1234A923004B1200"));
+ assertTrue(Arrays.asList(decoded.deviceNames).contains("Name_Of_Device_56787022004B1200"));
+ assertTrue(Arrays.asList(decoded.deviceNames).contains("Name_Of_Device_ABCD6D13004B1200"));
+ }
+
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command17Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command17Test.java
new file mode 100644
index 00000000000..95cc64c623c
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command17Test.java
@@ -0,0 +1,63 @@
+/**
+ * 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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.openhab.binding.linktap.protocol.frames.SetDeviceConfigReq.CONFIG_VOLUME_LIMIT;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_PAUSE_WATER_PLAN;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_SET_CONFIGURATION;
+
+/**
+ * Command 17: Set Device Configuration parameter
+ * Flow 1 --> App->Broker->GW: Sets the given device configuration parameter
+ */
+@NonNullByDefault
+public class Command17Test {
+
+ /**
+ * Command 17: Set Device Configuration parameter
+ * Flow 1 --> App->Broker->GW: Sets the given device configuration parameter
+ */
+ @Test
+ public void RequestConfigUpdateGenerationTest() {
+ SetDeviceConfigReq req = new SetDeviceConfigReq();
+ req.gatewayId = "CCCCDDDDEEEEFFFF";
+ req.deviceId = "1111222233334444";
+ req.command = CMD_SET_CONFIGURATION;
+ req.tag = CONFIG_VOLUME_LIMIT;
+ req.value = 123;
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(req);
+
+ assertEquals("{\"value\":123,\"tag\":\"volume_limit\",\"dev_id\":\"1111222233334444\",\"cmd\":17,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+ /**
+ * Command 17: Set Device Configuration parameter
+ * Flow 1 --> App->Broker->GW: Sets the given device configuration parameter
+ */
+ @Test
+ public void RequestConfigUpdateResponseDecoding() {
+ final EndpointDeviceResponse decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":17, \"gw_id\":\"CCCCDDDDEEEEFFFF\", \"dev_id\":\"1122334455667788\", \"ret\":0\n}",EndpointDeviceResponse.class);
+ assertEquals(CMD_SET_CONFIGURATION,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ assertEquals("1122334455667788",decoded.deviceId );
+ assertEquals(GatewayDeviceResponse.ResultStatus.RET_SUCCESS,decoded.getRes());
+ }
+
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command18Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command18Test.java
new file mode 100644
index 00000000000..861e83addbd
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command18Test.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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_PAUSE_WATER_PLAN;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_WIRELESS_CHECK;
+
+/**
+ * Command 18: Pause watering plan for given duration
+ * Flow 1 --> App->Broker->GW: Pause watering plan for given duration 0.1 -> 240 hours
+ */
+@NonNullByDefault
+public class Command18Test {
+
+ /**
+ * Command 18: Pause watering plan for given duration
+ * Flow 1 --> App->Broker->GW: Pause watering plan for given duration 0.1 -> 240 hours
+ */
+ @Test
+ public void RequestWateringPlanPauseGenerationTest() {
+ PauseWateringPlanReq req = new PauseWateringPlanReq();
+ req.gatewayId = "CCCCDDDDEEEEFFFF";
+ req.deviceId = "1111222233334444";
+ req.command = CMD_PAUSE_WATER_PLAN;
+ req.duration = 12d;
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(req);
+
+ assertEquals("{\"duration\":12.0,\"dev_id\":\"1111222233334444\",\"cmd\":18,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+ /**
+ * Command 18: Pause watering plan for given duration
+ * Flow 1 --> App->Broker->GW: Pause watering plan for given duration 0.1 -> 240 hours
+ */
+ @Test
+ public void RequestWateringPlanPauseResponseDecoding() {
+ final EndpointDeviceResponse decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":18, \"gw_id\":\"CCCCDDDDEEEEFFFF\", \"dev_id\":\"1122334455667788\", \"ret\":0\n}",EndpointDeviceResponse.class);
+ assertEquals(CMD_PAUSE_WATER_PLAN,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ assertEquals("1122334455667788",decoded.deviceId );
+ assertEquals(GatewayDeviceResponse.ResultStatus.RET_SUCCESS,decoded.getRes());
+ }
+
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command1Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command1Test.java
new file mode 100644
index 00000000000..5fafb07004c
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command1Test.java
@@ -0,0 +1,63 @@
+/**
+ * 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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+import java.util.Arrays;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_ADD_END_DEVICE;
+
+/**
+ * Command 1: Add / Register Endpoint Device to Gateway
+ * Flow 1 --> App->Broker->GW: Add specified water timer to gateway
+ */
+@NonNullByDefault
+public class Command1Test {
+
+ /**
+ * Command 1:
+ * Flow 1 --> App->Broker->GW: Add specified water timer to gateway encoding test
+ */
+ @Test
+ public void AddDeviceRequestEncoding() {
+ final GatewayEndDevListReq req = new GatewayEndDevListReq();
+ req.command = CMD_ADD_END_DEVICE;
+ req.gatewayId = "CCCCDDDDEEEEFFFF";
+ req.endDevices = new String[] {"11112222333344448888","77778889333366661111"};
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(req);
+
+ assertEquals("{\"end_dev\":[\"11112222333344448888\",\"77778889333366661111\"],\"cmd\":1,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+ /**
+ * Command 1:
+ * Flow 1 --> App->Broker->GW: Add specified water timer to gateway response decoding test
+ */
+ @Test
+ public void AddDeviceRequestResponseDecoding() {
+ final GatewayDeviceResponse decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":1, \"gw_id\":\"CCCCDDDDEEEEFFFF\", \"ret\":0\n" +
+ "}",GatewayDeviceResponse.class);
+
+ assertEquals(CMD_ADD_END_DEVICE,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ assertEquals(GatewayDeviceResponse.ResultStatus.RET_SUCCESS,decoded.getRes());
+ }
+
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command2Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command2Test.java
new file mode 100644
index 00000000000..63ee1df156d
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command2Test.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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_REMOVE_END_DEVICE;
+
+/**
+ * Command 2: Delete / Unregister Endpoint Device to Gateway
+ * Flow 1 --> App->Broker->GW: Delete specified water timer to gateway
+ */
+@NonNullByDefault
+public class Command2Test {
+
+ /**
+ * Command 1:
+ * Flow 1 --> App->Broker->GW: Delete specified water timer to gateway encoding test
+ */
+ @Test
+ public void DeleteDeviceRequestEncoding() {
+ final GatewayEndDevListReq req = new GatewayEndDevListReq();
+ req.command = CMD_REMOVE_END_DEVICE;
+ req.gatewayId = "CCCCDDDDEEEEFFFF";
+ req.endDevices = new String[] {"1111222233334444","7777888933336666"};
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(req);
+
+ assertEquals("{\"end_dev\":[\"1111222233334444\",\"7777888933336666\"],\"cmd\":2,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+ /**
+ * Command 2:
+ * Flow 2 --> App->Broker->GW: Delete specified water timer to gateway response decoding test
+ */
+ @Test
+ public void DeleteDeviceRequestResponseDecoding() {
+ final GatewayDeviceResponse decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":2, \"gw_id\":\"CCCCDDDDEEEEFFFF\", \"ret\":0\n}",GatewayDeviceResponse.class);
+
+ assertEquals(CMD_REMOVE_END_DEVICE,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ assertEquals(GatewayDeviceResponse.ResultStatus.RET_SUCCESS,decoded.getRes());
+ }
+
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command3Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command3Test.java
new file mode 100644
index 00000000000..2937e51b665
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command3Test.java
@@ -0,0 +1,136 @@
+/**
+ * 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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_UPDATE_WATER_TIMER_STATUS;
+
+/**
+ * Command 3: Water Timer Status Update Notification
+ * Flow 1 --> GW->Broker->App: Notification that there is a update to one or more water timer's
+ * Default format --> object's within an array
+ * Optional format --> object not wrapped within an array
+ * Flow 2 --> App->Broker->GW: Request Water Timer Status
+ *
+ * (ret is only provided in case of an error so -1 would be the same as if 0 was provided)
+ */
+@NonNullByDefault
+public class Command3Test {
+
+ /**
+ * Command 3: Water Timer Status Update Notification
+ * Flow 1 --> GW->Broker->App: Notification that there is a update to one or more water timer's
+ * Default format --> object's within an array
+ * Optional format --> object not wrapped within an array
+ * Flow 2 --> App->Broker->GW: Request Water Timer Status
+ */
+ @Test
+ public void NotificationTimerUpdateRequest1Decoding() {
+ final WaterMeterStatus decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":3, \"gw_id\":\"CCCCDDDDEEEEFFFF\", \"dev_stat\": [ { \"dev_id\":\"1111222233334444\", \"plan_mode\":2, \"plan_sn\":3134, \"is_rf_linked\":true, \"is_flm_plugin\":false, \"is_fall\":false, \"is_broken\":false, \"is_cutoff\":false, \"is_leak\":false, \"is_clog\":false, \"signal\":100, \"battery\":0, \"child_lock\":0, \"is_manual_mode\":false, \"is_watering\":false, \"is_final\":true, \"total_duration\":0, \"remain_duration\":0, \"speed\":0, \"volume\":0\n} ]\n}",WaterMeterStatus.class);
+
+ assertEquals(CMD_UPDATE_WATER_TIMER_STATUS,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ assertNotNull(decoded.deviceStatuses);
+ assertEquals(1,decoded.deviceStatuses.size());
+ assertEquals("1111222233334444",decoded.deviceStatuses.get(0).deviceId);
+ assertEquals(2,decoded.deviceStatuses.get(0).planMode);
+ assertEquals(3134,decoded.deviceStatuses.get(0).planSerialNo);
+ assertTrue(decoded.deviceStatuses.get(0).isRfLinked);
+ assertFalse(decoded.deviceStatuses.get(0).isFlmPlugin);
+ assertFalse(decoded.deviceStatuses.get(0).isBroken);
+ assertFalse(decoded.deviceStatuses.get(0).isCutoff);
+ assertFalse(decoded.deviceStatuses.get(0).isLeak);
+ assertFalse(decoded.deviceStatuses.get(0).isClog);
+ assertEquals(100,decoded.deviceStatuses.get(0).signal);
+ assertEquals(0,decoded.deviceStatuses.get(0).battery);
+ assertEquals(0,decoded.deviceStatuses.get(0).childLock);
+ assertFalse(decoded.deviceStatuses.get(0).isManualMode);
+ assertFalse(decoded.deviceStatuses.get(0).isWatering);
+ assertTrue(decoded.deviceStatuses.get(0).isFinal);
+ assertEquals(0,decoded.deviceStatuses.get(0).totalDuration);
+ assertEquals(0,decoded.deviceStatuses.get(0).remainDuration);
+ assertEquals(0,decoded.deviceStatuses.get(0).speed);
+ assertEquals(0,decoded.deviceStatuses.get(0).volume);
+ assertEquals(GatewayDeviceResponse.ResultStatus.RET_SUCCESS,decoded.getRes()); // Only given in case of error
+ assertEquals(GatewayDeviceResponse.ResultStatus.RET_SUCCESS,decoded.getRes());
+ }
+
+ /**
+ * Command 3: Water Timer Status Update Notification
+ * Flow 1 --> GW->Broker->App: Notification that there is a update to one water timer
+ * Optional format --> object's without array wrapper
+ */
+ @Test
+ public void NotificationTimerUpdateRequest2Decoding() {
+ final WaterMeterStatus decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":3, \"gw_id\":\"CCCCDDDDEEEEFFFF\", \"dev_stat\": { \"dev_id\":\"1111222233334444\", \"plan_mode\":2, \"plan_sn\":3134, \"is_rf_linked\":true, \"is_flm_plugin\":false, \"is_fall\":false, \"is_broken\":false, \"is_cutoff\":false, \"is_leak\":false, \"is_clog\":false, \"signal\":100, \"battery\":0, \"child_lock\":0, \"is_manual_mode\":false, \"is_watering\":false, \"is_final\":true, \"total_duration\":0, \"remain_duration\":0, \"speed\":0, \"volume\":0\n}\n}",WaterMeterStatus.class);
+
+ assertEquals(CMD_UPDATE_WATER_TIMER_STATUS,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ assertEquals(1,decoded.deviceStatuses.size());
+ assertEquals("1111222233334444",decoded.deviceStatuses.get(0).deviceId);
+ assertEquals(2,decoded.deviceStatuses.get(0).planMode);
+ assertEquals(3134,decoded.deviceStatuses.get(0).planSerialNo);
+ assertTrue(decoded.deviceStatuses.get(0).isRfLinked);
+ assertFalse(decoded.deviceStatuses.get(0).isFlmPlugin);
+ assertFalse(decoded.deviceStatuses.get(0).isBroken);
+ assertFalse(decoded.deviceStatuses.get(0).isCutoff);
+ assertFalse(decoded.deviceStatuses.get(0).isLeak);
+ assertFalse(decoded.deviceStatuses.get(0).isClog);
+ assertEquals(100,decoded.deviceStatuses.get(0).signal);
+ assertEquals(0,decoded.deviceStatuses.get(0).battery);
+ assertEquals(0,decoded.deviceStatuses.get(0).childLock);
+ assertFalse(decoded.deviceStatuses.get(0).isManualMode);
+ assertFalse(decoded.deviceStatuses.get(0).isWatering);
+ assertTrue(decoded.deviceStatuses.get(0).isFinal);
+ assertEquals(0,decoded.deviceStatuses.get(0).totalDuration);
+ assertEquals(0,decoded.deviceStatuses.get(0).remainDuration);
+ assertEquals(0,decoded.deviceStatuses.get(0).speed);
+ assertEquals(0,decoded.deviceStatuses.get(0).volume);
+ assertEquals(GatewayDeviceResponse.ResultStatus.RET_SUCCESS,decoded.getRes()); // Only given in case of error
+ assertEquals(GatewayDeviceResponse.ResultStatus.RET_SUCCESS,decoded.getRes());
+ }
+
+ /**
+ * Command 3: Water Timer Status Update Notification
+ * Flow 2 --> App->Broker->GW: Request Water Timer Status
+ */
+ @Test
+ public void RequestWaterMeterStatusGenerationTest() {
+ DeviceCmdReq req = new DeviceCmdReq();
+ req.gatewayId = "CCCCDDDDEEEEFFFF";
+ req.deviceId = "1111222233334444";
+ req.command = CMD_UPDATE_WATER_TIMER_STATUS;
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(req);
+
+ assertEquals("{\"dev_id\":\"1111222233334444\",\"cmd\":3,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+ /**
+ * Command 3: Water Timer Status Update Notification
+ * Flow 2 --> App->Broker->GW: Request Water Timer Status
+ */
+ @Test
+ public void RequestWaterMeterStatusErrorResponseDecoding() {
+ final WaterMeterStatus decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":3, \"gw_id\":\"CCCCDDDDEEEEFFFF\", \"ret\":5\n}",WaterMeterStatus.class);
+
+ assertEquals(CMD_UPDATE_WATER_TIMER_STATUS,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ assertEquals(GatewayDeviceResponse.ResultStatus.RET_DEVICE_NOT_FOUND,decoded.getRes());
+ }
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command5Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command5Test.java
new file mode 100644
index 00000000000..eacc35b697c
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command5Test.java
@@ -0,0 +1,60 @@
+/**
+ * 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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_REMOVE_WATER_PLAN;
+
+/**
+ * Command 5: Delete Watering Plan from Device Endpoint
+ * Flow 1 --> App->Broker->GW: Delete existing watering plan from device
+ */
+@NonNullByDefault
+public class Command5Test {
+
+ /**
+ * Command 5:
+ * Flow 1 --> App->Broker->GW: Delete existing watering plan from device encoding test
+ */
+ @Test
+ public void DeleteWateringPlanRequestEncoding() {
+ final DeviceCmdReq req = new DeviceCmdReq();
+ req.command = CMD_REMOVE_WATER_PLAN;
+ req.gatewayId = "CCCCDDDDEEEEFFFF";
+ req.deviceId = "1111222233334444";
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(req);
+
+ assertEquals("{\"dev_id\":\"1111222233334444\",\"cmd\":5,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+ /**
+ * Command 5:
+ * Flow 1 --> App->Broker->GW: Delete existing watering plan from device response decoding test
+ */
+ @Test
+ public void DeleteWateringPlanRequestResponseDecoding() {
+ final EndpointDeviceResponse decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":5, \"gw_id\":\"CCCCDDDDEEEEFFFF\", \"dev_id\":\"1111222233334444\", \"ret\":0\n}",EndpointDeviceResponse.class);
+
+ assertEquals(CMD_REMOVE_WATER_PLAN,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ assertEquals("1111222233334444",decoded.deviceId);
+ assertEquals(GatewayDeviceResponse.ResultStatus.RET_SUCCESS,decoded.getRes());
+ }
+
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command6Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command6Test.java
new file mode 100644
index 00000000000..acd15f6d394
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command6Test.java
@@ -0,0 +1,63 @@
+/**
+ * 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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_IMMEDIATE_WATER_START;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_IMMEDIATE_WATER_STOP;
+
+/**
+ * Command 6: Start watering immediately
+ * Flow 1 --> App->Broker->GW: Start watering immediately, once time only for the given parameters
+ */
+@NonNullByDefault
+public class Command6Test {
+
+ /**
+ * Command 6:
+ * Flow 1 --> App->Broker->GW: Start watering immediately, once time only for the given parameters
+ */
+ @Test
+ public void StartWateringRequestEncoding() {
+ final StartWateringReq req = new StartWateringReq();
+ req.command = CMD_IMMEDIATE_WATER_START;
+ req.gatewayId = "CCCCDDDDEEEEFFFF";
+ req.deviceId = "1111222233334444";
+ req.duration = 60;
+ req.volume = 0;
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(req);
+
+ assertEquals("{\"duration\":60,\"volume\":0,\"dev_id\":\"1111222233334444\",\"cmd\":6,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+ /**
+ * Command 6:
+ * Flow 1 --> App->Broker->GW: Start watering immediately, once time only for the given parameters
+ */
+ @Test
+ public void StartWateringRequestResponseDecoding() {
+ final EndpointDeviceResponse decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":6, \"gw_id\":\"CCCCDDDDEEEEFFFF\", \"dev_id\":\"1111222233334444\", \"ret\":0\n}",EndpointDeviceResponse.class);
+
+ assertEquals(CMD_IMMEDIATE_WATER_START,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ assertEquals("1111222233334444",decoded.deviceId);
+ assertEquals(GatewayDeviceResponse.ResultStatus.RET_SUCCESS,decoded.getRes());
+ }
+
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command7Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command7Test.java
new file mode 100644
index 00000000000..2b6fd9a3695
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command7Test.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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_IMMEDIATE_WATER_STOP;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_REMOVE_WATER_PLAN;
+
+/**
+ * Command 7: Stop watering immediately
+ * Flow 1 --> App->Broker->GW: Stop watering immediately, next cycled watering plan will still run
+ */
+@NonNullByDefault
+public class Command7Test {
+
+ /**
+ * Command 7:
+ * Flow 1 --> App->Broker->GW: Stop watering immediately, next cycled watering plan will still run
+ */
+ @Test
+ public void StopWateringRequestEncoding() {
+ final DeviceCmdReq req = new DeviceCmdReq();
+ req.command = CMD_IMMEDIATE_WATER_STOP;
+ req.gatewayId = "CCCCDDDDEEEEFFFF";
+ req.deviceId = "1111222233334444";
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(req);
+
+ assertEquals("{\"dev_id\":\"1111222233334444\",\"cmd\":7,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+ /**
+ * Command 7:
+ * Flow 1 --> App->Broker->GW: Stop watering immediately, next cycled watering plan will still run
+ */
+ @Test
+ public void StopWateringRequestResponseDecoding() {
+ final EndpointDeviceResponse decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":7, \"gw_id\":\"CCCCDDDDEEEEFFFF\", \"dev_id\":\"1111222233334444\", \"ret\":0\n}",EndpointDeviceResponse.class);
+
+ assertEquals(CMD_IMMEDIATE_WATER_STOP,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ assertEquals("1111222233334444",decoded.deviceId);
+ assertEquals(GatewayDeviceResponse.ResultStatus.RET_SUCCESS,decoded.getRes());
+ }
+
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command8Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command8Test.java
new file mode 100644
index 00000000000..7090c535f6b
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command8Test.java
@@ -0,0 +1,96 @@
+/**
+ * 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.linktap.protocol.frames;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_RAINFALL_DATA;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+/**
+ * Command 8: Rain Data
+ * Flow 1 --> GW->Broker->App: Request for rain data
+ * Flow 2 --> App->Broker->GW: Push to update rain data
+ */
+@NonNullByDefault
+public class Command8Test {
+
+ /**
+ * Command 8:
+ * Flow 1 --> GW->Broker->App: Request for Rain Data Decoding Test
+ */
+ @Test
+ public void RainDataRequestDecoding() {
+ final TLGatewayFrame decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":8, \"gw_id\":\"CCCCDDDDEEEEFFFF\"\n" +
+ "}",TLGatewayFrame.class);
+
+ assertEquals(CMD_RAINFALL_DATA,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ }
+
+ /**
+ * Command 8:
+ * Flow 1 --> GW->Broker->App: Response serialisation test for Rain Data reply
+ */
+ @Test
+ public void RainDataRequestResponseGenerationTest() {
+ RainDataForecast forecastReply = new RainDataForecast();
+ forecastReply.command = CMD_RAINFALL_DATA;
+ forecastReply.gatewayId = "CCCCDDDDEEEEFFFF";
+ forecastReply.setPastRainfall(2.5);
+ forecastReply.setFutureRainfall(6.3);
+ forecastReply.validDuration = 60;
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(forecastReply);
+
+ assertEquals("{\"valid_duration\":60,\"rain\":[2.5,6.3],\"cmd\":8,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+ /**
+ * Command 8:
+ * Flow 2 --> App->Broker->GW: Push of Rain Data to Gateway request serialisation
+ */
+ @Test
+ public void RainDataPushGenerationTest() {
+ RainDataForecast req = new RainDataForecast();
+ req.command = CMD_RAINFALL_DATA;
+ req.gatewayId = "CCCCDDDDEEEEFFFF";
+ req.setPastRainfall(2.5);
+ req.setFutureRainfall(6.3);
+ req.validDuration = 60;
+
+ String encoded = LinkTapBindingConstants.GSON.toJson(req);
+
+ assertEquals("{\"valid_duration\":60,\"rain\":[2.5,6.3],\"cmd\":8,\"gw_id\":\"CCCCDDDDEEEEFFFF\"}",
+ encoded);
+ }
+
+ /**
+ * Command 8:
+ * Flow 2 --> App->Broker->GW: Response decoding test for Rain Data Push reply
+ */
+ @Test
+ public void RainDataPushResponseDecoding() {
+ final GatewayDeviceResponse decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":8, \"gw_id\":\"CCCCDDDDEEEEFFFF\", \"ret\":0\n" +
+ "}",GatewayDeviceResponse.class);
+
+ assertEquals(CMD_RAINFALL_DATA,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ assertEquals(GatewayDeviceResponse.ResultStatus.RET_SUCCESS,decoded.getRes());
+ }
+
+
+}
diff --git a/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command9Test.java b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command9Test.java
new file mode 100644
index 00000000000..2051ad4ba57
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command9Test.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.linktap.protocol.frames;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.CMD_NOTIFICATION_WATERING_SKIPPED;
+
+/**
+ * Command 9: Watering Skipped Notification
+ * Flow 1 --> App->Broker->GW: Notification that watering was skipped with the rainfall data
+ */
+@NonNullByDefault
+public class Command9Test {
+
+ /**
+ * Command 9:
+ * Flow 1 --> GW->Broker->App: Notification that watering was skipped with the rainfall data
+ */
+ @Test
+ public void NotificationWateringSkippedRequestDecoding() {
+ final WateringSkippedNotification decoded = LinkTapBindingConstants.GSON.fromJson("{ \"cmd\":9, \"gw_id\":\"CCCCDDDDEEEEFFFF\",\n\"dev_id\":\"1111222233334444\", \"rain\":[2.5,6.3]\n}",WateringSkippedNotification.class);
+
+ assertNotNull(decoded);
+ assertEquals(CMD_NOTIFICATION_WATERING_SKIPPED,decoded.command);
+ assertEquals("CCCCDDDDEEEEFFFF",decoded.gatewayId );
+ assertEquals("1111222233334444", decoded.deviceId);
+ assertEquals(2, decoded.rainfallData.length);
+ assertEquals(2.5,decoded.rainfallData[0]);
+ assertEquals(6.3,decoded.rainfallData[1]);
+ }
+}
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 293021091b5..f8a7c1b0e3c 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -229,6 +229,7 @@
org.openhab.binding.lifxorg.openhab.binding.linkyorg.openhab.binding.linuxinput
+ org.openhab.binding.linktaporg.openhab.binding.liquidcheckorg.openhab.binding.lircorg.openhab.binding.livisismarthome