Skip to content

Commit

Permalink
Add Jython Helper libraries
Browse files Browse the repository at this point in the history
This will should be merged after I have created a separate package for
the helper libraries, as discussed here...
openhab#7208 (comment).
For now, this was built to test using the helper libraries as a
separate bundle.

Signed-off-by: Scott Rushworth <openhab@5iver.com>
  • Loading branch information
Scott Rushworth committed Mar 25, 2020
1 parent 602b0ab commit 5b34677
Show file tree
Hide file tree
Showing 38 changed files with 3,948 additions and 171 deletions.
2 changes: 1 addition & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* @openhab/2-x-add-ons-maintainers

# Add-on maintainers:
/bundles/org.openhab.automation.scriptenginefactory.jython/ @openhab-5iver
/bundles/org.openhab.automation.helperlibraries.jython/ @openhab-5iver
/bundles/org.openhab.binding.airquality/ @kubawolanin
/bundles/org.openhab.binding.airvisualnode/ @3cky
/bundles/org.openhab.binding.allplay/ @dominicdesu
Expand Down
5 changes: 5 additions & 0 deletions bom/openhab-addons/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
<name>openHAB Add-ons :: BOM :: openHAB Add-ons</name>

<dependencies>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.automation.helperlibraries.jython</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.airquality</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@
<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">
<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="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<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="src" output="target/test-classes" path="src/test/java">
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<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"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Jython Helper Libraries

This addon provides helper libraries for use with Jython and scripted automation.
3 changes: 3 additions & 0 deletions bundles/org.openhab.automation.helperlibraries.jython/bnd.bnd
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Bundle-SymbolicName: ${project.artifactId}
DynamicImport-Package: *
-includeresource.resources: -src/main/resources
24 changes: 24 additions & 0 deletions bundles/org.openhab.automation.helperlibraries.jython/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?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.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>2.5.4-SNAPSHOT</version>
</parent>

<artifactId>org.openhab.automation.helperlibraries.jython</artifactId>

<name>openHAB Add-ons :: Automation :: Jython Helper Libraries</name>

<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.automation.module.script</artifactId>
<version>2.5.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.automation.scriptenginefactory.jython-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<features name="org.openhab.automation.helperlibraries.jython-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/2.5.0/xml/features</repository>

<feature name="openhab-scriptenginefactory-jython" description="Jython ScriptEngineFactory" version="${project.version}">
<feature name="openhab-helperlibraries-jython" description="Jython Helper Libraries" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-core-automation</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.automation.scriptenginefactory.jython/${project.version}</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.automation.helperlibraries.jython/${project.version}</bundle>
</feature>
</features>
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* 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.automation.helperlibraries.jython.internal;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.TreeSet;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* These helper libraries aid in the use of Jython within scripted automation.
*
* @author Scott Rushworth - Initial contribution
*/
@Component(service = JythonHelperLibraries.class)
@NonNullByDefault
public class JythonHelperLibraries {

protected final Logger logger = LoggerFactory.getLogger(getClass());
private static final String HELPER_LIBRARY_PYTHON_PATH = new StringBuilder(JythonHelperLibraries.class
.getProtectionDomain().getCodeSource().getLocation().toString().replace("file:", "")).toString();

@Activate
public JythonHelperLibraries() {
logger.debug("Loading Jython helper libraries");
String existingPythonPath = System.getProperty("python.path");
// logger.warn("existingPythonPath: '{}'", existingPythonPath);
if (existingPythonPath == null || existingPythonPath.isEmpty()) {
System.setProperty("python.path", HELPER_LIBRARY_PYTHON_PATH);
} else if (!existingPythonPath.contains(HELPER_LIBRARY_PYTHON_PATH)) {
TreeSet<String> newPythonPathList = new TreeSet<>(
new ArrayList<String>(Arrays.asList(existingPythonPath.split(File.pathSeparator))));
// logger.warn("Before addition: '{}'", newPythonPathList);
newPythonPathList.add(HELPER_LIBRARY_PYTHON_PATH);
// logger.warn("After addition: '{}'", newPythonPathList);
String newPythonPath = String.join(File.pathSeparator, newPythonPathList);
// logger.warn("String: '{}'", newPythonPath);
System.setProperty("python.path", newPythonPath);
}

logger.trace("python.home [{}], python.path [{}]", System.getProperty("python.home"),
System.getProperty("python.path"));
}

// This does not work properly, since Deactivate() is called immediately after Activate() due to there being no
// dependencies on the service.
// @Deactivate
// public void deactivate() {
// logger.debug("Unloading Jython helper libraries");
// String existingPythonPath = System.getProperty("python.path");
// //logger.warn("existingPythonPath: '{}'", existingPythonPath);
// if (existingPythonPath.contains(HELPER_LIBRARY_PYTHON_PATH)) {
// TreeSet<String> newPythonPathList = new TreeSet<>(
// new ArrayList<String>(Arrays.asList(existingPythonPath.split(File.pathSeparator))));
// //logger.warn("Before removal: '{}'", newPythonPathList);
// newPythonPathList.remove(HELPER_LIBRARY_PYTHON_PATH);
// //logger.warn("After removal: '{}'", newPythonPathList);
// String newPythonPath = String.join(File.pathSeparator, newPythonPathList);
// //logger.warn("String: '{}'", newPythonPath);
// System.setProperty("python.path", newPythonPath);
// }
// logger.trace("python.home [{}], python.path [{}], python.cachdir [{}]", System.getProperty("python.home"),
// System.getProperty("python.path"), System.getProperty("python.cachedir"));
// }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

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))
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 5b34677

Please # to comment.