ESPHome solar data over Lora communication

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;
}


Welcome to forum!

Alternative for what?
You don't need esphome, since you have working c++ setup you could use MQTT for communication.

Thank Karosm!

MQTT is definitely an option, as I am using it for another application i.e. router solaire by F1ATB.

As I am new to HA and ESPHome, I read different messages, where some people raised the question: why using MQTT if you have ESPHome.

Following your advice, I will try to setup the Lora data through MQTT and share this with HA.

It's valid question, esphome is impressive and definitely worth to try. But there are cases you just simply can't or don't want to use esphome, then lower level coding is best option.

2 Likes