From 7314704b6076a4ab64b440fa64c37ef420e5ab97 Mon Sep 17 00:00:00 2001 From: Scott Rushworth Date: Sun, 1 Dec 2019 05:11:38 -0500 Subject: [PATCH] Add JythonScriptEngineFactory ... w/ core and custom helper libraries! Requires #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 #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 --- .../.classpath | 32 + .../.project | 23 + .../NOTICE | 13 + .../README.md | 3 + .../bnd.bnd | 7 + .../pom.xml | 38 + .../jython/JythonScriptEngineFactory.java | 85 ++ .../main/resources/Lib/community/__init__.py | 1 + .../__init__$py.class | Bin 0 -> 9581 bytes .../area_triggers_and_actions/__init__.py | 78 ++ .../area_actions$py.class | Bin 0 -> 11398 bytes .../area_triggers_and_actions/area_actions.py | 105 +++ .../Lib/community/autoremote/__init__.py | 45 ++ .../Lib/community/clickatell/__init__.py | 224 ++++++ .../Lib/community/clickatell/sendsms.py | 29 + .../Lib/community/idealarm/__init__.py | 566 +++++++++++++ .../resources/Lib/community/sonos/__init__.py | 0 .../Lib/community/sonos/playSound.py | 80 ++ .../resources/Lib/community/sonos/speak.py | 106 +++ .../resources/Lib/configuration.py.example | 30 + .../main/resources/Lib/core/__init__$py.class | Bin 0 -> 4043 bytes .../src/main/resources/Lib/core/__init__.py | 28 + .../main/resources/Lib/core/actions$py.class | Bin 0 -> 6008 bytes .../src/main/resources/Lib/core/actions.py | 49 ++ .../src/main/resources/Lib/core/date$py.class | Bin 0 -> 22553 bytes .../src/main/resources/Lib/core/date.py | 376 +++++++++ .../main/resources/Lib/core/items$py.class | Bin 0 -> 9079 bytes .../src/main/resources/Lib/core/items.py | 102 +++ .../main/resources/Lib/core/jsr223$py.class | Bin 0 -> 8814 bytes .../src/main/resources/Lib/core/jsr223.py | 74 ++ .../main/resources/Lib/core/links$py.class | Bin 0 -> 9101 bytes .../src/main/resources/Lib/core/links.py | 110 +++ .../src/main/resources/Lib/core/log$py.class | Bin 0 -> 8597 bytes .../src/main/resources/Lib/core/log.py | 69 ++ .../main/resources/Lib/core/metadata$py.class | Bin 0 -> 18060 bytes .../src/main/resources/Lib/core/metadata.py | 285 +++++++ .../resources/Lib/core/osgi/__init__$py.class | Bin 0 -> 8640 bytes .../main/resources/Lib/core/osgi/__init__.py | 94 +++ .../resources/Lib/core/osgi/events$py.class | Bin 0 -> 14085 bytes .../main/resources/Lib/core/osgi/events.py | 137 ++++ .../main/resources/Lib/core/rules$py.class | Bin 0 -> 12326 bytes .../src/main/resources/Lib/core/rules.py | 146 ++++ .../main/resources/Lib/core/testing$py.class | Bin 0 -> 9741 bytes .../src/main/resources/Lib/core/testing.py | 81 ++ .../main/resources/Lib/core/triggers$py.class | Bin 0 -> 50777 bytes .../src/main/resources/Lib/core/triggers.py | 757 ++++++++++++++++++ .../main/resources/Lib/core/utils$py.class | Bin 0 -> 19012 bytes .../src/main/resources/Lib/core/utils.py | 279 +++++++ .../module/script/ScriptEngineFactory.java | 3 +- bundles/pom.xml | 1 + .../openhab-core/src/main/feature/feature.xml | 7 + 51 files changed, 4061 insertions(+), 2 deletions(-) create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/.classpath create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/.project create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/NOTICE create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/README.md create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/bnd.bnd create mode 100644 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/pom.xml create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/java/org/openhab/core/automation/module/script/scriptenginefactory/jython/JythonScriptEngineFactory.java create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/__init__.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/area_triggers_and_actions/__init__$py.class create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/area_triggers_and_actions/__init__.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/area_triggers_and_actions/area_actions$py.class create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/area_triggers_and_actions/area_actions.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/autoremote/__init__.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/clickatell/__init__.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/clickatell/sendsms.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/idealarm/__init__.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/sonos/__init__.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/sonos/playSound.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/sonos/speak.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/configuration.py.example create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/__init__$py.class create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/__init__.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/actions$py.class create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/actions.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/date$py.class create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/date.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/items$py.class create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/items.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/jsr223$py.class create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/jsr223.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/links$py.class create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/links.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/log$py.class create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/log.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/metadata$py.class create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/metadata.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/osgi/__init__$py.class create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/osgi/__init__.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/osgi/events$py.class create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/osgi/events.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/rules$py.class create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/rules.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/testing$py.class create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/testing.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/triggers$py.class create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/triggers.py create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/utils$py.class create mode 100755 bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/utils.py diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/.classpath b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/.classpath new file mode 100755 index 00000000000..935534b41f1 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/.project b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/.project new file mode 100755 index 00000000000..715a5c02552 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/.project @@ -0,0 +1,23 @@ + + + org.openhab.core.automation.module.script.scriptenginefactory.jython + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/NOTICE b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/NOTICE new file mode 100755 index 00000000000..4c20ef446c1 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/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/openhab2-addons diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/README.md b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/README.md new file mode 100755 index 00000000000..76b455a65ca --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/README.md @@ -0,0 +1,3 @@ +# Jython ScriptEngineFactory + +This addon provides a ScriptEngineFactory for Jython. diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/bnd.bnd b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/bnd.bnd new file mode 100755 index 00000000000..2334dc4f1c9 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/bnd.bnd @@ -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 diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/pom.xml b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/pom.xml new file mode 100644 index 00000000000..c8aaac53e87 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/pom.xml @@ -0,0 +1,38 @@ + + + + 4.0.0 + + + org.openhab.core.bundles + org.openhab.core.reactor.bundles + 2.5.0-SNAPSHOT + + + org.openhab.core.automation.module.script.scriptenginefactory.jython + + openHAB Core :: Bundles :: Jython ScriptEngineFactory + + + + org.openhab.core.bundles + org.openhab.core.automation + ${project.version} + provided + + + org.openhab.core.bundles + org.openhab.core.automation.module.script + ${project.version} + provided + + + + org.python + jython-standalone + 2.7.0 + provided + + + + diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/java/org/openhab/core/automation/module/script/scriptenginefactory/jython/JythonScriptEngineFactory.java b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/java/org/openhab/core/automation/module/script/scriptenginefactory/jython/JythonScriptEngineFactory.java new file mode 100755 index 00000000000..93ee148d0ca --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/java/org/openhab/core/automation/module/script/scriptenginefactory/jython/JythonScriptEngineFactory.java @@ -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 getScriptTypes() { + List scriptTypes = new ArrayList<>(); + + for (javax.script.ScriptEngineFactory factory : ENGINE_MANAGER.getEngineFactories()) { + List 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; + } +} diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/__init__.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/__init__.py new file mode 100755 index 00000000000..8b137891791 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/__init__.py @@ -0,0 +1 @@ + diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/area_triggers_and_actions/__init__$py.class b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/area_triggers_and_actions/__init__$py.class new file mode 100755 index 0000000000000000000000000000000000000000..5e2964ebe6b604b00e85dcb8c5bdcb7cb5db606c GIT binary patch literal 9581 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 ThQtYx;2+1txR~G{MRD@00FXpk literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/area_triggers_and_actions/area_actions.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/area_triggers_and_actions/area_actions.py new file mode 100755 index 00000000000..9854efa3c52 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/area_triggers_and_actions/area_actions.py @@ -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) diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/autoremote/__init__.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/autoremote/__init__.py new file mode 100755 index 00000000000..e37c8de87f3 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/autoremote/__init__.py @@ -0,0 +1,45 @@ +""" +Purpose +======= + +With AutoRemote you have full control of your phone, from wherever you are by sending push notifications to your phone and reacting to them in Tasker or AutoRemote standalone apps. +This helper library aims to facilitate sending such AutoRemote notifications from custom openHAB scripts. + + +Requires +======== + +* - Setup an AutoRemote profile in Tasker to react to the message + +Further reading is available at the author's web site (https://joaoapps.com/autoremote/). + + +Known Issues +============ + +None + + +Change Log +========== + +* 09/19/19: Added this description. +""" +import os +from configuration import autoremote_configuration + +def sendMessage(message, ttl=300, sender='openHAB'): + ''' + Sends an autoremote message + ''' + + # Use GCM Server for delivery + cmd = 'curl -s -G "https://autoremotejoaomgcd.appspot.com/sendmessage" ' \ + + '--data-urlencode "key='+autoremote_configuration['key']+'" ' \ + + '--data-urlencode "password='+autoremote_configuration['password']+'" ' \ + + '--data-urlencode "message='+message+'" ' \ + + '--data-urlencode "sender='+sender+'" ' \ + + '--data-urlencode "ttl='+str(ttl)+'" ' \ + + ' 1>/dev/null 2>&1 &' + + os.system(cmd) diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/clickatell/__init__.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/clickatell/__init__.py new file mode 100755 index 00000000000..7af1f838ef3 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/clickatell/__init__.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +""" +This module can be used to send SMS messages via the Clickatell HTTP/S API at https://api.clickatell.com/. + +This file was originally published at https://github.com/jacques/pyclickatell. + +2018-07-07: B. Synnerlig added smsEncode() function + +License +------- + +Copyright (c) 2006-2012 Jacques Marneweck. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +""" + +import urllib, urllib2 + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +__author__ = "Jacques Marneweck , Arne Brodowski " +__version__ = "0.1.1-alpha" +__copyright__ = "Copyright (c) 2006 Jacques Marneweck, 2008 Arne Brodowski. All rights reserved." +__license__ = "The MIT License" + +def smsEncode(my_str): + # Convert to GSM 03.38 character set and URL-encode + utf8Chars=['%','\n',' ','"','&',',','.',u'/',':',';','<','=','>','?',u'¡',u'£','#',u'¥',u'§',u'Ä',u'Å',u'à',u'ä',u'å',u'Æ',u'Ç',u'É',u'è',u'é',u'ì',u'Ñ',u'ñ',u'ò',u'ö',u'Ø',u'Ö',u'Ü',u'ù',u'ü',u'ß',u'\\',u'*',u'\'','(',u')',u'@',u'+',u'$',u'[',u']',u'^',u'{',u'|',u'}',u'~'] + gsmChars=['%25','%0D','%20','%22','%26','%2C','%2E','%2F','%3A','%3B','%3C','%3D','%3E','%3F','%A1','%A3','%A4','%A5','%A7','%C4','%C5','%E0','%E4','%E5','%C6','%C7','%C9','%E8','%E9','%EC','%D1','%F1','%F2','%F6','%D8','%D6','%DC','%F9','%FC','%DF','%5C','%2A','%27','%28','%29','%40','%2B','%24','%5B','%5D','%5E','%7B','%7C','%7D','%7E'] + + for i in range(0,len(gsmChars)): + my_str = my_str.replace(utf8Chars[i],gsmChars[i]) + return my_str + +def require_auth(func): + """ + decorator to ensure that the Clickatell object is authed before proceeding + """ + def inner(self, *args, **kwargs): + if not self.has_authed: + self.auth() + return func(self, *args, **kwargs) + return inner + +class ClickatellError(Exception): + """ + Base class for Clickatell errors + """ + +class ClickatellAuthenticationError(ClickatellError): + pass + +class Clickatell(object): + """ + Provides a wrapper around the Clickatell HTTP/S API interface + """ + + def __init__ (self, username, password, api_id, sender): + """ + Initialise the Clickatell class + Expects: + - username - your Clickatell Central username + - password - your Clickatell Central password + - api_id - your Clickatell Central HTTP API identifier + """ + self.has_authed = False + + self.username = username + self.password = password + self.api_id = api_id + self.sender = sender + + self.session_id = None + + + def auth(self, url='https://api.clickatell.com/http/auth'): + """ + Authenticate against the Clickatell API server + """ + post = [ + ('user', self.username), + ('password', self.password), + ('api_id', self.api_id), + ] + + result = self.curl(url, post) + + if result[0] == 'OK': + assert (32 == len(result[1])) + self.session_id = result[1] + self.has_authed = True + return True + else: + raise ClickatellAuthenticationError(': '.join(result)) + + @require_auth + def getbalance(self, url='https://api.clickatell.com/http/getbalance'): + """ + Get the number of credits remaining at Clickatell + """ + post = [ + ('session_id', self.session_id), + ] + + result = self.curl(url, post) + if result[0] == 'Credit': + assert (0 <= result[1]) + return result[1] + else: + return False + + @require_auth + def getmsgcharge(self, apimsgid, url='https://api.clickatell.com/http/getmsgcharge'): + """ + Get the message charge for a previous sent message + """ + assert (32 == len(apimsgid)) + post = [ + ('session_id', self.session_id), + ('apimsgid', apimsgid), + ] + + result = self.curl(url, post) + result = ' '.join(result).split(' ') + + if result[0] == 'apiMsgId': + assert (apimsgid == result[1]) + assert (0 <= result[3]) + return result[3] + else: + return False + + @require_auth + def ping(self, url='https://api.clickatell.com/http/ping'): + """ + Ping the Clickatell API interface to keep the session open + """ + post = [ + ('session_id', self.session_id), + ] + + result = self.curl(url, post) + + if result[0] == 'OK': + return True + else: + self.has_authed = False + return False + + @require_auth + def sendmsg(self, message, url = 'https://api.clickatell.com/http/sendmsg'): + """ + Send a mesage via the Clickatell API server + Takes a message in the following format: + + message = { + 'to': 'to_msisdn', + 'text': 'This is a test message', + } + Return a tuple. The first entry is a boolean indicating if the message + was send successfully, the second entry is an optional message-id. + Example usage:: + result, uid = clickatell.sendmsg(message) + if result == True: + print "Message was sent successfully" + print "Clickatell returned %s" % uid + else: + print "Message was not sent" + """ + if not (message.has_key('to') or message.has_key('text')): + raise ClickatellError("A message must have a 'to' and a 'text' value") + + message['text'] = smsEncode(message['text']) + + post = [ + ('session_id', self.session_id), + ('from', self.sender), + ('to', message['to']), + ] + postStr = urllib.urlencode(post) + postStr += '&text='+ message['text'] + + result = self.curl(url, postStr, False) + + if result[0] == 'ID': + assert (result[1]) + return (True, result[1]) + else: + return (False, None) + + @require_auth + def tokenpay(self, voucher, url='https://api.clickatell.com/http/token_pay'): + """ + Redeem a voucher via the Clickatell API interface + """ + assert (16 == len(voucher)) + post = [ + ('session_id', self.session_id), + ('token', voucher), + ] + + result = self.curl(url, post) + + if result[0] == 'OK': + return True + else: + return False + + def curl(self, url, post, urlEncode=True): + """ + Inteface for sending web requests to the Clickatell API Server + """ + try: + if urlEncode: + data = urllib2.urlopen(url, urllib.urlencode(post)) + else: + data = urllib2.urlopen(url, post) + except urllib2.URLError(v): + raise ClickatellError(v) + + return data.read().split(": ") diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/clickatell/sendsms.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/clickatell/sendsms.py new file mode 100755 index 00000000000..4602e770eef --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/clickatell/sendsms.py @@ -0,0 +1,29 @@ +from core.log import logging, LOG_PREFIX +from community.clickatell import Clickatell +from configuration import clickatell_configuration + +def sms(message, subscriber='Default'): + ''' + Sends an SMS message through ClickaTell gateway. + Example: sms("Hello") + Example: sms("Hello", 'Amanda') + @param param1: SMS Text + @param param2: Subscriber. A numeric phone number or a phonebook name entry (String) + ''' + log = logging.getLogger(LOG_PREFIX + ".community.clickatell.sendsms") + phoneNumber = clickatell_configuration['phonebook'].get(subscriber, None) + if phoneNumber is None: + if subscriber.isdigit(): + phoneNumber = subscriber + else: + log.warn("Subscriber [{}] wasn't found in the phone book".format(subscriber)) + return + gateway = Clickatell(clickatell_configuration['user'], clickatell_configuration['password'], clickatell_configuration['apiid'], clickatell_configuration['sender']) + message = {'to': phoneNumber, 'text': message} + log.info("Sending SMS to: [{}]".format(phoneNumber)) + retval, msg = gateway.sendmsg(message) + if retval == True: + log.info("SMS sent: [{}]".format(msg)) + else: + log.warn("Error while sending SMS: [{}]".format(retval)) + return diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/idealarm/__init__.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/idealarm/__init__.py new file mode 100755 index 00000000000..06eb54c034e --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/idealarm/__init__.py @@ -0,0 +1,566 @@ +""" +:Author: `besynnerlig `_ +:Version: **4.0.0** + +Multi Zone Home Alarm package for openHAB. This software is distributed as a +community submission to the `openhab-helper-libraries `_. + + +About +----- + +The name ideAlarm comes from merging the two words ideal and alarm. Your home +is your castle. Keeping it safe and secure is a top priority of many +homeowners. With ideAlarm, you can easily set up your own DIY Home Security +System using the sensors that you already have in openHAB. + + +Release Notices +--------------- + +Below are important instructions if you are **upgrading** weatherStationUploader from a previous version. +If you are creating a new installation, you can ignore what follows. + +**PLEASE MAKE SURE THAT YOU GO THROUGH ALL STEPS BELOW WHERE IT SAYS "BREAKING CHANGE"... DON'T SKIP ANY VERSION** + + **Version 4.0.0** + **BREAKING CHANGE**: The script is now distributed as a part of + `openhab-helper-libraries `_. + If lucid had been previously installed, it should be completely removed. + + **Version 3.0.0** + **BREAKING CHANGE** ideAlarm requires at least `lucid V 1.0.0 `_. + + **BREAKING CHANGE** if you are using a `custom helper functions script for various alarm events `_ you should revise it so that it's working with the new version of lucid. + Please have a look at the `lucid release notices `_ and optionally look at the `example event helpers script `_. + + **Version 2.0.0** + **BREAKING CHANGE** ideAlarm new dependency: `lucid, an openHAB 2.x jsr223 Jython helper library `_. + Review that you've setup the item groups correctly as `described in wiki `_. + Removed dependency of `openhab2-jython `_. (All openhab2-jython functionality that's needed is now found in `lucid `_) + Removed dependency of `mylib `_ (All mylib functionality that's needed is now found in `lucid `_) + + **Version 1.0.0** + Added version info string to logging. + Added ideAlarm function `__version__()` + + **Version 0.9.0** + Initial version. + + +.. admonition:: **Disclaimer** + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED + WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +import weakref # Using this to prevent problems with garbage collection + +from org.joda.time import DateTime + +from core.jsr223 import scope +from core.date import format_date +from core.log import logging, LOG_PREFIX +from core.utils import getItemValue, post_update_if_different, send_command_if_different, kw +from core.actions import PersistenceExtensions +from configuration import idealarm_configuration, customDateTimeFormats, customGroupNames +from personal.idealarm import custom + +log = logging.getLogger('{}.community.ideAlarm'.format(LOG_PREFIX)) +ZONESTATUS = {'NORMAL': 0, 'ALERT': 1, 'ERROR': 2, 'TRIPPED': 3, 'ARMING': 4} +ARMINGMODE = {'DISARMED': 0, 'ARMED_HOME': 1, 'ARMED_AWAY': 2} + +def isActive(item): + ''' + Tries to determine if a device is active (tripped) from the perspective of an alarm system. + A door lock is special in the way that when it's locked its contacts are OPEN, hence + the value needs to be inverted for the alarm system to determine if it's 'active' + ''' + active = False + if item.state in [scope.ON, scope.OPEN]: + active = True + active = not active if customGroupNames['lockDevice'] in item.groupNames else active + return active + +class IdeAlarmError(Exception): + ''' + Base class for IdeAlarm errors + ''' + +class IdeAlarmSensor(object): + ''' + Alarm Sensor Object + ''' + def __init__(self, parent, cfg): + ''' + Initialise the IdeAlarmSensor class + + Expects: + - Parent object + - cfg (dictionary) The sensor's configuration dictionary + ''' + self.name = cfg['name'] + _label = scope.itemRegistry.getItem(self.name).label + self.label = _label if _label is not None else 'Sensor has no label' + self.parent = weakref.ref(parent) # <= garbage-collector safe! + self.sensorClass = cfg['sensorClass'] + self.nag = cfg['nag'] + self.nagTimeoutMins = cfg['nagTimeoutMins'] + self.armWarn = cfg['armWarn'] + self.enabled = cfg['enabled'] + self.log = logging.getLogger(u"{}.IdeAlarmSensor.{}".format(LOG_PREFIX, self.name.decode('utf8'))) + #self.log.info(u"ideAlarm sensor {} initialized...".format(self.name.decode('utf8'))) + + def isEnabled(self): + ''' + The sensor can be enabled/disabled in the configuration file by a boolean or a function. + ''' + if callable(self.enabled): + return self.enabled(scope.events, self.log) + else: + return self.enabled + + def isActive(self): + ''' + The sensor is considered active when its OPEN, ON or NULL. Locks are different. + ''' + return isActive(scope.itemRegistry.getItem(self.name)) + + def getLastUpdate(self): + ''' + Returns the sensors last update time (if available). + type is 'org.joda.time.DateTime', http://joda-time.sourceforge.net/apidocs/org/joda/time/DateTime.html + ''' + try: + lastUpdate = PersistenceExtensions.lastUpdate(scope.itemRegistry.getItem(self.name)).toDateTime() + except: + lastUpdate = DateTime(0) + self.log.info(u"Could not retrieve persistence data for sensor: {}".format(self.name.decode('utf8'))) + return lastUpdate + +class IdeAlarmZone(object): + ''' + Alarm Zone Object + ''' + def __init__(self, parent, zoneNumber, cfg): + ''' + Initialise the IdeAlarmZone class + + Expects: + - Parent object + - zoneNumber (integer) The zone's ordinal number + - cfg (dictionary) The zone's configuration dictionary + ''' + self._armingMode = None + self._zoneStatus = None + self.zoneNumber = zoneNumber + self.alertDevices = cfg['alertDevices'] + self.name = cfg['name'] + self.armAwayToggleSwitch = cfg['armAwayToggleSwitch'] + self.armHomeToggleSwitch = cfg['armHomeToggleSwitch'] + self.mainZone = cfg['mainZone'] + self.canArmWithTrippedSensors = cfg['canArmWithTrippedSensors'] + self.alarmTestMode = parent.alarmTestMode + self.parent = weakref.ref(parent) # <= garbage-collector safe! + self.log = logging.getLogger(u"{}.IdeAlarmZone.Zone.{}".format(LOG_PREFIX, self.zoneNumber)) + self.sensors = [] + for sensor in cfg['sensors']: + self.sensors.append(IdeAlarmSensor(self, sensor)) + self.armingModeItem = cfg['armingModeItem'] + self.statusItem = cfg['statusItem'] + + self.openSections = self.countOpenSections() + self.setArmingMode(getItemValue(self.armingModeItem, ARMINGMODE['DISARMED'])) # Will also set the zone status to normal + self.log.info(u"ideAlarm Zone {} initialized with {} open sensors".format(self.name.decode('utf8'), self.openSections)) + + def getArmingMode(self): + ''' + Returns the zones current arming mode + ''' + return self._armingMode + + def setArmingMode(self, newArmingMode, sendCommand=False): + ''' + Sets the zones current arming mode + ''' + oldArmingMode = self._armingMode + + if newArmingMode not in [ARMINGMODE['DISARMED'], ARMINGMODE['ARMED_HOME'], ARMINGMODE['ARMED_AWAY']]: + raise IdeAlarmError("Trying to set an invalid arming mode: {}".format(newArmingMode)) + + # There might be open sensors when trying to arm. If so, the custom function onArmingWithOpenSensors + # gets called. (That doesn't necessarily need to be an error condition). + # However if the zone has been configured not to allow opened sensors during arming, + # the zone status will be set to ERROR and onZoneStatusChange will be able to trap track it down. + if newArmingMode in [ARMINGMODE['ARMED_AWAY'], ARMINGMODE['ARMED_HOME']] \ + and self.getZoneStatus() != ZONESTATUS['ARMING'] and self.getZoneStatus() is not None \ + and self.openSections > 0: + if 'onArmingWithOpenSensors' in dir(custom): + custom.onArmingWithOpenSensors(self, newArmingMode) + if not self.canArmWithTrippedSensors: + self.setZoneStatus(ZONESTATUS['ERROR'], errorMessage='Arming is not allowed with open sensors') + self.log.warn(u"Zone \'{}'\' can not be set to new arming mode: {} due to that there are open sensors!".format(self.name.decode('utf8'), kw(ARMINGMODE, newArmingMode))) + import time + time.sleep(1) + self.setZoneStatus(ZONESTATUS['NORMAL']) + return + + # Don't set arming mode to 'ARMED_AWAY' immediately, we need to wait for the exit timer + # self.getZoneStatus() returns None when initializing + if newArmingMode == ARMINGMODE['ARMED_AWAY'] \ + and self.getZoneStatus() is not None and self.getZoneStatus() != ZONESTATUS['ARMING']: + self.setZoneStatus(ZONESTATUS['ARMING']) + post_update_if_different("Z{}_Exit_Timer".format(self.zoneNumber), scope.ON) + return + self._armingMode = newArmingMode + + # Sync the Item + post_update_if_different(self.armingModeItem, newArmingMode, sendCommand) + + # Call custom function if available + if 'onArmingModeChange' in dir(custom): + custom.onArmingModeChange(self, newArmingMode, oldArmingMode) + + # Whenever the arming mode is set, reset the zones status to NORMAL + self.setZoneStatus(ZONESTATUS['NORMAL']) + + def getZoneStatus(self): + ''' + Returns the zones current status + ''' + return self._zoneStatus + + def setZoneStatus(self, newZoneStatus, sendCommand=False, errorMessage=None): + ''' + Sets the zones current status + ''' + if newZoneStatus not in [ZONESTATUS['NORMAL'], ZONESTATUS['ALERT'], ZONESTATUS['ERROR'], ZONESTATUS['TRIPPED'], ZONESTATUS['ARMING']]: + raise IdeAlarmError('Trying to set an invalid zone status') + oldZoneStatus = self._zoneStatus + self._zoneStatus = newZoneStatus + + if newZoneStatus in [ZONESTATUS['NORMAL']]: + + # Cancel all timers so they won't fire + post_update_if_different("Z{}_Entry_Timer".format(self.zoneNumber), scope.OFF) + post_update_if_different("Z{}_Exit_Timer".format(self.zoneNumber), scope.OFF) + post_update_if_different("Z{}_Alert_Max_Timer".format(self.zoneNumber), scope.OFF) + + # Cancel sirens + for alertDevice in self.alertDevices: + send_command_if_different(alertDevice, scope.OFF) + + # Sync the Zone Status Item + post_update_if_different(self.statusItem, newZoneStatus, sendCommand) + + # Call custom function if available + if 'onZoneStatusChange' in dir(custom): + custom.onZoneStatusChange(self, newZoneStatus, oldZoneStatus, errorMessage=errorMessage) + + def getOpenSensors(self, mins=0, armingMode=None, isArming=False): + ''' + Gets all open sensor objects for the zone + * mins Integer 0-9999 Number of minutes that the sensor must have been updated within. A value of 0 will return sensor devices who are currently open. + * armingMode A sensor is regarded to be open only in the context of an arming mode. Defaults to the zones current arming mode. + * isArming Boolean. In an arming scenario we don't want to include sensors that are set not to warn when arming. + + returns a list with open sensor objects. + ''' + armingMode = self.getArmingMode() if armingMode is None else armingMode + openSensors = [] + if armingMode == ARMINGMODE['DISARMED']: + return openSensors + for sensor in self.sensors: + if (not sensor.isEnabled()) \ + or (mins == 0 and not sensor.isActive()) \ + or (isArming and not sensor.armWarn) \ + or (mins > 0 and sensor.getLastUpdate().isBefore(DateTime.now().minusMinutes(mins))): + continue + if armingMode == ARMINGMODE['ARMED_AWAY'] \ + or (armingMode == ARMINGMODE['ARMED_HOME'] and sensor.sensorClass != 'B'): + openSensors.append(sensor) + return openSensors + + def isArmed(self): + ''' + Returns true if armed, otherwise false + ''' + return self.getArmingMode() != ARMINGMODE['DISARMED'] + + def isDisArmed(self): + '''Returns true if disarmed, otherwise false''' + return not self.isArmed() + + def onToggleSwitch(self, itemName): + ''' + Called whenever an alarm arming mode toggle switch has been switched. + ''' + newArmingMode = None + if itemName == self.armAwayToggleSwitch: + if self.getArmingMode() in [ARMINGMODE['DISARMED']]: + newArmingMode = ARMINGMODE['ARMED_AWAY'] + else: + newArmingMode = ARMINGMODE['DISARMED'] + else: + if self.getArmingMode() in [ARMINGMODE['DISARMED']]: + newArmingMode = ARMINGMODE['ARMED_HOME'] + else: + newArmingMode = ARMINGMODE['DISARMED'] + + self.log.debug(u"Toggling zone [{}] to new arming mode: [{}]".format(self.name.decode('utf8'), kw(ARMINGMODE, newArmingMode))) + self.setArmingMode(newArmingMode) + + def onEntryTimer(self): + ''' + Called whenever the entry timer times out. + ''' + # Double check that the zone status is tripped, we can probably remove this check later + if self.getZoneStatus() not in [ZONESTATUS['TRIPPED']]: + raise IdeAlarmError('Entry Timer timed out but zone status is not tripped') + self.setZoneStatus(ZONESTATUS['ALERT']) + + # We need to make some noise here! + if not self.alarmTestMode: + for alertDevice in self.alertDevices: + send_command_if_different(alertDevice, scope.ON) + self.log.info('You should be able to hear the sirens now...') + else: + self.log.info('ALARM_TEST_MODE is activated. No sirens!') + post_update_if_different("Z{}_Alert_Max_Timer".format(self.zoneNumber), scope.ON) + + def onExitTimer(self): + ''' + Exit timer is used when ARMING AWAY only. When the exit timer times out, + set the zones arming mode + ''' + self.setArmingMode(ARMINGMODE['ARMED_AWAY']) + + def onAlertMaxTimer(self): + ''' + Called after the sirens (or whatever alert devices you use) have reached their time limit + ''' + # Cancel alert devices, e.g. the sirens + for alertDevice in self.alertDevices: + send_command_if_different(alertDevice, scope.OFF) + self.log.debug('Alert devices have been switched off due to they\'ve reached their time limit') + + def getNagSensors(self, timerTimedOut=False): + ''' + Check if nagging is required. Performed when a sensor changes its state and when the nag timer ends. + Nagging is only performed when a zone is disarmed. + ''' + nagSensors = [] + for sensor in self.sensors: + if sensor.isEnabled() and sensor.isActive() and sensor.nag and self.getArmingMode() == ARMINGMODE['DISARMED']: + nagSensors.append(sensor) + if len(nagSensors) == 0: + post_update_if_different("Z{}_Nag_Timer".format(self.zoneNumber), scope.OFF) # Cancel the nag timer + else: + post_update_if_different("Z{}_Nag_Timer".format(self.zoneNumber), scope.ON) + if timerTimedOut and 'onNagTimer' in dir(custom): + self.log.debug('Calling custom onNagTimer function') + custom.onNagTimer(self, nagSensors) + return nagSensors + + def countOpenSections(self): + ''' + A sensor has changed its state. We are here to calculate how many open + sensors there are in the zone at this very moment. Saves the result in + self.openSections and returns it. WE DO NOT INCLUDE MOTION DETECTORS + IN THE COUNT UNLESS ARMED AWAY! E.G. Those sensors that belongs to + group 'G_Motion' + ''' + self.openSections = 0 + for sensor in self.sensors: + #self.log.debug(u"Checking sensor: {} : {}".format(sensor.name.decode('utf8'), sensor.isEnabled() and sensor.isActive())) + if sensor.isEnabled() and sensor.isActive() \ + and ('G_Motion' not in scope.itemRegistry.getItem(sensor.name).groupNames or self.getArmingMode() in [ARMINGMODE['ARMED_AWAY']]): + self.openSections += 1 + self.log.debug(u"Open sensor: {}".format(sensor.name.decode('utf8'))) + self.log.debug(u"Number of open sections in {} is: {}".format(self.name.decode('utf8'), self.openSections)) + post_update_if_different("Z{}_Open_Sections".format(self.zoneNumber), self.openSections) + return self.openSections + + def onSensorChange(self, sensor): + ''' + Called whenever an enabled sensor has tripped ON or OPEN + ''' + if self.getArmingMode() not in [ARMINGMODE['ARMED_HOME'], ARMINGMODE['ARMED_AWAY']] \ + or self.getZoneStatus() not in [ZONESTATUS['NORMAL']] \ + or (self.getArmingMode() == ARMINGMODE['ARMED_HOME'] and sensor.sensorClass == 'B') \ + or getItemValue("Z{}_Exit_Timer".format(self.zoneNumber), scope.OFF) == scope.ON: + self.log.info(u"{} was tripped, but we are ignoring it".format(sensor.name.decode('utf8'))) + return + + self.setZoneStatus(ZONESTATUS['TRIPPED']) + self.log.info(u"{} was tripped, starting entry timer".format(sensor.name.decode('utf8'))) + post_update_if_different("Z{}_Entry_Timer".format(self.zoneNumber), scope.ON) + +class IdeAlarm(object): + ''' + Provides ideAlarm Home Alarm System functions to openHAB + ''' + + def __init__(self): + ''' + Initialise the IdeAlarm class + + Expects: + + * Nothing really... + ''' + self.__version__ = '4.0.0' + self.__version_info__ = tuple([ int(num) for num in self.__version__.split('.')]) + self.log = logging.getLogger("{}.IdeAlarm V{}".format(LOG_PREFIX, self.__version__)) + self.alarmTestMode = idealarm_configuration['ALARM_TEST_MODE'] + self.loggingLevel = idealarm_configuration['LOGGING_LEVEL'] or 'INFO' + self.log.setLevel(self.loggingLevel) + self.nagIntervalMinutes = idealarm_configuration['NAG_INTERVAL_MINUTES'] + self.timeCreated = DateTime.now() + + self.alarmZones = [] + for i in range(len(idealarm_configuration['ALARM_ZONES'])): + zoneNumber = i+1 + self.alarmZones.append(IdeAlarmZone(self, zoneNumber, idealarm_configuration['ALARM_ZONES'][i])) + + for alarmZone in self.alarmZones: + alarmZone.getNagSensors() + + self.log.info("ideAlarm object initialized with {} zones at {}".format(len(self.alarmZones), format_date(self.timeCreated, customDateTimeFormats['dateTime']))) + + def getZoneIndex(self, zoneName): + for i in range(len(self.alarmZones)): + if self.alarmZones[i].name == zoneName: + return i + self.log.debug(zoneIndex) + self.log.warn(u"There is no alarm zone named: [{}]".format(zoneName.decode('utf8'))) + + def __version__(self): + return self.__version__ + + def logVersion(self): + self.log.info("ideAlarm Version is {}".format(self.__version__)) + + def isArmed(self, zone='1'): + ''' + zone can be the ordinal number of the alarm zone or the zone name + ''' + zoneIndex = None + if (str(zone).isdigit()): + zoneIndex = int(zone) - 1 + else: + zoneIndex = self.getZoneIndex(zone) + return self.alarmZones[zoneIndex].isArmed() + + def isDisArmed(self, zone='1'): + ''' + zone can be the ordinal number of the alarm zone or the zone name + ''' + zoneIndex = None + if (str(zone).isdigit()): + zoneIndex = int(zone) - 1 + else: + zoneIndex = self.getZoneIndex(zone) + return self.alarmZones[zoneIndex].isDisArmed() + + def getZoneStatus(self, zone='1'): + ''' + zone can be the ordinal number of the alarm zone or the zone name + ''' + zoneIndex = None + if (str(zone).isdigit()): + zoneIndex = int(zone) - 1 + else: + zoneIndex = self.getZoneIndex(zone) + return self.alarmZones[zoneIndex].getZoneStatus() + + def getSensors(self): + ''' + Returns a Python list of all sensors in all zones. + ''' + sensorList = [] + for i in range(len(self.alarmZones)): + alarmZone = self.alarmZones[i] # Get the alarm zone object + #self.log.info(u"Getting sensors for alarm zone {}".format(alarmZone.name.decode('utf8'))) + for sensor in alarmZone.sensors: + #self.log.info(u"Sensor: {}".format(sensor.name.decode('utf8'))) + sensorList.append(sensor.name) + return sensorList + + def getAlertingZonesCount(self): + ''' + Returns the total number of alerting alarm zones. + ''' + alertingZones = 0 + for i in range(len(self.alarmZones)): + alarmZone = self.alarmZones[i] # Get the alarm zone object + #self.log.info(u"Checking for alert status in zone {}".format(alarmZone.name.decode('utf8'))) + if self.getZoneStatus(i) == ZONESTATUS['ALERT']: alertingZones += 1 + return alertingZones + + def get_triggers(self): + ''' + Wraps the function with core.triggers.when for all triggers that shall trigger ideAlarm. + ''' + from core.triggers import when + def generated_triggers(function): + for item in self.getSensors(): + when("Item {} changed".format(item))(function) # TODO: Check if this works for items with accented characters in the name + for i in range(len(self.alarmZones)): + when("Item {} changed to ON".format(self.alarmZones[i].armAwayToggleSwitch))(function) + when("Item {} changed to ON".format(self.alarmZones[i].armHomeToggleSwitch))(function) + when("Item Z{}_Entry_Timer received command OFF".format(i + 1))(function) + when("Item Z{}_Exit_Timer received command OFF".format(i + 1))(function) + when("Item Z{}_Nag_Timer received command OFF".format(i + 1))(function) + when("Item Z{}_Alert_Max_Timer received command OFF".format(i + 1))(function) + return function + return generated_triggers + + def execute(self, event): + ''' + Main function called whenever an item has triggered + ''' + + # Why are we here? What caused this script to trigger? + # Is it a change of status, armingMode toggleSwitch or is it a sensor? + + for i in range(len(self.alarmZones)): + alarmZone = self.alarmZones[i] + if event.itemName in [alarmZone.armAwayToggleSwitch, alarmZone.armHomeToggleSwitch]: + alarmZone.onToggleSwitch(event.itemName) + break + elif event.itemName == "Z{}_Entry_Timer".format(i+1): + alarmZone.onEntryTimer() # TODO: Figure out if we only should handle event.isCommand and skip event.isUpdate (Cancelled timer) + break + elif event.itemName == "Z{}_Exit_Timer".format(i+1): + alarmZone.onExitTimer() + break + elif event.itemName == "Z{}_Nag_Timer".format(i+1): + alarmZone.getNagSensors(True) + break + elif event.itemName == "Z{}_Alert_Max_Timer".format(i+1): + result = alarmZone.onAlertMaxTimer() + break + else: + for sensor in alarmZone.sensors: + if event.itemName == sensor.name: + if sensor.isEnabled(): + # The sensor object carries its own status. + # However, this is an alarm system. At this point we are not so + # interested in the sensors current state because it might have changed + # since this event triggered. We need to act upon the triggered state. + if isActive(scope.itemRegistry.getItem(event.itemName)): + alarmZone.onSensorChange(sensor) # Only active states are of interest here + alarmZone.getNagSensors() + alarmZone.countOpenSections() # updates the zone's open sections property. + break + +ideAlarm = IdeAlarm() diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/sonos/__init__.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/sonos/__init__.py new file mode 100755 index 00000000000..e69de29bb2d diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/sonos/playSound.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/sonos/playSound.py new file mode 100755 index 00000000000..3a48399e523 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/sonos/playSound.py @@ -0,0 +1,80 @@ +""" +This module provides functions for playing sounds. +""" +from core.actions import Audio +from core.utils import getItemValue +from configuration import sonos, customItemNames +from core.jsr223 import scope +from core.log import logging, LOG_PREFIX + +PRIO = {'LOW': 0, 'MODERATE': 1, 'HIGH': 2, 'EMERGENCY': 3} +TIMEOFDAY = {'NIGHT': 0, 'MORNING': 1, 'DAY': 2, 'EVENING': 3} # Regardless of the sun + +def playsound(fileName, ttsPrio=PRIO['MODERATE'], **keywords): + """ + Play a sound mp3 file function. First argument is positional and mandatory. + Remaining arguments are optionally keyword arguments. + + Examples: + .. code-block:: + + playsound("Hello.mp3") + playsound("Hello.mp3", PRIO['HIGH'], room='Kitchen', volume=42) + + Args: + fileName (str): Sound file name to play (files need to be put in the + folder ``/conf/sounds/``) + ttsPrio (str): (optional) priority as defined by PRIO (defaults to + PRIO['MODERATE']) + **keywords: ``room`` (room to play in defaults to ``All``) and + ``ttsVol`` (volume) + + Returns: + bool: ``True``, if sound was sent, else ``False`` + """ + log = logging.getLogger(LOG_PREFIX + ".community.sonos.playsound") + + def getDefaultRoom(): + # Search for the default room to speak in + for the_key, the_value in sonos['rooms'].iteritems(): + if the_value['defaultttsdevice']: + return the_key + return 'All' + + if getItemValue(customItemNames['allowTTSSwitch'], scope.ON) != scope.ON and ttsPrio <= PRIO['MODERATE']: + log.info("[{}] is OFF and ttsPrio is too low to play the sound [{}] at this moment".format(customItemNames['allowTTSSwitch'], fileName)) + return False + + room = getDefaultRoom() if 'room' not in keywords else keywords['room'] + + rooms = [] + if room == 'All' or room is None: + for the_key, the_value in sonos['rooms'].iteritems(): + rooms.append(sonos['rooms'][the_key]) + log.debug(u"Room found: [{}]".format(sonos['rooms'][the_key]['name'].decode('utf8'))) + else: + sonosSpeaker = sonos['rooms'].get(room, None) + if sonosSpeaker is None: + log.warn(u"Room [{}] wasn't found in the sonos rooms dictionary".format(room.decode('utf8'))) + return + rooms.append(sonosSpeaker) + log.debug(u"Room found: [{}]".format(sonosSpeaker['name'].decode('utf8'))) + + for aRoom in rooms: + ttsVol = None if 'ttsVol' not in keywords else keywords['ttsVol'] + if not ttsVol or ttsVol >= 70: + if ttsPrio == PRIO['LOW']: + ttsVol = 30 + elif ttsPrio == PRIO['MODERATE']: + ttsVol = 40 + elif ttsPrio == PRIO['HIGH']: + ttsVol = 60 + elif ttsPrio == PRIO['EMERGENCY']: + ttsVol = 70 + else: + ttsVol = aRoom['ttsvolume'] + + Audio.playSound(aRoom['audiosink'], fileName) + log.info(u"playSound: Playing [{}] in room [{}] at volume [{}]".format(fileName.decode('utf8'), aRoom['name'].decode('utf8'), ttsVol)) + + return True diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/sonos/speak.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/sonos/speak.py new file mode 100755 index 00000000000..694c19c10fa --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/community/sonos/speak.py @@ -0,0 +1,106 @@ +""" +This module provides functions for use with TTS. +""" +from core.actions import Voice +from core.utils import getItemValue +from configuration import sonos, customItemNames +from core.jsr223 import scope +from core.log import logging, LOG_PREFIX + +PRIO = {'LOW': 0, 'MODERATE': 1, 'HIGH': 2, 'EMERGENCY': 3} +TIMEOFDAY = {'NIGHT': 0, 'MORNING': 1, 'DAY': 2, 'EVENING': 3} # Regardless of the sun + +def tts(ttsSay, ttsPrio=PRIO['MODERATE'], **keywords): + ''' + Text To Speak function. First argument is positional and mandatory. + Remaining arguments are optionally keyword arguments. + + Examples: + .. code-block:: + + tts("Hello") + tts("Hello", PRIO['HIGH'], ttsRoom='Kitchen', ttsVol=42, ttsLang='en-GB', ttsVoice='Brian') + + Args: + ttsSay (str): text to speak + ttsPrio (str): (optional) priority as defined by PRIO (defaults to + PRIO['MODERATE']) + **keywords: ``ttsRoom`` (room to speak in), ``ttsVol`` (volume), + ``ttsLang`` (language), ``ttsVoice`` (voice), ``ttsEngine`` + (engine) + + Returns: + bool: ``True``, if sound was sent, else ``False`` + ''' + log = logging.getLogger(LOG_PREFIX + ".community.sonos.speak") + + def getDefaultRoom(): + # Search for the default room to speak in + for the_key, the_value in sonos['rooms'].iteritems(): + if the_value['defaultttsdevice']: + return the_key + return 'All' + + if getItemValue(customItemNames['allowTTSSwitch'], scope.ON) != scope.ON and ttsPrio <= PRIO['MODERATE']: + log.info(u"[{}] is OFF and ttsPrio is too low to speak [{}] at this moment".format(customItemNames['allowTTSSwitch'].decode('utf8'), ttsSay)) + return False + + ttsRoom = getDefaultRoom() if 'ttsRoom' not in keywords else keywords['ttsRoom'] + + ttsRooms = [] + if ttsRoom == 'All' or ttsRoom is None: + for the_key, the_value in sonos['rooms'].iteritems(): + ttsRooms.append(sonos['rooms'][the_key]) + log.debug(u"TTS room found: [{}]".format(sonos['rooms'][the_key]['name'].decode('utf8'))) + else: + sonosSpeaker = sonos['rooms'].get(ttsRoom, None) + if sonosSpeaker is None: + log.warn(u"Room [{}] wasn't found in the sonos rooms dictionary".format(ttsRoom.decode('utf8'))) + return + ttsRooms.append(sonosSpeaker) + log.debug(u"TTS room found: [{}]".format(sonosSpeaker['name'].decode('utf8'))) + + for room in ttsRooms: + ttsVol = None if 'ttsVol' not in keywords else keywords['ttsVol'] + if not ttsVol or ttsVol >= 70: + if ttsPrio == PRIO['LOW']: + ttsVol = 30 + elif ttsPrio == PRIO['MODERATE']: + ttsVol = 40 + elif ttsPrio == PRIO['HIGH']: + ttsVol = 60 + elif ttsPrio == PRIO['EMERGENCY']: + ttsVol = 70 + else: + ttsVol = room['ttsvolume'] + + ttsLang = room['ttslang'] if 'ttsLang' not in keywords else keywords['ttsLang'] + ttsVoice = room['ttsvoice'] if 'ttsVoice' not in keywords else keywords['ttsVoice'] + ttsEngine = room['ttsengine'] if 'ttsEngine' not in keywords else keywords['ttsEngine'] + #Voice.say(ttsSay, "{}:{}".format(ttsEngine, ttsVoice), room['audiosink']) + Voice.say(ttsSay, "{}:{}".format(ttsEngine, ttsVoice), room['audiosink'], scope.PercentType(ttsVol)) # Volume is not well implemented + log.info(u"TTS: Speaking [{}] in room [{}] at volume [{}]".format(ttsSay, room['name'].decode('utf8'), ttsVol)) + + return True + +def greeting(): + """ + To use this, you should set up astro.py as described `here `_ + It will take care of updating the item ``V_TimeOfDay`` for you. You can + customize and/or translate these greetings in your configuration file. + """ + timeOfDay = getItemValue('V_TimeOfDay', TIMEOFDAY['DAY']) + try: + from configuration import timeofdayGreetings + except ImportError: + # No customized greetings found in configuration file. We use the following english greetings then + timeofdayGreetings = { + 0: 'Good night', + 1: 'Good morning', + 2: 'Good day', + 3: 'Good evening' + } + if timeOfDay in timeofdayGreetings: + return timeofdayGreetings[timeOfDay] + else: + return 'good day' diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/configuration.py.example b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/configuration.py.example new file mode 100755 index 00000000000..06e51639bcc --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/configuration.py.example @@ -0,0 +1,30 @@ +""" +Set the default values used when performing an area action on any Item that +does not have these values set in its metadata. + +* low_lux_trigger: the lux level at which lights should turn ON/OFF, +* brightness: the dimming level for setting DimmerItems (anything greater than + zero will turn on a SwitchItem) +* hue and saturation: for ColorItems +* lux_item_name: the name of the Item to use for current outside lux or solar + radiation levels + +An easy way to get solar radiation values is an Item linked to the +``astro:sun:local:radiation#diffuse`` Channel from the Astro binding. This +Channel provides a smoother curve than the direct and total Channels. The +defaults set for color provide a warm white. By setting a high value for +low_lux_trigger, the lights will always turn on, which works well for areas +without windows, like closets. + +Number:Intensity Sun_Radiation_Diffuse "Solar Radiation (diffuse) [%.0f W/m\u00B2]" {channel="astro:sun:local:radiation#diffuse"} +""" +area_triggers_and_actions_dict = { + #"lux_item_name": "Sun_Radiation_Diffuse", + "default_levels": { + "low_lux_trigger": 99999, + "hue": 30, + "saturation": 100, + "brightness": 98 + }, + "default_action_function": "light_action" +} diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/__init__$py.class b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/__init__$py.class new file mode 100755 index 0000000000000000000000000000000000000000..102a72f9cd42087f2664043c9eff1d37e93e6ed8 GIT binary patch literal 4043 zcmbtX`E%RG75-MF3<{wWnn@hVvJ<#*6H*k(T26e)b!)j+OIu=8Q&nm?Ed+u~3K9sg z04SC2eW&+*q*r=nlBSodw6vy~^vC|tf2q@^Zx^J;5+O|w%}`kEzWv^N-#d2k_kaE6 zuK=cSn;}*|@ivC*nK{>M6kDBO)pd%d>+#}3XVx>Ce0pGEan<8Str8dkpUw`fE_9x$ zu5mM%W>9p!=9;>mK#Z}M)WucH*P3pvZF4O{5VqaX3~fQG)LMqQZZvq727$TCeJxn! zT8-C@wjF4`>9)AGvSJ0i>94G)uGBE8`7VhWfmRapnrS#%m1~A+a^L4Q&G0p=E^s>9 zCiiF|2t2FW4hTHs88q*S-Hv9cHOmYv*D<_Kwm@JK$*>8X;MzSXqi=fM^)!z+EI;6$ zY$^MQxR2qHK|M0%6dYt2@p%wZ%TSEav3KHSmy&n@DHZqQ5G5hu z*p|Z?;+c|IdJqqZrH2^~2^tD(zBe^>svvVLfkzlpyZuox#xN}7kia;@gS+jf$SF9+ zpoSrwC4v~9i!l1;AXgE7gmqWS-i%o56;~3EK~wQ4jx!{LhpuVZNjx69eS#sq+s1{C zf+r|?SDLM$SE4?{_`W(a$d2GiOfVc7gqv+UvT77eQnc!N7%aj(vY*)n6i0}CYB>++ zf>FJ}sgVTUiIe0AjinT*3s_f>Ze z=T)4A=xKG`HvGV1V5KD9j*G&ZchJ1Zr4ub8`4ZU_VftmN_vs|{+{+`F!Mhj^T(BG~ zxX7@!k5&7GXD=NQ^L-a7`)BcP6?wde;ouJb6SzuF5LXWM{QtbF@22A%=0!ltl5)p& zZgJ1miSk$`yPU)V-YfXKPAX(T#F?iRXmU4=^!21D_qkoC;+^XyFXg9;mdMW!5%m|s zv-)BJRMd*YH$@6FyqGCPwXxkzb9%7Vm6n$yxa#*2Y36hkc7(c8WP9k=5Qv$fb3ENmqa;PD$(_sBtDK$h|%XF zdZmDy69=rP;0qK&lP*62uh3y(^BLC-{J=9>4Ce+DaG@hj8MIMM-`z@K`y#%i;(2_T zeDm5)=-bnScssX6x|PIN@ijr;*Xbmrez0Q;!|@2hc8E}qe0(q$ueTjXq~Tqg6?~f^ zOZc;v%|&1?yAJWtHUrnY&g(YaAn8VSSDaz$-o(F5J=l-9X6&>iiSOcjD!zm7Q@4|C zq~1HTL^ml#<>LQ!;}!WfB$D7qqR-3GPa@J`q0?tW!HW#qPCJ?{BE&0RS|}}X&nFIu z(BTckZu1yj+35rykCdNZwCJur3^3>FnK<3+AB`02vrVAaicKdd){MX?((T$=HLCsR zRQ}|dr_Rw=fKx9T?Z9msA{Is4s_yte&~3X5nA**)B{GQ&d;MY`4A~qBU`ff1S z=Z%&uce(}4(cP@vZdSS1n^7xn+cUYalwpjh3dpl1%eN?fE;)4XmDcz;PDHO0<3B@; zu9MWah@MeV@$yOMEIk!^r0MoDlBx$^7C-^l$5C-Yml!tS6{&ZV#A zg-|({8Ydar)WtJhR9?WcbdG#G-Nl8n#Lo+KCIJu76Bwia)a-fy%Q#37KzZU0oQc=q#bU(SJoymBVzDiH zXDoIog>8KJHQX8=mW#3UM=F!?^v5bBb}cn3<>FF~Nx3V3Bt)T=T4_&Dik?Dyd<(aP zMWC;TPvTR(RgG*As}H@7nH%X(-@#|Q`204$GO>+sgjgf^Ccf2^PD@$fKze2yKM2tn zUce7~>(3Jd@F75sso#|Yx*WWg^=_$7WN23PSCUKRr%ulyUEDucoR literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/__init__.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/__init__.py new file mode 100755 index 00000000000..776b9d1fed7 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/__init__.py @@ -0,0 +1,28 @@ +## pylint: disable=too-many-function-args +""" +This module (really a Python package) patches the default scope ``items`` +object, so that Items can be accessed as if they were attributes (rather than a +dictionary). It can also be used as a module for registering global variables +that will outlive script reloads. + +.. code-block:: + + import core + + print items.TestString1 + +.. note:: + This patch will be applied when any module in the `core` package is loaded. +""" + +from core.jsr223.scope import items + +# +# Add an attribute-resolver to the items map +# + +def _item_getattr(self, name): + return self[name] + +if items:# this check prevents errors if no Items have been created yet + type(items).__getattr__ = _item_getattr.__get__(items, type(items)) diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/actions$py.class b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/actions$py.class new file mode 100755 index 0000000000000000000000000000000000000000..ecee631ba54b9ce9e1a7393203b829c09cc69d2d GIT binary patch literal 6008 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/actions.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/actions.py new file mode 100755 index 00000000000..f66b8703ed4 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/actions.py @@ -0,0 +1,49 @@ +""" +This module discovers action services registered from OH1 or OH2 bundles or +add-ons. The specific actions that are available will depend on which add-ons +are installed. Each action class is exposed as an attribute of the +``core.actions`` module. The action methods are static methods on those classes +(don't try to create instances of the action classes). + +.. warning:: In order to avoid namespace conflicts with the ``actions`` object + provided in the default scope, don't use ``import core.actions`` or + ``from core import actions``. + +See the :ref:`Guides/Actions:Actions` guide for details on the use of this +module. +""" +import sys +from core import osgi + +__all__ = [] + +OH1_ACTIONS = osgi.find_services("org.openhab.core.scriptengine.action.ActionService", None) or [] +OH2_ACTIONS = osgi.find_services("org.eclipse.smarthome.model.script.engine.action.ActionService", None) or [] + +_MODULE = sys.modules[__name__] + +for s in OH1_ACTIONS + OH2_ACTIONS: + action_class = s.actionClass + name = str(action_class.simpleName) + setattr(_MODULE, name, action_class) + __all__.append(name) + +try: + from org.openhab.core.model.script.actions import Exec + from org.openhab.core.model.script.actions import HTTP + from org.openhab.core.model.script.actions import LogAction + from org.openhab.core.model.script.actions import Ping + from org.openhab.core.model.script.actions import ScriptExecution +except: + from org.eclipse.smarthome.model.script.actions import Exec + from org.eclipse.smarthome.model.script.actions import HTTP + from org.eclipse.smarthome.model.script.actions import LogAction + from org.eclipse.smarthome.model.script.actions import Ping + from org.eclipse.smarthome.model.script.actions import ScriptExecution + +STATIC_IMPORTS = [Exec, HTTP, LogAction, Ping, ScriptExecution] + +for s in STATIC_IMPORTS: + name = str(s.simpleName) + setattr(_MODULE, name, s) + __all__.append(name) diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/date$py.class b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/date$py.class new file mode 100755 index 0000000000000000000000000000000000000000..e909842fe14dee5b107e4e1cb75bddbd37b5e7c9 GIT binary patch literal 22553 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 O%k{>7qaZI8qxe66`67z| literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/date.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/date.py new file mode 100755 index 00000000000..7dc009cd1cd --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/date.py @@ -0,0 +1,376 @@ +# pylint: disable=super-init-not-called +""" +This module provides functions for date and time conversions. The functions in +this module can accept any of the following date types: + +.. code-block:: + + java.time.ZonedDateTime + java.time.LocalDateTime + java.util.Calendar + java.util.Date + org.joda.time.DateTime + datetime.datetime (Python) + org.eclipse.smarthome.core.library.types.DateTimeType + org.openhab.core.library.types.DateTimeType +""" +__all__ = [ + "format_date", "days_between", "hours_between", "minutes_between", + "seconds_between", "to_java_zoneddatetime", "to_java_calendar", + "to_python_datetime", "to_joda_datetime", "human_readable_seconds" +] + +import sys +import datetime + +from java.time import LocalDateTime, ZonedDateTime +from java.time import ZoneId, ZoneOffset +from java.time.format import DateTimeFormatter +from java.time.temporal.ChronoUnit import DAYS, HOURS, MINUTES, SECONDS +from java.util import Calendar, Date, TimeZone +from org.joda.time import DateTime, DateTimeZone +from org.eclipse.smarthome.core.library.types import DateTimeType as eclipseDateTime + +if 'org.eclipse.smarthome.automation' in sys.modules or 'org.openhab.core.automation' in sys.modules: + # Workaround for Jython JSR223 bug where dates and datetimes are converted + # to java.sql.Date and java.sql.Timestamp + def remove_java_converter(clazz): + if hasattr(clazz, '__tojava__'): + del clazz.__tojava__ + remove_java_converter(datetime.date) + remove_java_converter(datetime.datetime) + +try: + # if the compat1x bundle is not installed, the OH 1.x DateTimeType is not available + from org.openhab.core.library.types import DateTimeType as LEGACY_DATETIME +except: + LEGACY_DATETIME = None + +def format_date(value, format_string="yyyy-MM-dd'T'HH:mm:ss.SSxx"): + """ + Returns string of ``value`` formatted according to ``format_string``. + + This function can be used when updating Items in openHAB or to format any + date value for output. The default format string follows the same ISO8601 + format used in openHAB. If ``value`` does not have timezone information, + the system default will be used. + + Examples: + .. code-block:: + + events.sendCommand("date_item", format_date(date_value)) + log.info("The time is currently: {}".format(format_date(ZonedDateTime.now()))) + + Args: + value: the value to convert + format_string (str): the pattern to format ``value`` with. + See `java.time.format.DateTimeFormatter `_ + for format string tokens. + + Returns: + str: the converted value + """ + return to_java_zoneddatetime(value).format(DateTimeFormatter.ofPattern(format_string)) + +def days_between(value_from, value_to, calendar_days=False): + """ + Returns the number of days between ``value_from`` and ``value_to``. + Will return a negative number if ``value_from`` is after ``value__to``. + + Examples: + .. code-block:: + + span_days = days_between(items["date_item"], ZonedDateTime.now()) + + Args: + value_from: value to start from + value_to: value to measure to + calendar_days (bool): if ``True``, the value returned will be the + number of calendar days rather than 24-hour periods (default) + + Returns: + int: the number of days between ``value_from`` and ``value_to`` + """ + if calendar_days: + return DAYS.between(to_java_zoneddatetime(value_from).toLocalDate().atStartOfDay(), to_java_zoneddatetime(value_to).toLocalDate().atStartOfDay()) + else: + return DAYS.between(to_java_zoneddatetime(value_from), to_java_zoneddatetime(value_to)) + +def hours_between(value_from, value_to): + """ + Returns the number of hours between ``value_from`` and ``value_to``. + Will return a negative number if ``value_from`` is after ``value__to``. + + Examples: + .. code-block:: + + span_hours = hours_between(items["date_item"], ZonedDateTime.now()) + + Args: + value_from: value to start from + value_to: value to measure to + + Returns: + int: the number of hours between ``value_from`` and ``value_to`` + """ + return HOURS.between(to_java_zoneddatetime(value_from), to_java_zoneddatetime(value_to)) + +def minutes_between(value_from, value_to): + """ + Returns the number of minutes between ``value_from`` and ``value_to``. + Will return a negative number if ``value_from`` is after ``value__to``. + + Examples: + .. code-block:: + + span_minutes = minutes_between(items["date_item"], ZonedDateTime.now()) + + Args: + value_from: value to start from + value_to: value to measure to + + Returns: + int: the number of minutes between ``value_from`` and ``value_to`` + """ + return MINUTES.between(to_java_zoneddatetime(value_from), to_java_zoneddatetime(value_to)) + +def seconds_between(value_from, value_to): + """ + Returns the number of seconds between ``value_from`` and ``value_to``. + Will return a negative number if ``value_from`` is after ``value__to``. + + Examples: + .. code-block:: + + span_seconds = seconds_between(items["date_item"], ZonedDateTime.now()) + + Args: + value_from: value to start from + value_to: value to measure to + + Returns: + int: the number of seconds between ``value_from`` and ``value_to`` + """ + return SECONDS.between(to_java_zoneddatetime(value_from), to_java_zoneddatetime(value_to)) + +def to_java_zoneddatetime(value): + """ + Converts any of the supported date types to ``java.time.ZonedDateTime``. If + ``value`` does not have timezone information, the system default will be + used. + + Examples: + .. code-block:: + + java_time = to_java_zoneddatetime(items["date_item"]) + + Args: + value: the value to convert + + Returns: + java.time.ZonedDateTime: the converted value + + Raises: + TypeError: if the type of ``value`` is not supported by this module + """ + if isinstance(value, ZonedDateTime): + return value + timezone_id = ZoneId.systemDefault() + # java.time.LocalDateTime + if isinstance(value, LocalDateTime): + return value.atZone(timezone_id) + # python datetime + if isinstance(value, datetime.datetime): + if value.tzinfo is not None: + timezone_id = ZoneId.ofOffset("GMT", ZoneOffset.ofTotalSeconds(int(value.utcoffset().total_seconds()))) + return ZonedDateTime.of( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.microsecond * 1000, + timezone_id + ) + # java.util.Calendar + if isinstance(value, Calendar): + return ZonedDateTime.ofInstant(value.toInstant(), ZoneId.of(value.getTimeZone().getID())) + # java.util.Date + if isinstance(value, Date): + return ZonedDateTime.ofInstant(value.toInstant(), ZoneId.ofOffset("GMT", ZoneOffset.ofHours(0 - value.getTimezoneOffset() / 60))) + # Joda DateTime + if isinstance(value, DateTime): + return value.toGregorianCalendar().toZonedDateTime() + # openHAB DateTimeType + if isinstance(value, eclipseDateTime): + return to_java_zoneddatetime(value.calendar) + # openHAB 1.x DateTimeType + if LEGACY_DATETIME and isinstance(value, LEGACY_DATETIME): + return to_java_zoneddatetime(value.calendar) + + raise TypeError("Unknown type: {}".format(str(type(value)))) + +def to_python_datetime(value): + """ + Converts any of the supported date types to Python ``datetime.datetime``. + If ``value`` does not have timezone information, the system default will be + used. + + Examples: + .. code-block:: + + python_time = to_python_datetime(items["date_item"]) + + Args: + value: the value to convert + + Returns: + datetime.datetime: the converted value + + Raises: + TypeError: if the type of ``value`` is not supported by this module + """ + if isinstance(value, datetime.datetime): + return value + + value_zoneddatetime = to_java_zoneddatetime(value) + return datetime.datetime( + value_zoneddatetime.getYear(), + value_zoneddatetime.getMonthValue(), + value_zoneddatetime.getDayOfMonth(), + value_zoneddatetime.getHour(), + value_zoneddatetime.getMinute(), + value_zoneddatetime.getSecond(), + int(value_zoneddatetime.getNano() / 1000), + _pythonTimezone(int(value_zoneddatetime.getOffset().getTotalSeconds() / 60)) + ) + +class _pythonTimezone(datetime.tzinfo): + + def __init__(self, offset=0, name=""): + """ + Python tzinfo with ``offset`` in minutes and name ``name``. + + Args: + offset (int): Timezone offset from UTC in minutes. + name (str): Display name of this instance. + """ + self.__offset = offset + self.__name = name + #super(_pythonTimezone, self).__init__() + + def utcoffset(self, value): + return datetime.timedelta(minutes=self.__offset) + + def tzname(self, value): + return self.__name + + def dst(self, value): + return datetime.timedelta(0) + +def to_joda_datetime(value): + """ + Converts any of the supported date types to ``org.joda.time.DateTime``. If + ``value`` does not have timezone information, the system default will be + used. + + Examples: + .. code-block:: + + joda_time = to_joda_datetime(items["date_item"]) + + Args: + value: the value to convert + + Returns: + org.joda.time.DateTime: the converted value + + Raises: + TypeError: if the type of ``value`` is not suported by this package + """ + if isinstance(value, DateTime): + return value + + value_zoneddatetime = to_java_zoneddatetime(value) + return DateTime( + value_zoneddatetime.toInstant().toEpochMilli(), + DateTimeZone.forID(value_zoneddatetime.getZone().getId()) + ) + +def to_java_calendar(value): + """ + Converts any of the supported date types to ``java.util.Calendar``. If + ``value`` does not have timezone information, the system default will be + used. + + Examples: + .. code-block:: + + calendar_time = to_java_calendar(items["date_item"]) + + Args: + value: the value to convert + + Returns: + java.util.Calendar: the converted value + + Raises: + TypeError: if the type of ``value`` is not supported by this package + """ + if isinstance(value, Calendar): + return value + + value_zoneddatetime = to_java_zoneddatetime(value) + new_calendar = Calendar.getInstance(TimeZone.getTimeZone(value_zoneddatetime.getZone().getId())) + new_calendar.set(Calendar.YEAR, value_zoneddatetime.getYear()) + new_calendar.set(Calendar.MONTH, value_zoneddatetime.getMonthValue() - 1) + new_calendar.set(Calendar.DAY_OF_MONTH, value_zoneddatetime.getDayOfMonth()) + new_calendar.set(Calendar.HOUR_OF_DAY, value_zoneddatetime.getHour()) + new_calendar.set(Calendar.MINUTE, value_zoneddatetime.getMinute()) + new_calendar.set(Calendar.SECOND, value_zoneddatetime.getSecond()) + new_calendar.set(Calendar.MILLISECOND, int(value_zoneddatetime.getNano() / 1000000)) + return new_calendar + +def human_readable_seconds(seconds): + """ + Converts seconds into a human readable string of days, hours, minutes and + seconds. + + Examples: + .. code-block:: + + message = human_readable_seconds(55555) + # 15 hours, 25 minutes and 55 seconds + + Args: + seconds: the number of seconds + + Returns: + str: a string in the format ``{} days, {} hours, {} minutes and {} + seconds`` + """ + seconds = int(round(seconds)) + number_of_days = seconds//86400 + number_of_hours = (seconds%86400)//3600 + number_of_minutes = (seconds%3600)//60 + number_of_seconds = (seconds%3600)%60 + + days_string = "{} day{}".format(number_of_days, "s" if number_of_days > 1 else "") + hours_string = "{} hour{}".format(number_of_hours, "s" if number_of_hours > 1 else "") + minutes_string = "{} minute{}".format(number_of_minutes, "s" if number_of_minutes > 1 else "") + seconds_string = "{} second{}".format(number_of_seconds, "s" if number_of_seconds > 1 else "") + + return "{}{}{}{}{}{}{}".format( + days_string if number_of_days > 0 else "", + "" if number_of_days == 0 or (number_of_hours == 0 and number_of_minutes == 0) else ( + " and " if (number_of_hours > 0 and number_of_minutes == 0 and number_of_seconds == 0) or (number_of_hours == 0 and number_of_minutes > 0 and number_of_seconds == 0) else ", " + ), + hours_string if number_of_hours > 0 else "", + "" if number_of_hours == 0 or number_of_minutes == 0 else ( + " and " if number_of_minutes > 0 and number_of_seconds == 0 else ", " + ), + minutes_string if number_of_minutes > 0 else "", + " and " if number_of_seconds > 0 and (number_of_minutes > 0 or number_of_hours > 0 or number_of_days > 0) else "", + seconds_string if number_of_seconds > 0 else "" + ) diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/items$py.class b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/items$py.class new file mode 100755 index 0000000000000000000000000000000000000000..f527e67411904781440599d4e34c3105ea0a9699 GIT binary patch literal 9079 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/items.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/items.py new file mode 100755 index 00000000000..0dfa008a067 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/items.py @@ -0,0 +1,102 @@ +# pylint: disable=wrong-import-position +""" +This module allows runtime creation and removal of items. It will also remove +any links from an Item before it is removed. +""" +__all__ = ["add_item", "remove_item"] + +from core.jsr223.scope import scriptExtension, itemRegistry +scriptExtension.importPreset(None) +#import core +from core import osgi +from core.log import logging, LOG_PREFIX +from core.links import remove_all_links + +ITEM_BUILDER_FACTORY = osgi.get_service("org.openhab.core.items.ItemBuilderFactory") or osgi.get_service("org.eclipse.smarthome.core.items.ItemBuilderFactory") + +MANAGED_ITEM_PROVIDER = osgi.get_service("org.openhab.core.items.ManagedItemProvider") or osgi.get_service("org.eclipse.smarthome.core.items.ManagedItemProvider") + +LOG = logging.getLogger("{}.core.items".format(LOG_PREFIX)) + +def add_item(item_or_item_name, item_type=None, category=None, groups=None, label=None, tags=None, gi_base_type=None, group_function=None): + """ + Adds an Item using a ManagedItemProvider. + + Args: + item_or_item_name (Item or str): Item object or name for the Item to + create + item_type (str): (optional, if item_oritem_name is an Item) the type + of the Item + category (str): (optional) the category (icon) for the Item + groups (str): (optional) a list of groups the Item is a member of + label (str): (optional) the label for the Item + tags (list): (optional) a list of tags for the Item + gi_base_type (str): (optional) the group Item base type for the Item + group_function (GroupFunction): (optional) the group function used by + the Item + + Returns: + Item or None: The Item that was created or None + + Raises: + TypeError: if item_or_item_name is not an Item or string, or if + item_or_item_name is not an Item and item_type is not provided + """ + try: + if not isinstance(item_or_item_name, basestring) and not hasattr(item_or_item_name, 'name'): + raise TypeError("\"{}\" is not a string or Item".format(item_or_item_name)) + item = item_or_item_name + if isinstance(item_or_item_name, basestring): + item_name = item_or_item_name + if item_type is None: + raise TypeError("Must provide item_type when creating an Item by name") + + base_item = None if item_type != "Group" or gi_base_type is None else ITEM_BUILDER_FACTORY.newItemBuilder(gi_base_type, item_name + "_baseItem").build() + group_function = None if item_type != "Group" else group_function + if tags is None: + tags = [] + item = ITEM_BUILDER_FACTORY.newItemBuilder(item_type, item_name)\ + .withCategory(category)\ + .withGroups(groups)\ + .withLabel(label)\ + .withBaseItem(base_item)\ + .withGroupFunction(group_function)\ + .withTags(set(tags))\ + .build() + + MANAGED_ITEM_PROVIDER.add(item) + LOG.debug("Item added: [{}]".format(item)) + return item + except: + import traceback + LOG.error(traceback.format_exc()) + return None + +def remove_item(item_or_item_name): + """ + This function removes an Item using a ManagedItemProvider. + + Args: + item_or_item_name (Item or str): the Item object or name for the + Item to create + + Returns: + Item or None: the Item that was removed or None + """ + try: + item = remove_all_links(item_or_item_name) + if item is None: + LOG.warn("Item cannot be removed because it does not exist in the ItemRegistry: [{}]".format(item_or_item_name)) + return None + + MANAGED_ITEM_PROVIDER.remove(item.name) + if itemRegistry.getItems(item.name) == []: + LOG.debug("Item removed: [{}]".format(item.name)) + return item + else: + LOG.warn("Failed to remove Item from the ItemRegistry: [{}]".format(item.name)) + return None + except: + import traceback + LOG.error(traceback.format_exc()) + return None diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/jsr223$py.class b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/jsr223$py.class new file mode 100755 index 0000000000000000000000000000000000000000..2a9ec26edd4084bfdbccee173a61a484f6d33998 GIT binary patch literal 8814 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/jsr223.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/jsr223.py new file mode 100755 index 00000000000..f0dc6a163ff --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/jsr223.py @@ -0,0 +1,74 @@ +# pylint: disable=protected-access, no-init +""" +One of the challenges of scripted automation with Jython is that modules +imported into scripts do not have direct access to the JSR223 scope types and +objects. This module allows imported modules to access that data. + +.. code-block:: + + # In Jython module, not script... + from core.jsr223.scope import events + + def update_data(data): + events.postUpdate("TestString1", str(data)) +""" +import sys +import types + +def get_scope(): + depth = 1 + while True: + try: + frame = sys._getframe(depth) + name = str(type(frame.f_globals)) + if name == "": + return frame.f_globals + depth += 1 + except ValueError: + raise EnvironmentError("No JSR223 scope is available") + +def _get_scope_value(scope, name): + return scope.get(name, None) or getattr(scope, name, None) + +_PRESETS = [ + [["SimpleRule"], "RuleSimple"], + [["automationManager"], "RuleSupport"], +] + +class _Jsr223ModuleFinder(object): + + class ScopeModule(types.ModuleType): + + def __getattr__(self, name): + global _PRESETS + scope = get_scope() + if name == "scope": + return scope + value = _get_scope_value(scope, name) + if value is None: + for preset in _PRESETS: + if name in preset[0]: + script_extension = _get_scope_value(scope, "scriptExtension") + # print "auto-import preset ", name, preset, scriptExtension + script_extension.importPreset(preset[1]) + return value if value is not None else _get_scope_value(scope, name) + + def load_module(self, fullname): + if fullname not in sys.modules: + module = _Jsr223ModuleFinder.ScopeModule('scope') + setattr(module, '__file__', '') + setattr(module, '__name__', 'scope') + setattr(module, '__loader__', self) + sys.modules[fullname] = module + + def find_module(self, fullname, path=None): + if fullname == "core.jsr223.scope": + return self + +sys.meta_path.append(_Jsr223ModuleFinder()) + +def get_automation_manager(): + scope = get_scope() + _get_scope_value(scope, "scriptExtension").importPreset("RuleSupport") + automation_manager = _get_scope_value(scope, "automationManager") + return automation_manager diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/links$py.class b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/links$py.class new file mode 100755 index 0000000000000000000000000000000000000000..abad4e2b2e44d53b664188d7d7cd505944af21ad GIT binary patch literal 9101 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

`vAwP8Cav;;IxTYEX|&ixuv4LaY?075Se)JH!pq z=JjpuE8ZwqlckKG*b!0CS`e4Mwt`rFq#)LS&H$}DB6vO_xH-BJO&bKc%mOUjwN)W$ q22=evI7r`5f-Vb0r#KJ4r{Vv9X@P#NF?5(3KkO21qJtlDqVpfzyRD=E literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/links.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/links.py new file mode 100755 index 00000000000..26736bfa9f3 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/links.py @@ -0,0 +1,110 @@ +# pylint: disable=wrong-import-position +""" +This module allows runtime creation and removal of links. +""" +__all__ = ["add_link", "remove_link"] + +from core.jsr223.scope import scriptExtension +scriptExtension.importPreset(None) +#import core +from core import osgi +from core.log import logging, LOG_PREFIX +from core.utils import validate_item, validate_channel_uid + +try: + from org.openhab.core.thing.link import ItemChannelLink +except: + from org.eclipse.smarthome.core.thing.link import ItemChannelLink + +ITEM_CHANNEL_LINK_REGISTRY = osgi.get_service("org.openhab.core.thing.link.ItemChannelLinkRegistry") or osgi.get_service("org.eclipse.smarthome.core.thing.link.ItemChannelLinkRegistry") + +MANAGED_ITEM_CHANNEL_LINK_PROVIDER = osgi.get_service("org.openhab.core.thing.link.ManagedItemChannelLinkProvider") or osgi.get_service("org.eclipse.smarthome.core.thing.link.ManagedItemChannelLinkProvider") + +LOG = logging.getLogger("{}.core.links".format(LOG_PREFIX)) + +def add_link(item_or_item_name, channel_uid_or_string): + """ + This function adds a Link to an Item using a + ManagedItemChannelLinkProvider. + + Args: + item_or_item_name (Item or str): the Item object or name to create + the Link for + channel_uid_or_string (ChannelUID or str): the ChannelUID or string + representation of a ChannelUID to link the Item to + + Returns: + Item or None: the Item that the Link was added to or None + """ + try: + item = validate_item(item_or_item_name) + channel_uid = validate_channel_uid(channel_uid_or_string) + if item is None or channel_uid is None: + return None + + link = ItemChannelLink(item.name, channel_uid) + MANAGED_ITEM_CHANNEL_LINK_PROVIDER.add(link) + LOG.debug("Link added: [{}]".format(link)) + return item + except: + import traceback + LOG.error(traceback.format_exc()) + return None + +def remove_link(item_or_item_name, channel_uid_or_string): + """ + This function removes a Link from an Item using a + ManagedItemChannelLinkProvider. + + Args: + item_or_item_name (Item or str): the Item object or name to create + the Link for + channel_uid_or_string (ChannelUID or str): the ChannelUID or string + representation of a ChannelUID to link the Item to + + Returns: + Item or None: the Item that the Link was removed from or None + """ + try: + item = validate_item(item_or_item_name) + channel_uid = validate_channel_uid(channel_uid_or_string) + if item is None or channel_uid is None: + return None + + link = ItemChannelLink(item.name, channel_uid) + MANAGED_ITEM_CHANNEL_LINK_PROVIDER.remove(str(link)) + LOG.debug("Link removed: [{}]".format(link)) + return item + except: + import traceback + LOG.error(traceback.format_exc()) + return None + +def remove_all_links(item_or_item_name): + """ + This function removes all Links from an Item using a + ManagedItemChannelLinkProvider. + + Args: + item_or_item_name (Item or str): the Item object or name to create + the Link for + + Returns: + Item or None: the Item that the Links were removed from or None + """ + try: + item = validate_item(item_or_item_name) + if item is None: + return None + + channels = ITEM_CHANNEL_LINK_REGISTRY.getBoundChannels(item.name) + #links = map(lambda channel: ItemChannelLink(item.name, channel), channels) + links = [ItemChannelLink(item.name, channel) for channel in channels] + for link in links: + MANAGED_ITEM_CHANNEL_LINK_PROVIDER.remove(str(link)) + LOG.debug("Link removed: [{}]".format(link)) + return item + except: + import traceback + LOG.error(traceback.format_exc()) + return None diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/log$py.class b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/log$py.class new file mode 100755 index 0000000000000000000000000000000000000000..1b89393f7fee90b0be8d090edbfc4139479a7421 GIT binary patch literal 8597 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 Sm-wi+^bbUxMq literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/log.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/log.py new file mode 100755 index 00000000000..25bd2bb716e --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/log.py @@ -0,0 +1,69 @@ +""" +This module bridges the `Python standard logging module `_ +with the slf4j library used by openHAB. The ``configuration`` module provides +a ``LOG_PREFIX`` variable that is used as the default logger throughout the +core modules and scripts. +""" +import logging +import functools +import traceback + +from org.slf4j import Logger, LoggerFactory + +from configuration import LOG_PREFIX + +class Slf4jHandler(logging.Handler): + def emit(self, record): + message = self.format(record) + logger_name = record.name + if record.name == "root": + logger_name = Logger.ROOT_LOGGER_NAME + logger = LoggerFactory.getLogger(logger_name) + level = record.levelno + if level == logging.CRITICAL: + logger.trace(message) + elif level == logging.ERROR: + logger.error(message) + elif level == logging.DEBUG: + logger.debug(message) + elif level == logging.WARNING: + logger.warn(message) + elif level == logging.INFO: + logger.info(message) + +HANDLER = Slf4jHandler() +logging.root.setLevel(logging.DEBUG) +logging.root.handlers = [HANDLER] + +def log_traceback(function): + """ + Decorator to provide better Jython stack traces + + Essentially, the decorated function/class/method is wrapped in a try/except + and will log a traceback for exceptions. If openHAB Cloud Connector is + installed, exceptions will be sent as a notification. If the + configuration.adminEmail variable is populated, the notification will be + sent to that address. Otherwise, a broadcast notification will be sent. + """ + functools.wraps(function) + def wrapper(*args, **kwargs): + try: + return function(*args, **kwargs) + except: + rule_name = None + if hasattr(function, 'log'): + function.log.error(traceback.format_exc()) + rule_name = function.name + elif args and hasattr(args[0], 'log'): + args[0].log.error(traceback.format_exc()) + rule_name = args[0].name + else: + logging.getLogger(LOG_PREFIX).error(traceback.format_exc()) + import core.actions + if hasattr(core.actions, 'NotificationAction'): + import configuration + if hasattr(configuration, 'admin_email') and configuration.admin_email != "admin_email@some_domain.com": + core.actions.NotificationAction.sendNotification(configuration.admin_email, "Exception: {}: [{}]".format(rule_name, traceback.format_exc())) + else: + core.actions.NotificationAction.sendBroadcastNotification("Exception: {}: [{}]".format(rule_name, traceback.format_exc())) + return wrapper diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/metadata$py.class b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/metadata$py.class new file mode 100755 index 0000000000000000000000000000000000000000..0da7985fcfe8ba2c4b6311bbe585aac92c63884c GIT binary patch literal 18060 zcmd^H3w#viwLd4>%?@$J&88qhz%if(c9RH*kDyjWzz9T25JbdfvpY!^HoNKW2BeBw zt@cgdw!Z75#oG3kq6Cy$?bAMP@B8-lR@>T#t+x;F``)V5^nbo@W_C6^SrGc`@BV(5 zdYIYy&Ue1^KIi*p=yzU!^2Hr8d|{w-cxP!apX*HJ3s&cvoy!Z! zA!~8fz`DVLl}z`Rk|k^L+^W$vJFn>5YNbkxnL>#~I-g1;460|kqjB9}rf3f3(<52S z94_RyXVO;D>>tUcN|}5Pclm-jl+0y@N3zLMCO2Sqm#iUkwN*-{lci)wV`HynnWaI? z>?&COU0YU+$ox)cyvw=SVh+fNBA3SGOtxs|b86y{O{=k3DvYE`BL%CY(V!sHv})OV zO9k){q7YNKzmOm5&Xue}&Pp?Nw5{5j+@9>rCUXOw8qHS?_E4B6QDY++bOO`K)$1r7 zhv-D6NkywPkj?icvrL^nYfP-^x$DDp5=9zmGBrW#pp(tyET%wPw+uDYn`CGT)70wq zS7nN&5S{F+18v9az^W;>flM2>Q ze!Jy)cp9XYiQALe5zC<2T6pK@=}afe=r(Jo^XN=G61tv8bD1JGVb9YTQ^N(BT&8pp z)2y~lwRP4`9gQ@NI+dr2aPZXmOok{hpGs!Kv`}mHZ0J&A!k}}o>Uisi=sb+)ivt;h zx|o{BD>*YX9HI-D8gY{^l$OJrn6B}$@TO|V@)1BsW%t|~!iDJ~dUGRPNEbsIVZ0-o zA28?=2#T8lIJQBTf}vGctVpa`yKH&)RR%5B?$MDSwsM2Xz791lgAjzFF9R7Tz6`pY z=>+H3N^7S#KTFYdDfetH`%?uZ_L%t81+Rgk=G$O+xdD z1bh+B$<$k&rM=16$60JH{q*PxkfOBk1=+!GE}wgwRmjU~^=)&n4$}Y)%4mjZ{*hGd z!~Zs>rq#>VExB~bx+RIV%T{#vu3Nj&AOwIUfK_ltt6)$bQFZ+;ZLD%Kc`ITWMPVpe zGN=f4;H?IYz^9fC4VUaHgXgEz41^tY-6VL(PNwPAF1p+z zyXeMAF!)VOZxVx7X2m)4nIC90@kfMm3qwo;k?jFndWsmBK0 zqCHSsJWTJ!uu*Mmxy^g&eUc~M zuL28Yxt%^B)#roo^5ig*ZrY#^OVR3{n21#Kh4fJ&^D#`r+Do!ogFeo5e(lUqld%-t zDG`2`N;R-Ck`>d8wwiP9p?hV{eGul#?n@2&Bqob{i1QfqARZ?y#P2!PY`OhmP5KYh zr-c5eVI|kd20en{Pb7rJoONAEa_Xa+kI!QHw3W51_UCoO^A||#{fTU{SVHpR?lA48 zFG}F=0prvpq|Ylu`M6HP?g=ed)!m5)5p zNH@@vOq0bIU4|EM-9qKa6DJvo52v1(dDW7e$zm~|%AjvgBOjFpC1w?s$_P201T=m{ zX`TJDoLW}Wko@tA=1oFzvsL^_wVQ7wEs4i?Qq<@n^J2P#6J(|FqG7wzlh0XQ zRa#EvbJ%v|WD8S~q+B+(Y*CHxmMzds+T5}Q6Z$g)BL!@G@;N+BXVjJ`S=bpj`$kH- zcJx`mlm&s1>33;Wtbit{oQKnSY_D?pl4)Ise5xiG^c0#8uev??Ebd9CI=?;5G*s>F zN7r%G@au_(!jz(~NZfxF2?T5!^z|@(Nr&Dy(PNAF27L=jdU-Ni#O-$w7~cKHoP}Kf zJ0)smV5G2PEkPZ!#muLr7!-9PcOoio=2eS|V!qSFW~H#BkdZQ@Xo-%M62+uyGirAC zE7f{1<4W`PzBX-QPf}FoRi-W~8!BS3qs+=CGikLi%?~@S>Mk*E?y<(>l61N=HHclR zWjbP-8FU}jJ%`zr&w1pAa;kZ0Rf4pd*uy)jnyUoTF>IP%JJgQixOsgmHV%pTtzBj- zgf-`znm7Vpo-e@j1@`?y+$&@6!UH!%^}f*QQXCFun^)MDJF>(o|K+S5%AUt5$0G2! z2>dzTU|qPbFk&5D3+2r~(muJYxU$yHRgUZxImB#}6oLk=R&FuTs3p^Bxj1#eA?B{_ za-;0yaAKi$SUS^)23>4zMF}z|Q@Qh9)tGI4`Fs|vW%_|7dfKujUUs)KEr~NH^SNY% z!E~mWN*2;qT0J%Bd(v!ViedU|-81|R(}>p>(MhJ$61m{ZVMBhpF;QMqImIR=YnORF zELx#ed9=A^!u&g?2mbU4bK-nC@kq_)DncTIWuu&*COkI+l)V9IL>Ii`2 zu=y{;^ex#CUO_K~=`VGy`ZadF;|{&%oXvGpl*d+9CXH4smu;6pIS-Y}Q*O@0QHP5- zdlOfd?URH9DpolHi^p0k)~X{q?77Op;BDP10&tvUwR63l1%*@jGuyORTC!?Yc`Y3N zdiunpUF?k~SlOYt)XS3EyIFbQ)Z+4>C!5919fO(FV7ZoSnM`a<<$$xCQKZUNn8n$X zdW`8NdxL(*WRq|^e6tM%kR=7n)ZN&>;*?DuC4)(w`A>1At=BMj}Y)zw9piE7UU)noO{6*$(w z^iEBBIGKU#i?fIlyyqrnYei;gb?q!hs*tR0gQaE9dve#Eg=2C-b@lt36 z(Tt*2rfsRJ4q|Zd635(BW+<7(Rs@CHO6KBf+kYM-ldv&0I#* z5g1m~E@Y)U%-&26$xOO>X)BY0kFL1vL`k6{QD^BFIP%LSr$PfvlyzFUhncw!=h|HF93f}m>^Sb=prXeOIG?FL&AimzHip<}pGRbD~qCMyQV9; zIEU*{vWclKjOvu&Sd&a|b&|m%Zz#o72zERv2AOBQycctJH%B;w^;WGvQLYh9BeQ5L zNStdUROc8^UU4Hy(v5$PkrDZ$<69D-UviO1lr<$Qpa5!Qa0C77$mA#@vqIZP8JYYh zG`v|WXYf?nG7OZ$JWaqw?xrLmE5vkadR<|Z9g zZEj!S39NIOU-AhoUH6@6(oI)6fufvNx_4cR68KtybYhc2ffqI$Io1 zQ2k%5+p1?g&blq}WN-{8?c=p|v_a7>v5>RE+^O9iAA{5n=Z6hmz;t;vQOOOJb@pf% zl9%YR-!~X9(brYSkoR1%#zA_ml0zvzPXg?Grc0|?cXw7t&A-9;ilw8Pk{hGaxrj#4 zXE;%6@P%QXr$h72NFCBcs3bP$Jm#l;V$%C&P|BJl@$y!pd{$i5RV!LKt{sPOJklhux`FS?<21hB`sNN!a72%i;*^fAa#}-DQ+%T-MO|@@rsM!h z)a}y5pv#4_L8Y6hF@rB*n&X!g0GdQM+?q z8RpBhwO2Du@qXsgtKMEOo{O(cPOWa*olg+f$%il@Uem~LkypZjqLuAOVP7@=MI|-| zy}Sc0AFpBAQq$;E!O zeU|I?eI2gfr}9R=S~fVF!o*r@oAIVs5`p@@z+miEjKrq1&rZx=u-V`wemU9O;1p1u zgIrV-$lC6Z+~+g}`b6tO=xcpIu4o*Fy^lGxKH)WIU-0~GlJXfT`YQ}JzCl{)9HH|TM^*yk%6VV~_`ek&K`?mC~3PlVj6d@^knLi{#( zjBd7j@rhX0x+I@3A(bVEec$V^*{N8mu7mQ}oeDp>fp<0X^?V~_D~#l{Y>TSvZsjyz zvBG>azg_Hg3tpW=PZd*`PV-S%1q!O@>}uCvJCe(Z*SkE2_;#kbpuaqWHXWyNOYvQ( z`cyMtSZnoX@l`2akhvEoEWRSkIF%uy1PTR%j)hE(&DSTyQHH~clSt9 zQFr%A!clkl_QL1eCP%v=evrwmlxt`h3NCamS<}7VDiop9#r60c_;h?r<%_p^T?XF) z2Z=a0u=v8gIKS{kbF2nypWlghgaGv^;R1Y5oa^sQj+FBFmJ6$OW;1;ipBg){-7$>s zv`!iCDW1kK#Gefj!pZXurao>6F~0T)h^b*#e43ovwmLbiaw^UoR^e;do{^zGt6)EA z?9Go9QkF2x)B;VFAmI8;F#~U0lEd~-v00?kV7Ipr^Fwq38IpIQrD>9})D_=^s4HGj zt1Axw)fFeI>WWhjxyn9GUC|V(E4l)8MZLiFB+!G4V3oauFLtQ`|D*cbV$ndf;UI-a zDH@A3jM7x~YeOs&!u=tdwlOm4DVn~qJ~{(Py&D73S?U^$n(EpRZB^G$bdI_j(KFOF z(hzOaH=*b}ePcx9a&v&r>WM`cw2#sv?ukWClqJs%JVodCwAV)$_qNCC4$>0bFX#%y zqKjJsf`{A4WI)3Oc943(Y_m|gQmAwVS_1XamAx$iA=DC3T&sr*x`MIj%9fzwJBV@hV72pL4+ZJz zSY!$)LA+7g6x}>ZTjJ3^=w_7qH;&TQyJ*^mim|L7gW7VA+D=yUm&yELHNOCyXi08k z(G_?MTKErBwmVZ=Fl8UTt>+Nkurc;H-Q2#94n}Xqv;%YqcrzTlj%vJTqhEIMW;u9R zd@58i9do~p1=|z#iQZWByXrQO==Ch~o9JJ;(@kgkHcW@vE2ewq{66}!JEzs2(?qvb z@y3zpYc67&i}*-2;@oC#b}{FqfI~DA!oGALP?l2@{zi;b2yh}`GC-a+1Ev5@2DAV=0LuW|0Ji}i0elzm zGS)G`8p?^z8oUPdgyM^_$UKOsya%ZwhI=^LDBT`?mvYAUKvLTo>qlv~_C+AZfhalU z0DWj<^dms+i3OmFJ0uav7#v)Pd_stf(%qwUf6pG;5JMCJ$won7?T$VGjOau73&AEU zv;-5Qw1>Mexb;pt-A2W%-SJp?!e=l6ylmJ*^%?_fe@=q-*|Gb|Q@+p>6T7q{P1@F& zPjzE7*9z)W z^OT?cKr3jaV#!BOF_+FLJ*OS#^;je=1Z1R=Nu`8SV-d@dA^Ibb?};^N;gruIB|`;2 zQR|7A(VuFMjQ(6WfyW~bZ2VYcK#-**?~cBx7!^m>G7Ks@9@&32PIfiTNBE4IsD#%v zqel>45wfLMVWHBM6Bx57xOj{%^8|oOQqy0f8@pbkr>I+)rTL8C0J?P$T^}K*YgCx&y2d{Twaa9pF;0H2jWn%t=592CO z91}OuJ!+0$m^z->ME43#WeA|T>`@``ORRgJGp-yw`VH58>ko2Zl#w&gnS|`9Z=(AJ zT@s0p9cO^tCoz7ACv8Oca)2A1)P;cuWuk;d0Cbs0c``~yS6~ly#SCe9J$>8ZobJmtvteo`hiGa5=%3DKn(NA!v0%hR<{`Fo&#NKvot7+(TT zS-D;tKP9s#aG8am{$)j7r$#~ZNsYF{NG5Hfrv+d8j8i_E=oxt^GH{(Zg!Tut5I;a3 z)jdQLeMN9oT0`$3&d|&=#6OXJrHkNGoh~AdRlW-3vOM+#JsbXomaQwO+4-86s&Ei_ zUAuZ^XVL+hUw4=k9uW)^P@MzRZwjjLsF$>nm1kpRAc(*SBfE)_Sb0{aM9s=7YZuPv zkk2}SgLd8alV(0$WN+p(%Z_6EfPaR%oI;1`)!-PNRoxEJVcLw`b>%B$j?touqCjKl zfhyZ1Qo*3R)Pr#HCjd_Z4gtOdcnTo@c=&0+Gk~uEz6$sn;Ol^I0-gap3-~rd0|ys& zfVA5I@?nVjLx`%oc?l2{@o@ptQI=bBzRKj|65{hRNh@XRopmiBgTf9nwIkujg5&S+ z`~Yac(?R2C{2jA%m%_^)JKWh>VU7aiFe zB6Zp0CxgyQijMMlL+vNUka{$uLMwRl~6G7amA0lxse2>2C1p1%b6HQ=`h z8l3d$(7{u^1=$;GlG7)Ij%;rcIw-D=we%jZnU_th5a8D-e7_h8I(466DDA=Jy6?6T z$Yscm_PE_j>unhZU_-bL(%9`zMTR80@HP-1rn`gX{)sWf_>1cR^?(2XSaPbVrRJ-V zFy!e;AT0+gdaEu{Pg4t(8|`LZ4M{*9p&qh}l^;5!#$94YX0z$t*4 z=u`2ULhHHi0&E3ffx5Hi9pJU|9^>`(^*Bmf9Y+veRbOAf4-c5@n<8|OuX&8G4K%3H z`exqJ8xJ&dUvE6v%;{e2X=g-Q6h4|h;IrVt&ZFH(a#9ttVoAK40;fsH6o$$ zn8D~zd*cn!AN9sV&D`G`H`GFrhGrfVGG=6%BGcd@^BceQS|i&OV#tH|`-Vt`B*Ga4 z*@^|#Dp)bAR?Nv#Z8+r$h%SsW363=Quv|$3e1hrW-ncNWD9vl4M--)qhi#sFETrO1 z^j%HLBN$&C^pi^&XigKotP~OPim{@>NAb|P)IlwzcdFMgCY2HD;RTG=U@8H{P^r%9{ zSy^=dL4L0$$NWBizaHmLAUPmCQ2)Gm`zepgVtW57*GYi$02cr*23!ib3?PSeYXEYR ze-+@GQU1W*QT`C%BY=+qJ`T7Oa5vyy!2N&+_R8rz)&-Yr(g|ZAECm54O>zoI_3la2 u7-anaK7y407_?Q#kMiTV&cy%y%fqnc^WFR@#-53uznu5*XXK{HkNy!yy8tNw literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/metadata.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/metadata.py new file mode 100755 index 00000000000..0ff2fd40427 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/metadata.py @@ -0,0 +1,285 @@ +""" +This module provides functions for manipulating Item Metadata. + +See the :ref:`Guides/Metadata:Metadata` guide for details on the metadata +structure. +""" +__all__ = [ + "get_all_namespaces", "get_metadata", "set_metadata", "remove_metadata", + "get_value", "set_value", "get_key_value", "set_key_value", + "remove_key_value" +] + +from core import osgi +from core.log import logging, LOG_PREFIX + +try: + from org.openhab.core.items import Metadata, MetadataKey +except: + from org.eclipse.smarthome.core.items import Metadata, MetadataKey + +METADATA_REGISTRY = osgi.get_service("org.openhab.core.items.MetadataRegistry") or osgi.get_service("org.eclipse.smarthome.core.items.MetadataRegistry") + +LOG = logging.getLogger("{}.core.metadata".format(LOG_PREFIX)) + +def get_all_namespaces(item_name): + """ + This function will return a list of an Item's namespaces. + + Examples: + .. code-block:: + + # Get a list of an Item's namespaces + get_all_namespaces("Item_Name") + + Args: + item_name (str): the name of the Item to retrieve the namespace names + for + + Returns: + list: a list of strings representing the namespace names found for the + specified Item + """ + LOG.debug("get_all_namespaces: Item [{}]".format(item_name)) + return [metadata.UID.namespace for metadata in METADATA_REGISTRY.getAll() if metadata.UID.itemName == item_name] + +def get_metadata(item_name, namespace): + """ + This function will return the Metadata object associated with the + specified Item. + + Examples: + .. code-block:: + + # Get Metadata object from an Item's namespace + get_metadata("Item_Name", "Namespace_Name") + + Args: + item_name (str): name of the Item + namespace (str): name of the namespace + + Returns: + Metadata object or None: Metadata object containing the namespace + ``value`` and ``configuration`` dictionary, but will be ``None`` if + the namespace or the Item does not exist + """ + LOG.debug("get_metadata: Item [{}], namespace [{}]".format(item_name, namespace)) + return METADATA_REGISTRY.get(MetadataKey(namespace, item_name)) + +def set_metadata(item_name, namespace, configuration, value=None, overwrite=False): + """ + This function creates or modifies Item metadata, optionally overwriting + the existing data. If not overwriting, the provided keys and values will + be overlaid on top of the existing keys and values. + + Examples: + .. code-block:: + + # Add/change metadata in an Item's namespace (only overwrites existing keys and "value" is optional) + set_metadata("Item_Name", "Namespace_Name", {"Key_1": "key 1 value", "Key_2": 2, "Key_3": False}, "namespace_value") + + # Overwrite metadata in an Item's namespace with new data + set_metadata("Item_Name", "Namespace_Name", {"Key_5": 5}, overwrite=True) + + Args: + item_name (str): name of the Item + namespace (str): name of the namespace + configuration (dict): ``configuration`` dictionary to add to the + namespace + value (str): either the new namespace value or ``None`` + overwrite (bool): if ``True``, existing namespace data will be + discarded + """ + if overwrite: + remove_metadata(item_name, namespace) + metadata = get_metadata(item_name, namespace) + if metadata is None or overwrite: + LOG.debug("set_metadata: adding or overwriting metadata namespace with [value: {}, configuration: {}]: Item [{}], namespace [{}]".format(value, configuration, item_name, namespace)) + METADATA_REGISTRY.add(Metadata(MetadataKey(namespace, item_name), value, configuration)) + else: + if value is None: + value = metadata.value + new_configuration = dict(metadata.configuration).copy() + new_configuration.update(configuration) + LOG.debug("set_metadata: setting metadata namespace to [value: {}, configuration: {}]: Item [{}], namespace [{}]".format(value, new_configuration, item_name, namespace)) + METADATA_REGISTRY.update(Metadata(MetadataKey(namespace, item_name), value, new_configuration)) + +def remove_metadata(item_name, namespace=None): + """ + This function removes the Item metadata for the specified namepsace or for + all namespaces. + + Examples: + .. code-block:: + + # Remove a namespace from an Item + remove_metadata("Item_Name", "Namespace_Name") + + # Remove ALL namespaces from an Item + remove_metadata("Item_Name") + + Args: + item_name (str): name of the item + namespace (str): name of the namespace or ``None``, which will + remove metadata in all namespaces for the specified Item + """ + if namespace is None: + LOG.debug("remove_metadata (all): Item [{}]".format(item_name)) + METADATA_REGISTRY.removeItemMetadata(item_name) + else: + LOG.debug("remove_metadata: Item [{}], namespace [{}]".format(item_name, namespace)) + METADATA_REGISTRY.remove(MetadataKey(namespace, item_name)) + +def get_key_value(item_name, namespace, *args): + """ + Ths function returns the ``configuration`` value for the specified key. + + Examples: + .. code-block:: + + # Get key/value pair from Item's namespace "configuration" + get_key_value("Item_Name", "Namespace_Name", "Key", "Subkey", "Subsubkey") + + Args: + item_name (str): name of the Item + namespace (str): name of the namespace + key (str): ``configuration`` key to return (multiple keys in + descending branches can be used) + + Returns: + str, decimal, boolean, None, or dict: The ``configuration`` key value + will be returned. Since None is a valid value for a key, when the key, + Item or namespace does not exist, this function will return an empty + dictionary. + """ + LOG.debug("get_key_value: Item [{}], namespace [{}], args [{}]".format(item_name, namespace, args)) + metadata = get_metadata(item_name, namespace) + if metadata is not None: + result = metadata.configuration.get(args[0]) + if result is None: + return {} + else: + for arg in args[1:]: + result = result.get(arg, {}) + return result + else: + return {} + +def set_key_value(item_name, namespace, *args): + """ + This function creates or updates a key value in the specified namespace. + + Examples: + .. code-block:: + + # Set key/value pair in Item's namespace "configuration" + set_key_value("Item_Name", "Namespace_Name", "Key", "Subkey", "Subsubkey", "Value") + + Args: + item_name (str): name of the Item + namespace (str): name of the namespace + key (str): key to create or update (multiple keys in descending + branches can be used) + value (str, decimal, boolean, dict or None): value to set + """ + LOG.debug("set_key_value: Item [{}], namespace [{}], args [{}]".format(item_name, namespace, args)) + if len(args) > 1: + metadata = get_metadata(item_name, namespace) + new_configuration = {} + if metadata is not None: + new_configuration = dict(metadata.configuration).copy() + sub_dict = new_configuration + for arg in args[:-1]: + if arg not in sub_dict.keys(): + sub_dict[arg] = {} + if arg == args[-2]: + sub_dict[arg] = args[-1] + else: + sub_dict = sub_dict[arg] + set_metadata(item_name, namespace, new_configuration) + else: + LOG.warn("set_key_value: at least two args required: args [{}]".format(args)) + +def remove_key_value(item_name, namespace, *args): + """ + This function removes a key from a namespace's ``configuration``. + + Examples: + .. code-block:: + + # Remove key/value pair from namespace ``configuration`` + remove_key_value("Item_Name", "Namespace_Name", "Key", "Subkey", "Subsubkey") + + Args: + item_name (str): name of the Item + namespace (str): name of the namespace + key (str): ``configuration`` key to remove (multiple keys in + descending branches can be used) + """ + LOG.debug("remove_key_value: Item [{}], namespace [{}], args [{}]".format(item_name, namespace, args)) + if args: + metadata = get_metadata(item_name, namespace) + if metadata is not None: + new_configuration = dict(metadata.configuration).copy() + sub_dict = new_configuration + for arg in args: + if arg != args[-1]: + sub_dict = sub_dict.get(arg, {}) + elif sub_dict != {}: + sub_dict.pop(arg) + else: + LOG.warn("remove_key_value: key does not exist: Item [{}], namespace [{}], args [{}]".format(item_name, namespace, args)) + set_metadata(item_name, namespace, new_configuration, metadata.value, True) + else: + LOG.warn("remove_key_value: metadata does not exist: Item [{}], namespace [{}]".format(item_name, namespace)) + else: + LOG.warn("remove_key_value: at least one key is required") + +def get_value(item_name, namespace): + """ + This function will return the Item metadata ``value`` for the specified + namespace. + + Examples: + .. code-block:: + + # Get Item's namespace "value" + get_value("Item_Name", "Namespace_Name") + + Args: + item_name (str): name of the item + namespace (str): name of the namespace + + Returns: + str or None: namespace ``value`` or ``None`` if the namespace or + Item does not exist + """ + LOG.debug("get_value: Item [{}], namespace [{}]".format(item_name, namespace)) + metadata = get_metadata(item_name, namespace) + if metadata is not None: + return metadata.value + else: + return None + +def set_value(item_name, namespace, value): + """ + This function creates or updates the Item metadata ``value`` for the + specified namespace. + + Examples: + .. code-block:: + + # Set Item's namespace "value" + set_value("Item_Name", "Namespace_Name", "namespace value") + + Args: + item_name (str): name of the Item + namespace (str): name of the namespace + value (str): new or updated namespace value + """ + LOG.debug("set_value: Item [{}], namespace [{}], value [{}]".format(item_name, namespace, value)) + metadata = get_metadata(item_name, namespace) + if metadata is not None: + set_metadata(item_name, namespace, metadata.configuration, value, True) + else: + set_metadata(item_name, namespace, {}, value) diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/osgi/__init__$py.class b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/osgi/__init__$py.class new file mode 100755 index 0000000000000000000000000000000000000000..6557cc7f3b4a1028f2c22d13e9736e6c0b434678 GIT binary patch literal 8640 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/osgi/events.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/osgi/events.py new file mode 100755 index 00000000000..8b6909abf3d --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/osgi/events.py @@ -0,0 +1,137 @@ +# pylint: disable=wrong-import-position, invalid-name +""" +This module provides an OSGi EventAdmin event monitor and rule trigger. This +can trigger off any OSGi event. Rule manager events are filtered to avoid +circular loops in the rule execution. + +.. code-block:: + + class ExampleRule(SimpleRule): + def __init__(self): + self.triggers = [ core.osgi.events.OsgiEventTrigger() ] + + def execute(self, module, inputs): + event = inputs['event'] + # do something with event +""" +import uuid +import traceback + +from core.jsr223.scope import scriptExtension +scriptExtension.importPreset("RuleSupport") +from core.jsr223.scope import Trigger, TriggerBuilder, Configuration +#import core +from core.osgi import BUNDLE_CONTEXT +from core.log import logging, LOG_PREFIX + +import java.util + +#from org.osgi.framework import FrameworkUtil +from org.osgi.service.event import EventHandler, EventConstants#, EventAdmin +#from org.osgi.service.cm import ManagedService + +LOG = logging.getLogger("{}.core.osgi.events".format(LOG_PREFIX)) + +def hashtable(*key_values): + """ + Creates a Hashtable from 2-tuples of key/value pairs. + + Args: + key_values (2-tuples): the key/value pairs to add to the Hashtable + + Returns: + java.util.Hashtable: initialized Hashtable + """ + _hashtable = java.util.Hashtable() + for key, value in key_values: + _hashtable.put(key, value) + return _hashtable +class OsgiEventAdmin(object): + _event_handler = None + event_listeners = [] + + log = logging.getLogger("{}.core.osgi.events.OsgiEventAdmin".format(LOG_PREFIX)) + + # Singleton + class OsgiEventHandler(EventHandler): + def __init__(self): + self.log = logging.getLogger("jsr223.jython.core.osgi.events.OsgiEventHandler") + self.registration = BUNDLE_CONTEXT.registerService( + EventHandler, self, hashtable((EventConstants.EVENT_TOPIC, ["*"]))) + self.log.info("Registered openHAB OSGi event listener service") + self.log.debug("Registration: [{}]".format(self.registration)) + + def handleEvent(self, event): + self.log.critical("Handling event: [{}]".format(event)) + for listener in OsgiEventAdmin.event_listeners: + try: + listener(event) + except: + self.log.error("Listener failed: [{}]".format(traceback.format_exc())) + + def dispose(self): + self.registration.unregister() + + @classmethod + def add_listener(cls, listener): + cls.log.debug("Adding listener admin: [{} {}]".format(id(cls), listener)) + cls.event_listeners.append(listener) + if len(cls.event_listeners) == 1: + if cls._event_handler is None: + cls._event_handler = cls.OsgiEventHandler() + + @classmethod + def remove_listener(cls, listener): + cls.log.debug("Removing listener: [{}]".format(listener)) + if listener in cls.event_listeners: + cls.event_listeners.remove(listener) + if not cls.event_listeners: + if cls._event_handler is not None: + cls.log.info("Unregistering openHAB OSGi event listener service") + cls._event_handler.dispose() + cls._event_handler = None + + +# The OH / JSR223 design does not allow trigger handlers to access +# the original trigger instance. The trigger information is copied into a +# RuntimeTrigger and then provided to the trigger handler. Therefore, there +# is no way AFAIK to access the original trigger from the trigger handler. +# Another option is to pass trigger information in the configuration, but +# OSGi doesn't support passing Jython-related objects. To work around these +# issues, the following dictionary provides a side channel for obtaining the original +# trigger. +OSGI_TRIGGERS = {} + +class OsgiEventTrigger(Trigger): + def __init__(self, filter_predicate=None): + """ + The filter_predicate is a predicate taking an event argument and + returning True (keep) or False (drop). + """ + self.filter = filter_predicate or (lambda event: True) + trigger_name = type(self).__name__ + "-" + uuid.uuid1().hex + self.trigger = TriggerBuilder.create().withId(trigger_name).withTypeUID("jsr223.OsgiEventTrigger").withConfiguration(Configuration()).build() + #global OSGI_TRIGGERS + #OSGI_TRIGGERS[self.id] = self + #OSGI_TRIGGERS[trigger_name] = self + OSGI_TRIGGERS[self.trigger.id] = self + + def event_filter(self, event): + return self.filter(event) + + def event_transformer(self, event): + return event + +def log_event(event): + LOG.info("OSGi event: [{} ({})]".format(event, type(event).__name__)) + if isinstance(event, dict): + for name in event: + value = event[name] + LOG.info(" '{}': {} ({})".format(name, value, type(value))) + else: + for name in event.propertyNames: + value = event.getProperty(name) + LOG.info(" '{}': {} ({})".format(name, value, type(value))) + +def event_dict(event): + return {key: event.getProperty(key) for key in event.getPropertyNames()} diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/rules$py.class b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/rules$py.class new file mode 100755 index 0000000000000000000000000000000000000000..8f09fe72b0fc4bc2333f6cff5c359930f8f4fc52 GIT binary patch literal 12326 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 MALCESL!KZ11<1}nnE(I) literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/rules.py b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/rules.py new file mode 100755 index 00000000000..f39dbc11f52 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/rules.py @@ -0,0 +1,146 @@ +# pylint: disable=invalid-name +""" +The rules module contains some utility functions and a decorator that can: + + 1) decorate a Jython class to create a ``SimpleRule``, + 2) decorate the ``when`` function decorator to create a ``SimpleRule``. +""" +__all__ = [ + 'rule', + 'addRule', + 'set_uid_prefix' +] + +from inspect import isclass +from java.util import UUID + +try: + from org.openhab.core.automation import Rule as SmarthomeRule +except: + from org.eclipse.smarthome.automation import Rule as SmarthomeRule + +from core.log import logging, LOG_PREFIX, log_traceback +from core.jsr223.scope import SimpleRule, scriptExtension +from core.jsr223 import get_automation_manager + +LOG = logging.getLogger("{}.core.rules".format(LOG_PREFIX)) + +scriptExtension.importPreset("RuleSimple") + +def rule(name=None, description=None, tags=None): + """ + This decorator can be used with both functions and classes to create rules. + + See :ref:`Guides/Rules:Decorators` for a full description of how to use + this decorator. + + Examples: + .. code-block:: + + @rule('name', 'description', ['tag1', 'tag2']) + @rule('name', tags=['tag1', 'tag2']) + @rule('name') + + Args: + name (str): display name of the rule + description (str): (optional) description of the rule + tags (list): (optional) list of tags as strings + """ + def rule_decorator(new_rule): + if isclass(new_rule): + clazz = new_rule + def init(self, *args, **kwargs): + SimpleRule.__init__(self) + if name is None: + if hasattr(clazz, '__name__'): + self.name = clazz.__name__ + else: + self.name = "JSR223-Jython" + else: + self.name = name + #set_uid_prefix(self) + self.log = logging.getLogger("{}.{}".format(LOG_PREFIX, self.name)) + clazz.__init__(self, *args, **kwargs) + if description is not None: + self.description = description + elif self.description is None and clazz.__doc__: + self.description = clazz.__doc__ + if hasattr(self, "getEventTriggers"): + self.triggers = log_traceback(self.getEventTriggers)() + if tags is not None: + self.tags = set(tags) + subclass = type(clazz.__name__, (clazz, SimpleRule), dict(__init__=init)) + subclass.execute = log_traceback(clazz.execute) + new_rule = addRule(subclass()) + subclass.UID = new_rule.UID + return subclass + else: + callable_obj = new_rule + if callable_obj.triggers.count(None) == 0: + simple_rule = _FunctionRule(callable_obj, callable_obj.triggers, name=name, description=description, tags=tags) + new_rule = addRule(simple_rule) + callable_obj.UID = new_rule.UID + callable_obj.triggers = None + return callable_obj + else: + LOG.warn("rule: not creating rule [{}] due to an invalid trigger definition".format(name)) + return None + return rule_decorator + +class _FunctionRule(SimpleRule): + def __init__(self, callback, triggers, name=None, description=None, tags=None): + self.triggers = triggers + if name is None: + if hasattr(callback, '__name__'): + name = callback.__name__ + else: + name = "JSR223-Jython" + self.name = name + callback.log = logging.getLogger("{}.{}".format(LOG_PREFIX, name)) + self.callback = callback + if description is not None: + self.description = description + if tags is not None: + self.tags = set(tags) + + @log_traceback + def execute(self, module, inputs): + self.callback(inputs.get('event')) + +def addRule(new_rule): + """ + This function adds a ``rule`` to openHAB's ``ruleRegistry``. + + This is a wrapper of ``automationManager.addRule()`` that does not require + any additional imports. The `addRule` function is similar to the + `automationManager.addRule` function, except that it can be safely used in + modules (versus scripts). Since the `automationManager` is different for + every script scope, the `core.rules.addRule` function looks up the + automation manager for each call. + + Args: + new_rule (SimpleRule): a rule to add to openHAB + + Returns: + Rule: the Rule object that was created + """ + LOG.debug("Added rule [{}]".format(new_rule.name)) + return get_automation_manager().addRule(new_rule) + +def set_uid_prefix(new_rule, prefix=None): + """ + This function changes the UID of a rule, with the option to include a + specified text. + + .. warning:: This function needs some attention in order to work with the + Automation API changes included in S1319. + + Args: + new_rule (Rule): the rule to modify + prefix (str): (optional) the text to include in the UID + """ + if prefix is None: + prefix = type(new_rule).__name__ + uid_field = type(SmarthomeRule).getClass(SmarthomeRule).getDeclaredField(SmarthomeRule, "uid") + uid_field.setAccessible(True) + uid_field.set(new_rule, "{}-{}".format(prefix, str(UUID.randomUUID()))) diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/testing$py.class b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/testing$py.class new file mode 100755 index 0000000000000000000000000000000000000000..2a4a0aa05a835730e75fcd54a4ef45005d8acfaf GIT binary patch literal 9741 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 + to parse + """ + try: + def item_trigger(function): + if not hasattr(function, 'triggers'): + function.triggers = [] + item = itemRegistry.getItem(trigger_target) + group_members = [] + if target_type == "Member of": + group_members = item.getMembers() + elif target_type == "Descendent of": + group_members = item.getAllMembers() + else: + group_members = [item] + for member in group_members: + trigger_name = "Item-{}-{}{}{}{}{}".format( + member.name, + trigger_type.replace(" ", "-"), + "-from-{}".format(old_state) if old_state is not None else "", + "-to-" if new_state is not None and trigger_type == "changed" else "", + "-" if trigger_type == "received update" and new_state is not None else "", + new_state if new_state is not None else "") + trigger_name = validate_uid(trigger_name) + if trigger_type == "received update": + function.triggers.append(ItemStateUpdateTrigger(member.name, state=new_state, trigger_name=trigger_name).trigger) + elif trigger_type == "received command": + function.triggers.append(ItemCommandTrigger(member.name, command=new_state, trigger_name=trigger_name).trigger) + else: + function.triggers.append(ItemStateChangeTrigger(member.name, previous_state=old_state, state=new_state, trigger_name=trigger_name).trigger) + LOG.debug("when: Created item_trigger: [{}]".format(trigger_name)) + return function + + def item_registry_trigger(function): + if not hasattr(function, 'triggers'): + function.triggers = [] + event_names = { + 'added': 'ItemAddedEvent', + 'removed': 'ItemRemovedEvent', + 'modified': 'ItemUpdatedEvent' + } + function.triggers.append(ItemRegistryTrigger(event_names.get(trigger_target))) + LOG.debug("when: Created item_registry_trigger: [{}]".format(event_names.get(trigger_target))) + return function + + def cron_trigger(function): + if not hasattr(function, 'triggers'): + function.triggers = [] + function.triggers.append(CronTrigger(trigger_type, trigger_name=trigger_name).trigger) + LOG.debug("when: Created cron_trigger: [{}]".format(trigger_name)) + return function + + def system_trigger(function): + if not hasattr(function, 'triggers'): + function.triggers = [] + if trigger_target == "started": + function.triggers.append(StartupTrigger(trigger_name=trigger_name).trigger) + else: + function.triggers.append(ShutdownTrigger(trigger_name=trigger_name).trigger) + LOG.debug("when: Created system_trigger: [{}]".format(trigger_name)) + return function + + def thing_trigger(function): + if not hasattr(function, 'triggers'): + function.triggers = [] + if new_state is not None or old_state is not None: + if trigger_type == "changed": + function.triggers.append(ThingStatusChangeTrigger(trigger_target, previous_status=old_state, status=new_state, trigger_name=trigger_name).trigger) + else: + function.triggers.append(ThingStatusUpdateTrigger(trigger_target, status=new_state, trigger_name=trigger_name).trigger) + else: + event_types = "ThingStatusInfoChangedEvent" if trigger_type == "changed" else "ThingStatusInfoEvent" + function.triggers.append(ThingEventTrigger(trigger_target, event_types, trigger_name=trigger_name).trigger) + LOG.debug("when: Created thing_trigger: [{}]".format(trigger_name)) + return function + + def channel_trigger(function): + if not hasattr(function, 'triggers'): + function.triggers = [] + function.triggers.append(ChannelEventTrigger(trigger_target, event=new_state, trigger_name=trigger_name).trigger) + LOG.debug("when: Created channel_trigger: [{}]".format(trigger_name)) + return function + + target_type = None + trigger_target = None + trigger_type = None + old_state = None + new_state = None + trigger_name = None + + if isValidExpression(target): + # a simple cron target was used, so add a default target_type and trigger_target (Time cron XXXXX) + target_type = "Time" + trigger_target = "cron" + trigger_type = target + trigger_name = "Time-cron-{}".format(target) + else: + input_list = split(target) + if len(input_list) > 1: + # target_type trigger_target [trigger_type] [from] [old_state] [to] [new_state] + while input_list: + if target_type is None: + if " ".join(input_list[0:2]) in ["Member of", "Descendent of"]: + target_type = " ".join(input_list[0:2]) + input_list = input_list[2:] + else: + target_type = input_list.pop(0) + elif trigger_target is None: + if target_type == "System" and len(input_list) > 1: + if " ".join(input_list[0:2]) == "shuts down": + trigger_target = "shuts down" + else: + trigger_target = input_list.pop(0) + elif trigger_type is None: + if " ".join(input_list[0:2]) == "received update": + if target_type in ["Item", "Thing", "Member of", "Descendent of"]: + input_list = input_list[2:] + trigger_type = "received update" + else: + raise ValueError("when: \"{}\" could not be parsed. \"received update\" is invalid for target_type \"{}\"".format(target, target_type)) + elif " ".join(input_list[0:2]) == "received command": + if target_type in ["Item", "Member of", "Descendent of"]: + input_list = input_list[2:] + trigger_type = "received command" + else: + raise ValueError("when: \"{}\" could not be parsed. \"received command\" is invalid for target_type \"{}\"".format(target, target_type)) + elif input_list[0] == "changed": + if target_type in ["Item", "Thing", "Member of", "Descendent of"]: + input_list.pop(0) + trigger_type = "changed" + else: + raise ValueError("when: \"{}\" could not be parsed. \"changed\" is invalid for target_type \"{}\"".format(target, target_type)) + elif input_list[0] == "triggered": + if target_type == "Channel": + trigger_type = input_list.pop(0) + else: + raise ValueError("when: \"{}\" could not be parsed. \"triggered\" is invalid for target_type \"{}\"".format(target, target_type)) + elif trigger_target == "cron": + if target_type == "Time": + if isValidExpression(" ".join(input_list)): + trigger_type = " ".join(input_list) + del input_list[:] + else: + raise ValueError("when: \"{}\" could not be parsed. \"{}\" is not a valid cron expression. See http://www.quartz-scheduler.org/documentation/quartz-2.1.x/tutorials/tutorial-lesson-06".format(target, " ".join(input_list))) + else: + raise ValueError("when: \"{}\" could not be parsed. \"cron\" is invalid for target_type \"{}\"".format(target, target_type)) + else: + raise ValueError("when: \"{}\" could not be parsed because the trigger_type {}".format(target, "is missing" if input_list[0] is None else "\"{}\" is invalid".format(input_list[0]))) + else: + if old_state is None and trigger_type == "changed" and input_list[0] == "from": + input_list.pop(0) + old_state = input_list.pop(0) + elif new_state is None and trigger_type == "changed" and input_list[0] == "to": + input_list.pop(0) + new_state = input_list.pop(0) + elif new_state is None and (trigger_type == "received update" or trigger_type == "received command"): + new_state = input_list.pop(0) + elif new_state is None and target_type == "Channel": + new_state = input_list.pop(0) + elif input_list:# there are no more possible combinations, but there is more data + raise ValueError("when: \"{}\" could not be parsed. \"{}\" is invalid for \"{} {} {}\"".format(target, input_list, target_type, trigger_target, trigger_type)) + + else: + # a simple Item target was used (just an Item name), so add a default target_type and trigger_type (Item XXXXX changed) + if target_type is None: + target_type = "Item" + if trigger_target is None: + trigger_target = target + if trigger_type is None: + trigger_type = "changed" + + # validate the inputs, and if anything isn't populated correctly throw an exception + if target_type is None or target_type not in ["Item", "Member of", "Descendent of", "Thing", "Channel", "System", "Time"]: + raise ValueError("when: \"{}\" could not be parsed. target_type is missing or invalid. Valid target_type values are: Item, Member of, Descendent of, Thing, Channel, System, and Time.".format(target)) + elif target_type != "System" and trigger_target not in ["added", "removed", "modified"] and trigger_type is None: + raise ValueError("when: \"{}\" could not be parsed because trigger_type cannot be None".format(target)) + elif target_type in ["Item", "Member of", "Descendent of"] and trigger_target not in ["added", "removed", "modified"] and itemRegistry.getItems(trigger_target) == []: + raise ValueError("when: \"{}\" could not be parsed because Item \"{}\" is not in the ItemRegistry".format(target, trigger_target)) + elif target_type in ["Member of", "Descendent of"] and itemRegistry.getItem(trigger_target).type != "Group": + raise ValueError("when: \"{}\" could not be parsed because \"{}\" was specified, but \"{}\" is not a group".format(target, target_type, trigger_target)) + elif target_type == "Item" and trigger_target not in ["added", "removed", "modified"] and old_state is not None and trigger_type == "changed" and TypeParser.parseState(itemRegistry.getItem(trigger_target).acceptedDataTypes, old_state) is None: + raise ValueError("when: \"{}\" could not be parsed because \"{}\" is not a valid state for \"{}\"".format(target, old_state, trigger_target)) + elif target_type == "Item" and trigger_target not in ["added", "removed", "modified"] and new_state is not None and (trigger_type == "changed" or trigger_type == "received update") and TypeParser.parseState(itemRegistry.getItem(trigger_target).acceptedDataTypes, new_state) is None: + raise ValueError("when: \"{}\" could not be parsed because \"{}\" is not a valid state for \"{}\"".format(target, new_state, trigger_target)) + elif target_type == "Item" and trigger_target not in ["added", "removed", "modified"] and new_state is not None and trigger_type == "received command" and TypeParser.parseCommand(itemRegistry.getItem(trigger_target).acceptedCommandTypes, new_state) is None: + raise ValueError("when: \"{}\" could not be parsed because \"{}\" is not a valid command for \"{}\"".format(target, new_state, trigger_target)) + elif target_type == "Thing" and things.get(ThingUID(trigger_target)) is None:# returns null if Thing does not exist + raise ValueError("when: \"{}\" could not be parsed because Thing \"{}\" is not in the ThingRegistry".format(target, trigger_target)) + elif target_type == "Thing" and old_state is not None and not hasattr(ThingStatus, old_state): + raise ValueError("when: \"{}\" is not a valid Thing status".format(old_state)) + elif target_type == "Thing" and new_state is not None and not hasattr(ThingStatus, new_state): + raise ValueError("when: \"{}\" is not a valid Thing status".format(new_state)) + elif target_type == "Channel" and things.getChannel(ChannelUID(trigger_target)) is None:# returns null if Channel does not exist + raise ValueError("when: \"{}\" could not be parsed because Channel \"{}\" does not exist".format(target, trigger_target)) + elif target_type == "Channel" and things.getChannel(ChannelUID(trigger_target)).kind != ChannelKind.TRIGGER: + raise ValueError("when: \"{}\" could not be parsed because \"{}\" is not a trigger Channel".format(target, trigger_target)) + elif target_type == "System" and trigger_target != "started":# and trigger_target != "shuts down": + raise ValueError("when: \"{}\" could not be parsed. trigger_target \"{}\" is invalid for target_type \"System\". The only valid trigger_type value is \"started\"".format(target, target_type))# and \"shuts down\"".format(target, target_type)) + + LOG.debug("when: target=[{}], target_type={}, trigger_target={}, trigger_type={}, old_state={}, new_state={}".format(target, target_type, trigger_target, trigger_type, old_state, new_state)) + + trigger_name = validate_uid(trigger_name or target) + if target_type in ["Item", "Member of", "Descendent of"]: + if trigger_target in ["added", "removed", "modified"]: + return item_registry_trigger + else: + return item_trigger + elif target_type == "Thing": + return thing_trigger + elif target_type == "Channel": + return channel_trigger + elif target_type == "System": + return system_trigger + elif target_type == "Time": + return cron_trigger + + except ValueError as ex: + LOG.warn(ex) + + def bad_trigger(function): + if not hasattr(function, 'triggers'): + function.triggers = [] + function.triggers.append(None) + return function + + return bad_trigger + + except: + import traceback + LOG.debug(traceback.format_exc()) + +class ItemStateUpdateTrigger(Trigger): + """ + This class builds an ItemStateUpdateTrigger Module to be used when creating a Rule. + + See :ref:`Guides/Rules:Extensions` for examples of how to use these extensions. + + Examples: + .. code-block:: + + MyRule.triggers = [ItemStateUpdateTrigger("MyItem", "ON", "MyItem-received-update-ON").trigger] + MyRule.triggers.append(ItemStateUpdateTrigger("MyOtherItem", "OFF", "MyOtherItem-received-update-OFF").trigger) + + Args: + item_name (string): name of item to watch for updates + state (string): (optional) trigger only when updated TO this state + trigger_name (string): (optional) name of this trigger + + Attributes: + trigger (Trigger): Trigger object to be added to a Rule + """ + def __init__(self, item_name, state=None, trigger_name=None): + trigger_name = validate_uid(trigger_name) + configuration = {"itemName": item_name} + if state is not None: + configuration["state"] = state + self.trigger = TriggerBuilder.create().withId(trigger_name).withTypeUID("core.ItemStateUpdateTrigger").withConfiguration(Configuration(configuration)).build() + +class ItemStateChangeTrigger(Trigger): + """ + This class builds an ItemStateChangeTrigger Module to be used when creating a Rule. + + See :ref:`Guides/Rules:Extensions` for examples of how to use these extensions. + + Examples: + .. code-block:: + + MyRule.triggers = [ItemStateChangeTrigger("MyItem", "OFF", "ON", "MyItem-changed-from-OFF-to-ON").trigger] + MyRule.triggers.append(ItemStateChangeTrigger("MyOtherItem", "ON", "OFF","MyOtherItem-changed-from-ON-to-OFF").trigger) + + Args: + item_name (string): name of item to watch for changes + previous_state (string): (optional) trigger only when changing FROM this + state + state (string): (optional) trigger only when changing TO this state + trigger_name (string): (optional) name of this trigger + + Attributes: + trigger (Trigger): Trigger object to be added to a Rule + """ + def __init__(self, item_name, previous_state=None, state=None, trigger_name=None): + trigger_name = validate_uid(trigger_name) + configuration = {"itemName": item_name} + if state is not None: + configuration["state"] = state + if previous_state is not None: + configuration["previousState"] = previous_state + self.trigger = TriggerBuilder.create().withId(trigger_name).withTypeUID("core.ItemStateChangeTrigger").withConfiguration(Configuration(configuration)).build() + +class ItemCommandTrigger(Trigger): + """ + This class builds an ItemCommandTrigger Module to be used when creating a Rule. + + See :ref:`Guides/Rules:Extensions` for examples of how to use these extensions. + + Examples: + .. code-block:: + + MyRule.triggers = [ItemCommandTrigger("MyItem", "ON", "MyItem-received-command-ON").trigger] + MyRule.triggers.append(ItemCommandTrigger("MyOtherItem", "OFF", "MyOtherItem-received-command-OFF").trigger) + + Args: + item_name (string): name of item to watch for commands + command (string): (optional) trigger only when this command is received + trigger_name (string): (optional) name of this trigger + + Attributes: + trigger (Trigger): Trigger object to be added to a Rule + """ + def __init__(self, item_name, command=None, trigger_name=None): + trigger_name = validate_uid(trigger_name) + configuration = {"itemName": item_name} + if command is not None: + configuration["command"] = command + self.trigger = TriggerBuilder.create().withId(trigger_name).withTypeUID("core.ItemCommandTrigger").withConfiguration(Configuration(configuration)).build() + +class ThingStatusUpdateTrigger(Trigger): + """ + This class builds a ThingStatusUpdateTrigger Module to be used when creating a Rule. + + See :ref:`Guides/Rules:Extensions` for examples of how to use these extensions. + + Examples: + .. code-block:: + + MyRule.triggers = [ThingStatusUpdateTrigger("kodi:kodi:familyroom", "ONLINE").trigger] + + Args: + thing_uid (string): name of the Thing to watch for status updates + status (string): (optional) trigger only when Thing is updated to this + status + trigger_name (string): (optional) name of this trigger + + Attributes: + trigger (Trigger): Trigger object to be added to a Rule. + """ + def __init__(self, thing_uid, status=None, trigger_name=None): + trigger_name = validate_uid(trigger_name) + configuration = {"thingUID": thing_uid} + if status is not None: + configuration["status"] = status + self.trigger = TriggerBuilder.create().withId(trigger_name).withTypeUID("core.ThingStatusUpdateTrigger").withConfiguration(Configuration(configuration)).build() + +class ThingStatusChangeTrigger(Trigger): + """ + This class builds a ThingStatusChangeTrigger Module to be used when creating a Rule. + + See :ref:`Guides/Rules:Extensions` for examples of how to use these extensions. + + Examples: + .. code-block:: + + MyRule.triggers = [ThingStatusChangeTrigger("kodi:kodi:familyroom", "ONLINE", "OFFLINE).trigger] + + Args: + thing_uid (string): name of the Thing to watch for status changes + previous_status (string): (optional) trigger only when Thing is changed + from this status + status (string): (optional) trigger only when Thing is changed to this + status + trigger_name (string): (optional) name of this trigger + + Attributes: + trigger (Trigger): Trigger object to be added to a Rule + """ + def __init__(self, thing_uid, previous_status=None, status=None, trigger_name=None): + trigger_name = validate_uid(trigger_name) + configuration = {"thingUID": thing_uid} + if previous_status is not None: + configuration["previousStatus"] = previous_status + if status is not None: + configuration["status"] = status + self.trigger = TriggerBuilder.create().withId(trigger_name).withTypeUID("core.ThingStatusChangeTrigger").withConfiguration(Configuration(configuration)).build() + +class ChannelEventTrigger(Trigger): + """ + This class builds a ChannelEventTrigger Module to be used when creating a Rule. + + See :ref:`Guides/Rules:Extensions` for examples of how to use these extensions. + + Examples: + .. code-block:: + + MyRule.triggers = [ChannelEventTrigger("astro:sun:local:eclipse#event", "START", "solar-eclipse-event-start").trigger] + + Args: + channel_uid (string): name of the Channel to watch for trigger events + event (string): (optional) trigger only when Channel triggers this + event + trigger_name (string): (optional) name of this trigger + + Attributes: + trigger (Trigger): Trigger object to be added to a Rule + """ + def __init__(self, channel_uid, event=None, trigger_name=None): + trigger_name = validate_uid(trigger_name) + configuration = {"channelUID": channel_uid} + if event is not None: + configuration["event"] = event + self.trigger = TriggerBuilder.create().withId(trigger_name).withTypeUID("core.ChannelEventTrigger").withConfiguration(Configuration(configuration)).build() + +class GenericEventTrigger(Trigger): + """ + This class builds a GenericEventTrigger Module to be used when creating a Rule. + It allows you to trigger on any event that comes through the Event Bus. + It's one of the the most powerful triggers, but it is also the most complicated to configure. + + See :ref:`Guides/Rules:Extensions` for examples of how to use these extensions. + + Examples: + .. code-block:: + + MyRule.triggers = [GenericEventTrigger("smarthome/items/Test_Switch_1/", "ItemStateEvent", "smarthome/items/*", "Test_Switch_1-received-update").trigger] + + Args: + eventSource (string): source to watch for trigger events + event_types (string or list): types of events to watch + event_topic (string): (optional) topic to watch + trigger_name (string): (optional) name of this trigger + + Attributes: + trigger (Trigger): Trigger object to be added to a Rule + """ + def __init__(self, event_source, event_types, event_topic="smarthome/*", trigger_name=None): + trigger_name = validate_uid(trigger_name) + self.trigger = TriggerBuilder.create().withId(trigger_name).withTypeUID("core.GenericEventTrigger").withConfiguration(Configuration({ + "eventTopic": event_topic, + "eventSource": "smarthome/{}/".format(event_source), + "eventTypes": event_types + })).build() + +class ItemEventTrigger(Trigger): + """ + This class is the same as the ``GenericEventTrigger``, but simplifies it a bit for use with Items. + The available Item ``eventTypes`` are: + + .. code-block:: none + + "ItemStateEvent" (Item state update) + "ItemCommandEvent" (Item received Command) + "ItemStateChangedEvent" (Item state changed) + "GroupItemStateChangedEvent" (GroupItem state changed) + + See :ref:`Guides/Rules:Extensions` for examples of how to use these extensions. + + Examples: + .. code-block:: + + MyRule.triggers = [ItemEventTrigger("Test_Switch_1", "ItemStateEvent", "smarthome/items/*").trigger] + + Args: + event_source (string): source to watch for trigger events + event_types (string or list): types of events to watch + event_topic (string): (optional) topic to watch (no need to change + default) + trigger_name (string): (optional) name of this trigger + + Attributes: + trigger (Trigger): Trigger object to be added to a Rule + """ + def __init__(self, event_source, event_types, event_topic="smarthome/items/*", trigger_name=None): + trigger_name = validate_uid(trigger_name) + self.trigger = TriggerBuilder.create().withId(trigger_name).withTypeUID("core.GenericEventTrigger").withConfiguration(Configuration({ + "eventTopic": event_topic, + "eventSource": "smarthome/items/{}/".format(event_source), + "eventTypes": event_types + })).build() + +class ThingEventTrigger(Trigger): + """ + This class is the same as the ``GenericEventTrigger``, but simplifies it a bit for use with Things. + The available Thing ``eventTypes`` are: + + .. code-block:: none + + "ThingAddedEvent" + "ThingRemovedEvent" + "ThingStatusInfoChangedEvent" + "ThingStatusInfoEvent" + "ThingUpdatedEvent" + + See :ref:`Guides/Rules:Extensions` for examples of how to use these extensions. + + Examples: + .. code-block:: + + MyRule.triggers = [ThingEventTrigger("kodi:kodi:familyroom", "ThingStatusInfoEvent").trigger] + + Args: + event_source (string): source to watch for trigger events + event_types (string or list): types of events to watch + event_topic (string): (optional) topic to watch (no need to change + default) + trigger_name (string): (optional) name of this trigger + + Attributes: + trigger (Trigger): Trigger object to be added to a Rule + """ + def __init__(self, thing_uid, event_types, event_topic="smarthome/things/*", trigger_name=None): + trigger_name = validate_uid(trigger_name) + self.trigger = TriggerBuilder.create().withId(trigger_name).withTypeUID("core.GenericEventTrigger").withConfiguration(Configuration({ + "eventTopic": event_topic, + "eventSource": "smarthome/things/{}/".format(thing_uid), + "eventTypes": event_types + })).build() + +EVERY_SECOND = "0/1 * * * * ?" +EVERY_10_SECONDS = "0/10 * * * * ?" +EVERY_MINUTE = "0 * * * * ?" +EVERY_HOUR = "0 0 * * * ?" + +class CronTrigger(Trigger): + """ + This class builds a CronTrigger Module to be used when creating a Rule. + + See :ref:`Guides/Rules:Extensions` for examples of how to use these extensions. + + Examples: + .. code-block:: + + MyRule.triggers = [CronTrigger("0 55 17 * * ?").trigger] + + Args: + cron_expression (string): a valid `cron expression `_ + trigger_name (string): (optional) name of this trigger + + Attributes: + trigger (Trigger): Trigger object to be added to a Rule + """ + def __init__(self, cron_expression, trigger_name=None): + trigger_name = validate_uid(trigger_name) + configuration = { + 'cronExpression': cron_expression + } + self.trigger = TriggerBuilder.create().withId(trigger_name).withTypeUID("timer.GenericCronTrigger").withConfiguration(Configuration(configuration)).build() + +class StartupTrigger(Trigger): + """ + This class builds a StartupTrigger Module to be used when creating a Rule. + + See :ref:`Guides/Rules:Extensions` for examples of how to use these extensions. + + Examples: + .. code-block:: + + MyRule.triggers = [StartupTrigger().trigger] + + Args: + trigger_name (string): (optional) name of this trigger + + Attributes: + trigger (Trigger): Trigger object to be added to a Rule + """ + def __init__(self, trigger_name=None): + trigger_name = validate_uid(trigger_name) + self.trigger = TriggerBuilder.create().withId(trigger_name).withTypeUID("jsr223.StartupTrigger").withConfiguration(Configuration()).build() + +class ItemRegistryTrigger(OsgiEventTrigger): + """ + This class builds an OsgiEventTrigger Module to be used when creating a + Rule. Requires the 100_OsgiEventTrigger.py component script. The available + Item registry ``event_names`` are: + + .. code-block:: none + + "ItemAddedEvent" + "ItemRemovedEvent" + "ItemUpdatedEvent" + + See :ref:`Guides/Rules:Extensions` for examples of how to use these extensions. + + Examples: + .. code-block:: + + MyRule.triggers = [ItemRegistryTrigger("ItemAddedEvent").trigger] + + Args: + event_name (string): name of the event to watch + + Attributes: + trigger (Trigger): Trigger object to be added to a Rule + """ + def __init__(self, event_name): + OsgiEventTrigger.__init__(self) + self.event_name = event_name + + def event_filter(self, event): + return event.get('type') == self.event_name + + def event_transformer(self, event): + return json.loads(event['payload']) + +class ItemAddedTrigger(ItemRegistryTrigger): + """ + This class is the same as the ``ItemRegistryTrigger``, but limited to when + an Item is added. This trigger will fire when any Item is added. + + See :ref:`Guides/Rules:Extensions` for examples of how to use these extensions. + + Examples: + .. code-block:: + + MyRule.triggers = [ItemAddedTrigger().trigger] + + Attributes: + trigger (Trigger): Trigger object to be added to a Rule + """ + def __init__(self): + ItemRegistryTrigger.__init__(self, "ItemAddedEvent") + +class ItemRemovedTrigger(ItemRegistryTrigger): + """ + This class is the same as the ``ItemRegistryTrigger``, but limited to when + an Item is removed. This trigger will fire when any Item is removed. + + See :ref:`Guides/Rules:Extensions` for examples of how to use these extensions. + + Examples: + .. code-block:: + + MyRule.triggers = [ItemRemovedTrigger().trigger] + + Attributes: + trigger (Trigger): Trigger object to be added to a Rule + """ + def __init__(self): + ItemRegistryTrigger.__init__(self, "ItemRemovedEvent") + +class ItemUpdatedTrigger(ItemRegistryTrigger): + """ + This class is the same as the ``ItemRegistryTrigger``, but limited to when + an Item is updated. This trigger will fire when any Item is updated. + + See :ref:`Guides/Rules:Extensions` for examples of how to use these extensions. + + Examples: + .. code-block:: + + MyRule.triggers = [ItemUpdatedTrigger().trigger] + + Attributes: + trigger (Trigger): Trigger object to be added to a Rule + """ + def __init__(self): + ItemRegistryTrigger.__init__(self, "ItemUpdatedEvent") + +class DirectoryEventTrigger(Trigger): + """ + This class builds a DirectoryEventTrigger Module to be used when creating a + Rule. Requires the 100_DirectoryTrigger.py component script. + + See :ref:`Guides/Rules:Extensions` for examples of how to use these extensions. + + Args: + path (string): path of the directory to watch + event_kinds (list): (optional) list of the events to watch for + watch_subdirectories (Boolean): (optional) True will watch + subdirectories + + Attributes: + trigger (Trigger): Trigger object to be added to a Rule + """ + def __init__(self, path, event_kinds=[ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY], watch_subdirectories=False): + trigger_name = validate_uid(type(self).__name__) + configuration = { + 'path': path, + 'event_kinds': str(event_kinds), + 'watch_subdirectories': watch_subdirectories, + } + self.trigger = TriggerBuilder.create().withId(trigger_name).withTypeUID(core.DIRECTORY_TRIGGER_MODULE_ID).withConfiguration(Configuration(configuration)).build() diff --git a/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/utils$py.class b/bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/src/main/resources/Lib/core/utils$py.class new file mode 100755 index 0000000000000000000000000000000000000000..ade099d7dcd1eaac3798ca723f1ee81e72117e38 GIT binary patch literal 19012 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 0) + +def getItemValue(item_or_item_name, default_value): + """ + Returns the Item's value if the Item exists and is initialized, otherwise + returns the default value. ``itemRegistry.getItem`` will return an object + for uninitialized items, but it has less methods. ``itemRegistry.getItem`` + will throw an ItemNotFoundException if the Item is not in the registry. + + Args: + item_or_item_name (Item or str): name of the Item + default_value (int, float, ON, OFF, OPEN, CLOSED, str, DateTime): the default + value + + Returns: + int, float, ON, OFF, OPEN, CLOSED, str, DateTime, or None: the state if + the Item converted to the type of default value, or the default + value if the Item's state is NULL or UNDEF + """ + item = itemRegistry.getItem(item_or_item_name) if isinstance(item_or_item_name, basestring) else item_or_item_name + if isinstance(default_value, int): + return item.state.intValue() if item.state not in [NULL, UNDEF] else default_value + elif isinstance(default_value, float): + return item.state.floatValue() if item.state not in [NULL, UNDEF] else default_value + elif default_value in [ON, OFF, OPEN, CLOSED]: + return item.state if item.state not in [NULL, UNDEF] else default_value + elif isinstance(default_value, str): + return item.state.toFullString() if item.state not in [NULL, UNDEF] else default_value + elif isinstance(default_value, DateTime): + # We return a org.joda.time.DateTime from a org.eclipse.smarthome.core.library.types.DateTimeType + return DateTime(item.state.calendar.timeInMillis) if item.state not in [NULL, UNDEF] else default_value + else: + LOG.warn("The type of the passed default value is not handled") + return None + +def getLastUpdate(item_or_item_name): + """ + Returns the Item's last update datetime as an 'org.joda.time.DateTime `_. + + Args: + item_or_item_name (Item or str): name of the Item + + Returns: + DateTime: DateTime representing the time of the Item's last update + """ + try: + item = itemRegistry.getItem(item_or_item_name) if isinstance(item_or_item_name, basestring) else item_or_item_name + last_update = PersistenceExtensions.lastUpdate(item) + if last_update is None: + LOG.warning("No existing lastUpdate data for item: [{}], so returning 1970-01-01T00:00:00Z".format(item.name)) + return DateTime(0) + return last_update.toDateTime() + except: + # There is an issue using the StartupTrigger and saving scripts over SMB, where changes are detected before the file + # is completely written. The first read breaks because of a partial file write and the second read succeeds. + LOG.warning("Exception when getting lastUpdate data for item: [{}], so returning 1970-01-01T00:00:00Z".format(item.name)) + return DateTime(0) + +def sendCommand(item_or_item_name, new_value): + """ + Sends a command to an item regardless of its current state. + + Args: + item_or_item_name (Item or str): name of the Item + new_value (Command): Command to send to the Item + """ + item = itemRegistry.getItem(item_or_item_name) if isinstance(item_or_item_name, basestring) else item_or_item_name + events.sendCommand(item, new_value) + +def postUpdate(item_or_item_name, new_value): + """ + Posts an update to an item regardless of its current state. + + Args: + item_name (Item or str): Item or name of the Item + new_value (State): state to update the Item with + """ + item = itemRegistry.getItem(item_or_item_name) if isinstance(item_or_item_name, basestring) else item_or_item_name + events.postUpdate(item, new_value) + +def post_update_if_different(item_or_item_name, new_value, sendACommand=False, floatPrecision=None): + """ + Checks if the current state of the item is different than the desired new + state. If the target state is the same, no update is posted. + + sendCommand vs postUpdate: + If you want to tell something to change (turn a light on, change the + thermostat to a new temperature, start raising the blinds, etc.), then you + want to send a command to an Item using sendCommand. If your Items' states + are not being updated by a binding, the autoupdate feature or something + else external, you will probably want to update the state in a rule using + postUpdate. + + Unfortunately, most decimal fractions cannot be represented exactly as + binary fractions. A consequence is that, in general, the decimal + floating-point numbers you enter are only approximated by the binary + floating-point numbers actually stored in the machine. Therefore, + comparing the stored value with the new value will most likely always + result in a difference. You can supply the named argument floatPrecision + to round the value before comparing. + + Args: + item_or_item_name (Item or str): name of the Item + new_value (State or Command): state to update the Item with, or Command + if using sendACommand (must be of a type supported by the Item) + sendACommand (Boolean): (optional) ``True`` to send a command instead + of an update + floatPrecision (int): (optional) the precision of the Item's state to + use when comparing values + + Returns: + bool: ``True``, if the command or update was sent, else ``False`` + """ + compare_value = None + item = itemRegistry.getItem(item_or_item_name) if isinstance(item_or_item_name, basestring) else item_or_item_name + + if sendACommand: + compare_value = TypeParser.parseCommand(item.acceptedCommandTypes, str(new_value)) + else: + compare_value = TypeParser.parseState(item.acceptedDataTypes, str(new_value)) + + if compare_value is not None: + if item.state != compare_value or (isinstance(new_value, float) and floatPrecision is not None and round(item.state.floatValue(), floatPrecision) != new_value): + if sendACommand: + sendCommand(item, new_value) + LOG.debug("New sendCommand value for [{}] is [{}]".format(item.name, new_value)) + else: + postUpdate(item, new_value) + LOG.debug("New postUpdate value for [{}] is [{}]".format(item.name, new_value)) + return True + else: + LOG.debug("Not {} {} to {} since it is the same as the current state".format("sending command" if sendACommand else "posting update", new_value, item.name)) + return False + else: + LOG.warn("[{}] is not an accepted {} for [{}]".format(new_value, "command type" if sendACommand else "state", item.name)) + return False + +# backwards compatibility +postUpdateCheckFirst = post_update_if_different + +def send_command_if_different(item_or_item_name, new_value, floatPrecision=None): + """ + See postUpdateCheckFirst + """ + return postUpdateCheckFirst(item_or_item_name, new_value, sendACommand=True, floatPrecision=floatPrecision) + +# backwards compatibility +sendCommandCheckFirst = send_command_if_different + +def validate_item(item_or_item_name): + """ + This function validates whether an Item exists or if an Item name is valid. + + Args: + item_or_item_name (Item or str): name of the Item + + Returns: + Item or None: None, if the Item does not exist or the Item name is not + in a valid format, else validated Item + """ + item = item_or_item_name + if isinstance(item, basestring): + if itemRegistry.getItems(item) == []: + LOG.warn("[{}] is not in the ItemRegistry".format(item)) + return None + else: + item = itemRegistry.getItem(item_or_item_name) + elif not hasattr(item_or_item_name, 'name'): + LOG.warn("[{}] is not a Item or string".format(item)) + return None + + if itemRegistry.getItems(item.name) == []: + LOG.warn("[{}] is not in the ItemRegistry".format(item.name)) + return None + + return item + +def validate_channel_uid(channel_uid_or_string): + """ + This function validates whether a ChannelUID exists or if a ChannelUID is + valid. + + Args: + channel_uid_or_string (ChannelUID or string): the ChannelUID + + Returns: + ChannelUID or None: None, if the ChannelUID does not exist or the + ChannelUID is not in a valid format, else validated ChannelUID + """ + channel_uid = channel_uid_or_string + if isinstance(channel_uid_or_string, basestring): + channel_uid = ChannelUID(channel_uid_or_string) + elif not isinstance(channel_uid_or_string, ChannelUID): + LOG.warn("[{}] is not a string or ChannelUID".format(channel_uid_or_string)) + return None + if things.getChannel(channel_uid) is None: + LOG.warn("[{}] is not a valid Channel".format(channel_uid)) + return None + return channel_uid + +def validate_uid(uid): + """ + This function validates UIDs. + + Args: + uid (string or None): the UID to validate or None + + Returns: + string: a valid UID + """ + if uid is None: + uid = uuid.uuid1().hex + else: + uid = re.sub(r"[^A-Za-z0-9_-]", "_", uid) + uid = "{}_{}".format(uid, uuid.uuid1().hex) + if not re.match("^[A-Za-z0-9]", uid):# in case the first character is still invalid + uid = "{}_{}".format("jython", uid) + uid = re.sub(r"__+", "_", uid) + return uid diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineFactory.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineFactory.java index ee6d729d236..001ef7d652e 100644 --- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineFactory.java +++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineFactory.java @@ -16,7 +16,6 @@ import java.util.Map; import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -32,7 +31,7 @@ @NonNullByDefault public interface ScriptEngineFactory { - static final ScriptEngineManager ENGINE_MANAGER = new ScriptEngineManager(); + static final javax.script.ScriptEngineManager ENGINE_MANAGER = new javax.script.ScriptEngineManager(); /** * This method returns a list of file extensions and MimeTypes that are supported by the ScriptEngine, e.g. py, diff --git a/bundles/pom.xml b/bundles/pom.xml index cfbdf2eae0b..6ada8c5e657 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -22,6 +22,7 @@ org.openhab.core.automation.module.media org.openhab.core.automation.module.script org.openhab.core.automation.module.script.rulesupport + org.openhab.core.automation.module.script.scriptenginefactory.jython org.openhab.core.automation.rest org.openhab.core.config.core org.openhab.core.config.discovery diff --git a/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml index f0b17bec7f3..3d51d4c7cff 100644 --- a/features/karaf/openhab-core/src/main/feature/feature.xml +++ b/features/karaf/openhab-core/src/main/feature/feature.xml @@ -90,6 +90,13 @@ mvn:org.openhab.core.bundles/org.openhab.core.automation.module.script.rulesupport/${project.version} + + openhab-core-base + openhab-core-automation + openhab-core-automation-module-script + mvn:org.openhab.core.bundles/org.openhab.core.automation.module.script.scriptenginefactory.jython/${project.version} + + openhab-core-base openhab-core-automation