mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 07:02:02 +01:00
Compare commits
5 Commits
942a58ad26
...
74184aa889
Author | SHA1 | Date | |
---|---|---|---|
|
74184aa889 | ||
|
98ff656400 | ||
|
adacdebb9f | ||
|
3327261c71 | ||
|
bb2a06f398 |
@ -3,11 +3,55 @@
|
||||
|
||||
# JRuby Scripting
|
||||
|
||||
This add-on provides [JRuby](https://www.jruby.org/) 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 provides native Ruby access to common openHAB functionality within rules including items, things, actions, logging and more.
|
||||
This add-on provides Ruby scripting language for automation rules.
|
||||
It includes the [openhab-scripting](https://openhab.github.io/openhab-jruby/) helper library, a comprehensive Ruby gem designed to enhance automation in openHAB.
|
||||
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).
|
||||
|
||||
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)
|
||||
- [Installation](#installation)
|
||||
- [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)
|
||||
- [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?
|
||||
|
||||
- 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 makes writing automation enjoyable without having to fight with compilers and interpreters.
|
||||
- Rich ecosystem of tools, including things like Rubocop to help developers write clean code and RSpec to test the libraries.
|
||||
- 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 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.
|
||||
|
||||
### 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/)
|
||||
- Extensible.
|
||||
- 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
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. openHAB 3.4+
|
||||
1. The JRuby Scripting Language Addon
|
||||
|
||||
### 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).
|
||||
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)
|
||||
- Go to `Settings -> Add-ons -> Automation` and install the jrubyscripting automation addon following the [openHAB instructions](https://www.openhab.org/docs/configuration/addons.html).
|
||||
|
||||
### Using Files
|
||||
|
||||
1. 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
|
||||
```
|
||||
- Edit `<OPENHAB_CONF>/services/addons.cfg` and ensure that `jrubyscripting` is included in an uncommented `automation=` list of automations to install.
|
||||
|
||||
## Configuration
|
||||
|
||||
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/`.
|
||||
|
||||
> **_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.
|
||||
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.
|
||||
Simply change the `gems` and `require` configuration settings.
|
||||
|
||||
| Parameter | Description |
|
||||
| --------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `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` |
|
||||
| `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` |
|
||||
| `rubylib` | Search path for user libraries. <br/><br/>Default: `$OPENHAB_CONF/automation/ruby/lib` |
|
||||
| `dependency_tracking` | Enable dependency tracking. <br/><br/>Default: `true` |
|
||||
| `local_context` | See notes below. <br/><br/>Default: `singlethread` |
|
||||
| `local_variables` | See notes below. <br/><br/>Default: `transient` |
|
||||
| Parameter | Description |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `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` |
|
||||
| `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` |
|
||||
| `rubylib` | Search path for user libraries. <br/><br/>Default: `$OPENHAB_CONF/automation/ruby/lib` |
|
||||
| `dependency_tracking` | Enable dependency tracking. <br/><br/>Default: `true` |
|
||||
| `local_context` | See notes below. <br/><br/>Default: `singlethread` |
|
||||
| `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:
|
||||
|
||||
@ -766,8 +794,9 @@ To log a message on `INFO` log level:
|
||||
logger.info("The current time is #{Time.now}")
|
||||
```
|
||||
|
||||
The default logger name for UI rules is `org.openhab.automation.jrubyscripting.script`.
|
||||
For file-based rules, it's based on the rule's ID, such as `org.openhab.automation.jrubyscripting.rule.myrule.rb:15`.
|
||||
The main logger prefix is `org.openhab.automation.jrubyscripting`.
|
||||
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:
|
||||
|
||||
@ -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`.
|
||||
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).
|
||||
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).
|
||||
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
|
||||
rule "simple timer" do
|
||||
@ -1065,6 +1094,10 @@ end
|
||||
start_of_day = ZonedDateTime.now.with(LocalTime::MIDNIGHT)
|
||||
# or
|
||||
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 `<`
|
||||
max = Solar_Power.maximum_since(24.hours.ago)
|
||||
|
@ -51,7 +51,6 @@ public class DigiplexBindingConstants {
|
||||
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_EVENTS_RECEIVED = "statistics#events_received";
|
||||
|
||||
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_BATTERY_FAILURE = "troubles#battery_failure";
|
||||
|
@ -52,6 +52,9 @@ public interface DigiplexMessageHandler {
|
||||
default void handleUnknownResponse(UnknownResponse response) {
|
||||
}
|
||||
|
||||
default void handleErroneousResponse(ErroneousResponse response) {
|
||||
}
|
||||
|
||||
// Events
|
||||
default void handleZoneEvent(ZoneEvent event) {
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
package org.openhab.binding.digiplex.internal.communication;
|
||||
|
||||
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.AreaEventType;
|
||||
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
|
||||
*
|
||||
* @author Robert Michalak - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DigiplexResponseResolver {
|
||||
|
||||
private static final String OK = "&ok";
|
||||
// TODO: handle failures
|
||||
private static final String FAIL = "&fail";
|
||||
|
||||
public static DigiplexResponse resolveResponse(String message) {
|
||||
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);
|
||||
switch (commandType) {
|
||||
case "CO": // communication status
|
||||
@ -53,24 +52,36 @@ public class DigiplexResponseResolver {
|
||||
return CommunicationStatus.OK;
|
||||
}
|
||||
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)) {
|
||||
return ZoneLabelResponse.failure(zoneNo);
|
||||
} else {
|
||||
return ZoneLabelResponse.success(zoneNo, message.substring(5).trim());
|
||||
}
|
||||
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)) {
|
||||
return AreaLabelResponse.failure(areaNo);
|
||||
} else {
|
||||
return AreaLabelResponse.success(areaNo, message.substring(5).trim());
|
||||
}
|
||||
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)) {
|
||||
return ZoneStatusResponse.failure(zoneNo);
|
||||
} else {
|
||||
if (message.length() < 10) {
|
||||
return new ErroneousResponse(message);
|
||||
}
|
||||
return ZoneStatusResponse.success(zoneNo, // zone number
|
||||
ZoneStatus.fromMessage(message.charAt(5)), // status
|
||||
toBoolean(message.charAt(6)), // alarm
|
||||
@ -79,10 +90,16 @@ public class DigiplexResponseResolver {
|
||||
toBoolean(message.charAt(9))); // battery low
|
||||
}
|
||||
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)) {
|
||||
return AreaStatusResponse.failure(areaNo);
|
||||
} else {
|
||||
if (message.length() < 12) {
|
||||
return new ErroneousResponse(message);
|
||||
}
|
||||
return AreaStatusResponse.success(areaNo, // zone number
|
||||
AreaStatus.fromMessage(message.charAt(5)), // status
|
||||
toBoolean(message.charAt(6)), // zone in memory
|
||||
@ -95,7 +112,10 @@ public class DigiplexResponseResolver {
|
||||
case "AA": // area arm
|
||||
case "AQ": // area quick arm
|
||||
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)) {
|
||||
return AreaArmDisarmResponse.failure(areaNo, ArmDisarmType.fromMessage(commandType));
|
||||
} else {
|
||||
@ -105,21 +125,41 @@ public class DigiplexResponseResolver {
|
||||
case "PG": // PGM events
|
||||
default:
|
||||
if (message.startsWith("G")) {
|
||||
return resolveSystemEvent(message);
|
||||
if (message.length() >= 12) {
|
||||
return resolveSystemEvent(message);
|
||||
} else {
|
||||
return new ErroneousResponse(message);
|
||||
}
|
||||
} else {
|
||||
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) {
|
||||
return value != 'O';
|
||||
}
|
||||
|
||||
private static DigiplexResponse resolveSystemEvent(String message) {
|
||||
int eventGroup = Integer.parseInt(message.substring(1, 4));
|
||||
int eventNumber = Integer.parseInt(message.substring(5, 8));
|
||||
int areaNumber = Integer.parseInt(message.substring(9, 12));
|
||||
int eventGroup, eventNumber, areaNumber;
|
||||
try {
|
||||
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) {
|
||||
case 0:
|
||||
return new ZoneStatusEvent(eventNumber, ZoneStatus.CLOSED, areaNumber);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -15,7 +15,9 @@ package org.openhab.binding.digiplex.internal.communication;
|
||||
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
|
||||
*
|
||||
|
@ -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.DigiplexResponse;
|
||||
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.TroubleEvent;
|
||||
import org.openhab.binding.digiplex.internal.communication.events.TroubleStatus;
|
||||
@ -295,6 +296,12 @@ public class DigiplexBridgeHandler extends BaseBridgeHandler implements SerialPo
|
||||
updateState(channel, state);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleErroneousResponse(ErroneousResponse response) {
|
||||
logger.debug("Erroneous response: {}", response.message);
|
||||
handleCommunicationError();
|
||||
}
|
||||
}
|
||||
|
||||
private class DigiplexReceiverThread extends Thread {
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user