added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.modbus</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,145 @@
# 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.modbus</artifactId>
<version>${project.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.io.transport.modbus</artifactId>
<version>${project.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.openhab.osgiify</groupId>
<artifactId>net.wimpi.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)
2. Download [diagslave](https://www.modbusdriver.com/diagslave.html) and start modbus serial slave up using this command:
```
./diagslave -m rtu -a 1 -b 38400 -d 8 -s 1 -p none -4 10 /dev/pts/7
```
3. Configure openHAB's modbus slave to connect to `/dev/pts/8`.
4. Modify `start.sh` or `start_debug.sh` to include the unconventional port name by adding the following argument to `java`:
```
-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 TCP Implementation
1. Download [diagslave](https://www.modbusdriver.com/diagslave.html) and start modbus tcp server (slave) using this command:
```
./diagslave -m tcp -a 1 -p 55502
```
2. Configure openHAB's modbus slave to connect to `127.0.0.1:55502`.
## 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:
```bash
# write value=5 to holding register 40001 (index=0 in the binding)
./modpoll -m tcp -a 1 -r 1 -t4 -p 502 127.0.0.1 5
# set coil 00001 (index=0 in the binding) to TRUE
./modpoll -m tcp -a 1 -r 1 -t0 -p 502 127.0.0.1 1
# write float32
./modpoll -m tcp -a 1 -r 1 -t4:float -p 502 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.
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 TCP 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 by creating a `BasicPollTaskImpl` and submitting it using the `ModbusManager` `submitOneTimePoll` and `registerRegularPoll` methods.
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.
### 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(immediate = true)
@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,26 @@
<?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 http://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>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.modbus</artifactId>
<name>openHAB Add-ons :: Bundles :: Modbus Binding</name>
<dependencies>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.io.transport.modbus</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.modbus-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>file:${basedirRoot}/bundles/org.openhab.io.transport.modbus/target/feature/feature.xml</repository>
<feature name="openhab-binding-modbus" description="Modbus Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-modbus</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.modbus/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ModbusBinding} class defines some constants
* public that might be used from other bundles as well.
*
* @author Sami Salonen - Initial contribution
* @author Nagy Attila Gabor - Split the original ModbusBindingConstants in two
*/
@NonNullByDefault
public class ModbusBindingConstants {
public static final String BINDING_ID = "modbus";
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.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
*/
public 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.
*/
public void discoveryFinished();
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.discovery;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.modbus.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
*/
public Set<ThingTypeUID> getSupportedThingTypeUIDs();
/**
* Start an asynchronous discovery process of a Modbus endpoint
*
* @param handler the endpoint that should be discovered
*/
public void startDiscovery(ModbusEndpointThingHandler handler, ModbusDiscoveryListener listener);
}

View File

@@ -0,0 +1,188 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.discovery.internal;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.modbus.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 (TCP 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(immediate = true, 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 handler the Modbus bridge handler
*/
@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 handler the handler that has been removed
*/
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-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.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.modbus.discovery.ModbusDiscoveryListener;
import org.openhab.binding.modbus.discovery.ModbusDiscoveryParticipant;
import org.openhab.binding.modbus.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) {
this.handler = (ModbusEndpointThingHandler) handler;
}
}
@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-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.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
*/
public 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
*/
public boolean scanInProgress();
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Signals that {@link ModbusEndpointThingHandler} is not properly initialized yet, and the requested operation cannot
* be completed.
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class EndpointNotInitializedException extends Exception {
private static final long serialVersionUID = -6721646244844348903L;
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.registry.Identifiable;
import org.openhab.core.thing.ThingUID;
import org.openhab.io.transport.modbus.ModbusCommunicationInterface;
/**
* Base interface for thing handlers of endpoint things
*
* @author Sami Salonen - 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
*/
public @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
*/
public int getSlaveId() throws EndpointNotInitializedException;
/**
* Return true if auto discovery is enabled for this endpoint
*
* @return boolean true if the discovery is enabled
*/
public boolean isDiscoveryEnabled();
}

View File

@@ -0,0 +1,442 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.handler;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.modbus.internal.AtomicStampedValue;
import org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal;
import org.openhab.binding.modbus.internal.config.ModbusPollerConfiguration;
import org.openhab.binding.modbus.internal.handler.ModbusDataThingHandler;
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.openhab.io.transport.modbus.AsyncModbusFailure;
import org.openhab.io.transport.modbus.AsyncModbusReadResult;
import org.openhab.io.transport.modbus.ModbusCommunicationInterface;
import org.openhab.io.transport.modbus.ModbusConstants;
import org.openhab.io.transport.modbus.ModbusFailureCallback;
import org.openhab.io.transport.modbus.ModbusReadCallback;
import org.openhab.io.transport.modbus.ModbusReadFunctionCode;
import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint;
import org.openhab.io.transport.modbus.PollTask;
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 Sami Salonen - 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 Sami Salonen
*
*/
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) {
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 final static List<String> SORTED_READ_FUNCTION_CODES = ModbusBindingConstantsInternal.READ_FUNCTION_CODES
.keySet().stream().sorted().collect(Collectors.toList());
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 @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) {
ModbusEndpointThingHandler slaveEndpoint = (ModbusEndpointThingHandler) handler;
return slaveEndpoint;
} 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,
StringUtils.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;
}
/**
* 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.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) {
this.childCallbacks.add((ModbusDataThingHandler) childHandler);
}
}
@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;
}
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);
}
}
}
}

View File

@@ -0,0 +1,137 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.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 Sami Salonen - 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
* @throws CloneNotSupportedException
*/
@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,77 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal;
import static org.openhab.binding.modbus.ModbusBindingConstants.BINDING_ID;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.io.transport.modbus.ModbusReadFunctionCode;
/**
* The {@link ModbusBinding} class defines common constants, which are
* used across the whole binding.
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class ModbusBindingConstantsInternal {
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_MODBUS_TCP = new ThingTypeUID(BINDING_ID, "tcp");
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-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception for binding configuration exceptions
*
* @author Sami Salonen - 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,96 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal;
import static org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal.*;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.modbus.handler.ModbusPollerThingHandler;
import org.openhab.binding.modbus.internal.handler.ModbusDataThingHandler;
import org.openhab.binding.modbus.internal.handler.ModbusSerialThingHandler;
import org.openhab.binding.modbus.internal.handler.ModbusTcpThingHandler;
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.openhab.io.transport.modbus.ModbusManager;
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 Sami Salonen - 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_TCP);
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_TCP)) {
logger.debug("createHandler Modbus tcp");
return new ModbusTcpThingHandler((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,223 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal;
import static org.apache.commons.lang.StringUtils.isEmpty;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.StandardToStringStyle;
import org.apache.commons.lang.builder.ToStringBuilder;
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.State;
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 Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class Transformation {
public static final String TRANSFORM_DEFAULT = "default";
public static final Transformation IDENTITY_TRANSFORMATION = new Transformation(TRANSFORM_DEFAULT, null, null);
/** RegEx to extract and parse a function String <code>'(.*?)\((.*)\)'</code> */
private static final Pattern EXTRACT_FUNCTION_PATTERN = 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(Transformation.class);
private static StandardToStringStyle toStringStyle = new StandardToStringStyle();
static {
toStringStyle.setUseShortClassName(true);
}
private final @Nullable String transformation;
private final @Nullable String transformationServiceName;
private 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 Transformation(@Nullable String transformation) {
this.transformation = transformation;
//
// Parse transformation configuration here on construction, but delay the
// construction of TransformationService to call-time
if (isEmpty(transformation) || transformation.equalsIgnoreCase(TRANSFORM_DEFAULT)) {
// no-op (identity) transformation
transformationServiceName = null;
transformationServiceParam = null;
} else {
Matcher matcher = EXTRACT_FUNCTION_PATTERN.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
*/
Transformation(String transformation, @Nullable String transformationServiceName,
@Nullable String transformationServiceParam) {
this.transformation = transformation;
this.transformationServiceName = transformationServiceName;
this.transformationServiceParam = transformationServiceParam;
}
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;
}
public boolean isIdentityTransform() {
return TRANSFORM_DEFAULT.equalsIgnoreCase(this.transformation);
}
public static Optional<Command> tryConvertToCommand(String transformed) {
Optional<Command> transformedCommand = Optional.ofNullable(TypeParser.parseCommand(DEFAULT_TYPES, transformed));
return transformedCommand;
}
/**
* Transform state to another state using this transformation
*
* @param context
* @param types types to used to parse the transformation result
* @param command
* @return Transformed command, or null if no transformation was possible
*/
public @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);
}
public boolean hasTransformationService() {
return transformationServiceName != null;
}
@Override
public boolean equals(@Nullable Object obj) {
if (null == obj) {
return false;
}
if (this == obj) {
return true;
}
if (!(obj instanceof Transformation)) {
return false;
}
Transformation that = (Transformation) obj;
EqualsBuilder eb = new EqualsBuilder();
if (hasTransformationService()) {
eb.append(this.transformationServiceName, that.transformationServiceName);
eb.append(this.transformationServiceParam, that.transformationServiceParam);
} else {
eb.append(this.transformation, that.transformation);
}
return eb.isEquals();
}
@Override
public String toString() {
return new ToStringBuilder(this, toStringStyle).append("tranformation", transformation)
.append("transformationServiceName", transformationServiceName)
.append("transformationServiceParam", transformationServiceParam).toString();
}
}

View File

@@ -0,0 +1,117 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Configuration for data thing
*
* @author Sami Salonen - 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-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Configuration for poller thing
*
* @author Sami Salonen - 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,161 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Configuration for serial thing
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusSerialConfiguration {
private @Nullable String port;
private int id;
private int baud;
private @Nullable String stopBits;
private @Nullable String parity;
private int dataBits;
private @Nullable String encoding;
private boolean echo;
private int receiveTimeoutMillis;
private @Nullable String flowControlIn;
private @Nullable String flowControlOut;
private int timeBetweenTransactionsMillis;
private int connectMaxTries;
private int connectTimeoutMillis;
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 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 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,107 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Configuration for tcp thing
*
* @author Sami Salonen - Initial contribution
*
*/
@NonNullByDefault
public class ModbusTcpConfiguration {
private @Nullable String host;
private int port;
private int id;
private int timeBetweenTransactionsMillis;
private int timeBetweenReconnectMillis;
private int connectMaxTries;
private int reconnectAfterMillis;
private int connectTimeoutMillis;
private boolean enableDiscovery;
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 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 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-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler;
import org.openhab.binding.modbus.internal.ModbusConfigurationException;
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.openhab.io.transport.modbus.ModbusCommunicationInterface;
import org.openhab.io.transport.modbus.ModbusManager;
import org.openhab.io.transport.modbus.endpoint.EndpointPoolConfiguration;
import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for Modbus Slave endpoint thing handlers
*
* @author Sami Salonen - 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 @Nullable 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,962 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal.handler;
import static org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal.*;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.NotImplementedException;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler;
import org.openhab.binding.modbus.handler.ModbusPollerThingHandler;
import org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal;
import org.openhab.binding.modbus.internal.ModbusConfigurationException;
import org.openhab.binding.modbus.internal.Transformation;
import org.openhab.binding.modbus.internal.config.ModbusDataConfiguration;
import org.openhab.core.library.items.ContactItem;
import org.openhab.core.library.items.DateTimeItem;
import org.openhab.core.library.items.DimmerItem;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.items.RollershutterItem;
import org.openhab.core.library.items.StringItem;
import org.openhab.core.library.items.SwitchItem;
import org.openhab.core.library.types.DateTimeType;
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.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.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.openhab.io.transport.modbus.AsyncModbusFailure;
import org.openhab.io.transport.modbus.AsyncModbusReadResult;
import org.openhab.io.transport.modbus.AsyncModbusWriteResult;
import org.openhab.io.transport.modbus.BitArray;
import org.openhab.io.transport.modbus.ModbusBitUtilities;
import org.openhab.io.transport.modbus.ModbusCommunicationInterface;
import org.openhab.io.transport.modbus.ModbusConstants;
import org.openhab.io.transport.modbus.ModbusConstants.ValueType;
import org.openhab.io.transport.modbus.ModbusReadFunctionCode;
import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint;
import org.openhab.io.transport.modbus.ModbusRegisterArray;
import org.openhab.io.transport.modbus.ModbusWriteCoilRequestBlueprint;
import org.openhab.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
import org.openhab.io.transport.modbus.ModbusWriteRequestBlueprint;
import org.openhab.io.transport.modbus.exception.ModbusConnectionException;
import org.openhab.io.transport.modbus.exception.ModbusTransportException;
import org.openhab.io.transport.modbus.json.WriteRequestJsonUtilities;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ModbusDataThingHandler} is responsible for interpreting polled modbus data, as well as handling openHAB
* commands
*
* Thing can be re-initialized by the bridge in case of configuration changes (bridgeStatusChanged).
* Because of this, initialize, dispose and all callback methods (onRegisters, onBits, onError, onWriteResponse) are
* synchronized
* to avoid data race conditions.
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class ModbusDataThingHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(ModbusDataThingHandler.class);
private final BundleContext bundleContext;
private static final Duration MIN_STATUS_INFO_UPDATE_INTERVAL = Duration.ofSeconds(1);
private static final Map<String, List<Class<? extends State>>> CHANNEL_ID_TO_ACCEPTED_TYPES = new HashMap<>();
static {
CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_SWITCH,
new SwitchItem("").getAcceptedDataTypes());
CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_CONTACT,
new ContactItem("").getAcceptedDataTypes());
CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_DATETIME,
new DateTimeItem("").getAcceptedDataTypes());
CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_DIMMER,
new DimmerItem("").getAcceptedDataTypes());
CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_NUMBER,
new NumberItem("").getAcceptedDataTypes());
CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_STRING,
new StringItem("").getAcceptedDataTypes());
CHANNEL_ID_TO_ACCEPTED_TYPES.put(ModbusBindingConstantsInternal.CHANNEL_ROLLERSHUTTER,
new RollershutterItem("").getAcceptedDataTypes());
}
// data channels + 4 for read/write last error/success
private static final int NUMER_OF_CHANNELS_HINT = CHANNEL_ID_TO_ACCEPTED_TYPES.size() + 4;
//
// If you change the below default/initial values, please update the corresponding values in dispose()
//
private volatile @Nullable ModbusDataConfiguration config;
private volatile @Nullable ValueType readValueType;
private volatile @Nullable ValueType writeValueType;
private volatile @Nullable Transformation readTransformation;
private volatile @Nullable Transformation writeTransformation;
private volatile Optional<Integer> readIndex = Optional.empty();
private volatile Optional<Integer> readSubIndex = Optional.empty();
private volatile @Nullable Integer writeStart;
private volatile int pollStart;
private volatile int slaveId;
private volatile @Nullable ModbusReadFunctionCode functionCode;
private volatile @Nullable ModbusReadRequestBlueprint readRequest;
private volatile long updateUnchangedValuesEveryMillis;
private volatile @NonNullByDefault({}) ModbusCommunicationInterface comms;
private volatile boolean isWriteEnabled;
private volatile boolean isReadEnabled;
private volatile boolean writeParametersHavingTransformationOnly;
private volatile boolean childOfEndpoint;
private volatile @Nullable ModbusPollerThingHandler pollerHandler;
private volatile Map<String, ChannelUID> channelCache = new HashMap<>();
private volatile Map<ChannelUID, Long> channelLastUpdated = new HashMap<>(NUMER_OF_CHANNELS_HINT);
private volatile Map<ChannelUID, State> channelLastState = new HashMap<>(NUMER_OF_CHANNELS_HINT);
private volatile LocalDateTime lastStatusInfoUpdate = LocalDateTime.MIN;
private volatile ThingStatusInfo statusInfo = new ThingStatusInfo(ThingStatus.UNKNOWN, ThingStatusDetail.NONE,
null);
public ModbusDataThingHandler(Thing thing) {
super(thing);
this.bundleContext = FrameworkUtil.getBundle(ModbusDataThingHandler.class).getBundleContext();
}
@Override
public synchronized void handleCommand(ChannelUID channelUID, Command command) {
logger.trace("Thing {} '{}' received command '{}' to channel '{}'", getThing().getUID(), getThing().getLabel(),
command, channelUID);
ModbusDataConfiguration config = this.config;
if (config == null) {
return;
}
if (RefreshType.REFRESH == command) {
ModbusPollerThingHandler poller = pollerHandler;
if (poller == null) {
// Data thing must be child of endpoint, and thus write-only.
// There is no data to update
return;
}
// We *schedule* the REFRESH to avoid dead-lock situation where poller is trying update this
// data thing with cached data (resulting in deadlock in two synchronized methods: this (handleCommand) and
// onRegisters.
scheduler.schedule(() -> poller.refresh(), 0, TimeUnit.SECONDS);
return;
} else if (hasConfigurationError()) {
logger.debug(
"Thing {} '{}' command '{}' to channel '{}': Thing has configuration error so ignoring the command",
getThing().getUID(), getThing().getLabel(), command, channelUID);
return;
} else if (!isWriteEnabled) {
logger.debug(
"Thing {} '{}' command '{}' to channel '{}': no writing configured -> aborting processing command",
getThing().getUID(), getThing().getLabel(), command, channelUID);
return;
}
Optional<Command> transformedCommand = transformCommandAndProcessJSON(channelUID, command);
if (transformedCommand == null) {
// We have, JSON as transform output (which has been processed) or some error. See
// transformCommandAndProcessJSON javadoc
return;
}
// We did not have JSON output from the transformation, so writeStart is absolute required. Abort if it is
// missing
Integer writeStart = this.writeStart;
if (writeStart == null) {
logger.debug(
"Thing {} '{}': not processing command {} since writeStart is missing and transformation output is not a JSON",
getThing().getUID(), getThing().getLabel(), command);
return;
}
if (!transformedCommand.isPresent()) {
// transformation failed, return
logger.warn("Cannot process command {} (of type {}) with channel {} since transformation was unsuccessful",
command, command.getClass().getSimpleName(), channelUID);
return;
}
ModbusWriteRequestBlueprint request = requestFromCommand(channelUID, command, config, transformedCommand.get(),
writeStart);
if (request == null) {
return;
}
logger.trace("Submitting write task {} to endpoint {}", request, comms.getEndpoint());
comms.submitOneTimeWrite(request, this::onWriteResponse, this::handleWriteError);
}
/**
* Transform received command using the transformation.
*
* In case of JSON as transformation output, the output processed using {@link processJsonTransform}.
*
* @param channelUID channel UID corresponding to received command
* @param command command to be transformed
* @return transformed command. Null is returned with JSON transformation outputs and configuration errors
*
* @see processJsonTransform
*/
private @Nullable Optional<Command> transformCommandAndProcessJSON(ChannelUID channelUID, Command command) {
String transformOutput;
Optional<Command> transformedCommand;
Transformation writeTransformation = this.writeTransformation;
if (writeTransformation == null || writeTransformation.isIdentityTransform()) {
transformedCommand = Optional.of(command);
} else {
transformOutput = writeTransformation.transform(bundleContext, command.toString());
if (transformOutput.contains("[")) {
processJsonTransform(command, transformOutput);
return null;
} else if (writeParametersHavingTransformationOnly) {
updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
"Seems to have writeTransformation but no other write parameters. Since the transformation did not return a JSON for command '%s' (channel %s), this is a configuration error",
command, channelUID));
return null;
} else {
transformedCommand = Transformation.tryConvertToCommand(transformOutput);
logger.trace("Converted transform output '{}' to command '{}' (type {})", transformOutput,
transformedCommand.map(c -> c.toString()).orElse("<conversion failed>"),
transformedCommand.map(c -> c.getClass().getName()).orElse("<conversion failed>"));
}
}
return transformedCommand;
}
private @Nullable ModbusWriteRequestBlueprint requestFromCommand(ChannelUID channelUID, Command origCommand,
ModbusDataConfiguration config, Command transformedCommand, Integer writeStart) {
ModbusWriteRequestBlueprint request;
boolean writeMultiple = config.isWriteMultipleEvenWithSingleRegisterOrCoil();
String writeType = config.getWriteType();
if (writeType == null) {
return null;
}
if (writeType.equals(WRITE_TYPE_COIL)) {
Optional<Boolean> commandAsBoolean = ModbusBitUtilities.translateCommand2Boolean(transformedCommand);
if (!commandAsBoolean.isPresent()) {
logger.warn(
"Cannot process command {} with channel {} since command is not OnOffType, OpenClosedType or Decimal trying to write to coil. Do not know how to convert to 0/1. Transformed command was '{}'",
origCommand, channelUID, transformedCommand);
return null;
}
boolean data = commandAsBoolean.get();
request = new ModbusWriteCoilRequestBlueprint(slaveId, writeStart, data, writeMultiple,
config.getWriteMaxTries());
} else if (writeType.equals(WRITE_TYPE_HOLDING)) {
ValueType writeValueType = this.writeValueType;
if (writeValueType == null) {
// Should not happen in practice, since we are not in configuration error (checked above)
// This will make compiler happy anyways with the null checks
logger.warn("Received command but write value type not set! Ignoring command");
return null;
}
ModbusRegisterArray data = ModbusBitUtilities.commandToRegisters(transformedCommand, writeValueType);
writeMultiple = writeMultiple || data.size() > 1;
request = new ModbusWriteRegisterRequestBlueprint(slaveId, writeStart, data, writeMultiple,
config.getWriteMaxTries());
} else {
// Should not happen! This method is not called in case configuration errors and writeType is validated
// already in initialization (validateAndParseWriteParameters).
// We keep this here for future-proofing the code (new writeType values)
throw new NotImplementedException(String.format(
"writeType does not equal %s or %s and thus configuration is invalid. Should not end up this far with configuration error.",
WRITE_TYPE_COIL, WRITE_TYPE_HOLDING));
}
return request;
}
private void processJsonTransform(Command command, String transformOutput) {
ModbusCommunicationInterface localComms = this.comms;
if (localComms == null) {
return;
}
Collection<ModbusWriteRequestBlueprint> requests;
try {
requests = WriteRequestJsonUtilities.fromJson(slaveId, transformOutput);
} catch (IllegalArgumentException | IllegalStateException e) {
logger.warn(
"Thing {} '{}' could handle transformation result '{}'. Original command {}. Error details follow",
getThing().getUID(), getThing().getLabel(), transformOutput, command, e);
return;
}
requests.stream().forEach(request -> {
logger.trace("Submitting write request: {} to endpoint {} (based from transformation {})", request,
localComms.getEndpoint(), transformOutput);
localComms.submitOneTimeWrite(request, this::onWriteResponse, this::handleWriteError);
});
}
@Override
public synchronized void initialize() {
// Initialize the thing. If done set status to ONLINE to indicate proper working.
// Long running initialization should be done asynchronously in background.
try {
logger.trace("initialize() of thing {} '{}' starting", thing.getUID(), thing.getLabel());
ModbusDataConfiguration localConfig = config = getConfigAs(ModbusDataConfiguration.class);
updateUnchangedValuesEveryMillis = localConfig.getUpdateUnchangedValuesEveryMillis();
Bridge bridge = getBridge();
if (bridge == null || !bridge.getStatus().equals(ThingStatus.ONLINE)) {
logger.debug("Thing {} '{}' has no bridge or it is not online", getThing().getUID(),
getThing().getLabel());
updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "No online bridge");
return;
}
BridgeHandler bridgeHandler = bridge.getHandler();
if (bridgeHandler == null) {
logger.warn("Bridge {} '{}' has no handler.", bridge.getUID(), bridge.getLabel());
String errmsg = String.format("Bridge %s '%s' configuration incomplete or with errors", bridge.getUID(),
bridge.getLabel());
throw new ModbusConfigurationException(errmsg);
}
if (bridgeHandler instanceof ModbusEndpointThingHandler) {
// Write-only thing, parent is endpoint
ModbusEndpointThingHandler endpointHandler = (ModbusEndpointThingHandler) bridgeHandler;
slaveId = endpointHandler.getSlaveId();
comms = endpointHandler.getCommunicationInterface();
childOfEndpoint = true;
functionCode = null;
readRequest = null;
} else {
ModbusPollerThingHandler localPollerHandler = (ModbusPollerThingHandler) bridgeHandler;
pollerHandler = localPollerHandler;
ModbusReadRequestBlueprint localReadRequest = localPollerHandler.getRequest();
if (localReadRequest == null) {
logger.debug(
"Poller {} '{}' has no read request -- configuration is changing or bridge having invalid configuration?",
bridge.getUID(), bridge.getLabel());
updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
String.format("Poller %s '%s' has no poll task", bridge.getUID(), bridge.getLabel()));
return;
}
readRequest = localReadRequest;
slaveId = localReadRequest.getUnitID();
functionCode = localReadRequest.getFunctionCode();
comms = localPollerHandler.getCommunicationInterface();
pollStart = localReadRequest.getReference();
childOfEndpoint = false;
}
validateAndParseReadParameters(localConfig);
validateAndParseWriteParameters(localConfig);
validateMustReadOrWrite();
updateStatusIfChanged(ThingStatus.ONLINE);
} catch (ModbusConfigurationException | EndpointNotInitializedException e) {
logger.debug("Thing {} '{}' initialization error: {}", getThing().getUID(), getThing().getLabel(),
e.getMessage());
updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
} finally {
logger.trace("initialize() of thing {} '{}' finished", thing.getUID(), thing.getLabel());
}
}
@Override
public synchronized void dispose() {
config = null;
readValueType = null;
writeValueType = null;
readTransformation = null;
writeTransformation = null;
readIndex = Optional.empty();
readSubIndex = Optional.empty();
writeStart = null;
pollStart = 0;
slaveId = 0;
comms = null;
functionCode = null;
readRequest = null;
isWriteEnabled = false;
isReadEnabled = false;
writeParametersHavingTransformationOnly = false;
childOfEndpoint = false;
pollerHandler = null;
channelCache = new HashMap<>();
lastStatusInfoUpdate = LocalDateTime.MIN;
statusInfo = new ThingStatusInfo(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, null);
channelLastUpdated = new HashMap<>(NUMER_OF_CHANNELS_HINT);
channelLastState = new HashMap<>(NUMER_OF_CHANNELS_HINT);
}
@Override
public synchronized void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
logger.debug("bridgeStatusChanged for {}. Reseting handler", this.getThing().getUID());
this.dispose();
this.initialize();
}
private boolean hasConfigurationError() {
ThingStatusInfo statusInfo = getThing().getStatusInfo();
return statusInfo.getStatus() == ThingStatus.OFFLINE
&& statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
}
private void validateMustReadOrWrite() throws ModbusConfigurationException {
if (!isReadEnabled && !isWriteEnabled) {
throw new ModbusConfigurationException("Should try to read or write data!");
}
}
private void validateAndParseReadParameters(ModbusDataConfiguration config) throws ModbusConfigurationException {
ModbusReadFunctionCode functionCode = this.functionCode;
boolean readingDiscreteOrCoil = functionCode == ModbusReadFunctionCode.READ_COILS
|| functionCode == ModbusReadFunctionCode.READ_INPUT_DISCRETES;
boolean readStartMissing = StringUtils.isBlank(config.getReadStart());
boolean readValueTypeMissing = StringUtils.isBlank(config.getReadValueType());
if (childOfEndpoint && readRequest == null) {
if (!readStartMissing || !readValueTypeMissing) {
String errmsg = String.format(
"Thing %s readStart=%s, and readValueType=%s were specified even though the data thing is child of endpoint (that is, write-only)!",
getThing().getUID(), config.getReadStart(), config.getReadValueType());
throw new ModbusConfigurationException(errmsg);
}
}
// we assume readValueType=bit by default if it is missing
boolean allMissingOrAllPresent = (readStartMissing && readValueTypeMissing)
|| (!readStartMissing && (!readValueTypeMissing || readingDiscreteOrCoil));
if (!allMissingOrAllPresent) {
String errmsg = String.format(
"Thing %s readStart=%s, and readValueType=%s should be all present or all missing!",
getThing().getUID(), config.getReadStart(), config.getReadValueType());
throw new ModbusConfigurationException(errmsg);
} else if (!readStartMissing) {
// all read values are present
isReadEnabled = true;
if (readingDiscreteOrCoil && readValueTypeMissing) {
readValueType = ModbusConstants.ValueType.BIT;
} else {
try {
readValueType = ValueType.fromConfigValue(config.getReadValueType());
} catch (IllegalArgumentException e) {
String errmsg = String.format("Thing %s readValueType=%s is invalid!", getThing().getUID(),
config.getReadValueType());
throw new ModbusConfigurationException(errmsg);
}
}
if (readingDiscreteOrCoil && !ModbusConstants.ValueType.BIT.equals(readValueType)) {
String errmsg = String.format(
"Thing %s invalid readValueType: Only readValueType='%s' (or undefined) supported with coils or discrete inputs. Value type was: %s",
getThing().getUID(), ModbusConstants.ValueType.BIT, config.getReadValueType());
throw new ModbusConfigurationException(errmsg);
}
} else {
isReadEnabled = false;
}
if (isReadEnabled) {
String readStart = config.getReadStart();
if (readStart == null) {
throw new ModbusConfigurationException(
String.format("Thing %s invalid readStart: %s", getThing().getUID(), config.getReadStart()));
}
String[] readParts = readStart.split("\\.", 2);
try {
readIndex = Optional.of(Integer.parseInt(readParts[0]));
if (readParts.length == 2) {
readSubIndex = Optional.of(Integer.parseInt(readParts[1]));
} else {
readSubIndex = Optional.empty();
}
} catch (IllegalArgumentException e) {
String errmsg = String.format("Thing %s invalid readStart: %s", getThing().getUID(),
config.getReadStart());
throw new ModbusConfigurationException(errmsg);
}
}
readTransformation = new Transformation(config.getReadTransform());
validateReadIndex();
}
private void validateAndParseWriteParameters(ModbusDataConfiguration config) throws ModbusConfigurationException {
boolean writeTypeMissing = StringUtils.isBlank(config.getWriteType());
boolean writeStartMissing = StringUtils.isBlank(config.getWriteStart());
boolean writeValueTypeMissing = StringUtils.isBlank(config.getWriteValueType());
boolean writeTransformationMissing = StringUtils.isBlank(config.getWriteTransform());
writeTransformation = new Transformation(config.getWriteTransform());
boolean writingCoil = WRITE_TYPE_COIL.equals(config.getWriteType());
writeParametersHavingTransformationOnly = (writeTypeMissing && writeStartMissing && writeValueTypeMissing
&& !writeTransformationMissing);
boolean allMissingOrAllPresentOrOnlyNonDefaultTransform = //
// read-only thing, no write specified
(writeTypeMissing && writeStartMissing && writeValueTypeMissing)
// mandatory write parameters provided. With coils one can drop value type
|| (!writeTypeMissing && !writeStartMissing && (!writeValueTypeMissing || writingCoil))
// only transformation provided
|| writeParametersHavingTransformationOnly;
if (!allMissingOrAllPresentOrOnlyNonDefaultTransform) {
String errmsg = String.format(
"writeType=%s, writeStart=%s, and writeValueType=%s should be all present, or all missing! Alternatively, you can provide just writeTransformation, and use transformation returning JSON.",
config.getWriteType(), config.getWriteStart(), config.getWriteValueType());
throw new ModbusConfigurationException(errmsg);
} else if (!writeTypeMissing || writeParametersHavingTransformationOnly) {
isWriteEnabled = true;
// all write values are present
if (!writeParametersHavingTransformationOnly && !WRITE_TYPE_HOLDING.equals(config.getWriteType())
&& !WRITE_TYPE_COIL.equals(config.getWriteType())) {
String errmsg = String.format("Invalid writeType=%s. Expecting %s or %s!", config.getWriteType(),
WRITE_TYPE_HOLDING, WRITE_TYPE_COIL);
throw new ModbusConfigurationException(errmsg);
}
final ValueType localWriteValueType;
if (writeParametersHavingTransformationOnly) {
// Placeholder for further checks
localWriteValueType = writeValueType = ModbusConstants.ValueType.INT16;
} else if (writingCoil && writeValueTypeMissing) {
localWriteValueType = writeValueType = ModbusConstants.ValueType.BIT;
} else {
try {
localWriteValueType = writeValueType = ValueType.fromConfigValue(config.getWriteValueType());
} catch (IllegalArgumentException e) {
String errmsg = String.format("Invalid writeValueType=%s!", config.getWriteValueType());
throw new ModbusConfigurationException(errmsg);
}
}
if (writingCoil && !ModbusConstants.ValueType.BIT.equals(localWriteValueType)) {
String errmsg = String.format(
"Invalid writeValueType: Only writeValueType='%s' (or undefined) supported with coils. Value type was: %s",
ModbusConstants.ValueType.BIT, config.getWriteValueType());
throw new ModbusConfigurationException(errmsg);
} else if (!writingCoil && localWriteValueType.getBits() < 16) {
// trying to write holding registers with < 16 bit value types. Not supported
String errmsg = String.format(
"Invalid writeValueType: Only writeValueType with larger or equal to 16 bits are supported holding registers. Value type was: %s",
config.getWriteValueType());
throw new ModbusConfigurationException(errmsg);
}
try {
if (!writeParametersHavingTransformationOnly) {
String localWriteStart = config.getWriteStart();
if (localWriteStart == null) {
String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(),
config.getWriteStart());
throw new ModbusConfigurationException(errmsg);
}
writeStart = Integer.parseInt(localWriteStart.trim());
}
} catch (IllegalArgumentException e) {
String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(),
config.getWriteStart());
throw new ModbusConfigurationException(errmsg);
}
} else {
isWriteEnabled = false;
}
}
private void validateReadIndex() throws ModbusConfigurationException {
@Nullable
ModbusReadRequestBlueprint readRequest = this.readRequest;
ValueType readValueType = this.readValueType;
if (!readIndex.isPresent() || readRequest == null) {
return;
}
assert readValueType != null;
// bits represented by the value type, e.g. int32 -> 32
int valueTypeBitCount = readValueType.getBits();
int dataElementBits;
switch (readRequest.getFunctionCode()) {
case READ_INPUT_REGISTERS:
case READ_MULTIPLE_REGISTERS:
dataElementBits = 16;
break;
case READ_COILS:
case READ_INPUT_DISCRETES:
dataElementBits = 1;
break;
default:
throw new IllegalStateException(readRequest.getFunctionCode().toString());
}
boolean bitQuery = dataElementBits == 1;
if (bitQuery && readSubIndex.isPresent()) {
String errmsg = String.format("readStart=X.Y is not allowed to be used with coils or discrete inputs!");
throw new ModbusConfigurationException(errmsg);
}
if (valueTypeBitCount >= 16 && readSubIndex.isPresent()) {
String errmsg = String
.format("readStart=X.Y is not allowed to be used with value types larger than 16bit!");
throw new ModbusConfigurationException(errmsg);
} else if (!bitQuery && valueTypeBitCount < 16 && !readSubIndex.isPresent()) {
String errmsg = String.format("readStart=X.Y must be used with value types less than 16bit!");
throw new ModbusConfigurationException(errmsg);
} else if (readSubIndex.isPresent() && (readSubIndex.get() + 1) * valueTypeBitCount > 16) {
// the sub index Y (in X.Y) is above the register limits
String errmsg = String.format("readStart=X.Y, the value Y is too large");
throw new ModbusConfigurationException(errmsg);
}
// Determine bit positions polled, both start and end inclusive
int pollStartBitIndex = readRequest.getReference() * dataElementBits;
int pollEndBitIndex = pollStartBitIndex + readRequest.getDataLength() * dataElementBits - 1;
// Determine bit positions read, both start and end inclusive
int readStartBitIndex = readIndex.get() * dataElementBits + readSubIndex.orElse(0) * valueTypeBitCount;
int readEndBitIndex = readStartBitIndex + valueTypeBitCount - 1;
if (readStartBitIndex < pollStartBitIndex || readEndBitIndex > pollEndBitIndex) {
String errmsg = String.format(
"Out-of-bounds: Poller is reading from index %d to %d (inclusive) but this thing configured to read '%s' starting from element %d. Exceeds polled data bounds.",
pollStartBitIndex / dataElementBits, pollEndBitIndex / dataElementBits, readValueType,
readIndex.get());
throw new ModbusConfigurationException(errmsg);
}
}
private boolean containsOnOff(List<Class<? extends State>> channelAcceptedDataTypes) {
return channelAcceptedDataTypes.stream().anyMatch(clz -> {
return clz.equals(OnOffType.class);
});
}
private boolean containsOpenClosed(List<Class<? extends State>> acceptedDataTypes) {
return acceptedDataTypes.stream().anyMatch(clz -> {
return clz.equals(OpenClosedType.class);
});
}
public synchronized void onReadResult(AsyncModbusReadResult result) {
result.getRegisters().ifPresent(registers -> onRegisters(result.getRequest(), registers));
result.getBits().ifPresent(bits -> onBits(result.getRequest(), bits));
}
public synchronized void handleReadError(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
onError(failure.getRequest(), failure.getCause());
}
public synchronized void handleWriteError(AsyncModbusFailure<ModbusWriteRequestBlueprint> failure) {
onError(failure.getRequest(), failure.getCause());
}
private synchronized void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) {
if (hasConfigurationError()) {
return;
} else if (!isReadEnabled) {
return;
}
ValueType readValueType = this.readValueType;
if (readValueType == null) {
return;
}
State numericState;
// extractIndex:
// e.g. with bit, extractIndex=4 means 5th bit (from right) ("10.4" -> 5th bit of register 10, "10.4" -> 5th bit
// of register 10)
// bit of second register)
// e.g. with 8bit integer, extractIndex=3 means high byte of second register
//
// with <16 bit types, this is the index of the N'th 1-bit/8-bit item. Each register has 16/2 items,
// respectively.
// with >=16 bit types, this is index of first register
int extractIndex;
if (readValueType.getBits() >= 16) {
// Invariant, checked in initialize
assert readSubIndex.orElse(0) == 0;
extractIndex = readIndex.get() - pollStart;
} else {
int subIndex = readSubIndex.orElse(0);
int itemsPerRegister = 16 / readValueType.getBits();
extractIndex = (readIndex.get() - pollStart) * itemsPerRegister + subIndex;
}
numericState = ModbusBitUtilities.extractStateFromRegisters(registers, extractIndex, readValueType)
.map(state -> (State) state).orElse(UnDefType.UNDEF);
boolean boolValue = !numericState.equals(DecimalType.ZERO);
Map<ChannelUID, State> values = processUpdatedValue(numericState, boolValue);
logger.debug(
"Thing {} channels updated: {}. readValueType={}, readIndex={}, readSubIndex(or 0)={}, extractIndex={} -> numeric value {} and boolValue={}. Registers {} for request {}",
thing.getUID(), values, readValueType, readIndex, readSubIndex.orElse(0), extractIndex, numericState,
boolValue, registers, request);
}
private synchronized void onBits(ModbusReadRequestBlueprint request, BitArray bits) {
if (hasConfigurationError()) {
return;
} else if (!isReadEnabled) {
return;
}
boolean boolValue = bits.getBit(readIndex.get() - pollStart);
DecimalType numericState = boolValue ? new DecimalType(BigDecimal.ONE) : DecimalType.ZERO;
Map<ChannelUID, State> values = processUpdatedValue(numericState, boolValue);
logger.debug(
"Thing {} channels updated: {}. readValueType={}, readIndex={} -> numeric value {} and boolValue={}. Bits {} for request {}",
thing.getUID(), values, readValueType, readIndex, numericState, boolValue, bits, request);
}
private synchronized void onError(ModbusReadRequestBlueprint request, Exception error) {
if (hasConfigurationError()) {
return;
} else if (!isReadEnabled) {
return;
}
if (error instanceof ModbusConnectionException) {
logger.trace("Thing {} '{}' had {} error on read: {}", getThing().getUID(), getThing().getLabel(),
error.getClass().getSimpleName(), error.toString());
} else if (error instanceof ModbusTransportException) {
logger.trace("Thing {} '{}' had {} error on read: {}", getThing().getUID(), getThing().getLabel(),
error.getClass().getSimpleName(), error.toString());
} else {
logger.error(
"Thing {} '{}' had {} error on read: {} (message: {}). Stack trace follows since this is unexpected error.",
getThing().getUID(), getThing().getLabel(), error.getClass().getName(), error.toString(),
error.getMessage(), error);
}
Map<ChannelUID, State> states = new HashMap<>();
ChannelUID lastReadErrorUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_READ_ERROR);
if (isLinked(lastReadErrorUID)) {
states.put(lastReadErrorUID, new DateTimeType());
}
synchronized (this) {
// Update channels
states.forEach((uid, state) -> {
tryUpdateState(uid, state);
});
updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
String.format("Error (%s) with read. Request: %s. Description: %s. Message: %s",
error.getClass().getSimpleName(), request, error.toString(), error.getMessage()));
}
}
private synchronized void onError(ModbusWriteRequestBlueprint request, Exception error) {
if (hasConfigurationError()) {
return;
} else if (!isWriteEnabled) {
return;
}
if (error instanceof ModbusConnectionException) {
logger.debug("Thing {} '{}' had {} error on write: {}", getThing().getUID(), getThing().getLabel(),
error.getClass().getSimpleName(), error.toString());
} else if (error instanceof ModbusTransportException) {
logger.debug("Thing {} '{}' had {} error on write: {}", getThing().getUID(), getThing().getLabel(),
error.getClass().getSimpleName(), error.toString());
} else {
logger.error(
"Thing {} '{}' had {} error on write: {} (message: {}). Stack trace follows since this is unexpected error.",
getThing().getUID(), getThing().getLabel(), error.getClass().getName(), error.toString(),
error.getMessage(), error);
}
Map<ChannelUID, State> states = new HashMap<>();
ChannelUID lastWriteErrorUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_WRITE_ERROR);
if (isLinked(lastWriteErrorUID)) {
states.put(lastWriteErrorUID, new DateTimeType());
}
synchronized (this) {
// Update channels
states.forEach((uid, state) -> {
tryUpdateState(uid, state);
});
updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
String.format("Error (%s) with write. Request: %s. Description: %s. Message: %s",
error.getClass().getSimpleName(), request, error.toString(), error.getMessage()));
}
}
public synchronized void onWriteResponse(AsyncModbusWriteResult result) {
if (hasConfigurationError()) {
return;
} else if (!isWriteEnabled) {
return;
}
logger.debug("Successful write, matching request {}", result.getRequest());
updateStatusIfChanged(ThingStatus.ONLINE);
ChannelUID lastWriteSuccessUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_WRITE_SUCCESS);
if (isLinked(lastWriteSuccessUID)) {
updateState(lastWriteSuccessUID, new DateTimeType());
}
}
/**
* Update linked channels
*
* @param numericState numeric state corresponding to polled data (or UNDEF with floating point NaN or infinity)
* @param boolValue boolean value corresponding to polled data
* @return updated channel data
*/
private Map<ChannelUID, State> processUpdatedValue(State numericState, boolean boolValue) {
Transformation localReadTransformation = readTransformation;
if (localReadTransformation == null) {
// We should always have transformation available if thing is initalized properly
logger.trace("No transformation available, aborting processUpdatedValue");
return Collections.emptyMap();
}
Map<ChannelUID, State> states = new HashMap<>();
CHANNEL_ID_TO_ACCEPTED_TYPES.keySet().stream().forEach(channelId -> {
ChannelUID channelUID = getChannelUID(channelId);
if (!isLinked(channelUID)) {
return;
}
List<Class<? extends State>> acceptedDataTypes = CHANNEL_ID_TO_ACCEPTED_TYPES.get(channelId);
if (acceptedDataTypes.isEmpty()) {
return;
}
State boolLikeState;
if (containsOnOff(acceptedDataTypes)) {
boolLikeState = boolValue ? OnOffType.ON : OnOffType.OFF;
} else if (containsOpenClosed(acceptedDataTypes)) {
boolLikeState = boolValue ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
} else {
boolLikeState = null;
}
State transformedState;
if (localReadTransformation.isIdentityTransform()) {
if (boolLikeState != null) {
// A bit of smartness for ON/OFF and OPEN/CLOSED with boolean like items
transformedState = boolLikeState;
} else {
// Numeric states always go through transformation. This allows value of 17.5 to be
// converted to
// 17.5% with percent types (instead of raising error)
transformedState = localReadTransformation.transformState(bundleContext, acceptedDataTypes,
numericState);
}
} else {
transformedState = localReadTransformation.transformState(bundleContext, acceptedDataTypes,
numericState);
}
if (transformedState != null) {
logger.trace(
"Channel {} will be updated to '{}' (type {}). Input data: number value {} (value type '{}' taken into account) and bool value {}. Transformation: {}",
channelId, transformedState, transformedState.getClass().getSimpleName(), numericState,
readValueType, boolValue,
localReadTransformation.isIdentityTransform() ? "<identity>" : localReadTransformation);
states.put(channelUID, transformedState);
} else {
String types = StringUtils.join(acceptedDataTypes.stream().map(cls -> cls.getSimpleName()).toArray(),
", ");
logger.warn(
"Channel {} will not be updated since transformation was unsuccessful. Channel is expecting the following data types [{}]. Input data: number value {} (value type '{}' taken into account) and bool value {}. Transformation: {}",
channelId, types, numericState, readValueType, boolValue,
localReadTransformation.isIdentityTransform() ? "<identity>" : localReadTransformation);
}
});
ChannelUID lastReadSuccessUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_READ_SUCCESS);
if (isLinked(lastReadSuccessUID)) {
states.put(lastReadSuccessUID, new DateTimeType());
}
updateExpiredChannels(states);
return states;
}
private void updateExpiredChannels(Map<ChannelUID, State> states) {
synchronized (this) {
updateStatusIfChanged(ThingStatus.ONLINE);
long now = System.currentTimeMillis();
// Update channels that have not been updated in a while, or when their values has changed
states.forEach((uid, state) -> updateExpiredChannel(now, uid, state));
channelLastState = states;
}
}
// since lastState can be null, and "lastState == null" in conditional is not useless
@SuppressWarnings("null")
private void updateExpiredChannel(long now, ChannelUID uid, State state) {
@Nullable
State lastState = channelLastState.get(uid);
long lastUpdatedMillis = channelLastUpdated.getOrDefault(uid, 0L);
long millisSinceLastUpdate = now - lastUpdatedMillis;
if (lastUpdatedMillis <= 0L || lastState == null || updateUnchangedValuesEveryMillis <= 0L
|| millisSinceLastUpdate > updateUnchangedValuesEveryMillis || !lastState.equals(state)) {
tryUpdateState(uid, state);
channelLastUpdated.put(uid, now);
}
}
private void tryUpdateState(ChannelUID uid, State state) {
try {
updateState(uid, state);
} catch (IllegalArgumentException e) {
logger.warn("Error updating state '{}' (type {}) to channel {}: {} {}", state,
Optional.ofNullable(state).map(s -> s.getClass().getName()).orElse("null"), uid,
e.getClass().getName(), e.getMessage());
}
}
private ChannelUID getChannelUID(String channelID) {
return channelCache.computeIfAbsent(channelID, id -> new ChannelUID(getThing().getUID(), id));
}
private void updateStatusIfChanged(ThingStatus status) {
updateStatusIfChanged(status, ThingStatusDetail.NONE, null);
}
private void updateStatusIfChanged(ThingStatus status, ThingStatusDetail statusDetail,
@Nullable String description) {
ThingStatusInfo newStatusInfo = new ThingStatusInfo(status, statusDetail, description);
Duration durationSinceLastUpdate = Duration.between(lastStatusInfoUpdate, LocalDateTime.now());
boolean intervalElapsed = MIN_STATUS_INFO_UPDATE_INTERVAL.minus(durationSinceLastUpdate).isNegative();
if (statusInfo.getStatus() == ThingStatus.UNKNOWN || !statusInfo.equals(newStatusInfo) || intervalElapsed) {
statusInfo = newStatusInfo;
lastStatusInfoUpdate = LocalDateTime.now();
updateStatus(newStatusInfo);
}
}
/**
* Update status using pre-constructed ThingStatusInfo
*
* Implementation adapted from BaseThingHandler updateStatus implementations
*
* @param statusInfo new status info
*/
protected void updateStatus(ThingStatusInfo statusInfo) {
synchronized (this) {
ThingHandlerCallback callback = getCallback();
if (callback != null) {
callback.statusUpdated(this.thing, statusInfo);
} else {
logger.warn("Handler {} tried updating the thing status although the handler was already disposed.",
this.getClass().getSimpleName());
}
}
}
}

View File

@@ -0,0 +1,115 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal.handler;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.modbus.discovery.internal.ModbusEndpointDiscoveryService;
import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
import org.openhab.binding.modbus.internal.ModbusConfigurationException;
import org.openhab.binding.modbus.internal.config.ModbusSerialConfiguration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.io.transport.modbus.ModbusManager;
import org.openhab.io.transport.modbus.endpoint.EndpointPoolConfiguration;
import org.openhab.io.transport.modbus.endpoint.ModbusSerialSlaveEndpoint;
/**
* Endpoint thing handler for serial slaves
*
* @author Sami Salonen - 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.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 ThingUID getUID() {
return getThing().getUID();
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(ModbusEndpointDiscoveryService.class);
}
}

View File

@@ -0,0 +1,105 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal.handler;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.modbus.discovery.internal.ModbusEndpointDiscoveryService;
import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
import org.openhab.binding.modbus.internal.ModbusConfigurationException;
import org.openhab.binding.modbus.internal.config.ModbusTcpConfiguration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.io.transport.modbus.ModbusManager;
import org.openhab.io.transport.modbus.endpoint.EndpointPoolConfiguration;
import org.openhab.io.transport.modbus.endpoint.ModbusTCPSlaveEndpoint;
/**
* Endpoint thing handler for TCP slaves
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class ModbusTcpThingHandler
extends AbstractModbusEndpointThingHandler<ModbusTCPSlaveEndpoint, ModbusTcpConfiguration> {
public ModbusTcpThingHandler(Bridge bridge, ModbusManager manager) {
super(bridge, manager);
}
@Override
protected void configure() throws ModbusConfigurationException {
ModbusTcpConfiguration config = getConfigAs(ModbusTcpConfiguration.class);
String host = config.getHost();
if (host == null) {
throw new ModbusConfigurationException("host must be non-null!");
}
this.config = config;
endpoint = new ModbusTCPSlaveEndpoint(host, config.getPort());
EndpointPoolConfiguration poolConfiguration = new EndpointPoolConfiguration();
this.poolConfiguration = poolConfiguration;
poolConfiguration.setConnectMaxTries(config.getConnectMaxTries());
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 tcp 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 {
ModbusTcpConfiguration localConfig = config;
if (localConfig == null) {
throw new EndpointNotInitializedException();
}
return localConfig.getId();
}
@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 Collections.singleton(ModbusEndpointDiscoveryService.class);
}
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="modbus" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Modbus Binding</name>
<description>Binding for Modbus</description>
<author>Sami Salonen</author>
</binding:binding>

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="tcp"/>
<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,148 @@
<?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="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,71 @@
<?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="tcp">
<label>Modbus TCP Slave</label>
<description>Endpoint for Modbus TCP 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>502</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>
<!-- 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="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,146 @@
<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="tcp"/>
<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 it responsible of
tranlating 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|[1-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) 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.]]></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.]]></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) 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
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,174 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
/**
* @author Sami Salonen - Initial contribution
*/
@RunWith(MockitoJUnitRunner.class)
public class AtomicStampedKeyValueTest {
@Test(expected = NullPointerException.class)
public void testInitWithNullValue() {
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 key = new Object();
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 key2 = new Object();
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 key2 = new Object();
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)));
}
}