Compare commits

...

5 Commits

Author SHA1 Message Date
jimtng
74184aa889
Merge 3327261c71 into 98ff656400 2025-01-08 20:08:30 -07:00
Jacob Laursen
98ff656400
Fix headers (#18070)
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
2025-01-08 23:25:39 +01:00
Robert Michalak
adacdebb9f
[digiplex] Handle erroneous responses and restart the bridge (#18035)
Signed-off-by: Robert Michalak <rbrt.michalak@gmail.com>
2025-01-08 22:21:07 +01:00
Jimmy Tanagra
3327261c71 Rules DSL, table formatting
Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
2025-01-05 08:40:10 +10:00
Jimmy Tanagra
bb2a06f398 [jrubyscripting] Minor updates, add short examples at the top of README.md
Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
2025-01-04 12:11:50 +10:00
8 changed files with 400 additions and 57 deletions

View File

@ -3,11 +3,55 @@
# JRuby Scripting # JRuby Scripting
This add-on provides [JRuby](https://www.jruby.org/) scripting language for automation rules. This add-on provides Ruby scripting language for automation rules.
Also included is [openhab-scripting](https://openhab.github.io/openhab-jruby/), a fairly high-level Ruby gem to support automation in openHAB. It includes the [openhab-scripting](https://openhab.github.io/openhab-jruby/) helper library, a comprehensive Ruby gem designed to enhance automation in openHAB.
It provides native Ruby access to common openHAB functionality within rules including items, things, actions, logging and more. This library offers a streamlined syntax for writing file-based and UI-based rules, making it easier and more intuitive than Rules DSL, while delivering the full features of the Ruby language.
If you're new to Ruby, you may want to check out [Ruby Basics](https://openhab.github.io/openhab-jruby/main/file.ruby-basics.html). If you're new to Ruby, you may want to check out [Ruby Basics](https://openhab.github.io/openhab-jruby/main/file.ruby-basics.html).
Example file-based rules:
```ruby
rule "Turn on light when sensor changed to open" do
changed Door_Sensor # a Contact item
run do |event|
if event.open?
Cupboard_Light.on for: 3.minutes # Automatically turn it off after 3 minutes
else
Cupboard_Light.off # This will automatically cancel the timer set above
end
end
end
```
```ruby
rule "Door open reminder" do
changed Doors.members, to: OPEN
run do |event|
# Create a timer using the triggering item as the timer id
# If a timer with the given id already exists, it will be rescheduled
after 5.minutes, id: event.item do |timer|
next if timer.cancelled? || event.item.closed?
Voice.say "The #{event.item} is open"
timer.reschedule # Use the original duration by default
end
end
end
```
Example UI-based rules:
```ruby
only_every(2.minutes) do # apply rate-limiting
Audio.play_sound("doorbell.mp3")
Notification.send("Someone pressed the doorbell")
end
```
Additional [example rules are available](https://openhab.github.io/openhab-jruby/main/file.examples.html), as well as examples of [conversions from Rules DSL, JavaScript, and Python rules](https://openhab.github.io/openhab-jruby/main/file.conversions.html).
- [Why Ruby?](#why-ruby) - [Why Ruby?](#why-ruby)
- [Installation](#installation) - [Installation](#installation)
- [Configuration](#configuration) - [Configuration](#configuration)
@ -66,14 +110,12 @@ If you're new to Ruby, you may want to check out [Ruby Basics](https://openhab.g
- [Calling Java From JRuby](#calling-java-from-jruby) - [Calling Java From JRuby](#calling-java-from-jruby)
- [Full Documentation](#full-documentation) - [Full Documentation](#full-documentation)
Additional [example rules are available](https://openhab.github.io/openhab-jruby/main/file.examples.html), as well as examples of [conversions from DSL and Python rules](https://openhab.github.io/openhab-jruby/main/file.conversions.html).
## Why Ruby? ## Why Ruby?
- Ruby is designed for programmers' productivity with the idea that programming should be fun for programmers. - Ruby is designed for programmers' productivity with the idea that programming should be fun for programmers.
- Ruby emphasizes the necessity for software to be understood by humans first and computers second. - Ruby emphasizes the necessity for software to be understood by humans first and computers second.
- Ruby makes writing automation enjoyable without having to fight with compilers and interpreters. - Ruby makes writing automation enjoyable with its readable syntax and a rich collection of useful methods in its built-in classes.
- Rich ecosystem of tools, including things like Rubocop to help developers write clean code and RSpec to test the libraries. - Rich ecosystem of tools and libraries, including things like Rubocop to help developers write clean code and RSpec to test the libraries.
- Ruby is really good at letting one express intent and create a DSL to make that expression easier. - Ruby is really good at letting one express intent and create a DSL to make that expression easier.
### Design points ### Design points
@ -88,56 +130,42 @@ Additional [example rules are available](https://openhab.github.io/openhab-jruby
- Designed and tested using [Test-Driven Development](https://en.wikipedia.org/wiki/Test-driven_development) with [RSpec](https://rspec.info/) - Designed and tested using [Test-Driven Development](https://en.wikipedia.org/wiki/Test-driven_development) with [RSpec](https://rspec.info/)
- Extensible. - Extensible.
- Anyone should be able to customize and add/remove core language features - Anyone should be able to customize and add/remove core language features
- Easy access to the Ruby ecosystem in rules through Ruby gems. - Easy access to the Ruby ecosystem in rules through [Ruby Gems](https://rubygems.org/).
## Installation ## Installation
### Prerequisites
1. openHAB 3.4+
1. The JRuby Scripting Language Addon
### From the User Interface ### From the User Interface
1. Go to `Settings -> Add-ons -> Automation` and install the jrubyscripting automation addon following the [openHAB instructions](https://www.openhab.org/docs/configuration/addons.html). - Go to `Settings -> Add-ons -> Automation` and install the jrubyscripting automation addon following the [openHAB instructions](https://www.openhab.org/docs/configuration/addons.html).
In openHAB 4.0+ the defaults are set so the next step can be skipped.
1. Go to `Settings -> Add-on Settings -> JRuby Scripting`:
- **Ruby Gems**: `openhab-scripting=~>5.0`
- **Require Scripts**: `openhab/dsl` (not required, but recommended)
### Using Files ### Using Files
1. Edit `<OPENHAB_CONF>/services/addons.cfg` and ensure that `jrubyscripting` is included in an uncommented `automation=` list of automations to install. - Edit `<OPENHAB_CONF>/services/addons.cfg` and ensure that `jrubyscripting` is included in an uncommented `automation=` list of automations to install.
In openHAB 4.0+ the defaults are set so the next step can be skipped.
1. Configure JRuby openHAB services
Create a file called `jruby.cfg` in `<OPENHAB_CONF>/services/` with the following content:
```ini
org.openhab.automation.jrubyscripting:gems=openhab-scripting=~>5.0
org.openhab.automation.jrubyscripting:require=openhab/dsl
```
## Configuration ## Configuration
After installing this add-on, you will find configuration options in the openHAB portal under _Settings -> Add-on Settings -> JRuby Scripting_. After installing this add-on, you will find configuration options in the openHAB portal under _Settings -> Add-on Settings -> JRuby Scripting_.
Alternatively, JRuby configuration parameters may be set by creating a `jruby.cfg` file in `conf/services/`. Alternatively, JRuby configuration parameters may be set by creating a `jruby.cfg` file in `conf/services/`.
> **_NOTE:_**
> In openHAB 3.4.x, the `gems` and `require` settings must be manually configured to the value given in the table below.
Starting from openHAB 4.0, the correct defaults were added, so manual configurations are no longer necessary.
By default this add-on includes the [openhab-scripting](https://github.com/openhab/openhab-jruby) Ruby gem and automatically `require`s it. By default this add-on includes the [openhab-scripting](https://github.com/openhab/openhab-jruby) Ruby gem and automatically `require`s it.
This allows the use of [items](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL.html#items-class_method), [rules](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL.html#rules-class_method), [shared_cache](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL.html#shared_cache-class_method) and other objects in your scripts. This allows the use of [items](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL.html#items-class_method), [rules](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL.html#rules-class_method), [shared_cache](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL.html#shared_cache-class_method) and other objects in your scripts.
This functionality can be disabled for users who prefer to manage their own gems and `require`s via the add-on configuration options. This functionality can be disabled for users who prefer to manage their own gems and `require`s via the add-on configuration options.
Simply change the `gems` and `require` configuration settings. Simply change the `gems` and `require` configuration settings.
| Parameter | Description | | Parameter | Description |
| --------------------- | -------------------------------------------------------------------------------------------------------- | | --------------------- | ---------------------------------------------------------------------------------------------------------- |
| `gem_home` | The path to store Ruby Gems. <br/><br/>Default: `$OPENHAB_CONF/automation/ruby/.gem/RUBY_ENGINE_VERSION` | | `gem_home` | The path to store Ruby Gems. <br/><br/>Default: `$OPENHAB_CONF/automation/ruby/.gem/RUBY_ENGINE_VERSION` |
| `gems` | A list of gems to install. <br/><br/>Default: `openhab-scripting=~>5.0` | | `gems` | A list of gems to install. <br/><br/>Default: `openhab-scripting=~>5.0` |
| `check_update` | Check for updated version of `gems` on start up or settings change. <br/><br/>Default: `true` | | `check_update` | Check for updated version of `gems` on start up or settings change. <br/><br/>Default: `true` |
| `require` | List of scripts to be required automatically. <br/><br/>Default: `openhab/dsl` | | `require` | List of scripts to be required automatically. <br/><br/>Default: `openhab/dsl` |
| `rubylib` | Search path for user libraries. <br/><br/>Default: `$OPENHAB_CONF/automation/ruby/lib` | | `rubylib` | Search path for user libraries. <br/><br/>Default: `$OPENHAB_CONF/automation/ruby/lib` |
| `dependency_tracking` | Enable dependency tracking. <br/><br/>Default: `true` | | `dependency_tracking` | Enable dependency tracking. <br/><br/>Default: `true` |
| `local_context` | See notes below. <br/><br/>Default: `singlethread` | | `local_context` | See notes below. <br/><br/>Default: `singlethread` |
| `local_variables` | See notes below. <br/><br/>Default: `transient` | | `local_variables` | See notes below. <br/><br/>Default: `transient` |
When using file-based configuration, these parameters must be prefixed with `org.openhab.automation.jrubyscripting:`, for example: When using file-based configuration, these parameters must be prefixed with `org.openhab.automation.jrubyscripting:`, for example:
@ -766,8 +794,9 @@ To log a message on `INFO` log level:
logger.info("The current time is #{Time.now}") logger.info("The current time is #{Time.now}")
``` ```
The default logger name for UI rules is `org.openhab.automation.jrubyscripting.script`. The main logger prefix is `org.openhab.automation.jrubyscripting`.
For file-based rules, it's based on the rule's ID, such as `org.openhab.automation.jrubyscripting.rule.myrule.rb:15`. The default logger name for UI rules includes the rule ID: `org.openhab.automation.jrubyscripting.script.<RULE_ID>`.
The logger name for file-based rules includes the rule's filename and the rule ID: `org.openhab.automation.jrubyscripting.<filename>.rule.<RULE_ID>`.
To use a custom logger name: To use a custom logger name:
@ -776,7 +805,7 @@ logger = OpenHAB::Log.logger("org.openhab.custom")
``` ```
Please be aware that messages might not appear in the logs if the logger name does not start with `org.openhab`. Please be aware that messages might not appear in the logs if the logger name does not start with `org.openhab`.
This behaviour is due to [log4j2](https://logging.apache.org/log4j/2.x/) requiring definition for each logger prefix. This behavior is due to [log4j2](https://logging.apache.org/log4j/2.x/) requiring definition for each logger prefix.
The [logger](https://openhab.github.io/openhab-jruby/main/OpenHAB/Logger.html) is similar to a standard [Ruby Logger](https://docs.ruby-lang.org/en/master/Logger.html). The [logger](https://openhab.github.io/openhab-jruby/main/OpenHAB/Logger.html) is similar to a standard [Ruby Logger](https://docs.ruby-lang.org/en/master/Logger.html).
Supported logging functions include: Supported logging functions include:
@ -815,7 +844,7 @@ end
``` ```
Alternatively a timer can be used in either a file-based rule or in a UI based rule using [after](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL.html#after-class_method). Alternatively a timer can be used in either a file-based rule or in a UI based rule using [after](https://openhab.github.io/openhab-jruby/main/OpenHAB/DSL.html#after-class_method).
After takes a [Duration](#durations), e.g. `10.minutes` instead of using [ZonedDateTime](https://openhab.github.io/openhab-jruby/main/OpenHAB/CoreExt/Java/ZonedDateTime.html). After takes a [Duration](#durations) relative to `now`, e.g. `10.minutes`, or an absolute time with [ZonedDateTime](https://openhab.github.io/openhab-jruby/main/OpenHAB/CoreExt/Java/ZonedDateTime.html) or [Time](https://openhab.github.io/openhab-jruby/main/Time.html).
```ruby ```ruby
rule "simple timer" do rule "simple timer" do
@ -1065,6 +1094,10 @@ end
start_of_day = ZonedDateTime.now.with(LocalTime::MIDNIGHT) start_of_day = ZonedDateTime.now.with(LocalTime::MIDNIGHT)
# or # or
start_of_day = LocalTime::MIDNIGHT.to_zoned_date_time start_of_day = LocalTime::MIDNIGHT.to_zoned_date_time
# or
start_of_day = LocalDate.now.to_zoned_date_time
# or using Ruby Date
start_of_day = Date.today.to_zoned_date_time
# Comparing ZonedDateTime against LocalTime with `<` # Comparing ZonedDateTime against LocalTime with `<`
max = Solar_Power.maximum_since(24.hours.ago) max = Solar_Power.maximum_since(24.hours.ago)

View File

@ -51,7 +51,6 @@ public class DigiplexBindingConstants {
public static final String BRIDGE_MESSAGES_SENT = "statistics#messages_sent"; public static final String BRIDGE_MESSAGES_SENT = "statistics#messages_sent";
public static final String BRIDGE_RESPONSES_RECEIVED = "statistics#responses_received"; public static final String BRIDGE_RESPONSES_RECEIVED = "statistics#responses_received";
public static final String BRIDGE_EVENTS_RECEIVED = "statistics#events_received"; public static final String BRIDGE_EVENTS_RECEIVED = "statistics#events_received";
public static final String BRIDGE_TLM_TROUBLE = "troubles#tlm_trouble"; public static final String BRIDGE_TLM_TROUBLE = "troubles#tlm_trouble";
public static final String BRIDGE_AC_FAILURE = "troubles#ac_failure"; public static final String BRIDGE_AC_FAILURE = "troubles#ac_failure";
public static final String BRIDGE_BATTERY_FAILURE = "troubles#battery_failure"; public static final String BRIDGE_BATTERY_FAILURE = "troubles#battery_failure";

View File

@ -52,6 +52,9 @@ public interface DigiplexMessageHandler {
default void handleUnknownResponse(UnknownResponse response) { default void handleUnknownResponse(UnknownResponse response) {
} }
default void handleErroneousResponse(ErroneousResponse response) {
}
// Events // Events
default void handleZoneEvent(ZoneEvent event) { default void handleZoneEvent(ZoneEvent event) {
} }

View File

@ -13,6 +13,7 @@
package org.openhab.binding.digiplex.internal.communication; package org.openhab.binding.digiplex.internal.communication;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.digiplex.internal.communication.events.AreaEvent; import org.openhab.binding.digiplex.internal.communication.events.AreaEvent;
import org.openhab.binding.digiplex.internal.communication.events.AreaEventType; import org.openhab.binding.digiplex.internal.communication.events.AreaEventType;
import org.openhab.binding.digiplex.internal.communication.events.GenericEvent; import org.openhab.binding.digiplex.internal.communication.events.GenericEvent;
@ -29,21 +30,19 @@ import org.openhab.binding.digiplex.internal.communication.events.ZoneStatusEven
* Resolves serial messages to appropriate classes * Resolves serial messages to appropriate classes
* *
* @author Robert Michalak - Initial contribution * @author Robert Michalak - Initial contribution
*
*/ */
@NonNullByDefault @NonNullByDefault
public class DigiplexResponseResolver { public class DigiplexResponseResolver {
private static final String OK = "&ok"; private static final String OK = "&ok";
// TODO: handle failures
private static final String FAIL = "&fail"; private static final String FAIL = "&fail";
public static DigiplexResponse resolveResponse(String message) { public static DigiplexResponse resolveResponse(String message) {
if (message.length() < 4) { // sanity check: try to filter out malformed responses if (message.length() < 4) { // sanity check: try to filter out malformed responses
return new UnknownResponse(message); return new ErroneousResponse(message);
} }
int zoneNo, areaNo; Integer zoneNo, areaNo;
String commandType = message.substring(0, 2); String commandType = message.substring(0, 2);
switch (commandType) { switch (commandType) {
case "CO": // communication status case "CO": // communication status
@ -53,24 +52,36 @@ public class DigiplexResponseResolver {
return CommunicationStatus.OK; return CommunicationStatus.OK;
} }
case "ZL": // zone label case "ZL": // zone label
zoneNo = Integer.valueOf(message.substring(2, 5)); zoneNo = getZoneOrArea(message);
if (zoneNo == null) {
return new ErroneousResponse(message);
}
if (message.contains(FAIL)) { if (message.contains(FAIL)) {
return ZoneLabelResponse.failure(zoneNo); return ZoneLabelResponse.failure(zoneNo);
} else { } else {
return ZoneLabelResponse.success(zoneNo, message.substring(5).trim()); return ZoneLabelResponse.success(zoneNo, message.substring(5).trim());
} }
case "AL": // area label case "AL": // area label
areaNo = Integer.valueOf(message.substring(2, 5)); areaNo = getZoneOrArea(message);
if (areaNo == null) {
return new ErroneousResponse(message);
}
if (message.contains(FAIL)) { if (message.contains(FAIL)) {
return AreaLabelResponse.failure(areaNo); return AreaLabelResponse.failure(areaNo);
} else { } else {
return AreaLabelResponse.success(areaNo, message.substring(5).trim()); return AreaLabelResponse.success(areaNo, message.substring(5).trim());
} }
case "RZ": // zone status case "RZ": // zone status
zoneNo = Integer.valueOf(message.substring(2, 5)); zoneNo = getZoneOrArea(message);
if (zoneNo == null) {
return new ErroneousResponse(message);
}
if (message.contains(FAIL)) { if (message.contains(FAIL)) {
return ZoneStatusResponse.failure(zoneNo); return ZoneStatusResponse.failure(zoneNo);
} else { } else {
if (message.length() < 10) {
return new ErroneousResponse(message);
}
return ZoneStatusResponse.success(zoneNo, // zone number return ZoneStatusResponse.success(zoneNo, // zone number
ZoneStatus.fromMessage(message.charAt(5)), // status ZoneStatus.fromMessage(message.charAt(5)), // status
toBoolean(message.charAt(6)), // alarm toBoolean(message.charAt(6)), // alarm
@ -79,10 +90,16 @@ public class DigiplexResponseResolver {
toBoolean(message.charAt(9))); // battery low toBoolean(message.charAt(9))); // battery low
} }
case "RA": // area status case "RA": // area status
areaNo = Integer.valueOf(message.substring(2, 5)); areaNo = getZoneOrArea(message);
if (areaNo == null) {
return new ErroneousResponse(message);
}
if (message.contains(FAIL)) { if (message.contains(FAIL)) {
return AreaStatusResponse.failure(areaNo); return AreaStatusResponse.failure(areaNo);
} else { } else {
if (message.length() < 12) {
return new ErroneousResponse(message);
}
return AreaStatusResponse.success(areaNo, // zone number return AreaStatusResponse.success(areaNo, // zone number
AreaStatus.fromMessage(message.charAt(5)), // status AreaStatus.fromMessage(message.charAt(5)), // status
toBoolean(message.charAt(6)), // zone in memory toBoolean(message.charAt(6)), // zone in memory
@ -95,7 +112,10 @@ public class DigiplexResponseResolver {
case "AA": // area arm case "AA": // area arm
case "AQ": // area quick arm case "AQ": // area quick arm
case "AD": // area disarm case "AD": // area disarm
areaNo = Integer.valueOf(message.substring(2, 5)); areaNo = getZoneOrArea(message);
if (areaNo == null) {
return new ErroneousResponse(message);
}
if (message.contains(FAIL)) { if (message.contains(FAIL)) {
return AreaArmDisarmResponse.failure(areaNo, ArmDisarmType.fromMessage(commandType)); return AreaArmDisarmResponse.failure(areaNo, ArmDisarmType.fromMessage(commandType));
} else { } else {
@ -105,21 +125,41 @@ public class DigiplexResponseResolver {
case "PG": // PGM events case "PG": // PGM events
default: default:
if (message.startsWith("G")) { if (message.startsWith("G")) {
return resolveSystemEvent(message); if (message.length() >= 12) {
return resolveSystemEvent(message);
} else {
return new ErroneousResponse(message);
}
} else { } else {
return new UnknownResponse(message); return new UnknownResponse(message);
} }
} }
} }
private static @Nullable Integer getZoneOrArea(String message) {
if (message.length() < 5) {
return null;
}
try {
return Integer.valueOf(message.substring(2, 5));
} catch (NumberFormatException e) {
return null;
}
}
private static boolean toBoolean(char value) { private static boolean toBoolean(char value) {
return value != 'O'; return value != 'O';
} }
private static DigiplexResponse resolveSystemEvent(String message) { private static DigiplexResponse resolveSystemEvent(String message) {
int eventGroup = Integer.parseInt(message.substring(1, 4)); int eventGroup, eventNumber, areaNumber;
int eventNumber = Integer.parseInt(message.substring(5, 8)); try {
int areaNumber = Integer.parseInt(message.substring(9, 12)); eventGroup = Integer.parseInt(message.substring(1, 4));
eventNumber = Integer.parseInt(message.substring(5, 8));
areaNumber = Integer.parseInt(message.substring(9, 12));
} catch (NumberFormatException e) {
return new ErroneousResponse(message);
}
switch (eventGroup) { switch (eventGroup) {
case 0: case 0:
return new ZoneStatusEvent(eventNumber, ZoneStatus.CLOSED, areaNumber); return new ZoneStatusEvent(eventNumber, ZoneStatus.CLOSED, areaNumber);

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2010-2025 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.digiplex.internal.communication;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Erroneous message from PRT3.
*
* Message that is invalid, which happens sometimes due to communication errors.
*
* @author Robert Michalak - Initial contribution
*
*/
@NonNullByDefault
public class ErroneousResponse implements DigiplexResponse {
public final String message;
public ErroneousResponse(String message) {
this.message = message;
}
@Override
public void accept(DigiplexMessageHandler visitor) {
visitor.handleErroneousResponse(this);
}
}

View File

@ -15,7 +15,9 @@ package org.openhab.binding.digiplex.internal.communication;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
/** /**
* Unknown message from PRT3 * Unknown message from PRT3.
*
* Message that is otherwise valid, but not handled in this binding.
* *
* @author Robert Michalak - Initial contribution * @author Robert Michalak - Initial contribution
* *

View File

@ -38,6 +38,7 @@ import org.openhab.binding.digiplex.internal.communication.DigiplexMessageHandle
import org.openhab.binding.digiplex.internal.communication.DigiplexRequest; import org.openhab.binding.digiplex.internal.communication.DigiplexRequest;
import org.openhab.binding.digiplex.internal.communication.DigiplexResponse; import org.openhab.binding.digiplex.internal.communication.DigiplexResponse;
import org.openhab.binding.digiplex.internal.communication.DigiplexResponseResolver; import org.openhab.binding.digiplex.internal.communication.DigiplexResponseResolver;
import org.openhab.binding.digiplex.internal.communication.ErroneousResponse;
import org.openhab.binding.digiplex.internal.communication.events.AbstractEvent; import org.openhab.binding.digiplex.internal.communication.events.AbstractEvent;
import org.openhab.binding.digiplex.internal.communication.events.TroubleEvent; import org.openhab.binding.digiplex.internal.communication.events.TroubleEvent;
import org.openhab.binding.digiplex.internal.communication.events.TroubleStatus; import org.openhab.binding.digiplex.internal.communication.events.TroubleStatus;
@ -295,6 +296,12 @@ public class DigiplexBridgeHandler extends BaseBridgeHandler implements SerialPo
updateState(channel, state); updateState(channel, state);
} }
} }
@Override
public void handleErroneousResponse(ErroneousResponse response) {
logger.debug("Erroneous response: {}", response.message);
handleCommunicationError();
}
} }
private class DigiplexReceiverThread extends Thread { private class DigiplexReceiverThread extends Thread {

View File

@ -0,0 +1,221 @@
/*
* Copyright (c) 2010-2025 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.digiplex.internal.communication;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
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.MethodSource;
import org.openhab.binding.digiplex.internal.communication.events.GenericEvent;
/**
* Tests for {@link DigiplexResponseResolver}
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class DigiplexResponseResolverTest {
@ParameterizedTest
@MethodSource("provideTestCasesForResolveResponseReturnsErroneousResponseWhenMessageIsMalformed")
void resolveResponseReturnsErroneousResponseWhenMessageIsMalformed(String message) {
DigiplexResponse actual = DigiplexResponseResolver.resolveResponse(message);
assertThat(actual, is(instanceOf(ErroneousResponse.class)));
if (actual instanceof ErroneousResponse erroneousResponse) {
assertThat(erroneousResponse.message, is(equalTo(message)));
}
}
private static Stream<Arguments> provideTestCasesForResolveResponseReturnsErroneousResponseWhenMessageIsMalformed() {
return Stream.of( //
Arguments.of("CO&"), Arguments.of("ZL&fail"), Arguments.of("ZL12"), Arguments.of("AL&fail"),
Arguments.of("AL12"), Arguments.of("RZZZ3COOOO&fail"), Arguments.of("RZ123C"),
Arguments.of("RZ123COOO"), Arguments.of("RA&fail"), Arguments.of("RA123DOOXOO"),
Arguments.of("AA&fail"), Arguments.of("GGGGGGGGGGGG"), Arguments.of("G1234567890"));
}
@Test
void resolveResponseReturnsCommunicationStatusSuccessWhenWellformed() {
String message = "CO&ok";
DigiplexResponse actual = DigiplexResponseResolver.resolveResponse(message);
assertThat(actual, is(instanceOf(CommunicationStatus.class)));
if (actual instanceof CommunicationStatus communicationStatus) {
assertThat(communicationStatus.success, is(true));
}
}
@Test
void resolveResponseReturnsCommunicationStatusFailureWhenMessageContainsFail() {
String message = "CO&fail";
DigiplexResponse actual = DigiplexResponseResolver.resolveResponse(message);
assertThat(actual, is(instanceOf(CommunicationStatus.class)));
if (actual instanceof CommunicationStatus communicationStatus) {
assertThat(communicationStatus.success, is(false));
}
}
@ParameterizedTest
@MethodSource("provideTestCasesForResolveResponseReturnsZoneLabelResponse")
void resolveResponseReturnsZoneLabelResponse(String message, boolean expectedSuccess, int expectedZoneNo,
String expectedName) {
DigiplexResponse actual = DigiplexResponseResolver.resolveResponse(message);
assertThat(actual, is(instanceOf(ZoneLabelResponse.class)));
if (actual instanceof ZoneLabelResponse zoneLabelResponse) {
assertThat(zoneLabelResponse.success, is(expectedSuccess));
assertThat(zoneLabelResponse.zoneNo, is(expectedZoneNo));
assertThat(zoneLabelResponse.zoneName, is(expectedName));
}
}
private static Stream<Arguments> provideTestCasesForResolveResponseReturnsZoneLabelResponse() {
return Stream.of( //
Arguments.of("ZL123", true, 123, ""), Arguments.of("ZL123test ", true, 123, "test"),
Arguments.of("ZL123&fail", false, 123, null), Arguments.of("ZL123test&fail", false, 123, null));
}
@ParameterizedTest
@MethodSource("provideTestCasesForResolveResponseReturnsAreaLabelResponse")
void resolveResponseReturnsAreaLabelResponse(String message, boolean expectedSuccess, int expectedAreaNo,
String expectedName) {
DigiplexResponse actual = DigiplexResponseResolver.resolveResponse(message);
assertThat(actual, is(instanceOf(AreaLabelResponse.class)));
if (actual instanceof AreaLabelResponse areaLabelResponse) {
assertThat(areaLabelResponse.success, is(expectedSuccess));
assertThat(areaLabelResponse.areaNo, is(expectedAreaNo));
assertThat(areaLabelResponse.areaName, is(expectedName));
}
}
private static Stream<Arguments> provideTestCasesForResolveResponseReturnsAreaLabelResponse() {
return Stream.of( //
Arguments.of("AL123", true, 123, ""), Arguments.of("AL123test ", true, 123, "test"),
Arguments.of("AL123&fail", false, 123, null), Arguments.of("AL123test&fail", false, 123, null));
}
@ParameterizedTest
@MethodSource("provideTestCasesForResolveResponseReturnsZoneStatusResponse")
void resolveResponseReturnsZoneStatusResponse(String message, boolean expectedSuccess, int expectedZoneNo,
ZoneStatus expectedZoneStatus, boolean expectedAlarm, boolean expectedFireAlarm,
boolean expectedSupervisionLost, boolean expectedLowBattery) {
DigiplexResponse actual = DigiplexResponseResolver.resolveResponse(message);
assertThat(actual, is(instanceOf(ZoneStatusResponse.class)));
if (actual instanceof ZoneStatusResponse zoneStatusResponse) {
assertThat(zoneStatusResponse.success, is(expectedSuccess));
assertThat(zoneStatusResponse.zoneNo, is(expectedZoneNo));
assertThat(zoneStatusResponse.status, is(expectedZoneStatus));
assertThat(zoneStatusResponse.alarm, is(expectedAlarm));
assertThat(zoneStatusResponse.fireAlarm, is(expectedFireAlarm));
assertThat(zoneStatusResponse.supervisionLost, is(expectedSupervisionLost));
assertThat(zoneStatusResponse.lowBattery, is(expectedLowBattery));
}
}
private static Stream<Arguments> provideTestCasesForResolveResponseReturnsZoneStatusResponse() {
return Stream.of( //
Arguments.of("RZ123COOOO", true, 123, ZoneStatus.CLOSED, false, false, false, false),
Arguments.of("RZ123OOOOO", true, 123, ZoneStatus.OPEN, false, false, false, false),
Arguments.of("RZ123TOOOO", true, 123, ZoneStatus.TAMPERED, false, false, false, false),
Arguments.of("RZ123FOOOO", true, 123, ZoneStatus.FIRE_LOOP_TROUBLE, false, false, false, false),
Arguments.of("RZ123uOOOO", true, 123, ZoneStatus.UNKNOWN, false, false, false, false),
Arguments.of("RZ123cOOOO", true, 123, ZoneStatus.UNKNOWN, false, false, false, false),
Arguments.of("RZ123cXOOO", true, 123, ZoneStatus.UNKNOWN, true, false, false, false),
Arguments.of("RZ123cOXOO", true, 123, ZoneStatus.UNKNOWN, false, true, false, false),
Arguments.of("RZ123cOOXO", true, 123, ZoneStatus.UNKNOWN, false, false, true, false),
Arguments.of("RZ123cOOOX", true, 123, ZoneStatus.UNKNOWN, false, false, false, true),
Arguments.of("RZ123&fail", false, 123, null, false, false, false, false),
Arguments.of("RZ123COOOO&fail", false, 123, null, false, false, false, false));
}
@ParameterizedTest
@MethodSource("provideTestCasesForResolveResponseReturnsAreaStatusResponse")
void resolveResponseReturnsAreaStatusResponse(String message, boolean expectedSuccess, int expectedAreaNo,
AreaStatus expectedAreaStatus, boolean expectedZoneInMemory, boolean expectedTrouble, boolean expectedReady,
boolean expectedInProgramming, boolean expectedAlarm, boolean expectedStrobe) {
DigiplexResponse actual = DigiplexResponseResolver.resolveResponse(message);
assertThat(actual, is(instanceOf(AreaStatusResponse.class)));
if (actual instanceof AreaStatusResponse areaStatusResponse) {
assertThat(areaStatusResponse.success, is(expectedSuccess));
assertThat(areaStatusResponse.areaNo, is(expectedAreaNo));
assertThat(areaStatusResponse.status, is(expectedAreaStatus));
assertThat(areaStatusResponse.zoneInMemory, is(expectedZoneInMemory));
assertThat(areaStatusResponse.trouble, is(expectedTrouble));
assertThat(areaStatusResponse.ready, is(expectedReady));
assertThat(areaStatusResponse.inProgramming, is(expectedInProgramming));
assertThat(areaStatusResponse.alarm, is(expectedAlarm));
assertThat(areaStatusResponse.strobe, is(expectedStrobe));
}
}
private static Stream<Arguments> provideTestCasesForResolveResponseReturnsAreaStatusResponse() {
return Stream.of( //
Arguments.of("RA123DOOXOOO", true, 123, AreaStatus.DISARMED, false, false, false, false, false, false),
Arguments.of("RA123AOOXOOO", true, 123, AreaStatus.ARMED, false, false, false, false, false, false),
Arguments.of("RA123FOOXOOO", true, 123, AreaStatus.ARMED_FORCE, false, false, false, false, false,
false),
Arguments.of("RA123SOOXOOO", true, 123, AreaStatus.ARMED_STAY, false, false, false, false, false,
false),
Arguments.of("RA123IOOXOOO", true, 123, AreaStatus.ARMED_INSTANT, false, false, false, false, false,
false),
Arguments.of("RA123uOOXOOO", true, 123, AreaStatus.UNKNOWN, false, false, false, false, false, false),
Arguments.of("RA123dOOXOOO", true, 123, AreaStatus.UNKNOWN, false, false, false, false, false, false),
Arguments.of("RA123dXOXOOO", true, 123, AreaStatus.UNKNOWN, true, false, false, false, false, false),
Arguments.of("RA123dOXxOOO", true, 123, AreaStatus.UNKNOWN, false, true, false, false, false, false),
Arguments.of("RA123dOOOOOO", true, 123, AreaStatus.UNKNOWN, false, false, true, false, false, false),
Arguments.of("RA123dOOXXOO", true, 123, AreaStatus.UNKNOWN, false, false, false, true, false, false),
Arguments.of("RA123dOOXOXO", true, 123, AreaStatus.UNKNOWN, false, false, false, false, true, false),
Arguments.of("RA123dOOXOOX", true, 123, AreaStatus.UNKNOWN, false, false, false, false, false, true),
Arguments.of("RA123&fail", false, 123, null, false, false, false, false, false, false),
Arguments.of("RA123DOOXOOO&fail", false, 123, null, false, false, false, false, false, false));
}
@ParameterizedTest
@MethodSource("provideTestCasesForResolveResponseReturnsAreaArmDisarmResponse")
void resolveResponseReturnsAreaArmDisarmResponse(String message, boolean expectedSuccess, int expectedAreaNo,
ArmDisarmType expectedType) {
DigiplexResponse actual = DigiplexResponseResolver.resolveResponse(message);
assertThat(actual, is(instanceOf(AreaArmDisarmResponse.class)));
if (actual instanceof AreaArmDisarmResponse armDisarmResponse) {
assertThat(armDisarmResponse.success, is(expectedSuccess));
assertThat(armDisarmResponse.areaNo, is(expectedAreaNo));
assertThat(armDisarmResponse.type, is(expectedType));
}
}
private static Stream<Arguments> provideTestCasesForResolveResponseReturnsAreaArmDisarmResponse() {
return Stream.of( //
Arguments.of("AA123", true, 123, ArmDisarmType.ARM),
Arguments.of("AQ123", true, 123, ArmDisarmType.QUICK_ARM),
Arguments.of("AD123", true, 123, ArmDisarmType.DISARM),
Arguments.of("AA123&fail", false, 123, ArmDisarmType.ARM),
Arguments.of("AQ123&fail", false, 123, ArmDisarmType.QUICK_ARM),
Arguments.of("AD123&fail", false, 123, ArmDisarmType.DISARM));
}
@Test
void resolveResponseReturnsGenericEventWhenWellformed() {
DigiplexResponse actual = DigiplexResponseResolver.resolveResponse("G123 456 789");
assertThat(actual, is(instanceOf(GenericEvent.class)));
if (actual instanceof GenericEvent genericEvent) {
assertThat(genericEvent.getEventGroup(), is(123));
assertThat(genericEvent.getEventNumber(), is(456));
assertThat(genericEvent.getAreaNo(), is(789));
}
}
}