mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 23:22:02 +01:00
S-Bus binding protocol. Derived from Modbus protocol
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
parent
706ae58323
commit
2ee968f918
189
bundles/org.openhab.binding.sbus/DEVELOPERS.md
Normal file
189
bundles/org.openhab.binding.sbus/DEVELOPERS.md
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
# For Developers
|
||||||
|
|
||||||
|
## Debugging an addon
|
||||||
|
|
||||||
|
Please follow IDE setup guide at <https://www.openhab.org/docs/developer/ide/eclipse.html>.
|
||||||
|
|
||||||
|
When configuring dependencies in `openhab-distro/launch/app/pom.xml`, add all dependencies, including the transitive dependencies:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
|
<artifactId>org.openhab.binding.sbus</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openhab.core.bundles</groupId>
|
||||||
|
<artifactId>org.openhab.core.io.transport.sbus</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.wimpi</groupId>
|
||||||
|
<artifactId>jamod</artifactId>
|
||||||
|
<version>1.2.4.OH</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Serial Implementation
|
||||||
|
|
||||||
|
You can use test serial slaves without any hardware on Linux using these steps:
|
||||||
|
|
||||||
|
1. Set-up virtual null modem emulator using [tty0tty](https://github.com/freemed/tty0tty)
|
||||||
|
1. Download [diagslave](https://www.modbusdriver.com/diagslave.html) and start modbus serial slave up using this command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./diagslave -m rtu -a 1 -b 38400 -d 8 -s 1 -p none -4 10 /dev/pts/7
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Configure openHAB's modbus slave to connect to `/dev/pts/8`.
|
||||||
|
|
||||||
|
1. Modify `start.sh` or `start_debug.sh` to include the unconventional port name by adding the following argument to `java`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
-Dgnu.io.rxtx.SerialPorts=/dev/pts/8
|
||||||
|
```
|
||||||
|
|
||||||
|
Naturally this is not the same thing as the real thing but helps to identify simple issues.
|
||||||
|
|
||||||
|
## Testing UDP Implementation
|
||||||
|
|
||||||
|
1. Download [diagslave](https://www.modbusdriver.com/diagslave.html) and start modbus udp server (slave) using this command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./diagslave -m udp -a 1 -p 6000
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Configure openHAB's modbus slave to connect to `127.0.0.1:6000`.
|
||||||
|
|
||||||
|
## Writing Data
|
||||||
|
|
||||||
|
See this [community post](https://community.openhab.org/t/something-is-rounding-my-float-values-in-sitemap/13704/32?u=ssalonen) explaining how `pollmb` and `diagslave` can be used to debug modbus communication.
|
||||||
|
|
||||||
|
You can also use `modpoll` to write data:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# write value=5 to holding register 40001 (index=0 in the binding)
|
||||||
|
./modpoll -m udp -a 1 -r 1 -t4 -p 6000 127.0.0.1 5
|
||||||
|
# set coil 00001 (index=0 in the binding) to TRUE
|
||||||
|
./modpoll -m udp -a 1 -r 1 -t0 -p 6000 127.0.0.1 1
|
||||||
|
# write float32
|
||||||
|
./modpoll -m udp -a 1 -r 1 -t4:float -p 6000 127.0.0.1 3.14
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extending Modbus Binding
|
||||||
|
|
||||||
|
This Modbus binding can be extended by other OSGi bundles to add more specific support for Modbus enabled devices.
|
||||||
|
To do so to you have to create a new OSGi bundle which has the same binding id as this binding.
|
||||||
|
The best way is to use the `ModbusBindingConstants.BINDING_ID` constant.
|
||||||
|
|
||||||
|
### Thing Handler
|
||||||
|
|
||||||
|
You will have to create one or more handler classes for the devices you want to support.
|
||||||
|
For the modbus connection setup and handling you can use the Modbus UDP Slave or Modbus Serial Slave handlers.
|
||||||
|
Your handler should use these handlers as bridges and you can set up your regular or one shot modbus requests to read from the slave.
|
||||||
|
This is done by extending your `ThingHandler` by `BaseModbusThingHandler`.
|
||||||
|
You can use the inherited methods `submitOneTimePoll()` and `registerRegularPoll()` to poll values and `submitOneTimeWrite()` to send values to a slave.
|
||||||
|
The `BaseModbusThingHandler` takes care that every regular poll task is cancelled, when the Thing is disposed.
|
||||||
|
Despite that, you can cancel the task manually by storing the return value of `registerRegularPoll()` and use it as an argument to `unregisterRegularPoll()`.
|
||||||
|
|
||||||
|
Please keep in mind that these reads are asynchronous and they will call your callback once the read is done.
|
||||||
|
|
||||||
|
Once you have your data read from the modbus device you can parse and transform them then update your channels to publish these data to the openHAB system.
|
||||||
|
|
||||||
|
See the following example:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@NonNullByDefault
|
||||||
|
public class MyHandler extends BaseModbusThingHandler {
|
||||||
|
public MyHandler(Thing thing) {
|
||||||
|
super(thing);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||||
|
if (command instanceof RefreshType) {
|
||||||
|
ModbusReadRequestBlueprint blueprint = new ModbusReadRequestBlueprint(getSlaveId(),
|
||||||
|
ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 0, 1, 2);
|
||||||
|
|
||||||
|
submitOneTimePoll(blueprint, this::readSuccessful, this::readError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void modbusInitialize() {
|
||||||
|
// do other Thing initialization
|
||||||
|
|
||||||
|
ModbusReadRequestBlueprint blueprint = new ModbusReadRequestBlueprint(getSlaveId(),
|
||||||
|
ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 0, 1, 2);
|
||||||
|
|
||||||
|
registerRegularPoll(blueprint, 1000, 0, this::readSuccessful, this::readError);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readSuccessful(AsyncModbusReadResult result) {
|
||||||
|
result.getRegisters().ifPresent(registers -> {
|
||||||
|
Optional<DecimalType> value = ModbusBitUtilities.extractStateFromRegisters(registers, 0, ValueType.INT16);
|
||||||
|
// process value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readError(AsyncModbusFailure<ModbusReadRequestBlueprint> error) {
|
||||||
|
// set the Thing offline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Discovery
|
||||||
|
|
||||||
|
If you write a device specific handler then adding discovery for this device is very welcome.
|
||||||
|
You will have to write a discovery participant class which implements the `ModbusDiscoveryParticipant` interface and registers itself as a component. Example:
|
||||||
|
|
||||||
|
```java
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@NonNullByDefault
|
||||||
|
public class SunspecDiscoveryParticipant implements ModbusDiscoveryParticipant {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There are two methods you have to implement:
|
||||||
|
|
||||||
|
- `getSupportedThingTypeUIDs` should return a list of the thing type UIDs that are supported by this discovery participant. This is fairly straightforward.
|
||||||
|
|
||||||
|
- `startDiscovery` method will be called when a discovery process has began. This method receives two parameters:
|
||||||
|
|
||||||
|
- `ModbusEndpointThingHandler` is the endpoint's handler that should be tested if it is known by your bundle. You can start your read requests against this handler.
|
||||||
|
|
||||||
|
- `ModbusDiscoveryListener` this listener instance should be used to report any known devices found and to notify the main discovery process when your binding has finished the discovery.
|
||||||
|
|
||||||
|
Please try to avoid write requests to the endpoint because it could be some unknown device that write requests could misconfigure.
|
||||||
|
|
||||||
|
When a known device is found a `DiscoveryResult` object has to be created then the `thingDiscovered` method has to be called.
|
||||||
|
The `DiscoveryResult` supports properties, and you should use this to store any data that will be useful when the actual thing will be created.
|
||||||
|
For example you could store the start Modbus address of the device or vendor/model informations.
|
||||||
|
|
||||||
|
When the discovery process is finished either by detecting a device or by realizing it is not supported you should call the `discoveryFinished` method.
|
||||||
|
This will tear down any resources allocated for the discovery process.
|
||||||
|
|
||||||
|
### Discovery Architecture
|
||||||
|
|
||||||
|
The following diagram shows the concept how discovery is implemented in this binding. (Note that some intermediate classes and interfaces are not shown for clarity.)
|
||||||
|
|
||||||
|
![Discovery architecture](doc/images/ModbusExtensibleDiscovery.png)
|
||||||
|
|
||||||
|
As stated above the discovery process can be extended by OSGi bundles.
|
||||||
|
For this they have to define their own `ModbusDisvoceryParticipant` that gets registered at the `ModbusDiscoveryService`.
|
||||||
|
This object also keeps track of any of the Modbus handlers.
|
||||||
|
Handler level discovery logic is implemented in the `ModbusEndpointDiscoveryService` which gets instantiated for each Modbus `BridgeHandler`.
|
||||||
|
|
||||||
|
The communication flow is detailed in the diagram below:
|
||||||
|
|
||||||
|
![Discovery process](doc/images/DiscoveryProcess.png)
|
||||||
|
|
||||||
|
As can be seen the process is initiated by the `ModbusDiscoveryService` which calls each of the `ModbusEndpointDiscoveryService` instances to start the discovery on the available participants.
|
||||||
|
Then a reference to the `ThingHandler` is passed to each of the participants who can use this to do the actual discovery.
|
||||||
|
|
||||||
|
Any things discovered are reported back in this chain and ultimately sent to openHAB core.
|
13
bundles/org.openhab.binding.sbus/NOTICE
Normal file
13
bundles/org.openhab.binding.sbus/NOTICE
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
This content is produced and maintained by the openHAB project.
|
||||||
|
|
||||||
|
* Project home: https://www.openhab.org
|
||||||
|
|
||||||
|
== Declared Project Licenses
|
||||||
|
|
||||||
|
This program and the accompanying materials are made available under the terms
|
||||||
|
of the Eclipse Public License 2.0 which is available at
|
||||||
|
https://www.eclipse.org/legal/epl-2.0/.
|
||||||
|
|
||||||
|
== Source Code
|
||||||
|
|
||||||
|
https://github.com/openhab/openhab-addons
|
1304
bundles/org.openhab.binding.sbus/README.md
Normal file
1304
bundles/org.openhab.binding.sbus/README.md
Normal file
File diff suppressed because it is too large
Load Diff
BIN
bundles/org.openhab.binding.sbus/doc/images/DiscoveryProcess.png
Normal file
BIN
bundles/org.openhab.binding.sbus/doc/images/DiscoveryProcess.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
17
bundles/org.openhab.binding.sbus/pom.xml
Normal file
17
bundles/org.openhab.binding.sbus/pom.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
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.2.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>org.openhab.binding.sbus</artifactId>
|
||||||
|
|
||||||
|
<name>openHAB Add-ons :: Bundles :: S-Bus Binding</name>
|
||||||
|
|
||||||
|
</project>
|
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link ModbusBindingConstants} class defines some constants
|
||||||
|
* public that might be used from other bundles as well.
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
* @author Nagy Attila Gabor - Split the original ModbusBindingConstants in two
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ModbusBindingConstants {
|
||||||
|
|
||||||
|
public static final String BINDING_ID = "sbus";
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.discovery;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener for discovery results
|
||||||
|
*
|
||||||
|
* Each discovered thing should be supplied to the thingDiscovered
|
||||||
|
* method.
|
||||||
|
*
|
||||||
|
* When the discovery process has been finished then the discoveryFinished
|
||||||
|
* method should be called.
|
||||||
|
*
|
||||||
|
* @author Nagy Attila Gabor - initial contribution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public interface ModbusDiscoveryListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discovery participant should call this method when a new
|
||||||
|
* thing has been discovered
|
||||||
|
*/
|
||||||
|
void thingDiscovered(DiscoveryResult result);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method should be called once the discovery has been finished
|
||||||
|
* or aborted by any error.
|
||||||
|
* It is important to call this even when there were no things discovered.
|
||||||
|
*/
|
||||||
|
void discoveryFinished();
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.discovery;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.binding.sbus.handler.ModbusEndpointThingHandler;
|
||||||
|
import org.openhab.core.thing.ThingTypeUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for participants of Modbus discovery
|
||||||
|
* This is an asynchronous process where a participant can discover
|
||||||
|
* multiple things on a Modbus endpoint.
|
||||||
|
*
|
||||||
|
* Results should be submitted using the ModbusDiscvoeryListener
|
||||||
|
* supplied at the begin of the scan.
|
||||||
|
*
|
||||||
|
* @author Nagy Attila Gabor - initial contribution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public interface ModbusDiscoveryParticipant {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the list of thing types that this participant can identify
|
||||||
|
*
|
||||||
|
* @return a set of thing type UIDs for which results can be created
|
||||||
|
*/
|
||||||
|
Set<ThingTypeUID> getSupportedThingTypeUIDs();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start an asynchronous discovery process of a Modbus endpoint
|
||||||
|
*
|
||||||
|
* @param handler the endpoint that should be discovered
|
||||||
|
*/
|
||||||
|
void startDiscovery(ModbusEndpointThingHandler handler, ModbusDiscoveryListener listener);
|
||||||
|
}
|
@ -0,0 +1,188 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.discovery.internal;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.binding.sbus.discovery.ModbusDiscoveryParticipant;
|
||||||
|
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||||
|
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||||
|
import org.openhab.core.config.discovery.DiscoveryService;
|
||||||
|
import org.openhab.core.thing.ThingTypeUID;
|
||||||
|
import org.osgi.service.component.annotations.Component;
|
||||||
|
import org.osgi.service.component.annotations.Reference;
|
||||||
|
import org.osgi.service.component.annotations.ReferenceCardinality;
|
||||||
|
import org.osgi.service.component.annotations.ReferencePolicy;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Discovery service for Modbus bridges.
|
||||||
|
*
|
||||||
|
* This service acts as a rendezvous point between the different Modbus endpoints and any
|
||||||
|
* bundles that implement auto discovery through an endpoint.
|
||||||
|
*
|
||||||
|
* New bridges (UDP or Serial Modbus endpoint) should register with this service. This is
|
||||||
|
* handled automatically by the ModbusEndpointDiscoveryService.
|
||||||
|
* Also any bundles that perform auto discovery should register a ModbusDiscoveryParticipant.
|
||||||
|
* This ModbusDiscoveryParticipants will be called by the service when
|
||||||
|
* a discovery scan is requested.
|
||||||
|
*
|
||||||
|
* @author Nagy Attila Gabor - initial contribution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Component(service = DiscoveryService.class, configurationPid = "discovery.modbus")
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ModbusDiscoveryService extends AbstractDiscoveryService {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(ModbusDiscoveryService.class);
|
||||||
|
|
||||||
|
// Set of services that support Modbus discovery
|
||||||
|
private final Set<ModbusThingHandlerDiscoveryService> services = new CopyOnWriteArraySet<>();
|
||||||
|
|
||||||
|
// Set of the registered participants
|
||||||
|
private final Set<ModbusDiscoveryParticipant> participants = new CopyOnWriteArraySet<>();
|
||||||
|
|
||||||
|
// Set of the supported thing types. This is a union of all the thing types
|
||||||
|
// supported by the registered discovery services.
|
||||||
|
private final Set<ThingTypeUID> supportedThingTypes = new CopyOnWriteArraySet<>();
|
||||||
|
|
||||||
|
private static final int SEARCH_TIME_SECS = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for the discovery service.
|
||||||
|
* Set up default parameters
|
||||||
|
*/
|
||||||
|
public ModbusDiscoveryService() {
|
||||||
|
// No supported thing types by default
|
||||||
|
// Search time is for the visual reference
|
||||||
|
// Background discovery disabled by default
|
||||||
|
super(null, SEARCH_TIME_SECS, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThingHandlerService
|
||||||
|
* Begin a discovery scan over each endpoint
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void startScan() {
|
||||||
|
logger.trace("ModbusDiscoveryService starting scan");
|
||||||
|
|
||||||
|
if (participants.isEmpty()) {
|
||||||
|
// There's no point on continuing if there are no participants at the moment
|
||||||
|
stopScan();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean scanStarted = false;
|
||||||
|
for (ModbusThingHandlerDiscoveryService service : services) {
|
||||||
|
scanStarted |= service.startScan(this);
|
||||||
|
}
|
||||||
|
if (!scanStarted) {
|
||||||
|
stopScan();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to notify us when a handler has finished it's discovery process
|
||||||
|
*/
|
||||||
|
protected void scanFinished() {
|
||||||
|
for (ModbusThingHandlerDiscoveryService service : services) {
|
||||||
|
if (service.scanInProgress()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.trace("All endpoints finished scanning, stopping scan");
|
||||||
|
stopScan();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Real discovery is done by the ModbusDiscoveryParticipants
|
||||||
|
* They are executed in series for each Modbus endpoint by ModbusDiscoveryProcess
|
||||||
|
* instances. They call back this method when a thing has been discovered
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void thingDiscovered(DiscoveryResult discoveryResult) {
|
||||||
|
super.thingDiscovered(discoveryResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of {@code Thing} types which are supported by the {@link DiscoveryService}.
|
||||||
|
*
|
||||||
|
* @return the list of Thing types which are supported by the discovery service
|
||||||
|
* (not null, could be empty)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Set<ThingTypeUID> getSupportedThingTypes() {
|
||||||
|
return this.supportedThingTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This reference is used to register any new Modbus bridge with the discovery service
|
||||||
|
* Running bridges have a ModbusThingHandlerDiscoveryService connected
|
||||||
|
* which will be responsible for the discovery
|
||||||
|
*
|
||||||
|
* @param service discovery service
|
||||||
|
*/
|
||||||
|
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
|
||||||
|
protected void addModbusEndpoint(ModbusThingHandlerDiscoveryService service) {
|
||||||
|
logger.trace("Received new handler: {}", service);
|
||||||
|
services.add(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an already registered thing handler discovery component
|
||||||
|
*
|
||||||
|
* @param service discovery service
|
||||||
|
*/
|
||||||
|
protected void removeModbusEndpoint(ModbusThingHandlerDiscoveryService service) {
|
||||||
|
logger.trace("Removed handler: {}", service);
|
||||||
|
services.remove(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a discovery participant. This participant will be called
|
||||||
|
* with any new Modbus bridges that allow discovery
|
||||||
|
*
|
||||||
|
* @param participant
|
||||||
|
*/
|
||||||
|
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
|
||||||
|
protected void addModbusDiscoveryParticipant(ModbusDiscoveryParticipant participant) {
|
||||||
|
logger.trace("Received new participant: {}", participant);
|
||||||
|
participants.add(participant);
|
||||||
|
supportedThingTypes.addAll(participant.getSupportedThingTypeUIDs());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an already registered discovery participant
|
||||||
|
*
|
||||||
|
* @param participant
|
||||||
|
*/
|
||||||
|
protected void removeModbusDiscoveryParticipant(ModbusDiscoveryParticipant participant) {
|
||||||
|
logger.trace("Removing participant: {}", participant);
|
||||||
|
supportedThingTypes.removeAll(participant.getSupportedThingTypeUIDs());
|
||||||
|
participants.remove(participant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the set of participants
|
||||||
|
*
|
||||||
|
* @return a set of the participants. Note: this is a copy of the original set
|
||||||
|
*/
|
||||||
|
public Set<ModbusDiscoveryParticipant> getDiscoveryParticipants() {
|
||||||
|
return new CopyOnWriteArraySet<>(participants);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.discovery.internal;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.binding.sbus.discovery.ModbusDiscoveryListener;
|
||||||
|
import org.openhab.binding.sbus.discovery.ModbusDiscoveryParticipant;
|
||||||
|
import org.openhab.binding.sbus.handler.ModbusEndpointThingHandler;
|
||||||
|
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||||
|
import org.openhab.core.thing.binding.ThingHandler;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A new instance of this class is created for each Modbus endpoint handler
|
||||||
|
* that supports discovery.
|
||||||
|
* This service gets called each time a discovery is requested, and it is
|
||||||
|
* responsible to execute the discovery on the connected thing handler.
|
||||||
|
* Actual discovery is done by the registered ModbusDiscoveryparticipants
|
||||||
|
*
|
||||||
|
* @author Nagy Attila Gabor - initial contribution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ModbusEndpointDiscoveryService implements ModbusThingHandlerDiscoveryService {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(ModbusEndpointDiscoveryService.class);
|
||||||
|
|
||||||
|
// This is the handler we will do the discovery on
|
||||||
|
private @Nullable ModbusEndpointThingHandler handler;
|
||||||
|
|
||||||
|
// List of the registered participants
|
||||||
|
// this only contains data when there is scan in progress
|
||||||
|
private final List<ModbusDiscoveryParticipant> participants = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
// This is set true when we're waiting for a participant to finish discovery
|
||||||
|
private boolean waitingForParticipant = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setThingHandler(@Nullable ThingHandler handler) {
|
||||||
|
if (handler instanceof ModbusEndpointThingHandler thingHandler) {
|
||||||
|
this.handler = thingHandler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable ThingHandler getThingHandler() {
|
||||||
|
return (ThingHandler) handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean startScan(ModbusDiscoveryService service) {
|
||||||
|
ModbusEndpointThingHandler handler = this.handler;
|
||||||
|
if (handler == null || !handler.isDiscoveryEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
logger.trace("Starting discovery on endpoint {}", handler.getUID().getAsString());
|
||||||
|
|
||||||
|
participants.addAll(service.getDiscoveryParticipants());
|
||||||
|
|
||||||
|
startNextParticipant(handler, service);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean scanInProgress() {
|
||||||
|
return !participants.isEmpty() || waitingForParticipant;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the next participant's discovery process
|
||||||
|
*
|
||||||
|
* @param service reference to the ModbusDiscoveryService that will collect all the
|
||||||
|
* discovered items
|
||||||
|
*/
|
||||||
|
private void startNextParticipant(final ModbusEndpointThingHandler handler, final ModbusDiscoveryService service) {
|
||||||
|
if (participants.isEmpty()) {
|
||||||
|
logger.trace("All participants has finished");
|
||||||
|
service.scanFinished();
|
||||||
|
return; // We're finished, this will exit the process
|
||||||
|
}
|
||||||
|
|
||||||
|
ModbusDiscoveryParticipant participant = participants.remove(0);
|
||||||
|
|
||||||
|
waitingForParticipant = true;
|
||||||
|
|
||||||
|
// Call startDiscovery on the next participant. The ModbusDiscoveryListener
|
||||||
|
// callback will be notified each time a thing is discovered, and also when
|
||||||
|
// the discovery is finished by this participant
|
||||||
|
participant.startDiscovery(handler, new ModbusDiscoveryListener() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Participant has found a thing
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void thingDiscovered(DiscoveryResult result) {
|
||||||
|
service.thingDiscovered(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Participant finished discovery.
|
||||||
|
* We can continue to the next participant
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void discoveryFinished() {
|
||||||
|
waitingForParticipant = false;
|
||||||
|
startNextParticipant(handler, service);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.discovery.internal;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of this interface is responsible for discovery over
|
||||||
|
* a Modbus endpoint. Each time a supporting endpoint handler is created
|
||||||
|
* an instance of this service will be created as well and attached to the
|
||||||
|
* thing handler.
|
||||||
|
*
|
||||||
|
* @author Nagy Attila Gabor - initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public interface ModbusThingHandlerDiscoveryService extends ThingHandlerService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation should start a discovery when this method gets called
|
||||||
|
*
|
||||||
|
* @param service the discovery service that should be called when the discovery is finished
|
||||||
|
* @return returns true if discovery is enabled, false otherwise
|
||||||
|
*/
|
||||||
|
boolean startScan(ModbusDiscoveryService service);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method should return true, if an async scan is in progress
|
||||||
|
*
|
||||||
|
* @return true if a scan is in progress false otherwise
|
||||||
|
*/
|
||||||
|
boolean scanInProgress();
|
||||||
|
}
|
@ -0,0 +1,210 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.handler;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusCommunicationInterface;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusFailureCallback;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusReadCallback;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusReadRequestBlueprint;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusWriteCallback;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusWriteRequestBlueprint;
|
||||||
|
import org.openhab.core.io.transport.sbus.PollTask;
|
||||||
|
import org.openhab.core.thing.Bridge;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a convenience class to interact with the Thing's {@link ModbusCommunicationInterface}.
|
||||||
|
*
|
||||||
|
* @author Fabian Wolter - Initial contribution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public abstract class BaseModbusThingHandler extends BaseThingHandler {
|
||||||
|
private List<PollTask> periodicPollers = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
private List<Future<?>> oneTimePollers = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
|
public BaseModbusThingHandler(Thing thing) {
|
||||||
|
super(thing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called when the Thing is being initialized, but only if the Modbus Bridge is configured correctly.
|
||||||
|
* The code that normally goes into `BaseThingHandler.initialize()` like configuration reading and validation goes
|
||||||
|
* here.
|
||||||
|
*/
|
||||||
|
public abstract void modbusInitialize();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void initialize() {
|
||||||
|
try {
|
||||||
|
// check if the Bridge is configured correctly (fail-fast)
|
||||||
|
getModbus();
|
||||||
|
getSlaveId();
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
modbusInitialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Slave ID, also called as unit id, represented by the thing
|
||||||
|
*
|
||||||
|
* @return slave id represented by this thing handler
|
||||||
|
*/
|
||||||
|
public int getSlaveId() {
|
||||||
|
try {
|
||||||
|
return getBridgeHandler().getSlaveId();
|
||||||
|
} catch (EndpointNotInitializedException e) {
|
||||||
|
throw new IllegalStateException("Bridge not initialized");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if auto discovery is enabled for this endpoint
|
||||||
|
*
|
||||||
|
* @return boolean true if the discovery is enabled
|
||||||
|
*/
|
||||||
|
public boolean isDiscoveryEnabled() {
|
||||||
|
return getBridgeHandler().isDiscoveryEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register regularly polled task. The method returns immediately, and the execution of the poll task will happen in
|
||||||
|
* the background.
|
||||||
|
*
|
||||||
|
* One can register only one regular poll task for triplet of (endpoint, request, callback).
|
||||||
|
*
|
||||||
|
* @param request request to send
|
||||||
|
* @param pollPeriodMillis poll interval, in milliseconds
|
||||||
|
* @param initialDelayMillis initial delay before starting polling, in milliseconds
|
||||||
|
* @param resultCallback callback to call with data
|
||||||
|
* @param failureCallback callback to call in case of failure
|
||||||
|
* @return poll task representing the regular poll
|
||||||
|
* @throws IllegalStateException when this communication has been closed already
|
||||||
|
*/
|
||||||
|
public PollTask registerRegularPoll(ModbusReadRequestBlueprint request, long pollPeriodMillis,
|
||||||
|
long initialDelayMillis, ModbusReadCallback resultCallback,
|
||||||
|
ModbusFailureCallback<ModbusReadRequestBlueprint> failureCallback) {
|
||||||
|
PollTask task = getModbus().registerRegularPoll(request, pollPeriodMillis, initialDelayMillis, resultCallback,
|
||||||
|
failureCallback);
|
||||||
|
periodicPollers.add(task);
|
||||||
|
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister regularly polled task
|
||||||
|
*
|
||||||
|
* If this communication interface is closed already, the method returns immediately with false return value
|
||||||
|
*
|
||||||
|
* @param task poll task to unregister
|
||||||
|
* @return whether poll task was unregistered. Poll task is not unregistered in case of unexpected errors or
|
||||||
|
* in the case where the poll task is not registered in the first place
|
||||||
|
* @throws IllegalStateException when this communication has been closed already
|
||||||
|
*/
|
||||||
|
public boolean unregisterRegularPoll(PollTask task) {
|
||||||
|
periodicPollers.remove(task);
|
||||||
|
return getModbus().unregisterRegularPoll(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit one-time poll task. The method returns immediately, and the execution of the poll task will happen in
|
||||||
|
* background.
|
||||||
|
*
|
||||||
|
* @param request request to send
|
||||||
|
* @param resultCallback callback to call with data
|
||||||
|
* @param failureCallback callback to call in case of failure
|
||||||
|
* @return future representing the polled task
|
||||||
|
* @throws IllegalStateException when this communication has been closed already
|
||||||
|
*/
|
||||||
|
public Future<?> submitOneTimePoll(ModbusReadRequestBlueprint request, ModbusReadCallback resultCallback,
|
||||||
|
ModbusFailureCallback<ModbusReadRequestBlueprint> failureCallback) {
|
||||||
|
Future<?> future = getModbus().submitOneTimePoll(request, resultCallback, failureCallback);
|
||||||
|
oneTimePollers.add(future);
|
||||||
|
oneTimePollers.removeIf(Future::isDone);
|
||||||
|
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit one-time write task. The method returns immediately, and the execution of the task will happen in
|
||||||
|
* background.
|
||||||
|
*
|
||||||
|
* @param request request to send
|
||||||
|
* @param resultCallback callback to call with response
|
||||||
|
* @param failureCallback callback to call in case of failure
|
||||||
|
* @return future representing the task
|
||||||
|
* @throws IllegalStateException when this communication has been closed already
|
||||||
|
*/
|
||||||
|
public Future<?> submitOneTimeWrite(ModbusWriteRequestBlueprint request, ModbusWriteCallback resultCallback,
|
||||||
|
ModbusFailureCallback<ModbusWriteRequestBlueprint> failureCallback) {
|
||||||
|
Future<?> future = getModbus().submitOneTimeWrite(request, resultCallback, failureCallback);
|
||||||
|
oneTimePollers.add(future);
|
||||||
|
oneTimePollers.removeIf(Future::isDone);
|
||||||
|
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModbusCommunicationInterface getModbus() {
|
||||||
|
ModbusCommunicationInterface communicationInterface = getBridgeHandler().getCommunicationInterface();
|
||||||
|
|
||||||
|
if (communicationInterface == null) {
|
||||||
|
throw new IllegalStateException("Bridge not initialized");
|
||||||
|
} else {
|
||||||
|
return communicationInterface;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModbusEndpointThingHandler getBridgeHandler() {
|
||||||
|
try {
|
||||||
|
Bridge bridge = getBridge();
|
||||||
|
if (bridge == null) {
|
||||||
|
throw new IllegalStateException("No Bridge configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
BridgeHandler handler = bridge.getHandler();
|
||||||
|
|
||||||
|
if (handler instanceof ModbusEndpointThingHandler thingHandler) {
|
||||||
|
return thingHandler;
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Not a Modbus Bridge: " + handler);
|
||||||
|
}
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, e.getMessage());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
oneTimePollers.forEach(p -> p.cancel(true));
|
||||||
|
oneTimePollers.clear();
|
||||||
|
|
||||||
|
ModbusCommunicationInterface modbus = getModbus();
|
||||||
|
periodicPollers.forEach(p -> modbus.unregisterRegularPoll(p));
|
||||||
|
periodicPollers.clear();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.handler;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals that {@link ModbusEndpointThingHandler} is not properly initialized yet, and the requested operation cannot
|
||||||
|
* be completed.
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class EndpointNotInitializedException extends Exception {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -6721646244844348903L;
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.handler;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.core.common.registry.Identifiable;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusCommunicationInterface;
|
||||||
|
import org.openhab.core.thing.ThingUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base interface for thing handlers of endpoint things
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public interface ModbusEndpointThingHandler extends Identifiable<ThingUID> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the {@link ModbusCommunicationInterface} represented by the thing
|
||||||
|
*
|
||||||
|
* Note that this can be <code>null</code> in case of incomplete initialization
|
||||||
|
*
|
||||||
|
* @return communication interface represented by this thing handler
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
ModbusCommunicationInterface getCommunicationInterface();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Slave ID, also called as unit id, represented by the thing
|
||||||
|
*
|
||||||
|
* @return slave id represented by this thing handler
|
||||||
|
* @throws EndpointNotInitializedException in case the initialization is not complete
|
||||||
|
*/
|
||||||
|
int getSlaveId() throws EndpointNotInitializedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Slave ID, also called as unit id, represented by the thing
|
||||||
|
*
|
||||||
|
* @return slave id represented by this thing handler
|
||||||
|
* @throws EndpointNotInitializedException in case the initialization is not complete
|
||||||
|
*/
|
||||||
|
int getSubnetId() throws EndpointNotInitializedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if auto discovery is enabled for this endpoint
|
||||||
|
*
|
||||||
|
* @return boolean true if the discovery is enabled
|
||||||
|
*/
|
||||||
|
boolean isDiscoveryEnabled();
|
||||||
|
}
|
@ -0,0 +1,467 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.handler;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.binding.sbus.internal.AtomicStampedValue;
|
||||||
|
import org.openhab.binding.sbus.internal.ModbusBindingConstantsInternal;
|
||||||
|
import org.openhab.binding.sbus.internal.config.ModbusPollerConfiguration;
|
||||||
|
import org.openhab.binding.sbus.internal.handler.ModbusDataThingHandler;
|
||||||
|
import org.openhab.core.io.transport.sbus.AsyncModbusFailure;
|
||||||
|
import org.openhab.core.io.transport.sbus.AsyncModbusReadResult;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusCommunicationInterface;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusConstants;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusFailureCallback;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusReadCallback;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusReadFunctionCode;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusReadRequestBlueprint;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusRegisterArray;
|
||||||
|
import org.openhab.core.io.transport.sbus.PollTask;
|
||||||
|
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.ThingStatusInfo;
|
||||||
|
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||||
|
import org.openhab.core.thing.binding.ThingHandler;
|
||||||
|
import org.openhab.core.types.Command;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link ModbusPollerThingHandler} is responsible for polling Modbus slaves. Errors and data is delegated to
|
||||||
|
* child thing handlers inheriting from {@link ModbusReadCallback} -- in practice: {@link ModbusDataThingHandler}.
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ModbusPollerThingHandler extends BaseBridgeHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ModbusReadCallback} that delegates all tasks forward.
|
||||||
|
*
|
||||||
|
* All instances of {@linkplain ReadCallbackDelegator} are considered equal, if they are connected to the same
|
||||||
|
* bridge. This makes sense, as the callback delegates
|
||||||
|
* to all child things of this bridge.
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private class ReadCallbackDelegator
|
||||||
|
implements ModbusReadCallback, ModbusFailureCallback<ModbusReadRequestBlueprint> {
|
||||||
|
|
||||||
|
private volatile @Nullable AtomicStampedValue<PollResult> lastResult;
|
||||||
|
|
||||||
|
public synchronized void handleResult(PollResult result) {
|
||||||
|
// Ignore all incoming data and errors if configuration is not correct
|
||||||
|
if (hasConfigurationError() || disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (config.getCacheMillis() >= 0) {
|
||||||
|
AtomicStampedValue<PollResult> localLastResult = this.lastResult;
|
||||||
|
if (localLastResult == null) {
|
||||||
|
this.lastResult = new AtomicStampedValue<>(System.currentTimeMillis(), result);
|
||||||
|
} else {
|
||||||
|
localLastResult.update(System.currentTimeMillis(), result);
|
||||||
|
this.lastResult = localLastResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug("Thing {} received response {}", thing.getUID(), result);
|
||||||
|
notifyChildren(result);
|
||||||
|
if (result.failure != null) {
|
||||||
|
Exception error = result.failure.getCause();
|
||||||
|
assert error != null;
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||||
|
String.format("Error with read: %s: %s", error.getClass().getName(), error.getMessage()));
|
||||||
|
} else {
|
||||||
|
resetCommunicationError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void handle(AsyncModbusReadResult result) {
|
||||||
|
// Casting to allow registers.orElse(null) below..
|
||||||
|
Optional<@Nullable ModbusRegisterArray> registers = (Optional<@Nullable ModbusRegisterArray>) result
|
||||||
|
.getRegisters();
|
||||||
|
lastPolledDataCache.set(registers.orElse(null));
|
||||||
|
handleResult(new PollResult(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void handle(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
|
||||||
|
handleResult(new PollResult(failure));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetCommunicationError() {
|
||||||
|
ThingStatusInfo statusInfo = thing.getStatusInfo();
|
||||||
|
if (ThingStatus.OFFLINE.equals(statusInfo.getStatus())
|
||||||
|
&& ThingStatusDetail.COMMUNICATION_ERROR.equals(statusInfo.getStatusDetail())) {
|
||||||
|
updateStatus(ThingStatus.ONLINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update children data if data is fresh enough
|
||||||
|
*
|
||||||
|
* @param oldestStamp oldest data that is still passed to children
|
||||||
|
* @return whether data was updated. Data is not updated when it's too old or there's no data at all.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("null")
|
||||||
|
public boolean updateChildrenWithOldData(long oldestStamp) {
|
||||||
|
return Optional.ofNullable(this.lastResult).map(result -> result.copyIfStampAfter(oldestStamp))
|
||||||
|
.map(result -> {
|
||||||
|
logger.debug("Thing {} reusing cached data: {}", thing.getUID(), result.getValue());
|
||||||
|
notifyChildren(result.getValue());
|
||||||
|
return true;
|
||||||
|
}).orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyChildren(PollResult pollResult) {
|
||||||
|
@Nullable
|
||||||
|
AsyncModbusReadResult result = pollResult.result;
|
||||||
|
@Nullable
|
||||||
|
AsyncModbusFailure<ModbusReadRequestBlueprint> failure = pollResult.failure;
|
||||||
|
childCallbacks.forEach(handler -> {
|
||||||
|
if (result != null) {
|
||||||
|
handler.onReadResult(result);
|
||||||
|
} else if (failure != null) {
|
||||||
|
handler.handleReadError(failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rest data caches
|
||||||
|
*/
|
||||||
|
public void resetCache() {
|
||||||
|
lastResult = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable data object to cache the results of a poll request
|
||||||
|
*/
|
||||||
|
private class PollResult {
|
||||||
|
|
||||||
|
public final @Nullable AsyncModbusReadResult result;
|
||||||
|
public final @Nullable AsyncModbusFailure<ModbusReadRequestBlueprint> failure;
|
||||||
|
|
||||||
|
PollResult(AsyncModbusReadResult result) {
|
||||||
|
this.result = result;
|
||||||
|
this.failure = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
PollResult(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
|
||||||
|
this.result = null;
|
||||||
|
this.failure = failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return failure == null ? String.format("PollResult(result=%s)", result)
|
||||||
|
: String.format("PollResult(failure=%s)", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(ModbusPollerThingHandler.class);
|
||||||
|
|
||||||
|
private static final List<String> SORTED_READ_FUNCTION_CODES = ModbusBindingConstantsInternal.READ_FUNCTION_CODES
|
||||||
|
.keySet().stream().sorted().collect(Collectors.toUnmodifiableList());
|
||||||
|
|
||||||
|
private @NonNullByDefault({}) ModbusPollerConfiguration config;
|
||||||
|
private long cacheMillis;
|
||||||
|
private volatile @Nullable PollTask pollTask;
|
||||||
|
private volatile @Nullable ModbusReadRequestBlueprint request;
|
||||||
|
private volatile boolean disposed;
|
||||||
|
private volatile List<ModbusDataThingHandler> childCallbacks = new CopyOnWriteArrayList<>();
|
||||||
|
private volatile AtomicReference<@Nullable ModbusRegisterArray> lastPolledDataCache = new AtomicReference<>();
|
||||||
|
private @NonNullByDefault({}) ModbusCommunicationInterface comms;
|
||||||
|
|
||||||
|
private ReadCallbackDelegator callbackDelegator = new ReadCallbackDelegator();
|
||||||
|
|
||||||
|
private @Nullable ModbusReadFunctionCode functionCode;
|
||||||
|
|
||||||
|
public ModbusPollerThingHandler(Bridge bridge) {
|
||||||
|
super(bridge);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||||
|
// No channels, no commands
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
|
||||||
|
Bridge bridge = getBridge();
|
||||||
|
if (bridge == null) {
|
||||||
|
logger.debug("Bridge is null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (bridge.getStatus() != ThingStatus.ONLINE) {
|
||||||
|
logger.debug("Bridge is not online");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ThingHandler handler = bridge.getHandler();
|
||||||
|
if (handler == null) {
|
||||||
|
logger.debug("Bridge handler is null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handler instanceof ModbusEndpointThingHandler thingHandler) {
|
||||||
|
return thingHandler;
|
||||||
|
} else {
|
||||||
|
logger.debug("Unexpected bridge handler: {}", handler);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void initialize() {
|
||||||
|
if (this.getThing().getStatus().equals(ThingStatus.ONLINE)) {
|
||||||
|
// If the bridge was online then first change it to offline.
|
||||||
|
// this ensures that children will be notified about the change
|
||||||
|
updateStatus(ThingStatus.OFFLINE);
|
||||||
|
}
|
||||||
|
this.callbackDelegator.resetCache();
|
||||||
|
comms = null;
|
||||||
|
request = null;
|
||||||
|
disposed = false;
|
||||||
|
logger.trace("Initializing {} from status {}", this.getThing().getUID(), this.getThing().getStatus());
|
||||||
|
try {
|
||||||
|
config = getConfigAs(ModbusPollerConfiguration.class);
|
||||||
|
String type = config.getType();
|
||||||
|
if (!ModbusBindingConstantsInternal.READ_FUNCTION_CODES.containsKey(type)) {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||||
|
String.format("No function code found for type='%s'. Was expecting one of: %s", type,
|
||||||
|
String.join(", ", SORTED_READ_FUNCTION_CODES)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
functionCode = ModbusBindingConstantsInternal.READ_FUNCTION_CODES.get(type);
|
||||||
|
switch (functionCode) {
|
||||||
|
case READ_INPUT_REGISTERS:
|
||||||
|
case READ_MULTIPLE_REGISTERS:
|
||||||
|
if (config.getLength() > ModbusConstants.MAX_REGISTERS_READ_COUNT) {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
|
||||||
|
"Maximum of %d registers can be polled at once due to protocol limitations. Length %d is out of bounds.",
|
||||||
|
ModbusConstants.MAX_REGISTERS_READ_COUNT, config.getLength()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case READ_COILS:
|
||||||
|
case READ_INPUT_DISCRETES:
|
||||||
|
if (config.getLength() > ModbusConstants.MAX_BITS_READ_COUNT) {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
|
||||||
|
"Maximum of %d coils/discrete inputs can be polled at once due to protocol limitations. Length %d is out of bounds.",
|
||||||
|
ModbusConstants.MAX_BITS_READ_COUNT, config.getLength()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cacheMillis = this.config.getCacheMillis();
|
||||||
|
registerPollTask();
|
||||||
|
} catch (EndpointNotInitializedException e) {
|
||||||
|
logger.debug("Exception during initialization", e);
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String
|
||||||
|
.format("Exception during initialization: %s (%s)", e.getMessage(), e.getClass().getSimpleName()));
|
||||||
|
} finally {
|
||||||
|
logger.trace("initialize() of thing {} '{}' finished", thing.getUID(), thing.getLabel());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void dispose() {
|
||||||
|
logger.debug("dispose()");
|
||||||
|
// Mark handler as disposed as soon as possible to halt processing of callbacks
|
||||||
|
disposed = true;
|
||||||
|
unregisterPollTask();
|
||||||
|
this.callbackDelegator.resetCache();
|
||||||
|
comms = null;
|
||||||
|
lastPolledDataCache.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister poll task.
|
||||||
|
*
|
||||||
|
* No-op in case no poll task is registered, or if the initialization is incomplete.
|
||||||
|
*/
|
||||||
|
public synchronized void unregisterPollTask() {
|
||||||
|
logger.trace("unregisterPollTask()");
|
||||||
|
if (config == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PollTask localPollTask = this.pollTask;
|
||||||
|
if (localPollTask != null) {
|
||||||
|
logger.debug("Unregistering polling from ModbusManager");
|
||||||
|
comms.unregisterRegularPoll(localPollTask);
|
||||||
|
}
|
||||||
|
this.pollTask = null;
|
||||||
|
request = null;
|
||||||
|
comms = null;
|
||||||
|
updateStatus(ThingStatus.OFFLINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register poll task
|
||||||
|
*
|
||||||
|
* @throws EndpointNotInitializedException in case the bridge initialization is not complete. This should only
|
||||||
|
* happen in transient conditions, for example, when bridge is initializing.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("null")
|
||||||
|
private synchronized void registerPollTask() throws EndpointNotInitializedException {
|
||||||
|
logger.trace("registerPollTask()");
|
||||||
|
if (pollTask != null) {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
|
||||||
|
logger.debug("pollTask should be unregistered before registering a new one!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModbusEndpointThingHandler slaveEndpointThingHandler = getEndpointThingHandler();
|
||||||
|
if (slaveEndpointThingHandler == null) {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, String.format("Bridge '%s' is offline",
|
||||||
|
Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("<null>")));
|
||||||
|
logger.debug("No bridge handler available -- aborting init for {}", this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ModbusCommunicationInterface localComms = slaveEndpointThingHandler.getCommunicationInterface();
|
||||||
|
if (localComms == null) {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, String.format(
|
||||||
|
"Bridge '%s' not completely initialized", Optional.ofNullable(getBridge()).map(b -> b.getLabel())));
|
||||||
|
logger.debug("Bridge not initialized fully (no communication interface) -- aborting init for {}", this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.comms = localComms;
|
||||||
|
ModbusReadFunctionCode localFunctionCode = functionCode;
|
||||||
|
if (localFunctionCode == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModbusReadRequestBlueprint localRequest = new ModbusReadRequestBlueprint(
|
||||||
|
slaveEndpointThingHandler.getSubnetId(), slaveEndpointThingHandler.getSlaveId(), localFunctionCode,
|
||||||
|
config.getStart(), config.getLength(), config.getMaxTries());
|
||||||
|
this.request = localRequest;
|
||||||
|
|
||||||
|
if (config.getRefresh() <= 0L) {
|
||||||
|
logger.debug("Not registering polling with ModbusManager since refresh disabled");
|
||||||
|
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Not polling");
|
||||||
|
} else {
|
||||||
|
logger.debug("Registering polling with ModbusManager");
|
||||||
|
pollTask = localComms.registerRegularPoll(localRequest, config.getRefresh(), 0, callbackDelegator,
|
||||||
|
callbackDelegator);
|
||||||
|
assert pollTask != null;
|
||||||
|
updateStatus(ThingStatus.ONLINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasConfigurationError() {
|
||||||
|
ThingStatusInfo statusInfo = getThing().getStatusInfo();
|
||||||
|
return statusInfo.getStatus() == ThingStatus.OFFLINE
|
||||||
|
&& statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
|
||||||
|
logger.debug("bridgeStatusChanged for {}. Reseting handler", this.getThing().getUID());
|
||||||
|
this.dispose();
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
|
||||||
|
if (childHandler instanceof ModbusDataThingHandler modbusDataThingHandler) {
|
||||||
|
this.childCallbacks.add(modbusDataThingHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unlikely-arg-type")
|
||||||
|
@Override
|
||||||
|
public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
|
||||||
|
if (childHandler instanceof ModbusDataThingHandler) {
|
||||||
|
this.childCallbacks.remove(childHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@link ModbusReadRequestBlueprint} represented by this thing.
|
||||||
|
*
|
||||||
|
* Note that request might be <code>null</code> in case initialization is not complete.
|
||||||
|
*
|
||||||
|
* @return modbus request represented by this poller
|
||||||
|
*/
|
||||||
|
public @Nullable ModbusReadRequestBlueprint getRequest() {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get communication interface associated with this poller
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public ModbusCommunicationInterface getCommunicationInterface() {
|
||||||
|
return comms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the data
|
||||||
|
*
|
||||||
|
* If data or error was just recently received (i.e. cache is fresh), return the cached response.
|
||||||
|
*/
|
||||||
|
public void refresh() {
|
||||||
|
ModbusReadRequestBlueprint localRequest = this.request;
|
||||||
|
if (localRequest == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ModbusRegisterArray possiblyMutatedCache = lastPolledDataCache.get();
|
||||||
|
AtomicStampedValue<PollResult> lastPollResult = callbackDelegator.lastResult;
|
||||||
|
if (lastPollResult != null && possiblyMutatedCache != null) {
|
||||||
|
AsyncModbusReadResult lastSuccessfulPollResult = lastPollResult.getValue().result;
|
||||||
|
if (lastSuccessfulPollResult != null) {
|
||||||
|
ModbusRegisterArray lastRegisters = ((Optional<@Nullable ModbusRegisterArray>) lastSuccessfulPollResult
|
||||||
|
.getRegisters()).orElse(null);
|
||||||
|
if (lastRegisters != null && !possiblyMutatedCache.equals(lastRegisters)) {
|
||||||
|
// Register has been mutated in between by a data thing that writes "individual bits"
|
||||||
|
// Invalidate cache for a fresh poll
|
||||||
|
callbackDelegator.resetCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long oldDataThreshold = System.currentTimeMillis() - cacheMillis;
|
||||||
|
boolean cacheWasRecentEnoughForUpdate = cacheMillis > 0
|
||||||
|
&& this.callbackDelegator.updateChildrenWithOldData(oldDataThreshold);
|
||||||
|
if (cacheWasRecentEnoughForUpdate) {
|
||||||
|
logger.debug(
|
||||||
|
"Poller {} received refresh() and cache was recent enough (age at most {} ms). Reusing old response",
|
||||||
|
getThing().getUID(), cacheMillis);
|
||||||
|
} else {
|
||||||
|
// cache expired, poll new data
|
||||||
|
logger.debug("Poller {} received refresh() but the cache is not applicable. Polling new data",
|
||||||
|
getThing().getUID());
|
||||||
|
ModbusCommunicationInterface localComms = comms;
|
||||||
|
if (localComms != null) {
|
||||||
|
localComms.submitOneTimePoll(localRequest, callbackDelegator, callbackDelegator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AtomicReference<@Nullable ModbusRegisterArray> getLastPolledDataCache() {
|
||||||
|
return lastPolledDataCache;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp-value pair that can be updated atomically
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*
|
||||||
|
* @param <V> type of the value
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class AtomicStampedValue<V> implements Cloneable {
|
||||||
|
|
||||||
|
private long stamp;
|
||||||
|
private V value;
|
||||||
|
|
||||||
|
private AtomicStampedValue(AtomicStampedValue<V> copy) {
|
||||||
|
this(copy.stamp, copy.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct new stamped key-value pair
|
||||||
|
*
|
||||||
|
* @param stamp stamp for the data
|
||||||
|
* @param value value for the data
|
||||||
|
*
|
||||||
|
* @throws NullPointerException when key or value is null
|
||||||
|
*/
|
||||||
|
public AtomicStampedValue(long stamp, V value) {
|
||||||
|
Objects.requireNonNull(value, "value should not be null!");
|
||||||
|
this.stamp = stamp;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update data in this instance atomically
|
||||||
|
*
|
||||||
|
* @param stamp stamp for the data
|
||||||
|
* @param value value for the data
|
||||||
|
*
|
||||||
|
* @throws NullPointerException when value is null
|
||||||
|
*/
|
||||||
|
public synchronized void update(long stamp, V value) {
|
||||||
|
Objects.requireNonNull(value, "value should not be null!");
|
||||||
|
this.stamp = stamp;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy data atomically and return the new (shallow) copy
|
||||||
|
*
|
||||||
|
* @return new copy of the data
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public synchronized AtomicStampedValue<V> copy() {
|
||||||
|
return (AtomicStampedValue<V>) this.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronized implementation of clone with exception swallowing
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected synchronized Object clone() {
|
||||||
|
try {
|
||||||
|
return super.clone();
|
||||||
|
} catch (CloneNotSupportedException e) {
|
||||||
|
// We should never end up here since this class implements Cloneable
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy data atomically if data is after certain stamp ("fresh" enough)
|
||||||
|
*
|
||||||
|
* @param stampMin
|
||||||
|
* @return null, if the stamp of this instance is before stampMin. Otherwise return the data copied
|
||||||
|
*/
|
||||||
|
public synchronized @Nullable AtomicStampedValue<V> copyIfStampAfter(long stampMin) {
|
||||||
|
if (stampMin <= this.stamp) {
|
||||||
|
return new AtomicStampedValue<>(this);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stamp
|
||||||
|
*/
|
||||||
|
public long getStamp() {
|
||||||
|
return stamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value
|
||||||
|
*/
|
||||||
|
public V getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two AtomicStampedKeyValue objects based on stamps
|
||||||
|
*
|
||||||
|
* Nulls are ordered first
|
||||||
|
*
|
||||||
|
* @param x first instance
|
||||||
|
* @param y second instance
|
||||||
|
* @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater
|
||||||
|
* than the second.
|
||||||
|
*/
|
||||||
|
public static int compare(@SuppressWarnings("rawtypes") @Nullable AtomicStampedValue x,
|
||||||
|
@SuppressWarnings("rawtypes") @Nullable AtomicStampedValue y) {
|
||||||
|
if (x == null) {
|
||||||
|
return -1;
|
||||||
|
} else if (y == null) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return Long.compare(x.stamp, y.stamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.osgi.framework.BundleContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link CascadedValueTransformationImpl} implements {@link SingleValueTransformation for a cascaded set of
|
||||||
|
* transformations}
|
||||||
|
*
|
||||||
|
* @author Jan N. Klug - Initial contribution
|
||||||
|
* @author Ciprian Pascu - Copied from HTTP binding to provide consistent user experience
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class CascadedValueTransformationImpl implements ValueTransformation {
|
||||||
|
private final List<SingleValueTransformation> transformations;
|
||||||
|
|
||||||
|
public CascadedValueTransformationImpl(@Nullable String transformationString) {
|
||||||
|
String transformationNonNull = transformationString == null ? "" : transformationString;
|
||||||
|
List<SingleValueTransformation> localTransformations = Arrays.stream(transformationNonNull.split("∩"))
|
||||||
|
.filter(s -> !s.isEmpty()).map(transformation -> new SingleValueTransformation(transformation))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (localTransformations.isEmpty()) {
|
||||||
|
localTransformations = List.of(new SingleValueTransformation(transformationString));
|
||||||
|
}
|
||||||
|
transformations = localTransformations;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String transform(BundleContext context, String value) {
|
||||||
|
String input = value;
|
||||||
|
// process all transformations
|
||||||
|
for (final ValueTransformation transformation : transformations) {
|
||||||
|
input = transformation.transform(context, input);
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isIdentityTransform() {
|
||||||
|
return transformations.stream().allMatch(SingleValueTransformation::isIdentityTransform);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "CascadedValueTransformationImpl("
|
||||||
|
+ transformations.stream().map(SingleValueTransformation::toString).collect(Collectors.joining(" ∩ "))
|
||||||
|
+ ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SingleValueTransformation> getTransformations() {
|
||||||
|
return transformations;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal;
|
||||||
|
|
||||||
|
import static org.openhab.binding.sbus.ModbusBindingConstants.BINDING_ID;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusReadFunctionCode;
|
||||||
|
import org.openhab.core.thing.ThingTypeUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link ModbusBindingConstantsInternal} class defines common constants, which are
|
||||||
|
* used across the whole binding.
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ModbusBindingConstantsInternal {
|
||||||
|
|
||||||
|
// List of all Thing Type UIDs
|
||||||
|
public static final ThingTypeUID THING_TYPE_MODBUS_UDP = new ThingTypeUID(BINDING_ID, "udp");
|
||||||
|
public static final ThingTypeUID THING_TYPE_MODBUS_SERIAL = new ThingTypeUID(BINDING_ID, "serial");
|
||||||
|
public static final ThingTypeUID THING_TYPE_MODBUS_POLLER = new ThingTypeUID(BINDING_ID, "poller");
|
||||||
|
public static final ThingTypeUID THING_TYPE_MODBUS_DATA = new ThingTypeUID(BINDING_ID, "data");
|
||||||
|
|
||||||
|
// List of all Channel ids
|
||||||
|
public static final String CHANNEL_SWITCH = "switch";
|
||||||
|
public static final String CHANNEL_CONTACT = "contact";
|
||||||
|
public static final String CHANNEL_DATETIME = "datetime";
|
||||||
|
public static final String CHANNEL_DIMMER = "dimmer";
|
||||||
|
public static final String CHANNEL_NUMBER = "number";
|
||||||
|
public static final String CHANNEL_STRING = "string";
|
||||||
|
public static final String CHANNEL_ROLLERSHUTTER = "rollershutter";
|
||||||
|
public static final String CHANNEL_LAST_READ_SUCCESS = "lastReadSuccess";
|
||||||
|
public static final String CHANNEL_LAST_READ_ERROR = "lastReadError";
|
||||||
|
public static final String CHANNEL_LAST_WRITE_SUCCESS = "lastWriteSuccess";
|
||||||
|
public static final String CHANNEL_LAST_WRITE_ERROR = "lastWriteError";
|
||||||
|
|
||||||
|
public static final String[] DATA_CHANNELS = { CHANNEL_SWITCH, CHANNEL_CONTACT, CHANNEL_DATETIME, CHANNEL_DIMMER,
|
||||||
|
CHANNEL_NUMBER, CHANNEL_STRING, CHANNEL_ROLLERSHUTTER };
|
||||||
|
|
||||||
|
public static final String[] DATA_CHANNELS_TO_COPY_FROM_READ_TO_READWRITE = { CHANNEL_SWITCH, CHANNEL_CONTACT,
|
||||||
|
CHANNEL_DATETIME, CHANNEL_DIMMER, CHANNEL_NUMBER, CHANNEL_STRING, CHANNEL_ROLLERSHUTTER,
|
||||||
|
CHANNEL_LAST_READ_SUCCESS, CHANNEL_LAST_READ_ERROR };
|
||||||
|
|
||||||
|
public static final String[] DATA_CHANNELS_TO_DELEGATE_COMMAND_FROM_READWRITE_TO_WRITE = { CHANNEL_SWITCH,
|
||||||
|
CHANNEL_CONTACT, CHANNEL_DATETIME, CHANNEL_DIMMER, CHANNEL_NUMBER, CHANNEL_STRING, CHANNEL_ROLLERSHUTTER };
|
||||||
|
|
||||||
|
public static final String WRITE_TYPE_COIL = "coil";
|
||||||
|
public static final String WRITE_TYPE_HOLDING = "holding";
|
||||||
|
|
||||||
|
public static final String READ_TYPE_COIL = "coil";
|
||||||
|
public static final String READ_TYPE_HOLDING_REGISTER = "holding";
|
||||||
|
public static final String READ_TYPE_DISCRETE_INPUT = "discrete";
|
||||||
|
public static final String READ_TYPE_INPUT_REGISTER = "input";
|
||||||
|
|
||||||
|
public static final Map<String, ModbusReadFunctionCode> READ_FUNCTION_CODES = new HashMap<>();
|
||||||
|
static {
|
||||||
|
READ_FUNCTION_CODES.put(READ_TYPE_COIL, ModbusReadFunctionCode.READ_COILS);
|
||||||
|
READ_FUNCTION_CODES.put(READ_TYPE_DISCRETE_INPUT, ModbusReadFunctionCode.READ_INPUT_DISCRETES);
|
||||||
|
READ_FUNCTION_CODES.put(READ_TYPE_INPUT_REGISTER, ModbusReadFunctionCode.READ_INPUT_REGISTERS);
|
||||||
|
READ_FUNCTION_CODES.put(READ_TYPE_HOLDING_REGISTER, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for binding configuration exceptions
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ModbusConfigurationException extends Exception {
|
||||||
|
|
||||||
|
public ModbusConfigurationException(String errmsg) {
|
||||||
|
super(errmsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -466597103876477780L;
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal;
|
||||||
|
|
||||||
|
import static org.openhab.binding.sbus.internal.ModbusBindingConstantsInternal.THING_TYPE_MODBUS_DATA;
|
||||||
|
import static org.openhab.binding.sbus.internal.ModbusBindingConstantsInternal.THING_TYPE_MODBUS_POLLER;
|
||||||
|
import static org.openhab.binding.sbus.internal.ModbusBindingConstantsInternal.THING_TYPE_MODBUS_SERIAL;
|
||||||
|
import static org.openhab.binding.sbus.internal.ModbusBindingConstantsInternal.THING_TYPE_MODBUS_UDP;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.binding.sbus.handler.ModbusPollerThingHandler;
|
||||||
|
import org.openhab.binding.sbus.internal.handler.ModbusDataThingHandler;
|
||||||
|
import org.openhab.binding.sbus.internal.handler.ModbusSerialThingHandler;
|
||||||
|
import org.openhab.binding.sbus.internal.handler.ModbusUdpThingHandler;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusManager;
|
||||||
|
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.Component;
|
||||||
|
import org.osgi.service.component.annotations.Reference;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link ModbusHandlerFactory} is responsible for creating things and thing
|
||||||
|
* handlers.
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*/
|
||||||
|
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.modbus")
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ModbusHandlerFactory extends BaseThingHandlerFactory {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(ModbusHandlerFactory.class);
|
||||||
|
|
||||||
|
private @NonNullByDefault({}) ModbusManager manager;
|
||||||
|
|
||||||
|
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = new HashSet<>();
|
||||||
|
static {
|
||||||
|
SUPPORTED_THING_TYPES_UIDS.add(THING_TYPE_MODBUS_UDP);
|
||||||
|
SUPPORTED_THING_TYPES_UIDS.add(THING_TYPE_MODBUS_SERIAL);
|
||||||
|
SUPPORTED_THING_TYPES_UIDS.add(THING_TYPE_MODBUS_POLLER);
|
||||||
|
SUPPORTED_THING_TYPES_UIDS.add(THING_TYPE_MODBUS_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 (thingTypeUID.equals(THING_TYPE_MODBUS_UDP)) {
|
||||||
|
logger.debug("createHandler Modbus udp");
|
||||||
|
return new ModbusUdpThingHandler((Bridge) thing, manager);
|
||||||
|
} else if (thingTypeUID.equals(THING_TYPE_MODBUS_SERIAL)) {
|
||||||
|
logger.debug("createHandler Modbus serial");
|
||||||
|
return new ModbusSerialThingHandler((Bridge) thing, manager);
|
||||||
|
} else if (thingTypeUID.equals(THING_TYPE_MODBUS_POLLER)) {
|
||||||
|
logger.debug("createHandler Modbus poller");
|
||||||
|
return new ModbusPollerThingHandler((Bridge) thing);
|
||||||
|
} else if (thingTypeUID.equals(THING_TYPE_MODBUS_DATA)) {
|
||||||
|
logger.debug("createHandler data");
|
||||||
|
return new ModbusDataThingHandler(thing);
|
||||||
|
}
|
||||||
|
logger.error("createHandler for unknown thing type uid {}. Thing label was: {}", thing.getThingTypeUID(),
|
||||||
|
thing.getLabel());
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Reference
|
||||||
|
public void setModbusManager(ModbusManager manager) {
|
||||||
|
logger.debug("Setting manager: {}", manager);
|
||||||
|
this.manager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unsetModbusManager(ModbusManager manager) {
|
||||||
|
this.manager = null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.core.library.types.DecimalType;
|
||||||
|
import org.openhab.core.library.types.OnOffType;
|
||||||
|
import org.openhab.core.library.types.OpenClosedType;
|
||||||
|
import org.openhab.core.transform.TransformationException;
|
||||||
|
import org.openhab.core.transform.TransformationHelper;
|
||||||
|
import org.openhab.core.transform.TransformationService;
|
||||||
|
import org.openhab.core.types.Command;
|
||||||
|
import org.openhab.core.types.TypeParser;
|
||||||
|
import org.osgi.framework.BundleContext;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class describing transformation of a command or state.
|
||||||
|
*
|
||||||
|
* Inspired from other openHAB binding "Transformation" classes.
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class SingleValueTransformation implements ValueTransformation {
|
||||||
|
|
||||||
|
public static final String TRANSFORM_DEFAULT = "default";
|
||||||
|
public static final ValueTransformation IDENTITY_TRANSFORMATION = new SingleValueTransformation(TRANSFORM_DEFAULT,
|
||||||
|
null, null);
|
||||||
|
|
||||||
|
/** RegEx to extract and parse a function String <code>'(.*?)\((.*)\)'</code> */
|
||||||
|
private static final Pattern EXTRACT_FUNCTION_PATTERN_OLD = Pattern.compile("(?<service>.*?)\\((?<arg>.*)\\)");
|
||||||
|
private static final Pattern EXTRACT_FUNCTION_PATTERN_NEW = Pattern.compile("(?<service>.*?):(?<arg>.*)");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ordered list of types that are tried out first when trying to parse transformed command
|
||||||
|
*/
|
||||||
|
private static final List<Class<? extends Command>> DEFAULT_TYPES = new ArrayList<>();
|
||||||
|
static {
|
||||||
|
DEFAULT_TYPES.add(DecimalType.class);
|
||||||
|
DEFAULT_TYPES.add(OpenClosedType.class);
|
||||||
|
DEFAULT_TYPES.add(OnOffType.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(SingleValueTransformation.class);
|
||||||
|
|
||||||
|
private final @Nullable String transformation;
|
||||||
|
final @Nullable String transformationServiceName;
|
||||||
|
final @Nullable String transformationServiceParam;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param transformation either FUN(VAL) (standard transformation syntax), default (identity transformation
|
||||||
|
* (output equals input)) or some other value (output is a constant). Futhermore, empty string is
|
||||||
|
* considered the same way as "default".
|
||||||
|
*/
|
||||||
|
public SingleValueTransformation(@Nullable String transformation) {
|
||||||
|
this.transformation = transformation;
|
||||||
|
//
|
||||||
|
// Parse transformation configuration here on construction, but delay the
|
||||||
|
// construction of TransformationService to call-time
|
||||||
|
if (transformation == null || transformation.isEmpty() || transformation.equalsIgnoreCase(TRANSFORM_DEFAULT)) {
|
||||||
|
// no-op (identity) transformation
|
||||||
|
transformationServiceName = null;
|
||||||
|
transformationServiceParam = null;
|
||||||
|
} else {
|
||||||
|
int colonIndex = transformation.indexOf(":");
|
||||||
|
int parenthesisOpenIndex = transformation.indexOf("(");
|
||||||
|
|
||||||
|
final Matcher matcher;
|
||||||
|
if (parenthesisOpenIndex != -1 && (colonIndex == -1 || parenthesisOpenIndex < colonIndex)) {
|
||||||
|
matcher = EXTRACT_FUNCTION_PATTERN_OLD.matcher(transformation);
|
||||||
|
} else {
|
||||||
|
matcher = EXTRACT_FUNCTION_PATTERN_NEW.matcher(transformation);
|
||||||
|
}
|
||||||
|
if (matcher.matches()) {
|
||||||
|
matcher.reset();
|
||||||
|
matcher.find();
|
||||||
|
transformationServiceName = matcher.group("service");
|
||||||
|
transformationServiceParam = matcher.group("arg");
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
"Given transformation configuration '{}' did not match the FUN(VAL) pattern. Transformation output will be constant '{}'",
|
||||||
|
transformation, transformation);
|
||||||
|
transformationServiceName = null;
|
||||||
|
transformationServiceParam = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For testing, thus package visibility by design
|
||||||
|
*
|
||||||
|
* @param transformation
|
||||||
|
* @param transformationServiceName
|
||||||
|
* @param transformationServiceParam
|
||||||
|
*/
|
||||||
|
SingleValueTransformation(String transformation, @Nullable String transformationServiceName,
|
||||||
|
@Nullable String transformationServiceParam) {
|
||||||
|
this.transformation = transformation;
|
||||||
|
this.transformationServiceName = transformationServiceName;
|
||||||
|
this.transformationServiceParam = transformationServiceParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String transform(BundleContext context, String value) {
|
||||||
|
String transformedResponse;
|
||||||
|
String transformationServiceName = this.transformationServiceName;
|
||||||
|
String transformationServiceParam = this.transformationServiceParam;
|
||||||
|
|
||||||
|
if (transformationServiceName != null) {
|
||||||
|
try {
|
||||||
|
if (transformationServiceParam == null) {
|
||||||
|
throw new TransformationException(
|
||||||
|
"transformation service parameter is missing! Invalid transform?");
|
||||||
|
}
|
||||||
|
@Nullable
|
||||||
|
TransformationService transformationService = TransformationHelper.getTransformationService(context,
|
||||||
|
transformationServiceName);
|
||||||
|
if (transformationService != null) {
|
||||||
|
transformedResponse = transformationService.transform(transformationServiceParam, value);
|
||||||
|
} else {
|
||||||
|
transformedResponse = value;
|
||||||
|
logger.warn("couldn't transform response because transformationService of type '{}' is unavailable",
|
||||||
|
transformationServiceName);
|
||||||
|
}
|
||||||
|
} catch (TransformationException te) {
|
||||||
|
logger.error("transformation throws exception [transformation={}, response={}]", transformation, value,
|
||||||
|
te);
|
||||||
|
|
||||||
|
// in case of an error we return the response without any
|
||||||
|
// transformation
|
||||||
|
transformedResponse = value;
|
||||||
|
}
|
||||||
|
} else if (isIdentityTransform()) {
|
||||||
|
// identity transformation
|
||||||
|
transformedResponse = value;
|
||||||
|
} else {
|
||||||
|
// pass value as is
|
||||||
|
transformedResponse = this.transformation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformedResponse == null ? "" : transformedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isIdentityTransform() {
|
||||||
|
return TRANSFORM_DEFAULT.equalsIgnoreCase(this.transformation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<Command> tryConvertToCommand(String transformed) {
|
||||||
|
return Optional.ofNullable(TypeParser.parseCommand(DEFAULT_TYPES, transformed));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "SingleValueTransformation [transformation=" + transformation + ", transformationServiceName="
|
||||||
|
+ transformationServiceName + ", transformationServiceParam=" + transformationServiceParam + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.core.types.State;
|
||||||
|
import org.openhab.core.types.TypeParser;
|
||||||
|
import org.osgi.framework.BundleContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for Transformation
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public interface ValueTransformation {
|
||||||
|
|
||||||
|
String transform(BundleContext context, String value);
|
||||||
|
|
||||||
|
boolean isIdentityTransform();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform state to another state using this transformation
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* @param types types to used to parse the transformation result
|
||||||
|
* @param state
|
||||||
|
* @return Transformed command, or null if no transformation was possible
|
||||||
|
*/
|
||||||
|
default @Nullable State transformState(BundleContext context, List<Class<? extends State>> types, State state) {
|
||||||
|
// Note that even identity transformations go through the State -> String -> State steps. This does add some
|
||||||
|
// overhead but takes care of DecimalType -> PercentType conversions, for example.
|
||||||
|
final String stateAsString = state.toString();
|
||||||
|
final String transformed = transform(context, stateAsString);
|
||||||
|
return TypeParser.parseState(types, transformed);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal.config;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for data thing
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ModbusDataConfiguration {
|
||||||
|
|
||||||
|
private @Nullable String readStart;
|
||||||
|
private @Nullable String readTransform;
|
||||||
|
private @Nullable String readValueType;
|
||||||
|
private @Nullable String writeStart;
|
||||||
|
private @Nullable String writeType;
|
||||||
|
private @Nullable String writeTransform;
|
||||||
|
private @Nullable String writeValueType;
|
||||||
|
private boolean writeMultipleEvenWithSingleRegisterOrCoil;
|
||||||
|
private int writeMaxTries = 3; // backwards compatibility and tests
|
||||||
|
private long updateUnchangedValuesEveryMillis = 1000L;
|
||||||
|
|
||||||
|
public @Nullable String getReadStart() {
|
||||||
|
return readStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReadStart(String readStart) {
|
||||||
|
this.readStart = readStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable String getReadTransform() {
|
||||||
|
return readTransform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReadTransform(String readTransform) {
|
||||||
|
this.readTransform = readTransform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable String getReadValueType() {
|
||||||
|
return readValueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReadValueType(String readValueType) {
|
||||||
|
this.readValueType = readValueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable String getWriteStart() {
|
||||||
|
return writeStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWriteStart(String writeStart) {
|
||||||
|
this.writeStart = writeStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable String getWriteType() {
|
||||||
|
return writeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWriteType(String writeType) {
|
||||||
|
this.writeType = writeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable String getWriteTransform() {
|
||||||
|
return writeTransform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWriteTransform(String writeTransform) {
|
||||||
|
this.writeTransform = writeTransform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable String getWriteValueType() {
|
||||||
|
return writeValueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWriteValueType(String writeValueType) {
|
||||||
|
this.writeValueType = writeValueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isWriteMultipleEvenWithSingleRegisterOrCoil() {
|
||||||
|
return writeMultipleEvenWithSingleRegisterOrCoil;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWriteMultipleEvenWithSingleRegisterOrCoil(boolean writeMultipleEvenWithSingleRegisterOrCoil) {
|
||||||
|
this.writeMultipleEvenWithSingleRegisterOrCoil = writeMultipleEvenWithSingleRegisterOrCoil;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getWriteMaxTries() {
|
||||||
|
return writeMaxTries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWriteMaxTries(int writeMaxTries) {
|
||||||
|
this.writeMaxTries = writeMaxTries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getUpdateUnchangedValuesEveryMillis() {
|
||||||
|
return updateUnchangedValuesEveryMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdateUnchangedValuesEveryMillis(long updateUnchangedValuesEveryMillis) {
|
||||||
|
this.updateUnchangedValuesEveryMillis = updateUnchangedValuesEveryMillis;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal.config;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for poller thing
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ModbusPollerConfiguration {
|
||||||
|
private long refresh;
|
||||||
|
private int start;
|
||||||
|
private int length;
|
||||||
|
private @Nullable String type;
|
||||||
|
private int maxTries = 3;// backwards compatibility and tests
|
||||||
|
private long cacheMillis = 50L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets refresh period in milliseconds
|
||||||
|
*/
|
||||||
|
public long getRefresh() {
|
||||||
|
return refresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets refresh period in milliseconds
|
||||||
|
*/
|
||||||
|
|
||||||
|
public void setRefresh(long refresh) {
|
||||||
|
this.refresh = refresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get address of the first register, coil, or discrete input to poll. Input as zero-based index number.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public int getStart() {
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets address of the first register, coil, or discrete input to poll. Input as zero-based index number.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public void setStart(int start) {
|
||||||
|
this.start = start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets number of registers, coils or discrete inputs to read.
|
||||||
|
*/
|
||||||
|
public int getLength() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets number of registers, coils or discrete inputs to read.
|
||||||
|
*/
|
||||||
|
public void setLength(int length) {
|
||||||
|
this.length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets type of modbus items to poll
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public @Nullable String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets type of modbus items to poll
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public void setType(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxTries() {
|
||||||
|
return maxTries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxTries(int maxTries) {
|
||||||
|
this.maxTries = maxTries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets time to cache data.
|
||||||
|
*
|
||||||
|
* This is used for reusing cached data with explicit refresh calls.
|
||||||
|
*/
|
||||||
|
public long getCacheMillis() {
|
||||||
|
return cacheMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets time to cache data, in milliseconds
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public void setCacheMillis(long cacheMillis) {
|
||||||
|
this.cacheMillis = cacheMillis;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal.config;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for serial thing
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ModbusSerialConfiguration {
|
||||||
|
private @Nullable String port;
|
||||||
|
private int id = 1;
|
||||||
|
private int subnetId = 1;
|
||||||
|
private int baud;
|
||||||
|
private @Nullable String stopBits;
|
||||||
|
private @Nullable String parity;
|
||||||
|
private int dataBits;
|
||||||
|
private String encoding = "rtu";
|
||||||
|
private boolean echo;
|
||||||
|
private int receiveTimeoutMillis = 1500;
|
||||||
|
private String flowControlIn = "none";
|
||||||
|
private String flowControlOut = "none";
|
||||||
|
private int timeBetweenTransactionsMillis = 35;
|
||||||
|
private int connectMaxTries = 1;
|
||||||
|
private int afterConnectionDelayMillis;
|
||||||
|
private int connectTimeoutMillis = 10_000;
|
||||||
|
private boolean enableDiscovery;
|
||||||
|
|
||||||
|
public @Nullable String getPort() {
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPort(String port) {
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSubnetId() {
|
||||||
|
return subnetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubnetId(int subnetId) {
|
||||||
|
this.subnetId = subnetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBaud() {
|
||||||
|
return baud;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBaud(int baud) {
|
||||||
|
this.baud = baud;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable String getStopBits() {
|
||||||
|
return stopBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStopBits(String stopBits) {
|
||||||
|
this.stopBits = stopBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable String getParity() {
|
||||||
|
return parity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setParity(String parity) {
|
||||||
|
this.parity = parity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDataBits() {
|
||||||
|
return dataBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDataBits(int dataBits) {
|
||||||
|
this.dataBits = dataBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable String getEncoding() {
|
||||||
|
return encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEncoding(String encoding) {
|
||||||
|
this.encoding = encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEcho() {
|
||||||
|
return echo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEcho(boolean echo) {
|
||||||
|
this.echo = echo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getReceiveTimeoutMillis() {
|
||||||
|
return receiveTimeoutMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReceiveTimeoutMillis(int receiveTimeoutMillis) {
|
||||||
|
this.receiveTimeoutMillis = receiveTimeoutMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable String getFlowControlIn() {
|
||||||
|
return flowControlIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFlowControlIn(String flowControlIn) {
|
||||||
|
this.flowControlIn = flowControlIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable String getFlowControlOut() {
|
||||||
|
return flowControlOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFlowControlOut(String flowControlOut) {
|
||||||
|
this.flowControlOut = flowControlOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTimeBetweenTransactionsMillis() {
|
||||||
|
return timeBetweenTransactionsMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimeBetweenTransactionsMillis(int timeBetweenTransactionsMillis) {
|
||||||
|
this.timeBetweenTransactionsMillis = timeBetweenTransactionsMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getConnectMaxTries() {
|
||||||
|
return connectMaxTries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConnectMaxTries(int connectMaxTries) {
|
||||||
|
this.connectMaxTries = connectMaxTries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAfterConnectionDelayMillis() {
|
||||||
|
return afterConnectionDelayMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAfterConnectionDelayMillis(int afterConnectionDelayMillis) {
|
||||||
|
this.afterConnectionDelayMillis = afterConnectionDelayMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getConnectTimeoutMillis() {
|
||||||
|
return connectTimeoutMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConnectTimeoutMillis(int connectTimeoutMillis) {
|
||||||
|
this.connectTimeoutMillis = connectTimeoutMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDiscoveryEnabled() {
|
||||||
|
return enableDiscovery;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDiscoveryEnabled(boolean enableDiscovery) {
|
||||||
|
this.enableDiscovery = enableDiscovery;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,130 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal.config;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for udp thing
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ModbusUdpConfiguration {
|
||||||
|
private @Nullable String host;
|
||||||
|
private int port;
|
||||||
|
private int id = 1;
|
||||||
|
private int subnetId = 1;
|
||||||
|
private int timeBetweenTransactionsMillis = 60;
|
||||||
|
private int timeBetweenReconnectMillis;
|
||||||
|
private int connectMaxTries = 1;
|
||||||
|
private int reconnectAfterMillis;
|
||||||
|
private int afterConnectionDelayMillis;
|
||||||
|
private int connectTimeoutMillis = 10_000;
|
||||||
|
private boolean enableDiscovery;
|
||||||
|
private boolean rtuEncoded;
|
||||||
|
|
||||||
|
public boolean getRtuEncoded() {
|
||||||
|
return rtuEncoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable String getHost() {
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHost(String host) {
|
||||||
|
this.host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPort() {
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPort(int port) {
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSubnetId() {
|
||||||
|
return subnetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubnetId(int subnetId) {
|
||||||
|
this.subnetId = subnetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTimeBetweenTransactionsMillis() {
|
||||||
|
return timeBetweenTransactionsMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimeBetweenTransactionsMillis(int timeBetweenTransactionsMillis) {
|
||||||
|
this.timeBetweenTransactionsMillis = timeBetweenTransactionsMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTimeBetweenReconnectMillis() {
|
||||||
|
return timeBetweenReconnectMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimeBetweenReconnectMillis(int timeBetweenReconnectMillis) {
|
||||||
|
this.timeBetweenReconnectMillis = timeBetweenReconnectMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getConnectMaxTries() {
|
||||||
|
return connectMaxTries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConnectMaxTries(int connectMaxTries) {
|
||||||
|
this.connectMaxTries = connectMaxTries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getReconnectAfterMillis() {
|
||||||
|
return reconnectAfterMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReconnectAfterMillis(int reconnectAfterMillis) {
|
||||||
|
this.reconnectAfterMillis = reconnectAfterMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAfterConnectionDelayMillis() {
|
||||||
|
return afterConnectionDelayMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAfterConnectionDelayMillis(int afterConnectionDelayMillis) {
|
||||||
|
this.afterConnectionDelayMillis = afterConnectionDelayMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getConnectTimeoutMillis() {
|
||||||
|
return connectTimeoutMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConnectTimeoutMillis(int connectTimeoutMillis) {
|
||||||
|
this.connectTimeoutMillis = connectTimeoutMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDiscoveryEnabled() {
|
||||||
|
return enableDiscovery;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDiscoveryEnabled(boolean enableDiscovery) {
|
||||||
|
this.enableDiscovery = enableDiscovery;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,131 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal.handler;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.binding.sbus.handler.EndpointNotInitializedException;
|
||||||
|
import org.openhab.binding.sbus.handler.ModbusEndpointThingHandler;
|
||||||
|
import org.openhab.binding.sbus.internal.ModbusConfigurationException;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusCommunicationInterface;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusManager;
|
||||||
|
import org.openhab.core.io.transport.sbus.endpoint.EndpointPoolConfiguration;
|
||||||
|
import org.openhab.core.io.transport.sbus.endpoint.ModbusSlaveEndpoint;
|
||||||
|
import org.openhab.core.thing.Bridge;
|
||||||
|
import org.openhab.core.thing.ChannelUID;
|
||||||
|
import org.openhab.core.thing.ThingStatus;
|
||||||
|
import org.openhab.core.thing.ThingStatusDetail;
|
||||||
|
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||||
|
import org.openhab.core.types.Command;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for Modbus Slave endpoint thing handlers
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*
|
||||||
|
* @param <E> endpoint class
|
||||||
|
* @param <C> config class
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public abstract class AbstractModbusEndpointThingHandler<E extends ModbusSlaveEndpoint, C> extends BaseBridgeHandler
|
||||||
|
implements ModbusEndpointThingHandler {
|
||||||
|
|
||||||
|
protected volatile @Nullable C config;
|
||||||
|
protected volatile @Nullable E endpoint;
|
||||||
|
protected ModbusManager modbusManager;
|
||||||
|
protected volatile @NonNullByDefault({}) EndpointPoolConfiguration poolConfiguration;
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(AbstractModbusEndpointThingHandler.class);
|
||||||
|
private @NonNullByDefault({}) ModbusCommunicationInterface comms;
|
||||||
|
|
||||||
|
public AbstractModbusEndpointThingHandler(Bridge bridge, ModbusManager modbusManager) {
|
||||||
|
super(bridge);
|
||||||
|
this.modbusManager = modbusManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() {
|
||||||
|
synchronized (this) {
|
||||||
|
logger.trace("Initializing {} from status {}", this.getThing().getUID(), this.getThing().getStatus());
|
||||||
|
if (this.getThing().getStatus().equals(ThingStatus.ONLINE)) {
|
||||||
|
// If the bridge was online then first change it to offline.
|
||||||
|
// this ensures that children will be notified about the change
|
||||||
|
updateStatus(ThingStatus.OFFLINE);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
configure();
|
||||||
|
@Nullable
|
||||||
|
E endpoint = this.endpoint;
|
||||||
|
if (endpoint == null) {
|
||||||
|
throw new IllegalStateException("endpoint null after configuration!");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
comms = modbusManager.newModbusCommunicationInterface(endpoint, poolConfiguration);
|
||||||
|
updateStatus(ThingStatus.ONLINE);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||||
|
formatConflictingParameterError());
|
||||||
|
}
|
||||||
|
} catch (ModbusConfigurationException e) {
|
||||||
|
logger.debug("Exception during initialization", e);
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
|
||||||
|
"Exception during initialization: %s (%s)", e.getMessage(), e.getClass().getSimpleName()));
|
||||||
|
} finally {
|
||||||
|
logger.trace("initialize() of thing {} '{}' finished", thing.getUID(), thing.getLabel());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
try {
|
||||||
|
ModbusCommunicationInterface localComms = comms;
|
||||||
|
if (localComms != null) {
|
||||||
|
localComms.close();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Error closing modbus communication interface", e);
|
||||||
|
} finally {
|
||||||
|
comms = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable ModbusCommunicationInterface getCommunicationInterface() {
|
||||||
|
return comms;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public E getEndpoint() {
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract int getSlaveId() throws EndpointNotInitializedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Must be overriden by subclasses to initialize config, endpoint, and poolConfiguration
|
||||||
|
*/
|
||||||
|
protected abstract void configure() throws ModbusConfigurationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format error message in case some other endpoint has been configured with different
|
||||||
|
* {@link EndpointPoolConfiguration}
|
||||||
|
*/
|
||||||
|
protected abstract String formatConflictingParameterError();
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal.handler;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.binding.sbus.discovery.internal.ModbusEndpointDiscoveryService;
|
||||||
|
import org.openhab.binding.sbus.handler.EndpointNotInitializedException;
|
||||||
|
import org.openhab.binding.sbus.internal.ModbusConfigurationException;
|
||||||
|
import org.openhab.binding.sbus.internal.config.ModbusSerialConfiguration;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusManager;
|
||||||
|
import org.openhab.core.io.transport.sbus.endpoint.EndpointPoolConfiguration;
|
||||||
|
import org.openhab.core.io.transport.sbus.endpoint.ModbusSerialSlaveEndpoint;
|
||||||
|
import org.openhab.core.thing.Bridge;
|
||||||
|
import org.openhab.core.thing.ThingUID;
|
||||||
|
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint thing handler for serial slaves
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ModbusSerialThingHandler
|
||||||
|
extends AbstractModbusEndpointThingHandler<ModbusSerialSlaveEndpoint, ModbusSerialConfiguration> {
|
||||||
|
|
||||||
|
public ModbusSerialThingHandler(Bridge bridge, ModbusManager manager) {
|
||||||
|
super(bridge, manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure() throws ModbusConfigurationException {
|
||||||
|
ModbusSerialConfiguration config = getConfigAs(ModbusSerialConfiguration.class);
|
||||||
|
String port = config.getPort();
|
||||||
|
int baud = config.getBaud();
|
||||||
|
String flowControlIn = config.getFlowControlIn();
|
||||||
|
String flowControlOut = config.getFlowControlOut();
|
||||||
|
String stopBits = config.getStopBits();
|
||||||
|
String parity = config.getParity();
|
||||||
|
String encoding = config.getEncoding();
|
||||||
|
if (port == null || flowControlIn == null || flowControlOut == null || stopBits == null || parity == null
|
||||||
|
|| encoding == null) {
|
||||||
|
throw new ModbusConfigurationException(
|
||||||
|
"port, baud, flowControlIn, flowControlOut, stopBits, parity, encoding all must be non-null!");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
EndpointPoolConfiguration poolConfiguration = new EndpointPoolConfiguration();
|
||||||
|
this.poolConfiguration = poolConfiguration;
|
||||||
|
poolConfiguration.setConnectMaxTries(config.getConnectMaxTries());
|
||||||
|
poolConfiguration.setAfterConnectionDelayMillis(config.getAfterConnectionDelayMillis());
|
||||||
|
poolConfiguration.setConnectTimeoutMillis(config.getConnectTimeoutMillis());
|
||||||
|
poolConfiguration.setInterTransactionDelayMillis(config.getTimeBetweenTransactionsMillis());
|
||||||
|
|
||||||
|
// Never reconnect serial connections "automatically"
|
||||||
|
poolConfiguration.setInterConnectDelayMillis(1000);
|
||||||
|
poolConfiguration.setReconnectAfterMillis(-1);
|
||||||
|
|
||||||
|
endpoint = new ModbusSerialSlaveEndpoint(port, baud, flowControlIn, flowControlOut, config.getDataBits(),
|
||||||
|
stopBits, parity, encoding, config.isEcho(), config.getReceiveTimeoutMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if auto discovery is enabled in the config
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isDiscoveryEnabled() {
|
||||||
|
if (config != null) {
|
||||||
|
return config.isDiscoveryEnabled();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("null") // Since endpoint in Optional.map cannot be null
|
||||||
|
@Override
|
||||||
|
protected String formatConflictingParameterError() {
|
||||||
|
return String.format(
|
||||||
|
"Endpoint '%s' has conflicting parameters: parameters of this thing (%s '%s') are different from some other thing's parameter. Ensure that all endpoints pointing to serial port '%s' have same parameters.",
|
||||||
|
endpoint, thing.getUID(), this.thing.getLabel(),
|
||||||
|
Optional.ofNullable(this.endpoint).map(e -> e.getPortName()).orElse("<null>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSlaveId() throws EndpointNotInitializedException {
|
||||||
|
ModbusSerialConfiguration config = this.config;
|
||||||
|
if (config == null) {
|
||||||
|
throw new EndpointNotInitializedException();
|
||||||
|
}
|
||||||
|
return config.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSubnetId() throws EndpointNotInitializedException {
|
||||||
|
ModbusSerialConfiguration config = this.config;
|
||||||
|
if (config == null) {
|
||||||
|
throw new EndpointNotInitializedException();
|
||||||
|
}
|
||||||
|
return config.getSubnetId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ThingUID getUID() {
|
||||||
|
return getThing().getUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||||
|
return Set.of(ModbusEndpointDiscoveryService.class);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal.handler;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.binding.sbus.discovery.internal.ModbusEndpointDiscoveryService;
|
||||||
|
import org.openhab.binding.sbus.handler.EndpointNotInitializedException;
|
||||||
|
import org.openhab.binding.sbus.internal.ModbusConfigurationException;
|
||||||
|
import org.openhab.binding.sbus.internal.config.ModbusUdpConfiguration;
|
||||||
|
import org.openhab.core.io.transport.sbus.ModbusManager;
|
||||||
|
import org.openhab.core.io.transport.sbus.endpoint.EndpointPoolConfiguration;
|
||||||
|
import org.openhab.core.io.transport.sbus.endpoint.ModbusUDPSlaveEndpoint;
|
||||||
|
import org.openhab.core.thing.Bridge;
|
||||||
|
import org.openhab.core.thing.ThingUID;
|
||||||
|
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint thing handler for UDP slaves
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ModbusUdpThingHandler
|
||||||
|
extends AbstractModbusEndpointThingHandler<ModbusUDPSlaveEndpoint, ModbusUdpConfiguration> {
|
||||||
|
|
||||||
|
public ModbusUdpThingHandler(Bridge bridge, ModbusManager manager) {
|
||||||
|
super(bridge, manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure() throws ModbusConfigurationException {
|
||||||
|
ModbusUdpConfiguration config = getConfigAs(ModbusUdpConfiguration.class);
|
||||||
|
|
||||||
|
String host = config.getHost();
|
||||||
|
if (host == null) {
|
||||||
|
throw new ModbusConfigurationException("host must be non-null!");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.config = config;
|
||||||
|
endpoint = new ModbusUDPSlaveEndpoint(host, config.getPort());
|
||||||
|
|
||||||
|
EndpointPoolConfiguration poolConfiguration = new EndpointPoolConfiguration();
|
||||||
|
this.poolConfiguration = poolConfiguration;
|
||||||
|
poolConfiguration.setConnectMaxTries(config.getConnectMaxTries());
|
||||||
|
poolConfiguration.setAfterConnectionDelayMillis(config.getAfterConnectionDelayMillis());
|
||||||
|
poolConfiguration.setConnectTimeoutMillis(config.getConnectTimeoutMillis());
|
||||||
|
poolConfiguration.setInterConnectDelayMillis(config.getTimeBetweenReconnectMillis());
|
||||||
|
poolConfiguration.setInterTransactionDelayMillis(config.getTimeBetweenTransactionsMillis());
|
||||||
|
poolConfiguration.setReconnectAfterMillis(config.getReconnectAfterMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("null") // since Optional.map is always called with NonNull argument
|
||||||
|
@Override
|
||||||
|
protected String formatConflictingParameterError() {
|
||||||
|
return String.format(
|
||||||
|
"Endpoint '%s' has conflicting parameters: parameters of this thing (%s '%s') are different from some other thing's parameter. Ensure that all endpoints pointing to udp slave '%s:%s' have same parameters.",
|
||||||
|
endpoint, thing.getUID(), this.thing.getLabel(),
|
||||||
|
Optional.ofNullable(this.endpoint).map(e -> e.getAddress()).orElse("<null>"),
|
||||||
|
Optional.ofNullable(this.endpoint).map(e -> String.valueOf(e.getPort())).orElse("<null>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSlaveId() throws EndpointNotInitializedException {
|
||||||
|
ModbusUdpConfiguration localConfig = config;
|
||||||
|
if (localConfig == null) {
|
||||||
|
throw new EndpointNotInitializedException();
|
||||||
|
}
|
||||||
|
return localConfig.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSubnetId() throws EndpointNotInitializedException {
|
||||||
|
ModbusUdpConfiguration localConfig = config;
|
||||||
|
if (localConfig == null) {
|
||||||
|
throw new EndpointNotInitializedException();
|
||||||
|
}
|
||||||
|
return localConfig.getSubnetId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ThingUID getUID() {
|
||||||
|
return getThing().getUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if discovery is enabled
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isDiscoveryEnabled() {
|
||||||
|
if (config != null) {
|
||||||
|
return config.isDiscoveryEnabled();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||||
|
return Set.of(ModbusEndpointDiscoveryService.class);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,253 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal.profiles;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import javax.measure.Quantity;
|
||||||
|
import javax.measure.UnconvertibleException;
|
||||||
|
import javax.measure.Unit;
|
||||||
|
import javax.measure.quantity.Dimensionless;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.core.library.types.DecimalType;
|
||||||
|
import org.openhab.core.library.types.QuantityType;
|
||||||
|
import org.openhab.core.library.unit.Units;
|
||||||
|
import org.openhab.core.thing.profiles.ProfileCallback;
|
||||||
|
import org.openhab.core.thing.profiles.ProfileContext;
|
||||||
|
import org.openhab.core.thing.profiles.ProfileTypeUID;
|
||||||
|
import org.openhab.core.thing.profiles.StateProfile;
|
||||||
|
import org.openhab.core.types.Command;
|
||||||
|
import org.openhab.core.types.RefreshType;
|
||||||
|
import org.openhab.core.types.State;
|
||||||
|
import org.openhab.core.types.Type;
|
||||||
|
import org.openhab.core.types.UnDefType;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profile for applying gain and offset to values.
|
||||||
|
*
|
||||||
|
* Output of the profile is
|
||||||
|
* - (incoming value + pre-gain-offset) * gain (update towards item)
|
||||||
|
* - (incoming value / gain) - pre-gain-offset (command from item)
|
||||||
|
*
|
||||||
|
* Gain can also specify unit of the result, converting otherwise bare numbers to ones with quantity.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ModbusGainOffsetProfile<Q extends Quantity<Q>> implements StateProfile {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(ModbusGainOffsetProfile.class);
|
||||||
|
private static final String PREGAIN_OFFSET_PARAM = "pre-gain-offset";
|
||||||
|
private static final String GAIN_PARAM = "gain";
|
||||||
|
|
||||||
|
private final ProfileCallback callback;
|
||||||
|
private final ProfileContext context;
|
||||||
|
|
||||||
|
private Optional<QuantityType<Dimensionless>> pregainOffset;
|
||||||
|
private Optional<QuantityType<Q>> gain;
|
||||||
|
|
||||||
|
public ModbusGainOffsetProfile(ProfileCallback callback, ProfileContext context) {
|
||||||
|
this.callback = callback;
|
||||||
|
this.context = context;
|
||||||
|
{
|
||||||
|
Object rawOffsetValue = orDefault("0", this.context.getConfiguration().get(PREGAIN_OFFSET_PARAM));
|
||||||
|
logger.debug("Configuring profile with {} parameter '{}'", PREGAIN_OFFSET_PARAM, rawOffsetValue);
|
||||||
|
pregainOffset = parameterAsQuantityType(PREGAIN_OFFSET_PARAM, rawOffsetValue, Units.ONE);
|
||||||
|
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Object gainValue = orDefault("1", this.context.getConfiguration().get(GAIN_PARAM));
|
||||||
|
logger.debug("Configuring profile with {} parameter '{}'", GAIN_PARAM, gainValue);
|
||||||
|
gain = parameterAsQuantityType(GAIN_PARAM, gainValue);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
return pregainOffset.isPresent() && gain.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<QuantityType<Dimensionless>> getPregainOffset() {
|
||||||
|
return pregainOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<QuantityType<Q>> getGain() {
|
||||||
|
return gain;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProfileTypeUID getProfileTypeUID() {
|
||||||
|
return ModbusProfiles.GAIN_OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStateUpdateFromItem(State state) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCommandFromItem(Command command) {
|
||||||
|
Type result = applyGainOffset(command, false);
|
||||||
|
if (result instanceof Command cmd) {
|
||||||
|
logger.trace("Command '{}' from item, sending converted '{}' state towards handler.", command, result);
|
||||||
|
callback.handleCommand(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCommandFromHandler(Command command) {
|
||||||
|
Type result = applyGainOffset(command, true);
|
||||||
|
if (result instanceof Command cmd) {
|
||||||
|
logger.trace("Command '{}' from handler, sending converted '{}' command towards item.", command, result);
|
||||||
|
callback.sendCommand(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStateUpdateFromHandler(State state) {
|
||||||
|
State result = (State) applyGainOffset(state, true);
|
||||||
|
logger.trace("State update '{}' from handler, sending converted '{}' state towards item.", state, result);
|
||||||
|
callback.sendUpdate(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Type applyGainOffset(Type state, boolean towardsItem) {
|
||||||
|
Type result = UnDefType.UNDEF;
|
||||||
|
Optional<QuantityType<Q>> localGain = gain;
|
||||||
|
Optional<QuantityType<Dimensionless>> localPregainOffset = pregainOffset;
|
||||||
|
if (localGain.isEmpty() || localPregainOffset.isEmpty()) {
|
||||||
|
logger.warn("Gain or pre-gain-offset unavailable. Check logs for configuration errors.");
|
||||||
|
return UnDefType.UNDEF;
|
||||||
|
} else if (state instanceof UnDefType) {
|
||||||
|
return UnDefType.UNDEF;
|
||||||
|
}
|
||||||
|
|
||||||
|
QuantityType<Q> gain = localGain.get();
|
||||||
|
QuantityType<Dimensionless> pregainOffsetQt = localPregainOffset.get();
|
||||||
|
String formula = towardsItem ? String.format("( '%s' + '%s') * '%s'", state, pregainOffsetQt, gain)
|
||||||
|
: String.format("'%s'/'%s' - '%s'", state, gain, pregainOffsetQt);
|
||||||
|
if (state instanceof QuantityType quantityState) {
|
||||||
|
try {
|
||||||
|
if (towardsItem) {
|
||||||
|
@SuppressWarnings("unchecked") // xx.toUnit(ONE) returns null or QuantityType<Dimensionless>
|
||||||
|
@Nullable
|
||||||
|
QuantityType<Dimensionless> qtState = (QuantityType<Dimensionless>) (quantityState
|
||||||
|
.toUnit(Units.ONE));
|
||||||
|
if (qtState == null) {
|
||||||
|
logger.warn("Profile can only process plain numbers from handler. Got unit {}. Returning UNDEF",
|
||||||
|
quantityState.getUnit());
|
||||||
|
return UnDefType.UNDEF;
|
||||||
|
}
|
||||||
|
QuantityType<Dimensionless> offsetted = qtState.add(pregainOffsetQt);
|
||||||
|
result = applyGainTowardsItem(offsetted, gain);
|
||||||
|
} else {
|
||||||
|
result = applyGainTowardsHandler(quantityState, gain).subtract(pregainOffsetQt);
|
||||||
|
}
|
||||||
|
} catch (UnconvertibleException | UnsupportedOperationException e) {
|
||||||
|
logger.warn(
|
||||||
|
"Cannot apply gain ('{}') and pre-gain-offset ('{}') to state ('{}') (formula {}) because types do not match (towardsItem={}): {}",
|
||||||
|
gain, pregainOffsetQt, state, formula, towardsItem, e.getMessage());
|
||||||
|
return UnDefType.UNDEF;
|
||||||
|
}
|
||||||
|
} else if (state instanceof DecimalType decState) {
|
||||||
|
return applyGainOffset(new QuantityType<>(decState, Units.ONE), towardsItem);
|
||||||
|
} else if (state instanceof RefreshType) {
|
||||||
|
result = state;
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
"Gain '{}' cannot be applied to the incompatible state '{}' of type {} sent from the binding (towardsItem={}). Returning original state.",
|
||||||
|
gain, state, state.getClass().getSimpleName(), towardsItem);
|
||||||
|
result = state;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<QuantityType<Q>> parameterAsQuantityType(String parameterName, Object parameterValue) {
|
||||||
|
return parameterAsQuantityType(parameterName, parameterValue, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <QU extends Quantity<QU>> Optional<QuantityType<QU>> parameterAsQuantityType(String parameterName,
|
||||||
|
Object parameterValue, @Nullable Unit<QU> assertUnit) {
|
||||||
|
Optional<QuantityType<QU>> result = Optional.empty();
|
||||||
|
Unit<QU> sourceUnit = null;
|
||||||
|
if (parameterValue instanceof String str) {
|
||||||
|
try {
|
||||||
|
QuantityType<QU> qt = new QuantityType<>(str);
|
||||||
|
result = Optional.of(qt);
|
||||||
|
sourceUnit = qt.getUnit();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
logger.error("Cannot convert value '{}' of parameter '{}' into a QuantityType.", parameterValue,
|
||||||
|
parameterName);
|
||||||
|
}
|
||||||
|
} else if (parameterValue instanceof BigDecimal parameterBigDecimal) {
|
||||||
|
result = Optional.of(new QuantityType<QU>(parameterBigDecimal.toString()));
|
||||||
|
} else {
|
||||||
|
logger.error("Parameter '{}' is not of type String or BigDecimal", parameterName);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result = result.map(quantityType -> convertUnit(quantityType, assertUnit));
|
||||||
|
if (result.isEmpty()) {
|
||||||
|
logger.error("Unable to convert parameter '{}' to unit {}. Unit was {}.", parameterName, assertUnit,
|
||||||
|
sourceUnit);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private <QU extends Quantity<QU>> @Nullable QuantityType<QU> convertUnit(QuantityType<QU> quantityType,
|
||||||
|
@Nullable Unit<QU> unit) {
|
||||||
|
if (unit == null) {
|
||||||
|
return quantityType;
|
||||||
|
}
|
||||||
|
QuantityType<QU> normalizedQt = quantityType.toUnit(unit);
|
||||||
|
if (normalizedQt != null) {
|
||||||
|
return normalizedQt;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate qtState * gain or qtState/gain
|
||||||
|
*
|
||||||
|
* When the conversion is towards the handler (towardsItem=false), unit will be ONE
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private <QU extends Quantity<QU>> QuantityType<QU> applyGainTowardsItem(QuantityType<Dimensionless> qtState,
|
||||||
|
QuantityType<QU> gainDelta) {
|
||||||
|
return new QuantityType<>(qtState.toBigDecimal().multiply(gainDelta.toBigDecimal()), gainDelta.getUnit());
|
||||||
|
}
|
||||||
|
|
||||||
|
private QuantityType<Dimensionless> applyGainTowardsHandler(QuantityType<?> qtState, QuantityType<?> gainDelta) {
|
||||||
|
QuantityType<?> plain = qtState.toUnit(gainDelta.getUnit());
|
||||||
|
if (plain == null) {
|
||||||
|
throw new UnconvertibleException(
|
||||||
|
String.format("Cannot process command '%s', unit should compatible with gain", qtState));
|
||||||
|
}
|
||||||
|
return new QuantityType<>(plain.toBigDecimal().divide(gainDelta.toBigDecimal()), Units.ONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object orDefault(Object defaultValue, @Nullable Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
return defaultValue;
|
||||||
|
} else if (value instanceof String str && str.isBlank()) {
|
||||||
|
return defaultValue;
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal.profiles;
|
||||||
|
|
||||||
|
import static org.openhab.binding.sbus.internal.profiles.ModbusProfiles.GAIN_OFFSET;
|
||||||
|
import static org.openhab.binding.sbus.internal.profiles.ModbusProfiles.GAIN_OFFSET_TYPE;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.core.thing.profiles.Profile;
|
||||||
|
import org.openhab.core.thing.profiles.ProfileCallback;
|
||||||
|
import org.openhab.core.thing.profiles.ProfileContext;
|
||||||
|
import org.openhab.core.thing.profiles.ProfileFactory;
|
||||||
|
import org.openhab.core.thing.profiles.ProfileType;
|
||||||
|
import org.openhab.core.thing.profiles.ProfileTypeProvider;
|
||||||
|
import org.openhab.core.thing.profiles.ProfileTypeUID;
|
||||||
|
import org.osgi.service.component.annotations.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A factory and advisor for modbus profiles.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
@Component(service = { ProfileFactory.class, ProfileTypeProvider.class })
|
||||||
|
public class ModbusProfileFactory implements ProfileFactory, ProfileTypeProvider {
|
||||||
|
|
||||||
|
private static final Set<ProfileType> SUPPORTED_PROFILE_TYPES = Set.of(GAIN_OFFSET_TYPE);
|
||||||
|
|
||||||
|
private static final Set<ProfileTypeUID> SUPPORTED_PROFILE_TYPE_UIDS = Set.of(GAIN_OFFSET);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback,
|
||||||
|
ProfileContext context) {
|
||||||
|
if (GAIN_OFFSET.equals(profileTypeUID)) {
|
||||||
|
return new ModbusGainOffsetProfile<>(callback, context);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<ProfileType> getProfileTypes(@Nullable Locale locale) {
|
||||||
|
return SUPPORTED_PROFILE_TYPES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<ProfileTypeUID> getSupportedProfileTypeUIDs() {
|
||||||
|
return SUPPORTED_PROFILE_TYPE_UIDS;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal.profiles;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.core.thing.profiles.ProfileTypeBuilder;
|
||||||
|
import org.openhab.core.thing.profiles.ProfileTypeUID;
|
||||||
|
import org.openhab.core.thing.profiles.StateProfileType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modbus profile constants.
|
||||||
|
*
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public interface ModbusProfiles {
|
||||||
|
static final String MODBUS_SCOPE = "modbus";
|
||||||
|
static final ProfileTypeUID GAIN_OFFSET = new ProfileTypeUID(MODBUS_SCOPE, "gainOffset");
|
||||||
|
static final StateProfileType GAIN_OFFSET_TYPE = ProfileTypeBuilder.newState(GAIN_OFFSET, "Gain-Offset Correction")
|
||||||
|
.build();
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<addon:addon id="modbus" 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>Modbus Binding</name>
|
||||||
|
<description>Binding for Modbus</description>
|
||||||
|
<connection>local</connection>
|
||||||
|
|
||||||
|
</addon:addon>
|
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<config-description:config-descriptions
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||||
|
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
|
||||||
|
https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||||
|
|
||||||
|
<config-description uri="profile:modbus:gainOffset">
|
||||||
|
<parameter name="pre-gain-offset" type="decimal">
|
||||||
|
<label>Pre-gain Offset</label>
|
||||||
|
<description>Offset to add to raw value towards the item (before the gain). The negative
|
||||||
|
offset will be applied in the
|
||||||
|
reverse direction (before inverting the gain). If omitted, zero offset is used.</description>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="gain" type="text">
|
||||||
|
<label>Gain</label>
|
||||||
|
<description>Gain to apply to the state towards the item. One can also specify the unit to declare resulting unit.
|
||||||
|
This is used as divisor for values in the reverse direction. If omitted, gain of 1 is used.</description>
|
||||||
|
</parameter>
|
||||||
|
</config-description>
|
||||||
|
</config-description:config-descriptions>
|
@ -0,0 +1,188 @@
|
|||||||
|
# add-on
|
||||||
|
|
||||||
|
addon.sbus.name = S-Bus Binding
|
||||||
|
addon.sbus.description = Binding for S-Bus
|
||||||
|
|
||||||
|
# thing types
|
||||||
|
|
||||||
|
thing-type.sbus.data.label = S-Bus Data
|
||||||
|
thing-type.sbus.data.description = Data thing extracts values from binary data received from S-Bus slave. Similarly, it is responsible of translating openHAB commands to S-Bus write requests
|
||||||
|
thing-type.sbus.poller.label = Regular Poll
|
||||||
|
thing-type.sbus.poller.description = Regular poll of data from S-Bus slaves
|
||||||
|
thing-type.sbus.serial.label = S-Bus Serial Slave
|
||||||
|
thing-type.sbus.serial.description = Endpoint for S-Bus serial slaves
|
||||||
|
thing-type.sbus.udp.label = S-Bus UDP Slave
|
||||||
|
thing-type.sbus.udp.description = Endpoint for S-Bus UDP slaves
|
||||||
|
|
||||||
|
# thing types config
|
||||||
|
|
||||||
|
thing-type.config.sbus.data.readStart.label = Read Address
|
||||||
|
thing-type.config.sbus.data.readStart.description = Start address to start reading the value. Use empty for write-only things. <br /> <br />Input as zero-based index number, e.g. in place of 400001 (first holding register), use the address 0. Must be between (poller start) and (poller start + poller length - 1) (inclusive). <br /> <br />With registers and value type less than 16 bits, you must use X.Y format where Y specifies the sub-element to read from the 16 bit register: <ul> <li>For example, 3.1 would mean pick second bit from register index 3 with bit value type. </li> <li>With int8 valuetype, it would pick the high byte of register index 3.</li> </ul>
|
||||||
|
thing-type.config.sbus.data.readTransform.label = Read Transform
|
||||||
|
thing-type.config.sbus.data.readTransform.description = Transformation to apply to polled data, after it has been converted to number using readValueType <br /><br />Use "default" to communicate that no transformation is done and value should be passed as is. <br />Use SERVICENAME(ARG) or SERVICENAME:ARG to use transformation service. <br />Any other value than the above types will be interpreted as static text, in which case the actual content of the polled value is ignored. <br />You can chain many transformations with ∩, for example SERVICE1:ARG1∩SERVICE2:ARG2
|
||||||
|
thing-type.config.sbus.data.readValueType.label = Read Value Type
|
||||||
|
thing-type.config.sbus.data.readValueType.description = How data is read from modbus. Use empty for write-only things. <br /><br />With registers all value types are applicable.
|
||||||
|
thing-type.config.sbus.data.readValueType.option.int64 = 64bit signed integer (int64)
|
||||||
|
thing-type.config.sbus.data.readValueType.option.uint64 = 64bit unsigned integer (uint64)
|
||||||
|
thing-type.config.sbus.data.readValueType.option.int64_swap = 64bit signed integer, 16bit words in reverse order (dcba) (int64_swap)
|
||||||
|
thing-type.config.sbus.data.readValueType.option.uint64_swap = 64bit unsigned integer, 16bit words in reverse order (dcba) (uint64_swap)
|
||||||
|
thing-type.config.sbus.data.readValueType.option.float32 = 32bit floating point (float32)
|
||||||
|
thing-type.config.sbus.data.readValueType.option.float32_swap = 32bit floating point, 16bit words swapped (float32_swap)
|
||||||
|
thing-type.config.sbus.data.readValueType.option.int32 = 32bit signed integer (int32)
|
||||||
|
thing-type.config.sbus.data.readValueType.option.uint32 = 32bit unsigned integer (uint32)
|
||||||
|
thing-type.config.sbus.data.readValueType.option.int32_swap = 32bit signed integer, 16bit words swapped (int32_swap)
|
||||||
|
thing-type.config.sbus.data.readValueType.option.uint32_swap = 32bit unsigned integer, 16bit words swapped (uint32_swap)
|
||||||
|
thing-type.config.sbus.data.readValueType.option.int16 = 16bit signed integer (int16)
|
||||||
|
thing-type.config.sbus.data.readValueType.option.uint16 = 16bit unsigned integer (uint16)
|
||||||
|
thing-type.config.sbus.data.readValueType.option.int8 = 8bit signed integer (int8)
|
||||||
|
thing-type.config.sbus.data.readValueType.option.uint8 = 8bit unsigned integer (uint8)
|
||||||
|
thing-type.config.sbus.data.readValueType.option.bit = individual bit (bit)
|
||||||
|
thing-type.config.sbus.data.updateUnchangedValuesEveryMillis.label = Interval for Updating Unchanged Values
|
||||||
|
thing-type.config.sbus.data.updateUnchangedValuesEveryMillis.description = Interval to update unchanged values. Normally unchanged values are not updated. In milliseconds.
|
||||||
|
thing-type.config.sbus.data.writeMaxTries.label = Maximum Tries When Writing
|
||||||
|
thing-type.config.sbus.data.writeMaxTries.description = Number of tries when writing data, if some of the writes fail. For single try, enter 1.
|
||||||
|
thing-type.config.sbus.data.writeMultipleEvenWithSingleRegisterOrCoil.label = Write Multiple Even with Single Register or Coil
|
||||||
|
thing-type.config.sbus.data.writeMultipleEvenWithSingleRegisterOrCoil.description = Whether single register / coil of data is written using FC16 ("Write Multiple Holding Registers") / FC15 ("Write Multiple Coils"), respectively. <br /> <br />If false, FC6/FC5 are used with single register and single coil, respectively.
|
||||||
|
thing-type.config.sbus.data.writeStart.label = Write Address
|
||||||
|
thing-type.config.sbus.data.writeStart.description = Start address of the first holding register or coil in the write. Use empty for read-only things. <br />Use zero based address, e.g. in place of 400001 (first holding register), use the address 0. This address is passed to data frame as is. <br />One can write individual bits of a register using X.Y format where X is the register and Y is the bit (0 refers to least significant bit).
|
||||||
|
thing-type.config.sbus.data.writeTransform.label = Write Transform
|
||||||
|
thing-type.config.sbus.data.writeTransform.description = Transformation to apply to received commands. <br /><br />Use "default" to communicate that no transformation is done and value should be passed as is. <br />Use SERVICENAME(ARG) or SERVICENAME:ARG to use transformation service. <br />Any other value than the above types will be interpreted as static text, in which case the actual content of the command <br />You can chain many transformations with ∩, for example SERVICE1:ARG1∩SERVICE2:ARG2 value is ignored.
|
||||||
|
thing-type.config.sbus.data.writeType.label = Write Type
|
||||||
|
thing-type.config.sbus.data.writeType.description = Type of data to write. Leave empty for read-only things. <br /> <br /> Coil uses function code (FC) FC05 or FC15. Holding register uses FC06 or FC16. See writeMultipleEvenWithSingleRegisterOrCoil parameter.
|
||||||
|
thing-type.config.sbus.data.writeType.option.coil = coil, or digital out (DO)
|
||||||
|
thing-type.config.sbus.data.writeType.option.holding = holding register
|
||||||
|
thing-type.config.sbus.data.writeValueType.label = Write Value Type
|
||||||
|
thing-type.config.sbus.data.writeValueType.description = How data is written to modbus. Only applicable to registers, you can leave this undefined for coil. <br /><br />Negative integers are encoded with two's complement, while positive integers are encoded as is.
|
||||||
|
thing-type.config.sbus.data.writeValueType.option.int64 = 64bit positive or negative integer, 4 registers (int64, uint64)
|
||||||
|
thing-type.config.sbus.data.writeValueType.option.int64_swap = 64bit positive or negative integer, 4 registers but with 16bit words/registers in reverse order (dcba) (int64_swap, uint64_swap)
|
||||||
|
thing-type.config.sbus.data.writeValueType.option.float32 = 32bit floating point (float32)
|
||||||
|
thing-type.config.sbus.data.writeValueType.option.float32_swap = 32bit floating point, 16bit words swapped (float32_swap)
|
||||||
|
thing-type.config.sbus.data.writeValueType.option.int32 = 32bit positive or negative integer, 2 registers (int32, uint32)
|
||||||
|
thing-type.config.sbus.data.writeValueType.option.int32_swap = 32bit positive or negative integer, 2 registers but with 16bit words/registers in reverse order (ba) (int32_swap, uint32_swap)
|
||||||
|
thing-type.config.sbus.data.writeValueType.option.int16 = 16bit positive or negative integer, 1 register (int16, uint16)
|
||||||
|
thing-type.config.sbus.data.writeValueType.option.bit = individual bit (bit)
|
||||||
|
thing-type.config.sbus.poller.cacheMillis.label = Cache Duration
|
||||||
|
thing-type.config.sbus.poller.cacheMillis.description = Duration for data cache to be valid, in milliseconds. This cache is used only to serve REFRESH commands. <br /> <br />Use zero to disable the caching.
|
||||||
|
thing-type.config.sbus.poller.length.label = Length
|
||||||
|
thing-type.config.sbus.poller.length.description = Number of registers, coils or discrete inputs to read. <br /> <br />Maximum number of registers is 125 while 2000 is maximum for coils and discrete inputs.
|
||||||
|
thing-type.config.sbus.poller.maxTries.label = Maximum Tries When Reading
|
||||||
|
thing-type.config.sbus.poller.maxTries.description = Number of tries when reading data, if some of the reading fail. For single try, enter 1.
|
||||||
|
thing-type.config.sbus.poller.refresh.label = Poll Interval
|
||||||
|
thing-type.config.sbus.poller.refresh.description = Poll interval in milliseconds. Use zero to disable automatic polling.
|
||||||
|
thing-type.config.sbus.poller.start.label = Start
|
||||||
|
thing-type.config.sbus.poller.start.description = Address of the first register, coil, or discrete input to poll. <br /> <br />Input as zero-based index number, e.g. in place of 400001 (first holding register), use the address 0.
|
||||||
|
thing-type.config.sbus.poller.type.label = Type
|
||||||
|
thing-type.config.sbus.poller.type.description = Type of modbus items to poll
|
||||||
|
thing-type.config.sbus.poller.type.option.coil = coil, or digital out (DO)
|
||||||
|
thing-type.config.sbus.poller.type.option.discrete = discrete input, or digital in (DI)
|
||||||
|
thing-type.config.sbus.poller.type.option.holding = holding register
|
||||||
|
thing-type.config.sbus.poller.type.option.input = input register
|
||||||
|
thing-type.config.sbus.serial.afterConnectionDelayMillis.label = Connection warm-up time
|
||||||
|
thing-type.config.sbus.serial.afterConnectionDelayMillis.description = Connection warm-up time. Additional time which is spent on preparing connection which should be spent waiting while end device is getting ready to answer first modbus call. In milliseconds.
|
||||||
|
thing-type.config.sbus.serial.baud.label = Baud
|
||||||
|
thing-type.config.sbus.serial.baud.description = Baud of the connection
|
||||||
|
thing-type.config.sbus.serial.baud.option.75 = 75
|
||||||
|
thing-type.config.sbus.serial.baud.option.110 = 110
|
||||||
|
thing-type.config.sbus.serial.baud.option.300 = 300
|
||||||
|
thing-type.config.sbus.serial.baud.option.1200 = 1200
|
||||||
|
thing-type.config.sbus.serial.baud.option.2400 = 2400
|
||||||
|
thing-type.config.sbus.serial.baud.option.4800 = 4800
|
||||||
|
thing-type.config.sbus.serial.baud.option.9600 = 9600
|
||||||
|
thing-type.config.sbus.serial.baud.option.19200 = 19200
|
||||||
|
thing-type.config.sbus.serial.baud.option.38400 = 38400
|
||||||
|
thing-type.config.sbus.serial.baud.option.57600 = 57600
|
||||||
|
thing-type.config.sbus.serial.baud.option.115200 = 115200
|
||||||
|
thing-type.config.sbus.serial.connectMaxTries.label = Maximum Connection Tries
|
||||||
|
thing-type.config.sbus.serial.connectMaxTries.description = How many times we try to establish the connection. Should be at least 1.
|
||||||
|
thing-type.config.sbus.serial.connectTimeoutMillis.label = Timeout for Establishing the Connection
|
||||||
|
thing-type.config.sbus.serial.connectTimeoutMillis.description = The maximum time that is waited when establishing the connection. Value of zero means that system/OS default is respected. In milliseconds.
|
||||||
|
thing-type.config.sbus.serial.dataBits.label = Data Bits
|
||||||
|
thing-type.config.sbus.serial.dataBits.description = Data bits
|
||||||
|
thing-type.config.sbus.serial.dataBits.option.5 = 5
|
||||||
|
thing-type.config.sbus.serial.dataBits.option.6 = 6
|
||||||
|
thing-type.config.sbus.serial.dataBits.option.7 = 7
|
||||||
|
thing-type.config.sbus.serial.dataBits.option.8 = 8
|
||||||
|
thing-type.config.sbus.serial.echo.label = RS485 Echo Mode
|
||||||
|
thing-type.config.sbus.serial.echo.description = Flag for setting the RS485 echo mode <br/> <br/>This controls whether we should try to read back whatever we send on the line, before reading the response.
|
||||||
|
thing-type.config.sbus.serial.enableDiscovery.label = Discovery Enabled
|
||||||
|
thing-type.config.sbus.serial.enableDiscovery.description = When enabled we try to find a device specific handler. Turn this on if you're using one of the supported devices.
|
||||||
|
thing-type.config.sbus.serial.encoding.label = Encoding
|
||||||
|
thing-type.config.sbus.serial.encoding.description = Encoding
|
||||||
|
thing-type.config.sbus.serial.encoding.option.ascii = ASCII
|
||||||
|
thing-type.config.sbus.serial.encoding.option.rtu = RTU
|
||||||
|
thing-type.config.sbus.serial.encoding.option.bin = BIN
|
||||||
|
thing-type.config.sbus.serial.flowControlIn.label = Flow Control In
|
||||||
|
thing-type.config.sbus.serial.flowControlIn.description = Type of flow control for receiving
|
||||||
|
thing-type.config.sbus.serial.flowControlIn.option.none = None
|
||||||
|
thing-type.config.sbus.serial.flowControlIn.option.xon/xoff in = XON/XOFF
|
||||||
|
thing-type.config.sbus.serial.flowControlIn.option.rts/cts in = RTS/CTS
|
||||||
|
thing-type.config.sbus.serial.flowControlOut.label = Flow Control Out
|
||||||
|
thing-type.config.sbus.serial.flowControlOut.description = Type of flow control for sending
|
||||||
|
thing-type.config.sbus.serial.flowControlOut.option.none = None
|
||||||
|
thing-type.config.sbus.serial.flowControlOut.option.xon/xoff out = XON/XOFF
|
||||||
|
thing-type.config.sbus.serial.flowControlOut.option.rts/cts out = RTS/CTS
|
||||||
|
thing-type.config.sbus.serial.id.label = Id
|
||||||
|
thing-type.config.sbus.serial.id.description = Slave id. Also known as station address or unit identifier.
|
||||||
|
thing-type.config.sbus.serial.parity.label = Parity
|
||||||
|
thing-type.config.sbus.serial.parity.description = Parity
|
||||||
|
thing-type.config.sbus.serial.parity.option.none = None
|
||||||
|
thing-type.config.sbus.serial.parity.option.even = Even
|
||||||
|
thing-type.config.sbus.serial.parity.option.odd = Odd
|
||||||
|
thing-type.config.sbus.serial.port.label = Serial Port
|
||||||
|
thing-type.config.sbus.serial.port.description = Serial port to use, for example /dev/ttyS0 or COM1
|
||||||
|
thing-type.config.sbus.serial.receiveTimeoutMillis.label = Read Operation Timeout
|
||||||
|
thing-type.config.sbus.serial.receiveTimeoutMillis.description = Timeout for read operations. In milliseconds.
|
||||||
|
thing-type.config.sbus.serial.stopBits.label = Stop Bits
|
||||||
|
thing-type.config.sbus.serial.stopBits.description = Stop bits
|
||||||
|
thing-type.config.sbus.serial.stopBits.option.1.0 = 1
|
||||||
|
thing-type.config.sbus.serial.stopBits.option.1.5 = 1.5
|
||||||
|
thing-type.config.sbus.serial.stopBits.option.2.0 = 2
|
||||||
|
thing-type.config.sbus.serial.timeBetweenTransactionsMillis.label = Time Between Transactions
|
||||||
|
thing-type.config.sbus.serial.timeBetweenTransactionsMillis.description = How long to delay we must have at minimum between two consecutive MODBUS transactions. In milliseconds.
|
||||||
|
thing-type.config.sbus.udp.afterConnectionDelayMillis.label = Connection warm-up time
|
||||||
|
thing-type.config.sbus.udp.afterConnectionDelayMillis.description = Connection warm-up time. Additional time which is spent on preparing connection which should be spent waiting while end device is getting ready to answer first modbus call. In milliseconds.
|
||||||
|
thing-type.config.sbus.udp.connectMaxTries.label = Maximum Connection Tries
|
||||||
|
thing-type.config.sbus.udp.connectMaxTries.description = How many times we try to establish the connection. Should be at least 1.
|
||||||
|
thing-type.config.sbus.udp.connectTimeoutMillis.label = Timeout for Establishing the Connection
|
||||||
|
thing-type.config.sbus.udp.connectTimeoutMillis.description = The maximum time that is waited when establishing the connection. Value of zero means that system/OS default is respected. In milliseconds.
|
||||||
|
thing-type.config.sbus.udp.enableDiscovery.label = Discovery Enabled
|
||||||
|
thing-type.config.sbus.udp.enableDiscovery.description = When enabled we try to find a device specific handler. Turn this on if you're using one of the supported devices.
|
||||||
|
thing-type.config.sbus.udp.host.label = IP Address or Hostname
|
||||||
|
thing-type.config.sbus.udp.host.description = Network address of the device
|
||||||
|
thing-type.config.sbus.udp.id.label = Id
|
||||||
|
thing-type.config.sbus.udp.id.description = Slave id. Also known as station address or unit identifier.
|
||||||
|
thing-type.config.sbus.udp.port.label = Port
|
||||||
|
thing-type.config.sbus.udp.port.description = Port of the slave
|
||||||
|
thing-type.config.sbus.udp.reconnectAfterMillis.label = Reconnect Again After
|
||||||
|
thing-type.config.sbus.udp.reconnectAfterMillis.description = The connection is kept open at least the time specified here. Value of zero means that connection is disconnected after every MODBUS transaction. In milliseconds.
|
||||||
|
thing-type.config.sbus.udp.rtuEncoded.label = RTU Encoding
|
||||||
|
thing-type.config.sbus.udp.rtuEncoded.description = Use RTU Encoding over IP
|
||||||
|
thing-type.config.sbus.udp.timeBetweenReconnectMillis.label = Time Between Reconnections
|
||||||
|
thing-type.config.sbus.udp.timeBetweenReconnectMillis.description = How long to wait to before trying to establish a new connection after the previous one has been disconnected. In milliseconds.
|
||||||
|
thing-type.config.sbus.udp.timeBetweenTransactionsMillis.label = Time Between Transactions
|
||||||
|
thing-type.config.sbus.udp.timeBetweenTransactionsMillis.description = How long to delay we must have at minimum between two consecutive MODBUS transactions. In milliseconds.
|
||||||
|
|
||||||
|
# channel types
|
||||||
|
|
||||||
|
channel-type.sbus.contact-type.label = Value as Contact
|
||||||
|
channel-type.sbus.contact-type.description = Contact item channel
|
||||||
|
channel-type.sbus.datetime-type.label = Value as DateTime
|
||||||
|
channel-type.sbus.datetime-type.description = DateTime item channel
|
||||||
|
channel-type.sbus.dimmer-type.label = Value as Dimmer
|
||||||
|
channel-type.sbus.dimmer-type.description = Dimmer item channel
|
||||||
|
channel-type.sbus.last-erroring-read-type.label = Last Erroring Read
|
||||||
|
channel-type.sbus.last-erroring-read-type.description = Date of last read error
|
||||||
|
channel-type.sbus.last-erroring-write-type.label = Last Erroring Write
|
||||||
|
channel-type.sbus.last-erroring-write-type.description = Date of last write error
|
||||||
|
channel-type.sbus.last-successful-read-type.label = Last Successful Read
|
||||||
|
channel-type.sbus.last-successful-read-type.description = Date of last read
|
||||||
|
channel-type.sbus.last-successful-write-type.label = Last Successful Write
|
||||||
|
channel-type.sbus.last-successful-write-type.description = Date of last write
|
||||||
|
channel-type.sbus.number-type.label = Value as Number
|
||||||
|
channel-type.sbus.number-type.description = Number item channel
|
||||||
|
channel-type.sbus.rollershutter-type.label = Value as Rollershutter
|
||||||
|
channel-type.sbus.rollershutter-type.description = Rollershutter item channel
|
||||||
|
channel-type.sbus.string-type.label = Value as String
|
||||||
|
channel-type.sbus.string-type.description = String item channel
|
||||||
|
channel-type.sbus.switch-type.label = Value as Switch
|
||||||
|
channel-type.sbus.switch-type.description = Switch item channel
|
@ -0,0 +1,58 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<thing:thing-descriptions bindingId="modbus"
|
||||||
|
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="poller">
|
||||||
|
<supported-bridge-type-refs>
|
||||||
|
<bridge-type-ref id="udp"/>
|
||||||
|
<bridge-type-ref id="serial"/>
|
||||||
|
</supported-bridge-type-refs>
|
||||||
|
|
||||||
|
<label>Regular Poll</label>
|
||||||
|
<description>Regular poll of data from Modbus slaves</description>
|
||||||
|
<config-description>
|
||||||
|
<parameter name="refresh" type="integer" min="0" unit="ms">
|
||||||
|
<label>Poll Interval</label>
|
||||||
|
<description>Poll interval in milliseconds. Use zero to disable automatic polling.</description>
|
||||||
|
<default>500</default>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="start" type="integer">
|
||||||
|
<label>Start</label>
|
||||||
|
<description><![CDATA[Address of the first register, coil, or discrete input to poll.
|
||||||
|
<br />
|
||||||
|
<br />Input as zero-based index number, e.g. in place of 400001 (first holding register), use the address 0.]]></description>
|
||||||
|
<default>0</default>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="length" type="integer" required="true">
|
||||||
|
<label>Length</label>
|
||||||
|
<description><![CDATA[Number of registers, coils or discrete inputs to read.
|
||||||
|
<br />
|
||||||
|
<br />Maximum number of registers is 125 while 2000 is maximum for coils and discrete inputs.]]></description>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="type" type="text" required="true">
|
||||||
|
<label>Type</label>
|
||||||
|
<description>Type of modbus items to poll</description>
|
||||||
|
<options>
|
||||||
|
<option value="coil">coil, or digital out (DO)</option>
|
||||||
|
<option value="discrete">discrete input, or digital in (DI)</option>
|
||||||
|
<option value="holding">holding register</option>
|
||||||
|
<option value="input">input register</option>
|
||||||
|
</options>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="maxTries" type="integer" min="1">
|
||||||
|
<label>Maximum Tries When Reading</label>
|
||||||
|
<default>3</default>
|
||||||
|
<description>Number of tries when reading data, if some of the reading fail. For single try, enter 1.</description>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="cacheMillis" type="integer" min="0" unit="ms">
|
||||||
|
<label>Cache Duration</label>
|
||||||
|
<default>50</default>
|
||||||
|
<description><![CDATA[Duration for data cache to be valid, in milliseconds. This cache is used only to serve REFRESH commands.
|
||||||
|
<br />
|
||||||
|
<br />Use zero to disable the caching.]]></description>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
|
</config-description>
|
||||||
|
</bridge-type>
|
||||||
|
</thing:thing-descriptions>
|
@ -0,0 +1,155 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<thing:thing-descriptions bindingId="modbus"
|
||||||
|
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="serial">
|
||||||
|
<label>Modbus Serial Slave</label>
|
||||||
|
<description>Endpoint for Modbus serial slaves</description>
|
||||||
|
<config-description>
|
||||||
|
<parameter name="port" type="text" required="true">
|
||||||
|
<label>Serial Port</label>
|
||||||
|
<context>serial-port</context>
|
||||||
|
<limitToOptions>false</limitToOptions>
|
||||||
|
<description>Serial port to use, for example /dev/ttyS0 or COM1</description>
|
||||||
|
</parameter>
|
||||||
|
|
||||||
|
<parameter name="id" type="integer">
|
||||||
|
<label>Id</label>
|
||||||
|
<description>Slave id. Also known as station address or unit identifier.</description>
|
||||||
|
<default>1</default>
|
||||||
|
</parameter>
|
||||||
|
|
||||||
|
<!-- serial parameters -->
|
||||||
|
<parameter name="baud" type="integer" multiple="false">
|
||||||
|
<label>Baud</label>
|
||||||
|
<description>Baud of the connection</description>
|
||||||
|
<default>9600</default>
|
||||||
|
<options>
|
||||||
|
<option value="75">75</option>
|
||||||
|
<option value="110">110</option>
|
||||||
|
<option value="300">300</option>
|
||||||
|
<option value="1200">1200</option>
|
||||||
|
<option value="2400">2400</option>
|
||||||
|
<option value="4800">4800</option>
|
||||||
|
<option value="9600">9600</option>
|
||||||
|
<option value="19200">19200</option>
|
||||||
|
<option value="38400">38400</option>
|
||||||
|
<option value="57600">57600</option>
|
||||||
|
<option value="115200">115200</option>
|
||||||
|
</options>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="stopBits" type="text" multiple="false">
|
||||||
|
<label>Stop Bits</label>
|
||||||
|
<description>Stop bits</description>
|
||||||
|
<default>1.0</default>
|
||||||
|
<options>
|
||||||
|
<option value="1.0">1</option>
|
||||||
|
<option value="1.5">1.5</option>
|
||||||
|
<option value="2.0">2</option>
|
||||||
|
</options>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="parity" type="text" multiple="false">
|
||||||
|
<label>Parity</label>
|
||||||
|
<description>Parity</description>
|
||||||
|
<default>none</default>
|
||||||
|
<options>
|
||||||
|
<option value="none">None</option>
|
||||||
|
<option value="even">Even</option>
|
||||||
|
<option value="odd">Odd</option>
|
||||||
|
</options>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="dataBits" type="integer" multiple="false">
|
||||||
|
<label>Data Bits</label>
|
||||||
|
<description>Data bits</description>
|
||||||
|
<default>8</default>
|
||||||
|
<options>
|
||||||
|
<option value="5">5</option>
|
||||||
|
<option value="6">6</option>
|
||||||
|
<option value="7">7</option>
|
||||||
|
<option value="8">8</option>
|
||||||
|
</options>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="encoding" type="text" multiple="false">
|
||||||
|
<label>Encoding</label>
|
||||||
|
<description>Encoding</description>
|
||||||
|
<default>rtu</default>
|
||||||
|
<options>
|
||||||
|
<option value="ascii">ASCII</option>
|
||||||
|
<option value="rtu">RTU</option>
|
||||||
|
<option value="bin">BIN</option>
|
||||||
|
</options>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="enableDiscovery" type="boolean">
|
||||||
|
<label>Discovery Enabled</label>
|
||||||
|
<description>When enabled we try to find a device specific handler. Turn this on if you're using one of the
|
||||||
|
supported devices.</description>
|
||||||
|
<default>false</default>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="echo" type="boolean">
|
||||||
|
<label>RS485 Echo Mode</label>
|
||||||
|
<description><![CDATA[Flag for setting the RS485 echo mode
|
||||||
|
<br/>
|
||||||
|
<br/>This controls whether we should try to read back whatever we send on the line, before reading the response.
|
||||||
|
]]></description>
|
||||||
|
<default>false</default>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="receiveTimeoutMillis" type="integer" min="0" unit="ms">
|
||||||
|
<label>Read Operation Timeout</label>
|
||||||
|
<description>Timeout for read operations. In milliseconds.</description>
|
||||||
|
<default>1500</default>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="flowControlIn" type="text" multiple="false">
|
||||||
|
<label>Flow Control In</label>
|
||||||
|
<description>Type of flow control for receiving</description>
|
||||||
|
<default>none</default>
|
||||||
|
<!-- values here match SerialPort.FLOWCONTROL_* constants -->
|
||||||
|
<options>
|
||||||
|
<option value="none">None</option>
|
||||||
|
<option value="xon/xoff in">XON/XOFF</option>
|
||||||
|
<option value="rts/cts in">RTS/CTS</option>
|
||||||
|
</options>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="flowControlOut" type="text" multiple="false">
|
||||||
|
<label>Flow Control Out</label>
|
||||||
|
<description>Type of flow control for sending</description>
|
||||||
|
<default>none</default>
|
||||||
|
<!-- values here match SerialPort.FLOWCONTROL_* constants -->
|
||||||
|
<options>
|
||||||
|
<option value="none">None</option>
|
||||||
|
<option value="xon/xoff out">XON/XOFF</option>
|
||||||
|
<option value="rts/cts out">RTS/CTS</option>
|
||||||
|
</options>
|
||||||
|
</parameter>
|
||||||
|
|
||||||
|
<!-- connection handling -->
|
||||||
|
<parameter name="timeBetweenTransactionsMillis" type="integer" min="0" unit="ms">
|
||||||
|
<label>Time Between Transactions</label>
|
||||||
|
<description>How long to delay we must have at minimum between two consecutive MODBUS transactions. In milliseconds.</description>
|
||||||
|
<default>35</default>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="connectMaxTries" type="integer" min="1">
|
||||||
|
<label>Maximum Connection Tries</label>
|
||||||
|
<description>How many times we try to establish the connection. Should be at least 1.</description>
|
||||||
|
<default>1</default>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="afterConnectionDelayMillis" type="integer" min="0" unit="ms">
|
||||||
|
<label>Connection warm-up time</label>
|
||||||
|
<description>Connection warm-up time. Additional time which is spent on preparing connection which should be spent
|
||||||
|
waiting while end device is getting ready to answer first modbus call. In milliseconds.</description>
|
||||||
|
<default>0</default>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="connectTimeoutMillis" type="integer" min="0" unit="ms">
|
||||||
|
<label>Timeout for Establishing the Connection</label>
|
||||||
|
<description>The maximum time that is waited when establishing the connection. Value of zero means that system/OS
|
||||||
|
default is respected. In milliseconds.</description>
|
||||||
|
<default>10000</default>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
|
</config-description>
|
||||||
|
</bridge-type>
|
||||||
|
</thing:thing-descriptions>
|
@ -0,0 +1,89 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<thing:thing-descriptions bindingId="modbus"
|
||||||
|
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="udp">
|
||||||
|
<label>Modbus UDP Slave</label>
|
||||||
|
<description>Endpoint for Modbus UDP slaves</description>
|
||||||
|
<config-description>
|
||||||
|
<parameter name="host" type="text" required="true">
|
||||||
|
<label>IP Address or Hostname</label>
|
||||||
|
<description>Network address of the device</description>
|
||||||
|
<default>localhost</default>
|
||||||
|
<context>network-address</context>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="port" type="integer">
|
||||||
|
<label>Port</label>
|
||||||
|
<description>Port of the slave</description>
|
||||||
|
<default>6000</default>
|
||||||
|
</parameter>
|
||||||
|
|
||||||
|
<parameter name="subnetId" type="integer">
|
||||||
|
<label>SubnetId</label>
|
||||||
|
<description>Slave subnet id. Can take any value between 0 and 255.</description>
|
||||||
|
<default>1</default>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="id" type="integer">
|
||||||
|
<label>Id</label>
|
||||||
|
<description>Slave id. Also known as station address or unit identifier.</description>
|
||||||
|
<default>1</default>
|
||||||
|
</parameter>
|
||||||
|
|
||||||
|
<parameter name="enableDiscovery" type="boolean">
|
||||||
|
<label>Discovery Enabled</label>
|
||||||
|
<description>When enabled we try to find a device specific handler. Turn this on if you're using one of the
|
||||||
|
supported devices.</description>
|
||||||
|
<default>false</default>
|
||||||
|
</parameter>
|
||||||
|
|
||||||
|
<parameter name="rtuEncoded" type="boolean">
|
||||||
|
<label>RTU Encoding</label>
|
||||||
|
<description>Use RTU Encoding over IP</description>
|
||||||
|
<default>false</default>
|
||||||
|
</parameter>
|
||||||
|
|
||||||
|
<!-- connection handling -->
|
||||||
|
<parameter name="timeBetweenTransactionsMillis" type="integer" min="0" unit="ms">
|
||||||
|
<label>Time Between Transactions</label>
|
||||||
|
<description>How long to delay we must have at minimum between two consecutive MODBUS transactions. In milliseconds.
|
||||||
|
</description>
|
||||||
|
<default>60</default>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="timeBetweenReconnectMillis" type="integer" min="0" unit="ms">
|
||||||
|
<label>Time Between Reconnections</label>
|
||||||
|
<description>How long to wait to before trying to establish a new connection after the previous one has been
|
||||||
|
disconnected. In milliseconds.</description>
|
||||||
|
<default>0</default>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="connectMaxTries" type="integer" min="1">
|
||||||
|
<label>Maximum Connection Tries</label>
|
||||||
|
<description>How many times we try to establish the connection. Should be at least 1.</description>
|
||||||
|
<default>1</default>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="afterConnectionDelayMillis" type="integer" min="0" unit="ms">
|
||||||
|
<label>Connection warm-up time</label>
|
||||||
|
<description>Connection warm-up time. Additional time which is spent on preparing connection which should be spent
|
||||||
|
waiting while end device is getting ready to answer first modbus call. In milliseconds.</description>
|
||||||
|
<default>0</default>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="reconnectAfterMillis" type="integer" min="0" unit="ms">
|
||||||
|
<label>Reconnect Again After</label>
|
||||||
|
<description>The connection is kept open at least the time specified here. Value of zero means that connection is
|
||||||
|
disconnected after every MODBUS transaction. In milliseconds.</description>
|
||||||
|
<default>0</default>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="connectTimeoutMillis" type="integer" min="0" unit="ms">
|
||||||
|
<label>Timeout for Establishing the Connection</label>
|
||||||
|
<description>The maximum time that is waited when establishing the connection. Value of zero means that system/OS
|
||||||
|
default is respected. In milliseconds.</description>
|
||||||
|
<default>10000</default>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
|
</config-description>
|
||||||
|
</bridge-type>
|
||||||
|
</thing:thing-descriptions>
|
@ -0,0 +1,150 @@
|
|||||||
|
<thing:thing-descriptions bindingId="modbus"
|
||||||
|
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">
|
||||||
|
<thing-type id="data">
|
||||||
|
<supported-bridge-type-refs>
|
||||||
|
<bridge-type-ref id="poller"/>
|
||||||
|
<bridge-type-ref id="udp"/>
|
||||||
|
<bridge-type-ref id="serial"/>
|
||||||
|
</supported-bridge-type-refs>
|
||||||
|
<label>Modbus Data</label>
|
||||||
|
<description>Data thing extracts values from binary data received from Modbus slave. Similarly, it is responsible of
|
||||||
|
translating openHAB commands to Modbus write requests</description>
|
||||||
|
<channels>
|
||||||
|
<channel id="number" typeId="number-type"/>
|
||||||
|
<channel id="switch" typeId="switch-type"/>
|
||||||
|
<channel id="contact" typeId="contact-type"/>
|
||||||
|
<channel id="dimmer" typeId="dimmer-type"/>
|
||||||
|
<channel id="datetime" typeId="datetime-type"/>
|
||||||
|
<channel id="string" typeId="string-type"/>
|
||||||
|
<channel id="rollershutter" typeId="rollershutter-type"/>
|
||||||
|
<channel id="lastReadSuccess" typeId="last-successful-read-type"/>
|
||||||
|
<channel id="lastReadError" typeId="last-erroring-read-type"/>
|
||||||
|
<channel id="lastWriteSuccess" typeId="last-successful-write-type"/>
|
||||||
|
<channel id="lastWriteError" typeId="last-erroring-write-type"/>
|
||||||
|
</channels>
|
||||||
|
<config-description>
|
||||||
|
<!-- what to read -->
|
||||||
|
<parameter name="readStart" type="text" pattern="^(0|[0-9][0-9]*(\.[0-9]{1,2})?)?$">
|
||||||
|
<label>Read Address</label>
|
||||||
|
<description><![CDATA[Start address to start reading the value. Use empty for write-only things.
|
||||||
|
<br />
|
||||||
|
<br />Input as zero-based index number, e.g. in place of 400001 (first holding register), use the address 0. Must be between (poller start) and (poller start + poller length - 1) (inclusive).
|
||||||
|
<br />
|
||||||
|
<br />With registers and value type less than 16 bits, you must use X.Y format where Y specifies the sub-element to read from the 16 bit register:
|
||||||
|
<ul>
|
||||||
|
<li>For example, 3.1 would mean pick second bit from register index 3 with bit value type. </li>
|
||||||
|
<li>With int8 valuetype, it would pick the high byte of register index 3.</li>
|
||||||
|
</ul>
|
||||||
|
]]>
|
||||||
|
</description>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="readTransform" type="text">
|
||||||
|
<label>Read Transform</label>
|
||||||
|
<description><![CDATA[Transformation to apply to polled data, after it has been converted to number using readValueType
|
||||||
|
<br /><br />Use "default" to communicate that no transformation is done and value should be passed as is.
|
||||||
|
<br />Use SERVICENAME(ARG) or SERVICENAME:ARG to use transformation service.
|
||||||
|
<br />Any other value than the above types will be interpreted as static text, in which case the actual content of the polled
|
||||||
|
value is ignored.
|
||||||
|
<br />You can chain many transformations with ∩, for example SERVICE1:ARG1∩SERVICE2:ARG2]]></description>
|
||||||
|
<default>default</default>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="readValueType" type="text">
|
||||||
|
<label>Read Value Type</label>
|
||||||
|
<description><![CDATA[How data is read from modbus. Use empty for write-only things.
|
||||||
|
|
||||||
|
<br /><br />With registers all value types are applicable.]]></description>
|
||||||
|
<options>
|
||||||
|
<option value="int64">64bit signed integer (int64)</option>
|
||||||
|
<option value="uint64">64bit unsigned integer (uint64)</option>
|
||||||
|
<option value="int64_swap">64bit signed integer, 16bit words in reverse order (dcba) (int64_swap)</option>
|
||||||
|
<option value="uint64_swap">64bit unsigned integer, 16bit words in reverse order (dcba) (uint64_swap)</option>
|
||||||
|
|
||||||
|
<option value="float32">32bit floating point (float32)</option>
|
||||||
|
<option value="float32_swap">32bit floating point, 16bit words swapped (float32_swap)</option>
|
||||||
|
|
||||||
|
<option value="int32">32bit signed integer (int32)</option>
|
||||||
|
<option value="uint32">32bit unsigned integer (uint32)</option>
|
||||||
|
<option value="int32_swap">32bit signed integer, 16bit words swapped (int32_swap)</option>
|
||||||
|
<option value="uint32_swap">32bit unsigned integer, 16bit words swapped (uint32_swap)</option>
|
||||||
|
|
||||||
|
<option value="int16">16bit signed integer (int16)</option>
|
||||||
|
<option value="uint16">16bit unsigned integer (uint16)</option>
|
||||||
|
|
||||||
|
<option value="int8">8bit signed integer (int8)</option>
|
||||||
|
<option value="uint8">8bit unsigned integer (uint8)</option>
|
||||||
|
|
||||||
|
<option value="bit">individual bit (bit)</option>
|
||||||
|
</options>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="writeStart" type="text">
|
||||||
|
<label>Write Address</label>
|
||||||
|
<description><![CDATA[Start address of the first holding register or coil in the write. Use empty for read-only things.
|
||||||
|
<br />Use zero based address, e.g. in place of 400001 (first holding register), use the address 0. This address is passed to data frame as is.
|
||||||
|
<br />One can write individual bits of a register using X.Y format where X is the register and Y is the bit (0 refers to least significant bit).
|
||||||
|
]]></description>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="writeType" type="text">
|
||||||
|
<label>Write Type</label>
|
||||||
|
<description><![CDATA[Type of data to write. Leave empty for read-only things.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
Coil uses function code (FC) FC05 or FC15. Holding register uses FC06 or FC16. See writeMultipleEvenWithSingleRegisterOrCoil parameter.]]></description>
|
||||||
|
<options>
|
||||||
|
<option value="coil">coil, or digital out (DO)</option>
|
||||||
|
<option value="holding">holding register</option>
|
||||||
|
</options>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="writeTransform" type="text">
|
||||||
|
<label>Write Transform</label>
|
||||||
|
<description><![CDATA[Transformation to apply to received commands.
|
||||||
|
<br /><br />Use "default" to communicate that no transformation is done and value should be passed as is.
|
||||||
|
<br />Use SERVICENAME(ARG) or SERVICENAME:ARG to use transformation service.
|
||||||
|
<br />Any other value than the above types will be interpreted as static text, in which case the actual content of the command
|
||||||
|
<br />You can chain many transformations with ∩, for example SERVICE1:ARG1∩SERVICE2:ARG2
|
||||||
|
value is ignored.]]></description>
|
||||||
|
<default>default</default>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="writeValueType" type="text">
|
||||||
|
<label>Write Value Type</label>
|
||||||
|
<description><![CDATA[How data is written to modbus. Only applicable to registers, you can leave this undefined for coil.
|
||||||
|
<br /><br />Negative integers are encoded with two's complement, while positive integers are encoded as is.
|
||||||
|
]]>
|
||||||
|
</description>
|
||||||
|
<options>
|
||||||
|
<option value="int64">64bit positive or negative integer, 4 registers (int64, uint64)</option>
|
||||||
|
<option value="int64_swap">64bit positive or negative integer, 4 registers but with 16bit words/registers in reverse
|
||||||
|
order (dcba)
|
||||||
|
(int64_swap, uint64_swap)</option>
|
||||||
|
<option value="float32">32bit floating point (float32)</option>
|
||||||
|
<option value="float32_swap">32bit floating point, 16bit words swapped (float32_swap)</option>
|
||||||
|
<option value="int32">32bit positive or negative integer, 2 registers (int32, uint32)</option>
|
||||||
|
<option value="int32_swap">32bit positive or negative integer, 2 registers but with 16bit words/registers in reverse
|
||||||
|
order (ba)
|
||||||
|
(int32_swap, uint32_swap)</option>
|
||||||
|
<option value="int16">16bit positive or negative integer, 1 register (int16, uint16)</option>
|
||||||
|
<option value="bit">individual bit (bit)</option>
|
||||||
|
</options>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="writeMultipleEvenWithSingleRegisterOrCoil" type="boolean">
|
||||||
|
<label>Write Multiple Even with Single Register or Coil</label>
|
||||||
|
<default>false</default>
|
||||||
|
<description><![CDATA[Whether single register / coil of data is written using FC16 ("Write Multiple Holding Registers") / FC15 ("Write Multiple Coils"), respectively.
|
||||||
|
<br />
|
||||||
|
<br />If false, FC6/FC5 are used with single register and single coil, respectively.]]></description>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="writeMaxTries" type="integer" min="1">
|
||||||
|
<label>Maximum Tries When Writing</label>
|
||||||
|
<default>3</default>
|
||||||
|
<description>Number of tries when writing data, if some of the writes fail. For single try, enter 1.</description>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="updateUnchangedValuesEveryMillis" type="integer" min="0" unit="ms">
|
||||||
|
<label>Interval for Updating Unchanged Values</label>
|
||||||
|
<default>1000</default>
|
||||||
|
<description>Interval to update unchanged values. Normally unchanged values are not updated. In milliseconds.</description>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
|
</config-description>
|
||||||
|
</thing-type>
|
||||||
|
</thing:thing-descriptions>
|
@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<thing:thing-descriptions bindingId="modbus"
|
||||||
|
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="switch-type">
|
||||||
|
<item-type>Switch</item-type>
|
||||||
|
<label>Value as Switch</label>
|
||||||
|
<description>Switch item channel</description>
|
||||||
|
</channel-type>
|
||||||
|
<channel-type id="contact-type">
|
||||||
|
<item-type>Contact</item-type>
|
||||||
|
<label>Value as Contact</label>
|
||||||
|
<description>Contact item channel</description>
|
||||||
|
</channel-type>
|
||||||
|
<channel-type id="datetime-type">
|
||||||
|
<item-type>DateTime</item-type>
|
||||||
|
<label>Value as DateTime</label>
|
||||||
|
<description>DateTime item channel</description>
|
||||||
|
</channel-type>
|
||||||
|
<channel-type id="dimmer-type">
|
||||||
|
<item-type>Dimmer</item-type>
|
||||||
|
<label>Value as Dimmer</label>
|
||||||
|
<description>Dimmer item channel</description>
|
||||||
|
</channel-type>
|
||||||
|
<channel-type id="rollershutter-type">
|
||||||
|
<item-type>Rollershutter</item-type>
|
||||||
|
<label>Value as Rollershutter</label>
|
||||||
|
<description>Rollershutter item channel</description>
|
||||||
|
</channel-type>
|
||||||
|
<channel-type id="string-type">
|
||||||
|
<item-type>String</item-type>
|
||||||
|
<label>Value as String</label>
|
||||||
|
<description>String item channel</description>
|
||||||
|
</channel-type>
|
||||||
|
<channel-type id="number-type">
|
||||||
|
<item-type>Number</item-type>
|
||||||
|
<label>Value as Number</label>
|
||||||
|
<description>Number item channel</description>
|
||||||
|
<config-description></config-description>
|
||||||
|
</channel-type>
|
||||||
|
<channel-type id="last-successful-read-type">
|
||||||
|
<item-type>DateTime</item-type>
|
||||||
|
<label>Last Successful Read</label>
|
||||||
|
<description>Date of last read</description>
|
||||||
|
<config-description></config-description>
|
||||||
|
</channel-type>
|
||||||
|
<channel-type id="last-erroring-read-type">
|
||||||
|
<item-type>DateTime</item-type>
|
||||||
|
<label>Last Erroring Read</label>
|
||||||
|
<description>Date of last read error</description>
|
||||||
|
<config-description></config-description>
|
||||||
|
</channel-type>
|
||||||
|
<channel-type id="last-successful-write-type">
|
||||||
|
<item-type>DateTime</item-type>
|
||||||
|
<label>Last Successful Write</label>
|
||||||
|
<description>Date of last write</description>
|
||||||
|
<config-description></config-description>
|
||||||
|
</channel-type>
|
||||||
|
<channel-type id="last-erroring-write-type">
|
||||||
|
<item-type>DateTime</item-type>
|
||||||
|
<label>Last Erroring Write</label>
|
||||||
|
<description>Date of last write error</description>
|
||||||
|
<config-description></config-description>
|
||||||
|
</channel-type>
|
||||||
|
</thing:thing-descriptions>
|
@ -0,0 +1,171 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.*;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class AtomicStampedKeyValueTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInitWithNullValue() {
|
||||||
|
assertThrows(NullPointerException.class, () -> new AtomicStampedValue<>(0, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetters() {
|
||||||
|
Object val = new Object();
|
||||||
|
AtomicStampedValue<Object> keyValue = new AtomicStampedValue<>(42L, val);
|
||||||
|
assertThat(keyValue.getStamp(), is(equalTo(42L)));
|
||||||
|
assertThat(keyValue.getValue(), is(equalTo(val)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdateWithSameStamp() {
|
||||||
|
Object val = new Object();
|
||||||
|
AtomicStampedValue<Object> keyValue = new AtomicStampedValue<>(42L, val);
|
||||||
|
keyValue.update(42L, new Object());
|
||||||
|
assertThat(keyValue.getStamp(), is(equalTo(42L)));
|
||||||
|
assertThat(keyValue.getValue(), is(not(equalTo(val))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdateWithDifferentStamp() {
|
||||||
|
Object val = new Object();
|
||||||
|
AtomicStampedValue<Object> keyValue = new AtomicStampedValue<>(42L, val);
|
||||||
|
keyValue.update(-99L, new Object());
|
||||||
|
assertThat(keyValue.getStamp(), is(equalTo(-99L)));
|
||||||
|
assertThat(keyValue.getValue(), is(not(equalTo(val))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCopy() {
|
||||||
|
Object val = new Object();
|
||||||
|
AtomicStampedValue<Object> keyValue = new AtomicStampedValue<>(42L, val);
|
||||||
|
AtomicStampedValue<Object> copy = keyValue.copy();
|
||||||
|
|
||||||
|
// unchanged
|
||||||
|
assertThat(keyValue.getStamp(), is(equalTo(42L)));
|
||||||
|
assertThat(keyValue.getValue(), is(equalTo(val)));
|
||||||
|
|
||||||
|
// data matches
|
||||||
|
assertThat(keyValue.getStamp(), is(equalTo(copy.getStamp())));
|
||||||
|
assertThat(keyValue.getValue(), is(equalTo(copy.getValue())));
|
||||||
|
|
||||||
|
// after update they live life of their own
|
||||||
|
Object val2 = new Object();
|
||||||
|
copy.update(-99L, val2);
|
||||||
|
|
||||||
|
assertThat(keyValue.getStamp(), is(equalTo(42L)));
|
||||||
|
assertThat(keyValue.getValue(), is(equalTo(val)));
|
||||||
|
|
||||||
|
assertThat(copy.getStamp(), is(equalTo(-99L)));
|
||||||
|
assertThat(copy.getValue(), is(equalTo(val2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* instance(stamp=x).copyIfStampAfter(x)
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testCopyIfStampAfterEqual() {
|
||||||
|
Object val = new Object();
|
||||||
|
AtomicStampedValue<Object> keyValue = new AtomicStampedValue<>(42L, val);
|
||||||
|
AtomicStampedValue<Object> copy = keyValue.copyIfStampAfter(42L);
|
||||||
|
|
||||||
|
// keyValue unchanged
|
||||||
|
assertThat(keyValue.getStamp(), is(equalTo(42L)));
|
||||||
|
assertThat(keyValue.getValue(), is(equalTo(val)));
|
||||||
|
|
||||||
|
// data matches
|
||||||
|
assertThat(keyValue.getStamp(), is(equalTo(copy.getStamp())));
|
||||||
|
assertThat(keyValue.getValue(), is(equalTo(copy.getValue())));
|
||||||
|
|
||||||
|
// after update they live life of their own
|
||||||
|
Object val2 = new Object();
|
||||||
|
copy.update(-99L, val2);
|
||||||
|
|
||||||
|
assertThat(keyValue.getStamp(), is(equalTo(42L)));
|
||||||
|
assertThat(keyValue.getValue(), is(equalTo(val)));
|
||||||
|
|
||||||
|
assertThat(copy.getStamp(), is(equalTo(-99L)));
|
||||||
|
assertThat(copy.getValue(), is(equalTo(val2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* instance(stamp=x-1).copyIfStampAfter(x)
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testCopyIfStampAfterTooOld() {
|
||||||
|
Object val = new Object();
|
||||||
|
AtomicStampedValue<Object> keyValue = new AtomicStampedValue<>(42L, val);
|
||||||
|
AtomicStampedValue<Object> copy = keyValue.copyIfStampAfter(43L);
|
||||||
|
|
||||||
|
// keyValue unchanged
|
||||||
|
assertThat(keyValue.getStamp(), is(equalTo(42L)));
|
||||||
|
assertThat(keyValue.getValue(), is(equalTo(val)));
|
||||||
|
|
||||||
|
// copy is null
|
||||||
|
assertThat(copy, is(nullValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* instance(stamp=x).copyIfStampAfter(x-1)
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testCopyIfStampAfterFresh() {
|
||||||
|
Object val = new Object();
|
||||||
|
AtomicStampedValue<Object> keyValue = new AtomicStampedValue<>(42L, val);
|
||||||
|
AtomicStampedValue<Object> copy = keyValue.copyIfStampAfter(41L);
|
||||||
|
|
||||||
|
// keyValue unchanged
|
||||||
|
assertThat(keyValue.getStamp(), is(equalTo(42L)));
|
||||||
|
assertThat(keyValue.getValue(), is(equalTo(val)));
|
||||||
|
|
||||||
|
// data matches
|
||||||
|
assertThat(keyValue.getStamp(), is(equalTo(copy.getStamp())));
|
||||||
|
assertThat(keyValue.getValue(), is(equalTo(copy.getValue())));
|
||||||
|
|
||||||
|
// after update they live life of their own
|
||||||
|
Object val2 = new Object();
|
||||||
|
copy.update(-99L, val2);
|
||||||
|
|
||||||
|
assertThat(keyValue.getStamp(), is(equalTo(42L)));
|
||||||
|
assertThat(keyValue.getValue(), is(equalTo(val)));
|
||||||
|
|
||||||
|
assertThat(copy.getStamp(), is(equalTo(-99L)));
|
||||||
|
assertThat(copy.getValue(), is(equalTo(val2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCompare() {
|
||||||
|
// equal, smaller, larger
|
||||||
|
assertThat(AtomicStampedValue.compare(new AtomicStampedValue<>(42L, ""), new AtomicStampedValue<>(42L, "")),
|
||||||
|
is(equalTo(0)));
|
||||||
|
assertThat(AtomicStampedValue.compare(new AtomicStampedValue<>(41L, ""), new AtomicStampedValue<>(42L, "")),
|
||||||
|
is(equalTo(-1)));
|
||||||
|
assertThat(AtomicStampedValue.compare(new AtomicStampedValue<>(42L, ""), new AtomicStampedValue<>(41L, "")),
|
||||||
|
is(equalTo(1)));
|
||||||
|
|
||||||
|
// Nulls come first
|
||||||
|
assertThat(AtomicStampedValue.compare(null, new AtomicStampedValue<>(42L, "")), is(equalTo(-1)));
|
||||||
|
assertThat(AtomicStampedValue.compare(new AtomicStampedValue<>(42L, ""), null), is(equalTo(1)));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.osgi.framework.BundleContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class CascadedValueTransformationImplTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformation() {
|
||||||
|
CascadedValueTransformationImpl transformation = new CascadedValueTransformationImpl(
|
||||||
|
"REGEX(myregex:foo(.*))∩REG_(EX(myregex:foo(.*))∩JIHAA:test");
|
||||||
|
assertEquals(3, transformation.getTransformations().size());
|
||||||
|
assertEquals("REGEX", transformation.getTransformations().get(0).transformationServiceName);
|
||||||
|
assertEquals("myregex:foo(.*)", transformation.getTransformations().get(0).transformationServiceParam);
|
||||||
|
|
||||||
|
assertEquals("REG_", transformation.getTransformations().get(1).transformationServiceName);
|
||||||
|
assertEquals("EX(myregex:foo(.*)", transformation.getTransformations().get(1).transformationServiceParam);
|
||||||
|
|
||||||
|
assertEquals("JIHAA", transformation.getTransformations().get(2).transformationServiceName);
|
||||||
|
assertEquals("test", transformation.getTransformations().get(2).transformationServiceParam);
|
||||||
|
|
||||||
|
assertEquals(3, transformation.toString().split("∩").length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformationEmpty() {
|
||||||
|
CascadedValueTransformationImpl transformation = new CascadedValueTransformationImpl("");
|
||||||
|
assertFalse(transformation.isIdentityTransform());
|
||||||
|
assertEquals("", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformationNull() {
|
||||||
|
CascadedValueTransformationImpl transformation = new CascadedValueTransformationImpl(null);
|
||||||
|
assertFalse(transformation.isIdentityTransform());
|
||||||
|
assertEquals("", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformationDefault() {
|
||||||
|
CascadedValueTransformationImpl transformation = new CascadedValueTransformationImpl("deFault");
|
||||||
|
assertTrue(transformation.isIdentityTransform());
|
||||||
|
assertEquals("xx", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformationDefaultChained() {
|
||||||
|
CascadedValueTransformationImpl transformation = new CascadedValueTransformationImpl("deFault∩DEFAULT∩default");
|
||||||
|
assertTrue(transformation.isIdentityTransform());
|
||||||
|
assertEquals("xx", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformationDefaultChainedWithStatic() {
|
||||||
|
CascadedValueTransformationImpl transformation = new CascadedValueTransformationImpl(
|
||||||
|
"deFault∩DEFAULT∩default∩static");
|
||||||
|
assertFalse(transformation.isIdentityTransform());
|
||||||
|
assertEquals("static", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.osgi.framework.BundleContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class SingleValueTransformationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformationOldStyle() {
|
||||||
|
SingleValueTransformation transformation = new SingleValueTransformation("REGEX(myregex:foo(.*))");
|
||||||
|
assertEquals("REGEX", transformation.transformationServiceName);
|
||||||
|
assertEquals("myregex:foo(.*)", transformation.transformationServiceParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformationOldStyle2() {
|
||||||
|
SingleValueTransformation transformation = new SingleValueTransformation("REG_(EX(myregex:foo(.*))");
|
||||||
|
assertEquals("REG_", transformation.transformationServiceName);
|
||||||
|
assertEquals("EX(myregex:foo(.*)", transformation.transformationServiceParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformationNewStyle() {
|
||||||
|
SingleValueTransformation transformation = new SingleValueTransformation("REGEX:myregex(.*)");
|
||||||
|
assertEquals("REGEX", transformation.transformationServiceName);
|
||||||
|
assertEquals("myregex(.*)", transformation.transformationServiceParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformationNewStyle2() {
|
||||||
|
SingleValueTransformation transformation = new SingleValueTransformation("REGEX::myregex(.*)");
|
||||||
|
assertEquals("REGEX", transformation.transformationServiceName);
|
||||||
|
assertEquals(":myregex(.*)", transformation.transformationServiceParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformationEmpty() {
|
||||||
|
SingleValueTransformation transformation = new SingleValueTransformation("");
|
||||||
|
assertFalse(transformation.isIdentityTransform());
|
||||||
|
assertEquals("", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformationNull() {
|
||||||
|
SingleValueTransformation transformation = new SingleValueTransformation(null);
|
||||||
|
assertFalse(transformation.isIdentityTransform());
|
||||||
|
assertEquals("", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformationDefault() {
|
||||||
|
SingleValueTransformation transformation = new SingleValueTransformation("deFault");
|
||||||
|
assertTrue(transformation.isIdentityTransform());
|
||||||
|
assertEquals("xx", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformationDefaultChainedWithStatic() {
|
||||||
|
SingleValueTransformation transformation = new SingleValueTransformation("static");
|
||||||
|
assertFalse(transformation.isIdentityTransform());
|
||||||
|
assertEquals("static", transformation.transform(Mockito.mock(BundleContext.class), "xx"));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,296 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.sbus.internal.profiles;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.junit.jupiter.api.Assumptions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.EmptySource;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.junit.jupiter.params.provider.NullSource;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.openhab.core.config.core.Configuration;
|
||||||
|
import org.openhab.core.library.types.DecimalType;
|
||||||
|
import org.openhab.core.library.types.OnOffType;
|
||||||
|
import org.openhab.core.library.types.QuantityType;
|
||||||
|
import org.openhab.core.thing.profiles.ProfileCallback;
|
||||||
|
import org.openhab.core.thing.profiles.ProfileContext;
|
||||||
|
import org.openhab.core.types.Command;
|
||||||
|
import org.openhab.core.types.RefreshType;
|
||||||
|
import org.openhab.core.types.State;
|
||||||
|
import org.openhab.core.types.Type;
|
||||||
|
import org.openhab.core.types.UnDefType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Ciprian Pascu - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ModbusGainOffsetProfileTest {
|
||||||
|
|
||||||
|
static Stream<Arguments> provideArgsForBoth() {
|
||||||
|
return Stream.of(
|
||||||
|
// dimensionless
|
||||||
|
Arguments.of("100", "0.5", "250", "175.0"), Arguments.of("0", "1 %", "250", "250 %"),
|
||||||
|
//
|
||||||
|
// gain with same unit
|
||||||
|
//
|
||||||
|
// e.g. (handler) 3 <---> (item) 106K with pre-gain-offset=50, gain=2K
|
||||||
|
// e.g. (handler) 3 K <---> (item) 106K^2 with pre-gain-offset=50K, gain=2K
|
||||||
|
//
|
||||||
|
Arguments.of("50", "2 K", "3", "106 K"),
|
||||||
|
//
|
||||||
|
// gain with different unit
|
||||||
|
//
|
||||||
|
Arguments.of("50", "2 m/s", "3", "106 m/s"),
|
||||||
|
//
|
||||||
|
// gain without unit
|
||||||
|
//
|
||||||
|
Arguments.of("50", "2", "3", "106"),
|
||||||
|
//
|
||||||
|
// temperature tests
|
||||||
|
//
|
||||||
|
// celsius gain
|
||||||
|
Arguments.of("0", "0.1 °C", "25", "2.5 °C"),
|
||||||
|
// kelvin gain
|
||||||
|
Arguments.of("0", "0.1 K", "25", "2.5 K"),
|
||||||
|
// fahrenheit gain
|
||||||
|
Arguments.of("0", "10 °F", "0.18", "1.80 °F"),
|
||||||
|
//
|
||||||
|
// unsupported types are passed with error
|
||||||
|
Arguments.of("0", "0", OnOffType.ON, OnOffType.ON)
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Stream<Arguments> provideAdditionalArgsForStateUpdateFromHandler() {
|
||||||
|
return Stream.of(
|
||||||
|
// Dimensionless conversion 2.5/1% = 250%/1% = 250
|
||||||
|
Arguments.of("0", "1 %", "250", "250 %"), Arguments.of("2 %", "1 %", "249.9800", "250.0000 %"),
|
||||||
|
Arguments.of("50", "2 m/s", new DecimalType("3"), "106 m/s"),
|
||||||
|
// UNDEF passes the profile unchanged
|
||||||
|
Arguments.of("0", "0", UnDefType.UNDEF, UnDefType.UNDEF));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Test profile behaviour when handler updates the state
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource({ "provideArgsForBoth", "provideAdditionalArgsForStateUpdateFromHandler" })
|
||||||
|
public void testOnStateUpdateFromHandler(String preGainOffset, String gain, Object updateFromHandlerObj,
|
||||||
|
Object expectedUpdateTowardsItemObj) {
|
||||||
|
testOnUpdateFromHandlerGeneric(preGainOffset, gain, updateFromHandlerObj, expectedUpdateTowardsItemObj, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Test profile behaviour when handler sends command
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource({ "provideArgsForBoth", "provideAdditionalArgsForStateUpdateFromHandler" })
|
||||||
|
public void testOnCommandFromHandler(String preGainOffset, String gain, Object updateFromHandlerObj,
|
||||||
|
Object expectedUpdateTowardsItemObj) {
|
||||||
|
// UNDEF is not a command, cannot be sent by handler
|
||||||
|
assumeTrue(updateFromHandlerObj != UnDefType.UNDEF);
|
||||||
|
testOnUpdateFromHandlerGeneric(preGainOffset, gain, updateFromHandlerObj, expectedUpdateTowardsItemObj, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Test profile behaviour when handler updates the state
|
||||||
|
*
|
||||||
|
* @param preGainOffset profile pre-gain-offset offset
|
||||||
|
* @param gain profile gain
|
||||||
|
* @param updateFromHandlerObj state update from handler. String representing QuantityType or State/Command
|
||||||
|
* @param expectedUpdateTowardsItemObj expected state/command update towards item. String representing QuantityType
|
||||||
|
* or
|
||||||
|
* State
|
||||||
|
* @param stateUpdateFromHandler whether there is state update from handler. Otherwise command
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
private void testOnUpdateFromHandlerGeneric(String preGainOffset, String gain, Object updateFromHandlerObj,
|
||||||
|
Object expectedUpdateTowardsItemObj, boolean stateUpdateFromHandler) {
|
||||||
|
ProfileCallback callback = mock(ProfileCallback.class);
|
||||||
|
ModbusGainOffsetProfile profile = createProfile(callback, gain, preGainOffset);
|
||||||
|
|
||||||
|
final Type actualStateUpdateTowardsItem;
|
||||||
|
if (stateUpdateFromHandler) {
|
||||||
|
final State updateFromHandler;
|
||||||
|
if (updateFromHandlerObj instanceof String str) {
|
||||||
|
updateFromHandler = new QuantityType(str);
|
||||||
|
} else {
|
||||||
|
assertTrue(updateFromHandlerObj instanceof State);
|
||||||
|
updateFromHandler = (State) updateFromHandlerObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
profile.onStateUpdateFromHandler(updateFromHandler);
|
||||||
|
|
||||||
|
ArgumentCaptor<State> capture = ArgumentCaptor.forClass(State.class);
|
||||||
|
verify(callback, times(1)).sendUpdate(capture.capture());
|
||||||
|
actualStateUpdateTowardsItem = capture.getValue();
|
||||||
|
} else {
|
||||||
|
final Command updateFromHandler;
|
||||||
|
if (updateFromHandlerObj instanceof String str) {
|
||||||
|
updateFromHandler = new QuantityType(str);
|
||||||
|
} else {
|
||||||
|
assertTrue(updateFromHandlerObj instanceof State);
|
||||||
|
updateFromHandler = (Command) updateFromHandlerObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
profile.onCommandFromHandler(updateFromHandler);
|
||||||
|
|
||||||
|
ArgumentCaptor<Command> capture = ArgumentCaptor.forClass(Command.class);
|
||||||
|
verify(callback, times(1)).sendCommand(capture.capture());
|
||||||
|
actualStateUpdateTowardsItem = capture.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
Type expectedStateUpdateTowardsItem = (expectedUpdateTowardsItemObj instanceof String s) ? new QuantityType(s)
|
||||||
|
: (Type) expectedUpdateTowardsItemObj;
|
||||||
|
assertEquals(expectedStateUpdateTowardsItem, actualStateUpdateTowardsItem);
|
||||||
|
verifyNoMoreInteractions(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Stream<Arguments> provideAdditionalArgsForCommandFromItem() {
|
||||||
|
return Stream.of(
|
||||||
|
// Dimensionless conversion 2.5/1% = 250%/1% = 250
|
||||||
|
// gain in %, command as bare ratio and the other way around
|
||||||
|
Arguments.of("0", "1 %", "250", "2.5"), Arguments.of("2%", "1 %", "249.9800", "2.5"),
|
||||||
|
|
||||||
|
// celsius gain, kelvin command
|
||||||
|
Arguments.of("0", "0.1 °C", "-2706.5", "2.5 K"),
|
||||||
|
|
||||||
|
// incompatible command unit, should be convertible with gain
|
||||||
|
Arguments.of("0", "0.1 °C", null, "2.5 m/s"),
|
||||||
|
//
|
||||||
|
// incompatible offset unit
|
||||||
|
//
|
||||||
|
Arguments.of("50 K", "21", null, "30 m/s"), Arguments.of("50 m/s", "21", null, "30 K"),
|
||||||
|
//
|
||||||
|
// UNDEF command is not processed
|
||||||
|
//
|
||||||
|
Arguments.of("0", "0", null, UnDefType.UNDEF),
|
||||||
|
//
|
||||||
|
// REFRESH command is forwarded
|
||||||
|
//
|
||||||
|
Arguments.of("0", "0", RefreshType.REFRESH, RefreshType.REFRESH)
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Test profile behavior when item receives command
|
||||||
|
*
|
||||||
|
* @param preGainOffset profile pre-gain-offset
|
||||||
|
* @param gain profile gain
|
||||||
|
* @param expectedCommandTowardsHandlerObj expected command towards handler. String representing QuantityType or
|
||||||
|
* Command. Use null to verify that no commands are sent to handler.
|
||||||
|
* @param commandFromItemObj command that item receives. String representing QuantityType or Command.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({ "rawtypes" })
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource({ "provideArgsForBoth", "provideAdditionalArgsForCommandFromItem" })
|
||||||
|
public void testOnCommandFromItem(String preGainOffset, String gain,
|
||||||
|
@Nullable Object expectedCommandTowardsHandlerObj, Object commandFromItemObj) {
|
||||||
|
assumeFalse(commandFromItemObj.equals(UnDefType.UNDEF));
|
||||||
|
ProfileCallback callback = mock(ProfileCallback.class);
|
||||||
|
ModbusGainOffsetProfile profile = createProfile(callback, gain, preGainOffset);
|
||||||
|
|
||||||
|
Command commandFromItem = (commandFromItemObj instanceof String str) ? new QuantityType(str)
|
||||||
|
: (Command) commandFromItemObj;
|
||||||
|
profile.onCommandFromItem(commandFromItem);
|
||||||
|
|
||||||
|
boolean callsExpected = expectedCommandTowardsHandlerObj != null;
|
||||||
|
if (callsExpected) {
|
||||||
|
ArgumentCaptor<Command> capture = ArgumentCaptor.forClass(Command.class);
|
||||||
|
verify(callback, times(1)).handleCommand(capture.capture());
|
||||||
|
Command actualCommandTowardsHandler = capture.getValue();
|
||||||
|
Command expectedCommandTowardsHandler = (expectedCommandTowardsHandlerObj instanceof String str)
|
||||||
|
? new QuantityType(str)
|
||||||
|
: (Command) expectedCommandTowardsHandlerObj;
|
||||||
|
assertEquals(expectedCommandTowardsHandler, actualCommandTowardsHandler);
|
||||||
|
verifyNoMoreInteractions(callback);
|
||||||
|
} else {
|
||||||
|
verifyNoInteractions(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Test behaviour when item receives state update from item (no-op)
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
@Test
|
||||||
|
public void testOnCommandFromItem() {
|
||||||
|
ProfileCallback callback = mock(ProfileCallback.class);
|
||||||
|
ModbusGainOffsetProfile<?> profile = createProfile(callback, "1.0", "0.0");
|
||||||
|
|
||||||
|
profile.onStateUpdateFromItem(new DecimalType(3.78));
|
||||||
|
// should be no-op
|
||||||
|
verifyNoInteractions(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidInit() {
|
||||||
|
// preGainOffset must be dimensionless
|
||||||
|
ProfileCallback callback = mock(ProfileCallback.class);
|
||||||
|
ModbusGainOffsetProfile<?> profile = createProfile(callback, "1.0", "0.0 K");
|
||||||
|
assertFalse(profile.isValid());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@NullSource
|
||||||
|
@EmptySource
|
||||||
|
public void testInitGainDefault(String gain) {
|
||||||
|
ProfileCallback callback = mock(ProfileCallback.class);
|
||||||
|
ModbusGainOffsetProfile<?> p = createProfile(callback, gain, "0.0");
|
||||||
|
assertTrue(p.isValid());
|
||||||
|
assertEquals(p.getGain(), Optional.of(QuantityType.ONE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@NullSource
|
||||||
|
@EmptySource
|
||||||
|
public void testInitOffsetDefault(String preGainOffset) {
|
||||||
|
ProfileCallback callback = mock(ProfileCallback.class);
|
||||||
|
ModbusGainOffsetProfile<?> p = createProfile(callback, "1", preGainOffset);
|
||||||
|
assertTrue(p.isValid());
|
||||||
|
assertEquals(p.getPregainOffset(), Optional.of(QuantityType.ZERO));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModbusGainOffsetProfile<?> createProfile(ProfileCallback callback, @Nullable String gain,
|
||||||
|
@Nullable String preGainOffset) {
|
||||||
|
ProfileContext context = mock(ProfileContext.class);
|
||||||
|
Configuration config = new Configuration();
|
||||||
|
if (gain != null) {
|
||||||
|
config.put("gain", gain);
|
||||||
|
}
|
||||||
|
if (preGainOffset != null) {
|
||||||
|
config.put("pre-gain-offset", preGainOffset);
|
||||||
|
}
|
||||||
|
when(context.getConfiguration()).thenReturn(config);
|
||||||
|
|
||||||
|
return new ModbusGainOffsetProfile<>(callback, context);
|
||||||
|
}
|
||||||
|
}
|
@ -97,67 +97,67 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.grpc</groupId>
|
<groupId>io.grpc</groupId>
|
||||||
<artifactId>grpc-api</artifactId>
|
<artifactId>grpc-api</artifactId>
|
||||||
<version>1.53.0</version>
|
<version>1.60.1</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.grpc</groupId>
|
<groupId>io.grpc</groupId>
|
||||||
<artifactId>grpc-protobuf</artifactId>
|
<artifactId>grpc-protobuf</artifactId>
|
||||||
<version>1.53.0</version>
|
<version>1.60.1</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.grpc</groupId>
|
<groupId>io.grpc</groupId>
|
||||||
<artifactId>grpc-protobuf-lite</artifactId>
|
<artifactId>grpc-protobuf-lite</artifactId>
|
||||||
<version>1.53.0</version>
|
<version>1.60.1</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.grpc</groupId>
|
<groupId>io.grpc</groupId>
|
||||||
<artifactId>grpc-alts</artifactId>
|
<artifactId>grpc-alts</artifactId>
|
||||||
<version>1.53.0</version>
|
<version>1.60.1</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.grpc</groupId>
|
<groupId>io.grpc</groupId>
|
||||||
<artifactId>grpc-core</artifactId>
|
<artifactId>grpc-core</artifactId>
|
||||||
<version>1.53.0</version>
|
<version>1.60.1</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.grpc</groupId>
|
<groupId>io.grpc</groupId>
|
||||||
<artifactId>grpc-grpclb</artifactId>
|
<artifactId>grpc-grpclb</artifactId>
|
||||||
<version>1.53.0</version>
|
<version>1.60.1</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.grpc</groupId>
|
<groupId>io.grpc</groupId>
|
||||||
<artifactId>grpc-auth</artifactId>
|
<artifactId>grpc-auth</artifactId>
|
||||||
<version>1.53.0</version>
|
<version>1.60.1</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.grpc</groupId>
|
<groupId>io.grpc</groupId>
|
||||||
<artifactId>grpc-context</artifactId>
|
<artifactId>grpc-context</artifactId>
|
||||||
<version>1.53.0</version>
|
<version>1.60.1</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.grpc</groupId>
|
<groupId>io.grpc</groupId>
|
||||||
<artifactId>grpc-netty-shaded</artifactId>
|
<artifactId>grpc-netty-shaded</artifactId>
|
||||||
<version>1.53.0</version>
|
<version>1.60.1</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.grpc</groupId>
|
<groupId>io.grpc</groupId>
|
||||||
<artifactId>grpc-xds</artifactId>
|
<artifactId>grpc-xds</artifactId>
|
||||||
<version>1.53.0</version>
|
<version>1.60.1</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.grpc</groupId>
|
<groupId>io.grpc</groupId>
|
||||||
<artifactId>grpc-services</artifactId>
|
<artifactId>grpc-services</artifactId>
|
||||||
<version>1.53.0</version>
|
<version>1.60.1</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
@ -333,6 +333,7 @@
|
|||||||
<module>org.openhab.binding.sagercaster</module>
|
<module>org.openhab.binding.sagercaster</module>
|
||||||
<module>org.openhab.binding.samsungtv</module>
|
<module>org.openhab.binding.samsungtv</module>
|
||||||
<module>org.openhab.binding.satel</module>
|
<module>org.openhab.binding.satel</module>
|
||||||
|
<module>org.openhab.binding.sbus</module>
|
||||||
<module>org.openhab.binding.semsportal</module>
|
<module>org.openhab.binding.semsportal</module>
|
||||||
<module>org.openhab.binding.senechome</module>
|
<module>org.openhab.binding.senechome</module>
|
||||||
<module>org.openhab.binding.seneye</module>
|
<module>org.openhab.binding.seneye</module>
|
||||||
|
@ -48,6 +48,7 @@
|
|||||||
<include name="*/src/main/feature/feature.xml"/>
|
<include name="*/src/main/feature/feature.xml"/>
|
||||||
<exclude name="**/org.openhab.binding.bluetooth*/**/feature.xml"/>
|
<exclude name="**/org.openhab.binding.bluetooth*/**/feature.xml"/>
|
||||||
<exclude name="**/org.openhab.binding.modbus*/**/feature.xml"/>
|
<exclude name="**/org.openhab.binding.modbus*/**/feature.xml"/>
|
||||||
|
<exclude name="**/org.openhab.binding.sbus*/**/feature.xml"/>
|
||||||
<exclude name="**/org.openhab.binding.mqtt*/**/feature.xml"/>
|
<exclude name="**/org.openhab.binding.mqtt*/**/feature.xml"/>
|
||||||
</fileset>
|
</fileset>
|
||||||
<filterchain>
|
<filterchain>
|
||||||
@ -73,6 +74,7 @@
|
|||||||
<features>
|
<features>
|
||||||
<feature>openhab-binding-bluetooth</feature>
|
<feature>openhab-binding-bluetooth</feature>
|
||||||
<feature>openhab-binding-modbus</feature>
|
<feature>openhab-binding-modbus</feature>
|
||||||
|
<feature>openhab-binding-sbus</feature>
|
||||||
<feature>openhab-binding-mqtt</feature>
|
<feature>openhab-binding-mqtt</feature>
|
||||||
</features>
|
</features>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
@ -38,5 +38,9 @@
|
|||||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.modbus.studer/${project.version}</bundle>
|
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.modbus.studer/${project.version}</bundle>
|
||||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.modbus.sunspec/${project.version}</bundle>
|
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.modbus.sunspec/${project.version}</bundle>
|
||||||
</feature>
|
</feature>
|
||||||
|
<feature name="openhab-binding-sbus" description="S-Bus Binding" version="${project.version}">
|
||||||
|
<feature>openhab-runtime-base</feature>
|
||||||
|
<feature>openhab-transport-sbus</feature>
|
||||||
|
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.sbus/${project.version}</bundle>
|
||||||
|
</feature>
|
||||||
</features>
|
</features>
|
||||||
|
@ -4,4 +4,4 @@ checkstyle.forbiddenPackageUsageCheck.forbiddenPackages=com.fazecast.jSerialComm
|
|||||||
checkstyle.forbiddenPackageUsageCheck.exceptions=
|
checkstyle.forbiddenPackageUsageCheck.exceptions=
|
||||||
checkstyle.requiredFilesCheck.files=pom.xml
|
checkstyle.requiredFilesCheck.files=pom.xml
|
||||||
checkstyle.karafAddonFeatureCheck.featureNameMappings=-transform-:-transformation-,-io-:-misc-
|
checkstyle.karafAddonFeatureCheck.featureNameMappings=-transform-:-transformation-,-io-:-misc-
|
||||||
checkstyle.karafAddonFeatureCheck.excludeAddonPatterns=org.openhab.persistence.jdbc.*,org.openhab.binding.modbus.*
|
checkstyle.karafAddonFeatureCheck.excludeAddonPatterns=org.openhab.persistence.jdbc.*,org.openhab.binding.modbus.*,org.openhab.binding.sbus.*
|
||||||
|
Loading…
Reference in New Issue
Block a user