Hi there,
I want to use ESPHome and get data over Lora communication with ESP32-receiver.
Is a 'New Device' set-up procedure in ESPHome required?
Can you help me to get the 'YAWL code' used in ESPHome?
Are there alternative solutions?
Currently using C++ code and displaying on html-page.
Running code on ESP32-WROOM:
#pragma once
#include <SPI.h>
#include <LoRa.h>
#include <WiFi.h>
#include <WebServer.h>
#include <ArduinoJson.h>
static const long LORA_FREQUENCY = 868E6;
static const int LORA_SCK = 18;
static const int LORA_MISO = 19;
static const int LORA_MOSI = 23;
static const int LORA_SS = 5;
static const int LORA_RST = 14;
static const int LORA_DIO0 = 26;
struct SensorPacket {
uint32_t counter;
float voltage;
float current;
float power;
float energy;
float frequency;
float powerFactor;
};
WebServer server(80);
SensorPacket latestPacket = {};
bool hasPacket = false;
int lastRssi = 0;
unsigned long lastPacketMs = 0;
const char PAGE_HTML[] PROGMEM = R"HTML(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PZEM LoRa Monitor</title>
<style>
:root {
--bg1: #0f172a;
--bg2: #1e293b;
--card: rgba(255,255,255,0.08);
--line: rgba(255,255,255,0.12);
--text: #e2e8f0;
--muted: #94a3b8;
--accent: #22c55e;
--warn: #f59e0b;
--v-color: #38bdf8;
--i-color: #22c55e;
--p-color: #f97316;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Segoe UI", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top left, #1d4ed8 0%, transparent 30%),
radial-gradient(circle at top right, #059669 0%, transparent 25%),
linear-gradient(160deg, var(--bg1), var(--bg2));
min-height: 100vh;
}
.wrap {
max-width: 1100px;
margin: 0 auto;
padding: 24px;
}
.hero {
padding: 24px;
border: 1px solid var(--line);
background: var(--card);
border-radius: 20px;
backdrop-filter: blur(10px);
margin-bottom: 20px;
}
h1, h2 {
margin: 0 0 8px;
}
h1 {
font-size: clamp(28px, 5vw, 42px);
}
h2 {
font-size: 20px;
}
p {
margin: 0;
color: var(--muted);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-top: 20px;
}
.charts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
margin-top: 20px;
}
.card {
border: 1px solid var(--line);
background: var(--card);
border-radius: 18px;
padding: 18px;
backdrop-filter: blur(8px);
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
}
.label {
color: var(--muted);
font-size: 14px;
margin-bottom: 10px;
}
.value {
font-size: 30px;
font-weight: 700;
line-height: 1.1;
}
.meta {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 20px;
color: var(--muted);
font-size: 14px;
}
.status {
display: inline-block;
padding: 6px 10px;
border-radius: 999px;
font-weight: 600;
}
.ok { background: rgba(34,197,94,0.15); color: #86efac; }
.warn { background: rgba(245,158,11,0.15); color: #fcd34d; }
.chart-card {
padding-bottom: 14px;
}
.chart-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.chart-title {
font-size: 16px;
font-weight: 700;
}
.chart-range {
color: var(--muted);
font-size: 13px;
}
canvas {
width: 100%;
height: 180px;
display: block;
border-radius: 14px;
background: rgba(15,23,42,0.55);
border: 1px solid rgba(255,255,255,0.06);
}
.legend {
display: flex;
gap: 14px;
flex-wrap: wrap;
margin-top: 14px;
color: var(--muted);
font-size: 13px;
}
.legend-item {
display: inline-flex;
align-items: center;
gap: 8px;
}
.swatch {
width: 10px;
height: 10px;
border-radius: 999px;
display: inline-block;
}
</style>
</head>
<body>
<div class="wrap">
<section class="hero">
<h1>PZEM LoRa Monitor</h1>
<p>Live electrical data received by LoRa and served locally from the ESP32.</p>
<div class="meta">
<span id="status" class="status warn">Waiting for data</span>
<span id="packetInfo">No packet yet</span>
<span id="signalInfo">RSSI: --</span>
<span id="ageInfo">Age: --</span>
</div>
</section>
<section class="grid">
<div class="card"><div class="label">Voltage</div><div class="value" id="voltage">--</div></div>
<div class="card"><div class="label">Current</div><div class="value" id="current">--</div></div>
<div class="card"><div class="label">Power</div><div class="value" id="power">--</div></div>
<div class="card"><div class="label">Energy</div><div class="value" id="energy">--</div></div>
<div class="card"><div class="label">Frequency</div><div class="value" id="frequency">--</div></div>
<div class="card"><div class="label">Power Factor</div><div class="value" id="pf">--</div></div>
</section>
<section class="charts">
<div class="card chart-card">
<div class="chart-head">
<div class="chart-title">Voltage History</div>
<div class="chart-range" id="voltageRange">--</div>
</div>
<canvas id="voltageChart"></canvas>
</div>
<div class="card chart-card">
<div class="chart-head">
<div class="chart-title">Current History</div>
<div class="chart-range" id="currentRange">--</div>
</div>
<canvas id="currentChart"></canvas>
</div>
<div class="card chart-card">
<div class="chart-head">
<div class="chart-title">Power History</div>
<div class="chart-range" id="powerRange">--</div>
</div>
<canvas id="powerChart"></canvas>
</div>
</section>
<section class="card">
<h2>Legend</h2>
<div class="legend">
<span class="legend-item"><span class="swatch" style="background: var(--v-color)"></span>Voltage</span>
<span class="legend-item"><span class="swatch" style="background: var(--i-color)"></span>Current</span>
<span class="legend-item"><span class="swatch" style="background: var(--p-color)"></span>Power</span>
<span class="legend-item">Last 60 received points stored in browser memory</span>
</div>
</section>
</div>
<script>
const HISTORY_LIMIT = 60;
const history = {
voltage: [],
current: [],
power: []
};
let lastCounter = null;
function setText(id, value) {
document.getElementById(id).textContent = value;
}
function parseNumber(value) {
const num = Number(value);
return Number.isFinite(num) ? num : null;
}
function formatNumber(value, unit, decimals) {
const num = parseNumber(value);
if (num === null) return "--";
return num.toFixed(decimals) + " " + unit;
}
function renderNoPacket() {
setText("voltage", "--");
setText("current", "--");
setText("power", "--");
setText("energy", "--");
setText("frequency", "--");
setText("pf", "--");
setText("packetInfo", "No packet yet");
setText("signalInfo", "RSSI: --");
setText("ageInfo", "Age: --");
const status = document.getElementById("status");
status.textContent = "Waiting for data";
status.className = "status warn";
}
function pushHistory(key, value) {
history[key].push(value);
if (history[key].length > HISTORY_LIMIT) {
history[key].shift();
}
}
function updateHistory(data) {
if (data.counter === lastCounter) {
return;
}
lastCounter = data.counter;
pushHistory("voltage", parseNumber(data.voltage));
pushHistory("current", parseNumber(data.current));
pushHistory("power", parseNumber(data.power));
drawAllCharts();
}
function drawAllCharts() {
drawChart("voltageChart", history.voltage, getCssVar("--v-color"), "voltageRange", "V", 1);
drawChart("currentChart", history.current, getCssVar("--i-color"), "currentRange", "A", 3);
drawChart("powerChart", history.power, getCssVar("--p-color"), "powerRange", "W", 1);
}
function getCssVar(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
function drawChart(canvasId, data, color, rangeId, unit, decimals) {
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext("2d");
const width = canvas.clientWidth;
const height = canvas.clientHeight;
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = "rgba(15,23,42,0.55)";
ctx.fillRect(0, 0, width, height);
const values = data.filter((value) => value !== null);
if (values.length === 0) {
setText(rangeId, "No data yet");
ctx.fillStyle = "rgba(148,163,184,0.9)";
ctx.font = "14px Segoe UI";
ctx.fillText("Waiting for data", 16, 28);
return;
}
let min = Math.min(...values);
let max = Math.max(...values);
if (min === max) {
const pad = min === 0 ? 1 : Math.abs(min) * 0.05;
min -= pad;
max += pad;
}
setText(rangeId, min.toFixed(decimals) + " to " + max.toFixed(decimals) + " " + unit);
const padLeft = 14;
const padRight = 14;
const padTop = 12;
const padBottom = 18;
const plotWidth = width - padLeft - padRight;
const plotHeight = height - padTop - padBottom;
ctx.strokeStyle = "rgba(255,255,255,0.08)";
ctx.lineWidth = 1;
for (let i = 0; i < 4; i++) {
const y = padTop + (plotHeight / 3) * i;
ctx.beginPath();
ctx.moveTo(padLeft, y);
ctx.lineTo(width - padRight, y);
ctx.stroke();
}
ctx.beginPath();
data.forEach((value, index) => {
if (value === null) {
return;
}
const x = padLeft + (data.length <= 1 ? 0 : (plotWidth * index) / (data.length - 1));
const y = padTop + ((max - value) / (max - min)) * plotHeight;
if (ctx.currentX === undefined) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
ctx.currentX = x;
});
ctx.strokeStyle = color;
ctx.lineWidth = 2.5;
ctx.stroke();
ctx.currentX = undefined;
const lastValue = values[values.length - 1];
ctx.fillStyle = color;
ctx.font = "12px Segoe UI";
ctx.fillText("Now: " + lastValue.toFixed(decimals) + " " + unit, 16, height - 8);
}
function renderPacket(data) {
setText("voltage", formatNumber(data.voltage, "V", 1));
setText("current", formatNumber(data.current, "A", 3));
setText("power", formatNumber(data.power, "W", 1));
setText("energy", formatNumber(data.energy, "kWh", 3));
setText("frequency", formatNumber(data.frequency, "Hz", 1));
setText("pf", data.powerFactor === null || data.powerFactor === undefined || Number.isNaN(Number(data.powerFactor)) ? "--" : Number(data.powerFactor).toFixed(2));
setText("packetInfo", "Packet #" + data.counter);
setText("signalInfo", "RSSI: " + data.rssi + " dBm");
setText("ageInfo", "Age: " + Number(data.ageSeconds).toFixed(1) + " s");
const status = document.getElementById("status");
if (Number(data.ageSeconds) <= 10) {
status.textContent = "Live";
status.className = "status ok";
} else {
status.textContent = "Stale";
status.className = "status warn";
}
updateHistory(data);
}
async function refreshData() {
try {
const response = await fetch("/api/readings?t=" + Date.now(), { cache: "no-store" });
const data = await response.json();
if (!data.hasPacket) {
renderNoPacket();
return;
}
renderPacket(data);
} catch (error) {
renderNoPacket();
const status = document.getElementById("status");
status.textContent = "Web error";
}
}
window.addEventListener("resize", drawAllCharts);
renderNoPacket();
drawAllCharts();
refreshData();
setInterval(refreshData, 2000);
</script>
</body>
</html>
)HTML";
void setupLoRa() {
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS);
LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0);
if (!LoRa.begin(LORA_FREQUENCY)) {
Serial.println("LoRa init failed. Check wiring and frequency.");
while (true) {
delay(1000);
}
}
LoRa.enableCrc();
Serial.println("LoRa initialised.");
}
void handleRoot() {
server.send_P(200, "text/html", PAGE_HTML);
}
void handleReadings() {
StaticJsonDocument<256> doc;
doc["hasPacket"] = hasPacket;
if (hasPacket) {
doc["counter"] = latestPacket.counter;
doc["voltage"] = latestPacket.voltage;
doc["current"] = latestPacket.current;
doc["power"] = latestPacket.power;
doc["energy"] = latestPacket.energy;
doc["frequency"] = latestPacket.frequency;
doc["powerFactor"] = latestPacket.powerFactor;
doc["rssi"] = lastRssi;
doc["ageSeconds"] = (millis() - lastPacketMs) / 1000.0;
}
String body;
serializeJson(doc, body);
server.sendHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "0");
server.send(200, "application/json", body);
}
void setupWeb() {
if (WiFi.status() != WL_CONNECTED) {
return;
}
server.on("/", handleRoot);
server.on("/api/readings", handleReadings);
server.begin();
Serial.println("Web server started.");
}
bool receivePacketIfAvailable() {
int packetSize = LoRa.parsePacket();
if (packetSize != sizeof(SensorPacket)) {
return false;
}
SensorPacket incoming = {};
int readBytes = LoRa.readBytes(reinterpret_cast<uint8_t*>(&incoming), sizeof(incoming));
if (readBytes != sizeof(incoming)) {
return false;
}
latestPacket = incoming;
hasPacket = true;
lastRssi = LoRa.packetRssi();
lastPacketMs = millis();
Serial.printf(
"Received #%lu | V=%.1fV I=%.3fA P=%.1fW E=%.3fkWh F=%.1fHz PF=%.2f | RSSI=%d\n",
static_cast<unsigned long>(latestPacket.counter),
latestPacket.voltage,
latestPacket.current,
latestPacket.power,
latestPacket.energy,
latestPacket.frequency,
latestPacket.powerFactor,
lastRssi
);
return true;
}