mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +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>
|
<artifactId>org.openhab.binding.lifx</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
|
<artifactId>org.openhab.binding.linktap</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openhab.addons.bundles</groupId>
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
<artifactId>org.openhab.binding.linky</artifactId>
|
<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.lifx</module>
|
||||||
<module>org.openhab.binding.linky</module>
|
<module>org.openhab.binding.linky</module>
|
||||||
<module>org.openhab.binding.linuxinput</module>
|
<module>org.openhab.binding.linuxinput</module>
|
||||||
|
<module>org.openhab.binding.linktap</module>
|
||||||
<module>org.openhab.binding.liquidcheck</module>
|
<module>org.openhab.binding.liquidcheck</module>
|
||||||
<module>org.openhab.binding.lirc</module>
|
<module>org.openhab.binding.lirc</module>
|
||||||
<module>org.openhab.binding.livisismarthome</module>
|
<module>org.openhab.binding.livisismarthome</module>
|
||||||
|
Loading…
Reference in New Issue
Block a user