mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[linktap] Initial contribution (#17235)
* [linkTap] Initial Code Commit [Signed-off-by: dag81 <david.goodyear@gmail.com>
This commit is contained in:
parent
85b165208c
commit
b11c751d03
@ -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>
|
||||
|
20
bundles/org.openhab.binding.linktap/NOTICE
Normal file
20
bundles/org.openhab.binding.linktap/NOTICE
Normal 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
|
234
bundles/org.openhab.binding.linktap/README.md
Normal file
234
bundles/org.openhab.binding.linktap/README.md
Normal 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.
|
||||
|
36
bundles/org.openhab.binding.linktap/pom.xml
Normal file
36
bundles/org.openhab.binding.linktap/pom.xml
Normal 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>
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
@ -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;
|
||||
*/
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 timer’s status
|
||||
*/
|
||||
public static final int CMD_UPDATE_WATER_TIMER_STATUS = 3;
|
||||
|
||||
/**
|
||||
* Command - 103. Update Water Timer Status Unsolicited
|
||||
*
|
||||
* @direction GW->Broker->App
|
||||
* @description Update water timer’s status
|
||||
*/
|
||||
public static final int CMD_UPDATE_WATER_TIMER_STATUS_UNSOLICITED = 103;
|
||||
|
||||
/**
|
||||
* Command - 4. Send / Setup Water Plan
|
||||
*
|
||||
* @direction App->Broker->GW
|
||||
* @description Send / set up watering plan
|
||||
* (The prerequisite for the correct execution of the watering plan is that
|
||||
* the Gateway’s local time base has been properly set through
|
||||
* CMD:0 (CMD_HANDSHAKE) or
|
||||
* CMD:13)
|
||||
*/
|
||||
public static final int CMD_SETUP_WATER_PLAN = 4;
|
||||
|
||||
/**
|
||||
* Command - 5. Delete Water Plan
|
||||
*
|
||||
* @direction App->Broker->GW
|
||||
* @description Deletes the existing water plan
|
||||
*/
|
||||
public static final int CMD_REMOVE_WATER_PLAN = 5;
|
||||
|
||||
/**
|
||||
* Command - 6. Start Watering Immediately
|
||||
*
|
||||
* @direction App->Broker->GW
|
||||
* @description Start watering for the immediate duration irrelevant
|
||||
* of water plan. (Gateway local time base is not required
|
||||
* for the operation of this mode).
|
||||
*/
|
||||
public static final int CMD_IMMEDIATE_WATER_START = 6;
|
||||
|
||||
/**
|
||||
* Command - 7. Stop Watering Immediately
|
||||
*
|
||||
* @direction App->Broker->GW
|
||||
* @description Stop's watering immediately. The water plan will resume at
|
||||
* the next point as setup.
|
||||
*/
|
||||
public static final int CMD_IMMEDIATE_WATER_STOP = 7;
|
||||
|
||||
/**
|
||||
* Command - 8. Fetch / Push Rainfall Data
|
||||
*
|
||||
* @direction GW->Broker->App
|
||||
* @description Request for Rainfall data
|
||||
* @direction App->Broker->GW
|
||||
* @description Push of Rainfall data
|
||||
*/
|
||||
public static final int CMD_RAINFALL_DATA = 8;
|
||||
|
||||
/**
|
||||
* Command - 9. Notificaiton of watering has been skipped
|
||||
*
|
||||
* @direction GW->Broker->App
|
||||
* @description Notification that a watering cycle has been skipped due to rainfall
|
||||
*/
|
||||
public static final int CMD_NOTIFICATION_WATERING_SKIPPED = 9;
|
||||
|
||||
/**
|
||||
* Command - 10. Alert Enablement / Disablement
|
||||
*
|
||||
* @direction App->Broker->GW
|
||||
* @description Enable or disablement of particular monitoring alerts
|
||||
*/
|
||||
public static final int CMD_ALERT_ENABLEMENT = 10;
|
||||
|
||||
/**
|
||||
* Command - 11. Dismiss Alert
|
||||
*
|
||||
* @direction App->Broker->GW
|
||||
* @description Dismisses the given alert
|
||||
*/
|
||||
public static final int CMD_ALERT_DISMISS = 11;
|
||||
|
||||
/**
|
||||
* Command - 12 Lockout state setup
|
||||
*
|
||||
* @direction App->Broker->GW
|
||||
* @description Setup lockout state for manual On/Off button (for G15 and G25 models only)
|
||||
*/
|
||||
public static final int CMD_LOCKOUT_STATE = 12;
|
||||
|
||||
/**
|
||||
* Command - 13 Gateways Date & Time Sync Request
|
||||
*
|
||||
* @direction GW->Broker->App
|
||||
* @description Request for the current date and time, for the Gateway to apply
|
||||
*/
|
||||
public static final int CMD_DATETIME_SYNC = 13;
|
||||
|
||||
/**
|
||||
* Command - 14 Read the Gateways Date & Time
|
||||
*
|
||||
* @direction App->Broker->Gw
|
||||
* @description Fetch Gateway's local datetime
|
||||
*/
|
||||
public static final int CMD_DATETIME_READ = 14;
|
||||
|
||||
/**
|
||||
* Command - 15 Test wireless performance of end device
|
||||
*
|
||||
* @direction App->Broker->Gw
|
||||
* @description Request a communications test between the Gateway and End Device
|
||||
*/
|
||||
public static final int CMD_WIRELESS_CHECK = 15;
|
||||
|
||||
/**
|
||||
* Command - 16 Get Gateway's configuration
|
||||
*
|
||||
* @direction App->Broker->Gw
|
||||
* @description Request the current Gateway's configuration
|
||||
*/
|
||||
public static final int CMD_GET_CONFIGURATION = 16;
|
||||
|
||||
/**
|
||||
* Command - 17 Set Gateway's configuration
|
||||
*
|
||||
* @direction App->Broker->Gw
|
||||
* @description Update the configuration for a device in the Gateway
|
||||
*/
|
||||
public static final int CMD_SET_CONFIGURATION = 17;
|
||||
|
||||
/**
|
||||
* Command - 18 Pause Water Plan
|
||||
*
|
||||
* @direction App->Broker->Gw
|
||||
* @description Pause the Water Plan for the given duration
|
||||
* (0.1 to 240 hours)
|
||||
*/
|
||||
public static final int CMD_PAUSE_WATER_PLAN = 18;
|
||||
}
|
@ -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() {
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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" };
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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>
|
@ -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
|
@ -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>
|
@ -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>
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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 );
|
||||
}
|
||||
|
||||
}
|
@ -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 );
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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]);
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user