From 51e0b0e317b8096d6e9d18a44d5c5f534c43c005 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Tue, 9 May 2023 18:42:16 +0200 Subject: [PATCH] [udf] Unlock JavaScript for user-defined functions --- CHANGES.rst | 1 + .../owntracks-battery/mqttwarn-owntracks.js | 27 +++++++++++++++++ examples/owntracks-battery/readme.md | 20 +++++++++++++ mqttwarn/util.py | 30 +++++++++++++++++++ setup.py | 1 + 5 files changed, 79 insertions(+) create mode 100644 examples/owntracks-battery/mqttwarn-owntracks.js diff --git a/CHANGES.rst b/CHANGES.rst index 2e8691e0..e7e055f0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,6 +12,7 @@ in progress As per RFC 7568, SSLv3 has been deprecated in 2015 already. - Tests: Add more test cases to increase mqttwarn core coverage to ~100% - Improve example "Forward OwnTracks low-battery warnings to ntfy" +- [udf] Unlock JavaScript for user-defined functions 2023-04-28 0.34.0 diff --git a/examples/owntracks-battery/mqttwarn-owntracks.js b/examples/owntracks-battery/mqttwarn-owntracks.js new file mode 100644 index 00000000..4c098dc4 --- /dev/null +++ b/examples/owntracks-battery/mqttwarn-owntracks.js @@ -0,0 +1,27 @@ +/** + * + * Forward OwnTracks low-battery warnings to ntfy. + * https://mqttwarn.readthedocs.io/en/latest/examples/owntracks-battery/readme.html + * + */ + +function owntracks_batteryfilter(topic, message) { + let ignore = true; + let data; + try { + data = JSON.parse(message); + } catch { + data = null; + } + if (data && "batt" in data && data["batt"] !== null) { + ignore = Number.parseFloat(data["batt"]) > 20 + } + + return ignore; +} + +module.exports = { + "owntracks_batteryfilter": owntracks_batteryfilter, +}; + +console.log("Loaded JavaScript module."); diff --git a/examples/owntracks-battery/readme.md b/examples/owntracks-battery/readme.md index 3992add8..972bd865 100644 --- a/examples/owntracks-battery/readme.md +++ b/examples/owntracks-battery/readme.md @@ -124,12 +124,32 @@ a different URL, and make sure to restart mqttwarn afterwards. targets = {'testdrive': 'http://localhost:5555/testdrive'} ``` +### Using JavaScript +You can alternatively use JavaScript to implement user-defined functions, based on the +[JSPyBridge] package. + +To try that, use the alternative `mqttwarn-owntracks.js` implementation by adjusting +the `functions` setting within the `[defaults]` section of your configuration. +```ini +[defaults] +functions = mqttwarn-owntracks.js +``` + +The JavaScript function `owntracks_batteryfilter()` implements the same rule as the +previous one, which was written in Python. + +:::{literalinclude} mqttwarn-owntracks.js +:language: javascript +::: + + ### Backlog :::{todo} - [o] Define battery threshold level within the configuration file. ::: +[JSPyBridge]: https://pypi.org/project/javascript/ [Mosquitto]: https://mosquitto.org [ntfy.sh]: https://ntfy.sh/ [OwnTracks]: https://owntracks.org diff --git a/mqttwarn/util.py b/mqttwarn/util.py index 7ae49b47..c58d72a7 100644 --- a/mqttwarn/util.py +++ b/mqttwarn/util.py @@ -190,12 +190,17 @@ def load_functions(filepath: t.Optional[str] = None) -> t.Optional[types.ModuleT mod_name, file_ext = os.path.splitext(os.path.split(filepath)[-1]) + logger.info(f"Loading functions module {mod_name} from {filepath}") + if file_ext.lower() == ".py": py_mod = imp.load_source(mod_name, filepath) elif file_ext.lower() == ".pyc": py_mod = imp.load_compiled(mod_name, filepath) + elif file_ext.lower() in [".js", ".javascript"]: + py_mod = load_source_js(mod_name, filepath) + else: raise ValueError("'{}' does not have the .py or .pyc extension".format(filepath)) @@ -251,3 +256,28 @@ def load_file(path: t.Union[str, Path], retry_tries=None, retry_interval=0.075, except: # pragma: nocover pass return reader + + +def module_factory(name, variables): + """ + Create a synthetic Python module object. + + Derived from: + https://www.oreilly.com/library/view/python-cookbook/0596001673/ch15s03.html + """ + module = imp.new_module(name) + module.__dict__.update(variables) + module.__file__ = "" + return module + + +def load_source_js(mod_name, filepath): + """ + Load a JavaScript module, and import its exported symbols into a synthetic Python module. + """ + import javascript + + js_code = load_file(filepath, retry_tries=0).read().decode("utf-8") + module = {} + javascript.eval_js(js_code) + return module_factory(mod_name, module["exports"]) diff --git a/setup.py b/setup.py index c7596301..18a29646 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,7 @@ "funcy<3", "future>=0.18.0,<1", "importlib-metadata; python_version<'3.8'", + "javascript==1!1.0.1", "jinja2<4", "paho-mqtt<2", "requests<3",