From ffc9556982088d0a84c0fa91d1e30f9db577dffb Mon Sep 17 00:00:00 2001 From: Brian O'Connell Date: Sun, 29 Nov 2020 08:20:54 -0500 Subject: [PATCH] feat(jruby) JRuby scripting initial binding commit Signed-off-by: Brian O'Connell --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../NOTICE | 13 + .../README.md | 74 +++++ .../pom.xml | 35 +++ .../src/main/feature/feature.xml | 9 + .../JRubyScriptEngineConfiguration.java | 296 ++++++++++++++++++ .../internal/JRubyScriptEngineFactory.java | 115 +++++++ .../jrubyscripting/internal/package-info.java | 21 ++ bundles/pom.xml | 1 + 10 files changed, 570 insertions(+) create mode 100644 bundles/org.openhab.automation.jrubyscripting/NOTICE create mode 100644 bundles/org.openhab.automation.jrubyscripting/README.md create mode 100644 bundles/org.openhab.automation.jrubyscripting/pom.xml create mode 100644 bundles/org.openhab.automation.jrubyscripting/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/binding/jrubyscripting/internal/JRubyScriptEngineConfiguration.java create mode 100644 bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/binding/jrubyscripting/internal/JRubyScriptEngineFactory.java create mode 100644 bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/binding/jrubyscripting/internal/package-info.java diff --git a/CODEOWNERS b/CODEOWNERS index a74a451fe9807..6f004ed451fea 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -6,6 +6,7 @@ # Add-on maintainers: /bundles/org.openhab.automation.groovyscripting/ @wborn +/bundles/org.openhab.automation.jrubyscripting/ @boc-tothefuture /bundles/org.openhab.automation.jsscripting/ @jpg0 /bundles/org.openhab.automation.jythonscripting/ @openhab/add-ons-maintainers /bundles/org.openhab.automation.pidcontroller/ @fwolter diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 67288274817e9..6646e23c03d0c 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -21,6 +21,11 @@ org.openhab.automation.groovyscripting ${project.version} + + org.openhab.addons.bundles + org.openhab.automation.jrubyscripting + ${project.version} + org.openhab.addons.bundles org.openhab.automation.jsscripting diff --git a/bundles/org.openhab.automation.jrubyscripting/NOTICE b/bundles/org.openhab.automation.jrubyscripting/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.automation.jrubyscripting/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.automation.jrubyscripting/README.md b/bundles/org.openhab.automation.jrubyscripting/README.md new file mode 100644 index 0000000000000..b3a3269ca3358 --- /dev/null +++ b/bundles/org.openhab.automation.jrubyscripting/README.md @@ -0,0 +1,74 @@ +# JRuby Scripting + +This add-on provides [JRuby](https://www.jruby.org/) 9.3.1 that can be used as a scripting language within automation rules. + +## JRuby Scripting Configuration + +JRuby configuration parameters may be set by creating a jruby.cfg file in $OPENHAB_CONF/services/ + + +| Parameter | Default | Description | +|-------------------------------------------------------|-----------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| org.openhab.automation.jrubyscripting:gem_home | $OPENHAB_CONF/scripts/lib/ruby/gem_home | Location ruby gems will be installed and loaded, directory will be created if missing and gem installs are specified from | +| org.openhab.automation.jrubyscripting:rubylib | $OPENHAB_CONF/automation/lib/ruby/ | Search path for user libraries | +| org.openhab.automation.jrubyscripting:local_context | threadsafe | The local context holds Ruby runtime, name-value pairs for sharing variables between Java and Ruby. See [this](https://github.com/jruby/jruby/wiki/RedBridge#Context_Instance_Type) for options and details | +| org.openhab.automation.jrubyscripting:local_variables | transient | Defines how variables are shared between Ruby and Java. See [this](https://github.com/jruby/jruby/wiki/RedBridge#local-variable-behavior-options) for options and details | +| org.openhab.automation.jrubyscripting:gems | | Comma separated list of [Ruby Gems](https://rubygems.org/) to install. | + + +## Ruby Gems + +This binding will install user specified gems and make them available on the library search path. +Gem versions may be specified using the standard ruby gem_name=version format. +The version number follows the [pessimistic version constraint](https://guides.rubygems.org/patterns/#pessimistic-version-constraint) syntax. + +For example this configuration will install version 4 or higher of the [OpenHAB JRuby Scripting Libray](https://boc-tothefuture.github.io/openhab-jruby/). + +```text +org.openhab.automation.jrubyscripting:gems=openhab-scripting=~>4.0 +``` + +## Creating JRuby Scripts + +When this add-on is installed, you can select JRuby as a scripting language when creating a script action within the rule editor of the UI. + +Alternatively, you can create scripts in the `automation/jsr223` configuration directory. +If you create an empty file called `test.rb`, you will see a log line with information similar to: + +```text + ... [INFO ] [.a.m.s.r.i.l.ScriptFileWatcher:150 ] - Loading script 'test.rb' +``` + +To enable debug logging, use the [console logging]({{base}}/administration/logging.html) commands to +enable debug logging for the automation functionality: + +```text +log:set DEBUG org.openhab.core.automation +log:set DEBUG org.openhab.binding.jrubyscripting +``` + +## Imports + +All [ScriptExtensions]({{base}}/configuration/jsr223.html#scriptextension-objects-all-jsr223-languages) are available in JRuby with the following exceptions/modifications: + +* The File variable, referencing java.io.File is not available as it conflicts with Ruby's File class preventing Ruby from initializing +* Globals scriptExtension, automationManager, ruleRegistry, items, voice, rules, things, events, itemRegistry, ir, actions, se, audio, lifecycleTracker are prepended with a $ (e.g. $automationManager) making them available as a global objects in Ruby. + + +## Script Examples + +JRuby scripts provide access to almost all the functionality in an openHAB runtime environment. +As a simple example, the following script logs "Hello, World!". +Note that `puts` will usually not work since the output has no terminal to display the text. +The openHAB server uses the [SLF4J](https://www.slf4j.org/) library for logging. + +```ruby +require 'java' +java_import org.slf4j.LoggerFactory + +LoggerFactory.getLogger("org.openhab.core.automation.examples").info("Hello world!") +``` + +JRuby can [import Java classes](https://github.com/jruby/jruby/wiki/CallingJavaFromJRuby). +Depending on the openHAB logging configuration, you may need to prefix logger names with `org.openhab.core.automation` for them to show up in the log file (or you modify the logging configuration). + diff --git a/bundles/org.openhab.automation.jrubyscripting/pom.xml b/bundles/org.openhab.automation.jrubyscripting/pom.xml new file mode 100644 index 0000000000000..1d150eae78137 --- /dev/null +++ b/bundles/org.openhab.automation.jrubyscripting/pom.xml @@ -0,0 +1,35 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.2.0-SNAPSHOT + + + org.openhab.automation.jrubyscripting + + openHAB Add-ons :: Automation :: JRuby Scripting + + + + com.ibm.icu.*;resolution:=optional, + org.abego.treelayout.*;resolution:=optional, + org.apache.ivy.*;resolution:=optional, + org.stringtemplate.v4.*;resolution:=optional + 9.3.1.0 + + + + + org.jruby + jruby-complete + ${jruby.version} + compile + + + + diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/feature/feature.xml b/bundles/org.openhab.automation.jrubyscripting/src/main/feature/feature.xml new file mode 100644 index 0000000000000..2ef4f18277724 --- /dev/null +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.automation.jrubyscripting/${project.version} + + diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/binding/jrubyscripting/internal/JRubyScriptEngineConfiguration.java b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/binding/jrubyscripting/internal/JRubyScriptEngineConfiguration.java new file mode 100644 index 0000000000000..d7b779f04e642 --- /dev/null +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/binding/jrubyscripting/internal/JRubyScriptEngineConfiguration.java @@ -0,0 +1,296 @@ +/** + * 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.jrubyscripting.internal; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.OpenHAB; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * Processes JRuby Configuration Parameters + * + * @author Brian O'Connell - Initial contribution + */ +@NonNullByDefault +public class JRubyScriptEngineConfiguration { + + private final Logger logger = LoggerFactory.getLogger(JRubyScriptEngineConfiguration.class); + + private final static Path DEFAULT_GEM_HOME = Paths.get(OpenHAB.getConfigFolder(), "scripts", "lib", "ruby", + "gem_home"); + + private final static Path DEFAULT_RUBYLIB = Paths.get(OpenHAB.getConfigFolder(), "automation", "lib", "ruby"); + + private final static String GEM_HOME = "gem_home"; + + // Map of configuration parameters + private final static Map CONFIGURATION_PARAMETERS = Map.ofEntries( + Map.entry("local_context", + new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.SYSTEM_PROPERTY) + .mappedTo("org.jruby.embed.localcontext.scope").defaultValue("threadsafe").build()), + + Map.entry("local_variable", + new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.SYSTEM_PROPERTY) + .mappedTo("org.jruby.embed.localvariable.behavior").defaultValue("transient").build()), + + Map.entry(GEM_HOME, + new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT) + .mappedTo("GEM_HOME").defaultValue(DEFAULT_GEM_HOME.toString()).build()), + + Map.entry("rubylib", + new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT) + .mappedTo("RUBYLIB").defaultValue(DEFAULT_RUBYLIB.toString()).build()), + + Map.entry("gems", new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.GEM).build())); + + private final static Map> CONFIGURATION_TYPE_MAP = CONFIGURATION_PARAMETERS + .values().stream().collect(Collectors.groupingBy(v -> v.type)); + + /** + * Update configuration + * + * @param config Configuration parameters to apply to ScripEngine + * @param factory ScriptEngineFactory to configure + */ + void update(Map config, ScriptEngineFactory factory) { + logger.trace("JRuby Script Engine Configuration: {}", config); + config.forEach(this::processConfigValue); + configureScriptEngine(factory); + } + + /** + * Apply configuration key/value to known configuration parameters + * + * @param key Configuration key + * @param value Configuration value + */ + private void processConfigValue(String key, Object value) { + OptionalConfigurationElement configurationElement = CONFIGURATION_PARAMETERS.get(key); + if (configurationElement != null) { + configurationElement.setValue(value.toString()); + } else { + logger.debug("Ignoring unexpected configuration key: {}", key); + } + } + + /** + * Configure the ScriptEngine + * + * @param factory Script Engine to configure + */ + void configureScriptEngine(ScriptEngineFactory factory) { + + configureSystemProperties(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.SYSTEM_PROPERTY, + Collections. emptyList())); + + ScriptEngine engine = factory.getScriptEngine(); + + configureRubyEnvironment(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT, + Collections. emptyList()), engine); + + configureGems(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.GEM, + Collections. emptyList()), engine); + } + + /** + * Makes Gem home directory if it does not exist + */ + private void ensureGemHomeExists() { + + OptionalConfigurationElement gemHomeConfigElement = CONFIGURATION_PARAMETERS.get(GEM_HOME); + if (gemHomeConfigElement != null) { + Optional gemHome = gemHomeConfigElement.getValue(); + if (gemHome.isPresent()) { + File gemHomeDirectory = new File(gemHome.get()); + if (gemHomeDirectory.exists() == false) { + logger.debug("gem_home directory does not exist, creating"); + boolean created = gemHomeDirectory.mkdirs(); + if (created == false) { + logger.debug("Error creating gem_home direcotry"); + } + } + } else { + logger.debug("Gem install requested without gem_home specified, not ensuring gem_home path exists"); + } + } + } + + /** + * Install a gems in ScriptEngine + * + * @param gemsDirectives List of gems to install + * @param engine Engine to install gems + */ + private synchronized void configureGems(List gemDirectives, ScriptEngine engine) { + for (OptionalConfigurationElement gemDirective : gemDirectives) { + if (gemDirective.getValue().isPresent()) { + + ensureGemHomeExists(); + + String[] gems = gemDirective.getValue().get().split(","); + for (String gem : gems) { + gem = gem.trim(); + String gemCommand; + if (gem.contains("=")) { + String[] gemParts = gem.split("="); + gem = gemParts[0]; + String version = gemParts[1]; + gemCommand = "Gem.install('" + gem + "',version='" + version + "')\n"; + } else { + gemCommand = "Gem.install('" + gem + "')\n"; + } + + try { + logger.debug("Installing Gem: {} ", gem); + logger.trace("Gem install code:\n{}\n", gemCommand); + engine.eval(gemCommand); + } catch (Exception e) { + logger.error("Error installing Gem", e); + } + } + } else { + logger.debug("Ruby gem property has no value"); + } + } + } + + /** + * Configure the base Ruby Environment + * + * @param engine Engine to configure + */ + public ScriptEngine configureRubyEnvironment(ScriptEngine engine) { + configureRubyEnvironment(CONFIGURATION_TYPE_MAP.getOrDefault(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT, + Collections. emptyList()), engine); + return engine; + } + + /** + * Configure the optional elements of the Ruby Environment + * + * @param optionalConfigurationElements Optional elements to configure in the ruby environment + * @param engine Engine in which to configure environment + */ + private void configureRubyEnvironment(List optionalConfigurationElements, + ScriptEngine engine) { + for (OptionalConfigurationElement configElement : optionalConfigurationElements) { + String environmentProperty = configElement.mappedTo().get(); + if (configElement.getValue().isPresent()) { + String environmentSetting = "ENV['" + environmentProperty + "']='" + configElement.getValue().get() + + "'"; + try { + logger.trace("Setting Ruby environment with code: {} ", environmentSetting); + engine.eval(environmentSetting); + } catch (ScriptException e) { + logger.error("Error setting ruby environment", e); + } + } else { + logger.debug("Ruby environment property ({}) has no value", environmentProperty); + } + } + } + + /** + * Configure system properties + * + * @param optionalConfigurationElements Optional system properties to configure + */ + private void configureSystemProperties(List optionalConfigurationElements) { + for (OptionalConfigurationElement configElement : optionalConfigurationElements) { + String systemProperty = configElement.mappedTo().get(); + if (configElement.getValue().isPresent()) { + String propertyValue = configElement.getValue().get(); + logger.trace("Setting system property ({}) to ({})", systemProperty, propertyValue); + System.setProperty(systemProperty, propertyValue); + } else { + logger.warn("System property ({}) has no value", systemProperty); + } + } + } + + /** + * Inner static companion class for configuration elements + */ + private static class OptionalConfigurationElement { + + private final Optional defaultValue; + private final Optional mappedTo; + private final Type type; + private Optional value; + + private OptionalConfigurationElement(Type type, @Nullable String mappedTo, @Nullable String defaultValue) { + this.type = type; + this.defaultValue = Optional.ofNullable(defaultValue); + this.mappedTo = Optional.ofNullable(mappedTo); + value = Optional.empty(); + } + + private Optional getValue() { + return value.or(() -> defaultValue); + } + + private void setValue(String value) { + this.value = Optional.of(value); + } + + private Optional mappedTo() { + return mappedTo; + } + + private enum Type { + SYSTEM_PROPERTY, + RUBY_ENVIRONMENT, + GEM + } + + private static class Builder { + private final Type type; + private @Nullable String defaultValue = null; + private @Nullable String mappedTo = null; + + private Builder(Type type) { + this.type = type; + } + + private Builder mappedTo(String mappedTo) { + this.mappedTo = mappedTo; + return this; + } + + private Builder defaultValue(String value) { + this.defaultValue = value; + return this; + } + + private OptionalConfigurationElement build() { + return new OptionalConfigurationElement(type, mappedTo, defaultValue); + } + } + } +} diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/binding/jrubyscripting/internal/JRubyScriptEngineFactory.java b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/binding/jrubyscripting/internal/JRubyScriptEngineFactory.java new file mode 100644 index 0000000000000..b63890fedcc25 --- /dev/null +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/binding/jrubyscripting/internal/JRubyScriptEngineFactory.java @@ -0,0 +1,115 @@ +/** + * 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.jrubyscripting.internal; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.script.ScriptEngine; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.module.script.AbstractScriptEngineFactory; +import org.openhab.core.automation.module.script.ScriptEngineFactory; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Modified; + +/** + * This is an implementation of a {@link ScriptEngineFactory} for Ruby. + * handlers. + * + * @author Brian O'Connell - Initial contribution + */ +@NonNullByDefault +@Component(service = ScriptEngineFactory.class, configurationPid = "org.openhab.automation.jrubyscripting") +public class JRubyScriptEngineFactory extends AbstractScriptEngineFactory { + + private final JRubyScriptEngineConfiguration configuration = new JRubyScriptEngineConfiguration(); + + // Filter out the File entry to prevent shadowing the Ruby File class which breaks Ruby in spectacularly + // difficult ways to debug. + private final static Set FILTERED_PRESETS = Set.of("File"); + private final static Set INSTANCE_PRESETS = Set.of(); + private final static Set GLOBAL_PRESETS = Set.of("scriptExtension", "automationManager", "ruleRegistry", + "items", "voice", "rules", "things", "events", "itemRegistry", "ir", "actions", "se", "audio", + "lifecycleTracker"); + + private final javax.script.ScriptEngineFactory factory = new org.jruby.embed.jsr223.JRubyEngineFactory(); + + // formatter turned off because it does not appropriately format chained streams + private final List scriptTypes = Stream + .concat(factory.getExtensions().stream(), factory.getMimeTypes().stream()) + .collect(Collectors.toUnmodifiableList()); + + // Adds @ in front of a set of variables so that Ruby recogonizes them as instance variables + private static Map.Entry mapInstancePresets(Map.Entry entry) { + if (INSTANCE_PRESETS.contains(entry.getKey())) { + return Map.entry("@" + entry.getKey(), entry.getValue()); + } else { + return entry; + } + } + + // Adds $ in front of a set of variables so that Ruby recogonizes them as global variables + private static Map.Entry mapGlobalPresets(Map.Entry entry) { + if (GLOBAL_PRESETS.contains(entry.getKey())) { + return Map.entry("$" + entry.getKey(), entry.getValue()); + } else { + return entry; + } + } + + // The activate call is activate binding and set the bindings configuration + @Activate + protected void activate(ComponentContext componentContext, Map config) { + configuration.update(config, factory); + } + + // The modified call updates configuration for binding + @Modified + protected void modified(Map config) { + configuration.update(config, factory); + } + + @Override + public List getScriptTypes() { + return scriptTypes; + } + + @Override + public void scopeValues(ScriptEngine scriptEngine, Map scopeValues) { + + // Empty comments prevent the formatter from breaking up the correct streams chaining + Map filteredScopeValues = // + scopeValues // + .entrySet() // + .stream() // + .filter(map -> !FILTERED_PRESETS.contains(map.getKey())) // + .map(JRubyScriptEngineFactory::mapInstancePresets) // + .map(JRubyScriptEngineFactory::mapGlobalPresets) // + .collect(Collectors.toMap(map -> map.getKey(), map -> map.getValue())); // + + super.scopeValues(scriptEngine, filteredScopeValues); + } + + @Override + public @Nullable ScriptEngine createScriptEngine(String scriptType) { + return scriptTypes.contains(scriptType) ? configuration.configureRubyEnvironment(factory.getScriptEngine()) + : null; + } +} diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/binding/jrubyscripting/internal/package-info.java b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/binding/jrubyscripting/internal/package-info.java new file mode 100644 index 0000000000000..37b3d5a817918 --- /dev/null +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/binding/jrubyscripting/internal/package-info.java @@ -0,0 +1,21 @@ +/** + * 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 + */ + +@org.osgi.annotation.bundle.Header(name = org.osgi.framework.Constants.DYNAMICIMPORT_PACKAGE, value = "*") +package org.openhab.binding.jrubyscripting.internal; + +/** + * Additional information for the JRuby Scripting package + * + * @author Brian O'Connell - Initial contribution + */ diff --git a/bundles/pom.xml b/bundles/pom.xml index 345fc9daecd35..aee3db282c397 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -21,6 +21,7 @@ org.openhab.automation.groovyscripting org.openhab.automation.jsscripting org.openhab.automation.jythonscripting + org.openhab.automation.jrubyscripting org.openhab.automation.pidcontroller org.openhab.automation.pwm