Translate from javascript to rules DSL

Can somebody help me translate this javascript into rules DSL?

A friend did this for me, but I’ve been learning everything else in DSL, and I don’t understand this javascript enough to tweak it and change it for other purposes.

The code is supposed to:
1- Calculate a color temperature based on Sun Elevation (with some offsets for clouds and other weather conditions. Lights change from 2200K to 6000K as the Sun goes between -6Deg to 40 Deg elevation)
2 - Apply the calculated color temperature to any light that is currently on.

triggers:
  - id: "3"
    configuration:
      itemName: AstroSunData_Elevation
    type: core.ItemStateUpdateTrigger
conditions: []
actions:
  - inputs: {}
    id: "1"
    configuration:
      type: application/javascript
      script: >+
        var logger =
        Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' +
        ctx.ruleUID);

        var SunElevation = (itemRegistry.getItem("AstroSunData_Elevation").getState()) + 6;

        var Clouds = itemRegistry
          .getItem("Localweatherandforecast_Cloudiness")
          .getState();
        var weatherCond = itemRegistry.getItem("Localweatherandforecast_WeatherCondition").getState()

        var weatherID = itemRegistry.getItem("Localweatherandforecast_Current_Conditionid").getState()

        var outputstart = 2200;

        var outputend = 6000;

        var inputstart = 0;

        var inputend = 46;

        var output = 
            Math.floor(
              Math.min(Math.max((
                outputstart +
                ((outputend - outputstart) / (inputend - inputstart)) *
                (SunElevation - inputstart)
              ), 2200), 6000)
            );
        var cloudFactor = "801"

        if (weatherID == "801") { // few clouds: 11-25%
          cloudFactor = Math.floor(output * 0.05);
        } else if (weatherID == "802") { // scattered clouds: 25-50%
          cloudFactor = Math.floor(output * 0.1);
        } else if (weatherID == "803") { // broken clouds: 51-84%
          cloudFactor = Math.floor(output * 0.2);
        } else if (weatherID == "804") { // overcast clouds: 85-100%
          cloudFactor = Math.floor(output * 0.33);
        } else if (weatherID == "800") { // clear sky
          cloudFactor = 0
        } else if (weatherID == "500") { // light rain
          cloudFactor = Math.floor(output * 0.05);
        } else if (weatherID == "501") { // moderate rain
          cloudFactor = Math.floor(output * 0.25);
        } else if (weatherID == "502") { // heavy intensity rain
          cloudFactor = Math.floor(output * 0.33);
        } else if (weatherID == "503") { // very heavy rain
          cloudFactor = Math.floor(output * 0.5);
        } else if (weatherID == "504") { // extreme rain
          cloudFactor = Math.floor(output * 0.6);
        } else if (weatherID == "511") { // freezing rain
          cloudFactor = Math.floor(output * 0.6);
        } else if (weatherID == "520") { // light intensity shower rain
          cloudFactor = Math.floor(output * 0.2);
        } else if (weatherID == "521") { // shower rain
          cloudFactor = Math.floor(output * 0.3);
        } else if (weatherID == "522") { // heavy intensity shower rain
          cloudFactor = Math.floor(output * 0.4);
        } else if (weatherID == "531") { // ragged shower rain
          cloudFactor = Math.floor(output * 0.45);
        } else { 
          cloudFactor = Math.floor(output * Clouds * 0.002);
        }
          

        // https://openweathermap.org/weather-conditions adjust for type of cloud/atmosphere/rain



        var newColourValue = Math.min(Math.max((output - cloudFactor), 2200), 6000);

           
        events.sendCommand('Calculated_ColorTemperature', newColourValue);

        var lightLevelGroup = itemRegistry.getItem("gAll_Lights_Brightness");


        lightLevelGroup.members.forEach(function (item) {
          if (item.getState() > 0) {
            var thingName = item.getName().match(/^(.*)_.*/)[1];
            events.sendCommand(
              thingName + "_ColorTemperature",
              newColourValue
            );
          }
        });



        logger.info("Colour Temperature rule is running. Calculated Temp is " + output + "K - Cloud Factor (" + weatherCond + ", " + cloudFactor + "K) = " + (output - cloudFactor) + "K, clamped to " + newColourValue + "K");


    type: script.ScriptAction

Thanks for any help,

rule "weather and forecast"
when
    Item AstroSunData_Elevation changed
then
    val SunElevation = AstroSunData_Elevation.state + 6
    val Clouds = Localweatherandforecast_Cloudiness.state
    val weatherCond = Localweatherandforecast_WeatherCondition.state
    val weatherID = Localweatherandforecast_Current_Conditionid.state
    val outputstart = 2200
    val outputend = 6000
    val inputstart = 0
    val inputend = 46

    val output = Math::floor(Math::min(Math::max((outputstart + ((outputend - outputstart) / (inputend - inputstart)) * (SunElevation - inputstart)), 2200), 6000))
    var nFactor = 0
                                                // https://openweathermap.org/weather-conditions adjust for type of cloud/atmosphere/rain
    switch(weatherID.toString) {
        case "800" : nFactor = 0                // clear sky
        case "801" : nFactor = 0.05             // few clouds: 11-25%
        case "802" : nFactor = 0.1              // scattered clouds: 25-50%
        case "803" : nFactor = 0.2              // broken clouds: 51-84%
        case "804" : nFactor = 0.33             // overcast clouds: 85-100%
        case "500" : nFactor = 0.05             // light rain
        case "501" : nFactor = 0.25             // moderate rain
        case "502" : nFactor = 0.33             // heavy intensity rain
        case "503" : nFactor = 0.5              // very heavy rain
        case "504" : nFactor = 0.6              // extreme rain
        case "511" : nFactor = 0.6              // freezing rain
        case "520" : nFactor = 0.2              // light intensity shower rain
        case "521" : nFactor = 0.3              // shower rain
        case "522" : nFactor = 0.4              // heavy intensity shower rain
        case "531" : nFactor = 0.45             // ragged shower rain
        default    : nFactor = Clouds * 0.002
    }
    val cloudFactor = Math::floor(output * nFactor)

    var newColourValue = Math::min(Math::max((output - cloudFactor), 2200), 6000)
    Calculated_ColorTemperature.sendCommand(newColourValue)
    gAll_Lights_Brightness.members.filter[i|i.state > 0].forEach[item|
        gAll_Lights_ColorTemperature.members.filter[j|j.name.contains(item.name)].head.sendCommand(newColourValue)
    ]
    logInfo("colorTemp", "Calculated Temp is {} K - Cloud Factor ({}, {} K) = {} K, clamped to {} K",output, weatherCond, cloudFactor,(output - cloudFactor), newColourValue)
end

You have to create a second Group Item with name gAll_Lights_ColorTemperature and include all items which are related to brightness Items in gAll_Lights_Brightness.

Please be aware that I changed the code slightly (by using switch - case instead of all the if statements)

I did not test the rule, so maybe there are still some issues.

Thank you!

I will try this later today.

One more thing you might be able to help me with.

Any suggestions for how to limit the maximum change?
The problem is that when the weather condition changes, the color temperature makes a big step and is very noticable. It should ideeally only make amaximum step of approx 100K per change.

See this graph of what happens - The green line (Calculatede temp) should smoothly transition instead of these big steps.

Uuh, that’s hard…
I would recommend an additional Item and rule. The one rule does the change of Value, another rule gets the change, does a comparison between current value and suggested value, if difference is too big, it starts a timer which does the steps to smoothen the transition.

1 Like

Been trying to get this working.
The first rule is ok, but the second rule which is supposed to apply the value to all lights that are on is not working for me.
Any ideas?

rule "Sun elevation test"
when
    Item testSwitch changed 
then
    logInfo("Light Color Temperature", "Sun elevation test log")
    val SunElevation = (AstroSunData_Elevation.state as Number + 6).intValue
    val Number Clouds = Localweatherandforecast_Cloudiness.state
    val weatherCond = Localweatherandforecast_WeatherCondition.state
    val weatherID = Localweatherandforecast_Current_Conditionid.state
    val outputstart = 2200
    val outputend = 6000
    val inputstart = 0
    val inputend = 46
    val Number output1 = outputstart + (outputend - outputstart) / (inputend - inputstart) * (SunElevation - inputstart)
    val Number output2 = Math.min(Math.max(output1, 2200), 6000) 

    logInfo("Calulated temperature by Sun elevation", output2.toString)

    val Number cloudFactor = Math.floor( (output2.intValue - 2200) * (Clouds.intValue * 0.002))
    logInfo("cloudFactor", cloudFactor.toString)

    val Number newColourValue = Math.min(Math.max((output2.intValue - cloudFactor.intValue), 2200), 6000)
    logInfo("newColourValue", newColourValue.toString)

    Calculated_ColorTemperature.sendCommand(newColourValue)

end


rule "Updated Calculated Color" // Send Calculated Color Temperature to all lights thata are ON
when
    Item Calculated_ColorTemperature changed 
then
    logInfo("Updated Calculated Color", "Running Color Temp Update")
    gAll_Lights_Brightness.members.filter[i|i.state > 0].forEach[item|
        gAll_Lights_ColorTemperature.members.filter[j|j.name.contains(item.name)].head.sendCommand(newState)
    ]

end

Please try casting to Number:

rule "Updated Calculated Color" // Send Calculated Color Temperature to all lights thata are ON
when
    Item Calculated_ColorTemperature changed 
then
    logInfo("Updated Calculated Color", "Running Color Temp Update")
    gAll_Lights_Brightness.members.filter[i|(i.state as Number) > 0].forEach[item|
        gAll_Lights_ColorTemperature.members.filter[j|j.name.contains(item.name)].head.sendCommand(newState)
    ]
end

Are the Item names matching (i.e. having Items like MyLight in Group gAll_Lights_Brightness and the matiching Item MyLightColortemp in Group gAll_Lights_ColorTemperature)?

Yes. They have the same name except for the last piece.
Is this why it isn’t working? Because it’s trying to match the whole name including the ‘_Brightness’ bit?

This didnt work

Script execution of rule with UID 'sun2-1' failed: Could not cast UNDEF to java.lang.Number; line 6, column 46, length 17 in sun2

That’ pretty self-evident. One of your Items does not yet have a valid state.
That’s always a possibility e.g. a device is unplugged etc. You can code around it by testing before trying to handle it as number.

Yes. There was one light not powered on.
The log error has gone, but it still does not send the new color temp to the _ColorTemperature items.

Can someone explain what the .head part of this does?

gAll_Lights_ColorTemperature.members

This make a list (of member Items)

.filter[j|j.name.contains(item.name)]

This filters the elements in the list according to some conditions.
The result is still a list.
The list may be empty.

.head

Gets the first element of that list.
The result is no longer a list, it’s that actual object. (an Item here)
Or if the list is empty, it’s null.

So it still seems like I need to find a way to change the filter search term from item_Brightness to item_ColorTemperature somehow?

I think you’d find this useful

If you can build the complete name string that you want, you can fetch it direct without rummaging in Groups at all.

I have read through this a a few times already. I’m struggling to find what I’m looking for.

I’ve been working on this problem for a long time and I think I’m stuck. I’m just not getting how the filters are working and how to manipulate it.

Your last question was about string manipulation, not filters.

Item with name fred_banana.
Item with name fred_apple.
If you have source Item fred_banana, how do you make “fred_apple”?

val targetSubstring = sourceItem.name.split("_")
// that creates an array with parts "fred" and "banana"
// part 0 and part 1 respectively
val targetItemName = targetSubstring.get(0) + "_apple"
// we can pick one part and just stitch strings together
// now we have a string "fred_apple"

// that's just a string
// we can do limited stuff with an Item name string
sendCommand(targetItemName, "ON")

// but often you'll want to get the actual Item

val directItem = ScriptServiceUtil.getItemRegistry.getItem(targetItemName)
// but this will blow up and stop the rule if that Item does not exist

val searchItem = gSomeGroup.members.findFirst[ j | j.name == targetItemName]
// .findFirst[ ] is just a handy shortcut version of .filter[ ].head
// this will return null if it doesn't exist
// allowing your rule to continue and deal with it gracefully
if (searchItem === null) {
   logInfo("test", "Oh dear, Item " + targetItemName + " not found")
}

Break the rule down and add logging.

“not working for me” is not very descriptive. There are a million ways that rule could not be working. Is it triggering? Generating an error?

As rossko tried to do for you, break it down command by command.

The first command in the rule logs out. Look in the logs for that statement to confirm the rule is triggering at all.

Break that long group line down into multiple lines. Save gAll_Lights_Brightness.members to a variable and log it out. Is it as expected?

Save the .filter to a variable and log it out. Is it as expected?

Add a logging statement to your forEach so you can see that the loop is running.

Do the same for the filter on gAll_Lights_ColorTemperature. Or even better, forget the filter and just sendCommand to the Item by name.

sendCommand(i.name+"ColorItem", newState)

Obviously use what ever naming scheme is appropriate.

That raises a question. newState? What the heck is that? It’s not defined anywhere.

Thanks. I think this is what was asking for. However, after thinking about this, trying to apply this to my problem doesn’t work. For example, Kitchen_Ceiling_Light_Brightness and Hall_Lamp have a different number of splits, so I can’t use a set number of splits to get the right bit.
Where can I find the refences to these things like .name .split .get(0) .head? I can’t find it in the documentation. I’m wondering if I can somehow get the last split and then replace it with _ColorTemperature.

Thanks,

Sorry for any vagueness. I had logged everything out to get to this point and this is the last piece I’m not sure how to break down and test.
The rule is triggering and I have been monitoring the logs, but no further errors are being shown.
I added a log to this filter at the end so it looks like

    logInfo("Updated Calculated Color", "Running Color Temp Update")
    gAll_Lights_Brightness.members.filter[i|(i.state as Number) > 0].forEach[item|
        gAll_Lights_ColorTemperature.members.filter[j|j.name.contains(item.name)].head.sendCommand(newState)
        logInfo("loggerName", item.toString)
    ]

And in the logs, when the rule triggers, I get a log line for each light that is on which looks like

[INFO ] [openhab.core.model.script.loggerName] - BackRoom_Ceiling_Light_Brightness (Type=DimmerItem, State=100, Label=Brightness, Category=slider, Tags=[Control, Light], Groups=[BackRoom_Ceiling_Light, gAll_Lights_Brightness, gAll_Lights_OnOff, gSelection_ItemsToRestoreOnStartUp])

I believe this is the full item object being listed from the first filter. But I can’t see how I get from this to the second part, changing _Brightness for _ColorTemperature to get the right item name.

newState is the new state of the CalculatedColorTemperature item that triggers the rule