Skip to content

Commit

Permalink
Add JythonScriptEngineFactory
Browse files Browse the repository at this point in the history
... w/ core and custom helper libraries! Requires openhab#1251. Fixes
https://github.com/openhab/openhab2-addons/issues/4801.

This dramatically simplifies the installation of Jython and the Jython
core and community helper libraries.

Questions:

* Should this project go into OHC or openhab2-addons? My preference is
to keep all automation in OHC and eventually split it out into another
repo.
* Is the copyOnWriteArray needed in ScriptModuleTyeProvider? I don't
think so.
* I've unrolled the Jython jar. Is there and issue with the NOTICE file?

I first used Jython 2.7.2b2, but it no longer works with recent builds
of OH (works with S1749 though). See openhab#1252. I was seeing the same error
with 2.7.1, so this bundle uses 2.7.0.

I have another PR that makes a custom NashornScriptEngineFactory, but
I'll wait for that one until the decisions have been made for this one.
If custom ScriptEngineFactories or to go into openhab2-addons, then
NashornScriptEngineFqactory should be moved there too, which will be
difficult to have it load by default.

Signed-off-by: Scott Rushworth <openhab@5iver.com>
  • Loading branch information
Scott Rushworth committed Dec 18, 2019
1 parent 853179e commit 169dcd9
Show file tree
Hide file tree
Showing 51 changed files with 4,061 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.automation.module.script.scriptenginefactory.jython</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>
Original file line number Diff line number Diff line change
@@ -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/openhab2-addons
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Jython ScriptEngineFactory

This addon provides a ScriptEngineFactory for Jython.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Bundle-SymbolicName: ${project.artifactId}
DynamicImport-Package: *
Import-Package: org.openhab.core.automation.module.script
-includeresource: @jython-standalone-2.7.[0-9a-z]*.jar; lib:=true
-includeresource.resources: -src/main/resources
-fixupmessages: "Classes found in the wrong directory",\
"The default package '.' is not permitted by the Import-Package syntax"; restrict:=error; is:=warning
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>

<artifactId>org.openhab.core.automation.module.script.scriptenginefactory.jython</artifactId>

<name>openHAB Core :: Bundles :: Jython ScriptEngineFactory</name>

<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.automation</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.automation.module.script</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.python/jython-standalone -->
<dependency>
<groupId>org.python</groupId>
<artifactId>jython-standalone</artifactId>
<version>2.7.0</version>
<scope>provided</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Copyright (c) 2010-2019 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.core.automation.module.script.scriptenginefactory.jython;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

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.annotations.Component;

/**
* An implementation of {@link ScriptEngineFactory} for Jython.
*
* @author Scott Rushworth - Initial contribution
*/
@NonNullByDefault
@Component(service = org.openhab.core.automation.module.script.ScriptEngineFactory.class)
public class JythonScriptEngineFactory extends AbstractScriptEngineFactory {

private static final String SCRIPT_TYPE = "py";
private static javax.script.ScriptEngineManager ENGINE_MANAGER = new javax.script.ScriptEngineManager();

public JythonScriptEngineFactory() {
String home = JythonScriptEngineFactory.class.getProtectionDomain().getCodeSource().getLocation().toString()
.replace("file:", "");
String openhabConf = System.getenv("OPENHAB_CONF");
StringBuilder newPythonPath = new StringBuilder();
String previousPythonPath = System.getenv("python.path");
if (previousPythonPath != null) {
newPythonPath.append(previousPythonPath).append(File.pathSeparator);
}
newPythonPath.append(openhabConf).append(File.separator).append("automation").append(File.separator)
.append("lib").append(File.separator).append("python");

System.setProperty("python.home", home);
System.setProperty("python.path", newPythonPath.toString());
System.setProperty("python.cachedir", openhabConf);
logger.trace("python.home [{}], python.path [{}]", System.getProperty("python.home"),
System.getProperty("python.path"));
}

@Override
public List<String> getScriptTypes() {
List<String> scriptTypes = new ArrayList<>();

for (javax.script.ScriptEngineFactory factory : ENGINE_MANAGER.getEngineFactories()) {
List<String> extensions = factory.getExtensions();

if (extensions.contains(SCRIPT_TYPE)) {
scriptTypes.addAll(extensions);
scriptTypes.addAll(factory.getMimeTypes());
}
}
return Collections.unmodifiableList(scriptTypes);
}

@Override
public @Nullable ScriptEngine createScriptEngine(String scriptType) {
ScriptEngine scriptEngine = ENGINE_MANAGER.getEngineByExtension(scriptType);
if (scriptEngine == null) {
scriptEngine = ENGINE_MANAGER.getEngineByMimeType(scriptType);
}
if (scriptEngine == null) {
scriptEngine = ENGINE_MANAGER.getEngineByName(scriptType);
}
return scriptEngine;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""
The ``area_triggers_and_actions`` package provides a mechanism for using group
logic to trigger rules and then perform a particular action.
This package provides the following modules:
* ``area_actions``
"""
__all__ = ['start_action', 'stop_timer']

from threading import Timer

from core.jsr223.scope import ON, OFF, OPEN, CLOSED
from core.metadata import get_key_value
from core.log import logging, LOG_PREFIX, log_traceback

from community.area_triggers_and_actions.area_actions import *

try:
import sys
import personal.area_triggers_and_actions.area_actions
reload(sys.modules['personal.area_triggers_and_actions.area_actions'])
from personal.area_triggers_and_actions.area_actions import *
except:
pass

#from org.joda.time import DateTime

log = logging.getLogger("{}.community.area_triggers_and_actions".format(LOG_PREFIX))

timer_dict = {}

@log_traceback
def _timer_function(item, active, function_name, timer_type, timer_delay, recurring, function):
"""This is the function called by the timers."""
#log.warn("_timer_function: item.name [{}], active [{}], function_name [{}], timer_type [{}], timer_delay [{}], recurring [{}], function [{}]".format(item.name, active, function_name, timer_type, timer_delay, recurring, function))
function(item, active)
log.debug("{}: [{}] second {} {} timer has completed".format(item.name, timer_delay, function_name, timer_type))
if recurring and item.state in [ON, OPEN] if active else item.state in [OFF, CLOSED]:
timer_dict.update({item.name: {function_name: {timer_type: Timer(timer_delay, _timer_function, [item, active, function_name, timer_type, timer_delay, recurring, function])}}})
timer_dict[item.name][function_name][timer_type].start()
log.debug("{}: [{}] second recurring {} {} timer has started".format(item.name, timer_delay, function_name, timer_type))

def start_action(item, active, function_name):
"""
This is the function called by the rule to begin the selected action,
which may be first passed through a timer.
Args:
item Item: The Item to perform the action on
active boolean: Area activity (True for active and False for inactive)
function_name string: Name of the action function
"""
#start_time = DateTime.now().getMillis()
timer_type = "ON" if active else "OFF"
function = globals()[function_name]
function_metadata = get_key_value(item.name, "area_triggers_and_actions", "actions", function_name)
limited = function_metadata.get("limited")
timer_metadata = function_metadata.get(timer_type, {})
if not limited or timer_metadata:
timer_delay = timer_metadata.get("delay")
recurring = timer_metadata.get("recurring")
#log.warn("start_action: item.name [{}], active [{}], function_name [{}], timer_type [{}], timer_delay [{}], recurring [{}], function [{}]".format(item.name, active, function_name, timer_type, timer_delay, recurring, function))
if not timer_delay:
function(item, active)
elif timer_dict.get(item.name, {}).get(function_name, {}).get(timer_type) is None or not timer_dict[item.name][function_name][timer_type].isAlive():# if timer does not exist, create it
timer_dict.update({item.name: {function_name: {timer_type: Timer(timer_delay, _timer_function, [item, active, function_name, timer_type, timer_delay, recurring, function])}}})
timer_dict[item.name][function_name][timer_type].start()
log.debug("{}: [{}] second {}{} {} timer has started".format(item.name, timer_delay, "recurring " if recurring else "", function_name, timer_type))
stop_timer(item.name, function_name, "OFF" if active else "ON")
#log.warn("Test: start_action: {}: [{}]: time=[{}]".format(item.name, timer_type, DateTime.now().getMillis() - start_time))

def stop_timer(item_name, function_name, timer_type):
"""This function stops the timer."""
#log.warn("stop_timer: function_name [{}], timer_type [{}], item_name [{}]".format(function_name, timer_type, item_name))
if timer_dict.get(item_name, {}).get(function_name, {}).get(timer_type) is not None and timer_dict[item_name][function_name][timer_type].isAlive():# if timer exists, stop it
timer_dict[item_name][function_name][timer_type].cancel()
log.debug("{}: {} {} timer has been cancelled".format(item_name, function_name, timer_type))
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""
The ``area_actions`` module contains the ``light_action`` and
``toggle_action`` functions that should be useable by everyone without
customization. Custom actions should not be put into this file, as they could
be overwritten during an upgrade. Instead, place them in the
``personal.area_triggers_and_actions.area_actions`` module.
"""
__all__ = ['light_action', 'toggle_action']

from core.jsr223.scope import events, items, PercentType, DecimalType, HSBType, ON, OFF
from core.metadata import get_key_value
from core.log import logging, LOG_PREFIX

import configuration
reload(configuration)
from configuration import area_triggers_and_actions_dict

#from org.joda.time import DateTime

log = logging.getLogger("{}.community.area_triggers_and_actions.area_actions".format(LOG_PREFIX))

def light_action(item, active):
"""
This function performs an action on a light Item.
When called, this function pulls in the metadata for the supplied Item or
uses the default values specified in
``configuration.area_triggers_and_actions_dict"["default_levels"]``, if the
metadata does not exist. This metadata is then compared to the current lux
level to determine if a light should be turned OFF or set to the specified
level. This function should work for everyone without modification.
Args:
Item item: The Item to perform the action on
boolean active: Area activity (True for active and False for inactive)
"""
#start_time = DateTime.now().getMillis()
item_metadata = get_key_value(item.name, "area_triggers_and_actions", "modes", str(items["Mode"]))
low_lux_trigger = item_metadata.get("low_lux_trigger", area_triggers_and_actions_dict["default_levels"]["low_lux_trigger"])
hue = DecimalType(item_metadata.get("hue", area_triggers_and_actions_dict["default_levels"]["hue"]))
saturation = PercentType(str(item_metadata.get("saturation", area_triggers_and_actions_dict["default_levels"]["saturation"])))
brightness = PercentType(str(item_metadata.get("brightness", area_triggers_and_actions_dict["default_levels"]["brightness"])))
#log.warn("light_action: item.name [{}], active [{}], brightness [{}], lux [{}], low_lux_trigger [{}]".format(item.name, active, brightness, items[area_triggers_and_actions_dict["lux_item_name"]], low_lux_trigger))
lux_item_name = get_key_value(item.name, "area_triggers_and_actions", "light_action", "lux_item_name") or area_triggers_and_actions_dict.get("lux_item_name")
if active and brightness > PercentType(0) and (True if lux_item_name is None else items[lux_item_name].intValue() <= low_lux_trigger):
if item.type == "Dimmer" or (item.type == "Group" and item.baseItem.type == "Dimmer"):
if item.state != brightness:
if item.state < PercentType(99):
events.sendCommand(item, brightness)
log.info(">>>>>>> {}: {}".format(item.name, brightness))
else:
log.info("[{}]: dimmer was manually set > 98, so not adjusting".format(item.name))
else:
log.debug("[{}]: dimmer is already set to [{}], so not sending command".format(item.name, brightness))
elif item.type == "Color" or (item.type == "Group" and item.baseType == "Color"):
if item.state != HSBType(hue, saturation, brightness):
if item.state.brightness < PercentType(99):
events.sendCommand(item, HSBType(hue, saturation, brightness))
log.info(">>>>>>> {}: [{}]".format(item.name, HSBType(hue, saturation, brightness)))
else:
log.info("[{}]: brightness was manually set > 98, so not adjusting".format(item.name))
else:
log.debug("[{}]: color is already set to [{}, {}, {}], so not sending command".format(item.name, hue, saturation, brightness))
elif item.type == "Switch" or (item.type == "Group" and item.baseItem.type == "Switch"):
if item.state == OFF:
events.sendCommand(item, ON)
log.info(">>>>>>> {}: ON".format(item.name))
else:
log.debug("[{}]: switch is already [ON], so not sending command".format(item.name))
else:
if item.type == "Dimmer" or (item.type == "Group" and item.baseItem.type == "Dimmer"):
if item.state != PercentType(0):
if item.state < PercentType(99):
events.sendCommand(item, PercentType(0))
log.info("<<<<<<<<<<<<<<<<<<<<< {}: 0".format(item.name))
else:
log.info("{}: dimmer was manually set > 98, so not adjusting".format(item.name))
else:
log.debug("[{}]: dimmer is already set to [0], so not sending command".format(item.name))
elif item.type == "Color" or (item.type == "Group" and item.baseType == "Color"):
if item.state != HSBType(DecimalType(0), PercentType(0), PercentType(0)):
if item.state.brightness < PercentType(99):
events.sendCommand(item, "0, 0, 0")
log.info("<<<<<<<<<<<<<<<<<<<<< {}: [0, 0, 0]".format(item.name))
else:
log.info("{}: brightness was manually set > 98, so not adjusting".format(item.name))
else:
log.debug("[{}]: color is already set to [0, 0, 0], so not sending command".format(item.name))
elif item.type == "Switch" or (item.type == "Group" and item.baseItem.type == "Switch"):
if item.state == ON:
events.sendCommand(item, OFF)
log.info("<<<<<<<<<<<<<<<<<<<<< {}: OFF".format(item.name))
else:
log.debug("[{}]: switch is already set to [OFF], so not sending command".format(item.name))
#log.warn("Test: light_action: {}: [{}]: time=[{}]".format(item.name, "ON" if active else "OFF", DateTime.now().getMillis() - start_time))

def toggle_action(item, active):
"""
This function sends the OFF command to the Item.
Args:
Item item: The Item to perform the action on
boolean active: Area activity (True for active and False for inactive)
"""
events.sendCommand(item, ON if item.state == OFF else OFF)
Loading

0 comments on commit 169dcd9

Please sign in to comment.