// #############################################
// Author: Eisbaeeer
// Date: 20250519
//
// Dieses Script ermöglicht es, mit einem OpenEpaperTag und Tastern an dem Display (oder touch mit 4" Display), mehrere View´s durchzublättern. Die Tasten werden zum vor- bzw. zurückblättern
// verwendet. Bevor das Script verwendet werden kann, müssen die Views angelegt werden. Die Views werden durchnummeriert, beginnend bei 1. Der "Basis" Name der View muss unten konfiguriert werden.
// Die Anzahl der verwendeten Seiten werden mit "pageCount" konfiguriert.
// Für jede View muss noch der Selektor 'waitForSelectorArray' definiert werden. Im Beispiel wartet puppettier beim View1 auf den Selector '#w00042'.
// Die Erste View ist eine Uhr, welche jede Minute aktualisiert wird.
//
// In dem Script wird die Socket-Verbindung von Beowolf verwendet (https://forum.iobroker.net/topic/66380/e-ink-display-openepaperlink-displayanzeige-mit-batterie/809?_=1747419968864)
//
// Version: 0.4 : 20250519 Added socket
// Version: 0.3 : 20250518 AP-4" kann in Verbindung mit iobroker.open-epaper-llink Adapter mehrere Ansichten durchblättern
// Version 0.2 : Bugfix async pictures
// Version: 0.1 : Convert from Blockly to js
//
// Benötigt:
// - Puppeteer Adapter
// - Chrome Headless
//
// #############################################
// Hier die Anpassungen vornehmen!
const urlOfVISView = 'http://192.168.1.200:8082/vis/index.html?OpenEpaper#Tag4Zoll'; // Eure View oder URL vom Bild + Nummer der View beginnend bei 1
const targetWidth = 480; // Breite des Displays
const targetHeight = 480; // Höhe des Displays
const cutoutX = 0; // Abstand Pixel von links für Screenshot
const cutoutY = 0; // Abstand Pixel von oben für Screenshot
const pquality = 100; // Bildqualität
const pageCount = 3; // Anzahl der Seiten zum Durchblättern
const waitForSelectorArray = ["","#w00042","#w00044","#w00053"]; // Letztes Element auf das Peppeteer wartet als Array
const inputPath = "/tmp/Tag01_480x480"; // Screenshot temporär Format: /Pfad/prefix
const accesspointIP = '192.168.1.78'; // Accesspoint IP Adresse
const macAddress = '0000840B822F3728'; // MAC-Adresse des Displays anpassen
const ditherValue = 1; // Setze den Dither-Wert (Farbanpassungen: 0 = View hat gleiche Farben | 1 = View hat andere Farben als das Display
const autostart = true; // Websocket automatisch starten? true|false
// ENDE Anpassungen! Ab hier nichts mehr ändern!
// #############################################
var buttons = '0_userdata.0.open-epaper-link.Tag_Buttons.' + macAddress + '.page';
createState(buttons,1);
var uploadUrl = 'http://' + accesspointIP + '/imgupload';
var wakeupR = '0_userdata.0.open-epaper-link.Tag_Buttons.' + macAddress + '.wakeupReason';
var wakeup = '0_userdata.0.open-epaper-link.Tag_Buttons.' + macAddress + '.lastseen';
schedule("* * * * *", function() {
console.log("Send clock page every minute if pageVal is 1");
var pageVal = getState(buttons).val;
console.log("Page: " + pageVal);
let waitForSelector = waitForSelectorArray[pageVal];
console.log("waitForSelector: " + waitForSelector);
var view = urlOfVISView+pageVal;
console.log("View: " + view);
if ( pageVal == 1 ) {
takeScreenshots(pageVal,waitForSelector,view);
}
});
on(wakeup,function() {
var pageVal = getState(buttons).val;
if ( getState(wakeupR).val == 5 ) {
if (pageVal >= pageCount) {
setState(buttons, 1 );
pageVal = 1;
} else {
pageVal++;
setState(buttons,pageVal);
}
let waitForSelector = waitForSelectorArray[pageVal];
console.log("waitForSelector: " + waitForSelector);
var view = urlOfVISView+pageVal;
console.log("View: " + view);
takeScreenshots(pageVal,waitForSelector,view);
}
if ( getState(wakeupR).val == 4 ) {
if (pageVal <= 1 ) {
setState(buttons,pageCount);
pageVal = pageCount;
} else {
pageVal--;
setState(buttons,pageVal);
}
let waitForSelector = waitForSelectorArray[pageVal];
console.log("waitForSelector: " + waitForSelector);
var view = urlOfVISView+pageVal;
console.log("View: " + view);
takeScreenshots(pageVal,waitForSelector,view);
}
setState(wakeupR,0);
});
// Requirements
const puppeteer = require('puppeteer');
const axios = require('axios');
const fs = require('fs');
const FormData = require('form-data');
async function takeScreenshots(pageVal,waitForSelector,view) {
console.log("*** function takeScreenshot");
sendTo('puppeteer.0', 'screenshot', {
url: view,
path: inputPath+macAddress+pageVal+'.jpg',
width: targetWidth,
height: targetHeight,
quality: pquality,
waitOption: {
waitForSelector: waitForSelector,
waitForTimeout: 10000
},
fullPage: false,
clip: {
x: cutoutX,
y: cutoutY,
width: targetWidth,
height: targetHeight
}
}, obj => {
if (obj.error) {
log(`Error taking screenshot: ${obj.error.message}`, 'error');
} else {
// the binary representation of the image is contained in `obj.result`
log(`Successfully took screenshot: ${obj.result}`);
console.log("*** upload Image");
// uploading Screenshot
async function uploadImage() {
try {
// Prüfen, ob die Datei existiert
if (!fs.existsSync(inputPath+pageVal+'.jpg')) {
throw new Error(`Datei nicht gefunden: ${inputPath+macAddress+pageVal+'.jpg'}`);
}
// FormData erstellen
const form = new FormData();
form.append("mac", macAddress);
form.append("dither", ditherValue); // Dither-Parameter hinzufügen
form.append("file", fs.createReadStream(inputPath+macAddress+pageVal+'.jpg'));
// POST-Anfrage senden
const response = await axios.post(uploadUrl, form, {
headers: {
...form.getHeaders(),
},
});
console.log('Upload erfolgreich:', response.data);
} catch (error) {
if (error.response) {
console.error('Server-Antwort:', error.response.status, error.response.data);
} else if (error.request) {
console.error('Keine Antwort erhalten:', error.request);
} else {
console.error('Fehler beim Hochladen:', error.message);
}
}
}
uploadImage();
}
})
}
const WebSocket = require('ws');
const http = require('http');
const wsUrl = ws://${accesspointIP}/ws
; // WebSocket-URL
let ws;
let pingInterval;
let scriptStopping = false; // Flag, um zu prüfen, ob das Skript gestoppt wird
const controlState = '0_userdata.0.open-epaper-link.Start'; // Datenpunkt zur Steuerung des Skripts - anhalten oder starten
function ensureOpenEPaperLinkFolderExists(callback) {
const OpenEPaperLinkFolderPath = '0_userdata.0.open-epaper-link.Tag_Buttons';
getObject(OpenEPaperLinkFolderPath, (err, obj) => {
if (err || !obj) {
setObject(OpenEPaperLinkFolderPath, {
type: 'channel',
common: { name: 'Open E-Paper Link' },
native: {}
}, callback);
} else {
callback();
}
});
}
function ensureChannelExists(path, alias, callback) {
getObject(path, (err, obj) => {
if (err || !obj) {
setObject(path, {
type: 'channel',
common: { name: alias || 'Unbekanntes Gerät' },
native: {}
}, callback);
} else if (obj.common.name !== alias) {
extendObject(path, { common: { name: alias } }, callback);
} else {
callback();
}
});
}
function createStateAndSet(statePath, value) {
setObject(statePath, {
type: 'state',
common: {
name: statePath.split('.').pop(),
type: 'string',
role: 'value',
read: true,
write: true
},
native: {}
}, (err) => {
if (!err) {
setState(statePath, String(value), true);
}
});
}
function updateStateIfChanged(statePath, value) {
getState(statePath, (err, state) => {
if (err || !state) {
createStateAndSet(statePath, String(value));
} else if (state.val !== String(value)) {
setState(statePath, String(value), true);
}
});
}
function fetchDimensions(hwType, callback) {
const hwTypeHex = hwType.toString(16).padStart(2, '0').toUpperCase(); // Convert hwType to two-digit uppercase hexadecimal
const url = http://${accesspointIP}/tagtypes/${hwTypeHex}.json
;
http.get(url, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
if (res.statusCode === 200) {
try {
const dimensions = JSON.parse(data);
callback(null, dimensions);
} catch (e) {
callback(Error parsing JSON from ${url}: ${e}
);
}
} else {
callback(HTTP Error ${res.statusCode} from ${url}
);
}
});
}).on('error', (err) => {
callback(Error fetching ${url}: ${err.message}
);
});
}
function handleHWType(basePath, hwType) {
createStateAndSet(${basePath}.hwType
, String(hwType)); // Save hwType as a state
fetchDimensions(hwType, (err, dimensions) => {
if (!err && dimensions) {
createStateAndSet(${basePath}.height
, String(dimensions.height));
createStateAndSet(${basePath}.width
, String(dimensions.width));
createStateAndSet(${basePath}.name
, String(dimensions.name));
if (dimensions.colors) {
createStateAndSet(${basePath}.colors
, String(dimensions.colors));
}
if (dimensions.colortable) {
createStateAndSet(${basePath}.colortable
, JSON.stringify(dimensions.colortable));
}
} else {
// console.error(Failed to fetch or set dimensions for hwType ${hwType}: ${err}
);
}
});
}
function connectWebSocket() {
if (scriptStopping) {
return; // Wenn das Skript gestoppt wird, keine Verbindung mehr herstellen
}
ws = new WebSocket(wsUrl);
ws.on('open', function open() {
// console.log('Verbunden mit WebSocket');
startHeartbeat();
});
ws.on('message', function incoming(data) {
// console.log('Daten empfangen:', data);
if (data) {
try {
let parsedData = JSON.parse(data);
// console.log('Verarbeitete Daten:', JSON.stringify(parsedData, null, 2));
handleData(parsedData);
} catch (err) {
// console.error('Fehler bei der Verarbeitung der Daten:', err);
}
} else {
// console.log('Keine Daten oder leere Nachricht empfangen');
}
});
ws.on('close', function close() {
if (!scriptStopping) {
// console.log('WebSocket-Verbindung geschlossen, versuche neu zu verbinden...');
clearInterval(pingInterval);
setTimeout(connectWebSocket, 5000);
}
});
ws.on('error', function error(err) {
// console.error('WebSocket-Fehler:', err);
});
}
function startHeartbeat() {
pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping(() => {
// console.log('Ping sent');
});
}
}, 10000); // Send ping every 10 seconds
ws.on('pong', () => {
// console.log('Pong received');
});
}
function handleData(parsedData) {
if (parsedData.tags && Array.isArray(parsedData.tags)) {
parsedData.tags.forEach(tag => {
let basePath = 0_userdata.0.open-epaper-link.Tag_Buttons.${tag.mac.replace(/:/g, '')}
;
ensureChannelExists(basePath, tag.alias, () => {
Object.keys(tag).forEach(key => {
let statePath = ${basePath}.${key}
;
let value = tag[key];
updateStateIfChanged(statePath, value);
if (key === 'hwType') {
handleHWType(basePath, tag.hwType);
}
});
});
});
}
}
function disconnectWebSocket() {
if (ws) {
ws.close();
ws = null;
}
clearInterval(pingInterval);
}
// Skript-Start und -Stopp basierend auf einem Datenpunkt steuern
function setupScriptControl() {
if (autostart == true) {
ensureOpenEPaperLinkFolderExists(connectWebSocket);
}
setObject(controlState, {
type: 'state',
common: {
name: 'EPaper Script Control',
type: 'boolean',
role: 'switch',
read: true,
write: true,
def: false
},
native: {}
});
on({id: controlState, change: 'ne'}, (obj) => {
const state = obj.state.val;
if (state) {
// Skript starten
scriptStopping = false;
ensureOpenEPaperLinkFolderExists(connectWebSocket);
} else {
// Skript stoppen
scriptStopping = true;
disconnectWebSocket();
// console.log('Skript beendet durch Steuer-Datenpunkt');
}
});
}
// Initiale Einrichtung
setupScriptControl();