Dieses Skript ist das erste echte Release. Es darf von hier per "mark, copy and paste" heruntergeladen werden.
Ich übernehme keine Gewähr für die Folgen des Einsatzes, darf aber feststellen, dass die Implementation sehr solide gebaut ist.
// (C) Gerhard Eichelsdörfer alias eiche
// Device: Shelly Plus 2
// Application for controlling a slave clock
// Version: 2023-04-17_01
// description at https://zisterne.eichelsdoerfer.net/index.php/projekte/89-emulation-einer-hauptuhr-per-shelly-plus-2
// There must exist two schedule jobs or more, 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(500)"}}]
// 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 400ms
// http://<ip-address>/rpc/Schedule.Update?id=1&calls=[{"method":"Script.Eval","params":{"id":1,"code":"pulse(400)"}}]
// 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 "x * * * * *" (every minute at second x) and a params.code "pulse(<duration in ms>)".
// Schedule job 2 has a higher frequently timespec like "*/2 * * * * *" (every 2 seconds) and a params.code "pulse(<duration in ms>)".
let debug = false; // set to true if you need prints
let chkTime = false; // set to true if you want to check the values of getTime()
let AppKey = "app_"; // the key prefix for app keys in KVS
let Config = {
DailyMin: 1440, // number of minutes of a day
ClkPeriod: 720, // clock period in minutes - change it to 1440, if your clock shows 24 hours or set in KVS app_period to 24
Decision: 2, // decision factor for adjusting the clock display
NvsEver: true, // if true, timestamp is written at every pulse, also when clock is adjusted
// If you change any of the following string values, you must also change the assigned structure identifier
// at the end of this script in Shelly.call("KVS.GetMany", {"match":AppKey+'*'}, ...).
KeyPulse: AppKey+"pulse", // key of pulse timestamp in KVS
KeyPeriod: AppKey+"period", // key of clock period
KeyDecision: AppKey+"decision" // key of decision factor
};
let Status = {
PulseTs: 0, // last clock trigger timestamp, default 0 daily minutes = 00:00
SchedJob: 0, // the enabled schedule job, 0: neither job 1 nor job 2
Remain: 0, // remaining additional pulses
En: false, // if true, the clock works, else it sleeps
Run: true // the running status of the clock
};
// enable or disable one of the schedule jobs (1 or 2) without checking the id
function scheduleJob(id, en) {
let doit = (Status.SchedJob & id)===id ? !en : en; // on change
if (doit) {
Shelly.call("Schedule.Update", {"id":id, "enable":en},
function (res, errc, errm) {
if (errc) print(errc, errm, JSON.stringify(res));
}
);
}
if (en) Status.SchedJob |= id else Status.SchedJob &= ~id; // register in Status.SchedJob
}
// change between two alternative schedule jobs 1 and 2
function changeSchedJob(id) { // id of the job to enable
if (Status.SchedJob===0) scheduleJob(id, true)
else {
let i = [[2,1],[1,2]];
scheduleJob(i[id-1][0], false);
scheduleJob(i[id-1][1], true);
}
}
// store the number of daily minutes in KVS
function putPulseTs () {
Shelly.call("KVS.Set", {"key":Config.KeyPulse, "value":Status.PulseTs},
function (res, errc, errm) {
if (errc) print(errc, errm, JSON.stringify(res));
}
);
}
// send a pulse if the clock is in running mode
function pulse(duration) {
if (Status.En) {
if (Status.Run) {
Shelly.call("Switch.Set", {"id":0, "on":true});
Timer.set(duration, false,
function(){
Shelly.call("Switch.Toggle", {"id":0});
}
);
} else {
Status.Remain++;
if (Status.Remain===0) Status.Run = true;
}
return duration;
}
}
// calculate the number of pulses including the minutes while schedule job 2 works
function calcPulses(n) {
let a = Math.floor((n+14)/30);
return n + a + Math.floor((a+14)/30); // pulses in distances of 2s
}
// add n pulses, e.g. to adjust the displayed time or to switch to "summer time"
function addPulses(n, force) {
if (force===undefined || force===null) force = false;
if (Status.Run || force) {
Status.Remain = calcPulses(n);
Status.Run = Status.Remain>=0 ? true : false; //Status.Run = Status.Remain>=0;
if (chkTime) print("addPulses n:", n, "Status:", JSON.stringify(Status));
if (Status.Remain>0) changeSchedJob(2);
}
}
// substract n pulses, e.g. to adjust the displayed time or to switch to "normal time"
function subPulses(n, force) {
if (force===undefined || force===null) force = false;
if (Status.Run || force) {
Status.Remain = -n;
Status.Run = Status.Remain===0 ? true : false; //Status.Run = Status.Remain===0;
if (chkTime) print("subPulses n:", n, "Status:", JSON.stringify(Status));
changeSchedJob(1);
}
}
// adjust timestamp in relation to display period
function adjTsPeriod(dt) {
while (dt>=Config.ClkPeriod) {
dt -= Config.ClkPeriod;
Status.PulseTs += Config.ClkPeriod;
Status.PulseTs %= Config.DailyMin;
putPulseTs();
}
while (dt<0) {
dt += Config.ClkPeriod;
if (Status.PulseTs>=Config.ClkPeriod) Status.PulseTs -= Config.ClkPeriod;
putPulseTs();
}
return dt;
}
// a user can adjust the displayed time
function setClockRel(dt) {
Status.En = true;
if(dt===undefined || dt===null || dt===0) {
Status.Run = true;
return "synchronized, nothing else to do";
}
dt = adjTsPeriod(dt);
if(dt>0) addPulses(dt, true) else subPulses(-dt, true);
return "setting clock relative by "+JSON.stringify(dt)+" minutes";
}
// syncs the displayed time by a user
// todo:
// 1. import an additional date
// 2. store date and time into KVS per JSON string {"lastSync":{"date":<date>,"time":<time>}}
function setClock(h, m) {
if (h<0 || h>23 || m<0 || m>59) return "invalid time";
let tt = 60*h + m; // target time
let hh = JSON.stringify(h), mm = JSON.stringify(m);
let dt = adjTsPeriod(tt-Status.PulseTs);
Status.En = true;
if(dt>0) addPulses(dt) else if(dt<0) subPulses(-dt) else Status.Run = true;
return "setting clock to "+(hh.length<2?"0":"")+hh+':'+(mm.length<2?"0":"")+mm+" -> "+JSON.stringify(tt);
}
// set the actually valid daily minutes as result of the imported time
// this should be applied depending on the actual time displayed
function setPulseTs(h, m) {
if (h<0 || h>23 || m<0 || m>59) return "invalid time";
Status.PulseTs = 60*h + m; // convert into local day minute
// put the timestamp additional into KVS
Status.En = false;
Shelly.call("KVS.Set", {"key":Config.KeyPulse, "value":Status.PulseTs},
function (res, errc, errm) {
if (errc) print(errc, errm, JSON.stringify(res))
else Status.En = true;
}
);
return "setting day minutes to " + JSON.stringify(Status.PulseTs);
}
// check the synchronization and adjust the daily minutes if available
// this also implements daylight saving time and standard time changeover automation
function getTime() {
Shelly.call("Sys.GetStatus", {}, function (res) {
if (res.time===null) return;
// from time string
let h = JSON.parse(res.time.slice(0,2));
let m = JSON.parse(res.time.slice(3));
let DayMin = 60*h+m; // local elapsed minutes of this day
let dt = DayMin - Status.PulseTs;
dt = adjTsPeriod(dt);
if (chkTime) {
print("getTime: DayMin =", DayMin, ", Status.PulseTs = ", Status.PulseTs, ", dt =", dt);
print(JSON.stringify(Status));
}
Status.En = true;
Status.Run = true;
if (dt!==0) {
// The fast action must be Config.Decision or more faster than the more economical action to be used.
if (dt < Math.floor(Config.ClkPeriod/(30+Config.Decision))*30) addPulses(dt)
else subPulses(Config.ClkPeriod-dt);
}
});
}
// the event handler when changing output 0
Shelly.addEventHandler(
function (event) {
let info = event.info;
if(info.component==="switch:0" && info.event==="toggle") {
if (debug) print(JSON.stringify(event));
if (Status.En && info.state) {
Status.PulseTs = (Status.PulseTs + 1) % Config.DailyMin;
if (Config.NvsEver) putPulseTs();
if (Status.SchedJob===2) {
Status.Remain--;
if (Status.Remain<=0) changeSchedJob(1);
} else if (!Config.NvsEver) putPulseTs();
}
else Timer.set(500, false, function(){ Shelly.call("switch.toggle", {"id":1}); }); // new, instead of output configuration
}
}
);
// get the status of schedule job 1 vs. job 2
Shelly.call("Schedule.List", {}, function(res) {
if (res.jobs[0].enable) Status.SchedJob |= 1;
if (res.jobs[1].enable) Status.SchedJob |= 2;
});
// get the last pulse timestamp before reboot from KVS
Shelly.call("KVS.GetMany", {"match":AppKey+'*'},
function (res, errc, errm) {
if (errc) print(errc, errm, JSON.stringify(res));
if(!errc) {
let msg = "{";
let itemsStr = JSON.stringify(res.items);
if(debug) print(itemsStr);
if (itemsStr.indexOf(Config.KeyPulse)>=0) {
Status.PulseTs = res.items.app_pulse.value; // get the timestamp, app_pulse may be changed - look at Config
msg += '"PulseTs":'+JSON.stringify(Math.round(Status.PulseTs));
}
if (itemsStr.indexOf(Config.KeyPeriod)>=0) {
let cp = res.items.app_period.value; // get the clock period, period may be changed - look at Config
if (cp>0) {
Config.ClkPeriod = cp * 60;
msg += ', "ClkPeriod":'+JSON.stringify(Config.ClkPeriod);
}
}
if (itemsStr.indexOf(Config.KeyDecision)>=0) {
let cd = res.items.app_decision.value; // get the decision factor, decision may be changed - look at Config
if (cd>0) {
Config.Decision = cd;
msg += ', "Decision":'+JSON.stringify(Config.Decision);
}
}
msg += '}';
if (debug) print("from KVS: ", msg);
}
}
);
// after a short while enable schedule job 1 and start the clock
Timer.set(1000, false, function () {
changeSchedJob(1);
});
// now pulse generation can be processed