From e8799ee5bd3a83447d1b510bc97808faf5125e65 Mon Sep 17 00:00:00 2001
From: dag81
Date: Tue, 4 Jun 2024 18:37:48 +0100
Subject: [PATCH] [linkTap] Initial Code Commit
[linkTap] Initial code commit.
Signed-off-by: dag81
---
bundles/org.openhab.binding.linktap/NOTICE | 13 +
bundles/org.openhab.binding.linktap/README.md | 201 ++++++
.../Saved/LinkTapHandler.java | 559 ++++++++++++++
bundles/org.openhab.binding.linktap/pom.xml | 36 +
.../src/main/feature/feature.xml | 10 +
.../LinkTapBridgeConfiguration.java | 28 +
.../LinkTapDeviceConfiguration.java | 27 +
.../linktap/internal/BridgeManager.java | 81 +++
.../DeviceMetaDataUpdatedHandler.java | 25 +
.../binding/linktap/internal/IBridgeData.java | 23 +
.../internal/LinkTapBindingConstants.java | 200 +++++
.../LinkTapBridgeDiscoveryService.java | 248 +++++++
.../internal/LinkTapBridgeHandler.java | 496 +++++++++++++
.../internal/LinkTapDeviceConfiguration.java | 43 ++
.../LinkTapDeviceDiscoveryService.java | 103 +++
.../internal/LinkTapDeviceMetadata.java | 40 +
.../linktap/internal/LinkTapHandler.java | 429 +++++++++++
.../internal/LinkTapHandlerFactory.java | 89 +++
.../linktap/internal/LookupWrapper.java | 90 +++
.../internal/PollingDeviceHandler.java | 334 +++++++++
.../internal/TransactionProcessor.java | 315 ++++++++
.../binding/linktap/internal/Utils.java | 39 +
.../protocol/frames/AlertStateReq.java | 52 ++
.../linktap/protocol/frames/DeviceCmdReq.java | 56 ++
.../protocol/frames/DismissAlertReq.java | 85 +++
.../frames/EndpointDeviceResponse.java | 51 ++
.../protocol/frames/GatewayConfigResp.java | 73 ++
.../frames/GatewayDeviceResponse.java | 176 +++++
.../protocol/frames/GatewayEndDevListReq.java | 66 ++
.../linktap/protocol/frames/HandshakeReq.java | 52 ++
.../protocol/frames/HandshakeResp.java | 91 +++
.../protocol/frames/IPayloadValidator.java | 26 +
.../linktap/protocol/frames/LockReq.java | 69 ++
.../protocol/frames/PauseWateringPlanReq.java | 51 ++
.../linktap/protocol/frames/RainData.java | 66 ++
.../protocol/frames/RainDataForecast.java | 51 ++
.../protocol/frames/SetDeviceConfigReq.java | 77 ++
.../protocol/frames/SetupWaterPlan.java | 148 ++++
.../protocol/frames/StartWateringReq.java | 67 ++
.../protocol/frames/TLGatewayFrame.java | 251 +++++++
.../linktap/protocol/frames/TimeDataResp.java | 32 +
.../protocol/frames/WaterMeterStatus.java | 275 +++++++
.../frames/WateringSkippedNotification.java | 65 ++
.../protocol/frames/WirelessTestResp.java | 72 ++
.../http/CommandNotSupportedException.java | 50 ++
.../protocol/http/DeviceIdException.java | 53 ++
.../protocol/http/GatewayIdException.java | 53 ++
.../http/InvalidParameterException.java | 50 ++
.../http/NotTapLinkGatewayException.java | 51 ++
.../TransientCommunicationIssueException.java | 49 ++
.../linktap/protocol/http/WebServerApi.java | 501 +++++++++++++
.../protocol/servers/BindingServlet.java | 237 ++++++
.../protocol/servers/IHttpClientProvider.java | 26 +
.../src/main/resources/OH-INF/addon/addon.xml | 28 +
.../resources/OH-INF/i18n/linktap.properties | 3 +
.../resources/OH-INF/thing/channel-types.xml | 223 ++++++
.../resources/OH-INF/thing/thing-types.xml | 108 +++
.../linktap/protocol/frames/Command0Test.java | 71 ++
.../protocol/frames/Command10Test.java | 64 ++
.../protocol/frames/Command11Test.java | 63 ++
.../protocol/frames/Command12Test.java | 62 ++
.../protocol/frames/Command13Test.java | 59 ++
.../protocol/frames/Command14Test.java | 60 ++
.../protocol/frames/Command15Test.java | 64 ++
.../protocol/frames/Command16Test.java | 71 ++
.../protocol/frames/Command17Test.java | 63 ++
.../protocol/frames/Command18Test.java | 62 ++
.../linktap/protocol/frames/Command1Test.java | 63 ++
.../linktap/protocol/frames/Command2Test.java | 59 ++
.../linktap/protocol/frames/Command3Test.java | 136 ++++
.../linktap/protocol/frames/Command5Test.java | 60 ++
.../linktap/protocol/frames/Command6Test.java | 63 ++
.../linktap/protocol/frames/Command7Test.java | 61 ++
.../linktap/protocol/frames/Command8Test.java | 96 +++
.../linktap/protocol/frames/Command9Test.java | 46 ++
bundles/pom.xml | 681 ++++++++++++++++++
76 files changed, 8717 insertions(+)
create mode 100644 bundles/org.openhab.binding.linktap/NOTICE
create mode 100644 bundles/org.openhab.binding.linktap/README.md
create mode 100644 bundles/org.openhab.binding.linktap/Saved/LinkTapHandler.java
create mode 100644 bundles/org.openhab.binding.linktap/pom.xml
create mode 100644 bundles/org.openhab.binding.linktap/src/main/feature/feature.xml
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/configuration/LinkTapBridgeConfiguration.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/configuration/LinkTapDeviceConfiguration.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/internal/BridgeManager.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/internal/DeviceMetaDataUpdatedHandler.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/internal/IBridgeData.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/internal/LinkTapBindingConstants.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/internal/LinkTapBridgeDiscoveryService.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/internal/LinkTapBridgeHandler.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/internal/LinkTapDeviceConfiguration.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/internal/LinkTapDeviceDiscoveryService.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/internal/LinkTapDeviceMetadata.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/internal/LinkTapHandler.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/internal/LinkTapHandlerFactory.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/internal/LookupWrapper.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/internal/PollingDeviceHandler.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/internal/TransactionProcessor.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/internal/Utils.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/AlertStateReq.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/DeviceCmdReq.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/DismissAlertReq.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/EndpointDeviceResponse.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/GatewayConfigResp.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/GatewayDeviceResponse.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/GatewayEndDevListReq.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/HandshakeReq.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/HandshakeResp.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/IPayloadValidator.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/LockReq.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/PauseWateringPlanReq.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/RainData.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/RainDataForecast.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/SetDeviceConfigReq.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/SetupWaterPlan.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/StartWateringReq.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/TLGatewayFrame.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/TimeDataResp.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/WaterMeterStatus.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/WateringSkippedNotification.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/WirelessTestResp.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/CommandNotSupportedException.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/DeviceIdException.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/GatewayIdException.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/InvalidParameterException.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/NotTapLinkGatewayException.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/TransientCommunicationIssueException.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/WebServerApi.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/servers/BindingServlet.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/servers/IHttpClientProvider.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/addon/addon.xml
create mode 100644 bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/i18n/linktap.properties
create mode 100644 bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/thing/channel-types.xml
create mode 100644 bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/thing/thing-types.xml
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command0Test.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command10Test.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command11Test.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command12Test.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command13Test.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command14Test.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command15Test.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command16Test.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command17Test.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command18Test.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command1Test.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command2Test.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command3Test.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command5Test.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command6Test.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command7Test.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command8Test.java
create mode 100644 bundles/org.openhab.binding.linktap/src/main/tests/org/openhab/binding/linktap/protocol/frames/Command9Test.java
diff --git a/bundles/org.openhab.binding.linktap/NOTICE b/bundles/org.openhab.binding.linktap/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.linktap/README.md b/bundles/org.openhab.binding.linktap/README.md
new file mode 100644
index 0000000000000..d4ecb642f0239
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/README.md
@@ -0,0 +1,201 @@
+# LinkTap Binding
+
+This binding is for [LinkTap](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 syncrhonisation 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, in order for openHAB to connect to the system, as a bridge.
+
+## Connection options
+
+LinkTap supports MQTT and a direct interaction via HTTP.
+This binding directly interact's with LinkTap's bridges using the Local HTTP API (HTTP).
+The binding connects to the bridge'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 behaviour the Gateway should be able to
+connect to OpenHab on an HTTP port from 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)**.
+
+## Supported Things
+
+This binding supports the follow thing types:
+
+| Thing | Thing Type | Thing Type UID | Discovery | Description |
+|----------------|------------|----------------|--------------------|------------------------------------------|
+| Bridge | Bridge | linkTapBridge | Manual / Automatic | A connection to a LinkTap Gateway device |
+| LinkTap Device | Thing | linkTapDevice | Automatic | A end device such as 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
+
+### Bridge configuration parameters
+
+| 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 | ON | No | Yes |
+
+**NOTE** When enableMDNS is enabled, upon connection to the gateway option "Enable mDNS responder" is switched on
+
+### LinkTap Device configuration parameters
+
+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
+
+| Name | Type | Description | Recommended Values | Required | Advanced |
+|--------------|--------|-----------------------------------------------------------------------|--------------------|----------|----------|
+| deviceId | String | The Device Id for the device under the gateway | | Yes | No |
+| deviceName | String | The name allocated to the device by the app. (Must be unique if used) | | Yes | No |
+| enableAlerts | Switch | On connection whether the device should be configured to send alerts | ON | No | Yes |
+
+**NOTE**
+
+- a **deviceId** or a **deviceName must be provided** to communicate with the device
+- **enableAlerts** allows the binding to **receive updates of Water Cut, and other alerts** conditions are detected for a LinkTap device.
+
+## Thing Configuration
+
+_Describe what is needed to manually configure a thing, either through the UI or via a thing-file._
+_This should be mainly about its mandatory and optional configuration parameters._
+
+_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._
+
+### `sample` Thing Configuration
+
+| Name | Type | Description | Default | Required | Advanced |
+|-----------------|---------|---------------------------------------|---------|----------|----------|
+| host | text | Hostname or IP address of the device | N/A | yes | no |
+| password | text | Password to access the device | N/A | yes | no |
+| refreshInterval | integer | Interval the device is polled in sec. | 600 | no | yes |
+
+## Channels
+
+There are 3 different Area of channels:
+
+- RO Data (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
+- WR Data (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, 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
+
+
+| Name | Type | Description | Representation | Read/Write | Write Action |
+|-------------------|---------------------------|---------------------------------------------------------------------------|----------------|------------|------------------------------------------------------------------------------------------------------------------------------|
+| waterCut | Switch | Water cut-off alert | Alert | Write | Dismiss alert |
+| shutdownFailure | Switch | The device has failed to close the valve | Alert | Write | Dismiss alert |
+| highFlow | Switch | Unusually high flow rate detected alert | Alert | Write | Dismiss alert |
+| lowFlow | Switch | Unusually low flow rate detected alert | Alert | Write | Dismiss alert |
+| fallStatus | Switch | The device has fallen | Alert | Write | Dismiss alert |
+| flmLinked | Switch | The device has a included flow meter | RO Data | Read | |
+| rfLinked | Switch | Is the device RF linked | RO Data | Read | |
+| signal | Number:Dimensionless | Reception Signal Strength | RO Data | Read | |
+| battery | Number:Dimensionless | Battery Remaining Level | RO Data | Read | |
+| flowRate | Number:VolumetricFlowRate | Current water flow rate | RO Data | Read | |
+| volume | Number:Volume | Accumulated volume of current watering cycle | RO Data | Read | |
+| ecoFinal | Switch | In ECO mode this is true when the final ON watering on segment is running | RO Data | Read | |
+| remaining | Number:Time | Remaining duration of the current watering cycle | RO Data | Read | |
+| duration | Number:Time | Total duration of current watering cycle | RO Data | Read | |
+| activeWatering | Switch | Active watering status | RW Data | Write | True - Start immediate watering, False - Stops the current watering process, the next planned watering will run as scheduled |
+| manualWatering | Switch | Manual watering mode status | RW Data | Write | False - Stops the current watering process, the next planned watering will run as scheduled |
+| childLock | Text | The child lock mode | RW Data | Write | Unlocked - Button enabled, Partially locked -> 3 second push required, Completely locked -> Button disabled |
+| ohDurLimit | Number:Time | Max duration allowed for the immediate watering | W Data | Write | Max Time duration for "Start immediate watering" |
+| ohVolLimit | Number:Volume | Max Volume limit for immediate watering | W Data | Write | Max Volume for "Start immediate watering" |
+| waterSkipDateTime | DateTime | Time when watering was skipped | RO Data | Read | |
+| waterSkipPrev | Number:Length | Previous rainfall calculated when watering was skipped | RO Data | Read | |
+| waterSkipNext | Number:Length | Future rainfall calculated when watering was skipped | RO Data | Read | |
+
+## Full Example
+
+_Provide a full usage example based on textual configuration files._
+_*.things, *.items examples are mandatory as textual configuration is well used by many users._
+_*.sitemap examples are optional._
+
+### Thing Configuration
+
+- **Gateway Model**: GW_02
+- **Device Model**: Q1
+
+```java
+Bridge linktap:bridge:home "LinkTap GW02" [ host="192.168.0.21", enableMDNS=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:rfLinked"}
+Switch Tap1FlmLinked "Tap 1 - FLM Linked" ["Point"] { channel="linktap:device:home:tapValve1:flmLinked"}
+Switch Tap1WaterCutAlert "Tap 1 - Water Cut Alert" ["Point"] { channel="linktap:device:home:tapValve1:waterCut" }
+Switch Tap1WaterFallAlert "Tap 1 - Fallen Alert" ["Point"] { channel="linktap:device:home:tapValve1:fallStatus" }
+Switch Tap1WaterValveAlert "Tap 1 - Shutdown Failure Alert" ["Point"] { channel="linktap:device:home:tapValve1:shutdownFailure" }
+Switch Tap1WaterLowFlowAlert "Tap 1 - Low Flow Alert" ["Point"] { channel="linktap:device:home:tapValve1:lowFlow" }
+Switch Tap1WaterHighFlowAlert "Tap 1 - High Flow Alert" ["Point"] { channel="linktap:device:home:tapValve1:highFlow" }
+String Tap1ChildLockMode "Tap 1 - Child Lock Mode" ["Point"] { channel="linktap:device:home:tapValve1:childLock" }
+Number:VolumetricFlowRate Tap1FlowRate "Tap 1 - Flow Rate" ["Point"] { channel="linktap:device:home:tapValve1:flowRate",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:ecoFinal" }
+Switch Tap1Watering "Tap 1 - Watering" ["Point"] { channel="linktap:device:home:tapValve1:activeWatering" }
+Switch Tap1ManualWatering "Tap 1 - Manual Watering" ["Point"] { channel="linktap:device:home:tapValve1:manualWatering" }
+String Tap1WateringkMode "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 0000000000000..2988b40417624
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/TimeDataResp.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.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() {
+ }
+
+ public String isValid() {
+ return super.isValid();
+ }
+}
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 0000000000000..68839ec315d9e
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/WaterMeterStatus.java
@@ -0,0 +1,275 @@
+/**
+ * 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.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 String isValid() {
+ final Integer planModeRaw = planMode;
+ if (planModeRaw == null || planModeRaw < 1 || planModeRaw > OP_MODE_DESC.length) {
+ return "planMode not in range 1 -> " + OP_MODE_DESC.length;
+ }
+
+ final Integer signalRaw = signal;
+ if (signalRaw == null || signalRaw < 0 || signalRaw > 100) {
+ return "signal not in range 0 -> 100";
+ }
+ final Integer batteryRaw = battery;
+ if (batteryRaw == null || batteryRaw < 0 || batteryRaw > 100) {
+ return "battery not in range 0 -> 100";
+ }
+ if (planSerialNo == DEFAULT_INT) {
+ return "planSerialNo invalid";
+ }
+ final Integer childLockRaw = childLock;
+ if (childLockRaw == null || childLockRaw < LockReq.LOCK_UNLOCKED || childLockRaw > LockReq.LOCK_FULL) {
+ return "childLock not in range " + LockReq.LOCK_UNLOCKED + " -> " + LockReq.LOCK_FULL;
+ }
+ if (!DEVICE_ID_PATTERN.matcher(deviceId).matches()) {
+ return "deviceId invalid";
+ }
+
+ return EMPTY_STRING;
+ }
+ }
+
+ public String isValid() {
+ return super.isValid();
+ }
+
+ 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 0000000000000..62e6bddcd3011
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/frames/WateringSkippedNotification.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.linktap.protocol.frames;
+
+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 String isValid() {
+ final String superRst = super.isValid();
+ if (!superRst.isEmpty()) {
+ return superRst;
+ }
+ if (rainfallData.length != 2) {
+ return "rainfallData invalid length";
+ }
+
+ return EMPTY_STRING;
+ }
+}
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 0000000000000..7815801966d8a
--- /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 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 String isValid() {
+ final String superRst = super.isValid();
+ if (!superRst.isEmpty()) {
+ return superRst;
+ }
+ if (pingCount == DEFAULT_INT) {
+ return "pingCount invalid";
+ }
+
+ if (pongCount == DEFAULT_INT) {
+ return "pongCount invalid";
+ }
+
+ if (!DEVICE_ID_PATTERN.matcher(deviceId).matches()) {
+ return "DeviceId invalid";
+ }
+
+ return EMPTY_STRING;
+ }
+}
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 0000000000000..9c26cf4ec4aa5
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/CommandNotSupportedException.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.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 Exception {
+ @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());
+ }
+}
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 0000000000000..cd7475f737f85
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/DeviceIdException.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;
+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 Exception {
+ @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());
+ }
+}
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 0000000000000..04a2097d77725
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/GatewayIdException.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;
+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 Exception {
+ @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());
+ }
+}
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 0000000000000..ad502b535cd43
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/InvalidParameterException.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.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 Exception {
+ @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());
+ }
+}
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 0000000000000..52f30fa90e784
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/NotTapLinkGatewayException.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.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 Exception {
+ @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 static final String HEADERS_MISSING = "Missing header markers";
+ public static final String MISSING_API_TITLE = "Not a LinkTap API response";
+ public static final String MISSING_SERVER_TITLE = "Not a LinkTap response";
+ public static final String UNEXPECTED_STATUS_CODE = "Unexpected status code response";
+ public static final String UNEXPECTED_HTTPS = "Unexpected protocol";
+}
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 0000000000000..6fd01b8778436
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/TransientCommunicationIssueException.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.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 Exception {
+ @Serial
+ private static final long serialVersionUID = -7786449325604143287L;
+
+ public TransientCommunicationIssueException() {
+ super();
+ }
+
+ public TransientCommunicationIssueException(final String message) {
+ super(message);
+ }
+
+ public TransientCommunicationIssueException(final Throwable cause) {
+ super(cause);
+ }
+
+ public TransientCommunicationIssueException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ public static final String HOST_UNREACHABLE = "Could not connect";
+ public static final String HOST_NOT_RESOLVED = "Could not resolve IP address";
+ public static final String HOST_COMM_TIMEOUT = "Communications Lost";
+}
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 0000000000000..491aa1c1da634
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/http/WebServerApi.java
@@ -0,0 +1,501 @@
+/**
+ * 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 javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
+import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.BRIDGE_HTTP_API_ENABLED;
+import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.BRIDGE_HTTP_API_EP;
+import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.BRIDGE_PROP_GW_ID;
+import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.BRIDGE_PROP_GW_VER;
+import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.BRIDGE_PROP_HW_MODEL;
+import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.BRIDGE_PROP_MAC_ADDR;
+import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.BRIDGE_PROP_VOL_UNIT;
+import static org.openhab.binding.linktap.protocol.http.NotTapLinkGatewayException.*;
+import static org.openhab.binding.linktap.protocol.http.TransientCommunicationIssueException.*;
+
+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.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 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.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+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 {
+
+ private final Logger logger = LoggerFactory.getLogger(WebServerApi.class);
+
+ private @NonNullByDefault({}) HttpClient httpClient;
+
+ private static final int REQ_TIMEOUT_SECONDS = 3;
+
+ private static final WebServerApi INSTANCE = new WebServerApi();
+
+ private WebServerApi() {
+ }
+
+ public static WebServerApi getInstance() {
+ return INSTANCE;
+ }
+
+ /**
+ * Sets the httpClient object to be used for API calls to Vesync.
+ *
+ * @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 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);
+ }
+ getMdnsEnableArgs(doc);
+ return getMetadataProperties(doc);
+
+ } catch (InterruptedException | TimeoutException e) {
+ throw new TransientCommunicationIssueException(HOST_COMM_TIMEOUT);
+ } 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("ExecutionException -> {}", e.getMessage());
+ }
+ throw new NotTapLinkGatewayException("Unexpected failure -> " + e.getMessage());
+ }
+ }
+
+ /**
+ * 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<>(4);
+
+ /*
+ * 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_HTTP_API_ENABLED, String.valueOf(httpApiEnabled));
+ newProps.put(BRIDGE_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 String targetServer) {
+ 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);
+ }
+
+ // Change the enable Local HTTP API flag to true
+ final int enableApiIdx = sb.indexOf("htapi=0");
+ if (enableApiIdx != -1) {
+ sb.replace(enableApiIdx, enableApiIdx + 7, "htapi=1");
+ }
+
+ final int urlApiMarker = sb.indexOf("URL=");
+ boolean updatedUri = false;
+ 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));
+ }
+ }
+
+ if (enableApiIdx != -1 || updatedUri) {
+ return sb.toString();
+ }
+
+ return "";
+ }
+
+ public boolean configureBridge(final @Nullable String hostname, final Optional mdnsEnable,
+ final Optional localServer)
+ throws NotTapLinkGatewayException, TransientCommunicationIssueException {
+ try {
+ if (hostname == null) {
+ throw new TransientCommunicationIssueException("Hostname invalid - null");
+ }
+ 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 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 = this.getMdnsEnableArgs(doc);
+ if (!mdnsEnableReqStr.isEmpty()) {
+ logger.debug("Updating mdns server settings on gateway");
+ final Request mdnsRequest = httpClient
+ .newRequest(URI_HOST_PREFIX + hostname + "/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()) {
+ logger.trace("Setting Local HTTP Api on gateway");
+ String localHttpApiReqStr = this.getLocalHttpApiArgs(doc, localServer.get());
+ if (!localHttpApiReqStr.isEmpty()) {
+ logger.debug("Updating Local HTTP API server settings on gateway");
+ final Request lhttpApiRequest = httpClient
+ .newRequest(URI_HOST_PREFIX + hostname + "/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(URI_HOST_PREFIX + hostname + "/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 (InterruptedException | TimeoutException e) {
+ throw new TransientCommunicationIssueException(HOST_COMM_TIMEOUT);
+ } 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("ExecutionException -> {}", e.getMessage());
+ }
+ throw new NotTapLinkGatewayException("Unexpected failure -> " + e.getMessage());
+ }
+ }
+
+ public boolean unlockWebInterface(final String hostname, final String username, final String password)
+ throws 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 | TimeoutException e) {
+ throw new TransientCommunicationIssueException(HOST_COMM_TIMEOUT);
+ } 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("ExecutionException -> {}", e.getMessage());
+ }
+ throw new NotTapLinkGatewayException("Unexpected failure -> " + e.getMessage());
+ }
+ }
+
+ /**
+ * 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), APPLICATION_JSON_TYPE.toString());
+
+ 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());
+ String responseData = cr.getContentAsString();
+ 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();
+ return responseData;
+ } catch (InterruptedException | TimeoutException | UnknownHostException e) {
+ throw new TransientCommunicationIssueException(HOST_COMM_TIMEOUT);
+ } 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("ExecutionException -> {}", e.getMessage());
+ }
+ throw new NotTapLinkGatewayException("Unexpected failure -> " + e.getMessage());
+ }
+ }
+
+ 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";
+}
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 0000000000000..268ac9cad0d44
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/servers/BindingServlet.java
@@ -0,0 +1,237 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.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.ArrayList;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+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.protocol.frames.TLGatewayFrame;
+import org.openhab.core.thing.Thing;
+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 {
+ private static final long serialVersionUID = -23L;
+
+ private final Logger logger = LoggerFactory.getLogger(BindingServlet.class);
+
+ public static final String SERVLET_URL_WITHOUT_ROOT = "linkTap";
+ private static final String SERVLET_URL = "/" + SERVLET_URL_WITHOUT_ROOT;
+ @Nullable
+ HttpService httpService;
+
+ volatile boolean registered;
+ List accountHandlers = new ArrayList<>();
+
+ public static final BindingServlet INSTANCE = new BindingServlet();
+
+ public static final BindingServlet getInstance() {
+ return INSTANCE;
+ }
+
+ public void setHttpService(final HttpService httpService) {
+ this.httpService = httpService;
+ }
+
+ public static String getServletAddress(final String hostname) {
+ final String httpPortStr = System.getProperty("org.osgi.service.http.port");
+ final String httpsPortStr = System.getProperty("org.osgi.service.https.port");
+ final Logger logger = LoggerFactory.getLogger(BindingServlet.class);
+ if (httpPortStr == null || httpPortStr.isEmpty()) {
+ logger.warn("HTTP Server port is not running, cannot use for API callbacks");
+ if (httpsPortStr != null && !httpsPortStr.isEmpty()) {
+ logger.warn("Looks like HTTPS is enabled - the device needs HTTP for efficient comm's");
+ }
+ return EMPTY_STRING;
+ }
+ return "http://" + hostname + ":" + httpPortStr + SERVLET_URL;
+ }
+
+ public void registerServlet() {
+ final HttpService srv = httpService;
+ 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("Register servlet failed for {}", SERVLET_URL, e);
+ }
+ }
+ }
+
+ public void unregisterServlet() {
+ final HttpService srv = httpService;
+ if (registered && srv != null) {
+ srv.unregister(SERVLET_URL);
+ registered = false;
+ logger.trace("Unregistered servlet");
+ }
+ }
+
+ @Override
+ protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
+ throws ServletException, IOException {
+ if (req == null) {
+ return;
+ }
+ /*
+ * if (BridgeManager.getInstance().isEmpty()) {
+ * resp.setStatus(HttpStatus.NOT_FOUND_404);
+ * resp.setContentLength(0);
+ * resp.flushBuffer();
+ * }
+ */
+
+ logger.warn("Got GET request from {}", req.getRemoteAddr());
+ logger.warn("Got GET request from {}", req.getRemoteHost());
+
+ StringBuilder html = new StringBuilder();
+ html.append("" + "My first page" + "");
+ html.append("
" + "Some Heading" + "
");
+
+ html.append("Remote Host").append(req.getRemoteHost()).append("");
+ html.append("Remote Addr").append(req.getRemoteAddr()).append("");
+ html.append("Remote User").append(req.getRemoteUser()).append("");
+ html.append("Remote Port").append(req.getRemotePort()).append("");
+
+ if (resp == null) {
+ return;
+ }
+
+ String requestUri = req.getRequestURI();
+ if (requestUri == null) {
+ return;
+ }
+ String uri = requestUri.substring(SERVLET_URL.length());
+ String queryString = req.getQueryString();
+ if (queryString != null && queryString.length() > 0) {
+ uri += "?" + queryString;
+ }
+ logger.debug("doGet {}", uri);
+
+ // if (!"/".equals(uri)) {
+ // String newUri = req.getServletPath() + "/";
+ // resp.sendRedirect(newUri);
+ // return;
+ // }
+
+ html.append("");
+
+ resp.addHeader("content-type", "text/html;charset=UTF-8");
+ try {
+ resp.getWriter().write(html.toString());
+ } catch (IOException e) {
+ logger.warn("return html failed with uri syntax error", e);
+ }
+ }
+
+ @Override
+ protected void doPost(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
+ throws ServletException, IOException {
+ if (req == null) {
+ return;
+ }
+
+ /*
+ * if (BridgeManager.getInstance().isEmpty()) {
+ * resp.setStatus(HttpStatus.NOT_FOUND_404);
+ * resp.setContentLength(0);
+ * resp.flushBuffer();
+ * }
+ */
+
+ // logger.warn("Got request from {}", req.getRemoteAddr());
+ // logger.warn("Got request from {}", req.getRemoteHost());
+ // Enumeration headers = req.getHeaderNames();
+ /*
+ * if (headers != null) {
+ * for (Iterator it = headers.asIterator(); it.hasNext();) {
+ * String header = it.next();
+ * // logger.warn("Got header {} with value {}", header, req.getHeader(header));
+ * }
+ * }
+ */
+
+ int bufferSize = 1000; // The payload string is technically limited to 768 characters - this should be enough
+ // for one buffer for the whole lot
+ char[] buffer = new char[bufferSize];
+ StringBuilder out = new StringBuilder();
+ 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();
+ // logger.warn("Output {}", payload);
+ final TLGatewayFrame tlFrame = LinkTapBindingConstants.GSON.fromJson(payload, TLGatewayFrame.class);
+ // if (tlFrame.command == DEFAULT_INT) {
+ // logger.warn("Unsolicited frame - Mapping to CMD 3");
+ // } else {
+ String result = "";
+ if (tlFrame != null) {
+ // logger.warn("Got Command {}", tlFrame.command);
+ TransactionProcessor tp = TransactionProcessor.getInstance();
+ result = tp.processGwRequest(req.getRemoteAddr(), tlFrame.command, payload);
+ }
+ // resp.setStatus(HttpServletResponse.SC_OK);
+ // resp.setContentType("application/json");
+ // resp.setContentLength(result.length());
+ // resp.setCharacterEncoding("UTF-8");
+ // resp.getWriter().write(result);
+ // resp.getWriter().flush();
+ // resp.getWriter().close();
+ // resp.flushBuffer();
+ // }
+ // logger.warn("SENDING RESPONSE BODY {}", result);
+ // resp.addHeader("content-type", "text/html;charset=UTF-8");
+ // try {
+ // resp.getWriter().write(result);
+ // } catch (IOException e) {
+ // logger.warn("return html failed with uri syntax error", e);
+ // }
+ if (resp == null) {
+ return;
+ }
+ // logger.warn("SENDING RESPONSE BODY 2 {}", result);
+
+ resp.setContentType("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 0000000000000..3c304a3bd1ef0
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/java/org/openhab/binding/linktap/protocol/servers/IHttpClientProvider.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.linktap.protocol.servers;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+
+/**
+ * @author David Goodyear - Initial contribution
+ */
+@NonNullByDefault
+public interface IHttpClientProvider {
+ @Nullable
+ 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 0000000000000..0941d8cee26e3
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/addon/addon.xml
@@ -0,0 +1,28 @@
+
+
+
+ binding
+ LinkTap Binding
+ This is the binding for LinkTap.
+
+
+
+ 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 0000000000000..0c2f44015a92d
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/i18n/linktap.properties
@@ -0,0 +1,3 @@
+# FIXME: please add all English translations to this file so the texts can be translated using Crowdin
+# FIXME: to generate the content of this file run: mvn i18n:generate-default-translations
+# FIXME: see also: https://www.openhab.org/docs/developer/utils/i18n.html
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 0000000000000..1081a4f4dc35b
--- /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
+
+
+
+
+ Number:Length
+
+ Previous rainfall calculated when watering was skipped
+ Rain
+
+
+
+
+ Number:Length
+
+ Future rainfall calculated when watering was skipped
+ Rain
+
+
+
+
+ DateTime
+
+ Time when watering was skipped
+ Time
+
+
+
+
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 0000000000000..3ddb17241efa3
--- /dev/null
+++ b/bundles/org.openhab.binding.linktap/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,108 @@
+
+
+
+
+
+ The LinkTap bridge represents a LinkTap gateway device
+
+
+
+
+
+
+
+
+
+
+
+
+
+ email
+ true
+
+ The hostname / IP address of the gateway device
+
+
+ false
+
+ The username if set for the gateway device
+
+
+ password
+ false
+
+ The password if set for the gateway device
+
+
+
+ On connection whether the mDNS responder should be enabled on the gateway device
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+ LinkTap Binding Device
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ deviceId
+
+
+
+ false
+
+ The Device Id for the device under the gateway
+
+
+ false
+
+ The name allocated to the device by the app. (Must be unique if used)
+
+
+ false
+
+ 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 0000000000000..ed55b558385d9
--- /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 0000000000000..09512f81caa14
--- /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 0000000000000..29a67970a1e1e
--- /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 0000000000000..8a4b1e9299bc6
--- /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 0000000000000..9dbe24dcb1615
--- /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 0000000000000..b1ea9670b9f34
--- /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 0000000000000..e4a4bfa8c6d82
--- /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 0000000000000..61b2950c1e6f4
--- /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 0000000000000..95cc64c623c68
--- /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 0000000000000..861e83addbdf7
--- /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 0000000000000..5fafb07004c35
--- /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 0000000000000..63ee1df156df7
--- /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 0000000000000..2937e51b66592
--- /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 0000000000000..eacc35b697c21
--- /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 0000000000000..acd15f6d39415
--- /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 0000000000000..2b6fd9a36957e
--- /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 0000000000000..7090c535f6b89
--- /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 0000000000000..2051ad4ba5730
--- /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 33b950b9ec09c..c06cd8d6f61ce 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -5,691 +5,1372 @@
4.0.0
+
org.openhab.addons
+
org.openhab.addons.reactor
+
4.2.0-SNAPSHOT
+
org.openhab.addons.bundles
+
org.openhab.addons.reactor.bundles
+
pomopenHAB Add-ons :: Bundles
+
+
org.openhab.automation.groovyscripting
+
org.openhab.automation.jrubyscripting
+
org.openhab.automation.jsscripting
+
org.openhab.automation.jsscriptingnashorn
+
org.openhab.automation.jythonscripting
+
org.openhab.automation.pidcontroller
+
org.openhab.automation.pwm
+
+
org.openhab.io.homekit
+
org.openhab.io.hueemulation
+
org.openhab.io.metrics
+
org.openhab.io.neeo
+
org.openhab.io.openhabcloud
+
+
org.openhab.transform.basicprofiles
+
org.openhab.transform.bin2json
+
org.openhab.transform.exec
+
org.openhab.transform.jinja
+
org.openhab.transform.jsonpath
+
org.openhab.transform.map
+
org.openhab.transform.regex
+
org.openhab.transform.rollershutterposition
+
org.openhab.transform.scale
+
org.openhab.transform.vat
+
org.openhab.transform.xpath
+
org.openhab.transform.xslt
+
+
org.openhab.binding.adorne
+
org.openhab.binding.ahawastecollection
+
org.openhab.binding.airgradient
+
org.openhab.binding.airq
+
org.openhab.binding.airquality
+
org.openhab.binding.airvisualnode
+
org.openhab.binding.alarmdecoder
+
org.openhab.binding.allplay
+
org.openhab.binding.amazondashbutton
+
org.openhab.binding.amazonechocontrol
+
org.openhab.binding.ambientweather
+
org.openhab.binding.amplipi
+
org.openhab.binding.androiddebugbridge
+
org.openhab.binding.androidtv
+
org.openhab.binding.anel
+
org.openhab.binding.anthem
+
org.openhab.binding.astro
+
org.openhab.binding.asuswrt
+
org.openhab.binding.atlona
+
org.openhab.binding.autelis
+
org.openhab.binding.automower
+
org.openhab.binding.avmfritz
+
org.openhab.binding.awattar
+
org.openhab.binding.benqprojector
+
org.openhab.binding.bigassfan
+
org.openhab.binding.bluetooth
+
org.openhab.binding.bluetooth.airthings
+
org.openhab.binding.bluetooth.am43
+
org.openhab.binding.bluetooth.bluegiga
+
org.openhab.binding.bluetooth.bluez
+
org.openhab.binding.bluetooth.blukii
+
org.openhab.binding.bluetooth.daikinmadoka
+
org.openhab.binding.bluetooth.enoceanble
+
org.openhab.binding.bluetooth.generic
+
org.openhab.binding.bluetooth.govee
+
org.openhab.binding.bluetooth.grundfosalpha
+
org.openhab.binding.bluetooth.radoneye
+
org.openhab.binding.bluetooth.roaming
+
org.openhab.binding.bluetooth.ruuvitag
+
org.openhab.binding.bondhome
+
org.openhab.binding.boschindego
+
org.openhab.binding.boschshc
+
org.openhab.binding.bosesoundtouch
+
org.openhab.binding.broadlinkthermostat
+
org.openhab.binding.bsblan
+
org.openhab.binding.bticinosmarther
+
org.openhab.binding.buienradar
+
org.openhab.binding.caddx
+
org.openhab.binding.cbus
+
org.openhab.binding.chatgpt
+
org.openhab.binding.chromecast
+
org.openhab.binding.cm11a
+
org.openhab.binding.comfoair
+
org.openhab.binding.coolmasternet
+
org.openhab.binding.coronastats
+
org.openhab.binding.daikin
+
org.openhab.binding.dali
+
org.openhab.binding.danfossairunit
+
org.openhab.binding.dbquery
+
org.openhab.binding.deconz
+
org.openhab.binding.denonmarantz
+
org.openhab.binding.deutschebahn
+
org.openhab.binding.digiplex
+
org.openhab.binding.digitalstrom
+
org.openhab.binding.dlinksmarthome
+
org.openhab.binding.dmx
+
org.openhab.binding.dolbycp
+
org.openhab.binding.dominoswiss
+
org.openhab.binding.doorbird
+
org.openhab.binding.draytonwiser
+
org.openhab.binding.dscalarm
+
org.openhab.binding.dsmr
+
org.openhab.binding.dwdpollenflug
+
org.openhab.binding.dwdunwetter
+
org.openhab.binding.easee
+
org.openhab.binding.echonetlite
+
org.openhab.binding.ecobee
+
org.openhab.binding.ecotouch
+
org.openhab.binding.ecovacs
+
org.openhab.binding.ecowatt
+
org.openhab.binding.ekey
+
org.openhab.binding.electroluxair
+
org.openhab.binding.elerotransmitterstick
+
org.openhab.binding.elroconnects
+
org.openhab.binding.energenie
+
org.openhab.binding.energidataservice
+
org.openhab.binding.enigma2
+
org.openhab.binding.enocean
+
org.openhab.binding.enphase
+
org.openhab.binding.enturno
+
org.openhab.binding.ephemeris
+
org.openhab.binding.epsonprojector
+
org.openhab.binding.etherrain
+
org.openhab.binding.evcc
+
org.openhab.binding.evohome
+
org.openhab.binding.exec
+
org.openhab.binding.feed
+
org.openhab.binding.feican
+
org.openhab.binding.fineoffsetweatherstation
+
org.openhab.binding.flicbutton
+
org.openhab.binding.fmiweather
+
org.openhab.binding.folderwatcher
+
org.openhab.binding.folding
+
org.openhab.binding.foobot
+
org.openhab.binding.freeathome
+
org.openhab.binding.freebox
+
org.openhab.binding.freeboxos
+
org.openhab.binding.freecurrency
+
org.openhab.binding.frenchgovtenergydata
+
org.openhab.binding.fronius
+
org.openhab.binding.fsinternetradio
+
org.openhab.binding.ftpupload
+
org.openhab.binding.gardena
+
org.openhab.binding.gce
+
org.openhab.binding.generacmobilelink
+
org.openhab.binding.goecharger
+
org.openhab.binding.govee
+
org.openhab.binding.gpio
+
org.openhab.binding.globalcache
+
org.openhab.binding.gpstracker
+
org.openhab.binding.gree
+
org.openhab.binding.gridbox
+
org.openhab.binding.groheondus
+
org.openhab.binding.groupepsa
+
org.openhab.binding.growatt
+
org.openhab.binding.guntamatic
+
org.openhab.binding.haassohnpelletstove
+
org.openhab.binding.harmonyhub
+
org.openhab.binding.haywardomnilogic
+
org.openhab.binding.hccrubbishcollection
+
org.openhab.binding.hdanywhere
+
org.openhab.binding.hdpowerview
+
org.openhab.binding.helios
+
org.openhab.binding.heliosventilation
+
org.openhab.binding.heos
+
org.openhab.binding.herzborg
+
org.openhab.binding.homeconnect
+
org.openhab.binding.homematic
+
org.openhab.binding.homewizard
+
org.openhab.binding.hpprinter
+
org.openhab.binding.http
+
org.openhab.binding.hue
+
org.openhab.binding.hydrawise
+
org.openhab.binding.hyperion
+
org.openhab.binding.iammeter
+
org.openhab.binding.iaqualink
+
org.openhab.binding.icalendar
+
org.openhab.binding.icloud
+
org.openhab.binding.ihc
+
org.openhab.binding.insteon
+
org.openhab.binding.ipcamera
+
org.openhab.binding.ipobserver
+
org.openhab.binding.intesis
+
org.openhab.binding.iotawatt
+
org.openhab.binding.ipp
+
org.openhab.binding.irobot
+
org.openhab.binding.irtrans
+
org.openhab.binding.ism8
+
org.openhab.binding.jablotron
+
org.openhab.binding.jeelink
+
org.openhab.binding.jellyfin
+
org.openhab.binding.juicenet
+
org.openhab.binding.kaleidescape
+
org.openhab.binding.keba
+
org.openhab.binding.km200
+
org.openhab.binding.knx
+
org.openhab.binding.kodi
+
org.openhab.binding.konnected
+
org.openhab.binding.kostalinverter
+
org.openhab.binding.kvv
+
org.openhab.binding.lametrictime
+
org.openhab.binding.lcn
+
org.openhab.binding.leapmotion
+
org.openhab.binding.lghombot
+
org.openhab.binding.lgtvserial
+
org.openhab.binding.lgwebos
+
org.openhab.binding.lifx
+
org.openhab.binding.linky
+
+ org.openhab.binding.linktap
+
org.openhab.binding.linuxinput
+
org.openhab.binding.liquidcheck
+
org.openhab.binding.lirc
+
org.openhab.binding.livisismarthome
+
org.openhab.binding.logreader
+
org.openhab.binding.loxone
+
org.openhab.binding.lutron
+
org.openhab.binding.luxom
+
org.openhab.binding.luxtronikheatpump
+
org.openhab.binding.magentatv
+
org.openhab.binding.mail
+
org.openhab.binding.max
+
org.openhab.binding.mcd
+
org.openhab.binding.mcp23017
+
org.openhab.binding.meater
+
org.openhab.binding.mecmeter
+
org.openhab.binding.melcloud
+
org.openhab.binding.mercedesme
+
org.openhab.binding.meteoalerte
+
org.openhab.binding.meteoblue
+
org.openhab.binding.meteostick
+
org.openhab.binding.miele
+
org.openhab.binding.mielecloud
+
org.openhab.binding.mihome
+
org.openhab.binding.miio
+
org.openhab.binding.mikrotik
+
org.openhab.binding.millheat
+
org.openhab.binding.milight
+
org.openhab.binding.minecraft
+
org.openhab.binding.modbus
+
org.openhab.binding.modbus.e3dc
+
org.openhab.binding.modbus.sbc
+
org.openhab.binding.modbus.studer
+
org.openhab.binding.modbus.sungrow
+
org.openhab.binding.modbus.sunspec
+
org.openhab.binding.modbus.stiebeleltron
+
org.openhab.binding.modbus.helioseasycontrols
+
org.openhab.binding.monopriceaudio
+
org.openhab.binding.mpd
+
org.openhab.binding.mqtt
+
org.openhab.binding.mqtt.espmilighthub
+
org.openhab.binding.mqtt.generic
+
org.openhab.binding.mqtt.homeassistant
+
org.openhab.binding.mqtt.homie
+
org.openhab.binding.mqtt.ruuvigateway
+
org.openhab.binding.mybmw
+
org.openhab.binding.mycroft
+
org.openhab.binding.mynice
+
org.openhab.binding.mystrom
+
org.openhab.binding.nanoleaf
+
org.openhab.binding.neato
+
org.openhab.binding.neeo
+
org.openhab.binding.neohub
+
org.openhab.binding.nest
+
org.openhab.binding.netatmo
+
org.openhab.binding.network
+
org.openhab.binding.networkupstools
+
org.openhab.binding.nibeheatpump
+
org.openhab.binding.nibeuplink
+
org.openhab.binding.nikobus
+
org.openhab.binding.nikohomecontrol
+
org.openhab.binding.nobohub
+
org.openhab.binding.novafinedust
+
org.openhab.binding.ntp
+
org.openhab.binding.nuki
+
org.openhab.binding.nuvo
+
org.openhab.binding.nzwateralerts
+
org.openhab.binding.oceanic
+
org.openhab.binding.ojelectronics
+
org.openhab.binding.omnikinverter
+
org.openhab.binding.omnilink
+
org.openhab.binding.onebusaway
+
org.openhab.binding.onewiregpio
+
org.openhab.binding.onewire
+
org.openhab.binding.onkyo
+
org.openhab.binding.opengarage
+
org.openhab.binding.opensprinkler
+
org.openhab.binding.openthermgateway
+
org.openhab.binding.openuv
+
org.openhab.binding.openweathermap
+
org.openhab.binding.openwebnet
+
org.openhab.binding.oppo
+
org.openhab.binding.orbitbhyve
+
org.openhab.binding.orvibo
+
org.openhab.binding.panasonicbdp
+
org.openhab.binding.paradoxalarm
+
org.openhab.binding.pentair
+
org.openhab.binding.phc
+
org.openhab.binding.pilight
+
org.openhab.binding.pioneeravr
+
org.openhab.binding.pixometer
+
org.openhab.binding.pjlinkdevice
+
org.openhab.binding.playstation
+
org.openhab.binding.plclogo
+
org.openhab.binding.plex
+
org.openhab.binding.plugwise
+
org.openhab.binding.plugwiseha
+
org.openhab.binding.powermax
+
org.openhab.binding.proteusecometer
+
org.openhab.binding.prowl
+
org.openhab.binding.publictransportswitzerland
+
org.openhab.binding.pulseaudio
+
org.openhab.binding.pushbullet
+
org.openhab.binding.pushover
+
org.openhab.binding.pushsafer
+
org.openhab.binding.qbus
+
org.openhab.binding.qolsysiq
+
org.openhab.binding.radiobrowser
+
org.openhab.binding.radiothermostat
+
org.openhab.binding.regoheatpump
+
org.openhab.binding.revogi
+
org.openhab.binding.remoteopenhab
+
org.openhab.binding.renault
+
org.openhab.binding.resol
+
org.openhab.binding.rfxcom
+
org.openhab.binding.rme
+
org.openhab.binding.robonect
+
org.openhab.binding.roku
+
org.openhab.binding.rotel
+
org.openhab.binding.russound
+
org.openhab.binding.sagercaster
+
org.openhab.binding.saicismart
+
org.openhab.binding.salus
+
org.openhab.binding.samsungtv
+
org.openhab.binding.satel
+
org.openhab.binding.semsportal
+
org.openhab.binding.senechome
+
org.openhab.binding.seneye
+
org.openhab.binding.sensebox
+
org.openhab.binding.sensibo
+
org.openhab.binding.sensorcommunity
+
org.openhab.binding.serial
+
org.openhab.binding.serialbutton
+
org.openhab.binding.shelly
+
org.openhab.binding.silvercrestwifisocket
+
org.openhab.binding.siemensrds
+
org.openhab.binding.sinope
+
org.openhab.binding.sleepiq
+
org.openhab.binding.smaenergymeter
+
org.openhab.binding.smartmeter
+
org.openhab.binding.smartthings
+
org.openhab.binding.smgw
+
org.openhab.binding.smhi
+
org.openhab.binding.smsmodem
+
org.openhab.binding.sncf
+
org.openhab.binding.snmp
+
org.openhab.binding.solaredge
+
org.openhab.binding.solarforecast
+
org.openhab.binding.solarlog
+
org.openhab.binding.solarmax
+
org.openhab.binding.solarwatt
+
org.openhab.binding.solax
+
org.openhab.binding.somfymylink
+
org.openhab.binding.somfytahoma
+
org.openhab.binding.somneo
+
org.openhab.binding.sonnen
+
org.openhab.binding.sonos
+
org.openhab.binding.sonyaudio
+
org.openhab.binding.sonyprojector
+
org.openhab.binding.souliss
+
org.openhab.binding.speedtest
+
org.openhab.binding.spotify
+
org.openhab.binding.squeezebox
+
org.openhab.binding.surepetcare
+
org.openhab.binding.synopanalyzer
+
org.openhab.binding.systeminfo
+
org.openhab.binding.tacmi
+
org.openhab.binding.tado
+
org.openhab.binding.tankerkoenig
+
org.openhab.binding.tapocontrol
+
org.openhab.binding.tasmotaplug
+
org.openhab.binding.telegram
+
org.openhab.binding.teleinfo
+
org.openhab.binding.tellstick
+
org.openhab.binding.tesla
+
org.openhab.binding.tibber
+
org.openhab.binding.tivo
+
org.openhab.binding.touchwand
+
org.openhab.binding.tplinkrouter
+
org.openhab.binding.tplinksmarthome
+
org.openhab.binding.tr064
+
org.openhab.binding.tradfri
+
org.openhab.binding.unifi
+
org.openhab.binding.unifiedremote
+
org.openhab.binding.upnpcontrol
+
org.openhab.binding.upb
+
org.openhab.binding.urtsi
+
org.openhab.binding.valloxmv
+
org.openhab.binding.vdr
+
org.openhab.binding.vektiva
+
org.openhab.binding.velbus
+
org.openhab.binding.velux
+
org.openhab.binding.venstarthermostat
+
org.openhab.binding.ventaair
+
org.openhab.binding.verisure
+
org.openhab.binding.vesync
+
org.openhab.binding.vigicrues
+
org.openhab.binding.vitotronic
+
org.openhab.binding.vizio
+
org.openhab.binding.volvooncall
+
org.openhab.binding.volumio
+
org.openhab.binding.warmup
+
org.openhab.binding.weathercompany
+
org.openhab.binding.weatherunderground
+
org.openhab.binding.webexteams
+
org.openhab.binding.webthing
+
org.openhab.binding.wemo
+
org.openhab.binding.wifiled
+
org.openhab.binding.windcentrale
+
org.openhab.binding.wlanthermo
+
org.openhab.binding.wled
+
org.openhab.binding.wolfsmartset
+
org.openhab.binding.wundergroundupdatereceiver
+
org.openhab.binding.x
+
org.openhab.binding.xmltv
+
org.openhab.binding.xmppclient
+
org.openhab.binding.yamahamusiccast
+
org.openhab.binding.yamahareceiver
+
org.openhab.binding.yioremote
+
org.openhab.binding.yeelight
+
org.openhab.binding.zoneminder
+
org.openhab.binding.zway
+
+
org.openhab.persistence.dynamodb
+
org.openhab.persistence.influxdb
+
org.openhab.persistence.inmemory
+
org.openhab.persistence.jdbc
+
org.openhab.persistence.jpa
+
org.openhab.persistence.mapdb
+
org.openhab.persistence.mongodb
+
org.openhab.persistence.rrd4j
+
+
org.openhab.voice.googlestt
+
org.openhab.voice.googletts
+
org.openhab.voice.mactts
+
org.openhab.voice.marytts
+
org.openhab.voice.mimictts
+
org.openhab.voice.picotts
+
org.openhab.voice.pipertts
+
org.openhab.voice.pollytts
+
org.openhab.voice.rustpotterks
+
org.openhab.voice.voicerss
+
org.openhab.voice.voskstt
+
org.openhab.voice.watsonstt
+
+
+
target/dependency
+
+
+
+
org.lastnpe.eea
+
eea-all
+
${eea.version}
+
+
+
+
org.openhab.core.bom
+
org.openhab.core.bom.compile
+
pom
+
provided
+
+
+
org.openhab.core.bom
+
org.openhab.core.bom.openhab-core
+
pom
+
provided
+
+
+
commons-net
+
commons-net
+
+
+
+
+
org.openhab.core.bom
+
org.openhab.core.bom.test
+
pom
+
test
+
+
+
+
org.apache.karaf.features
+
framework
+
${karaf.version}
+
kar
+
true
+
+
+
*
+
*
+
+
+
+
+
+
org.apache.karaf.features
+
standard
+
${karaf.version}
+
features
+
xml
+
provided
+
+
+
+
+
+
org.apache.maven.plugins
+
maven-jar-plugin
+
+
+
${project.build.outputDirectory}/META-INF/MANIFEST.MF
+
+
true
+
+
+
+
org.apache.karaf.tooling
+
karaf-maven-plugin
+
${karaf.version}
+
true
+
+
80
+
true
+
true
+
false
+
true
+
true
+
+
+
+
compile
+
+
features-generate-descriptor
+
+
generate-resources
+
+
${feature.directory}
+
+
+
+
karaf-feature-verification
+
+
verify
+
+
verify
+
+
+
+
mvn:org.apache.karaf.features/framework/${karaf.version}/xml/features
+
mvn:org.apache.karaf.features/standard/${karaf.version}/xml/features
+
mvn:org.apache.karaf.features/specs/${karaf.version}/xml/features
+
+
file:${project.build.directory}/feature/feature.xml
+
+
org.apache.karaf.features:framework
+
${oh.java.version}
+
+
framework
+
+
+
openhab-*
+
+
false
+
true
+
first
+
+
+
+
+
+
+
+
biz.aQute.bnd
+
bnd-maven-plugin
+
+
+
org.apache.maven.plugins
+
maven-source-plugin
+
+
+
attach-sources
+
+
jar-no-fork
+
+
+
+
+
+
org.apache.karaf.tooling
+
karaf-maven-plugin
+
+
+
+
org.apache.maven.plugins
+
maven-dependency-plugin
+
3.6.1
+
+
+
embed-dependencies
+
+
unpack-dependencies
+
+
+
runtime
+
jar
+
**/module-info.class
+
javax.activation,org.apache.karaf.features,org.lastnpe.eea
+
${dep.noembedding}
+
${project.build.directory}/classes
+
true
+
true
+
true
+
jar
+
+
+
+
unpack-eea
+
+
unpack
+
+
+
+
+
org.lastnpe.eea
+
eea-all
+
${eea.version}
+
true
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ 8
+
+
+
+
+
+
no-embed-dependencies
+
+
+
noEmbedDependencies.profile
+
+
+
+
+
+
org.apache.maven.plugins
+
maven-dependency-plugin
+
+
+
embed-dependencies
+
none
+
+
+
+
+
+
+