JRule - openHAB Rules using Java

Oh yes, that’s exactly the same “issue” I have.

Added BETA8 Build:

BETA8

2 Likes

BETA10

  • Optimized items by gerrieg https://github.com/seaside1/jrule/pull/33
  • Syntax change: event.getValue(), event.getValuesAsDouble() etc replaced with event.getState().getValue() and event.getState().getValueAsDouble()
  • Syntax change JRuleSwitchItem.sendCommand(myItem, ON) replaced with JRuleSwitchItem.forName(myItem).sendCommand(ON)
1 Like

I’ve got several Contact items which are sometimes out of reach. In this case, JRule throws an IllegalArgumentException when trying to access the state:

var ct = JRuleItemRegistry.get("GN_CT_DOOR", JRuleContactItem.class)
ct.getState();

This is the Exception:

2022-05-14 11:40:12.269 [ERROR] [on.jrule.internal.engine.JRuleEngine] - [UpdateDisplayHall] Error message: No enum constant org.openhab.core.library.types.OpenClosedType.NULL
2022-05-14 11:40:12.273 [ERROR] [on.jrule.internal.engine.JRuleEngine] - [UpdateDisplayHall] Error Stacktrace: java.lang.IllegalArgumentException: No enum constant org.openhab.core.library.types.OpenClosedType.NULL
	at java.base/java.lang.Enum.valueOf(Enum.java:240)
	at org.openhab.core.library.types.OpenClosedType.valueOf(OpenClosedType.java:1)
	at org.openhab.automation.jrule.internal.handler.JRuleEventHandler.getOpenClosedValueFromState(JRuleEventHandler.java:276)
	at org.openhab.automation.jrule.internal.handler.JRuleEventHandler.getOpenClosedValue(JRuleEventHandler.java:241)
	at org.openhab.automation.jrule.items.JRuleContactItem.getState(JRuleContactItem.java:37)
	at org.openhab.automation.jrule.rules.user.DisplayHallRule.lambda$updateDisplayHall$0(DisplayHallRule.java:90)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
	at org.openhab.automation.jrule.rules.user.DisplayHallRule.updateDisplayHall(DisplayHallRule.java:83)

It would be nicer to get UNDEF in this case.

trying to use transform leads to an Exception

final var dayInfo = transform("MAP(DayOfWeek_de.map):%s", dayOfWeek.name());

Result:

2022-05-14 13:01:08.253 [INFO ] [rule.internal.compiler.JRuleCompiler] - [JRuleCompiler] Error on line 54 in file:///etc/openhab/automation/jrule/rules/org/openhab/automation/jrule/rules/user/DateRule.java: cannot access org.openhab.core.transform.TransformationException
  class file for org.openhab.core.transform.TransformationException not found

I have created a fix for this. Have not tested it yet. Will build a new version soon.
See Fixed issued with undef item for state · seaside1/jrule@6827940 · GitHub

1 Like

This is fixed by seime: See: Wrap TransformationException in JRuleExecutionException by seime · Pull Request #39 · seaside1/jrule · GitHub

1 Like

This engine and the support/dedication it gets is awesome.

I’ve reimplemented my 250+ items/50+ rules openHAB 2.5 system with JRule on openHAB 3.2 within 6 hours. Some rules are really complex, but it was really easy to do a 1:1 migration from JSR223 groovy/jython and xtext rules.

Thanks to static typing, I could realize a long-postponed “rename items for more consistency” project without any problems.
Using IntelliJ and gradle instead of VSCode or the openHAB UI for editing really helps a lot.

For me, this is by far the best rule engine available in openHAB since openHAB 1.8.

5 Likes

It is fantastic to see that several people are contributing to the project. :+1:

3 Likes

Built a BETA11

BETA11

2 Likes

I have added a template project for building and deploying rules using maven.
In that example project there is also code for testing your rules with a fake eventbus.
see: GitHub - seaside1/jrule-user: JRule User Demo and Test Project
@querdenker2k

I recently came across a really weird memory leak. Since I added a rule which gets executed once per minute and updates some Items the openhab crashes with OutOfMemory after a few days.

This is the rule:

package org.openhab.automation.jrule.rules.user;

import org.json.JSONArray;
import org.json.JSONObject;
import org.openhab.automation.jrule.items.generated.*;
import org.openhab.automation.jrule.rules.JRule;
import org.openhab.automation.jrule.rules.JRuleName;
import org.openhab.automation.jrule.rules.JRuleWhen;
import org.openhab.automation.jrule.rules.user.dto.BrightSkyResponse;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Optional;

/**
 * Created by Finn on 29.04.2022.
 */
public class OHBrightSky extends JRule {
    private static final SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd");
    private static final DateTimeFormatter df1 = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    public OHBrightSky() {

    }

    @JRuleName("Fetch BrightSkyData")
    @JRuleWhen(item = _BrightSkyUpdate.ITEM, trigger = _BrightSkyUpdate.TRIGGER_RECEIVED_COMMAND_ON)
    @JRuleWhen(cron = "0 * * * * *")
    public void updateBrightSkyData() {
        Date date = new Date();

        LocalDateTime today = LocalDateTime.now();
        LocalDateTime yesterday = LocalDateTime.now().minusDays(1);

        Optional<BrightSkyResponse> brightSkyResponseToday = this.loadBrightSkyData("xx.xxxxxxxx", "yy.yyyyyyyy", today);

        if (brightSkyResponseToday.isPresent()) {
            _BrightSkyRainToday.sendCommand(brightSkyResponseToday.get().getRain());

            _BrightSkyTempCurrent.sendCommand(brightSkyResponseToday.get().getTempCurrent());
            _BrightSkyTempMaxToday.sendCommand(brightSkyResponseToday.get().getTempMax());

            _BrightSkyWindCurrent.sendCommand(brightSkyResponseToday.get().getWindCurrent());

            logInfo(String.format("tempMaxToday: %s; rainToday: %s; tempCurrent: %s; windCurrent: %s",
                    brightSkyResponseToday.get().getTempMax(),
                    brightSkyResponseToday.get().getRain(),
                    brightSkyResponseToday.get().getTempCurrent(),
                    brightSkyResponseToday.get().getWindCurrent()
            ));
        }

        Optional<BrightSkyResponse> brightSkyResponseYesterday = this.loadBrightSkyData("xx.xxxxxxxx", "yy.yyyyyyyy", yesterday);

        if (brightSkyResponseYesterday.isPresent()) {
            _BrightSkyTempMaxYesterday.sendCommand(brightSkyResponseYesterday.get().getTempMax());
            _BrightSkyRainYesterday.sendCommand(brightSkyResponseYesterday.get().getRain());

            logInfo(String.format("tempMaxYesterday: %s; rainYesterday: %s",
                    brightSkyResponseYesterday.get().getTempMax(),
                    brightSkyResponseYesterday.get().getRain()
            ));
        }
    }

    private Optional<BrightSkyResponse> loadBrightSkyData(String lat, String lng, LocalDateTime date) {
        String dateTimeFormatted = date.format(df1);

        String url = String.format("https://api.brightsky.dev/weather?lat=%s&lon=%s&date=%s", lat, lng, dateTimeFormatted);

        logInfo("Requesting: {}", url);

        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build();

        int currentHour = date.getHour();

        BrightSkyResponse brightSkyResponse = new BrightSkyResponse();

        try {
            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
            JSONObject myObject = new JSONObject(response.body());

            JSONArray hours = myObject.getJSONArray("weather");

            for (int i = 0; i < hours.length(); i++) {
                JSONObject hour = hours.getJSONObject(i);

                brightSkyResponse.setRain(brightSkyResponse.getRain() + hour.getDouble("precipitation"));

                if (hour.getDouble("temperature") > brightSkyResponse.getTempMax()) {
                    brightSkyResponse.setTempMax(hour.getDouble("temperature"));
                }

                if (i == currentHour) {
                    brightSkyResponse.setTempCurrent(hour.getDouble("temperature"));
                    brightSkyResponse.setWindCurrent(hour.getDouble("wind_speed"));
                }
            }
        } catch (Exception e) {
            return Optional.empty();
        }

        return Optional.of(brightSkyResponse);
    }
}

The issue is gone after removing the rule again.

Wild guess: Maybe reuse client instead of creating 2 new ones every minute.

Hmm I think the Java GC should handle that fine but I’ll try it, thanks!

I would love to learn more about the lifecycle and limitations of JRules. Some questions are

  • Are classes extending JRule singletons?
  • When are they instantiated?
  • Are they discarded (besides when openHAB shuts down)?
  • Is it ok to have multiple rule methods (= methods annotated with @JRuleWhen/@JRuleName) in one XyzRule extends JRule class?
  • Is it ok to share state between multiple rule methods within one rule class?
  • Does a rule method need to be designed for reentrancy? Or are there measures that ensure that a rule method is not called before its previous invocation is finished?
  • Are there any known problems using certain Java features (i.e. regarding final classes, inner classes, Threads, …)?

It would be great to have documentation on these topics.

1 Like

No they are not. The are however only instantiated once by the engine, and then reused.
Except on reload of the engine, then they are discarded and re instantiated.

When the automation addon is loaded in OpenHab, it will wait 5 seconds and then perform
an initialization:

  1. Create threads and executors (depending on config)
  2. Checking folder structure and read write permissions
  3. Extract the jrule.jar and write it to disk
  4. Read all items from the ItemRegistry and generate java files for the items if they don’t exist already.
  5. Compile items source files and store to the jrule-items.jar
  6. Compile the user defined rules (unless they are supplied with a jar-file)
  7. Create Rule instances, meaning this is where they are instantiated once.
  8. Starting to monitor folders for rules, items etc To perform a reload if needed.
  9. Start listening on the eventbus for events to be able to trigger rules

The rules are instantiated under step 7.

Yes if a reload of the engine is triggered, for instance when adding new items or rules.

Yes I tend to do this, I have typically one File for one area, like LightHandlingRules and under there several rules which handles the lights in the house.

Yes, not sure I would recommend it, but it is possible. You have to take care of thread safety etc. A lot of times, such states are easier to keep in items.

It can be triggered multiple times from different threads, you should add thread safety if you need it. Synchronize methods add locks etc. See example 2, 8, 9, 10 for examples of both locks and timers that are built into JRule.

Not that I know. Most bugs are fixed. There are some feature gaps still, like script actions, member of features, openhab gui support etc.

Yes I will try to update the documentation, and don’t be afraid to submit PRs on the documentation :slight_smile:

Regards, S

1 Like

Unfortunately it didn’t fix the issue, memory usage is still growing. What else could I try to fix this issue?

You could connect (your favourite) profiler to the JVM and take a heap dump. It might shed some light in what is going on.

Regards

Is it possible to postUpdate with UNDEF?
Is it planned to use all the other persistance functions? (not just lastUpdated)

I’ve done some work this weekend regarding persistence functions
[WIP] Generate Items file by seime · Pull Request #42 · seaside1/jrule · GitHub. Not complete nor reviewed by @Seaside yet