NodeJS app weergeven p1monitor data

Voor het even snel bekijken waar onze zonnepanelen nu mee bezig zijn, heb ik een raspberry pi met daarop p1monitor gekoppeld aan onze slimme meter. p1monitor heeft een complete website geintegreerd, maar deze is niet responsive en schaalt helemaal niet lekker op een telefoon. En al die grafische metertjes zijn leuk, maar een eenvoudige tabelweergave is voor mij meer dan voldoende:

Dit is alles wat ik wil:

Een eenvoudig projectje, wat in een half uurtje is op te zetten.

Ik begin met het maken van een lege git repository op onze server. Deze repository wordt vervolgens op mijn chromebook gecloned naar een map die vervolgens in Visual Studio Code wordt geconfigureerd als workfolder. De ‘live’ app zal vervolgens met een pull bijgewerkt worden.

chromebook dev (vsc) — commit push –> central repository — pull — app

Hoe ik de git repo opzet, heb ik hier beschreven: New Git Repository.

In mijn geval heb ik een project gemaakt p1monitor-api. Deze ga ik eerst opzetten als een standaard nodejs express applicatie met ejs als template systeem. Deze app moet 2 dingen doen:

  1. data ophalen van de p1monitor api en deze ‘ombouwen’ naar een lokale api
  2. Deze api data op een heel eenvoudige website tonen, met een refresh-rate van 10 seconden.

Opzetten van een express applicatie (waarbij ik ervanuit ga dat een draaiende nodejs applicatie is geinstalleerd) is een fluitje van een cent. Draai daarvoor het volgende commando vanuit de werkfolder:

npx express-generator --view ejs --css --git

Na uitvoer van dit commando is dit de projectmap structuur:

Om de boel initieel draaiend te krijgen, voer ik in de folder het volgende 2 commando’s uit:

npm install
npm start

De eerste voert de installatie van de packages uit de package.json uit en het tweede commando start de applicatie op poort 3000.

Start in een browser de volgende url: localhost:3000 en als alles goed is krijg je deze pagina voorgeschoteld:

Het fundament van de applicatie werkt nu.

Het gaat hier om SPA een single page die gevoed wordt met data uit een rest api. Laten we met het eenvoudigste deel eerst verder gaan. Het allereenvoudigst nu is om de index.ejs aan te passen. Er zou ook voor een fixed index.html kunnen worden gekozen. Maar dat is voor nu meer aanpaswerk in de routers. Er wordt geen dynamic pagina data geproduceerd. De index.ejs is 100% html.

index.ejs:

<!DOCTYPE html>
<html>
  <head>
    <title>P1 data</title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>
  <body>
    <h2>Huidig</h2>
    <p>Verbruik: <span id="wattcons"></span>&nbsp;<span>watt</span></p>
    <p>Levering: <span id="wattprod"></span>&nbsp;<span>watt</span></p>

    <h2>Dag</h2>
    <p>Verbruikt: <span id="kwhcons"></span>&nbsp;<span>kWh</span></p>
    <p>Geleverd: <span id="kwhprod"></span>&nbsp;<span>kWh</span></p>
    <br>
    <p>Gas: <span id="m3"></span>&nbsp;<span>m<sup>3</sup></span></p>
  </body>
</html>

Het ophalen van de data en deze realtime weergeven wordt met een brokje javascript gedaan. Omdat dit een zo minimale website is, heb ik het javascript in de body geplaatst. Om te beginnen heb ik Axios als http handler gebruikt:

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

Vervolgens het script om api data op te halen (2 endpoints) en dit in de website te parsen.

<script>

async function getData() {
  try {
    let endpoints = [
    './api/current',
    './api/day'
  ];

  Promise.all(endpoints.map((endpoint) => axios.get(endpoint))).then(([{data: current}, {data: day}] )=> {
        document.getElementById('wattcons').innerHTML=current.CONSUMPTION_W;
        document.getElementById('wattprod').innerHTML=current.PRODUCTION_W;
        document.getElementById('kwhcons').innerHTML=day.CONSUMPTION_DELTA_KWH.toFixed(1);
        document.getElementById('kwhprod').innerHTML=day.PRODUCTION_DELTA_KWH.toFixed(1);
        document.getElementById('m3').innerHTML=day.CONSUMPTION_GAS_DELTA_M3.toFixed(1);
      });
  } catch (error) {
    console.error(error);
  }
}

getData();
setInterval(function(){
   getData();
}, 10000);

</script>

Deze pagina laadt nu wel, maar zal in de console 404 meldingen geven. Er moeten nog 2 api’s worden toegevoegd. /api/day en /api/current/. Allereerst de router zelf. Deze maakt gebruik van Axios om data van de p1 rest api te halen en door de routen naar onze rest api. Installeer deze eerst (en save als dependency):

npm install axios -s

Voor de router zelf herbruik de user.js in de routes folder. Deze hernoem ik naar api.js en vervang alles met deze code:

const createError = require('http-errors');

const express = require('express');
const router = express.Router();
const axios = require('axios');

router.get('/day', async function(req, res, next) {
    try {
        const response = await axios.get('http://p1monitor/api/v1/powergas/day?limit=1&json=object');
        console.log(response.data[0]);
        res.json(response.data[0]);
    } catch (error) {
        console.error(error);
        res.json({message:error});
      }
});

router.get('/current', async function(req, res, next) {
    try {
        const response = await axios.get('http://p1monitor/api/v1/smartmeter?limit=1&json=object');
        //console.log(response.data[0]);
        res.json(response.data[0]);
    } catch (error) {
        console.error(error);
        res.json({message:error});
      }
});

/* GET users listing. */
router.get('/', async function(req, res, next) {
    res.render('p1');
});

module.exports = router;

Nu moet de api nog bekend worden gemaakt als router mogelijkheid. Hiervoor pas ik de user regel aan naar api in de app.js:

var indexRouter = require('./routes/index');
//var usersRouter = require('./routes/users');
var apiRouter = require('./routes/api');

en

app.use('/', indexRouter);
//app.use('/users', usersRouter);
app.use('/api', apiRouter);

Voordat dit alles geupload wordt naar de git repo, eerst nog 2 bestanden toevoegen .env en .gitignore. Die .env doen we nu nog even niks mee, maar de .gitignore wel. Zo hoeft de hele node_modules boom niet geimporteerd te worden. En nog wat andere directories ook niet. Er zijn talloze voorbeelden te vinden op internet, wat wel en wat niet. De mijne ziet er zo uit:

Deze variant werkt heel goed, binnen mijn eigen netwerk, waarbij aangenomen wordt dat de raspberry pi in de netwerk dns bekend is als p1monitor. Als je deze app ook buiten de deur wil gebruiken, dan zul je een portforward in je modem router moeten instellen. Deze instelling valt ver buiten de scope van deze blog en is per modem en provider anders. Voor de beeldvorming, bij ons thuis is het geconfigureerd in de fritzbox en is deze gekoppeld via een dynamic dns service.

Daarom toch nog maar even wat aanpassingen. Om de environment file beschikbaar te stellen, is het nodig om dotenv te installeren:

npm install dotenv -s

De .env file wordt nu gevuld met een aantal variabelen. Omdat ik deze app achter een proxie draai, is het in sommige gevallen nodig om het volledige pad op te nemen. Omdat ik meerdere apps draai, heb ik verschillende poortnummers. In ontwikkel is 3000 prima, maar in produktie zal het iets van 30006 worden. Het is de zevende app. Ook de url van de p1monitor is beter om die niet wereldkundig te maken. De eerste stap van security is de obscurity :-). Daarom 3 parameters, die ik in ontwikkel vul met ontwikkelwaardes:

Om dotenv te gebruiken, zet ik gewoonlijk deze regel ergens bovenin de app.js file, in ieder geval ergens voordat de config gebruikt moet worden.

require('dotenv').config()

Het gebruik van het poortnummer uit deze file wordt in de /bin/www file gebruikt. Hoeft niks voor te gebeuren.

Zie deze regel

Ik persoonlijk voeg aan het eind van deze file nog even een console message toe:

Als de app nu opnieuw gestart wordt, dan krijg je dit in de log te zien: