Marketplace versioning with embedded resource

I didn’t have it available/installed, but I figured out how to include it - but it gave me the GraalVM error, which I think is likely related to the Java version in use. Eclipse use different JVMs for different projects, which is why there can be some “complications”. But, there can be other causes, like some missing dependency, I really don’t know much about GraalVM.

The “demo app” is very different indeed, but it’s also brilliant. It lets you run OH directly from Eclipse, compiling only what you have changed. So, I can change some Java code in OH and press F11, and voila, it runs using the new code. That makes it worth some hassle.

The problem with the “demo app” is that it’s very sparsely documented, and I suspect that it’s not being kept properly up-to-date when things change in OH. So, it can be a bit of a handful to get to “play ball”, but I can’t quiet imagine doing development another way. The path from change to test is so short, and you also have an integrated debugger so that you can put breakpoints whereever you want and step through exactly what happens.

Ok, I understand what you mean. The categorization in hardcoded in MainUI, so it must be changed there. It should be quite simple to do though, here is the bindings part (all the rest is in the same file):

I’d think that @florian-h05 could fix that in a breeze :wink:

Actually, when looking some more at the categorization, I realize that there’s a problem. MainUI needs some information to base the categorizaton on. It’s not immediately obvious to me what this filter criteria should be, considering the information available from the REST API.

Here are the criteria that are used to divide the add-ons into “groups”:

    allAddons () {
      return Object.keys(this.addons).flatMap((k) => this.addons[k]).filter((a) => this.isInFilter(a))
    },
    installedAddons () {
      return this.allAddons.filter((a) => a.installed)
    },
    suggestedAddons () {
      return this.allAddons.filter((a) => !a.installed && this.suggestions.some((s) => s.id === a.id)).filter((a) => this.isInFilter(a))
    },
    unsuggestedAddons () {
      return this.allAddons.filter((a) => !this.suggestedAddons.includes(a)).filter((a) => this.isInFilter(a))
    },
    officialAddons () {
      return Object.keys(this.addons).filter((k) => k === 'eclipse' || k === 'karaf').flatMap((k) => this.addons[k]).filter((a) => this.isInFilter(a)).filter((a) => !this.suggestedAddons.includes(a))
    },
    marketplaceAddons () {
      return this.addons.marketplace.filter((a) => !this.suggestedAddons.includes(a)).filter((a) => this.isInFilter(a))
    },
    otherAddons () {
      return Object.keys(this.addons).filter((k) => k !== 'eclipse' && k !== 'karaf' && k !== 'marketplace').flatMap((k) => this.addons[k]).filter((a) => this.isInFilter(a)).filter((a) => !this.suggestedAddons.includes(a))
    },

I did a quick tweak to MainUI to test - I’m not saying this is the best way to categorize them, but I just applied a similar logic as is to bindings:

 .../web/src/pages/addons/addons-store.vue               | 17 ++++++++++++++---
 1 file changed, 14 insertions(+), 3 deletions(-)

diff --git a/bundles/org.openhab.ui/web/src/pages/addons/addons-store.vue b/bundles/org.openhab.ui/web/src/pages/addons/addons-store.vue
index 956b092c..ecc437fe 100644
--- a/bundles/org.openhab.ui/web/src/pages/addons/addons-store.vue
+++ b/bundles/org.openhab.ui/web/src/pages/addons/addons-store.vue
@@ -205,10 +205,21 @@
         <addons-section
           v-if="addons && officialAddons"
           @addonButtonClick="addonButtonClick"
-          :addons="unsuggestedAddons.filter((a) => a.type === 'transformation')" :show-all="true"
+          :addons="officialAddons.filter((a) => a.type === 'transformation')" :show-all="true"
           :featured="['transformation-jsonpath', 'transformation-map', 'transformation-regex']"
-          :title="'Transformation Add-ons'"
-          :subtitle="'Translate raw values into processed or human-readable representations'" />
+          :title="'openHAB Distribution'"
+          :subtitle="'Official transformations maintained by the openHAB project'" />
+        <addons-section
+          v-if="addons && addons.marketplace"
+          @addonButtonClick="addonButtonClick"
+          :addons="marketplaceAddons.filter((a) => a.type === 'transformation')"
+          :title="'Community Marketplace'"
+          :subtitle="'Transformations independently released by the community'" />
+        <addons-section
+          v-if="otherAddons && otherAddons.length"
+          @addonButtonClick="addonButtonClick"
+          :addons="otherAddons.filter((a) => a.type === 'transformation')"
+          :title="'Other Transformations'" />
       </f7-tab>
 
       <f7-tab id="voice" @tabShow="onTabShow">

This results in the transformations being shown like this:


It seems like the installed add-ons are also shown under “Other” for some reason, but I have observed that issue with bindings as well (at least some). So, there should probably be some more filtering done on otherAddons().

(Please not that I had to do two screenshots, so “Community Marketplace” is not listed twice as it might appear)

I figured out why some addons were also listen under “Other”, it again has to do with some weirdness of the “demo app”. The “Other” category consists of all add-ons that aren’t of type “eclipse”, “karaf” or “marketplace”:

    otherAddons () {
      return Object.keys(this.addons).filter((k) => k !== 'eclipse' && k !== 'karaf' && k !== 'marketplace').flatMap((k) => this.addons[k]).filter((a) => this.isInFilter(a)).filter((a) => !this.suggestedAddons.includes(a))
    },

In reality that means JARs dropped in the addons folder and those available from a JSON marketplace. For some magical reason, the add-ons provided via Maven/Eclipse are registered both as “eclipse” and as “jar” add-ons when running the “demo app”. So, that’s not actually a “problem”, it’s just another one of those “weirdnesses” that applies to the “demo app”.

So, instead, I started a “webui dev server” (npm start) with my tweaked code that works against the REST API of my production OH installation. There I have nothing under “Other”, and I haven’t enabled “show incompatible & unpublished”, which means that this is the view I get with my above tweak.

There’s no possibility to categorize them “by type of transformation” without changing things in Core as far as I can see. The way it is done with “automation” is that contentType is used to categorize by. Unfortunately (?), all transformations share the same contentType, so that’s not on option:

public class MarketplaceConstants {
    public static final String JAR_CONTENT_TYPE = "application/vnd.openhab.bundle";
    public static final String KAR_CONTENT_TYPE = "application/vnd.openhab.feature;type=karfile";
    public static final String RULETEMPLATES_CONTENT_TYPE = "application/vnd.openhab.ruletemplate";
    public static final String UIWIDGETS_CONTENT_TYPE = "application/vnd.openhab.uicomponent;type=widget";
    public static final String BLOCKLIBRARIES_CONTENT_TYPE = "application/vnd.openhab.uicomponent;type=blocks";
    public static final String TRANSFORMATIONS_CONTENT_TYPE = "application/vnd.openhab.transformation";
    public static final String JAR_DOWNLOAD_URL_PROPERTY = "jar_download_url";
    public static final String KAR_DOWNLOAD_URL_PROPERTY = "kar_download_url";
    public static final String JSON_DOWNLOAD_URL_PROPERTY = "json_download_url";
    public static final String YAML_DOWNLOAD_URL_PROPERTY = "yaml_download_url";
}

The contentTypes could be changed of course, but that would have to be done in Core, and there would have to be a meaningful way to assign the correct type without first having to parse the content of the add-on. Once there is a way to differentiate them, it would be easy to add a “type extension” also for transformations, e.g application/vnd.openhab.transformation;type=map. But these “structural changes” would be needed to have the necessary information available for MainUI to tell them apart.

Unfortunately, the way this is currently done is a bit “fishy”. Core has the concept of AddonType (new AddonTypes can be created dynamically by code, these are the default ones):

    public static final AddonType AUTOMATION = new AddonType("automation", "Automation");
    public static final AddonType BINDING = new AddonType("binding", "Bindings");
    public static final AddonType MISC = new AddonType("misc", "Misc");
    public static final AddonType PERSISTENCE = new AddonType("persistence", "Persistence");
    public static final AddonType TRANSFORMATION = new AddonType("transformation", "Transformations");
    public static final AddonType UI = new AddonType("ui", "User Interfaces");
    public static final AddonType VOICE = new AddonType("voice", "Voice");

..while the categories come straight from Discourse as integer values:

    private static final Integer BUNDLES_CATEGORY = 73;
    private static final Integer RULETEMPLATES_CATEGORY = 74;
    private static final Integer UIWIDGETS_CATEGORY = 75;
    private static final Integer BLOCKLIBRARIES_CATEGORY = 76;
    private static final Integer TRANSFORMATIONS_CATEGORY = 80;

This is then all “mapped” like this:

    private @Nullable AddonType getAddonType(@Nullable Integer category, List<String> tags) {
        // check if we can determine the addon type from the category
        if (TRANSFORMATIONS_CATEGORY.equals(category)) {
            return AddonType.TRANSFORMATION;
        } else if (RULETEMPLATES_CATEGORY.equals(category)) {
            return AddonType.AUTOMATION;
        } else if (UIWIDGETS_CATEGORY.equals(category)) {
            return AddonType.UI;
        } else if (BLOCKLIBRARIES_CATEGORY.equals(category)) {
            return AddonType.AUTOMATION;
        } else if (BUNDLES_CATEGORY.equals(category)) {
            // try to get it from tags if we have tags
            return AddonType.DEFAULT_TYPES.stream().filter(type -> tags.contains(type.getId())).findFirst()
                    .orElse(null);
        }

        // or return null
        return null;
    }

To sum it up, all that can be done without some restructuring in Core, is to separate between “official”, “markedplace” and “other” similar to my tweak, as far as I can understand.

I’ve found the bug causing the retrieval of managed transformations from storage to fail, and created a PR since I hope it’s small enough for the exception:

https://github.com/openhab/openhab-core/pull/4574

If this is merged and later released, posting transformations on the marketplace should work as far as I can tell. I consider organizing how they are presented more of a “cosmetic issue”, and I’ve also suggested how that can be at least improved. I can’t create a PR for that, as the changes are “too big”, but it should be easy for anybody interested to do this.

I had to remove the { } for the YAML parser to accept it, but other than that it does indeed install:

Furthermore, running this script

var test = transform("DSL", "config:dsl:doNothing", "test")

..results in this log entry:

07:09:58.026 [INFO ] [.openhab.core.model.script.transform] - this transform does nothing

All in all, I’d say that it seems like it works :slight_smile:

Do you want to create a PR to openhab-webui? I currently don‘t have the time to do so.

There’s the “CLA complication”, so it’s not straight forward for me. Also, this was more a suggestions - I’m not sure if it helps much to just categorize them by source, as long as all “types” will stay mixed. Maybe @rlkoshak has an opinion an as to what’s the best solution.

The number of 3rd part transformation bundles from any source is likely to remain small. I’m not sure there’s are any on the OH marketplace and only a couple on smartphone/j. Ideally I’d like to see the bundles and transformations be separate but just separating by source would be an improvement in usability and consistency.

I’ve explained what I’ve found above, I get that it might be a bit information overload, so I don’t know if you have read it, but in essence: Making a more detailed categorization is probabaly (I say probably because the devil is in the detail as to how Core would assign the different “types”) achievable, but it’s some more work. I could probably figure out how to do it, but I’m a bit hesitant to change things that I don’t know the full implications of changing.

As such, I feel like somebody with more knowledge about Core should share their view on potential consequences of changing the contentType. Also, I don’t know if other UIs than MainUI might use this information.

Categorizing them by source is quick and easy, since the necessary information is already available. If you think that “will do” for now (this can always be enhanced later), you could apply the diff I pasted above to the file in question, review the title texts (I just edited them quickly without much thought) and create a PR for MainUI.

I can say with certainty that BasicUI, HABPanal, Android app, and IOS App do not. I can’t think of any reason why ComitVisu would use it.

Probably not for a week or two would I have time to do this. But it’s definitely better than nothing.

1 Like

When looking a bit more at transformations, I think there’s a “terminology problem” here. I see two different conceps that are both referred to as “transformations”:

  1. What is some places called a “transformation service” or “transformation type”, which always comes as a bundle as far as I can understand, as it must be Java code that implements TransformationService. I TransformationService must also register a “code” that identifies that service, e.g @Component(property = { "openhab.transform=REGEX" }).
  2. A “transformation” that uses a “transformation service”, and is fundamentally a kind of “configuration” for said “transformation service”. It must use the regisitered “code” for the “transformation service”, and is implemented in Java as a Transformation. It basically has 4 fields:
    private final String uid;
    private final String label;
    private final String type;
    private final Map<String, String> configuration;

In Core, these things are clearly distinctive, one being a TransformationService and one being a Transformation, but in the UI (maybe just in the marketplace, I don’t have the full overview) they are not. This is probably where the distinction should be made, also regarding their categorization. A Transformation will always (?) depend on a TransformationService, except for DSL and script languages it seems, which also seems to be TransformationServices of their own.

Confusingly, there’s another class in Core called Transformation, which contains the methods transform() and transformRaw(). These refer to the “transformation service codes” as “types”:

    /**
     * Applies a transformation of a given type with some function to a value.
     *
     * @param type the transformation type, e.g. REGEX or MAP
     * @param function the function to call, this value depends on the transformation type
     * @param value the value to apply the transformation to
     * @return the transformed value or the original one, if there was no service registered for the
     *         given type or a transformation exception occurred.
     */
    public static @Nullable String transform(String type, String function, String value) {

This class has “action” in its namespace, so assume that this is something that can be called from scripts, and that serves as a link between scripts and “transformation services”..?

What has confused me is how the “scripts” also become TransformationServices, but I think I’ve found it: ScriptTransformationService. According to its documentation, it acts like a “bridge” between scripts and transformation service:

/**
 * The {@link ScriptTransformationService} implements a {@link TransformationService} using any available script
 * language
 *
 * @author Jan N. Klug - Initial contribution
 */

It seems to register “scriptType” in upper case as the “TransformationService code”:

properties.put(TransformationService.SERVICE_PROPERTY_NAME, scriptType.get().toUpperCase());

So, to sum it up, it seems that a “transformation” (in Core sense) will always depend on either a “transformation service” or a “script language”. It’s still not clear to me if every script will automatically be a ScriptTransformationService, or if there could potentially be “script languages” that aren’t.

When all these concepts are referred to merely as “transformations”, it’s bound to be some confusion. I think it is necessary to come up with some new terminology (or use that from Core) so that it’s possible for users to understand what is what.

For now, Rules DSL is part of core. There is some motivation to make it an add-on.

The Script transformation service is also a part of core and let’s us write transformation scripts in any supported rules language. For now, because Rules DSL is part of core, everyone gets DSL transformation support out-of-the-box. But that’s not guaranteed forever. If Rules DSL is moved to an add-on, you’ll have to install the add-on same as you do for JS, et al.

That’s the class that let’s once invoke a transformation from a rule.

Yes.

I don’t know but I know the intent is that they all would, where feasible. I didn’t know if it’s feasible for the JRule add-on on the marketplace, for example and Blockly needs UI support to be used.

I’d like to see Blockly support eventually.

On the forum and I’m the docs the coming used terminology are:

  • transformation “add-on”: “did you install the REFEX add-on?”
  • transformation: “here’s a .map transformation, put it in your $OH_CONF/transform folder”
  • transformation profile: “navigate to the item channel link and select transform from the list of profiles…”
  • transformation action: “You can extract that element from the JSON in your rules with the line transformation("json", "$.foo.bar", newState)

I’ve seen lots of places where the terminology in OH causes problems, but when it comes to “transformation” it’s always clear from the context what is being referred to. When the add-on or bundle is referenced, that’s always mentioned. When the profile or the action is begging referred to, that’s always mentioned. In all cases the context makes it pretty clear what’s being talked about.

Note, only the second bullet point in this list will be published to the transformations category on the marketplace. The "add-on"s would be published to the bundles section of the marketplace and that is already handled just fine.

The code is a bit complicated to follow, but for now it seems to me like anything implementing ScriptEngineFactory will automatically become a TransformationService as well.

A quick grep of the official add-ons folder for ScriptEngineFactory returns: Groovy, JRuby, Graal, Nashorn and Jython. In Core I find DSL only. There’s no reason that I can see why things installed through the marketplace wouldn’t also “qualify”. When it comes to Blocky, I don’t know where it “lives” (is it an add-on or is it a part of Core?), but since it doesn’t show up with grep, chances are that it doesn’t implement ScriptEngineFactory, and thus won’t be “automatically registered” by the ScriptTransformationService.

Yes, I think that is problematic, since “transformation” here means two different things. The first is a “transformation service”, the second is a “transformation”. Using “add-on” as a substitute for “service” just doesn’t work as I see it, partly because the word “add-on” is very generic and can mean different things to different people, and partly because Core defines everything that can be added via the marketplace (or via JAR/KAR files) as “add-ons”. These conflicting terms are bound to clash. Using “bundle” doesn’t make much sense either, as that’s strictly a OSGi term that doesn’t mean much to people not familiar with OSGi.

I personally find this confusing as well, but that has more to do with the general relationship between “transformation” and “profile”. I now understand a “transformation profile” as a profile (a “transform” that you can apply to a channel link) that is in fact a “transformation” under the hood. There are “profiles” that aren’t “transformation”. But, I’ve been digging into this for a couple of days now, and I think the threshold to understand the concepts is too high.

This confuses me as well, but that’s more because I still don’t think that I fully grasp the term “acion” in OH. I know Actions implemented by bindings that can be executed either from MainUI (from 4.3.0 on it the parameters are within a norm) or from rules. But, it’s not clear to me if a “transformation action” is essentially the same thing, and if there are other kind of “actions”.

I think it can be very hard to see how confusing things can be at times for people that are already familiar with all the concepts. Terms/concepts often cross-reference each other, and you cannot stop and look up every term you come across that you’re not 100% sure of when reading documentation. You have to be able to read through some sections and, at least to some extent, infer the meaning of unknown terms. You can then later read about the other terms/concepts, but you need to be able to store it somewhere in your brain in the meanwhile. When you already know what everything means, this process doesn’t take place when you read, and thus “it’s not a problem”. That’s why I think it’s helpful if terms are at least somewhat intuitive, as it helps smooth this process. The more you “misinfer” things, the more difficult it will be for you to finally “see the light” and make all the pieces fall in place.

But, I digress, all I can say is that for me, these terms are confusing in the sense that I don’t manage to infer them correctly from context and end up being “confused” (a feeling of “not having the overview”).

That’s incorrect, and I think that highlights the “terms problem” I’m trying to point out. This marketplace entry for example, shows up under “Transformations” - as it should. There are no category for “bundles” in the marketplace browser, although there is on the forum. But, because this “bundle” is in fact a “transformation service”, it shows up under “Transformations”, together with all the other “transformation services” and “transformations”.

When thinking about your wish to have a better organization/categorization in the marketplace browser for transformations, I think this is what it boils down to, and I agree: These are different things and should be presented as such. The fact that all the official transformation entries are bundles, and most of those found on the marketplace probably won’t be, isn’t enough. They should be separated by the role they play, which is fundamentally different. The problem of course, is that “script transformations” could be argued to be something in between the two…

I found out that contentType actually is different already. “Transformations” use application/vnd.openhab.transformation, while “transformation services” use application/vnd.openhab.bundle. I’m not sure if that’s a “100% trustworthy rule”, but assuming it is, I’ve tweaked MainUI to produce this:

This is just an example to show what I’m talking about, there are multiple problems with this “implementation”:

  • I have no idea how to do layout in F7, so I didn’t even try - I just changed the size of the titles.
  • The scripting doesn’t work completely like it should, the “main sections” aren’t hidden when there are no children, as they were supposed to. I’m not sure why, but I think there is something with the scope in Vue that I haven’t quite gotten.
  • I just used the terms from Core - I’m in not way suggesting that the terms have to be “Transformation Services” and “Transformations” - but it’s the closest thing to something meaningful I have.

It could also be done “the other way around”, there “Transformation Services” and “Transformations” were the “main titles” and the sources the “sub titles”, but it would require some more restructuring of the JS/Vue stream filtering, as they are organized in different “lists” based on the source as it is today, making it easy to just filter out contentType from those lists.

Is it anything here that would be of any interest?

It has nothing to do with coming from the marketplace. JRule times need to be compiled into jar files. I didn’t know if those can be used to create transformations.

This terminology has worked just fine since OH 1. Users of OH know they need to install stuff from the Add-on store to get support for that capability. For end users, there’s no such thing as a “service”. We install add-ons to get access to be capabilities in OH.

End users don’t see the code in core. Everything that runs users see, the docs, MainUI, forum posts call these “add-ons”.

if it’s a jar or a kar file, it is by definition an add-on.

There are many profiles. The transformation profile is one among many. The transformation profile let’s you call a transformation “service” with a specific transformation “configuration”.

That’s it. That’s the relationship. Nothing more. The transformation profile can invoke a transformation service.

Profile is not synonymous with transformation.

There’s a whole page about then in the docs. You’ll find the transformation action documented there among the others.

It’s right there between 'rule templates" and “UI widgets”.

I don’t control what terms of art are used in OH.

There is no “marketplace browser” anywhere. There is MainUI which has the Add-on Store.

Somehow rule templates, block libraries, and UI widgets have enough information to show them completely separated from the bundles. I didn’t see why it’s so hard to do the same for the posts on the forum in the Transformations category. It should work the same way.

The last things I want is for this to work differently.

The closest analog for what gets posted the the transformations category on the marketplace is rule templates. They should be presented the same way.

Ok, I didn’t understand that. A quick search of the JRule repo doesn’t show any matches - that said, I know that they have introduces some restriction on the GitHub search so that it’s no longer a full-text search. But it’s at least an indication that it probably doesn’t support it.

I’m not sure this situation has been as relevant in earlier versions of OH, the “inconsistency” might have been there for a long time, but it’s when you want to present it in a “user friendly way” that it becomes a problem. I’ve stated several times that I’m not saying that the term should be “service”, I’ve just used it in lack of a better term. That said, I don’t think “service” is that strange a word - other possibilities could be “provider”, “engine” or other things that I haven’t thought of.

As far as I know, it’s not that long ago where “extensions” were renamed to “add-ons” in OH. My assumption has been that this was in part to “broaden the term” so that it could apply to other things than just OSGi bundles. The inconsistency is already on full display, it’s called the “Add-on Store”, and in there you find widgets, block libraries, rule templates etc. that you claim aren’t add-ons as far as I can understand.

It’s not just about what end users see. It’s about the terminology used by “people on all levels”. If one group of people call it one thing and another group another things, there will be inconsistencies in documentation, forum topics etc. The Core terminology is also reflected in the REST API, check what you get under /transformations/services.

To turn it around, what do you call those things you can find in the add-on store that aren’t add-ons? Why are they then in the add-on store?

As I said, yes, it exists on the forum, but not in the add-on store. There is no “bundle” category there. It’s by coincidence that things are “so neatly separated” as they are - many things can only be provided either as a bundle or not as a bundle - but not both. The exceptions are UI and Automation where contentType is used to differentiate them. Everything with application/vnd.openhab.uicomponent;type=widget is under “Widgets for the Main UI”, everything that doesn’t match this is lumped together under “Other UI Add-ons”. Similarly for Automation, application/vnd.openhab.ruletemplate is “Rule Template”, application/vnd.openhab.uicomponent;type=blocks is “Block Library” and everything else is “Languages & Technologies”. For Automation, community marketplace, JSON 3rd party and official add-ons are mixed in the “Languages & Technologies” category.

This is honestly very similar to what I’ve been trying to suggest for Transformations, I just thought that it might be desirable to categorize them both by “type” and by “source”. Today, bindings are categorized by “source”, while misc/System Integrations, persistence, transformations and voice aren’t categorized at all, and only UI and automation by “type”. I was trying to suggest a way to categorize transformations by “type” as well, without losing the source categorization. But, maybe source isn’t important.

I never suggested that you did - I simply tried to explain why it’s helpful if things are as intuitive as possible, and especially that it’s easy to be confused when you don’t know exactly what many of the terms mean.

That’s what I meant, but I didn’t remember the “correct term” at the time of writing. I assumed that it would be understood.

As I’ve explained above, there isn’t actually much consistency as it is. Some add-on types use one categorization, others use another.

I take it that you mean that they should only be categorized by type, not by source? That still means that there must be different terms for the two types. You can’t make two categories and call them both “Transformations”..?

It seems like Java223 Scripting can, in theory at least: Code search results · GitHub

I’ve tried installing Java223 Scripting both on my dev and production installations, but it doesn’t show up under /rest/transformations/services. I don’t have any other scripting languages installed either, so I can’t compare, and maybe there are problems with my installations that prevent it from working. But, I’m pretty sure that if a scripting language is “correctly implemented”, it should show up.

(sorry, didn’t read all the thread)
Yes it does support transformations, without special precompilation or packaging. And it should show up in the /rest/transformations/services. At least I can see it in my environment : with the name “JAVA” (not java223). There is no dependencies or special setup involved, and it should show up in your env too.
I understand it’s not your main point here, but feel free to ask me if you want me to help you make it work.
(edit : it’s funny, I didn’t make many contributions, but you managed to ping me TWICE here (the rocker switch transformation and the Java223 script langage :sweat_smile:)