D3 und Aurelia

Im letzten Post habe ich gezeigt, wie man auf einen ioBroker Datenpunkt von aussen mit einem einfachen Javascript-Progrämmchen zugreifen kann. Jetzt wollen wir die Grundlagen für ein etwas grösseres Projekt legen. Als Basis verwende ich gern das moderne JavaScript Framework Aurelia.

Wir beginnen mit dem Aurelia-skeleton/typescript, das man hier herunterladen kann. Nach dem Entpacken benötigen Sie nur das Unterverzeichnis 'skeleton-typescript'. Ausserdem müssen npm und jspm installiert sein. Das Readme des Aurelia Skeleton erläutert die Installation recht gut.

Gehen Sie in das Verzeichnis und geben Sie ein:

    npm install
    jspm install
    npm install d3

Dann ändern wir nur die Datei src/app.ts und erstellen zwei neue Dateien: solar.html und solar.ts. Das ist alles, um anzufangen.

In app.ts fügen wir die unten gelb unterlegte Zeile ein, um einen neuen Menüpunkt zur Demo hinzuzufügen:

 1 import {Router, RouterConfiguration} from 'aurelia-router';
 2 
 3 export class App {
 4   public router: Router;
 5 
 6   public configureRouter(config: RouterConfiguration, router: Router) {
 7     config.title = 'Aurelia';
 8     config.map([
 9       { route: ['', 'welcome'], name: 'welcome',      moduleId: 'welcome',      nav: true, title: 'Welcome' },
10       { route: 'users',         name: 'users',        moduleId: 'users',        nav: true, title: 'Github Users' },
11       { route: 'child-router',  name: 'child-router', moduleId: 'child-router', nav: true, title: 'Child Router' },
12       { route: 'solar', name: 'solar', moduleId: 'solar', nav: true, title: 'Solaranlage'}
13     ]);
14 
15     this.router = router;
16   }
17 }

solar.html ist recht einfach:

 1 <template>
 2     <div class="container"  style="border-color: blue;border-width:2px;border-style:solid">
 3         <h2>Solaranlage</h2>
 4         <div class="row">
 5             <div class="col-sm-4">
 6                 <h3>Tagesenergie</h3>
 7                 ${day_energy} kWh
 8             </div>
 9             <div class="col-sm-4">
10                 <h3>Jahresenergie</h3>
11                 ${year_energy} kWh
12             </div>
13             <div class="col-sm-4">
14                 <h3>Gesamt</h3>
15                 ${total_energy} MWh
16             </div>
17         </div>
18         <div id="graph" class="area">
19             <h3>Momentane Leistung</h3>
20 
21         </div>
22     </div>
23 </template>

Die eigentliche Logik steckt in solar.ts:

  1 import * as d3 from 'd3';
  2 import 'fetch';
  3 
  4 const padding = 25;
  5 const lower_limit = 0;
  6 const height = "80px";
  7 const server = "192.168.16.140:8087";
  8 const ACT_POWER = "fronius.0.powerflow.P_PV";
  9 const DAY_ENERGY = "fronius.0.inverter.1.DAY_ENERGY"
 10 const YEAR_ENERGY = "fronius.0.inverter.1.YEAR_ENERGY"
 11 const TOTAL_ENERGY = "fronius.0.inverter.1.TOTAL_ENERGY"
 12 
 13 
 14 const format = d3.format(".2f")
 15 
 16 export class Solar {
 17   private act_power = 0
 18   private day_energy = 0
 19   private year_energy = 0
 20   private total_energy = 0
 21   private svg
 22   private svgAxis
 23   private xAxis
 24   private x
 25   private width
 26   private resizing = false
 27 
 28   private update_dimensions() {
 29     if (!this.resizing) {
 30       this.resizing = true
 31       let bounds = d3.select("#graph")
 32         .node().getBoundingClientRect()
 33 
 34       this.width = bounds.width - 2 * padding
 35       this.svg.attr("width", this.width + padding)
 36 
 37       this.x = d3.scaleLinear().range([padding, this.width])
 38         .domain([lower_limit, 10000]);
 39       this.xAxis.scale(this.x);
 40       this.svgAxis
 41         .attr("transform", "translate(0,50)")
 42         .call(this.xAxis);
 43       this.resizing = false
 44     }
 45   }
 46 
 47   attached() {
 48     this.svg = d3.select("#graph").append("svg")
 49       .attr("width", "90%")
 50       .attr("height", height)
 51       .classed("area", false)
 52 
 53     this.xAxis = d3.axisBottom().tickFormat(d => {
 54       return this.x.tickFormat(12, d3.format(",d"))(d)
 55     })
 56 
 57     this.svgAxis = this.svg.append("g").classed("x axis", true)
 58     this.update_dimensions()
 59 
 60     const bar = this.svg.append("rect")
 61       .attr("id", "power_bar")
 62       .attr("x", this.x(0))
 63       .attr("y", 0)
 64       .attr("height", "40px")
 65       .attr("fill", "yellow")
 66       .attr("stroke", "blue")
 67 
 68     const text = this.svg.append("text")
 69       .attr("id", "textval")
 70       .attr("x", padding + 5)
 71       .attr("y", 25)
 72 
 73     setInterval(() => {
 74       this.update()
 75     }, 10000)
 76 
 77     window.addEventListener("resize", () => {
 78       this.update_dimensions()
 79     })
 80     this.update()
 81 
 82   }
 83 
 84   async update() {
 85     const result = await fetch(`http://${server}/get/${ACT_POWER}`)
 86     const power = await result.json()
 87     this.update_dimensions()
 88 
 89     let bar = d3.select("#power_bar")
 90     bar.attr("width", this.x(power.val) - this.x(0))
 91     let text = d3.select("#textval")
 92     text.text(`${power.val} Watt`)
 93     const day = await
 94       (await fetch(`http://${server}/get/${DAY_ENERGY}`)).json()
 95     this.day_energy = format(day.val / 1000)
 96     const year = await
 97       (await fetch(`http://${server}/get/${YEAR_ENERGY}`)).json()
 98     this.year_energy = format(year.val / 1000)
 99     const total = await
100       (await fetch(`http://${server}/get/${TOTAL_ENERGY}`)).json()
101     this.total_energy = format(total.val / 1000000)
102   }
103 
104 }

Starten Sie den Entwicklungsserver, indem Sie 'gulp watch' eingeben.  Wenn Sie dann Ihren Browser auf localhost:9000 richten, sollten Sie ungefähr folgendes sehen:


Abgesehen von ein wenig "drumrum" für die Integration in eine noch zu bauende Aurelia WebApp, ist das eigentlich weitgehend dasselbe, wie im vorherigen Teil gezeigt. Insbesondere der Code in "attached()" und "update()" sollte Ihnen bekannt vorkommen. attached() ruft das Framework dann auf, wenn unsere Seite ans DOM gebunden ist. Update wird wie gehabt alle 10 Sekunden aufgerufen. Die neue Funktion update_dimensions() dient dazu, die Grössen anzupassen, wenn der Anwender das Fenster ändert oder das Handy von Horizontal nach Vertikal dreht.

Ach ja, und die Programmiersprache ist jetzt TypeScript. Das ist nicht so arg weit weg von ECMA 6 und sollte deshalb verständlich sein. In update() arbeiten wir jetzt mit dem Standard-Fetch-Api anstelle der d3-eigenen Datenbeschaffungs-Funktionen. 
Das "await" Schlüsselwort in den ersten beiden Zeilen von update() ist "syntactic sugar" um Promises und dient dazu, das UI nicht einfrieren zu lassen, während man auf die Ergebnisse länger laufender Funktionen wartet. Bei weitergehendem Interesse finden Sie dazu viel Dokumentation unter "asynchrones Programmieren" und "Javascript Promise".



Kommentare

Beliebte Posts aus diesem Blog

von Schedules und Triggern

myStrom WiFi Button an ioBroker anbinden

Einfache Script-Beispiele