Uhrsteuerung per Shelly 2. Generation mit Schedule Jobs und Skript
Auf ausdrücklichen Wunsch von thgoebel habe ich mich um eine nutzbare Implementation bemüht. horkatz könnte sie auch interessieren.
Die erste auslieferbare Testfassung ist fertig.
Ich habe sie getestet mit einem Shelly Plus 1, MQTT Nachrichten, Node-RED zwecks Speicherung in einer Influx Zeitreihendatenbank und Grafana zur Visualisierung.
Zusätzlich habe ich per Node-RED einen Zähler für die Schaltpulse eingerichtet. Per Inject Node wird der Zählerstand auf 0 gesetzt und der Zeitstempel zu diesem Zeitpunkt gespeichert.
So kann jederzeit die zur vergangenen Zeit gezählten Pulse erfasst und ausgewertet werden.
Mein letzter Test zeigte, dass auch nach einem (simulierten) Stromausfall schließlich alle ausgefallenen Pulse nachträglich erzeugt wurden.
Ich kann nicht ganz ausschließen, dass nach einem Stromausfall evtl. ein Puls zu wenig erzeugt wird, um die Uhr auf die richtige Zeit zu bringen.
Dies sollte mit einer Uhr, die ich nicht habe, getestet werden.
Für den praktischen Einsatz ist eine Funktion pulse(duration) so anzupassen, dass die physikalisch geeigneten Pulse erzeugt werden, bspw. mit zwei Ausgängen.
Das erforderliche Timing der Pulse ist relativ leicht per zwei anzulegender Schedule Jobs einzustellen.
Die von mir angelegten Schedule Jobs für Testzwecke sind folgende:
{"jobs":[
{"id":1,"enable":true,"timespec":"0 * * * * *","calls":[{"method":"Script.Eval","params":{"id":1,"code":"pulse(5000)"}}]},
{"id":2,"enable":false,"timespec":"*/5 * * * * *","calls":[{"method":"Script.Eval","params":{"id":1,"code":"pulse(1000)"}}]}
]}
Die beiden Jobs unterscheiden sich im wesentlichen in der Frequenz.
Job 1 ruft minütlich die Funktion pulse(5000) auf. Es wird somit alle 60 s ein Puls mit einer Dauer von 5000 ms = 5 s erzeugt.
Job 2 ruft alle 5 s die Funktion pulse(1000) auf, wodurch alle 5 s ein Puls mit einer Dauer von 1000 ms = 1 s erzeugt wird.
Job 1 ist für den regulären Betrieb zuständig, Job 2 für die Nachlieferung von per Stromausfall nicht gesendeten Pulse, um die Uhr auf die richtige Zeit zu stellen.
Das Skript aktiviert immer nur einen dieser beiden Jobs, sie sind also nie zugleich aktiv.
Nun das Skript:
// Author: eiche
// Device: Shelly Plus 1
// Application for controlling a slave clock
// Version: 2023-03-16_01
// There must exist two schedule jobs, a first (id=1) for sending regular pulses, a second (id=2) for sending missing pulses.
// a schedule job may create per http get
// http://<ip-address>/rpc/Schedule.Create?timespec="0 * * * * *"&calls=[{"method":"Script.Eval","params":{"id":1,"code":"pulse(5000)"}}]
// to update the timespec with schedule job id=1
// http://<ip-address>/rpc/Schedule.Update?id=1×pec="58 * * * * *"
// to update the duration parameter to 1000ms
// http://<ip-address>/rpc/Schedule.Update?id=1&calls=[{"method":"Script.Eval","params":{"id":1,"code":"pulse(1000)"}}]
// The duration parameter of function pulse() may unuse, if the duration is stored in the Config,
// but this pulse parameter allows the script independent define of the pulse duration.
// If the pulse duration in seconds is sufficient, Timer.set can be omitted and toggle_after used in Switch.Set.
// Schedule job 1 has a timespec "0 * * * * *" (every minute at 0s) and a params.code "pulse(<duration in ms>)".
// Schedule job 2 has a higher frequently timespac like "*/5 * * * * *" (every 5 seconds) and a params.code "pulse(<duration in ms>)".
// Messages sent via MQTT are not required and can be removed.
// If a non-IT savvy user wants a configuration dashboard, maybe I can build a comfortable Node-RED frontend - a donation would be helpful. ;-)
let debug = false; // set to false if no need for prints
let Device = "develop"; // the device name using in MQTT messages as pre topic
let Config = {
EventTop: Device+"/event",
StatusTop: Device+"/status",
PulseOn: '{"pulse":true}',
PulseOff: '{"pulse":false}',
PulseKey: "pulse" // key in KVS
};
let Status = {
ScriptId: null, // the id of this script, using in schedule job
PulseTs: null, // last clock trigger timestamp
SchedJob: 1, // the enabled schedule job
Remain: 0 // remaining additional pulses after reboot
};
// enable or disable a created schedule job
function scheduleJob(id, en) {
if (en) Status.SchedJob = id;
Shelly.call("Schedule.Update", {"id":id, "enable":en},
function (res, errc, errm) {
if (debug || errc) print(errc, errm, JSON.stringify(res));
}
);
}
// change between two alternative schedule jobs 1 and 2
function changeSchedJob(id) { // id of the job to enable
let i = [[2,1],[1,2]];
scheduleJob(i[id-1][0], false);
scheduleJob(i[id-1][1], true);
}
// put the new timestamp of a pulse start
// the time stamp is also stored in Status as in KVS
function putPulseTs (ts) {
Status.PulseTs = ts;
// put the timestamp additional into KVS
Shelly.call("KVS.Set", {"key":Config.PulseKey, "value":ts},
function (res, errc, errm) {
if (debug) print(errc, errm, JSON.stringify(res));
}
);
}
// send a pulse
function pulse(duration) {
if (debug) print("pulse on");
Shelly.call("Switch.Set", {"id":0, "on":true});
Timer.set(duration, false,
function(){
if (debug) print("pulse off");
Shelly.call("Switch.Toggle", {"id":0});
}
);
return duration;
}
// check the difference between two timestamps
// and add missing pulses
function checkTime(ts) { // ts = timestamp
if (Status.SchedJob===2) {
Status.Remain--;
print(Status.Remain);
if (Status.Remain===0) { // Additional pulses requred?
let dt = ts - Status.PulseTs; // elapsed time for additional pulses
putPulseTs(ts); // store pulse timestamp
print("elapsed time for additional pulses: ", dt);
Status.Remain = Math.round(dt/60); // Additional remaining pulses?
// todo: May for synchronize with higher precision a Timer.set be useful before enable schedule job 1?
if (Status.Remain===0) changeSchedJob(1);
}
} else {
if (debug) print(JSON.stringify(Status));
if (Status.PulseTs!==null) {
let dt = ts - Status.PulseTs;
Status.Remain = Math.round(dt/60) - 1; // Additional pulses required?
if (Status.Remain<0) Status.Remain = 0;
print("dt: ", dt, ", missing pulses: ", Status.Remain);
if(Status.Remain>0) changeSchedJob(2); // there are missing pulses
}
putPulseTs(ts); // store pulse timestamp
}
}
Shelly.addEventHandler(
function (event) {
if (debug) print(JSON.stringify(event));
let info = event.info;
if(info.component==="switch:0" && info.event==="toggle") {
if (debug) print("switch toggle: ", info.state);
// the use of messaging allows storage in a database ...
MQTT.publish(Config.EventTop, info.state ? Config.PulseOn : Config.PulseOff);
MQTT.publish(Config.StatusTop, JSON.stringify(Status));
if (info.state) checkTime(info.ts);
}
}
);
// get the id of this script for extensions in future
Status.ScriptId = Shelly.getCurrentScriptId();
// first disable both schedule jobs
scheduleJob(2, false);
scheduleJob(1, false);
// get the last pulse timestamp before reboot from KVS
Shelly.call("kvs.get", {"key":Config.PulseKey},
function (res, errc, errm) {
if (debug || errc) print(errc, errm, JSON.stringify(res));
if(!errc) {
Status.PulseTs = res.value; // get the timestamp from KVS
print("PulseTs: ", Status.PulseTs);
}
}
);
// enable schedule job 1 after a short while
Timer.set(1000, false, function () {scheduleJob(1, true);});
// now pulse generation can start
Alles anzeigen
Es enthält neben debug Ausgaben (print), welche per debug=true aktiviert bzw. debug=false deaktiviert werden, noch zusätzliche Ausgaben, die später entfernt werden können.
Sie dienen der Beobachtung insbesondere der Pulserzeugung nach Stromausfall.
Um einen Stromausfall zu simulieren, genügt es, das Skript anzuhalten und es nach der simulierten Ausfalldauer zu starten.
Selbstverständlich lässt sich der Code auch ein Stück weit objektorientiert verfassen.
Da ich zu diesem Projekt keine einschlägige Vorerfahrung hatte, habe ich schlicht Wert auf dessen Funktionalität und Funktionssicherheit gelegt.
Eine Grafana Zeitgrafik zur Pulserzeugung:
Links sind die in dichter Folge erzeugten nach einem Stromausfall gelieferten Uhrstell-Pulse zu sehen, rechts die regulären minütlichen Pulse.
Bei konkreten Fragen zum Skript und den Schedule Jobs gebe ich gerne Auskunft. Ob ich die Muße finden werde, zum Einsatz eine Anleitung zu verfassen, weiß ich noch nicht.
Das wird auch vom Kreis der Interessenten abhängen.
Nun hoffe ich, dass diese Implementation irgendwo ihre Anwendung findet.
Gruß aus Rheinland-Pfalz