[linktap] Initial contribution (#17235)

* [linkTap] Initial Code Commit

[Signed-off-by: dag81 <david.goodyear@gmail.com>
This commit is contained in:
dag81 2024-09-30 04:48:55 +01:00 committed by GitHub
parent 85b165208c
commit b11c751d03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
78 changed files with 8376 additions and 0 deletions

View File

@ -956,6 +956,11 @@
<artifactId>org.openhab.binding.lifx</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.linktap</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.linky</artifactId>

View File

@ -0,0 +1,20 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons
== Third-party Content
jsoup
* License: MIT License
* Project: https://jsoup.org/
* Source: https://github.com/jhy/jsoup

View File

@ -0,0 +1,234 @@
# LinkTap Binding
This binding is for [Link-Tap](https://www.link-tap.com/) devices.
**This is for communication over a local area network, that prevents direct access to your gateway / openHAB instance from the internet.
E.g. behind a router**
The method of interaction this binding supports is:
**Program and execution of the watering plan within the application**
The currently supported capabilities include where supported by the gateway / device:
- Time synchronisation to openHAB
- Child lock controls
- Monitoring and dismissal for device alarms (Water Cut, etc.)
- Monitoring of sensor states (Battery, Zigbee Signal, Flow Meters Statistics, etc.)
- Enable watering based on time duration / volume limits
- Shutdown of active watering
## Requirements
A LinkTap gateway device such as the GW_02, in order for openHAB to connect to the system, as a gateway.
Older GW_01 gateway devices have not been tested but should work with a static IP setup.
The recommended minimum version of the firmware is:
- **GW_01** is to start with **at least S609**
- **GW_02** is to start with **at least G609**
## Connection Options
LinkTap supports MQTT and a direct interaction via HTTP.
This binding directly interacts with LinkTap's gateway devices using the Local HTTP API (HTTP).
The binding connects to the gateway's directly, and the Gateway is configured automatically to push updates to openHAB if it has a HTTP configured server.
(Note HTTPS is not supported).
Should the Gateway device's not be able to connect to the binding it automatically falls-back to a polling implementation (15 second cycle).
The gateway supports 1 Local HTTP API, for an ideal behavior the Gateway should be able to connect to openHAB on a HTTP port by its IP, and only a single openHAB instance should be connected to a Gateway.
It is recommended that you use **static IP's** for this binding, **for both openHAB and the Gateway device(s)** regardless of the gateway's model.
If dynamic IPs are used for the gateway, the mDNS address is recommended to be used.
This can be found when running a manual scan, for LinkTap Gateways.
This will remove any DNS caching related issues, depending on your setup.
## Supported Things
This binding supports the follow thing types:
| Thing Type | Thing Type UID | Discovery | Description |
|------------|----------------|--------------------|------------------------------------------------------------------|
| Bridge | gateway | Manual / Automatic | A connection to a LinkTap Gateway device |
| Thing | device | Automatic | A end device such as one of the four controlled values on the Q1 |
**NOTE** This binding was developed and tested using a GW-02 gateway with a Q1 device.
## Discovery
### Gateways
If mDNS has been enabled on the Gateway device via it's webpage, then the gateway(s) will be discovered, and appear in the inbox when a manual scan is run when adding a LinkTap Gateway.
It is however recommended to use **static IP addresses** and add the gateways directly using the IP address.
### Devices
Once connected to a LinkTap gateway, the binding will listen for updates of new devices and add them, to the inbox.
If the gateway cannot publish to openHAB, then the gateway is checked every 2 minutes for new devices, and they are added to the inbox when discovered.
## Binding Configuration
### Gateway Configuration
| Name | Type | Description | Recommended Values | Required | Advanced |
|-----------------------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|----------|----------|
| host | String | The hostname / IP address of the gateway device | | Yes | No |
| username | String | The username if set for the gateway device | | No | No |
| password | String | The password if set for the gateway device | | No | No |
| enableMDNS | Switch | On connection whether the mDNS responder should be enabled on the gateway device | true | No | Yes |
| enforceProtocolLimits | Switch | If true data outside of the allowed ranges against the protocol will be logged and not sent | true | No | Yes |
| enableJSONComms | Switch | false by default for backwards compatibility, if using up to date firmware with no other local network applications set this to true, for more efficient communications | true | No | Yes |
**NOTE** When enableMDNS is enabled, upon connection to the gateway option "Enable mDNS responder" is switched on.
### Device Configuration
| Name | Type | Description | Recommended Values | Required | Advanced |
|--------------|---------|-----------------------------------------------------------------------|--------------------|----------|----------|
| deviceId | String | The Device Id for the device under the gateway | | No (A,B) | No |
| deviceName | String | The name allocated to the device by the app. (Must be unique if used) | | No (B) | No |
| enableAlerts | boolean | On connection whether the device should be configured to send alerts | true | No | Yes |
**NOTE:**
(A) It is recommended to use the Device Id, for locating devices.
This can be found in the LinkTap mobile application under Settings->TapLinker / ValveLinker, e.g.
- ValueLinker_1 (D71BC52F004B1200_1-xxxx)
- has Device Id "ValveLinker_1"
- has Device Name D71BC52F004B1200_1
(B) Either a **deviceId or deviceName is required** for the device to be located and used.
## Channels
| Name | Type | Description | Representation | Write Action | Note |
|-------------------|---------------------------|-------------------------------------------------------------------------------------|----------------|------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
| water-cut | Switch | Water cut-off alert | Alert | Dismiss alert | |
| shutdown-failure | Switch | The device has failed to close the valve | Alert | Dismiss alert | |
| high-flow | Switch | Unusually high flow rate detected alert | Alert | Dismiss alert | |
| low-flow | Switch | Unusually low flow rate detected alert | Alert | Dismiss alert | |
| fall-status | Switch | The device has fallen | Alert | Dismiss alert | |
| mode | Text | The current watering plan mode | R | | |
| flm-linked | Switch | The device has a included flow meter | R | | |
| rf-linked | Switch | Is the device RF linked | R | | |
| signal | Number:Dimensionless | Reception Signal Strength | R | | |
| battery | Number:Dimensionless | Battery Remaining Level | R | | |
| flow-rate | Number:VolumetricFlowRate | Current water flow rate | R | | |
| volume | Number:Volume | Accumulated volume of current watering cycle | R | | |
| eco-final | Switch | In ECO mode this is true when the final ON watering on segment is running | R | | |
| remaining | Number:Time | Remaining duration of the current watering cycle | R | | |
| duration | Number:Time | Total duration of current watering cycle | R | | |
| watering | Switch | Active watering status | RW | True - Start immediate watering, False - Stops the current watering process, the next planned watering will run as scheduled | |
| manual-watering | Switch | Manual watering mode status | R | | |
| child-lock | Text | The child lock mode | RW | Unlocked - Button enabled, Partially locked -> 3 second push required, Completely locked -> Button disabled | If the GW has internet connectivity settings will be reset when it sync's to TapLink's servers. |
| oh-dur-limit | Number:Time | Max duration allowed for the immediate watering | W | Max Time duration for "Start immediate watering" | |
| oh-vol-limit | Number:Volume | Max Volume limit for immediate watering | W | Max Volume for "Start immediate watering" | |
| plan-pause-enable | Switch | When ON will pause the current watering plan for an hour every 55 minutes | RW | Pause current watering plan every 55 mins for one hour | This disables the TapLink Watering Plan from being run, it is not reflected in the mobile app. |
| plan-resume-time | DateTime | Displays when the last pause issued will expiry, resuming the current watering plan | R | | |
| watering-plan-id | Text | Displays the current watering plan id | R | | |
**NOTE:**
There are 4 different areas of channels:
- R (Read Only Data)
- These represent data published by the device
- Alerts
- These are switches that are set to ON by the device when an alert condition is detected, such as a Water Cut.
- The alert can be dismissed by setting the switch to OFF
- RW (Read Write Data)
- Provides the ability to read data
- Provides the ability to set a relevant state to the data
- W Data
- Provides parameter values for the named action, it is stored within openHAB is not read from the device
- E.g. Start Immediate Watering
- Can be limited by a time duration - ohDurLimit
- If a flow meter is attached can be limited by a volume limit - ohVolLimit
## Full Example
### Thing Configuration
- **Gateway Model**: GW_02
- **Device Model**: Q1
```java
Bridge linktap:gateway:home "LinkTap GW02" [ host="192.168.0.21", enableMDNS=true, enableJSONComms=false, enforceProtocolLimits=true ] {
Thing device TapValve1 "Outdoor Tap 1" [ id="D71BC52E985B1200_1", name="ValveLinker_1", enableAlerts=true ]
Thing device TapValve2 "Outdoor Tap 2" [ id="D71BC52E985B1200_2", name="ValveLinker_2", enableAlerts=true ]
Thing device TapValve3 "Outdoor Tap 3" [ id="D71BC52E985B1200_3", name="ValveLinker_3", enableAlerts=true ]
Thing device TapValve4 "Outdoor Tap 4" [ id="D71BC52E985B1200_4", name="ValveLinker_4", enableAlerts=true ]
}
```
### Item Configuration
```java
Number:Dimensionless Tap1BatteryLevel "Tap 1 - Battery Level" <batterylevel> ["Point"] { channel="linktap:device:home:tapValve1:battery",unit="%%" }
Number:Dimensionless Tap1SignalLevel "Tap 1 - Signal Level" <qualityofservice> ["Point"] { channel="linktap:device:home:tapValve1:signal",unit="%%" }
Switch Tap1RfLinked "Tap 1 - RF Linked" <switch> ["Point"] { channel="linktap:device:home:tapValve1:rf-linked"}
Switch Tap1FlmLinked "Tap 1 - FLM Linked" <switch> ["Point"] { channel="linktap:device:home:tapValve1:flm-linked"}
Switch Tap1WaterCutAlert "Tap 1 - Water Cut Alert" <alarm> ["Point"] { channel="linktap:device:home:tapValve1:water-cut" }
Switch Tap1WaterFallAlert "Tap 1 - Fallen Alert" <alarm> ["Point"] { channel="linktap:device:home:tapValve1:fall-status" }
Switch Tap1WaterValveAlert "Tap 1 - Shutdown Failure Alert" <alarm> ["Point"] { channel="linktap:device:home:tapValve1:shutdown-failure" }
Switch Tap1WaterLowFlowAlert "Tap 1 - Low Flow Alert" <alarm> ["Point"] { channel="linktap:device:home:tapValve1:low-flow" }
Switch Tap1WaterHighFlowAlert "Tap 1 - High Flow Alert" <alarm> ["Point"] { channel="linktap:device:home:tapValve1:high-flow" }
String Tap1ChildLockMode "Tap 1 - Child Lock Mode" <lock> ["Point"] { channel="linktap:device:home:tapValve1:child-lock" }
Number:VolumetricFlowRate Tap1FlowRate "Tap 1 - Flow Rate" <flow> ["Point"] { channel="linktap:device:home:tapValve1:flow-rate",unit="l/min" }
Number:Volume Tap1WateringVolume "Tap 1 - Watering Volume" <water> ["Point"] { channel="linktap:device:home:tapValve1:volume",unit="l" }
Switch Tap1FinalEcoSegment "Tap 1 - Final ECO Segment" <switch> ["Point"] { channel="linktap:device:home:tapValve1:eco-final" }
Switch Tap1Watering "Tap 1 - Watering" <water> ["Point"] { channel="linktap:device:home:tapValve1:watering" }
Switch Tap1ManualWatering "Tap 1 - Manual Watering" <water> ["Point"] { channel="linktap:device:home:tapValve1:manual-watering" }
String Tap1WateringMode "Tap 1 - Watering Mode" <time> ["Point"] { channel="linktap:device:home:tapValve1:mode" }
Number:Time Tap1TimeDuration "Tap 1 - Current Cycle Duration" <time> ["Point"] { channel="linktap:device:home:tapValve1:duration",unit="s" }
Number:Time Tap1TimeRemain "Tap 1 - Current Cycle Remaining" <time> ["Point"] { channel="linktap:device:home:tapValve1:remaining",unit="s" }
Number:Time Tap1WateringCycleDur "Tap 1 - Current Cycle Duration Limit" <time> ["Point"] { channel="linktap:device:home:tapValve1:dur-limit",unit="s" }
Number:Volume Tap1WateringCycleVol "Tap 1 - Current Cycle Volume Limit" <water> ["Point"] { channel="linktap:device:home:tapValve1:vol-limit",unit="l" }
Number:Time Tap1ManTimeLimit "Tap 1 - Instant On Duration Limit" <time> ["Point"] { channel="linktap:device:home:tapValve1:oh-dur-limit",unit="s" }
Number:Volume Tap1ManVolLimit "Tap 1 - Instant On Volume Limit" <water> ["Point"] { channel="linktap:device:home:tapValve1:oh-vol-limit",unit="l" }
Switch Tap1PauseWateringPlan "Tap 1 - Pause Current Plan" <time> ["Point"] { channel="linktap:device:home:tapValve1:plan-pause-enable" }
DateTime Tap1PauseExpiry "Tap 1 - Pause Expiry" <calendar> ["Point"] { channel="linktap:device:home:tapValve1:plan-resume-time" }
String Tap1WateringPlanId "Tap 1 - Watering Plan Id" <calendar> ["Point"] { channel="linktap:device:home:tapValve1:watering-plan-id" }
```
### Sitemap Configuration
```perl
Text item=Tap1BatteryLevel
Switch item=Tap1WaterCutAlert
Switch item=Tap1WaterFallAlert
Switch item=Tap1WaterValveAlert
Switch item=Tap1WaterLowFlowAlert
Switch item=Tap1WaterHighFlowAlert
Text item=Tap1ChildLockMode
Text item=Tap1FlowRate label="Tap 1 - Flow Rate [%.0f %unit%]"
Text item=Tap1WateringVolume label="Tap 1 - Watering Volume [%.0f %unit%]"
Text item=Tap1FinalEcoSegment label="Tap 1 - Final Segment [%s]"
Switch item=Tap1Watering
Switch item=Tap1ManualWatering
Text item=Tap1WateringMode
Text item=Tap1TimeDuration label="Tap 1 - Time Duration [%.0f %unit%]"
Text item=Tap1TimeRemain label="Tap 1 - Time Remaining [%.0f %unit%]"
Text item=Tap1WateringCycleDur label="Tap 1 - Cycle Duration [%.0f %unit%]"
Text item=Tap1WateringCycleVol label="Tap 1 - Cycle Volume [%.0f %unit%]"
Slider item=Tap1ManTimeLimit minValue=3 maxValue=86340 step=30 releaseOnly label="Tap 1 - Instant On Time Limit [%.0f %unit%]"
Slider item=Tap1ManVolLimit minValue=1 maxValue=5000 step=1 releaseOnly label="Tap 1 - Instant On Volume Limit [%.0f %unit%]"
Switch item=Tap1PauseWateringPlan
Text item=Tap1PauseExpiry
Text item=Tap1WateringPlanId
```
#### Other Models
Please check the [Link-Tap](https://www.link-tap.com/) website.
Presently at this location [here](https://www.link-tap.com/#!/wireless-water-timer) is a chart that shows the features available for the products.
If a product such as the G1S is used, it will not support flow based commands or readings.
In this case exclude the volume based Items and Sitemap entries.
Note in cases such as the G1S where flow meters are not included, or are disconnected, the instant watering will be based solely on the time arguments.
Flow data would as expected not be updated.
## Thanks To
A note goes out to Bill at Link-Tap who has been extremely responsive in providing specifications, and quick fixes for a single issue noticed, as well as answering many questions about the behaviours of untested devices.

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>4.3.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.linktap</artifactId>
<name>openHAB Add-ons :: Bundles :: LinkTap Binding</name>
<properties>
<jsoup.version>1.15.4</jsoup.version>
</properties>
<dependencies>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>${jsoup.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.linktap-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-linktap" description="LinkTap Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle dependency="true">mvn:org.jsoup/jsoup/1.15.4</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.linktap/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.configuration;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link LinkTapBridgeConfiguration} class contains fields mapping bridge configuration parameters.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class LinkTapBridgeConfiguration {
public String host = "";
public String username = "";
public String password = "";
public boolean enableMDNS = true;
public boolean enableJSONComms = false;
public boolean enforceProtocolLimits = true;
}

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.configuration;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link LinkTapDeviceConfiguration} class contains fields mapping the configuration parameters for a LinkTap
* device's configuration.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class LinkTapDeviceConfiguration {
/**
* The clear text device name as reported by the API.
*/
public String name = "";
/**
* The device id as stored by the gateway to address the device.
*/
public String id = "";
/**
* If enabled the device, will enable all alerts during device initialization.
*/
public boolean enableAlerts = true;
}

View File

@ -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.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link DeviceMetaDataUpdatedHandler} enables call-backs for when the device meta-data is updated from a bridge.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public interface DeviceMetaDataUpdatedHandler {
/**
* Any registered metadata handlers, will have this
* invoked after new configuration data has been retrieved from the GW.
*
* An example use is for the discovery service to refresh its data based on received
* new configuration data of devices attached to a GW.
*/
void handleMetadataRetrieved(LinkTapBridgeHandler handler);
}

View File

@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link Firmware} class defines the firmware version.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class Firmware {
String raw;
int buildVer;
int hwVer;
public Firmware(final @Nullable String fwVersion) {
raw = "S00000000";
hwVer = 0;
buildVer = 0;
if (fwVersion == null || fwVersion.length() < 7) {
return;
} else {
raw = fwVersion;
buildVer = Integer.parseInt(raw.substring(1, 7));
}
switch (fwVersion.charAt(0)) {
case 'G':
hwVer = 2;
break;
case 'S':
hwVer = 1;
break;
default:
break;
}
}
public boolean supportsLocalConfig() {
return buildVer >= 60883;
}
public boolean supportsMDNS() {
return buildVer >= 60880;
}
public String generateTestedRevisionForHw(final int versionNo) {
return String.format("%c%05d", raw.charAt(0), versionNo);
}
public String getRecommendedMinVer() {
return generateTestedRevisionForHw(60883);
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Defines a interface that Things under the Bridge can implement to receive
* callbacks, when the bridges configuration data has been updated.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public interface IBridgeData {
/**
* Any things under a Bridge that implement this interface, will have this
* invoked after new configuration data has been retrieved from the GW.
*/
void handleBridgeDataUpdated();
}

View File

@ -0,0 +1,192 @@
/**
* 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.internal;
import java.lang.reflect.Type;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.linktap.protocol.frames.WaterMeterStatus;
import org.openhab.core.thing.ThingTypeUID;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
/**
* The {@link LinkTapBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class LinkTapBindingConstants {
private static final Type DEVICE_STATUS_CLASS_LIST_TYPE = new TypeToken<List<WaterMeterStatus.DeviceStatus>>() {
}.getType();
public static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(DEVICE_STATUS_CLASS_LIST_TYPE, new WaterMeterStatus.DeviceStatusClassTypeAdapter())
.excludeFieldsWithoutExposeAnnotation().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.disableHtmlEscaping().create();
private static final String BINDING_ID = "linktap";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_DEVICE = new ThingTypeUID(BINDING_ID, "device");
public static final ThingTypeUID THING_TYPE_GATEWAY = new ThingTypeUID(BINDING_ID, "gateway");
public static final String BRIDGE_PROP_GW_ID = "gatewayId";
public static final String BRIDGE_PROP_HW_MODEL = "hardwareModel";
public static final String BRIDGE_PROP_GW_VER = "version";
public static final String BRIDGE_PROP_MAC_ADDR = "macAddress";
public static final String BRIDGE_PROP_HTTP_API_ENABLED = "httpApiEnabled";
public static final String BRIDGE_PROP_HTTP_API_EP = "httpApiCallback";
public static final String BRIDGE_PROP_VOL_UNIT = "volumeUnit";
public static final String BRIDGE_PROP_UTC_OFFSET = "utcOffset";
public static final String BRIDGE_CONFIG_HOSTNAME = "host";
public static final String BRIDGE_CONFIG_MDNS_ENABLE = "enableMDNS";
public static final String BRIDGE_CONFIG_NON_HTML_COMM_ENABLE = "enableJSONComms";
public static final String BRIDGE_CONFIG_ENFORCE_COMM_LIMITS = "enforceProtocolLimits";
public static final String DEVICE_PROP_DEV_ID = "deviceId";
public static final String DEVICE_PROP_DEV_NAME = "deviceName";
public static final String DEVICE_CONFIG_DEV_ID = "id";
public static final String DEVICE_CONFIG_DEV_NAME = "name";
public static final String DEVICE_CONFIG_AUTO_ALERTS_ENABLE = "autoEnableAlerts";
public static final String DEVICE_CHANNEL_WATERING_MODE = "mode";
public static final String DEVICE_CHANNEL_IS_MANUAL_MODE = "manual-watering";
public static final String DEVICE_CHANNEL_ACTIVE_WATERING = "watering";
public static final String DEVICE_CHANNEL_RF_LINKED = "rf-linked";
public static final String DEVICE_CHANNEL_FLM_LINKED = "flm-linked";
public static final String DEVICE_CHANNEL_FALL_STATUS = "fall-status";
public static final String DEVICE_CHANNEL_SHUTDOWN_FAILURE = "shutdown-failure";
public static final String DEVICE_CHANNEL_HIGH_FLOW = "high-flow";
public static final String DEVICE_CHANNEL_LOW_FLOW = "low-flow";
public static final String DEVICE_CHANNEL_FINAL_SEGMENT = "eco-final";
public static final String DEVICE_CHANNEL_SIGNAL = "signal";
public static final String DEVICE_CHANNEL_BATTERY = "battery";
public static final String DEVICE_CHANNEL_WATER_CUT = "water-cut";
public static final String DEVICE_CHANNEL_CHILD_LOCK = "child-lock";
public static final String DEVICE_CHANNEL_FLOW_RATE = "flow-rate";
public static final String DEVICE_CHANNEL_CURRENT_VOLUME = "volume";
public static final String DEVICE_CHANNEL_TOTAL_DURATION = "duration";
public static final String DEVICE_CHANNEL_REMAIN_DURATION = "remaining";
public static final String DEVICE_CHANNEL_FAILSAFE_DURATION = "dur-limit";
public static final String DEVICE_CHANNEL_FAILSAFE_VOLUME = "vol-limit";
public static final String DEVICE_CHANNEL_OH_VOLUME_LIMIT = "oh-vol-limit";
public static final String DEVICE_CHANNEL_OH_DURATION_LIMIT = "oh-dur-limit";
public static final String DEVICE_CHANNEL_PAUSE_PLAN_OVERRIDE = "plan-pause-enable";
public static final String DEVICE_CHANNEL_PAUSE_PLAN_EXPIRES = "plan-resume-time";
public static final String DEVICE_CHANNEL_WATER_PLAN_ID = "watering-plan-id";
public enum WateringMode {
/**
* OFF (Ordinal 0).
*/
OFF(0, "Off"),
/**
* INSTANT (Ordinal 1).
*/
INSTANT(1, "Instant"),
/**
* CALENDAR (Ordinal 2).
*/
CALENDAR(2, "Calendar"),
/**
* DAY (Ordinal 3).
*/
DAY(3, "Day"),
/**
* ODD_EVEN (Ordinal 4).
*/
ODD_EVEN(4, "Odd-even"),
/**
* INTERVAL (Ordinal 5).
*/
INTERVAL(5, "Interval"),
/**
* MONTH (Ordinal 6).
*/
MONTH(6, "Month");
private final int value;
private final String description;
private WateringMode(final int value, final String description) {
this.value = value;
this.description = description;
}
public int getValue() {
return value;
}
public String getDesc() {
return description;
}
@Override
public String toString() {
return String.format("%d - %s", value, description);
}
}
public enum ChildLockMode {
/**
* UNLOCKED (Ordinal 0).
*/
UNLOCKED(0, "Unlocked"),
/**
* PART_LOCKED (Ordinal 1).
*/
PART_LOCKED(1, "Partially locked"),
/**
* FULLY_LOCKED (Ordinal 2).
*/
FULLY_LOCKED(2, "Completely locked");
private final int value;
private final String description;
private ChildLockMode(final int value, final String description) {
this.value = value;
this.description = description;
}
public int getValue() {
return value;
}
public String getDesc() {
return description;
}
@Override
public String toString() {
return String.format("%d - %s", value, description);
}
}
}

View File

@ -0,0 +1,265 @@
/**
* 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.internal;
import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.*;
import static org.openhab.binding.linktap.internal.LinkTapBridgeHandler.MDNS_LOOKUP;
import static org.openhab.binding.linktap.internal.Utils.cleanPrintableChars;
import java.io.UnsupportedEncodingException;
import java.net.Inet4Address;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.linktap.protocol.frames.TLGatewayFrame;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
/**
* The {@link LinkTapBridgeDiscoveryService} is an implementation of a discovery service for VeSync devices. The
* meta-data is
* read by the bridge, and the discovery data updated via a callback implemented by the DeviceMetaDataUpdatedHandler.
*
* @author David Godyear - Initial contribution
*/
@NonNullByDefault
@Component(service = MDNSDiscoveryParticipant.class, configurationPid = "discovery.linktap")
public class LinkTapBridgeDiscoveryService implements MDNSDiscoveryParticipant {
private static final String SERVICE_TYPE = "_http._tcp.local.";
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_GATEWAY);
private static final String RAW_MODEL = "model";
private static final String RAW_ID = "ID";
private static final String RAW_MAC = "MAC";
private static final String RAW_IP = "IP";
private static final String RAW_ADMIN_URL = "admin_url";
private static final String RAW_VENDOR = "vendor";
private static final String RAW_VERSION = "version";
private static final String[] KEYS = new String[] { RAW_MODEL, RAW_ID, RAW_MAC, RAW_IP, RAW_ADMIN_URL, RAW_VENDOR,
RAW_VERSION };
private static final String TEXT_CHARSET = StandardCharsets.UTF_8.name();
protected final ThingRegistry thingRegistry;
private final Logger logger = LoggerFactory.getLogger(LinkTapBridgeDiscoveryService.class);
private final TranslationProvider translationProvider;
private final LocaleProvider localeProvider;
private final Bundle bundle;
@Activate
public LinkTapBridgeDiscoveryService(final @Reference ThingRegistry thingRegistry,
@Reference TranslationProvider translationProvider, @Reference LocaleProvider localeProvider) {
this.thingRegistry = thingRegistry;
this.translationProvider = translationProvider;
this.localeProvider = localeProvider;
this.bundle = FrameworkUtil.getBundle(getClass());
}
public String getLocalizedText(String key, @Nullable Object @Nullable... arguments) {
String result = translationProvider.getText(bundle, key, key, localeProvider.getLocale(), arguments);
return Objects.nonNull(result) ? result : key;
}
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return SUPPORTED_THING_TYPES;
}
@Override
public String getServiceType() {
return SERVICE_TYPE;
}
@Override
public @Nullable DiscoveryResult createResult(ServiceInfo service) {
final String itemId = String.format("%04X", new Random().nextInt(Short.MAX_VALUE));
String qualifiedName = service.getQualifiedName();
String name = service.getName();
if (logger.isEnabledForLevel(Level.TRACE)) {
logger.trace("[{}] Device found: {}", itemId, cleanPrintableChars(qualifiedName));
}
if (!name.startsWith("LinkTapGw_")) {
logger.trace("[{}] Not a LinkTap Gateway - wrong name", itemId);
return null;
}
if (80 != service.getPort()) {
logger.trace("[{}] Not a LinkTap Gateway - incorrect port", itemId);
return null;
}
if (!"tcp".equals(service.getProtocol())) {
logger.trace("[{}] Not a LinkTap Gateway - incorrect protocol", itemId);
return null;
}
if (!"http".equals(service.getApplication())) {
logger.trace("[{}] Not a LinkTap Gateway - incorrect application", itemId);
return null;
}
ThingUID uid = getThingUID(service);
if (uid == null) {
return null;
}
Properties rawDataProps = extractProps(service);
if (rawDataProps.isEmpty()) {
return null;
}
final Map<String, Object> bridgeProperties = new HashMap<>(4);
final String gatewayId = getGwId(service.getName());
bridgeProperties.put(BRIDGE_PROP_GW_ID, gatewayId);
final String macId = (String) rawDataProps.get(RAW_MAC);
if (macId != null) {
bridgeProperties.put(BRIDGE_PROP_MAC_ADDR, macId);
}
final String version = (String) rawDataProps.get(RAW_VERSION);
if (version != null) {
bridgeProperties.put(BRIDGE_PROP_GW_VER, version);
}
final String hostname = getHostName(service);
if (hostname.isEmpty()) {
return null;
}
bridgeProperties.put(BRIDGE_CONFIG_HOSTNAME, qualifiedName);
if (gatewayId.isEmpty()) {
return null;
}
logger.debug("[{}] Discovered Gateway Id {}", itemId, gatewayId);
final String ipV4Addr = (String) rawDataProps.get(RAW_IP);
MDNS_LOOKUP.clearItem(qualifiedName);
MDNS_LOOKUP.registerItem(qualifiedName, ipV4Addr, () -> {
logger.debug("[{}] Registered mdns qualified name to IPv4 {} -> {}", itemId, qualifiedName, ipV4Addr);
List<Thing> things = thingRegistry.getAll().stream()
.filter(thing -> THING_TYPE_GATEWAY.equals(thing.getThingTypeUID())).toList();
for (final Thing thing : things) {
final ThingHandler handler = thing.getHandler();
if (handler instanceof LinkTapBridgeHandler bridgeHandler) {
bridgeHandler.attemptReconnectIfNeeded();
logger.trace("[{}] Bridge handler {} notified", itemId, handler.getThing().getLabel());
}
}
});
return DiscoveryResultBuilder.create((new ThingUID(THING_TYPE_GATEWAY, gatewayId)))
.withProperties(bridgeProperties).withLabel("LinkTap Gateway (" + gatewayId + ")")
.withRepresentationProperty(BRIDGE_PROP_GW_ID).build();
}
@Override
public @Nullable ThingUID getThingUID(ServiceInfo service) {
final Map<String, Object> bridgeProperties = new HashMap<>(4);
final String gatewayId = getGwId(service.getName());
bridgeProperties.put(BRIDGE_PROP_GW_ID, gatewayId);
if (bridgeProperties.get(BRIDGE_PROP_GW_ID) == null) {
return null;
}
return (new ThingUID(THING_TYPE_GATEWAY,
gatewayId + "_" + String.format("0x%08X", new Random().nextInt(Integer.MAX_VALUE))));
}
public Properties extractProps(ServiceInfo serviceInfo) {
final Properties result = new Properties();
String data = "";
try {
data = new String(serviceInfo.getTextBytes(), TEXT_CHARSET);
} catch (UnsupportedEncodingException uee) {
logger.warn("{}", getLocalizedText("warning.discovery-charset-missing"));
}
final int[] keyIndexes = new int[7];
for (int i = 0; i < KEYS.length; ++i) {
keyIndexes[i] = data.indexOf(KEYS[i] + "=");
}
Arrays.sort(keyIndexes);
if (keyIndexes[0] == -1) {
return result;
}
String wCopy = data;
for (int si = keyIndexes.length - 1; si > -1; --si) {
final String foundField = wCopy.substring(keyIndexes[si]).trim();
wCopy = wCopy.substring(0, keyIndexes[si]);
final Optional<String> potentialKey = Arrays.stream(KEYS).filter(foundField::startsWith).findFirst();
if (potentialKey.isPresent()) {
final String key = potentialKey.get();
result.put(key, foundField.substring(key.length() + 1));
}
}
return result;
}
@Override
public long getRemovalGracePeriodSeconds(ServiceInfo serviceInfo) {
return MDNSDiscoveryParticipant.super.getRemovalGracePeriodSeconds(serviceInfo);
}
private String getGwId(final String serviceName) {
String[] segments = serviceName.split("_");
if (segments.length > 1) {
return segments[1];
}
return TLGatewayFrame.EMPTY_STRING;
}
private String getHostName(final ServiceInfo serviceInfo) {
final Inet4Address[] addrs = serviceInfo.getInet4Addresses();
if (addrs.length == 0) {
logger.trace("No IPv4 given in mdns data");
return TLGatewayFrame.EMPTY_STRING;
}
String candidateDnsName = addrs[0].getHostName();
if (candidateDnsName.isEmpty()) {
logger.trace("No DNS given by IPv4 address from mdns data");
candidateDnsName = addrs[0].toString();
}
if (candidateDnsName.startsWith("/")) {
candidateDnsName = candidateDnsName.substring(1);
}
return candidateDnsName;
}
}

View File

@ -0,0 +1,634 @@
/**
* 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.internal;
import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.*;
import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.*;
import static org.openhab.binding.linktap.protocol.frames.ValidationError.Cause.BUG;
import static org.openhab.binding.linktap.protocol.frames.ValidationError.Cause.USER;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.validation.constraints.NotNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.linktap.configuration.LinkTapBridgeConfiguration;
import org.openhab.binding.linktap.protocol.frames.GatewayConfigResp;
import org.openhab.binding.linktap.protocol.frames.GatewayDeviceResponse;
import org.openhab.binding.linktap.protocol.frames.TLGatewayFrame;
import org.openhab.binding.linktap.protocol.frames.ValidationError;
import org.openhab.binding.linktap.protocol.http.CommandNotSupportedException;
import org.openhab.binding.linktap.protocol.http.DeviceIdException;
import org.openhab.binding.linktap.protocol.http.GatewayIdException;
import org.openhab.binding.linktap.protocol.http.InvalidParameterException;
import org.openhab.binding.linktap.protocol.http.LinkTapException;
import org.openhab.binding.linktap.protocol.http.NotTapLinkGatewayException;
import org.openhab.binding.linktap.protocol.http.TransientCommunicationIssueException;
import org.openhab.binding.linktap.protocol.http.WebServerApi;
import org.openhab.binding.linktap.protocol.servers.BindingServlet;
import org.openhab.binding.linktap.protocol.servers.IHttpClientProvider;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.config.discovery.DiscoveryServiceRegistry;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link LinkTapBridgeHandler} class defines the handler for a LinkTapHandler
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class LinkTapBridgeHandler extends BaseBridgeHandler {
public static final LookupWrapper<@Nullable LinkTapBridgeHandler> ADDR_LOOKUP = new LookupWrapper<>();
public static final LookupWrapper<@Nullable LinkTapBridgeHandler> GW_ID_LOOKUP = new LookupWrapper<>();
public static final LookupWrapper<@Nullable LinkTapHandler> DEV_ID_LOOKUP = new LookupWrapper<>();
public static final LookupWrapper<@Nullable String> MDNS_LOOKUP = new LookupWrapper<>();
private static final long MIN_TIME_BETWEEN_MDNS_SCANS_MS = 600000;
private final DiscoveryServiceRegistry discoverySrvReg;
private final TranslationProvider translationProvider;
private final LocaleProvider localeProvider;
private final Bundle bundle;
private final Logger logger = LoggerFactory.getLogger(LinkTapBridgeHandler.class);
private final Object schedulerLock = new Object();
private final Object reconnectFutureLock = new Object();
private final Object getConfigLock = new Object();
private volatile String currentGwId = "";
private volatile LinkTapBridgeConfiguration config = new LinkTapBridgeConfiguration();
private volatile long lastGwCommandRecvTs = 0L;
private volatile long lastMdnsScanMillis = -1L;
private String bridgeKey = "";
private IHttpClientProvider httpClientProvider;
private @Nullable ScheduledFuture<?> backgroundGwPollingScheduler;
private @Nullable ScheduledFuture<?> connectRepair = null;
protected ExpiringCache<String> lastGetConfigCache = new ExpiringCache<>(Duration.ofSeconds(10),
LinkTapBridgeHandler::expireCacheContents);
private static @Nullable String expireCacheContents() {
return null;
}
@Activate
public LinkTapBridgeHandler(final Bridge bridge, IHttpClientProvider httpClientProvider,
@Reference DiscoveryServiceRegistry discoveryService, @Reference TranslationProvider translationProvider,
@Reference LocaleProvider localeProvider) {
super(bridge);
this.httpClientProvider = httpClientProvider;
this.discoverySrvReg = discoveryService;
this.translationProvider = translationProvider;
this.localeProvider = localeProvider;
this.bundle = FrameworkUtil.getBundle(getClass());
TransactionProcessor.getInstance().setTranslationProviderInfo(translationProvider, localeProvider, bundle);
WebServerApi.getInstance().setTranslationProviderInfo(translationProvider, localeProvider, bundle);
BindingServlet.getInstance().setTranslationProviderInfo(translationProvider, localeProvider, bundle);
}
public String getLocalizedText(String key, @Nullable Object @Nullable... arguments) {
String result = translationProvider.getText(bundle, key, key, localeProvider.getLocale(), arguments);
return Objects.nonNull(result) ? result : key;
}
private void startGwPolling() {
synchronized (schedulerLock) {
cancelGwPolling();
backgroundGwPollingScheduler = scheduler.scheduleWithFixedDelay(() -> {
if (lastGwCommandRecvTs + 120000 < System.currentTimeMillis()) {
getGatewayConfiguration();
}
}, 5000, 120000, TimeUnit.MILLISECONDS);
}
}
private void cancelGwPolling() {
synchronized (schedulerLock) {
final ScheduledFuture<?> ref = backgroundGwPollingScheduler;
if (ref != null) {
ref.cancel(true);
backgroundGwPollingScheduler = null;
}
}
}
private void requestMdnsScan() {
final long sysMillis = System.currentTimeMillis();
if (lastMdnsScanMillis + MIN_TIME_BETWEEN_MDNS_SCANS_MS < sysMillis) {
logger.debug("Requesting MDNS Scan");
discoverySrvReg.startScan(THING_TYPE_GATEWAY, null);
lastMdnsScanMillis = sysMillis;
} else {
logger.trace("Not requesting MDNS Scan last ran under 10 min's ago");
}
}
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
config = getConfigAs(LinkTapBridgeConfiguration.class);
scheduleReconnect(0);
}
@Override
public void dispose() {
cancelReconnect();
cancelGwPolling();
deregisterBridge(this);
GW_ID_LOOKUP.deregisterItem(currentGwId, this, () -> {
});
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Set.of(LinkTapDeviceDiscoveryService.class);
}
public @Nullable String getGatewayId() {
return currentGwId;
}
private void deregisterBridge(final LinkTapBridgeHandler ref) {
if (!bridgeKey.isEmpty()) {
ADDR_LOOKUP.deregisterItem(bridgeKey, ref, () -> {
BindingServlet.getInstance().unregisterServlet();
});
bridgeKey = "";
}
}
private boolean registerBridge(final LinkTapBridgeHandler ref) {
final WebServerApi api = WebServerApi.getInstance();
api.setHttpClient(httpClientProvider.getHttpClient());
try {
final String host = getHostname();
if (!bridgeKey.equals(host)) {
deregisterBridge(this);
bridgeKey = host;
}
if (!ADDR_LOOKUP.registerItem(bridgeKey, this, () -> {
BindingServlet.getInstance().registerServlet();
})) {
return false;
}
} catch (UnknownHostException e) {
deregisterBridge(this);
return false;
}
return true;
}
public void getGatewayConfiguration() {
String resp = "";
synchronized (getConfigLock) {
resp = lastGetConfigCache.getValue();
if (lastGetConfigCache.isExpired() || resp == null || resp.isBlank()) {
TLGatewayFrame req = new TLGatewayFrame(CMD_GET_CONFIGURATION);
resp = sendApiRequest(req);
GatewayDeviceResponse respFrame = LinkTapBindingConstants.GSON.fromJson(resp,
GatewayDeviceResponse.class);
// The system may not have picked up the ID before in which case - extract it from the error response
// and re-run the request to ensure a full configuration data-set is retrieved.
// This is normally populated as part of the sendApiRequest sequencing where the gateway id is
// auto-added,
// if available.
if (req.gatewayId.isEmpty() && respFrame != null
&& respFrame.getRes() == GatewayDeviceResponse.ResultStatus.RET_GATEWAY_ID_NOT_MATCHED) {
// Use the response GW_ID from the error response - to re-request with the correct ID
// This only happens in occasional startup race conditions, but this removes a low change
// bug being hit.
req.gatewayId = respFrame.gatewayId;
resp = sendApiRequest(req);
}
lastGetConfigCache.putValue(resp);
}
}
final GatewayConfigResp gwConfig = LinkTapBindingConstants.GSON.fromJson(resp, GatewayConfigResp.class);
if (gwConfig == null) {
return;
}
currentGwId = gwConfig.gatewayId;
final String version = gwConfig.version;
final String volUnit = gwConfig.volumeUnit;
final String[] devIds = gwConfig.endDevices;
final String[] devNames = gwConfig.deviceNames;
final Integer utcOffset = gwConfig.utfOfs;
if (!version.equals(editProperties().get(BRIDGE_PROP_GW_VER))) {
final Map<String, String> props = editProperties();
props.put(BRIDGE_PROP_GW_VER, version);
updateProperties(props);
return;
}
if (!volUnit.equals(editProperties().get(BRIDGE_PROP_VOL_UNIT))) {
final Map<String, String> props = editProperties();
props.put(BRIDGE_PROP_VOL_UNIT, volUnit);
updateProperties(props);
}
if (utcOffset != DEFAULT_INT) { // This is only in later firmwares
final String strVal = String.valueOf(utcOffset);
if (!strVal.equals(editProperties().get(BRIDGE_PROP_UTC_OFFSET))) {
final Map<String, String> props = editProperties();
props.put(BRIDGE_PROP_UTC_OFFSET, strVal);
updateProperties(props);
}
}
boolean updatedDeviceInfo = devIds.length != discoveredDevices.size();
for (int i = 0; i < devIds.length; ++i) {
LinkTapDeviceMetadata deviceInfo = new LinkTapDeviceMetadata(devIds[i], devNames[i]);
LinkTapDeviceMetadata replaced = discoveredDevices.put(deviceInfo.deviceId, deviceInfo);
if (replaced != null
&& (!replaced.deviceId.equals(devIds[i]) || !replaced.deviceName.equals(devNames[i]))) {
updatedDeviceInfo = true;
}
}
handlers.forEach(x -> x.handleMetadataRetrieved(this));
if (updatedDeviceInfo) {
this.scheduler.execute(() -> {
for (Thing el : getThing().getThings()) {
final ThingHandler th = el.getHandler();
if (th instanceof IBridgeData bridgeData) {
bridgeData.handleBridgeDataUpdated();
}
}
});
}
}
public String sendApiRequest(final TLGatewayFrame req) {
final UUID uid = UUID.randomUUID();
final WebServerApi api = WebServerApi.getInstance();
String host = "Unresolved";
try {
host = getHostname();
final boolean confirmGateway = req.command != TLGatewayFrame.CMD_GET_CONFIGURATION;
if (confirmGateway && (host.isEmpty() || currentGwId.isEmpty())) {
logger.warn("{}", getLocalizedText("warning.host-gw-unknown-for-cmd", host, currentGwId, req.command));
return "";
}
if (req.gatewayId.isEmpty()) {
req.gatewayId = currentGwId;
}
final String reqData = LinkTapBindingConstants.GSON.toJson(req);
logger.debug("{} = APP BRIDGE -> GW -> Request {}", uid, reqData);
final String respData = api.sendRequest(host, reqData);
logger.debug("{} = APP BRIDGE -> GW -> Response {}", uid, respData);
final TLGatewayFrame gwResponseFrame = LinkTapBindingConstants.GSON.fromJson(respData,
TLGatewayFrame.class);
if (confirmGateway && gwResponseFrame != null && !gwResponseFrame.gatewayId.equals(req.gatewayId)) {
logger.warn("{}", getLocalizedText("warning.response-from-wrong-gw-id", uid, req.gatewayId,
gwResponseFrame.gatewayId));
return "";
}
if (gwResponseFrame != null && req.command != gwResponseFrame.command) {
logger.warn("{}",
getLocalizedText("warning.incorrect-cmd-resp", uid, req.command, gwResponseFrame.command));
return "";
}
return respData;
} catch (NotTapLinkGatewayException e) {
logger.warn("{}", getLocalizedText("warning.not-taplink-gw", uid, host));
} catch (UnknownHostException e) {
logger.warn("{}", getLocalizedText("warning.comms-issue-auto-retry", uid, e.getMessage()));
scheduleReconnect();
} catch (TransientCommunicationIssueException e) {
logger.warn("{}", getLocalizedText("warning.comms-issue-auto-retry", uid, getLocalizedText(e.getI18Key())));
scheduleReconnect();
}
return "";
}
private void connect() {
// Check if we can resolve the remote host, if so then it can be mapped back to a bridge handler.
// If not further communications would fail - so it's offline.
if (!registerBridge(this)) {
requestMdnsScan();
scheduleReconnect();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
getLocalizedText("bridge.error.host-not-found"));
return;
}
final WebServerApi api = WebServerApi.getInstance();
api.setHttpClient(httpClientProvider.getHttpClient());
try {
final Map<String, String> bridgeProps = api.getBridgeProperities(bridgeKey);
if (!bridgeProps.isEmpty()) {
final String readGwId = bridgeProps.get(BRIDGE_PROP_GW_ID);
if (readGwId != null) {
currentGwId = readGwId;
}
final Map<String, String> currentProps = editProperties();
currentProps.putAll(bridgeProps);
updateProperties(currentProps);
} else {
if (!api.unlockWebInterface(bridgeKey, config.username, config.password)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
getLocalizedText("bridge.error.check-credentials"));
return;
}
}
getGatewayConfiguration();
// Update the GW ID -> this bridge lookup
GW_ID_LOOKUP.registerItem(currentGwId, this, () -> {
});
if (Thread.currentThread().isInterrupted()) {
return;
}
@NotNull
final String hostname = getHostname(config);
String localServerAddr = "";
try (Socket socket = new Socket()) {
try {
socket.connect(new InetSocketAddress(hostname, 80), 1500);
} catch (IOException e) {
logger.warn("{}", getLocalizedText("warning.failed-local-address-detection", e.getMessage()));
throw new TransientCommunicationIssueException("Local address lookup failure",
"exception.local-addr-lookup-failure");
}
localServerAddr = socket.getLocalAddress().getHostAddress();
logger.trace("Local address for connectivity is {}", socket.getLocalAddress().getHostAddress());
} catch (IOException e) {
logger.trace("Failed to connect to remote device due to exception", e);
}
final String servletEp = BindingServlet.getServletAddress(localServerAddr,
getLocalizedText("warning.no-http-server-port"));
final Optional<String> servletEpOpt = (!servletEp.isEmpty()) ? Optional.of(servletEp) : Optional.empty();
api.configureBridge(hostname, Optional.of(config.enableMDNS), Optional.of(config.enableJSONComms),
servletEpOpt);
updateStatus(ThingStatus.ONLINE);
if (Thread.currentThread().isInterrupted()) {
return;
}
startGwPolling();
connectRepair = null;
final Firmware firmware = new Firmware(getThing().getProperties().get(BRIDGE_PROP_GW_VER));
if (!firmware.supportsLocalConfig()) {
logger.warn("{}", getLocalizedText("warning.fw-update-local-config", getThing().getLabel(),
firmware.getRecommendedMinVer()));
}
} catch (InterruptedException ignored) {
} catch (LinkTapException | NotTapLinkGatewayException e) {
deregisterBridge(this);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
getLocalizedText("bridge.error.target-is-not-gateway"));
} catch (TransientCommunicationIssueException e) {
scheduleReconnect();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
getLocalizedText("bridge.error.cannot-connect"));
} catch (UnknownHostException e) {
scheduleReconnect();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
getLocalizedText("bridge.error.unknown-host"));
}
}
private void scheduleReconnect() {
scheduleReconnect(15);
}
public void attemptReconnectIfNeeded() {
if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
synchronized (reconnectFutureLock) {
if (connectRepair != null) {
scheduleReconnect(0);
}
}
}
}
private void scheduleReconnect(int seconds) {
if (seconds < 1) {
seconds = 1;
}
logger.trace("Scheduling connection re-attempt in {} seconds", seconds);
synchronized (reconnectFutureLock) {
cancelReconnect();
connectRepair = scheduler.schedule(this::connect, seconds, TimeUnit.SECONDS); // Schedule a retry
}
}
private void cancelReconnect() {
synchronized (reconnectFutureLock) {
final @Nullable ScheduledFuture<?> ref = connectRepair;
if (ref != null) {
ref.cancel(true);
connectRepair = null;
}
}
}
@Override
public void handleCommand(final ChannelUID channelUID, final Command command) {
}
protected @NotNull String getHostname() throws UnknownHostException {
return getHostname(config);
}
private @NotNull String getHostname(final LinkTapBridgeConfiguration config) throws UnknownHostException {
@NotNull
String hostname = config.host;
final String mdnsLookup = MDNS_LOOKUP.getItem(hostname);
if (mdnsLookup != null) {
hostname = mdnsLookup;
}
return InetAddress.getByName(hostname).getHostAddress();
}
private final Object singleCommLock = new Object();
public String sendRequest(final TLGatewayFrame frame) throws DeviceIdException, InvalidParameterException {
// Validate the payload is within the expected limits for the device its being sent to
if (config.enforceProtocolLimits) {
final Collection<ValidationError> errors = frame.getValidationErrors();
if (!errors.isEmpty()) {
final String bugs = errors.stream().filter(x -> x.getCause() == BUG).map(ValidationError::toString)
.collect(Collectors.joining(","));
final String userDataIssues = errors.stream().filter(x -> x.getCause() == USER)
.map(ValidationError::toString).collect(Collectors.joining(","));
if (!bugs.isEmpty()) {
logger.warn("{}",
getLocalizedText("bug-report.unexpected-payload-failure", getThing().getLabel(), bugs));
}
if (!userDataIssues.isEmpty()) {
logger.warn("{}", getLocalizedText("warning.user-data-payload-failure", getThing().getLabel(),
userDataIssues));
}
return "";
}
}
final TransactionProcessor tp = TransactionProcessor.getInstance();
final String gatewayId = getGatewayId();
if (gatewayId == null) {
logger.warn("{}", getLocalizedText("warning.error-with-gw-id"));
return "";
}
frame.gatewayId = gatewayId;
// The gateway is a single device that may have to do RF, limit the comm's to ensure
// it can maintain a good QoS. Responses for most commands are very fast on a reasonable network.
try {
synchronized (singleCommLock) {
try {
return tp.sendRequest(this, frame);
} catch (final CommandNotSupportedException cnse) {
logger.warn("{}",
getLocalizedText("warning.device-no-accept", getThing().getLabel(), cnse.getMessage()));
}
}
} catch (final GatewayIdException gide) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, gide.getI18Key());
}
return "";
}
public ThingUID getUID() {
return thing.getUID();
}
/**
* Discovery handling of Gateway owned Devices
*/
public void registerMetaDataUpdatedHandler(DeviceMetaDataUpdatedHandler dmduh) {
handlers.add(dmduh);
}
public void unregisterMetaDataUpdatedHandler(DeviceMetaDataUpdatedHandler dmduh) {
handlers.remove(dmduh);
}
private final CopyOnWriteArrayList<DeviceMetaDataUpdatedHandler> handlers = new CopyOnWriteArrayList<>();
private ConcurrentMap<String, LinkTapDeviceMetadata> discoveredDevices = new ConcurrentHashMap<>();
public final Stream<LinkTapDeviceMetadata> getDiscoveredDevices() {
return discoveredDevices.values().stream();
}
public final Map<String, LinkTapDeviceMetadata> getDeviceLookup() {
return discoveredDevices;
}
public void processGatewayCommand(final int commandId, final String frame) {
logger.debug("{} processing gateway request with command {}", this.getThing().getLabel(), commandId);
// Store this so that the only when necessary can polls be done - aka
// no direct from Gateway communications.
lastGwCommandRecvTs = System.currentTimeMillis();
switch (commandId) {
case CMD_HANDSHAKE:
lastGetConfigCache.invalidateValue();
processCommand0(frame);
break;
case CMD_RAINFALL_DATA:
case CMD_NOTIFICATION_WATERING_SKIPPED:
case CMD_DATETIME_SYNC:
logger.debug("No implementation for command {} for processing the GW request", commandId);
}
}
private void processCommand0(final String request) {
final GatewayConfigResp decoded = LinkTapBindingConstants.GSON.fromJson(request, GatewayConfigResp.class);
// Check the current version property matches and if not update it
final String currentVerKnown = editProperties().get(BRIDGE_PROP_GW_VER);
if (decoded != null && currentVerKnown != null && !decoded.version.isEmpty()) {
if (!currentVerKnown.equals(decoded.version)) {
final Map<String, String> currentProps = editProperties();
currentProps.put(BRIDGE_PROP_GW_VER, decoded.version);
updateProperties(currentProps);
}
}
final String currentVolUnit = editProperties().get(BRIDGE_PROP_VOL_UNIT);
if (decoded != null && currentVolUnit != null && !decoded.volumeUnit.isEmpty()) {
if (!currentVolUnit.equals(decoded.volumeUnit)) {
final Map<String, String> currentProps = editProperties();
currentProps.put(BRIDGE_PROP_VOL_UNIT, decoded.volumeUnit);
updateProperties(currentProps);
}
}
final String[] devices = decoded != null ? decoded.endDevices : EMPTY_STRING_ARRAY;
// Go through all the device ID's returned check we know about them.
// If not a background scan should be done
boolean fullScanRequired = false;
if (discoveredDevices.size() != devices.length) {
fullScanRequired = true;
}
if (!discoveredDevices.keySet().containsAll(Arrays.stream(devices).toList())) {
fullScanRequired = true;
}
if (fullScanRequired) {
logger.trace("The configured devices have changed a full scan should be run");
scheduler.execute(this::getGatewayConfiguration);
}
}
@Override
public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
scheduler.execute(this::getGatewayConfiguration);
super.childHandlerDisposed(childHandler, childThing);
}
}

View File

@ -0,0 +1,102 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.internal;
import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ServiceScope;
/**
* The {@link LinkTapDeviceDiscoveryService} is an implementation of a discovery service for VeSync devices. The
* meta-data is
* read by the bridge, and the discovery data updated via a callback implemented by the DeviceMetaDataUpdatedHandler.
*
* @author David Godyear - Initial contribution
*/
@NonNullByDefault
@Component(scope = ServiceScope.PROTOTYPE, service = LinkTapDeviceDiscoveryService.class, configurationPid = "discovery.linktap.devices")
public class LinkTapDeviceDiscoveryService extends AbstractThingHandlerDiscoveryService<LinkTapBridgeHandler>
implements DeviceMetaDataUpdatedHandler {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_GATEWAY);
private static final int DISCOVER_TIMEOUT_SECONDS = 5;
private @NonNullByDefault({}) ThingUID bridgeUID;
/**
* Creates a VeSyncDiscoveryService with enabled autostart.
*/
public LinkTapDeviceDiscoveryService() {
super(LinkTapBridgeHandler.class, SUPPORTED_THING_TYPES, DISCOVER_TIMEOUT_SECONDS);
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return SUPPORTED_THING_TYPES;
}
@Override
public void activate() {
final Map<String, Object> properties = new HashMap<>();
properties.put(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY, Boolean.TRUE);
super.activate(properties);
}
@Override
public void initialize() {
bridgeUID = thingHandler.getUID();
super.initialize();
}
@Override
protected void startBackgroundDiscovery() {
thingHandler.registerMetaDataUpdatedHandler(this);
}
@Override
protected void stopBackgroundDiscovery() {
thingHandler.unregisterMetaDataUpdatedHandler(this);
}
@Override
protected void startScan() {
// If the bridge is not online no other thing devices can be found, so no reason to scan at this moment.
removeOlderResults(getTimestampOfLastScan());
}
@Override
public void handleMetadataRetrieved(final LinkTapBridgeHandler handler) {
thingHandler.getDiscoveredDevices().map(x -> {
final Map<String, Object> properties = new HashMap<>(4);
properties.put(DEVICE_PROP_DEV_ID, x.deviceId);
properties.put(DEVICE_PROP_DEV_NAME, x.deviceName);
properties.put(DEVICE_CONFIG_DEV_ID, x.deviceId);
properties.put(DEVICE_CONFIG_DEV_NAME, x.deviceName);
properties.put(DEVICE_CONFIG_AUTO_ALERTS_ENABLE, true);
return DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_DEVICE, bridgeUID, x.deviceId))
.withBridge(bridgeUID).withProperties(properties).withLabel(x.deviceName)
.withRepresentationProperty(DEVICE_PROP_DEV_ID).build();
}).forEach(this::thingDiscovered);
}
}

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link LinkTapDeviceMetadata} class contains the definition of a devices metadata as given by a Gateway device.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class LinkTapDeviceMetadata {
/**
* The ID of the device as stored in the relevant Gateway Device.
*/
public final String deviceId;
/**
* The human-readable name of the device as stored in the relevant Gateway Device.
*/
public final String deviceName;
public LinkTapDeviceMetadata(final String deviceId, final String deviceName) {
this.deviceId = deviceId;
this.deviceName = deviceName;
}
}

View File

@ -0,0 +1,464 @@
/**
* 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.internal;
import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.*;
import static org.openhab.binding.linktap.protocol.frames.DismissAlertReq.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.linktap.protocol.frames.AlertStateReq;
import org.openhab.binding.linktap.protocol.frames.DeviceCmdReq;
import org.openhab.binding.linktap.protocol.frames.DismissAlertReq;
import org.openhab.binding.linktap.protocol.frames.EndpointDeviceResponse;
import org.openhab.binding.linktap.protocol.frames.LockReq;
import org.openhab.binding.linktap.protocol.frames.PauseWateringPlanReq;
import org.openhab.binding.linktap.protocol.frames.StartWateringReq;
import org.openhab.binding.linktap.protocol.frames.WaterMeterStatus;
import org.openhab.binding.linktap.protocol.http.InvalidParameterException;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.storage.Storage;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link LinkTapHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class LinkTapHandler extends PollingDeviceHandler {
private static final String DEFAULT_INST_WATERING_VOL_LIMIT = "0";
private static final String DEFAULT_INST_WATERING_TIME_LIMIT = "15";
private static final List<String> READBACK_DISABLED_CHANNELS = List.of(DEVICE_CHANNEL_OH_VOLUME_LIMIT,
DEVICE_CHANNEL_OH_DURATION_LIMIT);
private final Logger logger = LoggerFactory.getLogger(LinkTapHandler.class);
private final Storage<String> strStore;
private final Object pausePlanLock = new Object();
private volatile boolean pausePlanActive = false;
private @Nullable ScheduledFuture<?> pausePlanFuture = null;
public LinkTapHandler(Thing thing, Storage<String> strStore, TranslationProvider translationProvider,
LocaleProvider localeProvider) {
super(thing, translationProvider, localeProvider);
this.strStore = strStore;
}
/**
* Abstract method implementations from PollingDeviceHandler
* required for the lifecycle of LinkTap devices.
*/
@Override
protected void runStartInit() {
try {
if (config.enableAlerts) {
sendRequest(new AlertStateReq(0, true));
}
final String[] chansToRefresh = new String[] { DEVICE_CHANNEL_PAUSE_PLAN_OVERRIDE,
DEVICE_CHANNEL_PAUSE_PLAN_EXPIRES, DEVICE_CHANNEL_OH_DURATION_LIMIT,
DEVICE_CHANNEL_OH_VOLUME_LIMIT };
for (String chanId : chansToRefresh) {
final Channel pausePlanChan = getThing().getChannel(chanId);
if (pausePlanChan != null) {
handleCommand(pausePlanChan.getUID(), RefreshType.REFRESH);
}
}
final String pausePlanState = strStore.get(DEVICE_CHANNEL_PAUSE_PLAN_OVERRIDE);
if (OnOffType.ON.toString().equals(pausePlanState)) {
scheduleRenewPlanPause();
}
} catch (final InvalidParameterException ipe) {
logger.warn("{}", getLocalizedText("bug-report.failed-alert-enable", getThing().getLabel()));
}
}
@Override
protected void registerDevice() {
LinkTapBridgeHandler.DEV_ID_LOOKUP.registerItem(registeredDeviceId, this, () -> {
});
}
@Override
protected void deregisterDevice() {
LinkTapBridgeHandler.DEV_ID_LOOKUP.deregisterItem(registeredDeviceId, this, () -> {
});
}
@Override
protected String getPollResponseData() {
try {
return sendRequest(new DeviceCmdReq(CMD_UPDATE_WATER_TIMER_STATUS));
} catch (final InvalidParameterException ipe) {
logger.warn("{}", getLocalizedText("bug-report.poll-failure", getThing().getLabel()));
return "";
}
}
@Override
protected void processPollResponseData(String data) {
processCommand3(data);
}
/**
* OpenHab handlers
*/
@Override
public void dispose() {
cancelPlanPauseRenew();
super.dispose();
}
private void scheduleRenewPlanPause() {
synchronized (pausePlanLock) {
cancelPlanPauseRenew();
pausePlanFuture = scheduler.scheduleWithFixedDelay(this::requestPlanPause, 0, 55, TimeUnit.MINUTES);
pausePlanActive = true;
}
}
private boolean isPlanPauseActive() {
return pausePlanActive;
}
private void cancelPlanPauseRenew() {
synchronized (pausePlanLock) {
ScheduledFuture<?> ref = pausePlanFuture;
if (ref != null) {
ref.cancel(false);
pausePlanFuture = null;
}
pausePlanActive = false;
}
}
private void requestPlanPause() {
try {
final String respRaw = sendRequest(new PauseWateringPlanReq(1.0));
final EndpointDeviceResponse devResp = GSON.fromJson(respRaw, EndpointDeviceResponse.class);
if (devResp != null && devResp.isSuccess()) {
final DateTimeType expiryTime = new DateTimeType(LocalDateTime.now().plusHours(1).toString());
strStore.put(DEVICE_CHANNEL_PAUSE_PLAN_EXPIRES, expiryTime.format(null));
updateState(DEVICE_CHANNEL_PAUSE_PLAN_EXPIRES, expiryTime);
}
} catch (final InvalidParameterException ignored) {
logger.warn("{}", getLocalizedText("bug-report.pause-plan-failure", getThing().getLabel()));
}
}
@Override
public void handleCommand(final ChannelUID channelUID, final Command command) {
scheduler.submit(() -> {
try {
if (command instanceof RefreshType) {
switch (channelUID.getId()) {
case DEVICE_CHANNEL_PAUSE_PLAN_EXPIRES: {
final String savedVal = strStore.get(DEVICE_CHANNEL_PAUSE_PLAN_EXPIRES);
if (savedVal != null) {
updateState(DEVICE_CHANNEL_PAUSE_PLAN_EXPIRES, new DateTimeType(savedVal));
}
}
break;
case DEVICE_CHANNEL_OH_DURATION_LIMIT: {
final String savedVal = strStore.get(DEVICE_CHANNEL_OH_DURATION_LIMIT);
if (savedVal != null) {
updateState(DEVICE_CHANNEL_OH_DURATION_LIMIT,
new QuantityType<>(Integer.valueOf(savedVal), Units.SECOND));
} else {
updateState(DEVICE_CHANNEL_OH_DURATION_LIMIT, new QuantityType<>(15, Units.SECOND));
}
}
break;
case DEVICE_CHANNEL_OH_VOLUME_LIMIT: {
final String savedVal = strStore.get(DEVICE_CHANNEL_OH_VOLUME_LIMIT);
if (savedVal != null) {
updateState(DEVICE_CHANNEL_OH_VOLUME_LIMIT,
new QuantityType<>(Integer.valueOf(savedVal), Units.LITRE));
} else {
updateState(DEVICE_CHANNEL_OH_VOLUME_LIMIT, new QuantityType<>(10, Units.LITRE));
}
}
break;
case DEVICE_CHANNEL_PAUSE_PLAN_OVERRIDE:
final String savedVal = strStore.get(DEVICE_CHANNEL_PAUSE_PLAN_OVERRIDE);
updateState(DEVICE_CHANNEL_OH_VOLUME_LIMIT,
OnOffType.ON.toString().equals(savedVal) ? OnOffType.ON : OnOffType.OFF);
break;
default:
pollForUpdate(false);
}
} else if (command instanceof QuantityType quantityCommand) {
int targetValue = quantityCommand.intValue();
switch (channelUID.getId()) {
case DEVICE_CHANNEL_OH_DURATION_LIMIT:
strStore.put(DEVICE_CHANNEL_OH_DURATION_LIMIT, String.valueOf(targetValue));
break;
case DEVICE_CHANNEL_OH_VOLUME_LIMIT:
strStore.put(DEVICE_CHANNEL_OH_VOLUME_LIMIT, String.valueOf(targetValue));
break;
}
} else if (command instanceof StringType stringCmd) {
switch (channelUID.getId()) {
case DEVICE_CHANNEL_CHILD_LOCK: {
sendRequest(new LockReq(Integer.valueOf(command.toString())));
}
break;
}
} else if (command instanceof OnOffType) {
// Alert dismiss events below
switch (channelUID.getId()) {
case DEVICE_CHANNEL_PAUSE_PLAN_OVERRIDE:
strStore.put(DEVICE_CHANNEL_PAUSE_PLAN_OVERRIDE, command.toString());
if (OnOffType.ON.equals(command)) {
scheduleRenewPlanPause();
} else {
cancelPlanPauseRenew();
}
break;
case DEVICE_CHANNEL_ACTIVE_WATERING:
if (OnOffType.ON.equals(command)) {
String volLimit = strStore.get(DEVICE_CHANNEL_OH_VOLUME_LIMIT);
if (volLimit == null) {
volLimit = DEFAULT_INST_WATERING_VOL_LIMIT;
}
String durLimit = strStore.get(DEVICE_CHANNEL_OH_DURATION_LIMIT);
if (durLimit == null) {
durLimit = DEFAULT_INST_WATERING_TIME_LIMIT;
}
sendRequest(
new StartWateringReq(Integer.parseInt(durLimit), Integer.parseInt(volLimit)));
} else if (OnOffType.OFF.equals(command)) {
sendRequest(new DeviceCmdReq(CMD_IMMEDIATE_WATER_STOP));
}
case DEVICE_CHANNEL_FALL_STATUS: // 1
if (OnOffType.OFF.equals(command)) {
sendRequest(new DismissAlertReq(ALERT_DEVICE_FALL));
}
break;
case DEVICE_CHANNEL_SHUTDOWN_FAILURE: // 2
if (OnOffType.OFF.equals(command)) {
sendRequest(new DismissAlertReq(ALERT_VALVE_SHUTDOWN_FAIL));
}
break;
case DEVICE_CHANNEL_WATER_CUT: // 3
if (OnOffType.OFF.equals(command)) {
sendRequest(new DismissAlertReq(ALERT_WATER_CUTOFF));
}
break;
case DEVICE_CHANNEL_HIGH_FLOW: // 4
if (OnOffType.OFF.equals(command)) {
sendRequest(new DismissAlertReq(ALERT_UNEXPECTED_HIGH_FLOW));
}
break;
case DEVICE_CHANNEL_LOW_FLOW: // 5
if (OnOffType.OFF.equals(command)) {
sendRequest(new DismissAlertReq(ALERT_UNEXPECTED_LOW_FLOW));
}
break;
}
}
if (!READBACK_DISABLED_CHANNELS.contains(channelUID.getId())) {
requestReadbackPoll();
}
} catch (final InvalidParameterException ipe) {
logger.warn("{}",
getLocalizedText("warning.parameter-not-accepted", getThing().getLabel(), channelUID.getId()));
}
});
}
/**
* LinkTap communication protocol handlers
*/
public void processDeviceCommand(final int commandId, final String frame) {
receivedDataPush();
logger.debug("{} processing device request with command {}", this.getThing().getLabel(), commandId);
switch (commandId) {
case CMD_UPDATE_WATER_TIMER_STATUS:
// Store the latest value in the cache - to prevent unnecessary polls
lastPollResultCache.putValue(frame);
processCommand3(frame);
break;
case CMD_NOTIFICATION_WATERING_SKIPPED:
case CMD_RAINFALL_DATA:
case CMD_DATETIME_SYNC:
logger.trace("No implementation for command {} for processing the Device request", commandId);
}
}
private void processCommand3(final String request) {
// There are three different formats that can arrive in this method:
// -> Unsolicited with is a WaterMeterStatus.DeviceStatus payload
// -> Solicited with a WaterMeterStatus payload (*)
// -> Solicited with a WaterMeterStatus payload within an array
// (*) A GSON plugin normalises the non array wrapped version to the array based version
// This is handled below before the normalised processing takes place.
WaterMeterStatus.DeviceStatus devStatus;
{
WaterMeterStatus mStatus = GSON.fromJson(request, WaterMeterStatus.class);
if (mStatus == null) {
return;
}
if (!mStatus.deviceStatuses.isEmpty()) {
devStatus = mStatus.deviceStatuses.get(0);
} else {
devStatus = GSON.fromJson(request, WaterMeterStatus.DeviceStatus.class);
}
if (devStatus == null) {
return;
}
}
// Normalized processing below which uses devStatus
final LinkTapBridgeHandler bridgeHandler = (LinkTapBridgeHandler) getBridgeHandler();
String volumeUnit = "L";
if (bridgeHandler != null) {
String volumeUnitProp = bridgeHandler.getThing().getProperties().get(BRIDGE_PROP_VOL_UNIT);
if (volumeUnitProp != null) {
volumeUnit = volumeUnitProp;
}
}
String prevPlanId = strStore.get(DEVICE_CHANNEL_WATER_PLAN_ID);
if (prevPlanId == null) {
prevPlanId = "0";
}
final String currPlanId = String.valueOf(devStatus.planSerialNo);
if (isPlanPauseActive() && !prevPlanId.equals(currPlanId)) {
scheduleRenewPlanPause();
}
updateState(DEVICE_CHANNEL_WATER_PLAN_ID, new StringType(String.valueOf(devStatus.planSerialNo)));
strStore.put(DEVICE_CHANNEL_WATER_PLAN_ID, String.valueOf(devStatus.planSerialNo));
final Integer planModeRaw = devStatus.planMode;
if (planModeRaw != null) {
updateState(DEVICE_CHANNEL_WATERING_MODE, new StringType(WateringMode.values()[planModeRaw].getDesc()));
}
final Integer childLockRaw = devStatus.childLock;
if (childLockRaw != null) {
updateState(DEVICE_CHANNEL_CHILD_LOCK, new StringType(ChildLockMode.values()[childLockRaw].getDesc()));
}
updateOnOffValue(DEVICE_CHANNEL_IS_MANUAL_MODE, devStatus.isManualMode);
updateOnOffValue(DEVICE_CHANNEL_ACTIVE_WATERING, devStatus.isWatering);
updateOnOffValue(DEVICE_CHANNEL_RF_LINKED, devStatus.isRfLinked);
updateOnOffValue(DEVICE_CHANNEL_FLM_LINKED, devStatus.isFlmPlugin);
updateOnOffValue(DEVICE_CHANNEL_FALL_STATUS, devStatus.isFall);
updateOnOffValue(DEVICE_CHANNEL_SHUTDOWN_FAILURE, devStatus.isBroken);
updateOnOffValue(DEVICE_CHANNEL_HIGH_FLOW, devStatus.isLeak);
updateOnOffValue(DEVICE_CHANNEL_LOW_FLOW, devStatus.isClog);
updateOnOffValue(DEVICE_CHANNEL_FINAL_SEGMENT, devStatus.isFinal);
updateOnOffValue(DEVICE_CHANNEL_WATER_CUT, devStatus.isCutoff);
final Integer signal = devStatus.signal;
if (signal != null) {
updateState(DEVICE_CHANNEL_SIGNAL, new QuantityType<>(signal, Units.PERCENT));
}
final Integer battery = devStatus.battery;
if (battery != null) {
updateState(DEVICE_CHANNEL_BATTERY, new QuantityType<>(battery, Units.PERCENT));
}
final Integer totalDuration = devStatus.totalDuration;
if (totalDuration != null) {
updateState(DEVICE_CHANNEL_TOTAL_DURATION, new QuantityType<>(totalDuration, Units.SECOND));
}
final Integer remainDuration = devStatus.remainDuration;
if (remainDuration != null) {
updateState(DEVICE_CHANNEL_REMAIN_DURATION, new QuantityType<>(remainDuration, Units.SECOND));
}
final Integer failsafeDuration = devStatus.failsafeDuration;
if (failsafeDuration != null) {
updateState(DEVICE_CHANNEL_FAILSAFE_DURATION, new QuantityType<>(failsafeDuration, Units.SECOND));
}
final Double speed = devStatus.speed;
if (speed != null) {
updateState(DEVICE_CHANNEL_FLOW_RATE, new QuantityType<>(speed,
"L".equals(volumeUnit) ? Units.LITRE_PER_MINUTE : ImperialUnits.GALLON_PER_MINUTE));
}
final Double volume = devStatus.volume;
if (volume != null) {
updateState(DEVICE_CHANNEL_CURRENT_VOLUME,
new QuantityType<>(volume, "L".equals(volumeUnit) ? Units.LITRE : ImperialUnits.GALLON_LIQUID_US));
}
final Double volumeLimit = devStatus.volumeLimit;
if (volumeLimit != null) {
updateState(DEVICE_CHANNEL_FAILSAFE_VOLUME, new QuantityType<>(volumeLimit,
"L".equals(volumeUnit) ? Units.LITRE : ImperialUnits.GALLON_LIQUID_US));
}
}
private void updateOnOffValue(final String channelName, final @Nullable Boolean value) {
if (value != null) {
updateState(channelName, OnOffType.from(value));
}
}
@Override
public void handleBridgeDataUpdated() {
switch (getThing().getStatus()) {
case OFFLINE:
case UNKNOWN:
logger.trace("Handling new bridge data for {}", getThing().getLabel());
final LinkTapBridgeHandler bridge = (LinkTapBridgeHandler) getBridgeHandler();
if (bridge != null) {
if (bridge.getThing().getStatus().equals(ThingStatus.OFFLINE)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return;
}
initAfterBridge(bridge);
}
break;
default:
logger.trace("Handling new bridge data for {} not required", getThing().getLabel());
}
}
}

View File

@ -0,0 +1,95 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.internal;
import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.*;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.linktap.protocol.servers.BindingServlet;
import org.openhab.binding.linktap.protocol.servers.IHttpClientProvider;
import org.openhab.core.config.discovery.DiscoveryServiceRegistry;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.storage.Storage;
import org.openhab.core.storage.StorageService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.HttpService;
/**
* The {@link LinkTapHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.linktap", service = ThingHandlerFactory.class)
public class LinkTapHandlerFactory extends BaseThingHandlerFactory implements IHttpClientProvider {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_DEVICE, THING_TYPE_GATEWAY);
private final StorageService storageService;
private final DiscoveryServiceRegistry discSrvReg;
private final HttpClientFactory httpClientFactory;
private final TranslationProvider translationProvider;
private final LocaleProvider localeProvider;
@Activate
public LinkTapHandlerFactory(@Reference HttpService httpService, @Reference StorageService storageService,
@Reference DiscoveryServiceRegistry discoveryService, @Reference HttpClientFactory httpClientFactory,
@Reference TranslationProvider translationProvider, @Reference LocaleProvider localeProvider) {
this.storageService = storageService;
this.discSrvReg = discoveryService;
this.httpClientFactory = httpClientFactory;
BindingServlet.getInstance().setHttpService(httpService);
this.translationProvider = translationProvider;
this.localeProvider = localeProvider;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_DEVICE.equals(thingTypeUID)) {
final Storage<String> storage = storageService.getStorage(thing.getUID().toString(),
String.class.getClassLoader());
return new LinkTapHandler(thing, storage, translationProvider, localeProvider);
} else if (THING_TYPE_GATEWAY.equals(thingTypeUID)) {
return new LinkTapBridgeHandler((Bridge) thing, this, discSrvReg, translationProvider, localeProvider);
}
return null;
}
@Override
public HttpClient getHttpClient() {
return httpClientFactory.getCommonHttpClient();
}
}

View File

@ -0,0 +1,89 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.internal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.validation.constraints.NotNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link LookupWrapper} is a container providing common functionality for providing
* key -> T mappings. The backend store is ConcurrentHashMap.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class LookupWrapper<@Nullable itemT> {
final Map<@NotNull String, @Nullable itemT> storeLookup = new ConcurrentHashMap<>();
/**
* Register using key the given T instance, and after addition call the specified Runnable
*
* @param key - The key for the item
* @param item - The instance to store a reference to
* @param afterAddition - The runnable to run after the addition has been completed
* @return - false if another item is already assigned to the key preventing the addition, or true
* when added successfully.
*/
public boolean registerItem(final @NotNull String key, final @NotNull itemT item,
@NotNull final Runnable afterAddition) {
if (storeLookup.containsKey(key)) {
final itemT found = storeLookup.get(key);
if (found != null && !found.equals(item)) {
return false;
}
}
storeLookup.put(key, item);
afterAddition.run();
return true;
}
/**
* Remove the given key and item combination
*
* @param key - The expected key of the item
* @param item - The item referenced by the key
* @param whenEmpty - Runnable executed when no more key -> item mappings exist
*/
public void deregisterItem(final @NotNull String key, final @NotNull itemT item,
@NotNull final Runnable whenEmpty) {
storeLookup.remove(key, item);
if (storeLookup.isEmpty()) {
whenEmpty.run();
}
}
/**
* Returns the item associated to the given key
*
* @param key - the key to find the item for
* @return - null if no item is found otherwise the found item
*/
public @Nullable itemT getItem(final @NotNull String key) {
return storeLookup.get(key);
}
/**
* Clears a entry when only the given key is known
*
* @param key - the key remove if it exists
*/
public void clearItem(final @NotNull String key) {
storeLookup.remove(key);
}
}

View File

@ -0,0 +1,339 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.internal;
import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.DEVICE_CHANNEL_OH_DURATION_LIMIT;
import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.DEVICE_CHANNEL_OH_VOLUME_LIMIT;
import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.EMPTY_STRING;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.validation.constraints.NotNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.linktap.configuration.LinkTapDeviceConfiguration;
import org.openhab.binding.linktap.protocol.frames.DeviceCmdReq;
import org.openhab.binding.linktap.protocol.frames.TLGatewayFrame;
import org.openhab.binding.linktap.protocol.http.DeviceIdException;
import org.openhab.binding.linktap.protocol.http.InvalidParameterException;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.types.RefreshType;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PollingDeviceHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public abstract class PollingDeviceHandler extends BaseThingHandler implements IBridgeData {
protected static final String MARKER_INVALID_DEVICE_KEY = "---INVALID---";
private final Logger logger = LoggerFactory.getLogger(PollingDeviceHandler.class);
private final Object pollLock = new Object();
private final Object schedulerLock = new Object();
private final Object readBackPollLock = new Object();
private final TranslationProvider translationProvider;
private final LocaleProvider localeProvider;
private final Bundle bundle;
protected volatile LinkTapDeviceConfiguration config = new LinkTapDeviceConfiguration();
private volatile long lastStatusCommandRecvTs = 0L;
protected String registeredDeviceId = EMPTY_STRING;
protected ExpiringCache<String> lastPollResultCache = new ExpiringCache<>(Duration.ofSeconds(5),
PollingDeviceHandler::expireCacheContents);
private @Nullable ScheduledFuture<?> backgroundGwPollingScheduler;
private @Nullable ScheduledFuture<?> readBackPollSf = null;
protected void requestReadbackPoll() {
synchronized (readBackPollLock) {
cancelReadbackPoll();
scheduler.schedule(() -> {
pollForUpdate(true);
}, 750, TimeUnit.MILLISECONDS);
}
}
protected void cancelReadbackPoll() {
synchronized (readBackPollLock) {
ScheduledFuture<?> readBackPollSfRef = readBackPollSf;
if (readBackPollSfRef != null) {
readBackPollSfRef.cancel(false);
readBackPollSf = null;
}
}
}
public PollingDeviceHandler(final Thing thing, TranslationProvider translationProvider,
LocaleProvider localeProvider) {
super(thing);
this.translationProvider = translationProvider;
this.localeProvider = localeProvider;
this.bundle = FrameworkUtil.getBundle(getClass());
}
public String getLocalizedText(String key, @Nullable Object @Nullable... arguments) {
String result = translationProvider.getText(bundle, key, key, localeProvider.getLocale(), arguments);
return Objects.nonNull(result) ? result : key;
}
private void startStatusPolling() {
synchronized (schedulerLock) {
cancelStatusPolling();
backgroundGwPollingScheduler = scheduler.scheduleWithFixedDelay(() -> {
if (lastStatusCommandRecvTs + 135000 > System.currentTimeMillis()) {
return;
}
pollForUpdate(false);
}, 1, 10, TimeUnit.SECONDS);
}
}
private void cancelStatusPolling() {
synchronized (schedulerLock) {
final ScheduledFuture<?> ref = backgroundGwPollingScheduler;
if (ref != null) {
ref.cancel(true);
backgroundGwPollingScheduler = null;
}
}
}
private static @Nullable String expireCacheContents() {
return null;
}
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
config = getConfigAs(LinkTapDeviceConfiguration.class);
if (!(getBridgeHandler() instanceof LinkTapBridgeHandler bridgeHandler)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
getLocalizedText("polling-device.error.bridge-unset"));
return;
} else if (ThingStatus.OFFLINE.equals(bridgeHandler.getThing().getStatus())) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return;
}
scheduler.execute(() -> {
if (ThingStatus.ONLINE.equals(bridgeHandler.getThing().getStatus())) {
initAfterBridge(bridgeHandler);
}
});
}
protected void initAfterBridge(final LinkTapBridgeHandler bridge) {
String deviceId = getValidatedIdString();
if (MARKER_INVALID_DEVICE_KEY.equals(deviceId)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
getLocalizedText("polling-device.error.device-unknown-in-bridge"));
if (!registeredDeviceId.isBlank()) {
deregisterDevice();
}
registeredDeviceId = EMPTY_STRING;
return;
} else {
registeredDeviceId = deviceId;
}
boolean knownToBridge = bridge.getDiscoveredDevices().anyMatch(x -> deviceId.equals(x.deviceId));
if (knownToBridge) {
updateStatus(ThingStatus.ONLINE);
registerDevice();
scheduleInitialPoll();
scheduler.execute(this::runStartInit);
startStatusPolling();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
getLocalizedText("polling-device.error.unknown-device-id"));
}
}
protected abstract void runStartInit();
protected abstract void registerDevice();
@Override
public void dispose() {
cancelInitialPoll(true);
deregisterDevice();
cancelStatusPolling();
}
protected abstract void deregisterDevice();
@Nullable
BridgeHandler getBridgeHandler() {
Bridge bridgeRef = getBridge();
if (bridgeRef == null) {
return null;
} else {
return bridgeRef.getHandler();
}
}
public String sendRequest(TLGatewayFrame frame) throws InvalidParameterException {
if (frame instanceof DeviceCmdReq devCmdReq) {
final String deviceAddr = getValidatedIdString();
if (deviceAddr.equals(MARKER_INVALID_DEVICE_KEY)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
getLocalizedText("polling-device.error.unknown-device"));
return EMPTY_STRING;
}
devCmdReq.deviceId = deviceAddr;
}
final Bridge parentBridge = getBridge();
if (parentBridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
getLocalizedText("polling-device.error.bridge-unset"));
return EMPTY_STRING;
}
final LinkTapBridgeHandler parentBridgeHandler = (LinkTapBridgeHandler) parentBridge.getHandler();
if (parentBridgeHandler == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
getLocalizedText("polling-device.error.bridge-unset"));
return EMPTY_STRING;
}
try {
return parentBridgeHandler.sendRequest(frame);
} catch (final DeviceIdException die) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, getLocalizedText(die.getI18Key()));
}
return EMPTY_STRING;
}
@NotNull
public String getValidatedIdString() {
BridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler instanceof LinkTapBridgeHandler vesyncBridgeHandler) {
final String devId = config.id;
// Try to use the device address id directly
if (!devId.isEmpty()) {
logger.trace("Searching for device address id : {}", devId);
@Nullable
final LinkTapDeviceMetadata metadata = vesyncBridgeHandler.getDeviceLookup().get(devId);
if (metadata != null) {
return metadata.deviceId;
}
}
final String deviceName = config.name;
// Check if the device name can be matched to a single device
if (!deviceName.isEmpty()) {
final String[] matchedAddressIds = vesyncBridgeHandler.getDiscoveredDevices()
.filter(x -> deviceName.equals(x.deviceName)).map(x -> x.deviceId).toArray(String[]::new);
for (String val : matchedAddressIds) {
logger.trace("Found Address ID match on name with : {}", val);
}
if (matchedAddressIds.length != 1) {
return MARKER_INVALID_DEVICE_KEY;
}
return matchedAddressIds[0];
}
}
return MARKER_INVALID_DEVICE_KEY;
}
@Override
public void channelLinked(ChannelUID channelUID) {
super.channelLinked(channelUID);
if (getThing().getStatusInfo().getStatus() == ThingStatus.ONLINE) {
scheduler.execute(() -> {
pollForUpdate(false);
});
}
}
private void scheduleInitialPoll() {
cancelInitialPoll(false);
initialPollingTask = scheduler.schedule(() -> {
// 15 second's is to ensure even slow systems have time to pull the gateway data
// ready.
sendChannelRefresh(DEVICE_CHANNEL_OH_DURATION_LIMIT);
sendChannelRefresh(DEVICE_CHANNEL_OH_VOLUME_LIMIT);
pollForUpdate(false);
}, 15, TimeUnit.SECONDS);
}
private void sendChannelRefresh(final String channelName) {
final Channel ch = getThing().getChannel(DEVICE_CHANNEL_OH_VOLUME_LIMIT);
if (ch != null) {
handleCommand(ch.getUID(), RefreshType.REFRESH);
}
}
private void cancelInitialPoll(final boolean interruptAllowed) {
final ScheduledFuture<?> pollJob = initialPollingTask;
if (pollJob != null && !pollJob.isCancelled()) {
pollJob.cancel(interruptAllowed);
initialPollingTask = null;
}
}
@Nullable
// This is used to coalesce poll's for CMD 3 - WATER METER STATUS
// otherwise bulk new channel links force many poll's, and an unsolicited update
// may recently have already provided the data needed.
ScheduledFuture<?> initialPollingTask = null;
public void pollForUpdate(boolean skipCache) {
String response = EMPTY_STRING;
synchronized (pollLock) {
response = lastPollResultCache.getValue();
if (response == null || skipCache) {
response = getPollResponseData();
lastPollResultCache.putValue(response);
}
}
processPollResponseData(response);
}
protected abstract String getPollResponseData();
protected abstract void processPollResponseData(final String data);
protected void receivedDataPush() {
lastStatusCommandRecvTs = System.currentTimeMillis();
}
}

View File

@ -0,0 +1,344 @@
/**
* 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.internal;
import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.BRIDGE_PROP_UTC_OFFSET;
import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.GSON;
import static org.openhab.binding.linktap.protocol.frames.GatewayDeviceResponse.*;
import static org.openhab.binding.linktap.protocol.http.TransientCommunicationIssueException.TransientExecptionDefinitions.*;
import java.net.UnknownHostException;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.UUID;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.linktap.protocol.frames.DeviceCmdReq;
import org.openhab.binding.linktap.protocol.frames.GatewayDeviceResponse;
import org.openhab.binding.linktap.protocol.frames.HandshakeResp;
import org.openhab.binding.linktap.protocol.frames.TLGatewayFrame;
import org.openhab.binding.linktap.protocol.frames.WaterMeterStatus;
import org.openhab.binding.linktap.protocol.http.CommandNotSupportedException;
import org.openhab.binding.linktap.protocol.http.DeviceIdException;
import org.openhab.binding.linktap.protocol.http.GatewayIdException;
import org.openhab.binding.linktap.protocol.http.InvalidParameterException;
import org.openhab.binding.linktap.protocol.http.NotTapLinkGatewayException;
import org.openhab.binding.linktap.protocol.http.TransientCommunicationIssueException;
import org.openhab.binding.linktap.protocol.http.WebServerApi;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.thing.ThingStatus;
import org.osgi.framework.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link TransactionProcessor} is a transaction processor, that each Gateway has an instance of.
* It is responsible for handling received frames from the Gateway.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public final class TransactionProcessor {
// The Gateway pushes messages to us, the majority expect a response and are documented as
// GW->Broker->App. These are sent via a HTTP request to the WebSerlet listening for the payloads.
// Then we can also send data to the Gateway, these all also typically get a response, and are documented as
// App->Broker->GW. These are sent via a POST request, to the relevant Gateway.
// As the Gateway is an embedded device,
private static final WebServerApi API = WebServerApi.getInstance();
private static final int MAX_COMMAND_RETRIES = 3;
private static final TransactionProcessor INSTANCE = new TransactionProcessor();
private final Logger logger = LoggerFactory.getLogger(TransactionProcessor.class);
private @Nullable TranslationProvider translationProvider;
private @Nullable LocaleProvider localeProvider;
private @Nullable Bundle bundle;
public void setTranslationProviderInfo(TranslationProvider translationProvider, LocaleProvider localeProvider,
Bundle bundle) {
this.bundle = bundle;
this.localeProvider = localeProvider;
this.translationProvider = translationProvider;
}
public String getLocalizedText(String key, @Nullable Object @Nullable... arguments) {
TranslationProvider translationProv = translationProvider;
LocaleProvider localeProv = localeProvider;
if (translationProv == null || localeProv == null) {
return key;
}
String result = translationProv.getText(bundle, key, key, localeProv.getLocale(), arguments);
return Objects.nonNull(result) ? result : key;
}
private TransactionProcessor() {
}
public static TransactionProcessor getInstance() {
return INSTANCE;
}
public String processGwRequest(final String sourceHost, int command, final String payload) {
final UUID uid = UUID.randomUUID();
logger.debug("{} = GW -> APP Request {} -> Payload {}", uid, sourceHost, payload);
String response = "";
try {
processGw(sourceHost, command, payload);
} catch (CommandNotSupportedException cnse) {
logger.warn("{}", getLocalizedText("bug-report.gw-unsupported-command", command));
}
logger.debug("{} = GW -> APP Response {} -> Payload {}", uid, payload, response);
return response;
}
public String processGw(final String sourceHost, int command, final String payload)
throws CommandNotSupportedException {
final GatewayDeviceResponse frame = GSON.fromJson(payload, GatewayDeviceResponse.class);
if (frame == null) {
return "";
}
final String fromGatewayId = frame.gatewayId;
final LinkTapBridgeHandler bridge = LinkTapBridgeHandler.GW_ID_LOOKUP.getItem(fromGatewayId);
if (bridge != null) {
logger.trace("Found bridge with ID: {} -> {}", fromGatewayId, bridge.getThing().getLabel());
} else {
logger.trace("Bridge not found with ID: {}", fromGatewayId);
}
// Only the water timer status payload arrives without a command, if there is a command id
// then we use the one from the frame instead.
command = CMD_UPDATE_WATER_TIMER_STATUS;
if (frame.command != DEFAULT_INT) {
command = frame.command;
}
final ResultStatus resultStatus = frame.getRes();
if (resultStatus == ResultStatus.RET_CMD_NOT_SUPPORTED) {
throw new CommandNotSupportedException(resultStatus);
}
String response = "";
switch (command) {
case CMD_UPDATE_WATER_TIMER_STATUS:
WaterMeterStatus meterStatus = GSON.fromJson(payload, WaterMeterStatus.class);
if (meterStatus != null) {
final String devId = meterStatus.deviceStatuses.get(0).deviceId;
final LinkTapHandler device = LinkTapBridgeHandler.DEV_ID_LOOKUP.getItem(devId);
if (device != null) {
logger.trace("Found device with ID: {} -> {}", devId, device.getThing().getLabel());
device.processDeviceCommand(command, payload);
} else {
logger.debug("No device with id {} found to process command {}",
meterStatus.deviceStatuses.get(0).deviceId, command);
}
}
break;
case CMD_HANDSHAKE:
case CMD_DATETIME_SYNC:
response = generateTimeDateResponse(bridge, frame.gatewayId, command);
if (bridge != null) {
bridge.processGatewayCommand(CMD_HANDSHAKE, payload);
}
break;
case CMD_NOTIFICATION_WATERING_SKIPPED: {
// This does not work - device id is within devStat!
final DeviceCmdReq devFrame = GSON.fromJson(payload, DeviceCmdReq.class);
if (devFrame != null) {
final LinkTapHandler device = LinkTapBridgeHandler.DEV_ID_LOOKUP.getItem(devFrame.deviceId);
if (device != null) {
logger.trace("Found device with ID: {} -> {}", devFrame.deviceId, device.getThing().getLabel());
device.processDeviceCommand(command, payload);
} else {
logger.debug("No device with id {} found to process command {}", devFrame.deviceId, command);
}
}
break;
}
case CMD_RAINFALL_DATA: {
final DeviceCmdReq devFrame = GSON.fromJson(payload, DeviceCmdReq.class);
if (devFrame != null) {
final LinkTapHandler device = LinkTapBridgeHandler.DEV_ID_LOOKUP.getItem(devFrame.deviceId);
if (device != null) {
logger.trace("Found device with ID: {} -> {}", devFrame.deviceId, device.getThing().getLabel());
device.processDeviceCommand(command, payload);
} else {
logger.trace("No device modelled to process meter status command");
}
}
break;
}
default:
logger.warn("{}", getLocalizedText("warning.unexpected-response-frame", command, payload));
}
return response;
}
public String sendRequest(final LinkTapBridgeHandler handler, final TLGatewayFrame request)
throws GatewayIdException, DeviceIdException, CommandNotSupportedException, InvalidParameterException {
if (handler.getThing().getStatus().equals(ThingStatus.OFFLINE)) {
logger.trace("Gateway offline");
return "";
}
int triesLeft = MAX_COMMAND_RETRIES;
int retry = 0;
while (triesLeft > 0) {
try {
return sendSingleRequest(handler, request);
} catch (TransientCommunicationIssueException tcie) {
--triesLeft;
try {
Thread.sleep(1000L * retry);
} catch (InterruptedException ie) {
return "";
}
++retry;
}
}
return "";
}
public String sendSingleRequest(final LinkTapBridgeHandler handler, final TLGatewayFrame request)
throws GatewayIdException, DeviceIdException, CommandNotSupportedException, InvalidParameterException,
TransientCommunicationIssueException {
// We need the hostname from the handler of the bridge
// Responses can be one of the following types
try {
UUID uid = UUID.randomUUID();
final String targetHost = handler.getHostname();
final String payloadJson = GSON.toJson(request);
logger.debug("{} = APP -> GW Request {} -> Payload {}", uid, targetHost, payloadJson);
String response = API.sendRequest(targetHost, GSON.toJson(request));
logger.debug("{} = APP -> GW Response {} -> Payload {}", uid, targetHost, response.trim());
GatewayDeviceResponse gatewayFrame = GSON.fromJson(response, GatewayDeviceResponse.class);
if (gatewayFrame == null) {
throw new TransientCommunicationIssueException(COMMUNICATIONS_LOST);
}
if (!(request.command == CMD_UPDATE_WATER_TIMER_STATUS && gatewayFrame.command == -1)
&& request.command != gatewayFrame.command) {
logger.warn("{}",
getLocalizedText("warning.incorrect-cmd-resp", request.command, gatewayFrame.command));
throw new TransientCommunicationIssueException(COMMUNICATIONS_LOST);
}
final ResultStatus rs = gatewayFrame.getRes();
switch (gatewayFrame.command) {
case CMD_ADD_END_DEVICE: // 1
case CMD_REMOVE_END_DEVICE: // 2
case CMD_UPDATE_WATER_TIMER_STATUS: // 3
case CMD_SETUP_WATER_PLAN: // 4
case CMD_REMOVE_WATER_PLAN: // 5
case CMD_IMMEDIATE_WATER_START: // 6
case CMD_IMMEDIATE_WATER_STOP: // 7
case CMD_RAINFALL_DATA: // 8
case CMD_ALERT_ENABLEMENT: // 10
case CMD_ALERT_DISMISS: // 11
case CMD_LOCKOUT_STATE: // 12
case CMD_DATETIME_READ: // 14
case CMD_WIRELESS_CHECK: // 15
case CMD_GET_CONFIGURATION: // 16
case CMD_SET_CONFIGURATION: // 17
case CMD_PAUSE_WATER_PLAN: // 18
switch (rs) {
case RET_SUCCESS:
logger.trace("Request successfully processed");
return response;
case RET_MESSAGE_FORMAT_ERR:
case RET_BAD_PARAMETER:
logger.trace("Request issued incorrectly - format or parameter error");
throw new InvalidParameterException(rs);
case RET_CMD_NOT_SUPPORTED:
logger.trace("Command not supported by device");
throw new CommandNotSupportedException(rs);
case RET_DEVICE_ID_ERROR:
case RET_DEVICE_NOT_FOUND:
logger.trace("Device configuration error - check DEVICE ID in metadata");
throw new DeviceIdException(rs);
case RET_GATEWAY_ID_NOT_MATCHED:
logger.trace("Gateway configuration error - check GATEWAY ID in metadata");
throw new GatewayIdException(rs);
case RET_GATEWAY_BUSY:
case RET_GW_INTERNAL_ERR:
logger.trace("The request can be re-tried");
break;
case RET_CONFLICT_WATER_PLAN:
logger.trace("Gateway rejected command due to water plan conflict");
break;
case INVALID:
default:
logger.warn("{}", getLocalizedText("warning.unexpected-cmd-result"));
}
break;
case DEFAULT_INT:
if (request.command == CMD_UPDATE_WATER_TIMER_STATUS) {
return response;
}
default:
logger.warn("{}", getLocalizedText("warning.unexpected-response-frame", gatewayFrame.command,
GSON.toJson(request)));
return "";
}
return response;
} catch (NotTapLinkGatewayException e) {
logger.warn("{}", getLocalizedText("warning.non-gw"));
} catch (UnknownHostException e) {
throw new TransientCommunicationIssueException(HOST_NOT_RESOLVED);
}
return "";
}
private String generateTimeDateResponse(final @Nullable LinkTapBridgeHandler bridge, final String gwId,
final int commandId) {
final LocalDateTime currentTime = LocalDateTime.now();
int wday = currentTime.getDayOfWeek().getValue();
String dateStr = currentTime.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String timeStr = currentTime.format(DateTimeFormatter.ofPattern("HHmmss"));
// Presume we are running in local time, unless we have the bridge modelled with the relevant UTC data
if (bridge != null) {
final String utcOffset = bridge.getThing().getProperties().get(BRIDGE_PROP_UTC_OFFSET);
if (utcOffset != null && !utcOffset.isEmpty()) {
OffsetDateTime odt = currentTime.atOffset(ZoneOffset.UTC);
odt = odt.plusSeconds(Long.parseLong(utcOffset));
wday = odt.getDayOfWeek().getValue();
dateStr = odt.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
timeStr = odt.format(DateTimeFormatter.ofPattern("HHmmss"));
}
}
final HandshakeResp respPayload = new HandshakeResp();
respPayload.command = commandId;
respPayload.gatewayId = gwId;
respPayload.wday = wday;
respPayload.date = dateStr;
respPayload.time = timeStr;
return GSON.toJson(respPayload);
}
}

View File

@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link Utils} contains static function's for useful functionality.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public final class Utils {
private Utils() {
}
/**
* This cleans a string down to characters relevant to a mdns reply
* '()*+,-./0-9:;<=>?@[\]^_`a-z{|}~ ensuring characters not included
* in this range are removed.
*
* @param chars - The string to be cleansed
* @return The string with only the relevant characters included
*/
public static String cleanPrintableChars(final String chars) {
final StringBuilder stBldr = new StringBuilder(chars.length());
for (char ch : chars.toCharArray()) {
final byte chBy = (byte) ch;
if (chBy >= 32 && chBy <= 126) {
stBldr.append(ch);
}
}
return stBldr.toString();
}
/**
* Return the localized error message if available otherwise the
* message. Should they both (either be null or empty) then return the
* class name of the throwable.
*
* @param t - The throwable to extract the data from
* @return The most representative text for the message
*/
public static String getMessage(final @Nullable Throwable t) {
if (t == null) {
return "?";
}
final String localizedMsg = t.getLocalizedMessage();
if (localizedMsg != null) {
if (!localizedMsg.isBlank()) {
return localizedMsg;
}
}
final String msg = t.getMessage();
if (msg != null) {
return msg;
}
return t.getClass().getName();
}
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link AlertStateReq} defines the request to enable or disable alerts from a given device.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class AlertStateReq extends DismissAlertReq {
public AlertStateReq() {
}
public AlertStateReq(final int alert, final boolean enable) {
this.command = CMD_ALERT_ENABLEMENT;
this.alert = alert;
this.enable = enable;
}
/**
* Defines the alert type to be enabled or disabled
*/
@SerializedName("enable")
@Expose
public boolean enable;
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link DeviceCmdReq} is a request targetted to a device.
*
* @provides App: Device ID
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class DeviceCmdReq extends TLGatewayFrame {
public DeviceCmdReq() {
}
public DeviceCmdReq(final int command) {
this.command = command;
}
/**
* Defines the targetted device ID
*/
@SerializedName("dev_id")
@Expose
public String deviceId = EMPTY_STRING;
public Collection<ValidationError> getValidationErrors() {
Collection<ValidationError> errors = super.getValidationErrors();
if (!DEVICE_ID_PATTERN.matcher(deviceId).matches() && !SUB_DEVICE_ID_PATTERN.matcher(deviceId).matches()) {
errors.add(new ValidationError("dev_id", "is invalid"));
}
return errors;
}
}

View File

@ -0,0 +1,86 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link DismissAlertReq} defines the request to dismiss alerts from a given device.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class DismissAlertReq extends DeviceCmdReq {
public DismissAlertReq() {
}
public DismissAlertReq(final int alert) {
this.command = CMD_ALERT_DISMISS;
this.alert = alert;
}
/**
* Defines the alert type the dismiss is for.
*/
@SerializedName("alert")
@Expose
public int alert = DEFAULT_INT;
@Override
public Collection<ValidationError> getValidationErrors() {
Collection<ValidationError> errors = super.getValidationErrors();
if (alert < ALERT_TYPES_ALL || alert > ALERT_UNEXPECTED_LOW_FLOW) {
errors.add(new ValidationError("alert",
"not in range " + ALERT_TYPES_ALL + " -> " + ALERT_UNEXPECTED_LOW_FLOW));
}
return errors;
}
/**
* Alert - 0. All types of alert
*/
public static final int ALERT_TYPES_ALL = 0;
/**
* Alert - 1. Device fall alert
*/
public static final int ALERT_DEVICE_FALL = 1;
/**
* Alert - 2. Valve shutdown failure alert
*/
public static final int ALERT_VALVE_SHUTDOWN_FAIL = 2;
/**
* Alert - 3. Water cut-off alert
*/
public static final int ALERT_WATER_CUTOFF = 3;
/**
* Alert - 4. Unusually high flow alert
*/
public static final int ALERT_UNEXPECTED_HIGH_FLOW = 4;
/**
* Alert - 5. Unusually low flow alert
*/
public static final int ALERT_UNEXPECTED_LOW_FLOW = 5;
}

View File

@ -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.frames;
import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link EndpointDeviceResponse} defines the response from the Gateway which includes
* the targetted endpoint device ID.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class EndpointDeviceResponse extends GatewayDeviceResponse {
public EndpointDeviceResponse() {
}
/**
* Defines the Endpoint Device ID the return value is for
*/
@SerializedName("dev_id")
@Expose
public String deviceId = EMPTY_STRING;
public Collection<ValidationError> getValidationErrors() {
final Collection<ValidationError> errors = super.getValidationErrors();
if (!DEVICE_ID_PATTERN.matcher(deviceId).matches()) {
errors.add(new ValidationError("dev_id", "is invalid"));
}
return errors;
}
}

View File

@ -0,0 +1,79 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link HandshakeResp} informs the Gateway of the current date, time and weekday in response to
* a HandshakeReq Frame.
*
* @provides Gw: Expects response of HandshakeResp, to inform the Gateway of the current local Date and Time
* @replyTo HandshakeReq
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class GatewayConfigResp extends HandshakeReq {
public GatewayConfigResp() {
}
/**
* Defines the units of measurement for volume
* L = Litres
* gal = Gallon
*/
@SerializedName("vol_unit")
@Expose
public String volumeUnit = EMPTY_STRING;
/**
* Defines the UTC offset the gateway TZ is located in (seconds offset)
*/
@SerializedName("utc_ofs")
@Expose
public Integer utfOfs = DEFAULT_INT;
/**
* Defines the names assigned to each Endpoint device.
*/
@SerializedName("dev_name")
@Expose
public String[] deviceNames = EMPTY_STRING_ARRAY;
public Collection<ValidationError> getValidationErrors() {
final Collection<ValidationError> errors = super.getValidationErrors();
if (deviceNames.length != endDevices.length) {
errors.add(new ValidationError("dev_name,end_dev", "DeviceNames != EndDevices length"));
}
return errors;
}
/**
* Unit Volume - Unit tag for gallons
*/
public static final String UNIT_VOL_GALLON = "gal";
/**
* Unit Volume - Unit tag for litres
*/
public static final String UNIT_VOL_LITRES = "L";
}

View File

@ -0,0 +1,181 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link GatewayDeviceResponse} defines the response from the Gateway when a status
* is given about the state of the requested command.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class GatewayDeviceResponse extends TLGatewayFrame {
public GatewayDeviceResponse() {
}
/**
* Defines the processing result from the gateway
*/
@SerializedName("ret")
@Expose(serialize = false, deserialize = true)
private @Nullable Integer returnValue = null;
@Expose(serialize = false, deserialize = false)
private ResultStatus cachedResEnum = ResultStatus.INVALID;
public ResultStatus getRes() {
if (cachedResEnum == ResultStatus.INVALID) {
final Integer retValClone = returnValue;
if (retValClone != null) {
cachedResEnum = ResultStatus.values()[retValClone.intValue()];
}
}
return cachedResEnum;
}
public boolean isSuccess() {
return ResultStatus.RET_SUCCESS == getRes();
}
public boolean isRetryableError() {
switch (getRes()) {
case RET_CONFLICT_WATER_PLAN: // Conflict with watering plan
case RET_GW_INTERNAL_ERR: // Gateway internal error
return true;
default:
return false;
}
}
public Collection<ValidationError> getValidationErrors() {
final Collection<ValidationError> errors = super.getValidationErrors();
if (ResultStatus.INVALID == getRes()) {
errors.add(new ValidationError("res", "is invalid"));
}
return errors;
}
public enum ResultStatus {
/**
* RET_SUCCESS (Ordinal 0).
*/
RET_SUCCESS(0, "Success", false, "protocol.ret.success"),
/**
* RET_MESSAGE_FORMAT_ERR (Ordinal 1).
*/
RET_MESSAGE_FORMAT_ERR(1, "Message format error", false, "protocol.ret.format-error"),
/**
* RET_CMD_NOT_SUPPORTED (Ordinal 2).
*/
RET_CMD_NOT_SUPPORTED(2, "CMD message not supported", false, "protocol.ret.cmd-unsupported"),
/**
* RET_GATEWAY_ID_NOT_MATCHED (Ordinal 3).
*/
RET_GATEWAY_ID_NOT_MATCHED(3, "Gateway ID not matched", false, "protocol.ret.gw-id-unmatched"),
/**
* RET_DEVICE_ID_ERROR (Ordinal 4).
*/
RET_DEVICE_ID_ERROR(4, "End device ID error", false, "protocol.ret.end-device-id-error"),
/**
* RET_DEVICE_NOT_FOUND (Ordinal 5).
*/
RET_DEVICE_NOT_FOUND(5, "End device ID not found", false, "protocol.ret.end-device-id-not-found"),
/**
* RET_GW_INTERNAL_ERR (Ordinal 6).
*/
RET_GW_INTERNAL_ERR(6, "Gateway internal error", true, "protocol.ret.gw-internal-error"),
/**
* RET_CONFLICT_WATER_PLAN (Ordinal 7).
*/
RET_CONFLICT_WATER_PLAN(7, "Conflict with watering plan", false, "protocol.ret.conflict-watering-plan"),
/**
* RET_GATEWAY_BUSY (Ordinal 8).
*/
RET_GATEWAY_BUSY(8, "Gateway busy", true, "protocol.ret.gw-busy"),
/**
* RET_BAD_PARAMETER (Ordinal 9).
*/
RET_BAD_PARAMETER(9, "Bad parameter in message", false, "protocol.ret.bad-parameter-in-msg"),
/**
* INVALID (Ordinal -1).
*/
INVALID(-1, "Not Provided", false, "protocol.ret.invalid");
private final int value;
private final String description;
private final boolean retry;
private final String i18Key;
private ResultStatus(final int value, final String description, final boolean retry, final String i18Key) {
this.value = value;
this.description = description;
this.retry = retry;
this.i18Key = i18Key;
}
public int getValue() {
return value;
}
public String getDesc() {
return description;
}
public boolean getCanRetry() {
return retry;
}
public String getI18Key() {
return i18Key;
}
@Override
public String toString() {
return String.format("%d - %s", value, description);
}
}
/*
* public static final int RET_SUCCESS = 0;
* public static final int RET_MESSAGE_FORMAT_ERR = 1;
* public static final int RET_CMD_NOT_SUPPORTED = 2;
* public static final int RET_GATEWAY_ID_NOT_MATCHED = 3;
* public static final int RET_DEVICE_ID_ERROR = 4;
* public static final int RET_DEVICE_NOT_FOUND = 5;
* public static final int RET_GW_INTERNAL_ERR = 6;
* public static final int RET_CONFLICT_WATER_PLAN = 7;
* public static final int RET_GATEWAY_BUSY = 8;
* public static final int RET_BAD_PARAMETER = 9;
*/
}

View File

@ -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 java.util.Collection;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link GatewayEndDevListReq} is a reusable frame used for multiple commands where a device endpoint list is
* included.
*
* @provides: Endpoint Device ID List
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class GatewayEndDevListReq extends TLGatewayFrame {
protected static final Pattern FULL_DEVICE_ID_PATTERN = Pattern.compile("[a-zA-Z0-9]{20}");
public GatewayEndDevListReq() {
}
/**
* Defines the endpoint devices added / registered to the Gateway.
* Limited to the first 16 digits and letters of the Device ID
*/
@SerializedName("end_dev")
@Expose
public String[] endDevices = EMPTY_STRING_ARRAY;
public Collection<ValidationError> getValidationErrors() {
final Collection<ValidationError> errors = super.getValidationErrors();
for (String ed : endDevices) {
if (command == CMD_ADD_END_DEVICE) {
if (!FULL_DEVICE_ID_PATTERN.matcher(ed).matches()) {
errors.add(new ValidationError("end_dev", "endDevice " + ed + " invalid"));
}
} else {
if (!DEVICE_ID_PATTERN.matcher(ed).matches()) {
errors.add(new ValidationError("end_dev", "endDevice " + ed + " invalid"));
}
}
}
return errors;
}
}

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link HandshakeReq} is a handshake from the Gateway.
*
* @provides App: Gateway ID, Firmware revision, Registered / Addded Endpoint Device ID List
* @response Gw: Expects response of HandshakeResp, to inform the Gateway of the current local Date and Time
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class HandshakeReq extends GatewayEndDevListReq {
public HandshakeReq() {
}
/**
* Defines the firmware version identifier.
*/
@SerializedName("ver")
@Expose
public String version = EMPTY_STRING;
public Collection<ValidationError> getValidationErrors() {
final Collection<ValidationError> errors = super.getValidationErrors();
if (version.isEmpty()) {
errors.add(new ValidationError("ver", "nis empty"));
}
return errors;
}
}

View File

@ -0,0 +1,89 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link HandshakeResp} informs the Gateway of the current date, time and weekday in response to
* a HandshakeReq Frame.
*
* @provides Gw: Expects response of HandshakeResp, to inform the Gateway of the current local Date and Time
* @replyTo HandshakeReq
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class HandshakeResp extends GatewayDeviceResponse {
public HandshakeResp() {
}
/**
* Defines the date in the format YYYYMMDD
*/
@SerializedName("date")
@Expose
public String date = EMPTY_STRING;
/**
* Defines the time for the GW in the format HHMMSS
*/
@SerializedName("time")
@Expose
public String time = EMPTY_STRING;
/**
* Defines the weekday for the GW
* 1 represents Monday.... 7 represents Sunday
*/
@SerializedName("wday")
@Expose
public int wday = DEFAULT_INT;
static final String DATE_PATTERN = "yyyyMMdd";
static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN);
static final String TIME_PATTERN = "HHmmss";
static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(TIME_PATTERN);
public Collection<ValidationError> getValidationErrors() {
final Collection<ValidationError> errors = super.getValidationErrors();
if (wday < 1 || wday > 7) {
errors.add(new ValidationError("wday", "not in range 1 -> 7"));
}
try {
LocalDate.parse(date, DATE_FORMATTER);
} catch (DateTimeParseException e) {
errors.add(new ValidationError("date", "is invalid"));
}
try {
LocalTime.parse(time, TIME_FORMATTER);
} catch (DateTimeParseException e) {
errors.add(new ValidationError("time", "is invalid"));
}
return errors;
}
}

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link IPayloadValidator} when implemented for frame definitions, allows the payload's to
* be validated as accurate data.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public interface IPayloadValidator {
/**
* This will return any validation errors with the payload, or otherwise
* a empty Collection.
*
* @return Collection of ValidationError instances highlighting payload issues
*/
Collection<ValidationError> getValidationErrors();
Collection<ValidationError> EMPTY_COLLECTION = Collections
.unmodifiableCollection(new ArrayList<ValidationError>(0));
}

View File

@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link LockReq} defines the request to dismiss alerts from a given device.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class LockReq extends DeviceCmdReq {
public LockReq() {
}
public LockReq(final int lock) {
this.command = CMD_LOCKOUT_STATE;
this.lock = lock;
}
/**
* Defines the lock type to reqest
*/
@SerializedName("lock")
@Expose
public int lock = DEFAULT_INT;
public Collection<ValidationError> getValidationErrors() {
final Collection<ValidationError> errors = super.getValidationErrors();
if (lock < LOCK_UNLOCKED || lock > LOCK_FULL) {
errors.add(new ValidationError("lock", "not in range " + LOCK_UNLOCKED + " -> " + LOCK_FULL));
}
return errors;
}
/**
* Lock - 0. Device is unlocked
*/
public static final int LOCK_UNLOCKED = 0;
/**
* Lock - 1. Partially locked
*/
public static final int LOCK_PARTIALLY = 1;
/**
* Lock - 2. Completely locked
*/
public static final int LOCK_FULL = 2;
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link PauseWateringPlanReq} requests the watering plan is disabled for a duration of hours.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class PauseWateringPlanReq extends DeviceCmdReq {
public PauseWateringPlanReq() {
}
public PauseWateringPlanReq(final double duration) {
this.command = CMD_PAUSE_WATER_PLAN;
this.duration = duration;
}
/**
* Defines the duration the watering plan is to be paused for.
* Acceptable range is between 0.1 to 240
* Units is hours
*/
@SerializedName("duration")
@Expose
public Double duration = 0.0;
public Collection<ValidationError> getValidationErrors() {
final Collection<ValidationError> errors = super.getValidationErrors();
if (duration < 0.1 || duration > 240) {
errors.add(new ValidationError("rain", "not in range 0.1 -> 240"));
}
return errors;
}
}

View File

@ -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 java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link DismissAlertReq} defines the payload to represent rainfall data.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class RainData extends TLGatewayFrame {
public RainData() {
}
/**
* Defines the past rainfall [0] and future rainfull [1] measurements in mm
*/
@SerializedName("rain")
@Expose
public double[] rainfallData = new double[] { 0.0, 0.0 };
public void setPastRainfall(final double pastRainMM) {
rainfallData[0] = pastRainMM;
}
public double getPastRainfall() {
return rainfallData[0];
}
public void setFutureRainfall(final double futureRainMM) {
rainfallData[1] = futureRainMM;
}
public double getFutureRainfall() {
return rainfallData[1];
}
public Collection<ValidationError> getValidationErrors() {
final Collection<ValidationError> errors = super.getValidationErrors();
if (rainfallData.length != 2) {
errors.add(new ValidationError("rain", "has a invalid number of parameters"));
}
return errors;
}
}

View File

@ -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.frames;
import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link DismissAlertReq} defines the payload to represent rainfall forecast data.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class RainDataForecast extends RainData {
public RainDataForecast() {
}
/**
* Defines the effective time relevant for the rainfall data returned.
* Minimum value is 1
*/
@SerializedName("valid_duration")
@Expose
public int validDuration = DEFAULT_INT;
public Collection<ValidationError> getValidationErrors() {
final Collection<ValidationError> errors = super.getValidationErrors();
if (validDuration < 1) {
errors.add(new ValidationError("valid_duration", "is less than 1"));
}
return errors;
}
}

View File

@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link SetDeviceConfigReq} sets the configuration parameter specified for a particular device.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class SetDeviceConfigReq extends DeviceCmdReq {
public SetDeviceConfigReq() {
}
public SetDeviceConfigReq(final String tag, final int value) {
this.command = CMD_SET_CONFIGURATION;
this.tag = tag;
this.value = value;
}
/**
* The value to send for the given tag
*/
@SerializedName("value")
@Expose
public int value = 0;
/**
* The tag that the value suppied is to be used for
*/
@SerializedName("tag")
@Expose
public String tag = EMPTY_STRING;
public Collection<ValidationError> getValidationErrors() {
final Collection<ValidationError> errors = super.getValidationErrors();
switch (tag) {
case CONFIG_VOLUME_LIMIT:
case CONFIG_DURATION_LIMIT:
break;
default:
errors.add(new ValidationError("tag", "invalid tag \"" + tag + "\""));
}
return errors;
}
/**
* Config - Water Volume Limit
*/
public static final String CONFIG_VOLUME_LIMIT = "volume_limit";
/**
* Config - Time Duration Limit
*/
public static final String CONFIG_DURATION_LIMIT = "total_duration";
}

View File

@ -0,0 +1,151 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import static org.openhab.binding.linktap.protocol.frames.HandshakeResp.DATE_FORMATTER;
import static org.openhab.binding.linktap.protocol.frames.ValidationError.Cause.BUG;
import static org.openhab.binding.linktap.protocol.frames.ValidationError.Cause.USER;
import static org.openhab.binding.linktap.protocol.frames.WaterMeterStatus.OP_MODE_INSTANT;
import static org.openhab.binding.linktap.protocol.frames.WaterMeterStatus.OP_MODE_MONTH;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link SetupWaterPlan} defines the request to dismiss alerts from a given device.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public abstract class SetupWaterPlan extends DeviceCmdReq {
public SetupWaterPlan() {
}
/**
* Defines the unique identifier for the instance of the watering plan
* that we are sending.
*/
@SerializedName("plan_sn")
@Expose
public int planSerialNo = DEFAULT_INT;
/**
* Defines the watering mode which can be:
* watering mode (1 - Instant Mode, 2 - Calendar mode, 3 - 7 day mode, 4 - Odd-even mode, 5
* - Interval mode, 6 - Month mode).
* See OP_MODE_INSTANT....
*/
@SerializedName("mode")
@Expose
public int mode = DEFAULT_INT;
/**
* Defines the eco mode options... only used by Instant mode but in all payloads
* eco: the ECO mode works in a way that the valve opens for X seconds then closes
* for Y seconds X, Y, X, Y, . in [X, Y], X denotes the Valve ON duration,
* Y denotes Valve OFF duration. The ECO mode will not be applied if either X or Y is zero.
*/
protected int[] eco = new int[] { 0, 0 };
/**
* Defines the watering plan information for the mode specified
*/
@SerializedName("sch")
@Expose
public WaterSchedule schedule = new WaterSchedule();
protected class WaterSchedule implements IPayloadValidator {
@Override
public Collection<ValidationError> getValidationErrors() {
return EMPTY_COLLECTION;
}
}
public Collection<ValidationError> getValidationErrors() {
final Collection<ValidationError> errors = super.getValidationErrors();
if (planSerialNo == DEFAULT_INT) {
errors.add(new ValidationError("plan_sn", "is invalid"));
}
if (mode < OP_MODE_INSTANT || mode > OP_MODE_MONTH) {
errors.add(new ValidationError("mode", "mode not in range " + OP_MODE_INSTANT + " -> " + OP_MODE_MONTH));
}
if (eco.length != 2) {
errors.add(new ValidationError("eco", "number of parameters is incorrect"));
}
return errors;
}
public class WaterPlanInstant extends WaterSchedule {
/**
* Defines the timestamp (YYYYMMDDHHMMSS) that the Instant Mode will take effect
*/
@SerializedName("timestamp")
@Expose
public String timestamp = EMPTY_STRING;
/**
* Defines the target capacity for a watering session, measured in liters or gallons
* (according to the Volume unit configuration in the gateway management page).
* The minimum value is 1. When the water timer has a flow meter connected, if its
* value is greater than 0, the watering process is controlled by both "volume" and "duration."
* The watering stops when either of these conditions is met.
* (Note: G1S does not support "watering by volume". D1, which includes two integrated flow meters,
* supports "watering by volume")
*/
@SerializedName("volume")
@Expose
public int volume = DEFAULT_INT;
/**
* This defines the watering duration in seconds. (Please note: for G1 & G2 models which support "watering
* by minute" only, the duration value here needs to be an integral multiple of 60 seconds. For the G1S & G2S
* and future models, the duration value can be any integer between 3 and 86399.)
*/
@SerializedName("duration")
@Expose
public int duration = DEFAULT_INT;
@Override
public Collection<ValidationError> getValidationErrors() {
final Collection<ValidationError> errors = new ArrayList<>(0);
if (duration < 3 || duration > 86399) {
errors.add(new ValidationError("duration", "not in range 3 -> 86340", USER));
}
if (volume < 1) {
errors.add(new ValidationError("volume", "is less than 1", USER));
}
if (timestamp.length() != 14) {
errors.add(new ValidationError("timestamp", "is not 14 characters long", BUG));
}
try {
LocalDate.parse(timestamp.substring(0, 8), DATE_FORMATTER);
LocalTime.parse(timestamp.substring(9), DATE_FORMATTER);
} catch (DateTimeParseException e) {
errors.add(new ValidationError("timestamp", "is invalid", BUG));
}
return errors;
}
}
}

View File

@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import static org.openhab.binding.linktap.protocol.frames.ValidationError.Cause.USER;
import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link DismissAlertReq} defines the request to start watering immediately.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class StartWateringReq extends DeviceCmdReq {
public StartWateringReq() {
}
public StartWateringReq(final int durationSecs, final int volume) {
this.command = CMD_IMMEDIATE_WATER_START;
this.duration = durationSecs;
this.volume = volume;
}
/**
* Defines the time duration in seconds to water for.
* Minimum value 3
* Maximum value 86340
*/
@SerializedName("duration")
@Expose
public int duration = DEFAULT_INT;
/**
* Defines the volume of water to use
* Units may be L or gal, depending on the units the device
* is operating in.
*/
@SerializedName("volume")
@Expose
public int volume = DEFAULT_INT;
public Collection<ValidationError> getValidationErrors() {
final Collection<ValidationError> errors = super.getValidationErrors();
if (duration < 3 || duration > 86340) {
errors.add(new ValidationError("duration", "not in range 3 -> 86340", USER));
}
return errors;
}
}

View File

@ -0,0 +1,254 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import java.util.ArrayList;
import java.util.Collection;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link TLGatewayFrame} defines the common framing data, for requests and responses
* from a Gateway device.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class TLGatewayFrame implements IPayloadValidator {
public static final int DEFAULT_INT = -1;
public static final String EMPTY_STRING = "";
public static final String[] EMPTY_STRING_ARRAY = new String[0];
protected static final Pattern DEVICE_ID_PATTERN = Pattern.compile("[a-zA-Z0-9]{0,16}");
protected static final Pattern SUB_DEVICE_ID_PATTERN = Pattern.compile("[a-zA-Z0-9]{0,16}_[0-9]");
public TLGatewayFrame() {
}
public TLGatewayFrame(final int command) {
this.command = command;
}
public @Nullable Class<TLGatewayFrame> getResponseFrame() {
return TLGatewayFrame.class;
}
/**
* Defines the CMD identifier, that defines the payload type.
* Possible values in constants CMD_*
*/
@SerializedName("cmd")
@Expose
public int command = DEFAULT_INT;
/**
* Defines the gateway identifier.
* Limited to the first 16 digits and letters of the Gateway ID
*/
@SerializedName("gw_id")
@Expose
public String gatewayId = EMPTY_STRING;
public Collection<ValidationError> getValidationErrors() {
final ArrayList<ValidationError> errors = new ArrayList<>(0);
if (command < CMD_HANDSHAKE || command > CMD_PAUSE_WATER_PLAN) {
errors.add(new ValidationError("cmd", "not in range " + CMD_HANDSHAKE + " -> " + CMD_PAUSE_WATER_PLAN));
}
if (!DEVICE_ID_PATTERN.matcher(gatewayId).matches()) {
errors.add(new ValidationError("gw_id", "not in range " + CMD_HANDSHAKE + " -> " + CMD_PAUSE_WATER_PLAN));
}
return errors;
}
// COMMAND Values
/**
* Command - 0. Handshake Message
*
* @direction GW->Broker->App
* @description Handshake message. It is the first message after the Gateway connects to the MQTT Broker.
* </p>
* 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 timers 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 timers 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 Gateways 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;
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link TimeDataResp} defines a response that defines the time, date and weekday
* from the gateway.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class TimeDataResp extends HandshakeResp {
public TimeDataResp() {
}
}

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ValidationError} represents a data payload validation error.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class ValidationError {
String variable = "";
String error = "";
Cause cause = Cause.BUG;
public ValidationError(String var, String err) {
this.variable = var;
this.error = err;
}
public ValidationError(String var, String err, Cause cause) {
this.variable = var;
this.error = err;
this.cause = cause;
}
public static enum Cause {
BUG,
USER
}
public Cause getCause() {
return this.cause;
}
public String toString() {
return this.variable + " " + this.cause;
}
}

View File

@ -0,0 +1,279 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link DeviceCmdReq} is a payload representing the current status of the
* water timer.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class WaterMeterStatus extends GatewayDeviceResponse {
public WaterMeterStatus() {
}
@Override
public ResultStatus getRes() {
if (super.getRes() == ResultStatus.INVALID) {
return ResultStatus.RET_SUCCESS;
}
return super.getRes();
}
public static class DeviceStatusClassTypeAdapter implements JsonDeserializer<List<DeviceStatus>> {
public @Nullable List<DeviceStatus> deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext ctx) {
List<DeviceStatus> 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<DeviceStatus> deviceStatuses = new ArrayList<DeviceStatus>();
public static class DeviceStatus implements IPayloadValidator {
/**
* Defines the targetted device ID
*/
@SerializedName("dev_id")
@Expose
public String deviceId = EMPTY_STRING;
/**
* Defines the currently active plan (Operating Mode)
*/
@SerializedName("plan_mode")
@Expose
public @Nullable Integer planMode;
/**
* Defines the serial number of the currently active plan
*/
@SerializedName("plan_sn")
@Expose
public int planSerialNo = DEFAULT_INT;
/**
* Defines if the water timer is connected to the Gateway
*/
@SerializedName("is_rf_linked")
@Expose
public @Nullable Boolean isRfLinked;
/**
* Defines whether the flow meter is plugin
*/
@SerializedName("is_flm_plugin")
@Expose
public @Nullable Boolean isFlmPlugin;
/**
* Water timer fall alert status
*/
@SerializedName("is_fall")
@Expose
public @Nullable Boolean isFall;
/**
* Valve shut-down failure alert status
*/
@SerializedName("is_broken")
@Expose
public @Nullable Boolean isBroken;
/**
* Water cut-off alert status
*/
@SerializedName("is_cutoff")
@Expose
public @Nullable Boolean isCutoff;
/**
* Unusually high flow alert status
*/
@SerializedName("is_leak")
@Expose
public @Nullable Boolean isLeak;
/**
* Unusually low flow alert status
*/
@SerializedName("is_clog")
@Expose
public @Nullable Boolean isClog;
/**
* Water timer signal reception level
*/
@SerializedName("signal")
@Expose
public @Nullable Integer signal;
/**
* Water timer battery level
*/
@SerializedName("battery")
@Expose
public @Nullable Integer battery;
/**
* Defines the lock in operation
*/
@SerializedName("child_lock")
@Expose
public @Nullable Integer childLock;
/**
* Is manual watering currently on
*/
@SerializedName("is_manual_mode")
@Expose
public @Nullable Boolean isManualMode;
/**
* Is watering currently on
*/
@SerializedName("is_watering")
@Expose
public @Nullable Boolean isWatering = false;
/**
* When the ECO mode is enabled, the watering duration is divided into multiple "on-off-on-off" segments.
* If is_final is true,it means current watering belongs to the last segment. If both is_watering and is_final
* are false,it means that the watering is currently suspended (i.e. in midst of the segments), and there are
* subsequent watering seqments to be executed.
*/
@SerializedName("is_final")
@Expose
public @Nullable Boolean isFinal;
/**
* The duration of the current watering cycle in seconds
*/
@SerializedName("total_duration")
@Expose
public @Nullable Integer totalDuration;
/**
* The remaining duration of the current watering cycle in seconds
*/
@SerializedName("remain_duration")
@Expose
public @Nullable Integer remainDuration;
/**
* The failsafe duration of the current watering cycle in seconds
*/
@SerializedName("failsafe_duration")
@Expose
public @Nullable Integer failsafeDuration;
/**
* The current water flow rate (LPN or GPM)
*/
@SerializedName("speed")
@Expose
public @Nullable Double speed = 0.0d;
/**
* The accumulated volume of the current watering cycle (Litre or Gallon)
*/
@SerializedName("volume")
@Expose
public @Nullable Double volume = 0.0d;
/**
* The volume limit of the current watering cycle (Litre or Gallon)
*/
@SerializedName("volume_limit")
@Expose
public @Nullable Double volumeLimit = 0.0d;
public Collection<ValidationError> getValidationErrors() {
final Collection<ValidationError> errors = new ArrayList<>(0);
final Integer planModeRaw = planMode;
if (planModeRaw == null || planModeRaw < 1 || planModeRaw > OP_MODE_DESC.length) {
errors.add(new ValidationError("planMode", "is not in range 1 -> " + OP_MODE_DESC.length));
}
final Integer signalRaw = signal;
if (signalRaw == null || signalRaw < 0 || signalRaw > 100) {
errors.add(new ValidationError("signal", "is not in range 0 -> 100"));
}
final Integer batteryRaw = battery;
if (batteryRaw == null || batteryRaw < 0 || batteryRaw > 100) {
errors.add(new ValidationError("battery", "is not in range 0 -> 100"));
}
if (planSerialNo == DEFAULT_INT) {
errors.add(new ValidationError("plan_sn", "is invalid"));
}
final Integer childLockRaw = childLock;
if (childLockRaw == null || childLockRaw < LockReq.LOCK_UNLOCKED || childLockRaw > LockReq.LOCK_FULL) {
errors.add(new ValidationError("child_lock",
"is not in range " + LockReq.LOCK_UNLOCKED + " -> " + LockReq.LOCK_FULL));
}
if (!DEVICE_ID_PATTERN.matcher(deviceId).matches()) {
errors.add(new ValidationError("dev_id", "is not in the expected format"));
}
return errors;
}
}
public Collection<ValidationError> getValidationErrors() {
return EMPTY_COLLECTION;
}
public static final int OP_MODE_INSTANT = 1;
public static final int OP_MODE_CALENDAR = 2;
public static final int OP_MODE_WEEK_TIMER = 3;
public static final int OP_MODE_ODD_EVEN = 4;
public static final int OP_MODE_INTERVAL = 5;
public static final int OP_MODE_MONTH = 6;
public static final String[] OP_MODE_DESC = new String[] { "Instant Mode", "Calendar Mode", "7 Day Mode",
"Odd-Even Mode", "Interval Mode", "Month Mode" };
}

View File

@ -0,0 +1,67 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import static org.openhab.binding.linktap.protocol.frames.ValidationError.Cause.BUG;
import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link WateringSkippedNotification} defines the request to dismiss alerts from a given device.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class WateringSkippedNotification extends DeviceCmdReq {
public WateringSkippedNotification() {
}
/**
* Defines the past rainfall [0] and future rainfull [1] measurements in mm
*/
@SerializedName("rain")
@Expose
public double[] rainfallData = new double[] { 0.0, 0.0 };
public void setPastRainfall(final double pastRainMM) {
rainfallData[0] = pastRainMM;
}
public double getPastRainfall() {
return rainfallData[0];
}
public void setFutureRainfall(final double futureRainMM) {
rainfallData[1] = futureRainMM;
}
public double getFutureRainfall() {
return rainfallData[1];
}
public Collection<ValidationError> getValidationErrors() {
final Collection<ValidationError> errors = super.getValidationErrors();
if (rainfallData.length != 2) {
errors.add(new ValidationError("rain", "invalid number of entries", BUG));
}
return errors;
}
}

View File

@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.frames;
import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* The {@link WirelessTestResp} defines the wireless test result data response
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class WirelessTestResp extends EndpointDeviceResponse {
public WirelessTestResp() {
}
/**
* Defines true if the last packet has been transmitted and
* therefore the test is complete
*/
@SerializedName("final")
@Expose
public boolean testComplete = false;
/**
* Defines how many pings have been sent to the endpoint device
*/
@SerializedName("ping")
@Expose
public int pingCount = DEFAULT_INT;
/**
* Defines how many pongs have been received from the endpoint device
*/
@SerializedName("pong")
@Expose
public int pongCount = DEFAULT_INT;
public Collection<ValidationError> getValidationErrors() {
final Collection<ValidationError> errors = super.getValidationErrors();
if (pingCount == DEFAULT_INT) {
errors.add(new ValidationError("ping", "count is missing"));
}
if (pongCount == DEFAULT_INT) {
errors.add(new ValidationError("pong", "count is missing"));
}
if (!DEVICE_ID_PATTERN.matcher(deviceId).matches()) {
errors.add(new ValidationError("dev_id", "is not in the expected format"));
}
return errors;
}
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.http;
import java.io.Serial;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.linktap.protocol.frames.GatewayDeviceResponse;
/**
* The {@link CommandNotSupportedException} should be thrown when the endpoint being communicated with
* does not appear to be a Tap Link Gateway device.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class CommandNotSupportedException extends I18Exception {
@Serial
private static final long serialVersionUID = -7784829325604153947L;
public CommandNotSupportedException() {
super();
}
public CommandNotSupportedException(final String message) {
super(message);
}
public CommandNotSupportedException(final Throwable cause) {
super(cause);
}
public CommandNotSupportedException(final String message, final Throwable cause) {
super(message, cause);
}
public CommandNotSupportedException(final GatewayDeviceResponse.ResultStatus rs) {
super(rs.getDesc());
this.i18Key = rs.getI18Key();
}
public String getI18Key() {
return getI18Key("exception-cmd-not-supported-exception");
}
}

View File

@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.http;
import java.io.Serial;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.linktap.protocol.frames.GatewayDeviceResponse;
/**
* The {@link DeviceIdException} should be thrown when the endpoint being communicated with
* does not appear to be a Tap Link Gateway device.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class DeviceIdException extends I18Exception {
@Serial
private static final long serialVersionUID = -7786449325604153947L;
// case RET_DEVICE_ID_ERROR:
// case RET_DEVICE_NOT_FOUND:
public DeviceIdException() {
super();
}
public DeviceIdException(final String message) {
super(message);
}
public DeviceIdException(final Throwable cause) {
super(cause);
}
public DeviceIdException(final String message, final Throwable cause) {
super(message, cause);
}
public DeviceIdException(final GatewayDeviceResponse.ResultStatus rs) {
super(rs.getDesc());
this.i18Key = rs.getI18Key();
}
public String getI18Key() {
return getI18Key("exception.device-id-exception");
}
}

View File

@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.http;
import java.io.Serial;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.linktap.protocol.frames.GatewayDeviceResponse;
/**
* The {@link GatewayIdException} should be thrown when the endpoint being communicated with
* does not appear to be a Tap Link Gateway device.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class GatewayIdException extends I18Exception {
@Serial
private static final long serialVersionUID = -7786449325604153947L;
// case RET_DEVICE_ID_ERROR:
// case RET_DEVICE_NOT_FOUND:
public GatewayIdException() {
super();
}
public GatewayIdException(final String message) {
super(message);
}
public GatewayIdException(final Throwable cause) {
super(cause);
}
public GatewayIdException(final String message, final Throwable cause) {
super(message, cause);
}
public GatewayIdException(final GatewayDeviceResponse.ResultStatus rs) {
super(rs.getDesc());
this.i18Key = rs.getI18Key();
}
public String getI18Key() {
return getI18Key("exception.gw-id-exception");
}
}

View File

@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.http;
import java.io.Serial;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link I18Exception} is a abstract class for exceptions that support
* i18key functionality.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public abstract class I18Exception extends Exception {
@Serial
private static final long serialVersionUID = -7784829349743963947L;
protected String i18Key = "";
public I18Exception() {
super();
}
public I18Exception(final String message) {
super(message);
}
public I18Exception(final Throwable cause) {
super(cause);
}
public I18Exception(final String message, final Throwable cause) {
super(message, cause);
}
public abstract String getI18Key();
public String getI18Key(final String defaultI18) {
if (!i18Key.isBlank()) {
return i18Key;
}
return Objects.requireNonNullElse(getMessage(), defaultI18);
}
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.http;
import java.io.Serial;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.linktap.protocol.frames.GatewayDeviceResponse;
/**
* The {@link InvalidParameterException} should be thrown when the endpoint being communicated with
* does not appear to be a Tap Link Gateway device.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class InvalidParameterException extends I18Exception {
@Serial
private static final long serialVersionUID = -7784829499604153947L;
public InvalidParameterException() {
super();
}
public InvalidParameterException(final String message) {
super(message);
}
public InvalidParameterException(final Throwable cause) {
super(cause);
}
public InvalidParameterException(final String message, final Throwable cause) {
super(message, cause);
}
public InvalidParameterException(final GatewayDeviceResponse.ResultStatus rs) {
super(rs.getDesc());
this.i18Key = rs.getI18Key();
}
public String getI18Key() {
return getI18Key("exception.invalid-parameter-exception");
}
}

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.http;
import java.io.Serial;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link LinkTapException} is a class for general exceptions that support
* i18key functionality.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class LinkTapException extends I18Exception {
@Serial
private static final long serialVersionUID = -7739358310944502365L;
protected String i18Key = "";
public LinkTapException() {
super();
}
public LinkTapException(final String message) {
super(message);
}
public LinkTapException(final Throwable cause) {
super(cause);
}
public LinkTapException(final String message, final Throwable cause) {
super(message, cause);
}
@Override
public String getI18Key() {
return getI18Key("exception.unexpected-exception");
}
}

View File

@ -0,0 +1,107 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.http;
import java.io.Serial;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link NotTapLinkGatewayException} should be thrown when the endpoint being communicated with
* does not appear to be a Tap Link Gateway device.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class NotTapLinkGatewayException extends I18Exception {
@Serial
private static final long serialVersionUID = -7786449325604153487L;
public NotTapLinkGatewayException() {
super();
}
public NotTapLinkGatewayException(final String message) {
super(message);
}
public NotTapLinkGatewayException(final Throwable cause) {
super(cause);
}
public NotTapLinkGatewayException(final String message, final Throwable cause) {
super(message, cause);
}
public NotTapLinkGatewayException(final String message, final String i18key) {
super(message);
this.i18Key = i18key;
}
public NotTapLinkGatewayException(final NotTapLinkGatewapExecptionDefinitions definition) {
this(definition.description, definition.i18Key);
}
public enum NotTapLinkGatewapExecptionDefinitions {
/**
* HEADERS_MISSING
*/
HEADERS_MISSING("Missing header markers", "exception.not-gw.missing-headers"),
/**
* MISSING_API_TITLE
*/
MISSING_API_TITLE("Not a LinkTap API response", "exception.not-gw.missing-api-title"),
/**
* MISSING_SERVER_TITLE
*/
MISSING_SERVER_TITLE("Not a LinkTap response", "exception.not-gw.missing-server-title"),
/**
* UNEXPECTED_STATUS_CODE
*/
UNEXPECTED_STATUS_CODE("Unexpected status code response", "exception.not-gw.unexpected-status-code"),
/**
* UNEXPECTED_HTTPS
*/
UNEXPECTED_HTTPS("Unexpected protocol", "exception.not-gw.unexpected-protocol");
private final String description;
private final String i18Key;
private NotTapLinkGatewapExecptionDefinitions(final String description, final String i18key) {
this.description = description;
this.i18Key = i18key;
}
public String getI18Key() {
return i18Key;
}
public String getDesc() {
return description;
}
@Override
public String toString() {
return description;
}
}
public String getI18Key() {
return getI18Key("exception.not-tap-link-gw");
}
}

View File

@ -0,0 +1,93 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.http;
import java.io.Serial;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link TransientCommunicationIssueException} should be thrown when the endpoint being communicated with
* does not appear to be a Tap Link Gateway device.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class TransientCommunicationIssueException extends I18Exception {
@Serial
private static final long serialVersionUID = -7786449325604143287L;
public TransientCommunicationIssueException() {
super();
}
public TransientCommunicationIssueException(final String message, final String i18key) {
super(message);
this.i18Key = i18key;
}
public TransientCommunicationIssueException(final TransientExecptionDefinitions definition) {
this(definition.description, definition.i18Key);
}
public TransientCommunicationIssueException(final Throwable cause) {
super(cause);
}
public TransientCommunicationIssueException(final String message, final Throwable cause) {
super(message, cause);
}
public enum TransientExecptionDefinitions {
/**
* HOST_UNREACHABLE
*/
HOST_UNREACHABLE("Could not connect", "exception.could-not-connect"),
/**
* HOST_NOT_RESOLVED
*/
HOST_NOT_RESOLVED("Could not resolve IP address", "exception.could-not-resolve"),
/**
* COMMUNICATIONS_LOST
*/
COMMUNICATIONS_LOST("Communications Lost", "exception.communications-lost");
private final String description;
private final String i18Key;
private TransientExecptionDefinitions(final String description, final String i18key) {
this.description = description;
this.i18Key = i18key;
}
public String getI18Key() {
return i18Key;
}
public String getDesc() {
return description;
}
@Override
public String toString() {
return description;
}
}
public String getI18Key() {
return getI18Key("exception.gw-id-exception");
}
}

View File

@ -0,0 +1,572 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.http;
import static org.openhab.binding.linktap.internal.LinkTapBindingConstants.*;
import static org.openhab.binding.linktap.protocol.http.NotTapLinkGatewayException.NotTapLinkGatewapExecptionDefinitions.*;
import static org.openhab.binding.linktap.protocol.http.TransientCommunicationIssueException.TransientExecptionDefinitions.*;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.net.ssl.SSLHandshakeException;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.MediaType;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.FormContentProvider;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.openhab.binding.linktap.internal.Firmware;
import org.openhab.binding.linktap.internal.Utils;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.osgi.framework.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link WebServerApi} defines interactions with the web server interface.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public final class WebServerApi {
public static final String URI_SCHEME = "http";
public static final String URI_HOST_PREFIX = URI_SCHEME + "://";
/**
* Headers
*/
public static final String HEADER_SERVER = "Server";
public static final String HEADER_GW_SERVER_NAME = "LinkTap Gateway";
/**
* HTML title field mappings to use cases
*/
private static final String TITLE_API_RESPONSE = "api";
private static final String TITLE_API_CONFIG_PAGE = "LinkTap Gateway";
private static final String TITLE_API_LOGIN_PAGE = "LinkTap Gateway Login";
/**
* Field names for form submission API's
*/
private static final String FIELD_ADMIN_USER = "admin";
private static final String FIELD_ADMIN_USER_PWD = "adminpwd";
private static final int REQ_TIMEOUT_SECONDS = 3;
private static final WebServerApi INSTANCE = new WebServerApi();
private static final String REQ_HDR_APPLICATION_JSON = new MediaType("application", "json", "UTF-8").toString();
private final Logger logger = LoggerFactory.getLogger(WebServerApi.class);
private @NonNullByDefault({}) HttpClient httpClient;
private @Nullable TranslationProvider translationProvider;
private @Nullable LocaleProvider localeProvider;
private @Nullable Bundle bundle;
private WebServerApi() {
}
public static WebServerApi getInstance() {
return INSTANCE;
}
public void setTranslationProviderInfo(TranslationProvider translationProvider, LocaleProvider localeProvider,
Bundle bundle) {
this.bundle = bundle;
this.localeProvider = localeProvider;
this.translationProvider = translationProvider;
}
public String getLocalizedText(String key, @Nullable Object @Nullable... arguments) {
TranslationProvider translationProv = translationProvider;
LocaleProvider localeProv = localeProvider;
if (translationProv == null || localeProv == null) {
return key;
}
String result = translationProv.getText(bundle, key, key, localeProv.getLocale(), arguments);
return Objects.nonNull(result) ? result : key;
}
/**
* Sets the httpClient object to be used for API calls to LinkTap.
*
* @param httpClient the client to be used.
*/
public void setHttpClient(@Nullable HttpClient httpClient) {
if (httpClient != null) {
this.httpClient = httpClient;
}
}
public Map<String, String> getBridgeProperities(final String hostname)
throws LinkTapException, NotTapLinkGatewayException, TransientCommunicationIssueException {
try {
final Request request = httpClient.newRequest(URI_HOST_PREFIX + hostname).method(HttpMethod.GET);
final ContentResponse cr = request.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
if (HttpURLConnection.HTTP_OK != cr.getStatus()) {
throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
}
validateHeaders(cr.getHeaders());
final String responseData = cr.getContentAsString();
final Document doc = Jsoup.parse(responseData);
switch (doc.title()) {
case TITLE_API_CONFIG_PAGE:
break;
case TITLE_API_LOGIN_PAGE:
return Map.of();
default:
throw new NotTapLinkGatewayException(MISSING_SERVER_TITLE);
}
final Map<String, String> deviceProps = getMetadataProperties(doc);
Firmware firmware = new Firmware(deviceProps.get(BRIDGE_PROP_GW_VER));
if (firmware.supportsMDNS()) {
getMdnsEnableArgs(doc);
} else {
logger.debug("Firmware revision does not include mDNS support");
}
return deviceProps;
} catch (InterruptedException e) {
return Map.of();
} catch (TimeoutException e) {
throw new TransientCommunicationIssueException(COMMUNICATIONS_LOST);
} catch (ExecutionException e) {
final Throwable t = e.getCause();
if (t instanceof UnknownHostException || t instanceof SocketTimeoutException) {
throw new TransientCommunicationIssueException(HOST_UNREACHABLE);
} else if (t instanceof SSLHandshakeException) {
throw new NotTapLinkGatewayException(UNEXPECTED_HTTPS);
} else {
logger.warn("{}", getLocalizedText("ExecutionException -> {}", Utils.getMessage(e)));
}
throw new LinkTapException(getLocalizedText("exception.unexpected-failure", Utils.getMessage(e)));
}
}
/**
* Extract the common properties for all devices, from the given meta-data of a device.
*
* @param doc the html document returns from the potential Gateway device
* @return Map of common props
*/
private Map<String, String> getMetadataProperties(final Document doc) {
final Map<String, String> newProps = new HashMap<>(7);
/*
* Extract elements based on td location using the text markers
*/
String firmwareVer = "?";
String hwModel = "?";
String id = "?";
String macAddr = "?";
final org.jsoup.select.Elements tdEntries = doc.getElementsByTag("td");
for (int i = 0; i < tdEntries.size(); ++i) {
if (tdEntries.get(i).hasText()) {
switch (tdEntries.get(i).text()) {
case "Firmware version":
firmwareVer = tdEntries.get(i + 1).text();
i++;
break;
case "Model":
hwModel = tdEntries.get(i + 1).text();
i++;
break;
case "ID":
id = tdEntries.get(i + 1).text();
i++;
break;
case "MAC address":
macAddr = tdEntries.get(i + 1).text();
i++;
break;
}
}
}
newProps.put(BRIDGE_PROP_GW_ID, id.split("[-]")[0]);
newProps.put(BRIDGE_PROP_GW_VER, firmwareVer.split("[_]")[0]);
newProps.put(BRIDGE_PROP_MAC_ADDR, macAddr);
newProps.put(BRIDGE_PROP_HW_MODEL, hwModel);
/*
* Extract elements based on name markers and attributes
*/
final boolean httpApiEnabled = doc.getElementsByAttributeValue("name", "htapi").hasAttr("checked");
final String httpApiEndpoint = doc.getElementsByAttributeValue("name", "URL").attr("value");
newProps.put(BRIDGE_PROP_HTTP_API_ENABLED, String.valueOf(httpApiEnabled));
newProps.put(BRIDGE_PROP_HTTP_API_EP, httpApiEndpoint);
Optional<Element> 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<Element> getSection(final Document doc, final String title) {
final Elements thead = doc.getElementsByTag("thead");
Optional<Element> 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<Element> miscSection = getSection(doc, "Misc settings");
StringBuilder sb = new StringBuilder();
if (!miscSection.isPresent()) {
return sb.toString();
}
final Elements inputs = miscSection.get().getElementsByTag("input");
for (int i = 0; i < inputs.size(); ++i) {
final String val = getUriInputArg(inputs.get(i));
if (!val.isEmpty() && !sb.isEmpty()) {
sb.append("&");
}
sb.append(val);
}
// Change the mdns flag to true
{
final int mdnsIdx = sb.indexOf("mdns=0");
if (mdnsIdx != -1) {
sb.replace(mdnsIdx, mdnsIdx + 6, "mdns=1");
return sb.toString();
}
}
return "";
}
public String getLocalHttpApiArgs(final Document doc, final Optional<String> targetServerOpt,
final Optional<Boolean> wrapHtmlDisable) {
final Optional<Element> localHttpApiSection = getSection(doc, "Local HTTP API settings");
StringBuilder sb = new StringBuilder();
if (!localHttpApiSection.isPresent()) {
return sb.toString();
}
final Elements inputs = localHttpApiSection.get().getElementsByTag("input");
for (int i = 0; i < inputs.size(); ++i) {
final String val = getUriInputArg(inputs.get(i));
if (!val.isEmpty() && !sb.isEmpty()) {
sb.append("&");
}
sb.append(val);
}
boolean updatedUri = false;
int enableApiIdx = -1;
if (targetServerOpt.isPresent()) {
String targetServer = targetServerOpt.get();
// Change the enable Local HTTP API flag to true
enableApiIdx = sb.indexOf("htapi=0");
if (enableApiIdx != -1) {
sb.replace(enableApiIdx, enableApiIdx + 7, "htapi=1");
}
final int urlApiMarker = sb.indexOf("URL=");
if (urlApiMarker != -1) {
final int nextArg = sb.indexOf("&", urlApiMarker);
String urlArg = (nextArg == -1) ? sb.substring(urlApiMarker + 4)
: sb.substring(urlApiMarker + 4, nextArg);
logger.trace("Found existing HTTP URL Server : {}", urlArg);
if (!urlArg.equals(targetServer)) {
updatedUri = true;
sb.replace(urlApiMarker, urlApiMarker + urlArg.length() + 4,
"URL=" + URLEncoder.encode(targetServer, StandardCharsets.UTF_8));
}
}
}
int wgrhIdx = -1;
if (wrapHtmlDisable.isPresent() && wrapHtmlDisable.get()) {
// Change the wgrhIdx flag to true
{
wgrhIdx = sb.indexOf("wgrh=1");
if (wgrhIdx != -1) {
sb.replace(wgrhIdx, wgrhIdx + 6, "wgrh=0");
return sb.toString();
}
}
}
if (wgrhIdx != -1 || enableApiIdx != -1 || updatedUri) {
return sb.toString();
}
return "";
}
public boolean configureBridge(final @Nullable String hostname, final Optional<Boolean> mdnsEnable,
final Optional<Boolean> nonHtmlEnable, final Optional<String> localServer)
throws InterruptedException, NotTapLinkGatewayException, TransientCommunicationIssueException {
try {
if (hostname == null) {
throw new TransientCommunicationIssueException(HOST_NOT_RESOLVED);
}
final String targetHost = URI_HOST_PREFIX + hostname;
final Request request = httpClient.newRequest(targetHost).method(HttpMethod.GET);
final ContentResponse cr = request.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
if (HttpURLConnection.HTTP_OK != cr.getStatus()) {
throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
}
validateHeaders(cr.getHeaders());
final String responseData = cr.getContentAsString();
final Document doc = Jsoup.parse(responseData);
switch (doc.title()) {
case TITLE_API_CONFIG_PAGE:
break;
case TITLE_API_LOGIN_PAGE:
return false;
default:
throw new NotTapLinkGatewayException(MISSING_SERVER_TITLE);
}
// Send the GET request to configure mdns if it's not enabled
boolean rebootReq = false;
if (mdnsEnable.isPresent() && mdnsEnable.get()) {
logger.trace("Enabling mdns server on gateway");
String mdnsEnableReqStr = getMdnsEnableArgs(doc);
if (!mdnsEnableReqStr.isEmpty()) {
logger.debug("Updating mdns server settings on gateway");
final Request mdnsRequest = httpClient
.newRequest(targetHost + "/index.shtml?flag=4&" + mdnsEnableReqStr).method(HttpMethod.GET);
final ContentResponse mdnsCr = mdnsRequest.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
if (HttpURLConnection.HTTP_OK != mdnsCr.getStatus()) {
throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
}
rebootReq = true;
}
}
if (localServer.isPresent() && !localServer.get().isBlank()
|| nonHtmlEnable.isPresent() && nonHtmlEnable.get()) {
if (localServer.isPresent() && !localServer.get().isBlank()) {
logger.trace("Setting Local HTTP Api on gateway");
}
if (nonHtmlEnable.isPresent() && nonHtmlEnable.get()) {
logger.trace("Enabling efficient non HTML communications on gateway");
}
String localHttpApiReqStr = this.getLocalHttpApiArgs(doc, localServer, nonHtmlEnable);
if (!localHttpApiReqStr.isEmpty()) {
logger.debug("Updating Local HTTP API server settings on gateway");
final Request lhttpApiRequest = httpClient
.newRequest(targetHost + "/index.shtml?flag=5&" + localHttpApiReqStr)
.method(HttpMethod.GET);
final ContentResponse mdnsCr = lhttpApiRequest.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.send();
if (HttpURLConnection.HTTP_OK != mdnsCr.getStatus()) {
throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
}
rebootReq = true;
}
}
if (rebootReq) {
logger.debug("Rebooting gateway to apply new settings");
final Request restartReq = httpClient.newRequest(targetHost + "/index.shtml?flag=0")
.method(HttpMethod.GET);
final ContentResponse mdnsCr = restartReq.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
if (HttpURLConnection.HTTP_OK != mdnsCr.getStatus()) {
throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
}
}
return rebootReq;
} catch (TimeoutException e) {
throw new TransientCommunicationIssueException(HOST_UNREACHABLE);
} catch (ExecutionException e) {
final Throwable t = e.getCause();
if (t instanceof UnknownHostException) {
throw new TransientCommunicationIssueException(HOST_NOT_RESOLVED);
} else if (t instanceof SocketTimeoutException) {
throw new TransientCommunicationIssueException(HOST_UNREACHABLE);
} else if (t instanceof SSLHandshakeException) {
throw new NotTapLinkGatewayException(UNEXPECTED_HTTPS);
} else {
logger.warn("{}", getLocalizedText("ExecutionException -> {}", Utils.getMessage(e)));
}
throw new NotTapLinkGatewayException(getLocalizedText("exception.unexpected-failure", Utils.getMessage(t)));
}
}
public boolean unlockWebInterface(final String hostname, final String username, final String password)
throws LinkTapException, NotTapLinkGatewayException, TransientCommunicationIssueException {
try {
org.eclipse.jetty.util.Fields fields = new org.eclipse.jetty.util.Fields();
fields.put(FIELD_ADMIN_USER, username);
fields.put(FIELD_ADMIN_USER_PWD, password);
final Request request = httpClient.newRequest(URI_HOST_PREFIX + hostname + "/login.shtml")
.method(HttpMethod.POST).content(new FormContentProvider(fields));
final ContentResponse cr = request.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
if (HttpURLConnection.HTTP_OK != cr.getStatus()) {
throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
}
validateHeaders(cr.getHeaders());
return !getBridgeProperities(hostname).isEmpty();
} catch (InterruptedException e) {
return false;
} catch (TimeoutException e) {
throw new TransientCommunicationIssueException(HOST_UNREACHABLE);
} catch (ExecutionException e) {
final Throwable t = e.getCause();
if (t instanceof UnknownHostException) {
throw new TransientCommunicationIssueException(HOST_NOT_RESOLVED);
} else if (t instanceof SocketTimeoutException) {
throw new TransientCommunicationIssueException(HOST_UNREACHABLE);
} else if (t instanceof SSLHandshakeException) {
throw new NotTapLinkGatewayException(UNEXPECTED_HTTPS);
} else {
logger.warn("{}", getLocalizedText("ExecutionException -> {}", Utils.getMessage(e)));
}
throw new NotTapLinkGatewayException(getLocalizedText("exception.unexpected-failure", Utils.getMessage(e)));
}
}
/**
* Returns whether a response from the HTTP endpoint reached, appears to have the correct
* header markers for a Link Tap Gateway device.
*
* @param headers the http headers from the response to be checked
* @throws NotTapLinkGatewayException if the response does not appear to be from a Link Tap Gateway
*/
private void validateHeaders(final HttpFields headers) throws NotTapLinkGatewayException {
if (!headers.contains(HEADER_SERVER, HEADER_GW_SERVER_NAME)) {
throw new NotTapLinkGatewayException(HEADERS_MISSING);
}
}
public String sendRequest(final String hostname, final String requestBody)
throws NotTapLinkGatewayException, TransientCommunicationIssueException {
try {
final InetAddress address = InetAddress.getByName(hostname);
logger.trace("API Endpoint: {}", URI_HOST_PREFIX + address.getHostAddress() + "/api.shtml");
final Request request = httpClient.POST(URI_HOST_PREFIX + address.getHostAddress() + "/api.shtml");
request.content(new StringContentProvider(requestBody), REQ_HDR_APPLICATION_JSON);
final ContentResponse cr = request.timeout(REQ_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
if (HttpURLConnection.HTTP_OK != cr.getStatus()) {
throw new NotTapLinkGatewayException(UNEXPECTED_STATUS_CODE);
}
final HttpFields headers = cr.getHeaders();
validateHeaders(headers);
String responseData = cr.getContentAsString();
final String contentType = headers.get(HttpHeader.CONTENT_TYPE);
// If content type is test/html its wrapped in HTML (Old standard)
// If content type is application/json it's a raw compact response (More efficient new standard)
switch (contentType) {
case "text/html":
final Document doc = Jsoup.parse(responseData);
final String docTitle = doc.title();
if (!docTitle.equals(TITLE_API_RESPONSE)) {
throw new NotTapLinkGatewayException(MISSING_API_TITLE);
}
responseData = doc.body().text();
break;
case "application/json":
// Do nothing - the raw content is the response
break;
default:
responseData = "";
}
return responseData;
} catch (InterruptedException e) {
return "";
} catch (TimeoutException e) {
throw new TransientCommunicationIssueException(HOST_UNREACHABLE);
} catch (UnknownHostException e) {
throw new TransientCommunicationIssueException(HOST_NOT_RESOLVED);
} catch (ExecutionException e) {
final Throwable t = e.getCause();
if (t instanceof UnknownHostException) {
throw new TransientCommunicationIssueException(HOST_NOT_RESOLVED);
} else if (t instanceof SSLHandshakeException) {
throw new NotTapLinkGatewayException(UNEXPECTED_HTTPS);
} else {
throw new TransientCommunicationIssueException(HOST_UNREACHABLE);
}
}
}
}

View File

@ -0,0 +1,163 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.servers;
import static org.openhab.binding.linktap.protocol.frames.TLGatewayFrame.EMPTY_STRING;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.MediaType;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.linktap.internal.LinkTapBindingConstants;
import org.openhab.binding.linktap.internal.TransactionProcessor;
import org.openhab.binding.linktap.internal.Utils;
import org.openhab.binding.linktap.protocol.frames.TLGatewayFrame;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.osgi.framework.Bundle;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link BindingServlet} defines the request to enable or disable alerts from a given device.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public class BindingServlet extends HttpServlet {
public static final BindingServlet INSTANCE = new BindingServlet();
public static final String SERVLET_URL_WITHOUT_ROOT = "linktap";
private static final String SERVLET_URL = "/" + SERVLET_URL_WITHOUT_ROOT;
private static final long serialVersionUID = -23L;
private final Logger logger = LoggerFactory.getLogger(BindingServlet.class);
private final Object registerLock = new Object();
private volatile boolean registered;
@Nullable
HttpService httpService;
private @Nullable TranslationProvider translationProvider;
private @Nullable LocaleProvider localeProvider;
private @Nullable Bundle bundle;
public void setTranslationProviderInfo(TranslationProvider translationProvider, LocaleProvider localeProvider,
Bundle bundle) {
this.bundle = bundle;
this.localeProvider = localeProvider;
this.translationProvider = translationProvider;
}
public static final BindingServlet getInstance() {
return INSTANCE;
}
public String getLocalizedText(String key, @Nullable Object @Nullable... arguments) {
TranslationProvider translationProv = translationProvider;
LocaleProvider localeProv = localeProvider;
if (translationProv == null || localeProv == null) {
return key;
}
String result = translationProv.getText(bundle, key, key, localeProv.getLocale(), arguments);
return Objects.nonNull(result) ? result : key;
}
public void setHttpService(final HttpService httpService) {
this.httpService = httpService;
}
public static String getServletAddress(final String hostname, final String localizedWarning) {
final String httpPortStr = System.getProperty("org.osgi.service.http.port");
final Logger logger = LoggerFactory.getLogger(BindingServlet.class);
if (httpPortStr == null || httpPortStr.isEmpty()) {
logger.warn("{}", localizedWarning);
return EMPTY_STRING;
}
return "http://" + hostname + ":" + httpPortStr + SERVLET_URL;
}
public void registerServlet() {
final HttpService srv = httpService;
synchronized (registerLock) {
if (!registered && srv != null) {
try {
srv.registerServlet(SERVLET_URL, this, null, srv.createDefaultHttpContext());
registered = true;
logger.trace("Registered servlet " + SERVLET_URL);
} catch (NamespaceException | ServletException e) {
logger.warn("{}",
getLocalizedText("exception.fail-servlet-registration", SERVLET_URL, Utils.getMessage(e)));
}
}
}
}
public void unregisterServlet() {
final HttpService srv = httpService;
synchronized (registerLock) {
if (registered && srv != null) {
srv.unregister(SERVLET_URL);
registered = false;
logger.trace("Unregistered servlet");
}
}
}
@Override
protected void doPost(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
throws ServletException, IOException {
if (req == null) {
return;
}
int bufferSize = 1000; // The payload string is technically limited to 768 characters - this should be enough
// a single read to fully fit
char[] buffer = new char[bufferSize];
StringBuilder out = new StringBuilder();
try (Reader in = new InputStreamReader(req.getInputStream(), StandardCharsets.UTF_8)) {
for (int numRead; (numRead = in.read(buffer, 0, buffer.length)) > 0;) {
out.append(buffer, 0, numRead);
}
}
String payload = out.toString();
final TLGatewayFrame tlFrame = LinkTapBindingConstants.GSON.fromJson(payload, TLGatewayFrame.class);
String result = "";
if (tlFrame != null) {
TransactionProcessor tp = TransactionProcessor.getInstance();
result = tp.processGwRequest(req.getRemoteAddr(), tlFrame.command, payload);
}
if (resp == null) {
return;
}
resp.setContentType(MediaType.APPLICATION_JSON);
resp.setStatus(HttpStatus.OK_200);
resp.getWriter().append(result);
resp.getWriter().close();
}
}

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.linktap.protocol.servers;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
/**
* Implementations of this interface, allow access to a HttpClient which can be used
* for communication requests to LinkTap Gateways.
*
* @author David Goodyear - Initial contribution
*/
@NonNullByDefault
public interface IHttpClientProvider {
/**
* This returns a HttpClient reference which can be used for communication requests.
*
* @return instance of HttpClient to use for requests
*/
HttpClient getHttpClient();
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon:addon id="linktap" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
<type>binding</type>
<name>LinkTap Binding</name>
<description>This is the binding for LinkTap.</description>
<connection>local</connection>
<discovery-methods>
<discovery-method>
<service-type>mdns</service-type>
<discovery-parameters>
<discovery-parameter>
<name>mdnsServiceType</name>
<value>_http._tcp.local.</value>
</discovery-parameter>
</discovery-parameters>
<match-properties>
<match-property>
<name>name</name>
<regex>^(LinkTapGw_)</regex>
</match-property>
</match-properties>
</discovery-method>
</discovery-methods>
</addon:addon>

View File

@ -0,0 +1,313 @@
# add-on
addon.linktap.name = LinkTap Binding
addon.linktap.description = This is the binding for LinkTap.
# thing types
thing-type.linktap.device.label = LinkTap Binding Thing
thing-type.linktap.device.description = LinkTap Binding Device
thing-type.linktap.gateway.label = LinkTap Gateway
thing-type.linktap.gateway.description = This represents a LinkTap gateway
# thing types config
thing-type.config.linktap.device.enableAlerts.label = Auto Enable Alerts
thing-type.config.linktap.device.enableAlerts.description = If enabled, during device initialisation all alerts are enabled
thing-type.config.linktap.device.id.label = Device Id
thing-type.config.linktap.device.id.description = The Device Id for the device under the gateway
thing-type.config.linktap.device.name.label = Device Name
thing-type.config.linktap.device.name.description = The name allocated to the device by the app. (Must be unique if used)
thing-type.config.linktap.gateway.enableJSONComms.label = Enable non HTML responses
thing-type.config.linktap.gateway.enableJSONComms.description = Enable only if openHAB is directly using the Gateway, to allow more efficient communications
thing-type.config.linktap.gateway.enableMDNS.label = Enable mDNS Responder
thing-type.config.linktap.gateway.enableMDNS.description = On connection whether the mDNS responder should be enabled on the gateway device
thing-type.config.linktap.gateway.enforceProtocolLimits.label = Enforce protocol limits
thing-type.config.linktap.gateway.enforceProtocolLimits.description = If parameters outside the limits acceptable to the device's are sent they will be blocked and logged
thing-type.config.linktap.gateway.host.label = Hostname / IP
thing-type.config.linktap.gateway.host.description = The hostname / IP address of the gateway device
thing-type.config.linktap.gateway.password.label = Device Password
thing-type.config.linktap.gateway.password.description = The password if set for the gateway device
thing-type.config.linktap.gateway.username.label = Device Username
thing-type.config.linktap.gateway.username.description = The username if set for the gateway device
# channel types
channel-type.linktap.battery-level-type.label = Battery Level
channel-type.linktap.battery-level-type.description = Battery Remaining Level
channel-type.linktap.child-lock-type.label = Child Lock Mode
channel-type.linktap.child-lock-type.description = The child lock mode
channel-type.linktap.child-lock-type.state.option.0 = Unlocked
channel-type.linktap.child-lock-type.state.option.1 = Partially locked
channel-type.linktap.child-lock-type.state.option.2 = Completely locked
channel-type.linktap.fail-status-type.label = Shutdown Value Failed
channel-type.linktap.fail-status-type.description = The device has failed to close the valve
channel-type.linktap.failsafe-duration-type.label = Watering Cycle Failsafe
channel-type.linktap.failsafe-duration-type.description = Failsafe duration of the current watering cycle
channel-type.linktap.fall-status-type.label = Fallen Status
channel-type.linktap.fall-status-type.description = The device has fallen
channel-type.linktap.final-segment-type.label = Final ECO Segment
channel-type.linktap.final-segment-type.description = In ECO mode this is true when the final ON watering on segment is running
channel-type.linktap.flm-linked-type.label = FLM Linked
channel-type.linktap.flm-linked-type.description = The device has a included flow meter
channel-type.linktap.flow-rate-type.label = Flow Rate
channel-type.linktap.flow-rate-type.description = Current water flow rate
channel-type.linktap.instant-duration-type.label = Instant Duration Limit
channel-type.linktap.instant-duration-type.description = Max duration allowed for the immediate watering
channel-type.linktap.instant-limit-type.label = Instant Volume Limit
channel-type.linktap.instant-limit-type.description = Max Volume limit for immediate watering
channel-type.linktap.is-clog-type.label = Low Flow Detected
channel-type.linktap.is-clog-type.description = Unusually low flow rate detected alert
channel-type.linktap.is-leak-type.label = High Flow Detected
channel-type.linktap.is-leak-type.description = Unusually high flow rate detected alert
channel-type.linktap.man-mode-type.label = Manual Watering
channel-type.linktap.man-mode-type.description = Manual watering mode status
channel-type.linktap.mode-type.label = Watering Mode
channel-type.linktap.mode-type.description = The watering mode
channel-type.linktap.mode-type.state.option.0 = Off
channel-type.linktap.mode-type.state.option.1 = Instant
channel-type.linktap.mode-type.state.option.2 = Calendar
channel-type.linktap.mode-type.state.option.3 = Day
channel-type.linktap.mode-type.state.option.4 = Odd-even
channel-type.linktap.mode-type.state.option.5 = Interval
channel-type.linktap.mode-type.state.option.6 = Month
channel-type.linktap.pause-enable-type.label = Pause plan schedule
channel-type.linktap.pause-enable-type.description = When ON will pause the current watering plan for an hour every 55 minutes
channel-type.linktap.pause-until-type.label = Plan Paused Until
channel-type.linktap.pause-until-type.description = Displays when the last pause issued will expiry, resuming the current watering plan
channel-type.linktap.plan-id-type.label = Watering Plan Id
channel-type.linktap.plan-id-type.description = Displays the current watering plan id
channel-type.linktap.remaining-duration-type.label = Watering Cycle Remaining
channel-type.linktap.remaining-duration-type.description = Remaining duration of the current watering cycle
channel-type.linktap.rf-linked-type.label = RF Linked
channel-type.linktap.rf-linked-type.description = Is the device RF linked
channel-type.linktap.signal-level-type.label = Signal Level
channel-type.linktap.signal-level-type.description = Reception Signal Strength
channel-type.linktap.total-duration-type.label = Watering Cycle Duration
channel-type.linktap.total-duration-type.description = Total duration of current watering cycle
channel-type.linktap.volume-limit-type.label = Current Watering Limit
channel-type.linktap.volume-limit-type.description = Volume limit for the current watering cycle
channel-type.linktap.volume-type.label = Current Watering Volume
channel-type.linktap.volume-type.description = Accumulated volume of current watering cycle
channel-type.linktap.water-cut-type.label = Water Cutoff
channel-type.linktap.water-cut-type.description = Water cut-off alert
channel-type.linktap.watering-type.label = Watering
channel-type.linktap.watering-type.description = Active watering status
# thing types
thing-type.linktap.bridge.label = LinkTap Bridge
thing-type.linktap.bridge.description = The LinkTap bridge represents a LinkTap gateway device
# thing types config
thing-type.config.linktap.bridge.enableMDNS.label = Enable mDNS Responder
thing-type.config.linktap.bridge.enableMDNS.description = On connection whether the mDNS responder should be enabled on the gateway device
thing-type.config.linktap.bridge.host.label = Hostname / IP
thing-type.config.linktap.bridge.host.description = The hostname / IP address of the gateway device
thing-type.config.linktap.bridge.password.label = Device Password
thing-type.config.linktap.bridge.password.description = The password if set for the gateway device
thing-type.config.linktap.bridge.username.label = Device Username
thing-type.config.linktap.bridge.username.description = The username if set for the gateway device
# channel types
channel-type.linktap.future-rain-type.label = Watering Skipped Future
channel-type.linktap.future-rain-type.description = Future rainfall calculated when watering was skipped
channel-type.linktap.prev-rain-type.label = Watering Skipped Previous
channel-type.linktap.prev-rain-type.description = Previous rainfall calculated when watering was skipped
channel-type.linktap.rain-timestamp-type.label = Watering Skipped Timestamp
channel-type.linktap.rain-timestamp-type.description = Time when watering was skipped
# channel types
channel-type.linktap.instant-volume-limit-type.label = OH Instance On Watering Limit
channel-type.linktap.instant-volume-limit-type.description = Max Volume limit for immediate watering
# channel types
channel-type.linktap.battery-level.label = Battery Level
channel-type.linktap.battery-level.description = Battery Remaining Level
channel-type.linktap.child-lock-mode.label = Child-lock mode
channel-type.linktap.child-lock-mode.description = The child lock mode
channel-type.linktap.child-lock-mode.state.option.0 = Unlocked
channel-type.linktap.child-lock-mode.state.option.1 = Partially locked
channel-type.linktap.child-lock-mode.state.option.2 = Completely locked
channel-type.linktap.fail-status.label = Shutdown value failed
channel-type.linktap.fail-status.description = The device has failed to close the valve
channel-type.linktap.failsafe-duration.label = Watering Cycle Failsafe
channel-type.linktap.failsafe-duration.description = Failsafe duration of the current watering cycle
channel-type.linktap.fall-status.label = Fallen status
channel-type.linktap.fall-status.description = The device has fallen
channel-type.linktap.final-segment.label = Final ECO Segment
channel-type.linktap.final-segment.description = In ECO mode this is true when the final ON watering on segment is running
channel-type.linktap.flm-linked.label = FLM Linked
channel-type.linktap.flm-linked.description = The device has a included flow meter
channel-type.linktap.flow-rate.label = Flow Rate
channel-type.linktap.flow-rate.description = Current water flow rate
channel-type.linktap.instant-duration.label = OH Instance On Duration
channel-type.linktap.instant-duration.description = Max duration allowed for the immediate watering
channel-type.linktap.instant-volume-limit.label = OH Instance On Watering Limit
channel-type.linktap.instant-volume-limit.description = Max Volume limit for immediate watering
channel-type.linktap.is-clog.label = Low Flow Detected
channel-type.linktap.is-clog.description = Unusually low flow rate detected alert
channel-type.linktap.is-leak.label = High Flow Detected
channel-type.linktap.is-leak.description = Unusually high flow rate detected alert
channel-type.linktap.man-mode.label = Manual watering
channel-type.linktap.man-mode.description = Manual watering mode status
channel-type.linktap.mode.label = Watering mode
channel-type.linktap.mode.description = The watering mode
channel-type.linktap.mode.state.option.0 = Off
channel-type.linktap.mode.state.option.1 = Instant
channel-type.linktap.mode.state.option.2 = Calendar
channel-type.linktap.mode.state.option.3 = Day
channel-type.linktap.mode.state.option.4 = Odd-even
channel-type.linktap.mode.state.option.5 = Interval
channel-type.linktap.mode.state.option.6 = Month
channel-type.linktap.remaining-duration.label = Watering Cycle Remaining
channel-type.linktap.remaining-duration.description = Remaining duration of the current watering cycle
channel-type.linktap.rf-inked.label = RF Linked
channel-type.linktap.rf-inked.description = Is the device RF linked
channel-type.linktap.signal-level.label = Signal Level
channel-type.linktap.signal-level.description = Reception Signal Strength
channel-type.linktap.skip-future-rain.label = Watering Skipped Future Rain
channel-type.linktap.skip-future-rain.description = Future rainfall calculated when watering was skipped
channel-type.linktap.skip-prev-rain.label = Watering Skipped Previous Rain
channel-type.linktap.skip-prev-rain.description = Previous rainfall calculated when watering was skipped
channel-type.linktap.skip-timestamp.label = Watering Skipped Timestamp
channel-type.linktap.skip-timestamp.description = Time when watering was skipped
channel-type.linktap.total-duration.label = Watering Cycle Duration
channel-type.linktap.total-duration.description = Total duration of current watering cycle
channel-type.linktap.volume-limit.label = Current Watering Limit
channel-type.linktap.volume-limit.description = Volume limit for the current watering cycle
channel-type.linktap.volume.label = Current Watering Volume
channel-type.linktap.volume.description = Accumulated volume of current watering cycle
channel-type.linktap.water-cut-off.label = Water Cutoff
channel-type.linktap.water-cut-off.description = Water cut-off alert
channel-type.linktap.watering.label = Watering
channel-type.linktap.watering.description = Active watering status
# channel types
channel-type.linktap.deviceBatteryLevel.label = Battery Level
channel-type.linktap.deviceBatteryLevel.description = Battery Remaining Level
channel-type.linktap.deviceChildLockMode.label = Child-lock mode
channel-type.linktap.deviceChildLockMode.description = The child lock mode
channel-type.linktap.deviceChildLockMode.state.option.0 = Unlocked
channel-type.linktap.deviceChildLockMode.state.option.1 = Partially locked
channel-type.linktap.deviceChildLockMode.state.option.2 = Completely locked
channel-type.linktap.deviceFailStatus.label = Shutdown value failed
channel-type.linktap.deviceFailStatus.description = The device has failed to close the valve
channel-type.linktap.deviceFailsafeDuration.label = Watering Cycle Failsafe
channel-type.linktap.deviceFailsafeDuration.description = Failsafe duration of the current watering cycle
channel-type.linktap.deviceFallStatus.label = Fallen status
channel-type.linktap.deviceFallStatus.description = The device has fallen
channel-type.linktap.deviceFinalSegment.label = Final ECO Segment
channel-type.linktap.deviceFinalSegment.description = In ECO mode this is true when the final ON watering on segment is running
channel-type.linktap.deviceFlmLinked.label = FLM Linked
channel-type.linktap.deviceFlmLinked.description = The device has a included flow meter
channel-type.linktap.deviceFlowRate.label = Flow Rate
channel-type.linktap.deviceFlowRate.description = Current water flow rate
channel-type.linktap.deviceIsClog.label = Low Flow Detected
channel-type.linktap.deviceIsClog.description = Unusually low flow rate detected alert
channel-type.linktap.deviceIsLeak.label = High Flow Detected
channel-type.linktap.deviceIsLeak.description = Unusually high flow rate detected alert
channel-type.linktap.deviceManMode.label = Manual watering
channel-type.linktap.deviceManMode.description = Manual watering mode status
channel-type.linktap.deviceMode.label = Watering mode
channel-type.linktap.deviceMode.description = The watering mode
channel-type.linktap.deviceMode.state.option.0 = Off
channel-type.linktap.deviceMode.state.option.1 = Instant
channel-type.linktap.deviceMode.state.option.2 = Calendar
channel-type.linktap.deviceMode.state.option.3 = Day
channel-type.linktap.deviceMode.state.option.4 = Odd-even
channel-type.linktap.deviceMode.state.option.5 = Interval
channel-type.linktap.deviceMode.state.option.6 = Month
channel-type.linktap.deviceRemainingDuration.label = Watering Cycle Remaining
channel-type.linktap.deviceRemainingDuration.description = Remaining duration of the current watering cycle
channel-type.linktap.deviceRfLinked.label = RF Linked
channel-type.linktap.deviceRfLinked.description = Is the device RF linked
channel-type.linktap.deviceSignalLevel.label = Signal Level
channel-type.linktap.deviceSignalLevel.description = Reception Signal Strength
channel-type.linktap.deviceTotalDuration.label = Watering Cycle Duration
channel-type.linktap.deviceTotalDuration.description = Total duration of current watering cycle
channel-type.linktap.deviceVolume.label = Current Watering Volume
channel-type.linktap.deviceVolume.description = Accumulated volume of current watering cycle
channel-type.linktap.deviceVolumeLimit.label = Current Watering Limit
channel-type.linktap.deviceVolumeLimit.description = Volume limit for the current watering cycle
channel-type.linktap.deviceWaterCutoff.label = Water Cutoff
channel-type.linktap.deviceWaterCutoff.description = Water cut-off alert
channel-type.linktap.deviceWatering.label = Watering
channel-type.linktap.deviceWatering.description = Active watering status
channel-type.linktap.ohInstantDuration.label = OH Instance On Duration
channel-type.linktap.ohInstantDuration.description = Max duration allowed for the immediate watering
channel-type.linktap.ohInstantVolumeLimit.label = OH Instance On Watering Limit
channel-type.linktap.ohInstantVolumeLimit.description = Max Volume limit for immediate watering
channel-type.linktap.waterSkipDateTime.label = Watering Skipped Timestamp
channel-type.linktap.waterSkipDateTime.description = Time when watering was skipped
channel-type.linktap.waterSkipFutureRain.label = Watering Skipped Future Rain
channel-type.linktap.waterSkipFutureRain.description = Future rainfall calculated when watering was skipped
channel-type.linktap.waterSkipPrevRain.label = Watering Skipped Previous Rain
channel-type.linktap.waterSkipPrevRain.description = Previous rainfall calculated when watering was skipped
# errors
bridge.error.host-not-found = Hostname / IP cannot be found
bridge.error.check-credentials = Check credentials provided
bridge.error.cannot-connect = Cannot connect to LinkTap Gateway
bridge.error.unknown-host = Unknown host
bridge.error.target-is-not-gateway = Target Host is not a LinkTap Gateway
polling-device.error.bridge-unset = Bridge is not selected / set
polling-device.error.device-unknown-in-bridge = Device not found in bridges known devices
polling-device.error.unknown-device-id = Bridge does not recognise device id
polling-device.error.unknown-device = Check device setup - device is unknown
protocol.ret.success = Success
protocol.ret.format-error = Message format error
protocol.ret.cmd-unsupported = CMD message not supported
protocol.ret.gw-id-unmatched = Gateway ID not matched
protocol.ret.end-device-id-error = End device ID error
protocol.ret.end-device-id-not-found = End device ID not found
protocol.ret.gw-internal-error = Gateway internal error
protocol.ret.conflict-watering-plan = Conflict with watering plan
protocol.ret.gw-busy = Gateway busy
protocol.ret.bad-parameter-in-msg = Bad parameter in message
protocol.ret.invalid = Missing ret response
warning.failed-local-address-detection = Failed to determine local openHab address due to connection failure with exception {}
warning.no-http-server-port = HTTP Server port is not running, cannot use API callbacks
warning.fw-update-local-config = {0} -> Local configuration support requires newer firmware it should be >= {1}
warning.user-data-payload-failure = Device {0} payload validation failed - will not send due to bad data -> {1}
warning.parameter-not-accepted = Parameter not accepted by device {0} for command {1}
warning.response-from-wrong-gw-id = {0} = Response from incorrect Gateway "{1}" != "{2}"
warning.incorrect-cmd-resp = {0} = Received incorrect CMD response {1} != {2}
warning.non-gw = Communicating with non TapLink Gateway detected
warning.not-taplink-gw = {0} = {1} is not a Link Tap Gateway!
warning.comms-issue-auto-retry = {0} = Possible communications issue (auto retry): {1}
warning.device-no-accept = Device {0} did not accept command {1}
warning.error-with-gw-id = Error with gateway ID
warning.host-gw-unknown-for-cmd = Request when host "{0}" or gateway "{1}" id is unknown for command {2}
warning.discovery-charset-missing = Missing character set to decode MDNS Text
warning.unexpected-response-frame = Unexpected response frame {0} -> {1}
warning.unexpected-cmd-result = Unexpected command result
bug-report.failed-alert-enable = Raise Bug Report: {0} - Failed to enable all alerts - invalid parameter exception
bug-report.poll-failure = Raise Bug Report: {0} - Poll failure - invalid parameter exception
bug-report.pause-plan-failure = Raise Bug Report: {0} - Pause plan failure - invalid parameter exception
bug-report.unexpected-payload-failure = Potential Bug: Device {0} payload validation failed - will not send -> {1}
bug-report.gw-unsupported-command = Raise Bug Report: Command {0} not supported by gateway
exception.device-id-exception = Device ID Exception
exception.gw-id-exception = Gateway ID Exception
exception-cmd-not-supported-exception = Command Not Supported Exception
exception.invalid-parameter-exception = Invalid Parameter Exception
exception.could-not-connect = Could not connect
exception.could-not-resolve = Could not resolve IP address
exception.communications-lost = Communications Lost
exception.local-addr-lookup-failure = Local address lookup failure
exception.exec-exception = ExecutionException -> {0}
exception.not-gw.missing-headers = Missing header markers
exception.not-gw.missing-title = Not a LinkTap API response
exception.not-gw.missing-server-title = Not a LinkTap response
exception.not-gw.unexpected-status-code = Unexpected status code response
exception.not-gw.unexpected-protocol = Unexpected protocol
exception.not-tap-link-gw = Not a TapLink GW
exception.fail-servlet-registration = Register servlet failed for {0}
exception.unexpected-failure = Unexpected failure -> {0}
exception.unexpected-exception = Unexpected exception

View File

@ -0,0 +1,223 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="linktap"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="mode-type">
<item-type>String</item-type>
<label>Watering Mode</label>
<description>The watering mode</description>
<category>Time</category>
<state readOnly="false">
<options>
<option value="0">Off</option>
<option value="1">Instant</option>
<option value="2">Calendar</option>
<option value="3">Day</option>
<option value="4">Odd-even</option>
<option value="5">Interval</option>
<option value="6">Month</option>
</options>
</state>
</channel-type>
<channel-type id="man-mode-type">
<item-type>Switch</item-type>
<label>Manual Watering</label>
<description>Manual watering mode status</description>
<category>Water</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="watering-type">
<item-type>Switch</item-type>
<label>Watering</label>
<description>Active watering status</description>
<category>Water</category>
<state readOnly="false"/>
</channel-type>
<channel-type id="rf-linked-type">
<item-type>Switch</item-type>
<label>RF Linked</label>
<description>Is the device RF linked</description>
<category>Switch</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="flm-linked-type">
<item-type>Switch</item-type>
<label>FLM Linked</label>
<description>The device has a included flow meter</description>
<category>Switch</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="is-leak-type">
<item-type>Switch</item-type>
<label>High Flow Detected</label>
<description>Unusually high flow rate detected alert</description>
<category>Alarm</category>
<state readOnly="false"/>
</channel-type>
<channel-type id="is-clog-type">
<item-type>Switch</item-type>
<label>Low Flow Detected</label>
<description>Unusually low flow rate detected alert</description>
<category>Alarm</category>
<state readOnly="false"/>
</channel-type>
<channel-type id="fall-status-type">
<item-type>Switch</item-type>
<label>Fallen Status</label>
<description>The device has fallen</description>
<category>Alarm</category>
<state readOnly="false"/>
</channel-type>
<channel-type id="fail-status-type">
<item-type>Switch</item-type>
<label>Shutdown Value Failed</label>
<description>The device has failed to close the valve</description>
<category>Alarm</category>
<state readOnly="false"/>
</channel-type>
<channel-type id="final-segment-type">
<item-type>Switch</item-type>
<label>Final ECO Segment</label>
<description>In ECO mode this is true when the final ON watering on segment is running</description>
<category>Switch</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="signal-level-type">
<item-type>Number:Dimensionless</item-type>
<label>Signal Level</label>
<description>Reception Signal Strength</description>
<category>QualityOfService</category>
<state readOnly="true" pattern="%.0f %%"/>
</channel-type>
<channel-type id="battery-level-type">
<item-type>Number:Dimensionless</item-type>
<label>Battery Level</label>
<description>Battery Remaining Level</description>
<category>BatteryLevel</category>
<state readOnly="true" pattern="%.0f %%"/>
</channel-type>
<channel-type id="water-cut-type">
<item-type>Switch</item-type>
<label>Water Cutoff</label>
<description>Water cut-off alert</description>
<category>Alarm</category>
<state readOnly="false"/>
</channel-type>
<channel-type id="flow-rate-type">
<item-type unitHint="l/min">Number:VolumetricFlowRate</item-type>
<label>Flow Rate</label>
<description>Current water flow rate</description>
<category>Flow</category>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="volume-type">
<item-type unitHint="l">Number:Volume</item-type>
<label>Current Watering Volume</label>
<description>Accumulated volume of current watering cycle</description>
<category>Water</category>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="volume-limit-type">
<item-type unitHint="l">Number:Volume</item-type>
<label>Current Watering Limit</label>
<description>Volume limit for the current watering cycle</description>
<category>Water</category>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="total-duration-type">
<item-type unitHint="s">Number:Time</item-type>
<label>Watering Cycle Duration</label>
<description>Total duration of current watering cycle</description>
<category>Time</category>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="remaining-duration-type">
<item-type unitHint="s">Number:Time</item-type>
<label>Watering Cycle Remaining</label>
<description>Remaining duration of the current watering cycle</description>
<category>Time</category>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="failsafe-duration-type">
<item-type unitHint="s">Number:Time</item-type>
<label>Watering Cycle Failsafe</label>
<description>Failsafe duration of the current watering cycle</description>
<category>Time</category>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="child-lock-type">
<item-type>String</item-type>
<label>Child Lock Mode</label>
<description>The child lock mode</description>
<category>Lock</category>
<state readOnly="false">
<options>
<option value="0">Unlocked</option>
<option value="1">Partially locked</option>
<option value="2">Completely locked</option>
</options>
</state>
</channel-type>
<channel-type id="instant-duration-type">
<item-type unitHint="s">Number:Time</item-type>
<label>Instant Duration Limit</label>
<description>Max duration allowed for the immediate watering</description>
<category>Time</category>
<state readOnly="false" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="instant-limit-type">
<item-type unitHint="l">Number:Volume</item-type>
<label>Instant Volume Limit</label>
<description>Max Volume limit for immediate watering</description>
<category>Water</category>
<state readOnly="false" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="pause-enable-type">
<item-type>Switch</item-type>
<label>Pause plan schedule</label>
<description>When ON will pause the current watering plan for an hour every 55 minutes</description>
<category>Time</category>
<state readOnly="false"/>
</channel-type>
<channel-type id="pause-until-type">
<item-type>DateTime</item-type>
<label>Plan Paused Until</label>
<description>Displays when the last pause issued will expiry, resuming the current watering plan</description>
<category>Calendar</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="plan-id-type">
<item-type>String</item-type>
<label>Watering Plan Id</label>
<description>Displays the current watering plan id</description>
<category>Calendar</category>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="linktap"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="gateway">
<label>LinkTap Gateway</label>
<description>This represents a LinkTap gateway</description>
<properties>
<property name="gatewayId"/>
<property name="hardwareModel"/>
<property name="version"/>
<property name="macAddress"/>
<property name="httpApiEnabled"/>
<property name="httpApiCallback"/>
<property name="volumeUnit"/>
<property name="utcOffset"/>
</properties>
<config-description>
<parameter name="host" type="text" required="true">
<context>network-address</context>
<label>Hostname / IP</label>
<description>The hostname / IP address of the gateway device</description>
</parameter>
<parameter name="username" type="text" required="false">
<label>Device Username</label>
<description>The username if set for the gateway device</description>
</parameter>
<parameter name="password" type="text" required="false">
<context>password</context>
<label>Device Password</label>
<description>The password if set for the gateway device</description>
</parameter>
<parameter name="enableMDNS" type="boolean">
<label>Enable mDNS Responder</label>
<description>On connection whether the mDNS responder should be enabled on the gateway device</description>
<default>true</default>
<advanced>true</advanced>
</parameter>
<parameter name="enableJSONComms" type="boolean">
<label>Enable non HTML responses</label>
<description>Enable only if openHAB is directly using the Gateway, to allow more efficient communications</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="enforceProtocolLimits" type="boolean">
<label>Enforce protocol limits</label>
<description>If parameters outside the limits acceptable to the device's are sent they will be blocked and logged</description>
<default>true</default>
<advanced>true</advanced>
</parameter>
</config-description>
</bridge-type>
<thing-type id="device">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>LinkTap Binding Thing</label>
<description>LinkTap Binding Device</description>
<channels>
<channel id="mode" typeId="mode-type"/>
<channel id="manual-watering" typeId="man-mode-type"/>
<channel id="watering" typeId="watering-type"/>
<channel id="rf-linked" typeId="rf-linked-type"/>
<channel id="flm-linked" typeId="flm-linked-type"/>
<channel id="water-cut" typeId="water-cut-type"/>
<channel id="fall-status" typeId="fall-status-type"/>
<channel id="shutdown-failure" typeId="fail-status-type"/>
<channel id="high-flow" typeId="is-leak-type"/>
<channel id="low-flow" typeId="is-clog-type"/>
<channel id="eco-final" typeId="final-segment-type"/>
<channel id="signal" typeId="signal-level-type"/>
<channel id="battery" typeId="battery-level-type"/>
<channel id="child-lock" typeId="child-lock-type"/>
<channel id="flow-rate" typeId="flow-rate-type"/>
<channel id="volume" typeId="volume-type"/>
<channel id="duration" typeId="total-duration-type"/>
<channel id="remaining" typeId="remaining-duration-type"/>
<channel id="dur-limit" typeId="failsafe-duration-type"/>
<channel id="vol-limit" typeId="volume-limit-type"/>
<channel id="oh-dur-limit" typeId="instant-duration-type"/>
<channel id="oh-vol-limit" typeId="instant-limit-type"/>
<channel id="plan-pause-enable" typeId="pause-enable-type"/>
<channel id="plan-resume-time" typeId="pause-until-type"/>
<channel id="watering-plan-id" typeId="plan-id-type"/>
</channels>
<properties>
<property name="deviceId">Device Id</property>
<property name="deviceName">Device Name</property>
</properties>
<representation-property>deviceId</representation-property>
<config-description>
<parameter name="id" type="text" required="false">
<label>Device Id</label>
<description>The Device Id for the device under the gateway</description>
</parameter>
<parameter name="name" type="text" required="false">
<label>Device Name</label>
<description>The name allocated to the device by the app. (Must be unique if used)</description>
</parameter>
<parameter name="enableAlerts" type="boolean" required="false">
<label>Auto Enable Alerts</label>
<description>If enabled, during device initialisation all alerts are enabled</description>
<default>true</default>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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 );
}
}

View File

@ -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 );
}
}

View File

@ -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);
}
}

View File

@ -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"));
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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]);
}
}

View File

@ -229,6 +229,7 @@
<module>org.openhab.binding.lifx</module>
<module>org.openhab.binding.linky</module>
<module>org.openhab.binding.linuxinput</module>
<module>org.openhab.binding.linktap</module>
<module>org.openhab.binding.liquidcheck</module>
<module>org.openhab.binding.lirc</module>
<module>org.openhab.binding.livisismarthome</module>