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