Verified Commit 9b907b8e authored by Peter Müller's avatar Peter Müller
Browse files

Added script "Gartensteuerung"

parent 7d7d03f4
/*
* ioBroker Script Gartensteuerung
*
* Dieses Script dient der Ansteuerung der cryCode Gartensteuerung über den CAN-Bus.
*
* Copyright (C) 2020 Peter Müller <peter@crycode.de> (https://crycode.de)
*/
const socketcan = require('socketcan');
const CAN_ID_INPUTS = 0x01004001
const CAN_ID_RESET = 0x020040FF
const CAN_ID_STARTUP = 0x05004000
const CAN_ID_LCD_CMD = 0x05004001
const CAN_ID_LCD_TEXT = 0x05004002
const CAN_ID_LCD_CREATE_CHAR = 0x05004003
const CAN_ID_TEMPERATURES = 0x05004004
const CAN_ID_TIME_AND_DATE = 0x1F000001
const LCD_CMD_CLEAR = 0x01
const LCD_CMD_SET_CURSOR = 0x02
const LCD_CMD_NO_DISPLAY = 0x03
const LCD_CMD_DISPLAY = 0x04
const LCD_CMD_NO_CURSOR = 0x05
const LCD_CMD_CURSOR = 0x06
const LCD_CMD_NO_BLINK = 0x07
const LCD_CMD_BLINK = 0x08
const LCD_CMD_NO_BACKLIGHT = 0x09
const LCD_CMD_BACKLIGHT = 0x10
const LCD_CMD_SCROLL_LEFT = 0x11
const LCD_CMD_SCROLL_RIGHT = 0x12
const ID_OUT_BUESCHE_PARKPLATZ = '0_userdata.0.gartensteuerung.out.buesche_parkplatz';
const ID_OUT_BUESCHE_VORNE = '0_userdata.0.gartensteuerung.out.buesche_vorne';
const ID_OUT_HOCHBEET = '0_userdata.0.gartensteuerung.out.hochbeet';
const ID_OUT_BUESCHE_HOCHBEET = '0_userdata.0.gartensteuerung.out.buesche_hochbeet';
const ID_OUT_REGNER_HOLZHAUS = '0_userdata.0.gartensteuerung.out.regner_holzhaus';
const ID_OUT_REGNER_MITTE = '0_userdata.0.gartensteuerung.out.regner_mitte';
const ID_OUT_REGNER_TERRASSE = '0_userdata.0.gartensteuerung.out.regner_terrasse';
const ID_OUT_REGNER_VORNE = '0_userdata.0.gartensteuerung.out.regner_vorne';
const ID_OUT_STEINBEET = '0_userdata.0.gartensteuerung.out.steinbeet';
const ID_OUT_LICHT = '0_userdata.0.gartensteuerung.out.licht';
const ID_OUT_WAERMELAMPE = '0_userdata.0.gartensteuerung.out.waermelampe';
const ID_OUT_HEIZUNG = '0_userdata.0.gartensteuerung.out.heizung';
const ID_TEMPERATUR_AUSSEN = '0_userdata.0.gartensteuerung.temperatur_aussen';
const ID_TEMPERATUR_INNEN = '0_userdata.0.gartensteuerung.temperatur_innen';
const REGEX_ID_VITUAL_BUTTON = /^0_userdata.0.gartensteuerung.virtual.buttons.[0-9a-z_]+$/;
const REGEX_ID_OUT = /^0_userdata.0.gartensteuerung.out.[0-9a-z_]+$/;
const SELECTOR_ID_OUT = 'state[id=0_userdata.0.gartensteuerung.out.*]';
const ID_LCD_POS_MAPPING = {
[ID_OUT_REGNER_HOLZHAUS]: { r: 3, c: 0 },
[ID_OUT_REGNER_MITTE]: { r: 3, c: 4 },
[ID_OUT_REGNER_TERRASSE]: { r: 3, c: 8 },
[ID_OUT_REGNER_VORNE]: { r: 3, c: 12 },
[ID_OUT_HOCHBEET]: { r: 2, c: 0 },
[ID_OUT_BUESCHE_HOCHBEET]: { r: 2, c: 4 },
[ID_OUT_BUESCHE_PARKPLATZ]: { r: 2, c: 8 },
[ID_OUT_BUESCHE_VORNE]: { r: 2, c: 12 },
[ID_OUT_STEINBEET]: { r: 2, c: 16 }
};
const ID_TEXT_MAPPING = {
[ID_OUT_REGNER_HOLZHAUS]: '0_userdata.0.gartensteuerung.virtual.times.regner_holzhaus',
[ID_OUT_REGNER_MITTE]: '0_userdata.0.gartensteuerung.virtual.times.regner_mitte',
[ID_OUT_REGNER_TERRASSE]: '0_userdata.0.gartensteuerung.virtual.times.regner_terrasse',
[ID_OUT_REGNER_VORNE]: '0_userdata.0.gartensteuerung.virtual.times.regner_vorne',
[ID_OUT_HOCHBEET]: '0_userdata.0.gartensteuerung.virtual.times.hochbeet',
[ID_OUT_BUESCHE_HOCHBEET]: '0_userdata.0.gartensteuerung.virtual.times.buesche_hochbeet',
[ID_OUT_BUESCHE_PARKPLATZ]: '0_userdata.0.gartensteuerung.virtual.times.buesche_parkplatz',
[ID_OUT_BUESCHE_VORNE]: '0_userdata.0.gartensteuerung.virtual.times.buesche_vorne',
[ID_OUT_STEINBEET]: '0_userdata.0.gartensteuerung.virtual.times.steinbeet'
};
const Q_INTERVAL = 1000;
const TIME_PER_PUSH = 5 * 60000;
class GardenControll {
constructor () {
this.addQ = this.addQ.bind(this);
this.handleQ = this.handleQ.bind(this);
this.stopQ = this.stopQ.bind(this);
this.handleCanMsg = this.handleCanMsg.bind(this);
this.handleBtnPress = this.handleBtnPress.bind(this);
this.handleStateChangeOut = this.handleStateChangeOut.bind(this);
this.handleStateChangeVirtualButton = this.handleStateChangeVirtualButton.bind(this);
this.handleStartup = this.handleStartup.bind(this);
this.sendDateTime = this.sendDateTime.bind(this);
this.sendQTimeToLcd = this.sendQTimeToLcd.bind(this);
this.sendQTimeToLcdAll = this.sendQTimeToLcdAll.bind(this);
this.q = [];
this.qTimer = null;
this.inputBytes = {
0: 255,
1: 255,
2: 255
};
this.idLcdText = {};
this.can = socketcan.createRawChannel('can0', true);
this.can.addListener('onMessage', this.handleCanMsg);
this.can.start();
schedule('* * * * *', this.sendDateTime);
on({id: REGEX_ID_OUT, change: 'any', ack: false}, this.handleStateChangeOut);
on({id: REGEX_ID_VITUAL_BUTTON, change: 'any', ack: false}, this.handleStateChangeVirtualButton);
// register onStop callback
onStop((cb) => {
// stop the queue if active
this.stopQ();
// do the rest delayed to allow the stop to be send
setTimeout(() => {
// stop can interface
this.can.stop();
// stop queue interval
clearInterval(this.qTimer);
cb();
}, 1000);
}, 2000);
this.handleStartup();
}
addQ (id, time) {
if (!id || !time) return;
const q = this.q.find((e) => e.id === id);
if (q) {
// add time
q.time += time;
//log(`${id} added ${time}ms to existing entry`);
} else {
// new entry
this.q.push({
id,
time,
active: false
});
//log(`${id} added ${time}ms to new entry`);
}
this.sendQTimeToLcd(id);
// start queue if not already active
if (this.qTimer === null) {
this.qTimer = setInterval(this.handleQ, Q_INTERVAL);
}
}
stopQ () {
if (this.qTimer !== null) {
clearInterval(this.qTimer);
this.qTimer = null;
}
// stop active
if (this.q[0] && this.q[0].active) {
setState(this.q[0].id, false, false);
}
// empty queue
this.q = [];
// update lcd
this.sendQTimeToLcdAll();
}
handleQ () {
if (this.q.length === 0) {
//log('Queue is empty');
this.stopQ();
return;
}
if (!this.q[0].active) {
// start
this.q[0].active = true;
setState(this.q[0].id, true, false);
//log(`${this.q[0].id} started`);
return;
}
this.q[0].time -= Q_INTERVAL;
if (this.q[0].time <= 0) {
// stop and remove from q
setState(this.q[0].id, false, false);
this.sendQTimeToLcd(this.q[0].id);
//log(`${this.q[0].id} done and removed`);
this.q.shift();
} else {
// just update the lcd time
this.sendQTimeToLcd(this.q[0].id);
}
}
handleCanMsg (msg) {
switch (msg.id) {
case CAN_ID_STARTUP:
setTimeout(this.handleStartup, 2000);
break;
case CAN_ID_INPUTS:
for (let ic = 0; ic < 3; ic++) {
if (msg.data[ic] !== this.inputBytes[ic]) {
// changed
//log(`change at ic ${ic} ${msg.data[ic].toString(2)}`);
for (let i = 0; i < 8; i++) {
if ((msg.data[ic] & (1<<i)) === 0) {
const btnId = 1 + ic*8 + i;
//log(`${ic} ${i} #${btnId} pressed`);
this.handleBtnPress(btnId);
}
}
this.inputBytes[ic] = msg.data[ic];
}
}
break;
case CAN_ID_TEMPERATURES:
const temp0 = msg.data.readFloatLE(0);
const temp1 = msg.data.readFloatLE(4);
if (temp0 > -99) {
setState(ID_TEMPERATUR_AUSSEN, temp0, true);
}
if (temp1 > -99) {
setState(ID_TEMPERATUR_INNEN, temp1, true);
}
break;
}
}
handleBtnPress (btnId) {
switch (btnId) {
case 1: // Hochbeet
this.addQ(ID_OUT_HOCHBEET, TIME_PER_PUSH);
break;
case 2: // Büsche Hochbeet
this.addQ(ID_OUT_BUESCHE_HOCHBEET, TIME_PER_PUSH);
break;
case 3: // Büsche Parkplatz
this.addQ(ID_OUT_BUESCHE_PARKPLATZ, TIME_PER_PUSH);
break;
case 4: // Büsche vorne
this.addQ(ID_OUT_BUESCHE_VORNE, TIME_PER_PUSH);
break;
case 5: // Steinbeet
this.addQ(ID_OUT_STEINBEET, TIME_PER_PUSH);
break;
case 6: // Regner Holzhaus
this.addQ(ID_OUT_REGNER_HOLZHAUS, TIME_PER_PUSH);
break;
case 7: // Regner Mitte
this.addQ(ID_OUT_REGNER_MITTE, TIME_PER_PUSH);
break;
case 8: // Regner Terrasse
this.addQ(ID_OUT_REGNER_TERRASSE, TIME_PER_PUSH);
break;
case 9: // Regner vorne
this.addQ(ID_OUT_REGNER_VORNE, TIME_PER_PUSH);
break;
case 10: // Stopp
this.stopQ();
setState(ID_OUT_BUESCHE_PARKPLATZ, false, false);
setState(ID_OUT_BUESCHE_VORNE , false, false);
setState(ID_OUT_HOCHBEET , false, false);
setState(ID_OUT_BUESCHE_HOCHBEET , false, false);
setState(ID_OUT_REGNER_HOLZHAUS , false, false);
setState(ID_OUT_REGNER_MITTE , false, false);
setState(ID_OUT_REGNER_TERRASSE , false, false);
setState(ID_OUT_REGNER_VORNE , false, false);
setState(ID_OUT_STEINBEET , false, false);
break;
case 11: // P1
this.addQ(ID_OUT_REGNER_HOLZHAUS, 60000 * 10);
this.addQ(ID_OUT_REGNER_MITTE, 60000 * 10);
this.addQ(ID_OUT_REGNER_TERRASSE, 60000 * 10);
this.addQ(ID_OUT_REGNER_VORNE, 60000 * 5);
break;
case 12: // P2
this.addQ(ID_OUT_BUESCHE_PARKPLATZ, 60000 * 10);
this.addQ(ID_OUT_BUESCHE_VORNE, 60000 * 10);
//this.addQ(ID_OUT_BUESCHE_HOCHBEET, 60000 * 10);
this.addQ(ID_OUT_STEINBEET, 60000 * 10);
//this.addQ(ID_OUT_HOCHBEET, 60000 * 5);
break;
case 13: // P3
break;
case 14: // P4
break;
case 15: // P5
this.addQ(ID_OUT_REGNER_HOLZHAUS, 60000 * 5);
this.addQ(ID_OUT_REGNER_MITTE, 60000 * 5);
this.addQ(ID_OUT_REGNER_TERRASSE, 60000 * 5);
this.addQ(ID_OUT_REGNER_VORNE, 60000 * 5);
this.addQ(ID_OUT_BUESCHE_PARKPLATZ, 60000 * 5);
this.addQ(ID_OUT_BUESCHE_VORNE, 60000 * 5);
//this.addQ(ID_OUT_BUESCHE_HOCHBEET, 60000 * 5);
this.addQ(ID_OUT_STEINBEET, 60000 * 5);
this.addQ(ID_OUT_HOCHBEET, 60000 * 5);
break;
case 17: // Licht
setState(ID_OUT_LICHT, !getState(ID_OUT_LICHT).val, false);
break;
case 18: // Wärmelampe
setState(ID_OUT_WAERMELAMPE, !getState(ID_OUT_WAERMELAMPE).val, false);
break;
case 19: // Heizung
setState(ID_OUT_HEIZUNG, !getState(ID_OUT_HEIZUNG).val, false);
break;
default:
log(`Button ${btnId} was pressed but there is currently no handler for it.`, 'warn');
}
}
handleStateChangeVirtualButton (obj) {
const btnId = obj.native.btnId;
if (!btnId) {
log(`No btnId specified at ${obj.id}!`);
return;
}
this.handleBtnPress(btnId);
}
handleStateChangeOut (obj) {
// get the can ID
const canId = parseInt(obj.native.canId, 16);
if (!canId) {
log(`No canId specified at ${obj.id}!`);
return;
}
// send can message
this.can.send({
id: canId,
ext: true, rtr: false,
data: Buffer.from([obj.state.val ? 0x01 : 0x00])
});
// set ack
const state = obj.state;
state.ack = true;
setState(obj.id, {
...obj.state,
ack: true
});
}
handleStartup () {
// clear LCD
this.can.send({
id: CAN_ID_LCD_CMD,
ext: true,
rtr: false,
data: Buffer.from([LCD_CMD_CLEAR])
});
// send time and date
this.sendDateTime();
// send current states
$(SELECTOR_ID_OUT).each((id) => {
const canId = parseInt(getObject(id).native.canId, 16);
if (!canId) return;
this.can.send({
id: canId,
ext: true, rtr: false,
data: Buffer.from([getState(id).val ? 0x01 : 0x00])
});
});
// send lcd texts
this.sendQTimeToLcdAll();
}
sendQTimeToLcdAll () {
this.idLcdText = {}; // clear cache
for (let id of Object.keys(ID_LCD_POS_MAPPING)) {
this.sendQTimeToLcd(id);
}
}
sendQTimeToLcd (id) {
if (!ID_LCD_POS_MAPPING[id]) return; // no pos given for this
const idx = this.q.findIndex((e) => e.id === id);
let time = idx >= 0 ? this.q[idx].time : 0;
let str;
if (time <= 0) {
// no time left
str = ' - ';
} else if (time > 60000) {
// more than 1m left
time = Math.floor(time/60000);
str = time > 9 ? `${time}m` : ` ${time}m`;
} else {
// less than 1m left
time = Math.floor(time/1000);
str = time > 9 ? `${time}s` : ` ${time}s`;
}
// check if we send this text already
if (this.idLcdText[id] === str) {
// no need to send
return;
}
const buf = Buffer.alloc(4);
buf[0] = (ID_LCD_POS_MAPPING[id].r & 0b00000111) | (ID_LCD_POS_MAPPING[id].c << 3); // position
buf.write(str, 1, 3, 'latin1');
this.can.send({
id: CAN_ID_LCD_TEXT,
ext: true,
rtr: false,
data: buf
});
this.idLcdText[id] = str;
// save to state for vis
if (ID_TEXT_MAPPING[id]) {
setState(ID_TEXT_MAPPING[id], str, true);
}
}
sendDateTime () {
const d = new Date();
const buf = Buffer.alloc(7);
buf[1] = Math.floor(d.getFullYear() / 100);
buf[0] = d.getFullYear() - (buf[1] * 100);
buf[2] = d.getMonth() + 1;
buf[3] = d.getDate();
buf[4] = d.getHours();
buf[5] = d.getMinutes();
buf[6] = 0; // d.getSeconds();
this.can.send({
id: CAN_ID_TIME_AND_DATE,
ext: true,
rtr: false,
data: buf
});
}
}
const gc = new GardenControll();
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment