[modbus] Gain-offset profile (QuantityType support) and writing of individual bits of holding registers (#9980)

* [modbus] gainOffset and bitMask profiles for working with modbus data

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] README trailing whitespaces

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] README and some final renaming

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] log error with incompatible units

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] gainOffset profile: test for incompatible unit

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] example renamed

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Remove unused fields

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] gainOffset profile: make configuration parameters optional

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] xml indentantion fix

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] static code analysis fixes

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Minor fixes for null checking

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] remove comment

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] bit profile README disclaimer with many commands

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Grammar fixes in README

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Fix bit profile UI configuration

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Bit profile: Added possibility to invert value on read/write

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] fix typo with explanation of inverted

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] bit profile: unit tests for inverted parameter

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] spotless:apply

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] static checker fixes

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] write bit feature in data thing

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* wip

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] resolve itest

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] fixes

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Remove bit profile

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Fix data thing readStart validation

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] readme fix

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Remove bit profile test

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Invalidate REFRESH data cache with cacheful writes

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] cleanup

- abort if command is not convertible to 0/1 (previously wrote the
  cached data)
- fail fast conditionals instead of deep if's

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] README Fix typo in example

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] fix data thing write when child of endpoint

Also added regression test

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* Update bundles/org.openhab.binding.modbus/src/main/resources/OH-INF/config/gainOffset.xml

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>

* [modbus] performance-optimized logging

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] README: Removing xtend syntax hint, not needed anymore

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] generics typing added

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] dead code

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] avoid supressing generic type warnings

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] unnecessary generics

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] rename type parameter name

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] QU (short for quantity output) generic type instead of Q2

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Remove unused localization

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] profile constant visibility harmonized

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] spotless:apply

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
This commit is contained in:
Sami Salonen 2021-04-16 23:59:55 +03:00 committed by GitHub
parent b42101addc
commit 265fd30ba1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1217 additions and 94 deletions

View File

@ -202,9 +202,9 @@ You must give each of your data Things a reference (thing ID) that is unique for
| ------------------------------------------- | ------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------------------------------- | ------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `readValueType` | text | | (empty) | How data is read from modbus. Use empty for write-only things.<br /><br />Bit value type must be used with coils and discrete inputs. With registers all value types are applicable. Valid values are: `"int64"`, `"int64_swap"`, `"uint64"`, `"uint64_swap"`, `"float32"`, `"float32_swap"`, `"int32"`, `"int32_swap"`, `"uint32"`, `"uint32_swap"`, `"int16"`, `"uint16"`, `"int8"`, `"uint8"`, or `"bit"`. See also [Value types on read and write](#value-types-on-read-and-write). | | `readValueType` | text | | (empty) | How data is read from modbus. Use empty for write-only things.<br /><br />Bit value type must be used with coils and discrete inputs. With registers all value types are applicable. Valid values are: `"int64"`, `"int64_swap"`, `"uint64"`, `"uint64_swap"`, `"float32"`, `"float32_swap"`, `"int32"`, `"int32_swap"`, `"uint32"`, `"uint32_swap"`, `"int16"`, `"uint16"`, `"int8"`, `"uint8"`, or `"bit"`. See also [Value types on read and write](#value-types-on-read-and-write). |
| `readStart` | text | | (empty) | 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> | | `readStart` | text | | (empty) | 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> |
| `readTransform` | text | | `"default"` | Transformation to apply to polled data, after it has been converted to number using `readValueType`. <br /><br />Use "default" to communicate that no transformation is done and value should be passed as is.<br />Use `"SERVICENAME:ARG"` or `"SERVICENAME(ARG)"` (old syntax) to use transformation service `SERVICENAME` with argument `ARG`. <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. ou can chain many transformations with ∩, for example `"SERVICE1:ARG1∩SERVICE2:ARG2"`. | | `readTransform` | text | | `"default"` | Transformation to apply to polled data, after it has been converted to number using `readValueType`. <br /><br />Use "default" to communicate that no transformation is done and value should be passed as is.<br />Use `"SERVICENAME:ARG"` or `"SERVICENAME(ARG)"` (old syntax) to use transformation service `SERVICENAME` with argument `ARG`. <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. You can chain many transformations with ∩, for example `"SERVICE1:ARG1∩SERVICE2:ARG2"`. |
| `writeValueType` | text | | (empty) | How data is written to modbus. Only applicable to registers. Valid values are: `"int64"`, `"int64_swap"`, `"float32"`, `"float32_swap"`, `"int32"`, `"int32_swap"`, `"int16"`. See also [Value types on read and write](#value-types-on-read-and-write). | | `writeValueType` | text | | (empty) | How data is written to modbus. Only applicable to registers. Valid values are: `"int64"`, `"int64_swap"`, `"float32"`, `"float32_swap"`, `"int32"`, `"int32_swap"`, `"int16"`. See also [Value types on read and write](#value-types-on-read-and-write). Value of `"bit"` can be used with registers as well when `writeStart` is of format `"X.Y"` (see below). See also [Value types on read and write](#value-types-on-read-and-write). |
| `writeStart` | text | | (empty) | 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. | | `writeStart` | text | | (empty) | 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. One can use `"X.Y"` to write individual bit `Y` of an holding `X` (analogous to `readStart`). |
| `writeType` | text | | (empty) | Type of data to write. Use empty for read-only things. Valid values: `"coil"` or `"holding"`.<br /><br /> Coil uses function code (FC) FC05 or FC15. Holding register uses FC06 or FC16. See `writeMultipleEvenWithSingleRegisterOrCoil` parameter. | | `writeType` | text | | (empty) | Type of data to write. Use empty for read-only things. Valid values: `"coil"` or `"holding"`.<br /><br /> Coil uses function code (FC) FC05 or FC15. Holding register uses FC06 or FC16. See `writeMultipleEvenWithSingleRegisterOrCoil` parameter. |
| `writeTransform` | text | | `"default"` | Transformation to apply to received commands.<br /><br />Use `"default"` to communicate that no transformation is done and value should be passed as is. <br />Use `"SERVICENAME:ARG"` or `"SERVICENAME(ARG)"` (old syntax) to use transformation service `SERVICENAME` with argument `ARG`. <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. You can chain many transformations with ∩, for example `"SERVICE1:ARG1∩SERVICE2:ARG2"`. | | `writeTransform` | text | | `"default"` | Transformation to apply to received commands.<br /><br />Use `"default"` to communicate that no transformation is done and value should be passed as is. <br />Use `"SERVICENAME:ARG"` or `"SERVICENAME(ARG)"` (old syntax) to use transformation service `SERVICENAME` with argument `ARG`. <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. You can chain many transformations with ∩, for example `"SERVICE1:ARG1∩SERVICE2:ARG2"`. |
| `writeMultipleEvenWithSingleRegisterOrCoil` | boolean | | `false` | Controls how single register / coil of data is written.<br /> By default, or when 'false, FC06 ("Write single holding register") / FC05 ("Write single coil"). Or when 'true', using FC16 ("Write Multiple Holding Registers") / FC15 ("Write Multiple Coils"). | | `writeMultipleEvenWithSingleRegisterOrCoil` | boolean | | `false` | Controls how single register / coil of data is written.<br /> By default, or when 'false, FC06 ("Write single holding register") / FC05 ("Write single coil"). Or when 'true', using FC16 ("Write Multiple Holding Registers") / FC15 ("Write Multiple Coils"). |
@ -310,6 +310,21 @@ Number Temperature_Modbus_Livingroom "Temperature Living
Main documentation on `autoupdate` in [Items section of openHAB docs](https://www.openhab.org/docs/configuration/items.html#item-definition-and-syntax). Main documentation on `autoupdate` in [Items section of openHAB docs](https://www.openhab.org/docs/configuration/items.html#item-definition-and-syntax).
### Profiles
#### `modbus:gainOffset`
This profile is meant for simple scaling and offsetting of values received from the Modbus slave.
The profile works also in the reverse direction, when commanding items.
In addition, the profile allows attaching units to the raw numbers, as well as converting the quantity-aware numbers to bare numbers on write.
Profile has two parameters, `gain` (bare number or number with unit) and `pre-offset` (bare number), both of which must be provided.
When reading from Modbus, the result will be `updateTowardsItem = (raw_value_from_modbus + preOffset) * gain`.
When applying command, the calculation goes in reverse.
See examples for concrete use case with value scaling.
### Discovery ### Discovery
Device specific modbus bindings can take part in the discovery of things, and detect devices automatically. The discovery is initiated by the `tcp` and `serial` bridges when they have `enableDiscovery` setting enabled. Device specific modbus bindings can take part in the discovery of things, and detect devices automatically. The discovery is initiated by the `tcp` and `serial` bridges when they have `enableDiscovery` setting enabled.
@ -551,7 +566,7 @@ For example, `openhab-transformation-javascript` feature provides the javascript
There are three different format to specify the configuration: There are three different format to specify the configuration:
1. String `"default"`, in which case the default transformation is used. The default is to convert non-zero numbers to `ON`/`OPEN`, and zero numbers to `OFF`/`CLOSED`, respectively. If the item linked to the data channel does not accept these states, the number is converted to best-effort-basis to the states accepted by the item. For example, the extracted number is passed as-is for `Number` items, while `ON`/`OFF` would be used with `DimmerItem`. 1. String `"default"`, in which case the default transformation is used. The default is to convert non-zero numbers to `ON`/`OPEN`, and zero numbers to `OFF`/`CLOSED`, respectively. If the item linked to the data channel does not accept these states, the number is converted to best-effort-basis to the states accepted by the item. For example, the extracted number is passed as-is for `Number` items, while `ON`/`OFF` would be used with `DimmerItem`.
1. `"SERVICENAME:ARG"` for calling a transformation service. The transformation receives the extracted number as input. This is useful for example scaling (divide by x) the polled data before it is used in openHAB. See examples for more details. 1. `"SERVICENAME:ARG"` for calling a transformation service. The transformation receives the extracted number as input. This is useful for applying complex arithmetic of the polled data before it is used in openHAB. See examples for more details.
1. Any other value is interpreted as static text, in which case the actual content of the polled value is ignored. Transformation result is always the same. The transformation output is converted to best-effort-basis to the states accepted by the item. 1. Any other value is interpreted as static text, in which case the actual content of the polled value is ignored. Transformation result is always the same. The transformation output is converted to best-effort-basis to the states accepted by the item.
Consult [background documentation on items](https://www.openhab.org/docs/concepts/items.html) to understand accepted data types (state) by each item. Consult [background documentation on items](https://www.openhab.org/docs/concepts/items.html) to understand accepted data types (state) by each item.
@ -563,42 +578,8 @@ Consult [background documentation on items](https://www.openhab.org/docs/concept
There are three different format to specify the configuration: There are three different format to specify the configuration:
1. String `"default"`, in which case the default transformation is used. The default is to do no conversion to the command. 1. String `"default"`, in which case the default transformation is used. The default is to do no conversion to the command.
1. `"SERVICENAME:ARG"` for calling a transformation service. The transformation receives the command as input. This is useful for example scaling ("multiply by x") commands before the data is written to Modbus. See examples for more details. 1. `"SERVICENAME:ARG"` for calling a transformation service. The transformation receives the command as input. This is useful for applying complex arithmetic for commands before the data is written to Modbus. See examples for more details.
1. Any other value is interpreted as static text, in which case the actual command is ignored. Transformation result is always the same. 1. Any other value is interpreted as static text, in which case the actual command is ignored. Transformation result is always the same.
#### Transformation Example: Scaling
Typical use case for transformations is scaling of numbers.
The data in Modbus slaves is quite commonly encoded as integers, and thus scaling is necessary to convert them to useful float numbers.
`transform/multiply10.js`:
```javascript
// Wrap everything in a function (no global variable pollution)
// variable "input" contains data passed by openHAB
(function(inputData) {
// on read: the polled number as string
// on write: openHAB command as string
var MULTIPLY_BY = 10;
return Math.round(parseFloat(inputData, 10) * MULTIPLY_BY);
})(input)
```
`transform/divide10.js`:
```javascript
// Wrap everything in a function (no global variable pollution)
// variable "input" contains data passed by openHAB
(function(inputData) {
// on read: the polled number as string
// on write: openHAB command as string
var DIVIDE_BY = 10;
return parseFloat(inputData) / DIVIDE_BY;
})(input)
```
See [Scaling example](#scaling-example) for full example with things, items and a sitemap.
#### Example: Inverting Binary Data On Read And Write #### Example: Inverting Binary Data On Read And Write
This example transformation is able to invert "boolean" input. This example transformation is able to invert "boolean" input.
@ -630,7 +611,7 @@ Please refer to the comments for more explanations.
`things/modbus_ex1.things`: `things/modbus_ex1.things`:
```xtend ```
Bridge modbus:tcp:localhostTCP [ host="127.0.0.1", port=502, id=2 ] { Bridge modbus:tcp:localhostTCP [ host="127.0.0.1", port=502, id=2 ] {
// read-write for coils. Reading 4 coils, with index 4, and 5. // read-write for coils. Reading 4 coils, with index 4, and 5.
@ -678,7 +659,7 @@ Bridge modbus:tcp:localhostTCP [ host="127.0.0.1", port=502, id=2 ] {
`items/modbus_ex1.items`: `items/modbus_ex1.items`:
```xtend ```
Switch DO4 "Digital Output index 4 [%d]" { channel="modbus:data:localhostTCP:coils:do4:switch" } Switch DO4 "Digital Output index 4 [%d]" { channel="modbus:data:localhostTCP:coils:do4:switch" }
Switch DO5 "Digital Output index 5 [%d]" { channel="modbus:data:localhostTCP:coils:do5:switch" } Switch DO5 "Digital Output index 5 [%d]" { channel="modbus:data:localhostTCP:coils:do5:switch" }
@ -696,7 +677,7 @@ Number Holding5writeonly "Holding index 5 [%.1f]" { channel="modbu
`sitemaps/modbus_ex1.sitemap`: `sitemaps/modbus_ex1.sitemap`:
```xtend ```
sitemap modbus_ex1 label="modbus_ex1" sitemap modbus_ex1 label="modbus_ex1"
{ {
Frame { Frame {
@ -728,7 +709,7 @@ Toggling these switches always have the same effect: either setting or resetting
`things/modbus_ex2.things`: `things/modbus_ex2.things`:
```xtend ```
Bridge modbus:tcp:localhostTCPex2 [ host="127.0.0.1", port=502 ] { Bridge modbus:tcp:localhostTCPex2 [ host="127.0.0.1", port=502 ] {
Bridge poller items [ start=4, length=2, refresh=1000, type="discrete" ] { Bridge poller items [ start=4, length=2, refresh=1000, type="discrete" ] {
@ -746,7 +727,7 @@ Bridge modbus:tcp:localhostTCPex2 [ host="127.0.0.1", port=502 ] {
`items/modbus_ex2.items`: `items/modbus_ex2.items`:
```xtend ```
Switch ReadDI4WriteDO5 "Coil 4/5 mix [%d]" { channel="modbus:data:localhostTCPex2:items:readDiscrete4WriteCoil5:switch" } Switch ReadDI4WriteDO5 "Coil 4/5 mix [%d]" { channel="modbus:data:localhostTCPex2:items:readDiscrete4WriteCoil5:switch" }
Switch ResetDO5 "Flip to turn Coil 5 OFF [%d]" { channel="modbus:data:localhostTCPex2:items:resetCoil5:switch" } Switch ResetDO5 "Flip to turn Coil 5 OFF [%d]" { channel="modbus:data:localhostTCPex2:items:resetCoil5:switch" }
Switch SetDO5 "Flip to turn Coil 5 ON [%d]" { channel="modbus:data:localhostTCPex2:items:setCoil5:switch" } Switch SetDO5 "Flip to turn Coil 5 ON [%d]" { channel="modbus:data:localhostTCPex2:items:setCoil5:switch" }
@ -756,7 +737,7 @@ Contact Coil5 "Coil 5 [%d]" { channel="modbus:data:localhostTCPex2
`sitemaps/modbus_ex2.sitemap`: `sitemaps/modbus_ex2.sitemap`:
```xtend ```
sitemap modbus_ex2 label="modbus_ex2" sitemap modbus_ex2 label="modbus_ex2"
{ {
Frame { Frame {
@ -770,37 +751,83 @@ sitemap modbus_ex2 label="modbus_ex2"
### Scaling Example ### Scaling Example
This example divides value on read, and multiplies them on write, using JS transforms. Often Modbus slave might have the numbers stored as integers, with no information of the measurement unit.
In openHAB, it is recommended to scale and attach units for the read data.
In the below example, modbus data needs to be multiplied by `0.1` to convert the value to Celsius.
For example, raw modbus register value of `45` corresponds to `4.5 °C`.
Note how that unit can be specified within the `gain` parameter of `modbus:gainOffset` profile.
This enables the use of quantity-aware `Number` item `Number:Temperature`.
The profile also works the other way round, scaling the commands sent to the item to bare-numbers suitable for Modbus.
`things/modbus_ex_scaling.things`: `things/modbus_ex_scaling.things`:
```xtend ```
Bridge modbus:tcp:localhostTCP3 [ host="127.0.0.1", port=502 ] { Bridge modbus:tcp:localhostTCP3 [ host="127.0.0.1", port=502 ] {
Bridge poller holdingPoller [ start=5, length=1, refresh=5000, type="holding" ] { Bridge poller holdingPoller [ start=5, length=1, refresh=5000, type="holding" ] {
Thing data holding5Scaled [ readStart="5", readValueType="int16", readTransform="JS:divide10.js", writeStart="5", writeValueType="int16", writeType="holding", writeTransform="JS:multiply10.js" ] Thing data temperatureDeciCelsius [ readStart="5", readValueType="int16", writeStart="5", writeValueType="int16", writeType="holding" ]
} }
} }
``` ```
`items/modbus_ex_scaling.items`: `items/modbus_ex_scaling.items`:
```xtend ```
Number Holding5Scaled "Holding index 5 scaled [%.1f]" { channel="modbus:data:localhostTCP3:holdingPoller:holding5Scaled:number" } Number:Temperature TemperatureItem "Temperature [%.1f °C]" { channel="modbus:data:localhostTCP3:holdingPoller:temperatureDeciCelsius:number"[ profile="modbus:gainOffset", gain="0.1 °C", offset="0" ] }
``` ```
`sitemaps/modbus_ex_scaling.sitemap`: `sitemaps/modbus_ex_scaling.sitemap`:
```xtend ```
sitemap modbus_ex_scaling label="modbus_ex_scaling" sitemap modbus_ex_scaling label="modbus_ex_scaling"
{ {
Frame { Frame {
Text item=Holding5Scaled Text item=TemperatureItem
Setpoint item=Holding5Scaled minValue=0 maxValue=100 step=20 Setpoint item=TemperatureItem minValue=0 maxValue=100 step=20
} }
} }
``` ```
See [transformation example](#transformation-example-scaling) for the `divide10.js` and `multiply10.js`.
### Commanding Individual Bits
In Modbus, holding registers represent 16 bits of data. The protocol allow to write the whole register at once.
The binding provides convenience functionality to command individual bits of a holding register by keeping a cache of the register internally.
In order to use this feature, one specifies `writeStart="X.Y"` (register `X`, bit `Y`) with `writeValueType="bit"` and `writeType="holding"`.
`things/modbus_ex_command_bit.things`:
```
Bridge modbus:tcp:localhostTCP3 [ host="127.0.0.1", port=502 ] {
Bridge poller holdingPoller [ start=5, length=1, refresh=5000, type="holding" ] {
Thing data register5 [ readStart="5.1", readValueType="bit", writeStart="5.1", writeValueType="bit", writeType="holding" ]
Thing data register5Bit1 [ readStart="5.1", readValueType="bit" ]
}
}
```
`items/modbus_ex_command_bit.items`:
```
Switch SecondLeastSignificantBit "2nd least significant bit write switch [%d]" { channel="modbus:data:localhostTCP3:holdingPoller:register5:switch" }
Number SecondLeastSignificantBitAltRead "2nd least significant bit is now [%d]" { channel="modbus:data:localhostTCP3:holdingPoller:register5Bit1:number" }
```
`sitemaps/modbus_ex_command_bit.sitemap`:
```
sitemap modbus_ex_command_bit label="modbus_ex_command_bit"
{
Frame {
Text item=SecondLeastSignificantBitAltRead
Switch item=SecondLeastSignificantBit
}
}
```
### Dimmer Example ### Dimmer Example
@ -812,7 +839,7 @@ Example for a dimmer device where 255 register value = 100% for fully ON:
`things/modbus_ex_dimmer.things`: `things/modbus_ex_dimmer.things`:
```xtend ```
Bridge modbus:tcp:remoteTCP [ host="192.168.0.10", port=502 ] { Bridge modbus:tcp:remoteTCP [ host="192.168.0.10", port=502 ] {
Bridge poller MBDimmer [ start=4700, length=2, refresh=1000, type="holding" ] { Bridge poller MBDimmer [ start=4700, length=2, refresh=1000, type="holding" ] {
Thing data DimmerReg [ readStart="4700", readValueType="uint16", readTransform="JS:dimread255.js", writeStart="4700", writeValueType="uint16", writeType="holding", writeTransform="JS:dimwrite255.js" ] Thing data DimmerReg [ readStart="4700", readValueType="uint16", readTransform="JS:dimread255.js", writeStart="4700", writeValueType="uint16", writeType="holding", writeTransform="JS:dimwrite255.js" ]
@ -821,13 +848,13 @@ Bridge modbus:tcp:remoteTCP [ host="192.168.0.10", port=502 ] {
``` ```
`items/modbus_ex_dimmer.items`: `items/modbus_ex_dimmer.items`:
```xtend ```
Dimmer myDimmer "My Dimmer d2 [%.1f]" { channel="modbus:data:remoteTCP:MBDimmer:DimmerReg:dimmer" } Dimmer myDimmer "My Dimmer d2 [%.1f]" { channel="modbus:data:remoteTCP:MBDimmer:DimmerReg:dimmer" }
``` ```
`sitemaps/modbus_ex_dimmer.sitemap`: `sitemaps/modbus_ex_dimmer.sitemap`:
```xtend ```
sitemap modbus_ex_dimmer label="modbus_ex_dimmer" sitemap modbus_ex_dimmer label="modbus_ex_dimmer"
{ {
Frame { Frame {
@ -890,7 +917,7 @@ The logic of processing commands are summarized in the table
`things/modbus_ex_rollershutter.things`: `things/modbus_ex_rollershutter.things`:
```xtend ```
Bridge modbus:tcp:localhostTCPRollerShutter [ host="127.0.0.1", port=502 ] { Bridge modbus:tcp:localhostTCPRollerShutter [ host="127.0.0.1", port=502 ] {
Bridge poller holding [ start=0, length=3, refresh=1000, type="holding" ] { Bridge poller holding [ start=0, length=3, refresh=1000, type="holding" ] {
// Since we are using advanced transformation outputting JSON, // Since we are using advanced transformation outputting JSON,
@ -907,7 +934,7 @@ Bridge modbus:tcp:localhostTCPRollerShutter [ host="127.0.0.1", port=502 ] {
`items/modbus_ex_rollershutter.items`: `items/modbus_ex_rollershutter.items`:
```xtend ```
// We disable auto-update to make sure that rollershutter position is updated from the slave, not "automatically" via commands // We disable auto-update to make sure that rollershutter position is updated from the slave, not "automatically" via commands
Rollershutter RollershutterItem "Roller shutter position [%.1f]" <temperature> { autoupdate="false", channel="modbus:data:localhostTCPRollerShutter:holding:rollershutterData:rollershutter" } Rollershutter RollershutterItem "Roller shutter position [%.1f]" <temperature> { autoupdate="false", channel="modbus:data:localhostTCPRollerShutter:holding:rollershutterData:rollershutter" }
@ -919,7 +946,7 @@ Number RollershutterItemDebug2 "Roller shutter Debug 2 [%d]" <temperature> { cha
`sitemaps/modbus_ex_rollershutter.sitemap`: `sitemaps/modbus_ex_rollershutter.sitemap`:
```xtend ```
sitemap modbus_ex_rollershutter label="modbus_ex_rollershutter" { sitemap modbus_ex_rollershutter label="modbus_ex_rollershutter" {
Switch item=RollershutterItem label="Roller shutter [(%d)]" mappings=[UP="up", STOP="X", DOWN="down", MOVE="move"] Switch item=RollershutterItem label="Roller shutter [(%d)]" mappings=[UP="up", STOP="X", DOWN="down", MOVE="move"]

View File

@ -15,6 +15,7 @@ package org.openhab.binding.modbus.handler;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
@ -31,6 +32,7 @@ import org.openhab.core.io.transport.modbus.ModbusFailureCallback;
import org.openhab.core.io.transport.modbus.ModbusReadCallback; import org.openhab.core.io.transport.modbus.ModbusReadCallback;
import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode; import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint; import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
import org.openhab.core.io.transport.modbus.PollTask; import org.openhab.core.io.transport.modbus.PollTask;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
@ -96,6 +98,10 @@ public class ModbusPollerThingHandler extends BaseBridgeHandler {
@Override @Override
public synchronized void handle(AsyncModbusReadResult result) { public synchronized void handle(AsyncModbusReadResult result) {
// Casting to allow registers.orElse(null) below..
Optional<@Nullable ModbusRegisterArray> registers = (Optional<@Nullable ModbusRegisterArray>) result
.getRegisters();
lastPolledDataCache.set(registers.orElse(null));
handleResult(new PollResult(result)); handleResult(new PollResult(result));
} }
@ -186,6 +192,7 @@ public class ModbusPollerThingHandler extends BaseBridgeHandler {
private volatile @Nullable ModbusReadRequestBlueprint request; private volatile @Nullable ModbusReadRequestBlueprint request;
private volatile boolean disposed; private volatile boolean disposed;
private volatile List<ModbusDataThingHandler> childCallbacks = new CopyOnWriteArrayList<>(); private volatile List<ModbusDataThingHandler> childCallbacks = new CopyOnWriteArrayList<>();
private volatile AtomicReference<@Nullable ModbusRegisterArray> lastPolledDataCache = new AtomicReference<>();
private @NonNullByDefault({}) ModbusCommunicationInterface comms; private @NonNullByDefault({}) ModbusCommunicationInterface comms;
private ReadCallbackDelegator callbackDelegator = new ReadCallbackDelegator(); private ReadCallbackDelegator callbackDelegator = new ReadCallbackDelegator();
@ -288,6 +295,7 @@ public class ModbusPollerThingHandler extends BaseBridgeHandler {
unregisterPollTask(); unregisterPollTask();
this.callbackDelegator.resetCache(); this.callbackDelegator.resetCache();
comms = null; comms = null;
lastPolledDataCache.set(null);
} }
/** /**
@ -420,6 +428,20 @@ public class ModbusPollerThingHandler extends BaseBridgeHandler {
if (localRequest == null) { if (localRequest == null) {
return; return;
} }
ModbusRegisterArray possiblyMutatedCache = lastPolledDataCache.get();
AtomicStampedValue<PollResult> lastPollResult = callbackDelegator.lastResult;
if (lastPollResult != null && possiblyMutatedCache != null) {
AsyncModbusReadResult lastSuccessfulPollResult = lastPollResult.getValue().result;
if (lastSuccessfulPollResult != null) {
ModbusRegisterArray lastRegisters = ((Optional<@Nullable ModbusRegisterArray>) lastSuccessfulPollResult
.getRegisters()).orElse(null);
if (lastRegisters != null && !possiblyMutatedCache.equals(lastRegisters)) {
// Register has been mutated in between by a data thing that writes "individual bits"
// Invalidate cache for a fresh poll
callbackDelegator.resetCache();
}
}
}
long oldDataThreshold = System.currentTimeMillis() - cacheMillis; long oldDataThreshold = System.currentTimeMillis() - cacheMillis;
boolean cacheWasRecentEnoughForUpdate = cacheMillis > 0 boolean cacheWasRecentEnoughForUpdate = cacheMillis > 0
@ -438,4 +460,8 @@ public class ModbusPollerThingHandler extends BaseBridgeHandler {
} }
} }
} }
public AtomicReference<@Nullable ModbusRegisterArray> getLastPolledDataCache() {
return lastPolledDataCache;
}
} }

View File

@ -25,6 +25,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -78,6 +79,7 @@ import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType; import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State; import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType; import org.openhab.core.types.UnDefType;
import org.openhab.core.util.HexUtils;
import org.osgi.framework.BundleContext; import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil; import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -133,7 +135,8 @@ public class ModbusDataThingHandler extends BaseThingHandler {
private volatile @Nullable CascadedValueTransformationImpl writeTransformation; private volatile @Nullable CascadedValueTransformationImpl writeTransformation;
private volatile Optional<Integer> readIndex = Optional.empty(); private volatile Optional<Integer> readIndex = Optional.empty();
private volatile Optional<Integer> readSubIndex = Optional.empty(); private volatile Optional<Integer> readSubIndex = Optional.empty();
private volatile @Nullable Integer writeStart; private volatile Optional<Integer> writeStart = Optional.empty();
private volatile Optional<Integer> writeSubIndex = Optional.empty();
private volatile int pollStart; private volatile int pollStart;
private volatile int slaveId; private volatile int slaveId;
private volatile @Nullable ModbusReadFunctionCode functionCode; private volatile @Nullable ModbusReadFunctionCode functionCode;
@ -200,8 +203,8 @@ public class ModbusDataThingHandler extends BaseThingHandler {
// We did not have JSON output from the transformation, so writeStart is absolute required. Abort if it is // We did not have JSON output from the transformation, so writeStart is absolute required. Abort if it is
// missing // missing
Integer writeStart = this.writeStart; Optional<Integer> writeStart = this.writeStart;
if (writeStart == null) { if (writeStart.isEmpty()) {
logger.debug( logger.debug(
"Thing {} '{}': not processing command {} since writeStart is missing and transformation output is not a JSON", "Thing {} '{}': not processing command {} since writeStart is missing and transformation output is not a JSON",
getThing().getUID(), getThing().getLabel(), command); getThing().getUID(), getThing().getLabel(), command);
@ -216,7 +219,7 @@ public class ModbusDataThingHandler extends BaseThingHandler {
} }
ModbusWriteRequestBlueprint request = requestFromCommand(channelUID, command, config, transformedCommand.get(), ModbusWriteRequestBlueprint request = requestFromCommand(channelUID, command, config, transformedCommand.get(),
writeStart); writeStart.get());
if (request == null) { if (request == null) {
return; return;
} }
@ -267,7 +270,9 @@ public class ModbusDataThingHandler extends BaseThingHandler {
ModbusWriteRequestBlueprint request; ModbusWriteRequestBlueprint request;
boolean writeMultiple = config.isWriteMultipleEvenWithSingleRegisterOrCoil(); boolean writeMultiple = config.isWriteMultipleEvenWithSingleRegisterOrCoil();
String writeType = config.getWriteType(); String writeType = config.getWriteType();
ModbusPollerThingHandler pollerHandler = this.pollerHandler;
if (writeType == null) { if (writeType == null) {
// disposed thing
return null; return null;
} }
if (writeType.equals(WRITE_TYPE_COIL)) { if (writeType.equals(WRITE_TYPE_COIL)) {
@ -289,7 +294,44 @@ public class ModbusDataThingHandler extends BaseThingHandler {
logger.warn("Received command but write value type not set! Ignoring command"); logger.warn("Received command but write value type not set! Ignoring command");
return null; return null;
} }
ModbusRegisterArray data = ModbusBitUtilities.commandToRegisters(transformedCommand, writeValueType); final ModbusRegisterArray data;
if (writeValueType.equals(ValueType.BIT)) {
if (writeSubIndex.isEmpty()) {
// Should not happen! should be in configuration error
logger.error("Bug: sub index not present but writeValueType=BIT. Should be in configuration error");
return null;
}
Optional<Boolean> commandBool = ModbusBitUtilities.translateCommand2Boolean(transformedCommand);
if (commandBool.isEmpty()) {
logger.warn(
"Data thing is configured to write individual bit but we received command that is not convertible to 0/1 bit. Ignoring.");
return null;
} else if (pollerHandler == null) {
logger.warn("Bug: sub index present but not child of poller. Should be in configuration erro");
return null;
}
// writing bit of an individual register. Using cache from poller
AtomicReference<@Nullable ModbusRegisterArray> cachedRegistersRef = pollerHandler
.getLastPolledDataCache();
ModbusRegisterArray mutatedRegisters = cachedRegistersRef
.updateAndGet(cachedRegisters -> cachedRegisters == null ? null
: combineCommandWithRegisters(cachedRegisters, writeStart, writeSubIndex.get(),
commandBool.get()));
if (mutatedRegisters == null) {
logger.warn(
"Received command to thing with writeValueType=bit (pointing to individual bit of a holding register) but internal cache not yet populated. Ignoring command");
return null;
}
// extract register (first byte index = register index * 2)
byte[] allMutatedBytes = mutatedRegisters.getBytes();
int writeStartRelative = writeStart - pollStart;
data = new ModbusRegisterArray(allMutatedBytes[writeStartRelative * 2],
allMutatedBytes[writeStartRelative * 2 + 1]);
} else {
data = ModbusBitUtilities.commandToRegisters(transformedCommand, writeValueType);
}
writeMultiple = writeMultiple || data.size() > 1; writeMultiple = writeMultiple || data.size() > 1;
request = new ModbusWriteRegisterRequestBlueprint(slaveId, writeStart, data, writeMultiple, request = new ModbusWriteRegisterRequestBlueprint(slaveId, writeStart, data, writeMultiple,
config.getWriteMaxTries()); config.getWriteMaxTries());
@ -304,6 +346,33 @@ public class ModbusDataThingHandler extends BaseThingHandler {
return request; return request;
} }
/**
* Combine boolean-like command with registers. Updated registers are returned
*
* @return
*/
private ModbusRegisterArray combineCommandWithRegisters(ModbusRegisterArray registers, int registerIndex,
int bitIndex, boolean b) {
byte[] allBytes = registers.getBytes();
int bitIndexWithinRegister = bitIndex % 16;
boolean hiByte = bitIndexWithinRegister >= 8;
int indexWithinByte = bitIndexWithinRegister % 8;
int registerIndexRelative = registerIndex - pollStart;
int byteIndex = 2 * registerIndexRelative + (hiByte ? 0 : 1);
if (b) {
allBytes[byteIndex] |= 1 << indexWithinByte;
} else {
allBytes[byteIndex] &= ~(1 << indexWithinByte);
}
if (logger.isTraceEnabled()) {
logger.trace(
"Boolean-like command {} from item, combining command with internal register ({}) with registerIndex={} (relative {}), bitIndex={}, resulting register {}",
b, HexUtils.bytesToHex(registers.getBytes()), registerIndex, registerIndexRelative, bitIndex,
HexUtils.bytesToHex(allBytes));
}
return new ModbusRegisterArray(allBytes);
}
private void processJsonTransform(Command command, String transformOutput) { private void processJsonTransform(Command command, String transformOutput) {
ModbusCommunicationInterface localComms = this.comms; ModbusCommunicationInterface localComms = this.comms;
if (localComms == null) { if (localComms == null) {
@ -398,7 +467,8 @@ public class ModbusDataThingHandler extends BaseThingHandler {
writeTransformation = null; writeTransformation = null;
readIndex = Optional.empty(); readIndex = Optional.empty();
readSubIndex = Optional.empty(); readSubIndex = Optional.empty();
writeStart = null; writeStart = Optional.empty();
writeSubIndex = Optional.empty();
pollStart = 0; pollStart = 0;
slaveId = 0; slaveId = 0;
comms = null; comms = null;
@ -553,19 +623,6 @@ public class ModbusDataThingHandler extends BaseThingHandler {
} }
} }
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 { try {
if (!writeParametersHavingTransformationOnly) { if (!writeParametersHavingTransformationOnly) {
String localWriteStart = config.getWriteStart(); String localWriteStart = config.getWriteStart();
@ -574,13 +631,57 @@ public class ModbusDataThingHandler extends BaseThingHandler {
config.getWriteStart()); config.getWriteStart());
throw new ModbusConfigurationException(errmsg); throw new ModbusConfigurationException(errmsg);
} }
writeStart = Integer.parseInt(localWriteStart.trim()); String[] writeParts = localWriteStart.split("\\.", 2);
try {
writeStart = Optional.of(Integer.parseInt(writeParts[0]));
if (writeParts.length == 2) {
writeSubIndex = Optional.of(Integer.parseInt(writeParts[1]));
} else {
writeSubIndex = Optional.empty();
}
} catch (IllegalArgumentException e) {
String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(),
config.getReadStart());
throw new ModbusConfigurationException(errmsg);
}
} }
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(), String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(),
config.getWriteStart()); config.getWriteStart());
throw new ModbusConfigurationException(errmsg); 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 (writeSubIndex.isEmpty() && !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);
}
if (writeSubIndex.isPresent()) {
if (writeValueTypeMissing || writeTypeMissing || !WRITE_TYPE_HOLDING.equals(config.getWriteType())
|| !ModbusConstants.ValueType.BIT.equals(localWriteValueType) || childOfEndpoint) {
String errmsg = String.format(
"Thing %s invalid writeType, writeValueType or parent. Since writeStart=X.Y, one should set writeType=holding, writeValueType=bit and have the thing as child of poller",
getThing().getUID(), config.getWriteStart());
throw new ModbusConfigurationException(errmsg);
}
ModbusReadRequestBlueprint readRequest = this.readRequest;
if (readRequest == null
|| readRequest.getFunctionCode() != ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS) {
String errmsg = String.format(
"Thing %s invalid. Since writeStart=X.Y, expecting poller reading holding registers.",
getThing().getUID());
throw new ModbusConfigurationException(errmsg);
}
}
validateWriteIndex();
} else { } else {
isWriteEnabled = false; isWriteEnabled = false;
} }
@ -646,6 +747,41 @@ public class ModbusDataThingHandler extends BaseThingHandler {
} }
} }
private void validateWriteIndex() throws ModbusConfigurationException {
@Nullable
ModbusReadRequestBlueprint readRequest = this.readRequest;
if (!writeStart.isPresent() || !writeSubIndex.isPresent()) {
//
// this validation is really about writeStart=X.Y validation
//
return;
} else if (readRequest == null) {
// should not happen, already validated
throw new ModbusConfigurationException("Must poll data with writeStart=X.Y");
}
if (writeSubIndex.isPresent() && (writeSubIndex.get() + 1) > 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() * 16;
int pollEndBitIndex = pollStartBitIndex + readRequest.getDataLength() * 16 - 1;
// Determine bit positions read, both start and end inclusive
int writeStartBitIndex = writeStart.get() * 16 + readSubIndex.orElse(0);
int writeEndBitIndex = writeStartBitIndex - 1;
if (writeStartBitIndex < pollStartBitIndex || writeEndBitIndex > pollEndBitIndex) {
String errmsg = String.format(
"Out-of-bounds: Poller is reading from index %d to %d (inclusive) but this thing configured to write starting from element %d. Must write within polled limits",
pollStartBitIndex / 16, pollEndBitIndex / 16, writeStart.get());
throw new ModbusConfigurationException(errmsg);
}
}
private boolean containsOnOff(List<Class<? extends State>> channelAcceptedDataTypes) { private boolean containsOnOff(List<Class<? extends State>> channelAcceptedDataTypes) {
return channelAcceptedDataTypes.stream().anyMatch(clz -> { return channelAcceptedDataTypes.stream().anyMatch(clz -> {
return clz.equals(OnOffType.class); return clz.equals(OnOffType.class);

View File

@ -0,0 +1,258 @@
/**
* Copyright (c) 2010-2021 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.profiles;
import java.math.BigDecimal;
import java.util.Optional;
import javax.measure.Quantity;
import javax.measure.UnconvertibleException;
import javax.measure.Unit;
import javax.measure.quantity.Dimensionless;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.profiles.ProfileCallback;
import org.openhab.core.thing.profiles.ProfileContext;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.openhab.core.thing.profiles.StateProfile;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.Type;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Profile for applying gain and offset to values.
*
* Output of the profile is
* - (incoming value + pre-gain-offset) * gain (update towards item)
* - (incoming value / gain) - pre-gain-offset (command from item)
*
* Gain can also specify unit of the result, converting otherwise bare numbers to ones with quantity.
*
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class ModbusGainOffsetProfile<Q extends Quantity<Q>> implements StateProfile {
private final Logger logger = LoggerFactory.getLogger(ModbusGainOffsetProfile.class);
private static final String PREGAIN_OFFSET_PARAM = "pre-gain-offset";
private static final String GAIN_PARAM = "gain";
private final ProfileCallback callback;
private final ProfileContext context;
private Optional<QuantityType<Dimensionless>> pregainOffset;
private Optional<QuantityType<Q>> gain;
public ModbusGainOffsetProfile(ProfileCallback callback, ProfileContext context) {
this.callback = callback;
this.context = context;
{
Object rawOffsetValue = orDefault("0", this.context.getConfiguration().get(PREGAIN_OFFSET_PARAM));
logger.debug("Configuring profile with {} parameter '{}'", PREGAIN_OFFSET_PARAM, rawOffsetValue);
pregainOffset = parameterAsQuantityType(PREGAIN_OFFSET_PARAM, rawOffsetValue, Units.ONE);
}
{
Object gainValue = orDefault("1", this.context.getConfiguration().get(GAIN_PARAM));
logger.debug("Configuring profile with {} parameter '{}'", GAIN_PARAM, gainValue);
gain = parameterAsQuantityType(GAIN_PARAM, gainValue);
}
}
public boolean isValid() {
return pregainOffset.isPresent() && gain.isPresent();
}
public Optional<QuantityType<Dimensionless>> getPregainOffset() {
return pregainOffset;
}
public Optional<QuantityType<Q>> getGain() {
return gain;
}
@Override
public ProfileTypeUID getProfileTypeUID() {
return ModbusProfiles.GAIN_OFFSET;
}
@Override
public void onStateUpdateFromItem(State state) {
// no-op
}
@Override
public void onCommandFromItem(Command command) {
Type result = applyGainOffset(command, false);
if (result instanceof Command) {
logger.trace("Command '{}' from item, sending converted '{}' state towards handler.", command, result);
callback.handleCommand((Command) result);
}
}
@Override
public void onCommandFromHandler(Command command) {
Type result = applyGainOffset(command, true);
if (result instanceof Command) {
logger.trace("Command '{}' from handler, sending converted '{}' command towards item.", command, result);
callback.sendCommand((Command) result);
}
}
@Override
public void onStateUpdateFromHandler(State state) {
State result = (State) applyGainOffset(state, true);
logger.trace("State update '{}' from handler, sending converted '{}' state towards item.", state, result);
callback.sendUpdate(result);
}
private Type applyGainOffset(Type state, boolean towardsItem) {
Type result = UnDefType.UNDEF;
Optional<QuantityType<Q>> localGain = gain;
Optional<QuantityType<Dimensionless>> localPregainOffset = pregainOffset;
if (localGain.isEmpty() || localPregainOffset.isEmpty()) {
logger.warn("Gain or offset unavailable. Check logs for configuration errors.");
return UnDefType.UNDEF;
} else if (state instanceof UnDefType) {
return UnDefType.UNDEF;
}
QuantityType<Q> gain = localGain.get();
QuantityType<Dimensionless> pregainOffsetQt = localPregainOffset.get();
String formula = towardsItem ? String.format("( '%s' + '%s') * '%s'", state, pregainOffsetQt, gain)
: String.format("'%s'/'%s' - '%s'", state, gain, pregainOffsetQt);
if (state instanceof QuantityType) {
try {
if (towardsItem) {
@SuppressWarnings("unchecked") // xx.toUnit(ONE) returns null or QuantityType<Dimensionless>
@Nullable
QuantityType<Dimensionless> qtState = (QuantityType<Dimensionless>) (((QuantityType<?>) state)
.toUnit(Units.ONE));
if (qtState == null) {
logger.warn("Profile can only process plain numbers from handler. Got unit {}. Returning UNDEF",
((QuantityType<?>) state).getUnit());
return UnDefType.UNDEF;
}
QuantityType<Dimensionless> offsetted = qtState.add(pregainOffsetQt);
result = applyGainTowardsItem(offsetted, gain);
} else {
final QuantityType<?> qtState = (QuantityType<?>) state;
result = applyGainTowardsHandler(qtState, gain).subtract(pregainOffsetQt);
}
} catch (UnconvertibleException | UnsupportedOperationException e) {
logger.warn(
"Cannot apply gain ('{}') and offset ('{}') to state ('{}') (formula {}) because types do not match (towardsItem={}): {}",
gain, pregainOffsetQt, state, formula, towardsItem, e.getMessage());
return UnDefType.UNDEF;
}
} else if (state instanceof DecimalType) {
DecimalType decState = (DecimalType) state;
return applyGainOffset(new QuantityType<>(decState, Units.ONE), towardsItem);
} else if (state instanceof RefreshType) {
result = state;
} else {
logger.warn(
"Gain '{}' cannot be applied to the incompatible state '{}' of type {} sent from the binding (towardsItem={}). Returning original state.",
gain, state, state.getClass().getSimpleName(), towardsItem);
result = state;
}
return result;
}
private Optional<QuantityType<Q>> parameterAsQuantityType(String parameterName, Object parameterValue) {
return parameterAsQuantityType(parameterName, parameterValue, null);
}
private <QU extends Quantity<QU>> Optional<QuantityType<QU>> parameterAsQuantityType(String parameterName,
Object parameterValue, @Nullable Unit<QU> assertUnit) {
Optional<QuantityType<QU>> result = Optional.empty();
Unit<QU> sourceUnit = null;
if (parameterValue instanceof String) {
try {
QuantityType<QU> qt = new QuantityType<>((String) parameterValue);
result = Optional.of(qt);
sourceUnit = qt.getUnit();
} catch (IllegalArgumentException e) {
logger.error("Cannot convert value '{}' of parameter '{}' into a QuantityType.", parameterValue,
parameterName);
}
} else if (parameterValue instanceof BigDecimal) {
BigDecimal parameterBigDecimal = (BigDecimal) parameterValue;
result = Optional.of(new QuantityType<QU>(parameterBigDecimal.toString()));
} else {
logger.error("Parameter '{}' is not of type String or BigDecimal", parameterName);
return result;
}
result = result.map(quantityType -> convertUnit(quantityType, assertUnit));
if (result.isEmpty()) {
logger.error("Unable to convert parameter '{}' to unit {}. Unit was {}.", parameterName, assertUnit,
sourceUnit);
}
return result;
}
private <QU extends Quantity<QU>> @Nullable QuantityType<QU> convertUnit(QuantityType<QU> quantityType,
@Nullable Unit<QU> unit) {
if (unit == null) {
return quantityType;
}
QuantityType<QU> normalizedQt = quantityType.toUnit(unit);
if (normalizedQt != null) {
return normalizedQt;
} else {
return null;
}
}
/**
* Calculate qtState * gain or qtState/gain
*
* When the conversion is towards the handler (towardsItem=false), unit will be ONE
*
*/
@SuppressWarnings("unchecked") // Safe cast since QU = Dimensionless * QU
private <QU extends Quantity<QU>> QuantityType<QU> applyGainTowardsItem(QuantityType<Dimensionless> qtState,
QuantityType<QU> gainDelta) {
return (QuantityType<QU>) qtState.multiply(gainDelta);
}
private QuantityType<Dimensionless> applyGainTowardsHandler(QuantityType<?> qtState, QuantityType<?> gainDelta) {
QuantityType<?> plain = qtState.toUnit(gainDelta.getUnit());
if (plain == null) {
throw new UnconvertibleException(
String.format("Cannot process command '%s', unit should compatible with gain", qtState));
}
return new QuantityType<>(plain.toBigDecimal().divide(gainDelta.toBigDecimal()), Units.ONE);
}
private static Object orDefault(Object defaultValue, @Nullable Object value) {
if (value == null) {
return defaultValue;
} else if (value instanceof String && ((String) value).isBlank()) {
return defaultValue;
} else {
return value;
}
}
}

View File

@ -0,0 +1,65 @@
/**
* Copyright (c) 2010-2021 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.profiles;
import static org.openhab.binding.modbus.internal.profiles.ModbusProfiles.*;
import java.util.Collection;
import java.util.Locale;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.profiles.Profile;
import org.openhab.core.thing.profiles.ProfileCallback;
import org.openhab.core.thing.profiles.ProfileContext;
import org.openhab.core.thing.profiles.ProfileFactory;
import org.openhab.core.thing.profiles.ProfileType;
import org.openhab.core.thing.profiles.ProfileTypeProvider;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.osgi.service.component.annotations.Component;
/**
* A factory and advisor for modbus profiles.
*
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
@Component(service = { ProfileFactory.class, ProfileTypeProvider.class })
public class ModbusProfileFactory implements ProfileFactory, ProfileTypeProvider {
private static final Set<ProfileType> SUPPORTED_PROFILE_TYPES = Set.of(GAIN_OFFSET_TYPE);
private static final Set<ProfileTypeUID> SUPPORTED_PROFILE_TYPE_UIDS = Set.of(GAIN_OFFSET);
@Override
public @Nullable Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback,
ProfileContext context) {
if (GAIN_OFFSET.equals(profileTypeUID)) {
return new ModbusGainOffsetProfile<>(callback, context);
} else {
return null;
}
}
@Override
public Collection<ProfileType> getProfileTypes(@Nullable Locale locale) {
return SUPPORTED_PROFILE_TYPES;
}
@Override
public Collection<ProfileTypeUID> getSupportedProfileTypeUIDs() {
return SUPPORTED_PROFILE_TYPE_UIDS;
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2021 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.profiles;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.profiles.ProfileTypeBuilder;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.openhab.core.thing.profiles.StateProfileType;
/**
* Modbus profile constants.
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public interface ModbusProfiles {
static final String MODBUS_SCOPE = "modbus";
static final ProfileTypeUID GAIN_OFFSET = new ProfileTypeUID(MODBUS_SCOPE, "gainOffset");
static final StateProfileType GAIN_OFFSET_TYPE = ProfileTypeBuilder.newState(GAIN_OFFSET, "Gain-Offset Correction")
.build();
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="profile:modbus:gainOffset">
<parameter name="pre-gain-offset" type="decimal">
<label>Pre-gain Offset</label>
<description>Offset to add to raw value towards the item (before the gain). The negative
offset will be applied in the
reverse direction (before inverting the gain). If omitted, zero offset is used.</description>
</parameter>
<parameter name="gain" type="text">
<label>Gain</label>
<description>Gain to apply to the state towards the item. One can also specify the unit to declare resulting unit.
This is used as divisor for values in the reverse direction. If omitted, gain of 1 is used.</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -26,7 +26,7 @@
</channels> </channels>
<config-description> <config-description>
<!-- what to read --> <!-- what to read -->
<parameter name="readStart" type="text" pattern="^(0|[1-9][0-9]*(\.[0-9]{1,2})?)?$"> <parameter name="readStart" type="text" pattern="^(0|[0-9][0-9]*(\.[0-9]{1,2})?)?$">
<label>Read Address</label> <label>Read Address</label>
<description><![CDATA[Start address to start reading the value. Use empty for write-only things. <description><![CDATA[Start address to start reading the value. Use empty for write-only things.
<br /> <br />
@ -81,7 +81,9 @@
<parameter name="writeStart" type="text"> <parameter name="writeStart" type="text">
<label>Write Address</label> <label>Write Address</label>
<description><![CDATA[Start address of the first holding register or coil in the write. Use empty for read-only things. <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> <br />Use zero based address, e.g. in place of 400001 (first holding register), use the address 0. This address is passed to data frame as is.
<br />One can write individual bits of an register using X.Y format where X is the register and Y is the bit (0 refers to least significant bit).
]]></description>
</parameter> </parameter>
<parameter name="writeType" type="text"> <parameter name="writeType" type="text">
<label>Write Type</label> <label>Write Type</label>

View File

@ -0,0 +1,307 @@
/**
* Copyright (c) 2010-2021 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.profiles;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.*;
import static org.mockito.Mockito.*;
import java.util.Optional;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EmptySource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.NullSource;
import org.mockito.ArgumentCaptor;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.thing.profiles.ProfileCallback;
import org.openhab.core.thing.profiles.ProfileContext;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.Type;
import org.openhab.core.types.UnDefType;
/**
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class ModbusGainOffsetProfileTest {
private static Stream<Arguments> provideArgsForBoth() {
return Stream.of(
// dimensionless
Arguments.of("100", "0.5", "250", "175.0"), Arguments.of("0", "1 %", "250", "250 %"),
//
// gain with same unit
//
// e.g. (handler) 3 <---> (item) 106K with raw-offset=50, gain=2K
// e.g. (handler) 3 K <---> (item) 106K^2 with raw-offset=50K, gain=2K
//
Arguments.of("50", "2 K", "3", "106 K"),
//
// gain with different unit
//
Arguments.of("50", "2 m/s", "3", "106 m/s"),
//
// gain without unit
//
Arguments.of("50", "2", "3", "106"),
//
// temperature tests
//
// celsius gain
Arguments.of("0", "0.1 °C", "25", "2.5 °C"),
// kelvin gain
Arguments.of("0", "0.1 K", "25", "2.5 K"),
// fahrenheit gain
Arguments.of("0", "10 °F", "0.18", "1.80 °F"),
//
// unsupported types are passed with error
Arguments.of("0", "0", OnOffType.ON, OnOffType.ON)
);
}
private static Stream<Arguments> provideAdditionalArgsForStateUpdateFromHandler() {
return Stream.of(
// Dimensionless conversion 2.5/1% = 250%/1% = 250
Arguments.of("0", "1 %", "250", "250 %"), Arguments.of("2 %", "1 %", "249.9800", "250.0000 %"),
Arguments.of("50", "2 m/s", new DecimalType("3"), "106 m/s"),
// UNDEF passes the profile unchanged
Arguments.of("0", "0", UnDefType.UNDEF, UnDefType.UNDEF));
}
/**
*
* Test profile behaviour when handler updates the state
*
*/
@ParameterizedTest
@MethodSource({ "provideArgsForBoth", "provideAdditionalArgsForStateUpdateFromHandler" })
public void testOnStateUpdateFromHandler(String rawOffset, String gain, Object updateFromHandlerObj,
Object expectedUpdateTowardsItemObj) {
testOnUpdateFromHandlerGeneric(rawOffset, gain, updateFromHandlerObj, expectedUpdateTowardsItemObj, true);
}
/**
*
* Test profile behaviour when handler sends command
*
*/
@ParameterizedTest
@MethodSource({ "provideArgsForBoth", "provideAdditionalArgsForStateUpdateFromHandler" })
public void testOnCommandFromHandler(String rawOffset, String gain, Object updateFromHandlerObj,
Object expectedUpdateTowardsItemObj) {
// UNDEF is not a command, cannot be sent by handler
assumeTrue(updateFromHandlerObj != UnDefType.UNDEF);
testOnUpdateFromHandlerGeneric(rawOffset, gain, updateFromHandlerObj, expectedUpdateTowardsItemObj, false);
}
/**
*
* Test profile behaviour when handler updates the state
*
* @param rawOffset profile raw offset
* @param gain profile gain
* @param updateFromHandlerObj state update from handler. String representing QuantityType or State/Command
* @param expectedUpdateTowardsItemObj expected state/command update towards item. String representing QuantityType
* or
* State
* @param stateUpdateFromHandler whether there is state update from handler. Otherwise command
*/
@SuppressWarnings("rawtypes")
private void testOnUpdateFromHandlerGeneric(String rawOffset, String gain, Object updateFromHandlerObj,
Object expectedUpdateTowardsItemObj, boolean stateUpdateFromHandler) {
ProfileCallback callback = mock(ProfileCallback.class);
ModbusGainOffsetProfile profile = createProfile(callback, gain, rawOffset);
final Type actualStateUpdateTowardsItem;
if (stateUpdateFromHandler) {
final State updateFromHandler;
if (updateFromHandlerObj instanceof String) {
updateFromHandler = new QuantityType((String) updateFromHandlerObj);
} else {
assertTrue(updateFromHandlerObj instanceof State);
updateFromHandler = (State) updateFromHandlerObj;
}
profile.onStateUpdateFromHandler(updateFromHandler);
ArgumentCaptor<State> capture = ArgumentCaptor.forClass(State.class);
verify(callback, times(1)).sendUpdate(capture.capture());
actualStateUpdateTowardsItem = capture.getValue();
} else {
final Command updateFromHandler;
if (updateFromHandlerObj instanceof String) {
updateFromHandler = new QuantityType((String) updateFromHandlerObj);
} else {
assertTrue(updateFromHandlerObj instanceof State);
updateFromHandler = (Command) updateFromHandlerObj;
}
profile.onCommandFromHandler(updateFromHandler);
ArgumentCaptor<Command> capture = ArgumentCaptor.forClass(Command.class);
verify(callback, times(1)).sendCommand(capture.capture());
actualStateUpdateTowardsItem = capture.getValue();
}
Type expectedStateUpdateTowardsItem = (expectedUpdateTowardsItemObj instanceof String)
? new QuantityType((String) expectedUpdateTowardsItemObj)
: (Type) expectedUpdateTowardsItemObj;
// Workaround for errors like "java.lang.UnsupportedOperationException: °C is non-linear, cannot convert"
if (expectedStateUpdateTowardsItem instanceof QuantityType<?>) {
assertTrue(actualStateUpdateTowardsItem instanceof QuantityType<?>);
assertEquals(((QuantityType<?>) expectedStateUpdateTowardsItem).getUnit(),
((QuantityType<?>) actualStateUpdateTowardsItem).getUnit());
assertEquals(((QuantityType<?>) expectedStateUpdateTowardsItem).toBigDecimal(),
((QuantityType<?>) actualStateUpdateTowardsItem).toBigDecimal());
} else {
assertEquals(expectedStateUpdateTowardsItem, actualStateUpdateTowardsItem);
}
verifyNoMoreInteractions(callback);
}
private static Stream<Arguments> provideAdditionalArgsForCommandFromItem() {
return Stream.of(
// Dimensionless conversion 2.5/1% = 250%/1% = 250
// gain in %, command as bare ratio and the other way around
Arguments.of("0", "1 %", "250", "2.5"), Arguments.of("2%", "1 %", "249.9800", "2.5"),
// celsius gain, kelvin command
Arguments.of("0", "0.1 °C", "-2706.5", "2.5 K"),
// incompatible command unit, should be convertible with gain
Arguments.of("0", "0.1 °C", null, "2.5 m/s"),
//
// incompatible offset unit
//
Arguments.of("50 K", "21", null, "30 m/s"), Arguments.of("50 m/s", "21", null, "30 K"),
//
// UNDEF command is not processed
//
Arguments.of("0", "0", null, UnDefType.UNDEF),
//
// REFRESH command is forwarded
//
Arguments.of("0", "0", RefreshType.REFRESH, RefreshType.REFRESH)
);
}
/**
*
* Test profile behaviour when item receives command
*
* @param rawOffset profile raw offset
* @param gain profile gain
* @param expectedCommandTowardsHandlerObj expected command towards handler. String representing QuantityType or
* Command. Use null to verify that no commands are sent to handler.
* @param commandFromItemObj command that item receives. String representing QuantityType or Command.
*/
@SuppressWarnings({ "rawtypes" })
@ParameterizedTest
@MethodSource({ "provideArgsForBoth", "provideAdditionalArgsForCommandFromItem" })
public void testOnCommandFromItem(String rawOffset, String gain, @Nullable Object expectedCommandTowardsHandlerObj,
Object commandFromItemObj) {
assumeFalse(commandFromItemObj.equals(UnDefType.UNDEF));
ProfileCallback callback = mock(ProfileCallback.class);
ModbusGainOffsetProfile profile = createProfile(callback, gain, rawOffset);
Command commandFromItem = (commandFromItemObj instanceof String) ? new QuantityType((String) commandFromItemObj)
: (Command) commandFromItemObj;
profile.onCommandFromItem(commandFromItem);
boolean callsExpected = expectedCommandTowardsHandlerObj != null;
if (callsExpected) {
ArgumentCaptor<Command> capture = ArgumentCaptor.forClass(Command.class);
verify(callback, times(1)).handleCommand(capture.capture());
Command actualCommandTowardsHandler = capture.getValue();
Command expectedCommandTowardsHandler = (expectedCommandTowardsHandlerObj instanceof String)
? new QuantityType((String) expectedCommandTowardsHandlerObj)
: (Command) expectedCommandTowardsHandlerObj;
assertEquals(expectedCommandTowardsHandler, actualCommandTowardsHandler);
verifyNoMoreInteractions(callback);
} else {
verifyNoInteractions(callback);
}
}
/**
*
* Test behaviour when item receives state update from item (no-op)
*
**/
@Test
public void testOnCommandFromItem() {
ProfileCallback callback = mock(ProfileCallback.class);
ModbusGainOffsetProfile<?> profile = createProfile(callback, "1.0", "0.0");
profile.onStateUpdateFromItem(new DecimalType(3.78));
// should be no-op
verifyNoInteractions(callback);
}
@Test
public void testInvalidInit() {
// offset must be dimensionless
ProfileCallback callback = mock(ProfileCallback.class);
ModbusGainOffsetProfile<?> profile = createProfile(callback, "1.0", "0.0 K");
assertFalse(profile.isValid());
}
@ParameterizedTest
@NullSource
@EmptySource
public void testInitGainDefault(String gain) {
ProfileCallback callback = mock(ProfileCallback.class);
ModbusGainOffsetProfile<?> p = createProfile(callback, gain, "0.0");
assertTrue(p.isValid());
assertEquals(p.getGain(), Optional.of(QuantityType.ONE));
}
@ParameterizedTest
@NullSource
@EmptySource
public void testInitOffsetDefault(String offset) {
ProfileCallback callback = mock(ProfileCallback.class);
ModbusGainOffsetProfile<?> p = createProfile(callback, "1", offset);
assertTrue(p.isValid());
assertEquals(p.getPregainOffset(), Optional.of(QuantityType.ZERO));
}
private ModbusGainOffsetProfile<?> createProfile(ProfileCallback callback, @Nullable String gain,
@Nullable String preGainOffset) {
ProfileContext context = mock(ProfileContext.class);
Configuration config = new Configuration();
if (gain != null) {
config.put("gain", gain);
}
if (preGainOffset != null) {
config.put("pre-gain-offset", preGainOffset);
}
when(context.getConfiguration()).thenReturn(config);
return new ModbusGainOffsetProfile<>(callback, context);
}
}

View File

@ -53,6 +53,7 @@ Fragment-Host: org.openhab.binding.modbus
org.mockito.mockito-core;version='[3.7.0,3.7.1)',\ org.mockito.mockito-core;version='[3.7.0,3.7.1)',\
org.objenesis;version='[3.1.0,3.1.1)',\ org.objenesis;version='[3.1.0,3.1.1)',\
org.mockito.junit-jupiter;version='[3.7.0,3.7.1)',\ org.mockito.junit-jupiter;version='[3.7.0,3.7.1)',\
junit-jupiter-params;version='[5.7.0,5.7.1)',\
org.glassfish.hk2.osgi-resource-locator;version='[1.0.3,1.0.4)',\ org.glassfish.hk2.osgi-resource-locator;version='[1.0.3,1.0.4)',\
biz.aQute.tester.junit-platform;version='[5.3.0,5.3.1)',\ biz.aQute.tester.junit-platform;version='[5.3.0,5.3.1)',\
com.google.gson;version='[2.8.6,2.8.7)',\ com.google.gson;version='[2.8.6,2.8.7)',\

View File

@ -20,6 +20,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import static org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal.*; import static org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal.*;
import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
@ -30,13 +31,20 @@ import java.util.Objects;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Stream;
import org.hamcrest.Matcher; import org.hamcrest.Matcher;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.openhab.binding.modbus.handler.EndpointNotInitializedException; import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
import org.openhab.binding.modbus.handler.ModbusPollerThingHandler; import org.openhab.binding.modbus.handler.ModbusPollerThingHandler;
import org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal;
import org.openhab.binding.modbus.internal.handler.ModbusDataThingHandler; import org.openhab.binding.modbus.internal.handler.ModbusDataThingHandler;
import org.openhab.binding.modbus.internal.handler.ModbusTcpThingHandler; import org.openhab.binding.modbus.internal.handler.ModbusTcpThingHandler;
import org.openhab.core.config.core.Configuration; import org.openhab.core.config.core.Configuration;
@ -46,6 +54,7 @@ import org.openhab.core.io.transport.modbus.AsyncModbusWriteResult;
import org.openhab.core.io.transport.modbus.BitArray; import org.openhab.core.io.transport.modbus.BitArray;
import org.openhab.core.io.transport.modbus.ModbusConstants; import org.openhab.core.io.transport.modbus.ModbusConstants;
import org.openhab.core.io.transport.modbus.ModbusConstants.ValueType; import org.openhab.core.io.transport.modbus.ModbusConstants.ValueType;
import org.openhab.core.io.transport.modbus.ModbusReadCallback;
import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode; import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint; import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
import org.openhab.core.io.transport.modbus.ModbusRegisterArray; import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
@ -94,6 +103,9 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
} }
} }
private static final String HOST = "thisishost";
private static final int PORT = 44;
private static final Map<String, String> CHANNEL_TO_ACCEPTED_TYPE = new HashMap<>(); private static final Map<String, String> CHANNEL_TO_ACCEPTED_TYPE = new HashMap<>();
static { static {
CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_SWITCH, "Switch"); CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_SWITCH, "Switch");
@ -109,10 +121,49 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_LAST_READ_ERROR, "DateTime"); CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_LAST_READ_ERROR, "DateTime");
} }
private List<ModbusWriteRequestBlueprint> writeRequests = new ArrayList<>(); private List<ModbusWriteRequestBlueprint> writeRequests = new ArrayList<>();
private Bridge realEndpointWithMockedComms;
public ModbusReadCallback getPollerCallback(ModbusPollerThingHandler handler) {
Field callbackField;
try {
callbackField = ModbusPollerThingHandler.class.getDeclaredField("callbackDelegator");
callbackField.setAccessible(true);
return (ModbusReadCallback) callbackField.get(handler);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
fail(e);
throw new RuntimeException(e);
}
}
@BeforeEach
public void beforeEach() {
mockCommsToModbusManager();
Configuration tcpConfig = new Configuration();
tcpConfig.put("host", HOST);
tcpConfig.put("port", PORT);
tcpConfig.put("id", 9);
realEndpointWithMockedComms = BridgeBuilder
.create(ModbusBindingConstantsInternal.THING_TYPE_MODBUS_TCP,
new ThingUID(ModbusBindingConstantsInternal.THING_TYPE_MODBUS_TCP, "mytcp"))
.withLabel("label for mytcp").withConfiguration(tcpConfig).build();
addThing(realEndpointWithMockedComms);
assertEquals(ThingStatus.ONLINE, realEndpointWithMockedComms.getStatus(),
realEndpointWithMockedComms.getStatusInfo().getDescription());
}
@AfterEach @AfterEach
public void tearDown() { public void tearDown() {
writeRequests.clear(); writeRequests.clear();
if (realEndpointWithMockedComms != null) {
thingProvider.remove(realEndpointWithMockedComms.getUID());
}
}
private static Arguments appendArg(Arguments args, Object obj) {
Object[] newArgs = Arrays.copyOf(args.get(), args.get().length + 1);
newArgs[args.get().length] = obj;
return Arguments.of(newArgs);
} }
private void captureModbusWrites() { private void captureModbusWrites() {
@ -455,10 +506,17 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
return dataHandler; return dataHandler;
} }
@SuppressWarnings({ "null" })
private ModbusDataThingHandler testWriteHandlingGeneric(String start, String transform, ValueType valueType, private ModbusDataThingHandler testWriteHandlingGeneric(String start, String transform, ValueType valueType,
String writeType, ModbusWriteFunctionCode successFC, String channel, Command command, Exception error, String writeType, ModbusWriteFunctionCode successFC, String channel, Command command, Exception error,
BundleContext context) { BundleContext context) {
return testWriteHandlingGeneric(start, transform, valueType, writeType, successFC, channel, command, error,
context, false);
}
@SuppressWarnings({ "null" })
private ModbusDataThingHandler testWriteHandlingGeneric(String start, String transform, ValueType valueType,
String writeType, ModbusWriteFunctionCode successFC, String channel, Command command, Exception error,
BundleContext context, boolean parentIsEndpoint) {
ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false); ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
// Minimally mocked request // Minimally mocked request
@ -468,7 +526,13 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
doReturn(endpoint).when(task).getEndpoint(); doReturn(endpoint).when(task).getEndpoint();
doReturn(request).when(task).getRequest(); doReturn(request).when(task).getRequest();
Bridge poller = createPollerMock("poller1", task); final Bridge parent;
if (parentIsEndpoint) {
parent = createTcpMock();
addThing(parent);
} else {
parent = createPollerMock("poller1", task);
}
Configuration dataConfig = new Configuration(); Configuration dataConfig = new Configuration();
dataConfig.put("readStart", ""); dataConfig.put("readStart", "");
@ -479,7 +543,7 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
String thingId = "write"; String thingId = "write";
ModbusDataThingHandler dataHandler = createDataHandler(thingId, poller, ModbusDataThingHandler dataHandler = createDataHandler(thingId, parent,
builder -> builder.withConfiguration(dataConfig), context); builder -> builder.withConfiguration(dataConfig), context);
assertThat(dataHandler.getThing().getStatus(), is(equalTo(ThingStatus.ONLINE))); assertThat(dataHandler.getThing().getStatus(), is(equalTo(ThingStatus.ONLINE)));
@ -596,6 +660,25 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
assertSingleStateUpdate(dataHandler, CHANNEL_STRING, is(equalTo(new StringType("ON")))); assertSingleStateUpdate(dataHandler, CHANNEL_STRING, is(equalTo(new StringType("ON"))));
} }
@Test
public void testWriteWithDataAsChildOfEndpoint() throws InvalidSyntaxException {
captureModbusWrites();
mockTransformation("MULTIPLY", new MultiplyTransformation());
ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "MULTIPLY(10)",
ModbusConstants.ValueType.BIT, "coil", ModbusWriteFunctionCode.WRITE_COIL, "number",
new DecimalType("2"), null, bundleContext, /* parent is endpoint */true);
assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class)));
assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class)));
assertThat(writeRequests.size(), is(equalTo(1)));
ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_COIL)));
assertThat(writeRequest.getReference(), is(equalTo(50)));
assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().size(), is(equalTo(1)));
// Since transform output is non-zero, it is mapped as "true"
assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().getBit(0), is(equalTo(true)));
}
@Test @Test
public void testWriteRealTransformation() throws InvalidSyntaxException { public void testWriteRealTransformation() throws InvalidSyntaxException {
captureModbusWrites(); captureModbusWrites();
@ -837,14 +920,94 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
waitForAssert(() -> verify((ModbusPollerThingHandler) poller.getHandler()).refresh()); waitForAssert(() -> verify((ModbusPollerThingHandler) poller.getHandler()).refresh());
} }
private static Stream<Arguments> provideArgsForUpdateThenCommandFromItem()
{
return Stream.of(//
// ON/OFF commands
Arguments.of((short) 0b1011_0100_0000_1111, "1", (short) 0b1011_0100_0000_1101, OnOffType.OFF),
Arguments.of((short) 0b1011_0100_0000_1111, "4", (short) 0b1011_0100_0001_1111, OnOffType.ON),
// OPEN/CLOSED commands
Arguments.of((short) 0b1011_0100_0000_1111, "1", (short) 0b1011_0100_0000_1101, OpenClosedType.CLOSED),
Arguments.of((short) 0b1011_0100_0000_1111, "4", (short) 0b1011_0100_0001_1111, OpenClosedType.OPEN),
// DecimalType commands
Arguments.of((short) 0b1011_0100_0000_1111, "1", (short) 0b1011_0100_0000_1101, new DecimalType(0)),
Arguments.of((short) 0b1011_0100_0010_1111, "5", (short) 0b1011_0100_0000_1111, new DecimalType(0)),
Arguments.of((short) 0b1011_0100_0000_1111, "4", (short) 0b1011_0100_0001_1111, new DecimalType(5)),
Arguments.of((short) 0b1011_0100_0000_1111, "15", (short) 0b0011_0100_0000_1111, new DecimalType(0))
).flatMap(a -> {
// parametrize by channel (yes, it does not matter what channel is used, commands are interpreted all the
// same)
Stream<String> channels = Stream.of("switch", "number", "contact");
return channels.map(channel -> appendArg(a, channel));
});
}
@ParameterizedTest
@MethodSource("provideArgsForUpdateThenCommandFromItem")
public void testUpdateFromHandlerThenCommandFromItem(short stateUpdateFromHandler, String bitIndex,
short expectedWriteDataToSlave, Command commandFromItem, String channel) {
int expectedWriteDataToSlaveUnsigned = expectedWriteDataToSlave & 0xFFFF;
captureModbusWrites();
Configuration pollerConfig = new Configuration();
pollerConfig.put("refresh", 0L); // 0 -> non polling
pollerConfig.put("start", "2");
pollerConfig.put("length", "3");
pollerConfig.put("type", ModbusBindingConstantsInternal.READ_TYPE_HOLDING_REGISTER);
ThingUID pollerUID = new ThingUID(ModbusBindingConstantsInternal.THING_TYPE_MODBUS_POLLER, "realPoller");
Bridge poller = BridgeBuilder.create(ModbusBindingConstantsInternal.THING_TYPE_MODBUS_POLLER, pollerUID)
.withLabel("label for realPoller").withConfiguration(pollerConfig)
.withBridge(realEndpointWithMockedComms.getUID()).build();
addThing(poller);
assertEquals(ThingStatus.ONLINE, poller.getStatus(), poller.getStatusInfo().getDescription());
Configuration dataConfig = new Configuration();
dataConfig.put("writeStart", "3." + bitIndex);
dataConfig.put("writeValueType", "bit");
dataConfig.put("writeType", "holding");
String thingId = "read1";
ModbusDataThingHandler dataHandler = createDataHandler(thingId, poller,
builder -> builder.withConfiguration(dataConfig), bundleContext);
assertEquals(ThingStatus.ONLINE, dataHandler.getThing().getStatus());
assertEquals(pollerUID, dataHandler.getThing().getBridgeUID());
AsyncModbusReadResult result = new AsyncModbusReadResult(Mockito.mock(ModbusReadRequestBlueprint.class),
new ModbusRegisterArray(/* register 2, dummy data */0, /* register 3 */ stateUpdateFromHandler,
/* register 4, dummy data */9));
// poller receives some data (and therefore data as well)
getPollerCallback(((ModbusPollerThingHandler) poller.getHandler())).handle(result);
dataHandler.handleCommand(new ChannelUID(dataHandler.getThing().getUID(), channel), commandFromItem);
// Assert data written
{
assertEquals(1, writeRequests.size());
ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
assertEquals(writeRequest.getFunctionCode(), ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER);
assertEquals(writeRequest.getReference(), 3);
assertEquals(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), 1);
assertEquals(expectedWriteDataToSlaveUnsigned,
((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0));
}
}
private void testInitGeneric(ModbusReadFunctionCode pollerFunctionCode, Configuration config,
Consumer<ThingStatusInfo> statusConsumer) {
testInitGeneric(pollerFunctionCode, 0, config, statusConsumer);
}
/** /**
* *
* @param pollerFunctionCode poller function code. Use null if you want to have data thing direct child of endpoint * @param pollerFunctionCode poller function code. Use null if you want to have data thing direct child of endpoint
* thing * thing
* @param pollerStart start index of poller
* @param config thing config * @param config thing config
* @param statusConsumer assertion method for data thingstatus * @param statusConsumer assertion method for data thingstatus
*/ */
private void testInitGeneric(ModbusReadFunctionCode pollerFunctionCode, Configuration config, private void testInitGeneric(ModbusReadFunctionCode pollerFunctionCode, int pollerStart, Configuration config,
Consumer<ThingStatusInfo> statusConsumer) { Consumer<ThingStatusInfo> statusConsumer) {
int pollLength = 3; int pollLength = 3;
@ -857,6 +1020,7 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
// Minimally mocked request // Minimally mocked request
ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class); ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
doReturn(pollerStart).when(request).getReference();
doReturn(pollLength).when(request).getDataLength(); doReturn(pollLength).when(request).getDataLength();
doReturn(pollerFunctionCode).when(request).getFunctionCode(); doReturn(pollerFunctionCode).when(request).getFunctionCode();
@ -947,7 +1111,19 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
dataConfig.put("writeValueType", "int8"); dataConfig.put("writeValueType", "int8");
dataConfig.put("writeType", "holding"); dataConfig.put("writeType", "holding");
testInitGeneric(null, dataConfig, status -> { testInitGeneric(null, dataConfig, status -> {
assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE))); assertEquals(ThingStatus.OFFLINE, status.getStatus(), status.getDescription());
assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
});
}
@Test
public void testWriteHoldingBitDataWrongWriteType() {
Configuration dataConfig = new Configuration();
dataConfig.put("writeStart", "0.15");
dataConfig.put("writeValueType", "bit");
dataConfig.put("writeType", "coil"); // X.Y writeStart only applicable with holding
testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
assertEquals(ThingStatus.OFFLINE, status.getStatus(), status.getDescription());
assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR))); assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
}); });
} }
@ -955,11 +1131,85 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
@Test @Test
public void testWriteHoldingBitData() { public void testWriteHoldingBitData() {
Configuration dataConfig = new Configuration(); Configuration dataConfig = new Configuration();
dataConfig.put("writeStart", "0"); dataConfig.put("writeStart", "0.15");
dataConfig.put("writeValueType", "bit"); dataConfig.put("writeValueType", "bit");
dataConfig.put("writeType", "holding"); dataConfig.put("writeType", "holding");
testInitGeneric(null, dataConfig, status -> { testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE))); assertEquals(status.getStatus(), ThingStatus.ONLINE, status.getDescription());
});
}
@Test
public void testWriteHoldingInt8WithSubIndexData() {
Configuration dataConfig = new Configuration();
dataConfig.put("writeStart", "1.0");
dataConfig.put("writeValueType", "int8");
dataConfig.put("writeType", "holding");
// OFFLINE since sub-register writes are not supported for other than bit
testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
assertEquals(status.getStatus(), ThingStatus.OFFLINE, status.getDescription());
});
}
@Test
public void testWriteHoldingBitDataRegisterOutOfBounds() {
Configuration dataConfig = new Configuration();
// in this test poller reads from register 2. Register 1 is out of bounds
dataConfig.put("writeStart", "1.15");
dataConfig.put("writeValueType", "bit");
dataConfig.put("writeType", "holding");
testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, /* poller start */2, dataConfig, status -> {
assertEquals(ThingStatus.OFFLINE, status.getStatus(), status.getDescription());
assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
});
}
@Test
public void testWriteHoldingBitDataRegisterOutOfBounds2() {
Configuration dataConfig = new Configuration();
// register 3 is the last one polled, 4 is out of bounds
dataConfig.put("writeStart", "4.15");
dataConfig.put("writeValueType", "bit");
dataConfig.put("writeType", "holding");
testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
assertEquals(ThingStatus.OFFLINE, status.getStatus(), status.getDescription());
});
}
@ParameterizedTest
@CsvSource({ "READ_COILS", "READ_INPUT_DISCRETES", "READ_INPUT_REGISTERS" })
public void testWriteHoldingBitDataWrongPoller(ModbusReadFunctionCode poller) {
Configuration dataConfig = new Configuration();
dataConfig.put("writeStart", "0.15");
dataConfig.put("writeValueType", "bit");
dataConfig.put("writeType", "holding");
testInitGeneric(poller, dataConfig, status -> {
assertEquals(ThingStatus.OFFLINE, status.getStatus(), status.getDescription());
assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
});
}
@Test
public void testWriteHoldingBitParentEndpointData() {
Configuration dataConfig = new Configuration();
dataConfig.put("writeStart", "0.15");
dataConfig.put("writeValueType", "bit");
dataConfig.put("writeType", "holding");
// OFFLINE since we require poller as parent when sub-register writes are used
testInitGeneric(/* poller not as parent */null, dataConfig, status -> {
assertEquals(ThingStatus.OFFLINE, status.getStatus(), status.getDescription());
assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
});
}
@Test
public void testWriteHoldingBitBadStartData() {
Configuration dataConfig = new Configuration();
dataConfig.put("writeStart", "0.16");
dataConfig.put("writeValueType", "int8");
dataConfig.put("writeType", "holding");
testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
assertEquals(ThingStatus.OFFLINE, status.getStatus(), status.getDescription());
assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR))); assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
}); });
} }
@ -980,7 +1230,7 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
dataConfig.put("writeValueType", "bit"); dataConfig.put("writeValueType", "bit");
// missing writeType --> error // missing writeType --> error
testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> { testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE))); assertEquals(ThingStatus.OFFLINE, status.getStatus(), status.getDescription());
assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR))); assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
assertThat(status.getDescription(), is(not(equalTo(null)))); assertThat(status.getDescription(), is(not(equalTo(null))));
}); });
@ -995,7 +1245,7 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
dataConfig.put("writeStart", "0"); dataConfig.put("writeStart", "0");
dataConfig.put("writeType", "coil"); dataConfig.put("writeType", "coil");
testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig,
status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE)))); status -> assertEquals(ThingStatus.ONLINE, status.getStatus(), status.getDescription()));
} }
@Test @Test
@ -1110,7 +1360,6 @@ public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
@Test @Test
public void testWriteTransformAndNecessary() { public void testWriteTransformAndNecessary() {
Configuration dataConfig = new Configuration(); Configuration dataConfig = new Configuration();
// It's illegal to have start and transform. Just have transform or have all
dataConfig.put("writeStart", "3"); dataConfig.put("writeStart", "3");
dataConfig.put("writeType", "holding"); dataConfig.put("writeType", "holding");
dataConfig.put("writeValueType", "int16"); dataConfig.put("writeValueType", "int16");