diff --git a/CODEOWNERS b/CODEOWNERS index 171df689062..45dfce187bf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -215,6 +215,7 @@ /bundles/org.openhab.binding.seneye/ @nikotanghe /bundles/org.openhab.binding.sensebox/ @hakan42 /bundles/org.openhab.binding.sensibo/ @seime +/bundles/org.openhab.binding.serial/ @MikeJMajor /bundles/org.openhab.binding.serialbutton/ @kaikreuzer /bundles/org.openhab.binding.shelly/ @markus7017 /bundles/org.openhab.binding.siemensrds/ @andrewfg diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 25de4404bc0..a64b92e1ca5 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1066,6 +1066,11 @@ org.openhab.binding.sensibo ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.serial + ${project.version} + org.openhab.addons.bundles org.openhab.binding.serialbutton diff --git a/bundles/org.openhab.binding.serial/NOTICE b/bundles/org.openhab.binding.serial/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.serial/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.serial/README.md b/bundles/org.openhab.binding.serial/README.md new file mode 100644 index 00000000000..522cc1f4a2c --- /dev/null +++ b/bundles/org.openhab.binding.serial/README.md @@ -0,0 +1,140 @@ +# Serial Binding + +The Serial binding allows openHAB to communicate over serial ports attached to the openHAB server. + +The binding allows data to be sent and received from a serial port. +The binding does not support any particular serial protocols and simply reads what is available and sends what is provided. + +The binding can be used to communicate with simple serial devices for which a dedicated openHAB binding does not exist. + +## Overview + +The Serial binding represents a serial port as a bridge thing and data matching defined patterns as things connected to the bridge. + +### Serial Bridge + +A Serial Bridge thing (`serialBridge`) represents a single serial port. + +The bridge supports a String channel which is set to the currently received data from the serial port. +Sending a command to this channel sends the command as a string to the serial port. + +The bridge also supports a String channel which encodes the received data as the string representation of a RawType to handle data that is +not supported by the REST interface. +A command sent to this channel will only be sent to the serial port if it is encoded as the string representation of a RawType. + +A trigger channel is also provided which triggers when data is received. + +### Serial Device + +A Serial Device thing (`serialDevice`) can be used to represent data matching a defined pattern as a device. +The serial port may be providing data for many different devices/sensors, such as a temperature sensor or a doorbell. +Usually such devices can be identified by performing a pattern match on the received data. +For example, a Serial Device could be configured to represent a temperature sensor. + +The thing will only update its channels if the received data matches the defined pattern. + +The thing supports generic String and Number channels which can apply a transform on the received data to set the channel state. +Commands sent to the channels can be formatted and transformed before being sent to the device. + +The thing also supports Switch and Rollershutter channels which provide simple mappings for the ON, OFF, UP, DOWN and STOP commands. + +When using a Serial Device the expectation is that the received data for each device is terminated by a line break. + +## Thing Configuration + +The configuration for the `serialBridge` consists of the following parameters: + +| Parameter | Description | +|---------------------|--------------------------------------------------------------------------------------------------------| +| serialPort | The serial port to use (e.g. Linux: /dev/ttyUSB0, Windows: COM1) (mandatory) | +| baudRate | Set the baud rate. Valid values: 4800, 9600, 19200, 38400, 57600, 115200 (default 9600) | +| dataBits | Set the data bits. Valid values: 5, 6, 7, 8 (default 8) | +| parity | Set the parity. Valid values: N(one), O(dd), E(even), M(ark), S(pace) (default N) | +| stopBits | Set the stop bits. Valid values: 1, 1.5, 2 (default 1) | +| charset | The charset to use for converting between bytes and string (e.g. UTF-8,ISO-8859-1) | + +The configuration for the `serialDevice` consists of the following parameters: + +| Parameter | Description | +|---------------------|--------------------------------------------------------------------------------------------------------| +| patternMatch | Regular expression used to identify device from received data (must match the whole line) (mandatory) | + +## Channels + +The channels supported by the `serialBridge` are: + +| Channel | Type | Description | +|----------|------------------|----------------------------------------------------------------------------------------------------------| +| `string` | String | Channel for sending/receiving data as a string to/from the serial port. The channel will update its state to a StringType that is the data received from the serial port. A command sent to this channel will be sent out as data through the serial port. | +| `binary` | String | Channel for sending/receiving data in Base64 format to/from the serial port. The channel will update its state to a StringType which is the string representation of a RawType that contains the data received from the serial port. A command sent to this channel must be encoded as the string representation of a RawType, e.g. `"data:application/octet-stream;base64 MjA7MDU7Q3Jlc3RhO0lEPTI4MDE7VEVNUD0yNTtIVU09NTU7QkFUPU9LOwo="` | +| `data` | system.rawbutton | Trigger which emits `PRESSED` events (no `RELEASED` events) whenever data is available on the serial port | + + +The channels supported by the `serialDevice` are: + +| Channel Type | Type | Description | +|---------------|------------------|----------------------------------------------------------------------------------------------------------| +| `string` | String | Channel for receiving string based commands. The channel can be configured to apply a transform on the received data to convert to the channel state. Commands received by the channel can be formatted and transformed before sending to the device. | +| `number` | Number | Channel for receiving number based commands. The channel can be configured to apply a transform on the received data to convert to the channel state. Commands received by the channel can be formatted and transformed before sending to the device. | +| `dimmer` | Dimmer | Channel for receiving commands from a Dimmer. The channel can be configured to apply a transform on the received data to convert to the channel state. The channel can be configured to apply a simple mapping for the ON, OFF, INCREASE and DECREASE commands. | +| `switch` | Switch | Channel for receiving commands from a Switch. The channel can be configured to apply a transform on the received data to convert to the channel state. The channel can be configured to apply a simple mapping for the ON and OFF commands. | +| `rollershutter` | Rollershutter | Channel for receiving commands from a Rollershutter. The channel can be configured to apply a transform on the received data to convert to the channel state. The channel can be configured to apply a simple mapping for the UP, DOWN and STOP commands. | + +The configuration for the `serialBridge` channels consists of the following parameters: + +| Parameter | Description | Supported Channels | +|------------------|----------------------------------------------------------------------------------------|--------------------| +| `stateTransformation` | One or more transformation (concatenated with `∩`) used to convert device data to channel state, e.g. `REGEX:.*?STATE=(.*?);.*` | string, number, dimmer, switch, rollershutter | +| `commandTransformation` | One or more transformation (concatenated with `∩`) used to convert command to device data, e.g. `JS:device.js` | string, number, dimmer, switch, rollershutter | +| `commandFormat` | Format string applied to the command before transform, e.g. `ID=671;COMMAND=%s` | string, number, dimmer, rollershutter | +| `onValue` | Send this value when receiving an ON command | switch, dimmer | +| `offValue` | Send this value when receiving an OFF command | switch, dimmer | +| `increaseValue` | Send this value when receiving an INCREASE command | dimmer | +| `decreaseValue` | Send this value when receiving a DECREASE command | dimmer | +| `upValue` | Send this value when receiving an UP command | rollershutter | +| `downValue` | Send this value when receiving a DOWN command | rollershutter | +| `stopValue` | Send this value when receiving a STOP command | rollershutter | + + +## Full Example + +The following example is for a device connected to a serial port which provides data for many different sensors and we are interested in the temperature from a particular sensor. + +The data for the sensor of interest is `20;05;Cresta;ID=2801;TEMP=25;HUM=55;BAT=OK;` + +demo.things: + +``` +Bridge serial:serialBridge:sensors [serialPort="/dev/ttyUSB01", baudRate=57600] { + Thing serialDevice temperatureSensor [patternMatch="20;05;Cresta;ID=2801;.*"] { + Channels: + Type number : temperature [transform="REGEX:.*?TEMP=(.*?);.*"] + Type number : humidity [transform="REGEX:.*?HUM=(.*?);.*"] + } + Thing serialDevice rollershutter [patternMatch=".*"] { + Channels: + Type rollershutter : serialRollo [transform="REGEX:Position:([0-9.]*)", up="Rollo_UP\n", down="Rollo_DOWN\n", stop="Rollo_STOP\n"] + Type switch : roloAt100 [transform="REGEX:s/Position:100/ON/"] + } + Thing serialDevice relay [patternMatch=".*"] { + Channels: + Type switch : serialRelay [on="Q1_ON\n", off="Q1_OFF\n"] + } + Thing serialDevice myDevice [patternMatch="ID=2341;.*"] { + Channels: + Type string : control [commandTransform="JS:addCheckSum.js", commandFormat="ID=2341;COMMAND=%s;"] + } +} + +``` + +demo.items: + +``` +Number:Temperature myTemp "My Temperature" {channel="serial:serialDevice:sensors:temperatureSensor:temperature"} +Number myHum "My Humidity" {channel="serial:serialDevice:sensors:temperatureSensor:humidity"} +Switch serialRelay "Relay Q1" (Entrance) {channel="serial:serialDevice:sensors:relay:serialRelay"} +Rollershutter serialRollo "Entrance Rollo" (Entrance) {channel="serial:serialDevice:sensors:rollershutter:serialRollo"} +Rollershutter roloAt100 "Rolo at 100" (Entrance) {channel="serial:serialDevice:sensors:rollershutter:roloAt100"} +String deviceControl {channel="serial:serialDevice:sensors:myDevice:control"} +``` diff --git a/bundles/org.openhab.binding.serial/pom.xml b/bundles/org.openhab.binding.serial/pom.xml new file mode 100644 index 00000000000..977960fd53f --- /dev/null +++ b/bundles/org.openhab.binding.serial/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.0.0-SNAPSHOT + + + org.openhab.binding.serial + + openHAB Add-ons :: Bundles :: Serial Binding + + diff --git a/bundles/org.openhab.binding.serial/src/main/feature/feature.xml b/bundles/org.openhab.binding.serial/src/main/feature/feature.xml new file mode 100644 index 00000000000..65279b0f797 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/feature/feature.xml @@ -0,0 +1,24 @@ + + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + openhab-transport-serial + mvn:org.openhab.addons.bundles/org.openhab.binding.serial/${project.version} + + diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/SerialBindingConstants.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/SerialBindingConstants.java new file mode 100644 index 00000000000..1aa6116a08e --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/SerialBindingConstants.java @@ -0,0 +1,42 @@ +/** + * 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.serial.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link SerialBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Mike Major - Initial contribution + */ +@NonNullByDefault +public class SerialBindingConstants { + + private static final String BINDING_ID = "serial"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "serialBridge"); + public static final ThingTypeUID THING_TYPE_DEVICE = new ThingTypeUID(BINDING_ID, "serialDevice"); + + // List of all Channel ids + public static final String TRIGGER_CHANNEL = "data"; + public static final String STRING_CHANNEL = "string"; + public static final String BINARY_CHANNEL = "binary"; + public static final String DEVICE_STRING_CHANNEL = "string"; + public static final String DEVICE_NUMBER_CHANNEL = "number"; + public static final String DEVICE_DIMMER_CHANNEL = "dimmer"; + public static final String DEVICE_SWITCH_CHANNEL = "switch"; + public static final String DEVICE_ROLLERSHUTTER_CHANNEL = "rollershutter"; +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/SerialHandlerFactory.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/SerialHandlerFactory.java new file mode 100644 index 00000000000..346e48c68e5 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/SerialHandlerFactory.java @@ -0,0 +1,85 @@ +/** + * 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.serial.internal; + +import static org.openhab.binding.serial.internal.SerialBindingConstants.THING_TYPE_BRIDGE; +import static org.openhab.binding.serial.internal.SerialBindingConstants.THING_TYPE_DEVICE; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.serial.internal.handler.SerialBridgeHandler; +import org.openhab.binding.serial.internal.handler.SerialDeviceHandler; +import org.openhab.binding.serial.internal.transform.CascadedValueTransformationImpl; +import org.openhab.binding.serial.internal.transform.NoOpValueTransformation; +import org.openhab.binding.serial.internal.transform.ValueTransformation; +import org.openhab.binding.serial.internal.transform.ValueTransformationProvider; +import org.openhab.core.io.transport.serial.SerialPortManager; +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.core.transform.TransformationHelper; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link SerialHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Mike Major - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.serial", service = ThingHandlerFactory.class) +public class SerialHandlerFactory extends BaseThingHandlerFactory implements ValueTransformationProvider { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE, THING_TYPE_DEVICE); + + private final SerialPortManager serialPortManager; + + @Activate + public SerialHandlerFactory(@Reference final SerialPortManager serialPortManager) { + this.serialPortManager = serialPortManager; + } + + @Override + public boolean supportsThingType(final ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(final Thing thing) { + final ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_BRIDGE.equals(thingTypeUID)) { + return new SerialBridgeHandler((Bridge) thing, serialPortManager); + } else if (THING_TYPE_DEVICE.equals(thingTypeUID)) { + return new SerialDeviceHandler(thing, this); + } + + return null; + } + + @Override + public ValueTransformation getValueTransformation(@Nullable final String pattern) { + if (pattern == null) { + return NoOpValueTransformation.getInstance(); + } + return new CascadedValueTransformationImpl(pattern, + name -> TransformationHelper.getTransformationService(bundleContext, name)); + } +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/ChannelConfig.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/ChannelConfig.java new file mode 100644 index 00000000000..5848d1ebedc --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/ChannelConfig.java @@ -0,0 +1,74 @@ +/** + * 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.serial.internal.channel; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Class describing the channel user configuration + * + * @author Mike Major - Initial contribution + */ +@NonNullByDefault +public class ChannelConfig { + /** + * Transform for received data + */ + public @Nullable String stateTransformation; + + /** + * Transform for command + */ + public @Nullable String commandTransformation; + + /** + * Format string for command + */ + public @Nullable String commandFormat; + + /** + * On value + */ + public @Nullable String onValue; + + /** + * Off value + */ + public @Nullable String offValue; + + /** + * Up value + */ + public @Nullable String upValue; + + /** + * Down value + */ + public @Nullable String downValue; + + /** + * Stop value + */ + public @Nullable String stopValue; + + /** + * Increase value + */ + public @Nullable String increaseValue; + + /** + * Decrease value + */ + public @Nullable String decreaseValue; +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/DeviceChannel.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/DeviceChannel.java new file mode 100644 index 00000000000..adc46565fd3 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/DeviceChannel.java @@ -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.serial.internal.channel; + +import java.util.IllegalFormatException; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.serial.internal.transform.ValueTransformation; +import org.openhab.binding.serial.internal.transform.ValueTransformationProvider; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link DeviceChannel} is the abstract class for handling a channel. Provides + * the ability to transform the device data into the channel state. + * + * @author Mike Major - Initial contribution + */ +@NonNullByDefault +public abstract class DeviceChannel { + protected final Logger logger = LoggerFactory.getLogger(DeviceChannel.class); + + protected final ChannelConfig config; + + private final ValueTransformation stateTransform; + private final ValueTransformation commandTransform; + + protected DeviceChannel(final ValueTransformationProvider valueTransformationProvider, final ChannelConfig config) { + this.config = config; + stateTransform = valueTransformationProvider.getValueTransformation(config.stateTransformation); + commandTransform = valueTransformationProvider.getValueTransformation(config.commandTransformation); + } + + /** + * Map the supplied command into the data to send to the device by + * applying a format followed by a transform. + * + * @param command the command to map + * @return the mapped data if the mapping produced a result. + */ + public Optional mapCommand(final Command command) { + final Optional result = transformCommand(formatCommand(command)); + + logger.debug("Mapped command is '{}'", result.orElse(null)); + + return result; + } + + /** + * Transform the data using the configured transform + * + * @param data the data to transform + * @return the transformed data if the transform produced a result. + */ + public Optional transformData(final String data) { + return stateTransform.apply(data); + } + + /** + * Transform the data using the configured command transform + * + * @param data the command to transform + * @return the transformed data if the transform produced a result. + */ + protected Optional transformCommand(final String data) { + return commandTransform.apply(data); + } + + /** + * Format the commnd using the configured format + * + * @param data the command to transform + * @return the formatted data. The orginal data is returned if there is no format string + * or if there is an error performing the format. + */ + protected String formatCommand(final Command command) { + String data; + + final String commandFormat = config.commandFormat; + if (commandFormat != null) { + try { + data = command.format(commandFormat); + } catch (final IllegalFormatException e) { + logger.warn("Couldn't format commmand because format string '{}' is invalid", commandFormat); + data = command.toFullString(); + } + } else { + data = command.toFullString(); + } + + return data; + } +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/DeviceChannelFactory.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/DeviceChannelFactory.java new file mode 100644 index 00000000000..2c4d3a44673 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/DeviceChannelFactory.java @@ -0,0 +1,69 @@ +/** + * 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.serial.internal.channel; + +import static org.openhab.binding.serial.internal.SerialBindingConstants.DEVICE_DIMMER_CHANNEL; +import static org.openhab.binding.serial.internal.SerialBindingConstants.DEVICE_NUMBER_CHANNEL; +import static org.openhab.binding.serial.internal.SerialBindingConstants.DEVICE_ROLLERSHUTTER_CHANNEL; +import static org.openhab.binding.serial.internal.SerialBindingConstants.DEVICE_STRING_CHANNEL; +import static org.openhab.binding.serial.internal.SerialBindingConstants.DEVICE_SWITCH_CHANNEL; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.serial.internal.transform.ValueTransformationProvider; + +/** + * A factory to create {@link DeviceChannel} objects + * + * @author Mike Major - Initial contribution + */ +@NonNullByDefault +public class DeviceChannelFactory { + + /** + * Create a {@link DeviceChannel} for the channel type + * + * @param bundleContext the bundle context + * @param channelConfig the channel configuration + * @param channelTypeID the channel type id + * @return the DeviceChannel or null if the channel type is not supported. + */ + public static @Nullable DeviceChannel createDeviceChannel( + final ValueTransformationProvider valueTransformationProvider, final ChannelConfig channelConfig, + final String channelTypeID) { + DeviceChannel deviceChannel; + + switch (channelTypeID) { + case DEVICE_STRING_CHANNEL: + deviceChannel = new StringChannel(valueTransformationProvider, channelConfig); + break; + case DEVICE_NUMBER_CHANNEL: + deviceChannel = new NumberChannel(valueTransformationProvider, channelConfig); + break; + case DEVICE_DIMMER_CHANNEL: + deviceChannel = new DimmerChannel(valueTransformationProvider, channelConfig); + break; + case DEVICE_SWITCH_CHANNEL: + deviceChannel = new SwitchChannel(valueTransformationProvider, channelConfig); + break; + case DEVICE_ROLLERSHUTTER_CHANNEL: + deviceChannel = new RollershutterChannel(valueTransformationProvider, channelConfig); + break; + default: + deviceChannel = null; + break; + } + + return deviceChannel; + } +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/DimmerChannel.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/DimmerChannel.java new file mode 100644 index 00000000000..f79c9e70c94 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/DimmerChannel.java @@ -0,0 +1,66 @@ +/** + * 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.serial.internal.channel; + +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.serial.internal.transform.ValueTransformationProvider; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.types.Command; + +/** + * The {@link DimmerChannel} channel applies a format followed by a transform. + * + * @author Mike Major - Initial contribution + */ +@NonNullByDefault +public class DimmerChannel extends SwitchChannel { + + public DimmerChannel(final ValueTransformationProvider valueTransformationProvider, final ChannelConfig config) { + super(valueTransformationProvider, config); + } + + @Override + public Optional mapCommand(final Command command) { + Optional result; + + if (command instanceof OnOffType) { + result = super.mapCommand(command); + } else { + String data; + + final String increaseValue = config.increaseValue; + final String decreaseValue = config.decreaseValue; + + if (command instanceof IncreaseDecreaseType) { + if (increaseValue != null && IncreaseDecreaseType.INCREASE.equals(command)) { + data = increaseValue; + } else if (decreaseValue != null && IncreaseDecreaseType.DECREASE.equals(command)) { + data = decreaseValue; + } else { + data = command.toFullString(); + } + } else { + data = formatCommand(command); + } + + result = transformCommand(data); + + logger.debug("Mapped command is '{}'", result.orElse(null)); + } + + return result; + } +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/NumberChannel.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/NumberChannel.java new file mode 100644 index 00000000000..a530af06f02 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/NumberChannel.java @@ -0,0 +1,29 @@ +/** + * 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.serial.internal.channel; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.serial.internal.transform.ValueTransformationProvider; + +/** + * The {@link NumberChannel} channel applies a format followed by a transform. + * + * @author Mike Major - Initial contribution + */ +@NonNullByDefault +public class NumberChannel extends DeviceChannel { + + public NumberChannel(final ValueTransformationProvider valueTransformationProvider, final ChannelConfig config) { + super(valueTransformationProvider, config); + } +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/RollershutterChannel.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/RollershutterChannel.java new file mode 100644 index 00000000000..87a78a30e87 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/RollershutterChannel.java @@ -0,0 +1,68 @@ +/** + * 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.serial.internal.channel; + +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.serial.internal.transform.ValueTransformationProvider; +import org.openhab.core.library.types.StopMoveType; +import org.openhab.core.library.types.UpDownType; +import org.openhab.core.types.Command; + +/** + * The {@link RollershutterChannel} channel provides mappings for the UP, DOWN and STOP commands + * + * @author Mike Major - Initial contribution + */ +@NonNullByDefault +public class RollershutterChannel extends DeviceChannel { + + public RollershutterChannel(final ValueTransformationProvider valueTransformationProvider, + final ChannelConfig config) { + super(valueTransformationProvider, config); + } + + @Override + public Optional mapCommand(final Command command) { + String data; + + final String upValue = config.upValue; + final String downValue = config.downValue; + final String stopValue = config.stopValue; + + if (command instanceof UpDownType) { + if (upValue != null && UpDownType.UP.equals(command)) { + data = upValue; + } else if (downValue != null && UpDownType.DOWN.equals(command)) { + data = downValue; + } else { + data = command.toFullString(); + } + } else if (command instanceof StopMoveType) { + if (stopValue != null && StopMoveType.STOP.equals(command)) { + data = stopValue; + } else { + data = command.toFullString(); + } + } else { + data = formatCommand(command); + } + + final Optional result = transformCommand(data); + + logger.debug("Mapped command is '{}'", result.orElse(null)); + + return result; + } +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/StringChannel.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/StringChannel.java new file mode 100644 index 00000000000..0bd46e181b5 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/StringChannel.java @@ -0,0 +1,29 @@ +/** + * 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.serial.internal.channel; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.serial.internal.transform.ValueTransformationProvider; + +/** + * The {@link StringChannel} channel applies a format followed by a transform. + * + * @author Mike Major - Initial contribution + */ +@NonNullByDefault +public class StringChannel extends DeviceChannel { + + public StringChannel(final ValueTransformationProvider valueTransformationProvider, final ChannelConfig config) { + super(valueTransformationProvider, config); + } +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/SwitchChannel.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/SwitchChannel.java new file mode 100644 index 00000000000..c4a4e6baba6 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/channel/SwitchChannel.java @@ -0,0 +1,55 @@ +/** + * 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.serial.internal.channel; + +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.serial.internal.transform.ValueTransformationProvider; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.types.Command; + +/** + * The {@link SwitchChannel} channel provides mappings for the ON and OFF commands + * + * @author Mike Major - Initial contribution + */ +@NonNullByDefault +public class SwitchChannel extends DeviceChannel { + + public SwitchChannel(final ValueTransformationProvider valueTransformationProvider, final ChannelConfig config) { + super(valueTransformationProvider, config); + } + + @Override + public Optional mapCommand(final Command command) { + String data; + + final String onValue = config.onValue; + final String offValue = config.offValue; + + if (onValue != null && OnOffType.ON.equals(command)) { + data = onValue; + } else if (offValue != null && OnOffType.OFF.equals(command)) { + data = offValue; + } else { + data = command.toFullString(); + } + + final Optional result = transformCommand(data); + + logger.debug("Mapped command is '{}'", result.orElse(null)); + + return result; + } +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/handler/SerialBridgeConfiguration.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/handler/SerialBridgeConfiguration.java new file mode 100644 index 00000000000..ec2c7f13115 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/handler/SerialBridgeConfiguration.java @@ -0,0 +1,60 @@ +/** + * 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.serial.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Class describing the serial bridge user configuration + * + * @author Mike Major - Initial contribution + */ +@NonNullByDefault +public class SerialBridgeConfiguration { + /** + * Serial port name + */ + public @Nullable String serialPort; + + /** + * Serial port baud rate + */ + public int baudRate = 9600; + + /** + * Serial port data bits + */ + public int dataBits = 8; + + /** + * Serial port parity + */ + public String parity = "N"; + + /** + * Serial port stop bits + */ + public String stopBits = "1"; + + /** + * Charset + */ + public @Nullable String charset; + + @Override + public String toString() { + return "SerialBridgeConfiguration [serialPort=" + serialPort + ", Baudrate=" + baudRate + ", Databits=" + + dataBits + ", Parity=" + parity + ", Stopbits=" + stopBits + ", charset=" + charset + "]"; + } +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/handler/SerialBridgeHandler.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/handler/SerialBridgeHandler.java new file mode 100644 index 00000000000..4830af0c35c --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/handler/SerialBridgeHandler.java @@ -0,0 +1,343 @@ +/** + * 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.serial.internal.handler; + +import static org.openhab.binding.serial.internal.SerialBindingConstants.BINARY_CHANNEL; +import static org.openhab.binding.serial.internal.SerialBindingConstants.STRING_CHANNEL; +import static org.openhab.binding.serial.internal.SerialBindingConstants.TRIGGER_CHANNEL; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.TooManyListenersException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.serial.internal.util.Parity; +import org.openhab.binding.serial.internal.util.StopBits; +import org.openhab.core.io.transport.serial.PortInUseException; +import org.openhab.core.io.transport.serial.SerialPort; +import org.openhab.core.io.transport.serial.SerialPortEvent; +import org.openhab.core.io.transport.serial.SerialPortEventListener; +import org.openhab.core.io.transport.serial.SerialPortIdentifier; +import org.openhab.core.io.transport.serial.SerialPortManager; +import org.openhab.core.io.transport.serial.UnsupportedCommOperationException; +import org.openhab.core.library.types.RawType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.CommonTriggerEvents; +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.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SerialBridgeHandler} is responsible for handling commands, which + * are sent to one of the channels. + * + * @author Mike Major - Initial contribution + */ +@NonNullByDefault +public class SerialBridgeHandler extends BaseBridgeHandler implements SerialPortEventListener { + + private final Logger logger = LoggerFactory.getLogger(SerialBridgeHandler.class); + + private SerialBridgeConfiguration config = new SerialBridgeConfiguration(); + + private final SerialPortManager serialPortManager; + private @Nullable SerialPort serialPort; + + private @Nullable InputStream inputStream; + private @Nullable OutputStream outputStream; + + private Charset charset = StandardCharsets.UTF_8; + + private @Nullable String lastValue; + + private final AtomicBoolean readerActive = new AtomicBoolean(false); + private @Nullable ScheduledFuture reader; + + public SerialBridgeHandler(final Bridge bridge, final SerialPortManager serialPortManager) { + super(bridge); + this.serialPortManager = serialPortManager; + } + + @Override + public void handleCommand(final ChannelUID channelUID, final Command command) { + if (command instanceof RefreshType) { + final String lastValue = this.lastValue; + + if (lastValue != null) { + refresh(channelUID.getId(), lastValue); + } + } else { + switch (channelUID.getId()) { + case STRING_CHANNEL: + writeString(command.toFullString(), false); + break; + case BINARY_CHANNEL: + writeString(command.toFullString(), true); + break; + default: + break; + } + + } + } + + @Override + public void initialize() { + config = getConfigAs(SerialBridgeConfiguration.class); + + try { + if (config.charset != null) { + charset = Charset.forName(config.charset); + } + logger.debug("Serial port '{}' charset '{}' set", config.serialPort, charset); + } catch (final IllegalCharsetNameException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Invalid charset"); + return; + } + + final String port = config.serialPort; + if (port == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set"); + return; + } + + // parse ports and if the port is found, initialize the reader + final SerialPortIdentifier portId = serialPortManager.getIdentifier(port); + if (portId == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port is not known"); + return; + } + + // initialize serial port + try { + final SerialPort serialPort = portId.open(getThing().getUID().toString(), 2000); + this.serialPort = serialPort; + + serialPort.setSerialPortParams(config.baudRate, config.dataBits, + StopBits.fromConfig(config.stopBits).getSerialPortValue(), + Parity.fromConfig(config.parity).getSerialPortValue()); + + serialPort.addEventListener(this); + + // activate the DATA_AVAILABLE notifier + serialPort.notifyOnDataAvailable(true); + inputStream = serialPort.getInputStream(); + outputStream = serialPort.getOutputStream(); + + updateStatus(ThingStatus.ONLINE); + } catch (final IOException ex) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "I/O error"); + } catch (final PortInUseException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Port is in use"); + } catch (final TooManyListenersException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Cannot attach listener to port"); + } catch (final UnsupportedCommOperationException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Unsupported port parameters: " + e.getMessage()); + } + } + + @Override + public void dispose() { + final SerialPort serialPort = this.serialPort; + if (serialPort != null) { + serialPort.removeEventListener(); + serialPort.close(); + this.serialPort = null; + } + + final InputStream inputStream = this.inputStream; + if (inputStream != null) { + try { + inputStream.close(); + } catch (final IOException e) { + logger.debug("Error while closing the input stream: {}", e.getMessage()); + } + this.inputStream = null; + } + + final OutputStream outputStream = this.outputStream; + if (outputStream != null) { + try { + outputStream.close(); + } catch (final IOException e) { + logger.debug("Error while closing the output stream: {}", e.getMessage()); + } + this.outputStream = null; + } + + readerActive.set(false); + final ScheduledFuture reader = this.reader; + if (reader != null) { + reader.cancel(false); + this.reader = null; + } + + lastValue = null; + } + + @Override + public void serialEvent(final SerialPortEvent event) { + switch (event.getEventType()) { + case SerialPortEvent.DATA_AVAILABLE: + if (readerActive.compareAndSet(false, true)) { + reader = scheduler.schedule(() -> receiveAndProcess(new StringBuilder(), true), 0, + TimeUnit.MILLISECONDS); + } + break; + default: + break; + } + } + + /** + * Sends a string to the serial port. + * + * @param string the string to send + */ + public void writeString(final String string) { + writeString(string, false); + } + + /** + * Refreshes the channel with the last received data + * + * @param channelId the channel to refresh + * @param channelId the data to use + */ + private void refresh(final String channelId, final String data) { + if (!isLinked(channelId)) { + return; + } + + switch (channelId) { + case STRING_CHANNEL: + updateState(channelId, new StringType(data)); + break; + case BINARY_CHANNEL: + final StringBuilder sb = new StringBuilder("data:"); + sb.append(RawType.DEFAULT_MIME_TYPE).append(";base64,") + .append(Base64.getEncoder().encodeToString(data.getBytes(charset))); + updateState(channelId, new StringType(sb.toString())); + break; + default: + break; + } + } + + /** + * Read from the serial port and process the data + * + * @param sb the string builder to receive the data + * @param firstAttempt indicates if this is the first read attempt without waiting + */ + private void receiveAndProcess(final StringBuilder sb, final boolean firstAttempt) { + final InputStream inputStream = this.inputStream; + + if (inputStream == null) { + readerActive.set(false); + return; + } + + try { + if (firstAttempt || inputStream.available() > 0) { + final byte[] readBuffer = new byte[20]; + + // read data from serial device + while (inputStream.available() > 0) { + final int bytes = inputStream.read(readBuffer); + sb.append(new String(readBuffer, 0, bytes, charset)); + } + + // Add wait states around reading the stream, so that interrupted transmissions + // are merged + if (readerActive.get()) { + reader = scheduler.schedule(() -> receiveAndProcess(sb, false), 100, TimeUnit.MILLISECONDS); + } + + } else { + final String result = sb.toString(); + + triggerChannel(TRIGGER_CHANNEL, CommonTriggerEvents.PRESSED); + refresh(STRING_CHANNEL, result); + refresh(BINARY_CHANNEL, result); + + result.lines().forEach(l -> getThing().getThings().forEach(t -> { + final SerialDeviceHandler device = (SerialDeviceHandler) t.getHandler(); + if (device != null) { + device.handleData(l); + } + })); + + lastValue = result; + + if (readerActive.compareAndSet(true, false)) { + // Check we haven't received more data while processing + if (inputStream.available() > 0 && readerActive.compareAndSet(false, true)) { + reader = scheduler.schedule(() -> receiveAndProcess(new StringBuilder(), true), 0, + TimeUnit.MILLISECONDS); + } + } + } + } catch (final IOException e) { + logger.debug("Error reading from serial port: {}", e.getMessage(), e); + readerActive.set(false); + } + } + + /** + * Sends a string to the serial port. + * + * @param string the string to send + * @param isRawType the string should be handled as a RawType + */ + private void writeString(final String string, final boolean isRawType) { + final OutputStream outputStream = this.outputStream; + + if (outputStream == null) { + return; + } + + logger.debug("Writing '{}' to serial port {}", string, config.serialPort); + + try { + // write string to serial port + if (isRawType) { + final RawType rt = RawType.valueOf(string); + outputStream.write(rt.getBytes()); + } else { + outputStream.write(string.getBytes(charset)); + } + + outputStream.flush(); + } catch (final IOException | IllegalArgumentException e) { + logger.warn("Error writing '{}' to serial port {}: {}", string, config.serialPort, e.getMessage()); + } + } +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/handler/SerialDeviceConfiguration.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/handler/SerialDeviceConfiguration.java new file mode 100644 index 00000000000..6186fbddffe --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/handler/SerialDeviceConfiguration.java @@ -0,0 +1,34 @@ +/** + * 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.serial.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Class describing the serial device user configuration + * + * @author Mike Major - Initial contribution + */ +@NonNullByDefault +public class SerialDeviceConfiguration { + /** + * + * Pattern match + */ + public String patternMatch = ""; + + @Override + public String toString() { + return "SerialDeviceConfiguration [patternMatch=" + patternMatch + "]"; + } +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/handler/SerialDeviceHandler.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/handler/SerialDeviceHandler.java new file mode 100644 index 00000000000..e20e5ce3693 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/handler/SerialDeviceHandler.java @@ -0,0 +1,168 @@ +/** + * 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.serial.internal.handler; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.serial.internal.channel.ChannelConfig; +import org.openhab.binding.serial.internal.channel.DeviceChannel; +import org.openhab.binding.serial.internal.channel.DeviceChannelFactory; +import org.openhab.binding.serial.internal.transform.ValueTransformationProvider; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; + +/** + * The {@link SerialDeviceHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Mike Major - Initial contribution + */ +@NonNullByDefault +public class SerialDeviceHandler extends BaseThingHandler { + + private final ValueTransformationProvider valueTransformationProvider; + + private @Nullable Pattern devicePattern; + + private @Nullable String lastValue; + + private final Map channels = new HashMap<>(); + + public SerialDeviceHandler(final Thing thing, final ValueTransformationProvider valueTransformationProvider) { + super(thing); + this.valueTransformationProvider = valueTransformationProvider; + } + + @Override + public void handleCommand(final ChannelUID channelUID, final Command command) { + if (command instanceof RefreshType) { + final String lastValue = this.lastValue; + + if (lastValue != null) { + final DeviceChannel channel = channels.get(channelUID); + if (channel != null) { + refresh(channelUID, channel, lastValue); + } + } + } else { + final DeviceChannel channel = channels.get(channelUID); + if (channel != null) { + final Bridge bridge = getBridge(); + if (bridge != null) { + final SerialBridgeHandler handler = (SerialBridgeHandler) bridge.getHandler(); + if (handler != null) { + channel.mapCommand(command).ifPresent(value -> handler.writeString(value)); + } + } + } + } + } + + @Override + public void initialize() { + final SerialDeviceConfiguration config = getConfigAs(SerialDeviceConfiguration.class); + + try { + devicePattern = Pattern.compile(config.patternMatch); + } catch (final PatternSyntaxException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Invalid device pattern: " + e.getMessage()); + return; + } + + for (final Channel c : getThing().getChannels()) { + final ChannelTypeUID type = c.getChannelTypeUID(); + if (type != null) { + final ChannelConfig channelConfig = c.getConfiguration().as(ChannelConfig.class); + try { + final DeviceChannel deviceChannel = DeviceChannelFactory + .createDeviceChannel(valueTransformationProvider, channelConfig, type.getId()); + if (deviceChannel != null) { + channels.put(c.getUID(), deviceChannel); + } + } catch (final IllegalArgumentException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Configuration error for channel " + c.getUID().getId() + ": " + e.getMessage()); + return; + } + } + } + + if (getBridgeStatus().getStatus() == ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + } + } + + @Override + public void dispose() { + channels.clear(); + lastValue = null; + super.dispose(); + } + + /** + * Handle a line of data received from the bridge + * + * @param data the line of data + */ + public void handleData(final String data) { + final Pattern devicePattern = this.devicePattern; + + if (devicePattern != null && devicePattern.matcher(data).matches()) { + channels.forEach((channelUID, channel) -> refresh(channelUID, channel, data)); + this.lastValue = data; + } + } + + /** + * Return the bridge status. + */ + private ThingStatusInfo getBridgeStatus() { + final Bridge b = getBridge(); + if (b != null) { + return b.getStatusInfo(); + } else { + return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, null); + } + } + + /** + * Refreshes the channel with the last received data + * + * @param channelId the channel to refresh + */ + private void refresh(final ChannelUID channelUID, final DeviceChannel channel, final String data) { + if (!isLinked(channelUID)) { + return; + } + + channel.transformData(data).ifPresent(value -> updateState(channelUID, new StringType(value))); + } +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/transform/CascadedValueTransformationImpl.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/transform/CascadedValueTransformationImpl.java new file mode 100644 index 00000000000..d515f69dfd6 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/transform/CascadedValueTransformationImpl.java @@ -0,0 +1,54 @@ +/** + * 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.serial.internal.transform; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.transform.TransformationService; + +/** + * The {@link CascadedValueTransformationImpl} implements {@link ValueTransformation for a cascaded set of + * transformations} + * + * @author Jan N. Klug - Initial contribution + * @author Mike Major - Copied from HTTP binding to provide consistent user experience + */ +@NonNullByDefault +public class CascadedValueTransformationImpl implements ValueTransformation { + private final List transformations; + + public CascadedValueTransformationImpl(final String transformationString, + final Function transformationServiceSupplier) { + transformations = Arrays.stream(transformationString.split("∩")).filter(s -> !s.isEmpty()) + .map(transformation -> new SingleValueTransformation(transformation, transformationServiceSupplier)) + .collect(Collectors.toList()); + } + + @Override + public Optional apply(final String value) { + Optional valueOptional = Optional.of(value); + + // process all transformations + for (final ValueTransformation transformation : transformations) { + valueOptional = valueOptional.flatMap(transformation::apply); + } + + return valueOptional; + } +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/transform/NoOpValueTransformation.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/transform/NoOpValueTransformation.java new file mode 100644 index 00000000000..dfdf6ee5e35 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/transform/NoOpValueTransformation.java @@ -0,0 +1,42 @@ +/** + * 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.serial.internal.transform; + +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link NoOpValueTransformation} implements a no-op (identity) transformation + * + * @author Jan N. Klug - Initial contribution + * @author Mike Major - Copied from HTTP binding to provide consistent user experience + */ +@NonNullByDefault +public class NoOpValueTransformation implements ValueTransformation { + private static final NoOpValueTransformation NO_OP_VALUE_TRANSFORMATION = new NoOpValueTransformation(); + + @Override + public Optional apply(final String value) { + return Optional.of(value); + } + + /** + * get the static value transformation for identity + * + * @return + */ + public static ValueTransformation getInstance() { + return NO_OP_VALUE_TRANSFORMATION; + } +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/transform/SingleValueTransformation.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/transform/SingleValueTransformation.java new file mode 100644 index 00000000000..f9f56033815 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/transform/SingleValueTransformation.java @@ -0,0 +1,90 @@ +/** + * 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.serial.internal.transform; + +import java.lang.ref.WeakReference; +import java.util.Optional; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.transform.TransformationException; +import org.openhab.core.transform.TransformationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A transformation for a value used in {@DeviceChannel}. + * + * @author David Graeff - Initial contribution + * @author Jan N. Klug - adapted from MQTT binding to HTTP binding + * @author Mike Major - Copied from HTTP binding to provide consistent user experience + */ +@NonNullByDefault +public class SingleValueTransformation implements ValueTransformation { + private final Logger logger = LoggerFactory.getLogger(SingleValueTransformation.class); + private final Function transformationServiceSupplier; + private WeakReference<@Nullable TransformationService> transformationService = new WeakReference<>(null); + private final String pattern; + private final String serviceName; + + /** + * Creates a new channel state transformer. + * + * @param pattern A transformation pattern, starting with the transformation service + * name, followed by a colon and the transformation itself. + * @param transformationServiceSupplier + */ + public SingleValueTransformation(final String pattern, + final Function transformationServiceSupplier) { + this.transformationServiceSupplier = transformationServiceSupplier; + final int index = pattern.indexOf(':'); + if (index == -1) { + throw new IllegalArgumentException( + "The transformation pattern must consist of the type and the pattern separated by a colon"); + } + this.serviceName = pattern.substring(0, index).toUpperCase(); + this.pattern = pattern.substring(index + 1); + } + + @Override + public Optional apply(final String value) { + TransformationService transformationService = this.transformationService.get(); + if (transformationService == null) { + transformationService = transformationServiceSupplier.apply(serviceName); + if (transformationService == null) { + logger.warn("Transformation service {} for pattern {} not found!", serviceName, pattern); + return Optional.empty(); + } + this.transformationService = new WeakReference<>(transformationService); + } + + try { + final String result = transformationService.transform(pattern, value); + if (result == null) { + logger.debug("Transformation {} returned empty result when applied to {}.", this, value); + return Optional.empty(); + } + return Optional.of(result); + } catch (final TransformationException e) { + logger.warn("Executing transformation {} failed: {}", this, e.getMessage()); + } + + return Optional.empty(); + } + + @Override + public String toString() { + return "ChannelStateTransformation{pattern='" + pattern + "', serviceName='" + serviceName + "'}"; + } +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/transform/ValueTransformation.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/transform/ValueTransformation.java new file mode 100644 index 00000000000..3cb88a53133 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/transform/ValueTransformation.java @@ -0,0 +1,35 @@ +/** + * 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.serial.internal.transform; + +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ValueTransformation} applies a set of transformations to a value + * + * @author Jan N. Klug - Initial contribution + * @author Mike Major - Copied from HTTP binding to provide consistent user experience + */ +@NonNullByDefault +public interface ValueTransformation { + + /** + * applies the value transformation to a value + * + * @param value The value + * @return Optional of string representing the transformed value (empty if transformation not present or failed) + */ + Optional apply(String value); +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/transform/ValueTransformationProvider.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/transform/ValueTransformationProvider.java new file mode 100644 index 00000000000..d3c3ddb17fb --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/transform/ValueTransformationProvider.java @@ -0,0 +1,34 @@ +/** + * 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.serial.internal.transform; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link ValueTransformationProvider} allows to retrieve a transformation service by name + * + * @author Jan N. Klug - Initial contribution + * @author Mike Major - Copied from HTTP binding to provide consistent user experience + */ +@NonNullByDefault +public interface ValueTransformationProvider { + + /** + * + * @param pattern A transformation pattern, starting with the transformation service + * * name, followed by a colon and the transformation itself. + * @return + */ + ValueTransformation getValueTransformation(@Nullable String pattern); +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/util/Parity.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/util/Parity.java new file mode 100644 index 00000000000..9f7b5a33614 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/util/Parity.java @@ -0,0 +1,59 @@ +/** + * 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.serial.internal.util; + +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.io.transport.serial.SerialPort; + +/** + * Enum to convert config parity value to serial port value + * + * @author Mike Major - Initial contribution + */ +@NonNullByDefault +public enum Parity { + NONE("N", SerialPort.PARITY_NONE), + ODD("O", SerialPort.PARITY_ODD), + EVEN("E", SerialPort.PARITY_EVEN), + MARK("M", SerialPort.PARITY_MARK), + SPACE("S", SerialPort.PARITY_SPACE); + + final String configValue; + final int serialPortValue; + + private Parity(final String configValue, final int serialPortValue) { + this.configValue = configValue; + this.serialPortValue = serialPortValue; + } + + /** + * Return the serial port value + * + * @return the serial port value + */ + public int getSerialPortValue() { + return serialPortValue; + } + + /** + * Return the enum value from the config value + * + * @param configValue the config value + * @return the enum value + */ + public static Parity fromConfig(final String configValue) { + return Arrays.asList(values()).stream().filter(p -> p.configValue.equals(configValue)).findFirst().orElse(NONE); + } +} diff --git a/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/util/StopBits.java b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/util/StopBits.java new file mode 100644 index 00000000000..9e836dc3401 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/java/org/openhab/binding/serial/internal/util/StopBits.java @@ -0,0 +1,58 @@ +/** + * 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.serial.internal.util; + +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.io.transport.serial.SerialPort; + +/** + * Enum to convert config stopBits value to serial port value + * + * @author Mike Major - Initial contribution + */ +@NonNullByDefault +public enum StopBits { + STOPBITS_1("1", SerialPort.STOPBITS_1), + STOPBITS_1_5("1.5", SerialPort.STOPBITS_1_5), + STOPBITS_2("2", SerialPort.STOPBITS_2); + + final String configValue; + final int serialPortValue; + + private StopBits(final String configValue, final int serialPortValue) { + this.configValue = configValue; + this.serialPortValue = serialPortValue; + } + + /** + * Return the serial port value + * + * @return the serial port value + */ + public int getSerialPortValue() { + return serialPortValue; + } + + /** + * Return the enum value from the config value + * + * @param configValue the config value + * @return the enum value + */ + public static StopBits fromConfig(final String configValue) { + return Arrays.asList(values()).stream().filter(p -> p.configValue.equals(configValue)).findFirst() + .orElse(STOPBITS_1); + } +} diff --git a/bundles/org.openhab.binding.serial/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.serial/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 00000000000..d471083eec6 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + Serial Binding + This binding supports sending/receiving data to/from a serial port + Mike Major + + diff --git a/bundles/org.openhab.binding.serial/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.serial/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 00000000000..5060e10eb08 --- /dev/null +++ b/bundles/org.openhab.binding.serial/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,245 @@ + + + + + + + Serial port which can send and receive data + + + + + + + + + + serial-port + + The serial port to use (e.g. Linux: /dev/ttyUSB0, Windows: COM1) + + + true + + Set the baud rate + 9600 + + + + + + + + + + + true + + Set the data bits + 8 + + + + + + + + + true + + Set the parity + N + + + + + + + + + + true + + Set the stop bits + 1 + + + + + + + + true + + The charset to use for converting between bytes and string (e.g. UTF-8, ISO-8859-1) + + + + + + + + + + + Represents a device + + + + + pattern-match + true + Regular expression used to identify device from received data (must match the whole line) + + + + + + + String + + Channel for sending/receiving data as a string to/from the serial port + + + + String + + Channel for sending/receiving data encoded as Base64 to/from the serial port + + + + String + + Channel to receive commands as a string + + + + Transform used to convert device data to channel state, e.g. REGEX:.*?STATE=(.*?);.* + + + + Format string applied to the command, e.g. ID=671;COMMAND=%s + + + + Transform used to convert command to device data, e.g. JS:device.js + + + + + + Number + + Channel to receive commands as a number + + + + Transform used to convert device data to channel state, e.g. REGEX:.*?STATE=(.*?);.* + + + + Format string applied to the command, e.g. ID=671;VAL=%f + + + + Transform used to convert command to device data, e.g. JS:device.js + + + + + + Dimmer + + Channel to receive commands from a Dimmer + + + + Transform used to convert device data to channel state, e.g. REGEX:.*?STATE=(.*?);.* + + + + Send this value when receiving an ON command + + + + Send this value when receiving an OFF command + + + + Send this value when receiving an INCREASE command + + + + Send this value when receiving a DECREASE command + + + + Format string applied to the percent command, e.g. ID=671;VAL=%d + + + + Transform used to convert command to device data, e.g. JS:device.js + + + + + + Switch + + Channel to receive commands from a Switch + + + + Transform used to convert device data to channel state, e.g. REGEX:.*?STATE=(.*?);.* + + + + Send this value when receiving an ON command + + + + Send this value when receiving an OFF command + + + + Transform used to convert command to device data, e.g. JS:device.js + + + + + + Rollershutter + + Channel to receive commands from a Rollershutter + + + + Transform used to convert device data to channel state, e.g. REGEX:.*?STATE=(.*?);.* + + + + Send this value when receiving an UP command + + + + Send this value when receiving a DOWN command + + + + Send this value when receiving a STOP command + + + + Format string applied to the percent command, e.g. ID=671;VAL=%d + + + + Transform used to convert command to device data, e.g. JS:device.js + + + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index 44ae4c02d5c..8e46d1fc461 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -247,6 +247,7 @@ org.openhab.binding.seneye org.openhab.binding.sensebox org.openhab.binding.sensibo + org.openhab.binding.serial org.openhab.binding.serialbutton org.openhab.binding.shelly org.openhab.binding.silvercrestwifisocket