As already mentioned somewhere else, I can turn off the heating of a tado TVR, but to return to schedule, I have to make a call to tado’s official API.
I’ve got a rule that turns off a specific TVR when a window is opened, by sending command 0 to e.g. Item Verwarming_badkamer_Heating_Cooling_Target. If the window is closed, that same rule sends command SCHEDULE to Item Verwarming_badkamer_mode. The window in the kitchen switches both the heating in the kitchen and the heating in the living room. The rule then simply iterates between the two relevant Items.
Another rule is triggered when Verwarming_badkamer_mode has received command SCHEDULE. This works, but not for the heating in the living room if the window is closed quickly after opening it (e.g. when it was only opened to let the dog in). I don’t really understand why, but since the thermostat is also connected to that tado “zone”, I assume there’s an internal process within the tado system, which takes too long for the SCHEDULE command to be processed properly.
I therefore tried to add a “double check” to this “tado rule”, via a cache:
configuration: {}
triggers:
- id: "2"
configuration:
command: SCHEDULE
itemName: Verwarming_badkamer_mode
type: core.ItemCommandTrigger
- id: "3"
configuration:
command: SCHEDULE
itemName: Verwarming_dressing_mode
type: core.ItemCommandTrigger
- id: "4"
configuration:
command: SCHEDULE
itemName: Verwarming_eetkamer_mode
type: core.ItemCommandTrigger
- id: "5"
configuration:
command: SCHEDULE
itemName: Verwarming_gang_gelijkvloers_mode
type: core.ItemCommandTrigger
- id: "6"
configuration:
command: SCHEDULE
itemName: Verwarming_keuken_mode
type: core.ItemCommandTrigger
- id: "7"
configuration:
command: SCHEDULE
itemName: Verwarming_studieruimte_mode
type: core.ItemCommandTrigger
- id: "8"
configuration:
itemName: Bewoners_Thuis
type: core.ItemStateChangeTrigger
- id: "9"
configuration:
itemName: testswitch
type: core.ItemStateChangeTrigger
conditions: []
actions:
- inputs: {}
id: "1"
configuration:
type: application/javascript
script: |-
var pythoninvenv = '/var/lib/openhab/bin/python/tado-manual-control/venv/bin/python';
var pythonscript = '/var/lib/openhab/bin/python/tado-manual-control/tado-manual-control.py';
var pythondurationinseconds = 3;
var controletijd = 5;
var tadoZones = {
"Verwarming_badkamer_mode": "5" ,
"Verwarming_dressing_mode": "9",
"Verwarming_eetkamer_mode": "2",
"Verwarming_gang_gelijkvloers_mode": "8",
"Verwarming_studieruimte_mode": "3",
"Verwarming_keuken_mode": "6"
}
function schemaActiveren(zonemodusitemnaam) {
var zone = tadoZones[zonemodusitemnaam];
var command = [pythoninvenv, pythonscript, 'back_to_schedule', '-z', zone];
callPython(command);
var cachenaam = "cachevoorcontrole_zone_"+zone;
if (cache.private.exists(cachenaam)) {
clearTimeout(cache.private.get(cachenaam));
}
var controlezometeen = setTimeout(schemaActiveren, controletijd * 1000, zone, zonemodusitemnaam);
cache.private.put(cachenaam, controlezometeen);
}
function activatieControle(zone, zonemodusitemnaam) {
var zonemodusitem = items[zonemodusitemnaam];
var statusitem = zonemodusitemnaam.split("_mode")[0]+"_Heating_Cooling_Target";
if (zonemodusitem.state == "SCHEDULE" && !zonemodusitem.persistence.changedSince(time.toZDT().minusMinutes(controletijd)) && parseInt(items[statusitem].state) == 0) {
var command = [pythoninvenv, pythonscript, 'back_to_schedule', '-z', zone];
callPython(command);
}
}
function statusBewonersAanpassen(thuis) {
var status = thuis == "ON" ? 'True' : 'False';
var command = [pythoninvenv, pythonscript, 'set_home_state', '-s', status];
callPython(command);
}
function callPython(command) {
console.log("Wachttijd voor pythonscript: "+pythondurationinseconds+" seconden.")
var pythonOutput = actions.Exec.executeCommandLine(time.Duration.ofSeconds(pythondurationinseconds), command);
console.log("Output pythonscript: "+pythonOutput);
}
function main() {
var triggeringItemnaam = event.itemName;
console.log(triggeringItemnaam);
if (triggeringItemnaam.includes("_mode")) {
schemaActiveren(triggeringItemnaam)
} else if (triggeringItemnaam == "Bewoners_Thuis") {
statusBewonersAanpassen(event.newState);
}
}
main()
type: script.ScriptAction
I tested this with the bathroom (“badkamer”) window, and it threw an error:
2026-01-20 20:03:18.502 [INFO ] [automation.jsscripting.rule.tado-API - 47446 ] - Verwarming_badkamer_mode
2026-01-20 20:03:18.505 [INFO ] [automation.jsscripting.rule.tado-API - 47446 ] - Wachttijd voor pythonscript: 3 seconden.
2026-01-20 20:03:19.421 [INFO ] [automation.jsscripting.rule.tado-API - 47446 ] - Output pythonscript: Login successful
Reactivating the schedule for zone 5...
2026-01-20 20:03:24.423 [INFO ] [automation.jsscripting.rule.tado-API - 47247 ] - Wachttijd voor pythonscript: 3 seconden.
2026-01-20 20:03:24.427 [WARN ] [ore.internal.scheduler.SchedulerImpl - 47247 ] - Scheduled job 'org.openhab.automation.jsscripting.rule.tado-API.timeout.1' failed and stopped
org.graalvm.polyglot.PolyglotException: null
at java.lang.ProcessBuilder.start(ProcessBuilder.java:1108) ~[?:?]
at java.lang.ProcessBuilder.start(ProcessBuilder.java:1089) ~[?:?]
at org.openhab.core.io.net.exec.ExecUtil.executeCommandLineAndWaitResponse(ExecUtil.java:81) ~[bundleFile:?]
at org.openhab.core.model.script.actions.Exec.executeCommandLine(Exec.java:56) ~[bundleFile:?]
at <js>.callPython(<eval>:50) ~[?:?]
at <js>.schemaActiveren(<eval>:21) ~[?:?]
at <js>.:=>(@jsscripting-globals.js:200) ~[?:?]
at com.oracle.truffle.polyglot.PolyglotFunctionProxyHandler.invoke(PolyglotFunctionProxyHandler.java:158) ~[bundleFile:?]
at jdk.proxy1.$Proxy1420.run(Unknown Source) ~[?:?]
at org.openhab.automation.jsscripting.internal.threading.ThreadsafeTimers.lambda$1(ThreadsafeTimers.java:114) ~[bundleFile:?]
at org.openhab.core.internal.scheduler.SchedulerImpl.lambda$12(SchedulerImpl.java:189) ~[?:?]
at org.openhab.core.internal.scheduler.SchedulerImpl.lambda$1(SchedulerImpl.java:88) ~[?:?]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) ~[?:?]
at java.util.concurrent.FutureTask.run(FutureTask.java:317) ~[?:?]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[?:?]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[?:?]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[?:?]
at java.lang.Thread.run(Thread.java:1583) [?:?]
So the first command was processed properly, but the second one threw an error. Initially, I thought it was because I had set pythondurationinseconds to 10, which is longer than controletijd, although I would have been surprised that would be a problem, since a different thread is used. But that’s not it (which is probably a good thing).
But what is the problem? Does anyone have any idea?
@Nadahar, I think to understand that you’re trying to battle thread pool problems, so I thought I’d ping you. But feel free to not engage, of course.
Maybe this has nothing to to with thread pools, but layman me sees it written in the error stack.