S-Bus binding protocol. Derived from Modbus protocol

Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Ciprian Pascu 2024-01-07 11:46:59 +02:00
parent 706ae58323
commit 2ee968f918
51 changed files with 7167 additions and 13 deletions

View 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.

View 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

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

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

View File

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

View File

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

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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.*