Smart Virtual Thermostat (beta version)

These null errors you can ignore. When you saved the rule file (I mean when openHAB reload the rule file), the existing timer items of the previous file still exists.
But when they run (timer went off) the environment in which they were created, doesn’t exist anymore.
With other word, they are ghost timers and thus creating errors of null.
If you look closely, the null expection is now at org.eclipse.smarthome.model.script.engine.ScriptError.<init>( ~[?:?] instead of at org.eclipse.smarthome.model.persistence.extensions.PersistenceExtensions.historicState( ~[?:?]

Good idea!
This was for now a quick and dirty fix. If it is null it wil convert to 0. VSCode will give you a warning if you do the follow:

val temp = (null as Number)?.doubleValue

The warning is:
Null-safe call of primitive-valued feature doubleValue, default value 0 will be used

But it’s nicer to catch the null ad generate a warning as you suggested.

I also get some warnings when saving:

There is no context to infer the closure's argument types from. Consider typing the arguments or put the closures into a typed context.

There is no context to infer the closure's argument types from. Consider typing the arguments or put the closures into a typed context.

There is no context to infer the closure's argument types from. Consider typing the arguments or put the closures into a typed context.

However it doesn’t help where are these variables but I assume that there are Strings mainly where the type is not referred. I will try to clean them up…

I also made some cosmetic changes to the code (I think it is easier to read now for someone who is not that familiar with the code) if you like that I can create a PR on GitHub…

That’s strange, I’ve never seen these warnings. When/Where do you get them?

Yes, great, I’m trying to make to code available and readable for everyone, so be my guest and create a PR.

I get these warnings when I save the file.
Basically it happens because it can’t refer the return type from lambdas (for example when filtering for Items). I have tried adding the type to some of them (hope I got it right), but it I couldn’t find which 3 statement causes these.

2020-01-09 19:26:10.633 [INFO ] [el.core.internal.ModelRepositoryImpl] - Validation issues found in configuration model 'Thermostat.rules', using it anyway:

There is no context to infer the closure's argument types from. Consider typing the arguments or put the closures into a typed context.

There is no context to infer the closure's argument types from. Consider typing the arguments or put the closures into a typed context.

There is no context to infer the closure's argument types from. Consider typing the arguments or put the closures into a typed context.

2020-01-09 19:26:10.697 [INFO ] [el.core.internal.ModelRepositoryImpl] - Refreshing model 'Thermostat.rules'

I only can find this part of code

					logInfo("svt2","retrigger {} with {}",,i.state)
					i.sendCommand(i.state as Number)

Here i is unkown. There is a discusion here that talks about the same problem.
But strange enough, I’ve used the Design Pattern: Working with Groups in Rules post to create them and there is no mention of this problem in the example.

You were right, this is one of the three. I have added Number (before i) and I get one less warning.

I think the other 2 is where 2 implicit variable is used like:

val lastTemp = Temperatures.allMembers.filter[name.contains(room)].map[(historicState(new DateTime(lastCalcItem.state.toString),DATABASE)?.state as Number).doubleValue].reduce[s, v | s + v ] / Temperatures.allMembers.filter[name.contains(room)].size  //get average of room temperatures (group) at the last calc time

Here s and v are not referred with type…

Most of the time the Rules DSL is able to figure out what type i needs to be in that context. Sometimes it fails. This appears to be one of those cases. As Kristof indicates, the solution is to just provide the type before the i so the Rules DSL doesn’t have to try, and fail, to guess what type it is.

However, @rkrisi, the type of i is not a Number, it’s a NumberItem. Your change probably made the parse time error go away but it will fail at runtime.

Thanks :slight_smile: I already wanted to say that I’m not sure that the item type is correct, I just wanted to check this is what causes this…

After adding any type to these two (with a reduce where s, v is used) these warnings are gone. However I’m not sure what is the type of s and v there…
If you can help me out with the type I can include these fixes also in the PR (which I have created)

The result of the previous map is a List of Doubles. It would be far better to leave it as a Number than force it to be a primitive only to have it reconverted back to a Double (which is a Number) behind the scenes because you can’t store primitives in Lists. So s and v can be either Number or Double or probably even double would work.

But you should avoid the use of primitives as much as is possible. It greatly slows down the .rules file parsing. A few primitive references can add minutes on an RPi.

And I thought that using primitives are better in terms of resources :slight_smile: (primitive type vs a complete Object…).
I will give a Double for them I think.

In Rules DSL primitives are the worst. Really, in Rules DSL you should avoid defining types in general unless you have to. When you define the types you force the parser to do a whole lot more work than it would otherwise because it has to check to make sure all the types it can know about at parse time work with that type. And for some reason, using primitives causes the engine to do even more work.

Even on a very fast machine, there are some expressions that can take a couple of minutes to parse when you force it to use primitives and less than a second when you let the engine figure out the type on it’s own.

When you let the engine wait until runtime before it has to care about the types of the variables, it can skip all the type checking that takes places at parse time and at runtime it has a lot more information so the checks are really easy there too.

Good to know.
But I can’t find (or I’m not looking in the right docs) what I get with the doubleValue method.
For example:

var lastSetpoint = (lastSetpointState as Number).doubleValue

Will lastSetPoint become a double (primitive) or a Double (Object)?

A primitive double. Documentation is here.

Though in that case it might get converted back to a BigDecimal/Double/Number. But if you have

var double lastSetpoint = ...

it will be a primitive double.

As a general Rule, just don’t worry about type unless you have to. Let everything just be what it happens to be, or if you need to (e.g. like the state from a Number Item) cast it to Number, no more. Only call xValue when you absolutely have to (e.g. like in now.plusSeconds(lastSetpointState as Number).doubleValue)).

Edited the openingspost to reference to new versions of the files at GitHub.

In my Oppinion This idea is Great because it make devices independent from vendor.
What we Need is a value to Compare these rules against vendor bindings. I thought About valve Opening in %*seconds as sum for whole days. Let me think about it :slight_smile:

i think i have an idea now:
i made some rules, that measure the usage of the thermostats.Note: In the Rules i do not address groups, but i plan to realize it due to simplyfing my code. Stay tuned.
What we need:

Group gTemperatur_Ventil "Ventilöffnungen alle Heizungen" (gFF)
Group gTemperatur_Ventil_Persistence
Group gTemperatur_Ventil_Counter "Counter für Ventilöffnungen" (gFF,gTemperatur_Ventil_Persistence)
Group:Number:SUM gTemperatur_Ventil_Hourly "Counter für Ventilöffnungen daily" (gFF,gTemperatur_Ventil_Persistence)
Group:Number:SUM gTemperatur_Ventil_Daily "Counter für Ventilöffnungen daily" (gFF,gTemperatur_Ventil_Persistence)
Group:Number:SUM gTemperatur_Ventil_Weekly "Counter für Ventilöffnungen daily" (gFF,gTemperatur_Ventil_Persistence)
Group:Number:SUM gTemperatur_Ventil_Monthly "Counter für Ventilöffnungen daily" (gFF,gTemperatur_Ventil_Persistence)
Number Wohnzimmer_Heizung_Links_Ventil_Counter "Zählt die Sekunden multipliziert mit der Öffnung entsprechend der Ventilöffnung" (gTemperatur_Ventil_Counter)
Number Wohnzimmer_Heizung_Links_Ventil_Counter_Daily "Zählt die Sekunden multipliziert mit der Öffnung entsprechend der Ventilöffnung" (gTemperatur_Ventil_Counter_Daily)
Number Wohnzimmer_Heizung_Links_Ventil_Counter_Hourly "Zählt die Sekunden multipliziert mit der Öffnung entsprechend der Ventilöffnung" (gTemperatur_Ventil_Counter_Hourly)
Number Wohnzimmer_Heizung_Links_Ventil_Counter_Weekly "Zählt die Sekunden multipliziert mit der Öffnung entsprechend der Ventilöffnung" (gTemperatur_Ventil_Counter_Weekly)
Number Wohnzimmer_Heizung_Links_Ventil_Counter_Monthly "Zählt die Sekunden multipliziert mit der Öffnung entsprechend der Ventilöffnung" (gTemperatur_Ventil_Counter_Monthly)
rule "WZ Heizung links an"
        Member of gTemperatur_Ventil changed
        val devicename =
        val devicename_Counter = devicename + "_Counter"
        val devicename_Counter_item = gTemperatur_Ventil_Counter.members.findFirst[ d | == devicename_Counter ]
                val lastOpen = triggeringItem.previousState(true).timestamp.time
                        logInfo("Valve Opened", "Ventil letzter Change: " + lastOpen)
                        logInfo("Valve Opened", "Ventil now: " + now + " timestamp: " + now.millis)
                        val totalTime = now.millis - lastOpen
                        val totalTimeSec = totalTime / 1000
                        val ValveState = triggeringItem.previousState(true).state as DecimalType
                        val ValveStatePercent = ValveState * 0.01
                        val heatingCounter = ValveStatePercent * totalTimeSec
                        logInfo("Valve Opened", "Ventil war für X sekunden offen: " + + " Sekunden: " + totalTimeSec + " heatingcounter: " + heatingCounter +$

rule "Heizungen Verbraucherstatistik - hourly"
        Time cron "0 0 0/1 1/1 * ? *"
        logInfo("Valve Hourly", "Hourly Ventilstatus schreiben")


rule "Heizungen Verbraucherstatistik - daily"
        Time cron "0 0 0 1/1 * ? *"
        logInfo("Valve Hourly", "Daily Ventilstatus schreiben")

rule "Heizungen Verbraucherstatistik - weekly"
        Time cron "0 0 0 ? * MON *"
        logInfo("Valve Hourly", "Daily Ventilstatus schreiben")
        Frame                   label="Heizungsverbrauch"
                Text item=Wohnzimmer_Heizung_Links_Ventil_Counter_Hourly label="Hourly von WZ Heizung Links: [%s]"
                Text item=Wohnzimmer_Heizung_Rechts_Ventil_Counter_Hourly label="Hourly von WZ Heizung Rechts: [%s]"
                Text item=Kinderzimmer_Heizung_Ventil_Counter_Hourly label="Hourly von Kinderzimmerheizung: [%s]"
                Text item=Schlafzimmer_Heizung_Ventil_Counter_Hourly label="Hourly von Schlafzimmerheizung: [%s]"
                Text item=Wohnzimmer_Heizung_Links_Ventil_Counter_Daily label="Daily von WZ Heizung Links: [%s]"
                Text item=Wohnzimmer_Heizung_Rechts_Ventil_Counter_Daily label="Daily von WZ Heizung Rechts: [%s]"
                Text item=Kinderzimmer_Heizung_Ventil_Counter_Daily label="Daily von Kinderzimmerheizung: [%s]"
                Text item=Schlafzimmer_Heizung_Ventil_Counter_Daily label="Daily von Schlafzimmerheizung: [%s]"

How does it work?
the Rule calculates the seconds how long the valve did not stay at 0%. This value is multiplied with the percentage the value stood open. This way you get a rating that is updated to a counter. With the persistence rules you are able to display the usage for a period.

What can you do with all that values now? You can compare them to values that were generated from vendor solutions i.e. Homematic. Therefore you can see how many heating is needed to fulfill the requirements.
What is missing? I will implement a Method, that will measure the Delta over time to the Target temperature. This way you can measure the effectivity of your ruleset compared to other mechanisms.
For Example.
If you have a low valveusage and a low delta to target temperature , you can say that your method works pretty well. When a homematic solution has much worse values you can be glad :slight_smile:

Best regards

Thanks for you idea, @Der_Gute!

A few things that I noticed.
First of all this won’t work with underfloor heating. Valves used for such systems are only open/closed.
Secondly, I doubt if you get an accurate rating that you can compare with other solutions. And my doubt is based on the fact of PID controllers in thermostats.

Intelligent/Digital room, but also radiator, thermostats works with a PID controller (as opposite to a mechanical one, which uses a bimetal). Meaning that it calculates it behavior based on delta time and delta temperature. Thus turning a heating on/off, modulate a heating or opening a valve at a certain percentage, based on the difference in time and temperature.

This works, in my opinion, not backworks, such as you suggest to keep record of how long and at what percentage the valve has been open. These variables are almost infinite, you’ll have to have a very very big database of values.

My rule isn’t a PID, but tries to mimic one. When I’ve more time, I’ll add another value, delta outsite temperature, so it can calculate the isolation value of the room and based on that prediction what time needed is to heat to room. Almost like a PID controller :slight_smile:

Did you thought about making a NGRE version of this rule?
I know that this is translated from a Python script and you can use Python in NGRE so it might make sense to do it and maybe only a fewer modification is needed for the code…