iobroker.faikout 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,963 @@
1
+ /*
2
+ * Bausteine der Klima-Kachel fuer VIS 1.x - iobroker.faikout
3
+ *
4
+ * Jedes Widget bekommt nur EIN Objekt: das Geraet (z. B. faikout.0.Wohnzimmer). Welche
5
+ * Datenpunkte es darunter gibt, ermittelt das Widget selbst - die Anlagen koennen
6
+ * unterschiedlich viel, und der Adapter legt entsprechend unterschiedlich viele Objekte an.
7
+ * Fehlt ein Datenpunkt, zeigt der Baustein einen Hinweis statt eines toten Bedienelements.
8
+ */
9
+ 'use strict';
10
+
11
+ vis.binds.faikout = {
12
+ version: '0.0.1',
13
+
14
+ /* Rueckfallwerte; die echten Grenzen liefert das Geraet ueber common.min/max/step. */
15
+ MIN: 16,
16
+ MAX: 29,
17
+ SCHRITT: 0.5,
18
+
19
+ SYM: {
20
+ auto: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M7 15l-3 3-3-3"/><path d="M4 18V9a4 4 0 0 1 4-4"/><path d="M17 9l3-3 3 3"/><path d="M20 6v9a4 4 0 0 1-4 4"/></svg>',
21
+ cool: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v18M4.2 7.5l15.6 9M19.8 7.5l-15.6 9"/><path d="M9.5 4.6L12 6.9l2.5-2.3M9.5 19.4L12 17.1l2.5 2.3"/></svg>',
22
+ heat: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2.5M12 19.5V22M2 12h2.5M19.5 12H22M4.9 4.9l1.8 1.8M17.3 17.3l1.8 1.8M19.1 4.9l-1.8 1.8M6.7 17.3l-1.8 1.8"/></svg>',
23
+ dry: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3s6 6.6 6 10.4A6 6 0 0 1 6 13.4C6 9.6 12 3 12 3z"/></svg>',
24
+ fan: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1.9"/><path d="M12 10.1c0-2.7.5-4.5 1.6-5.5 1.5-1.4 4-.6 4 1.6 0 2.4-2 3.9-5.6 3.9"/><path d="M13.9 12c2.7 0 4.5.5 5.5 1.6 1.4 1.5.6 4-1.6 4-2.4 0-3.9-2-3.9-5.6"/><path d="M12 13.9c0 2.7-.5 4.5-1.6 5.5-1.5 1.4-4 .6-4-1.6 0-2.4 2-3.9 5.6-3.9"/><path d="M10.1 12c-2.7 0-4.5-.5-5.5-1.6-1.4-1.5-.6-4 1.6-4 2.4 0 3.9 2 3.9 5.6"/></svg>',
25
+ temp: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M14 14.8V4.5a2.5 2.5 0 0 0-5 0v10.3a4.5 4.5 0 1 0 5 0z"/></svg>',
26
+ hum: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3.5s5.5 6.1 5.5 9.6a5.5 5.5 0 0 1-11 0C6.5 9.6 12 3.5 12 3.5z"/></svg>',
27
+
28
+ /* Zusatzfunktionen - reine Symbole, der Name steht im Tooltip */
29
+ swing: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8h16M4 8l3-3M4 8l3 3"/><path d="M20 16H4m16 0l-3-3m3 3l-3 3"/></svg>',
30
+ swingv: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v16M12 4l-3 3m3-3l3 3M12 20l-3-3m3 3l3-3"/></svg>',
31
+ swingh: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16M4 12l3-3m-3 3l3 3M20 12l-3-3m3 3l-3 3"/></svg>',
32
+ econo: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 20c0-8 5-13 14-14 .5 7-2.5 14-9.5 14C7 20 5.8 20 5 20z"/><path d="M9 16c1.5-3.5 4-6 7-7.5"/></svg>',
33
+ powerful: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L4.5 13.5H11L10 22l8.5-11.5H12L13 2z"/></svg>',
34
+ comfort: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12V8.5A2.5 2.5 0 0 1 7.5 6h9A2.5 2.5 0 0 1 19 8.5V12"/><path d="M3.5 12.5A1.5 1.5 0 0 1 5 14v3h14v-3a1.5 1.5 0 0 1 1.5-1.5A1.5 1.5 0 0 1 22 14v4.5H2V14a1.5 1.5 0 0 1 1.5-1.5z"/><path d="M6 20v1M18 20v1"/></svg>',
35
+ streamer: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="3.5" width="17" height="17" rx="3"/><path d="M7 9h10M7 12.5h10M7 16h6"/></svg>',
36
+ sensor: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2.5"/><path d="M6.5 6.5a7.8 7.8 0 0 0 0 11M17.5 6.5a7.8 7.8 0 0 1 0 11M3.5 3.5a12 12 0 0 0 0 17M20.5 3.5a12 12 0 0 1 0 17"/></svg>',
37
+ led: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6M10 21h4"/><path d="M12 3a6 6 0 0 0-3.5 10.9c.5.4.8 1 .8 1.6V16h5.4v-.5c0-.6.3-1.2.8-1.6A6 6 0 0 0 12 3z"/></svg>',
38
+ quiet: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5L6.5 9H3v6h3.5L11 19V5z"/><path d="M16 10.5l4 3M20 10.5l-4 3"/></svg>'
39
+ },
40
+
41
+ /**
42
+ * Zusatzfunktionen. Erscheinen nur, wenn die Anlage sie meldet - `comfort` etwa gibt es
43
+ * nur an manchen Geraeten, `sensor`/`led`/`quiet` nur, wenn sie im Modul nicht
44
+ * abgeschaltet sind.
45
+ */
46
+ ZUSATZ: [
47
+ { feld: 'control.swing', name: 'Schwenken', sym: 'swing' },
48
+ { feld: 'control.swingv', name: 'Lamellen senkrecht', sym: 'swingv' },
49
+ { feld: 'control.swingh', name: 'Lamellen waagerecht', sym: 'swingh' },
50
+ { feld: 'control.econo', name: 'Sparbetrieb', sym: 'econo' },
51
+ { feld: 'control.powerful', name: 'Turbo', sym: 'powerful' },
52
+ { feld: 'control.comfort', name: 'Komfortbetrieb', sym: 'comfort' },
53
+ { feld: 'control.streamer', name: 'Luftreinigung', sym: 'streamer' },
54
+ { feld: 'control.sensor', name: 'Anwesenheitssensor', sym: 'sensor' },
55
+ { feld: 'control.led', name: 'Anzeige-LED', sym: 'led' },
56
+ { feld: 'control.quiet', name: 'Leiser Außenbetrieb', sym: 'quiet' }
57
+ ],
58
+
59
+ MODI: [
60
+ { wert: 'heat_cool', text: 'Auto', farbe: '#2e7d52', sym: 'auto' },
61
+ { wert: 'cool', text: 'Kühlen', farbe: '#1e86c4', sym: 'cool' },
62
+ { wert: 'heat', text: 'Heizen', farbe: '#c96a22', sym: 'heat' },
63
+ { wert: 'dry', text: 'Trocknen', farbe: '#9a7b33', sym: 'dry' },
64
+ { wert: 'fan_only', text: 'Lüften', farbe: '#5c7186', sym: 'fan' }
65
+ ],
66
+
67
+ LUEFTER: [
68
+ { wert: 'auto', text: 'Auto' },
69
+ { wert: 'night', text: 'Nacht' },
70
+ { wert: 'low', text: '1' },
71
+ { wert: 'lowMedium', text: '2' },
72
+ { wert: 'medium', text: '3' },
73
+ { wert: 'mediumHigh', text: '4' },
74
+ { wert: 'high', text: '5' }
75
+ ],
76
+
77
+ /** Alle Messwerte, die als Kachel oder Einzelwert zur Verfuegung stehen. */
78
+ MESSWERTE: [
79
+ { schluessel: 'aussen', feld: 'status.outside', name: 'Außen', einheit: '°C', stellen: 1 },
80
+ { schluessel: 'leistung', feld: 'status.consumption', name: 'Leistung', einheit: 'W', stellen: 0 },
81
+ { schluessel: 'heute', feld: 'verbrauch.gesamt.heute', name: 'Heute', einheit: 'kWh', stellen: 1 },
82
+ { schluessel: 'verdichter', feld: 'status.comp', name: 'Verdichter', einheit: 'rpm', stellen: 0 },
83
+ { schluessel: 'ventilator', feld: 'status.fanfreq', name: 'Ventilator', einheit: 'rpm', stellen: 0 },
84
+ { schluessel: 'kaeltemittel', feld: 'status.liquid', name: 'Kältemittel', einheit: '°C', stellen: 1 },
85
+ { schluessel: 'raum', feld: 'status.temp', name: 'Raum', einheit: '°C', stellen: 1 },
86
+ { schluessel: 'feuchte', feld: 'status.hum', name: 'Luftfeuchte', einheit: '%', stellen: 0 },
87
+ { schluessel: 'energie', feld: 'status.energy', name: 'Energie gesamt', einheit: 'kWh', stellen: 1 },
88
+ { schluessel: 'gestern', feld: 'verbrauch.gesamt.gestern', name: 'Gestern', einheit: 'kWh', stellen: 1 },
89
+ { schluessel: 'stunde', feld: 'verbrauch.gesamt.stunde', name: 'Diese Stunde', einheit: 'kWh', stellen: 2 }
90
+ ],
91
+
92
+ /* ------------------------------------------------------------------ Hilfen */
93
+
94
+ zahl: function (v, stellen) {
95
+ if (v === null || v === undefined || v === '') return null;
96
+ var n = parseFloat(v);
97
+ if (isNaN(n)) return null;
98
+ return n.toFixed(stellen).replace('.', ',');
99
+ },
100
+
101
+ /**
102
+ * Gemeinsames Geruest aller Bausteine: wartet auf das DOM, ermittelt die vorhandenen
103
+ * Datenpunkte des Geraets, ruft den Aufbau und haengt die Zustandsbindung ein.
104
+ *
105
+ * @param {function} aufbau ($wurzel, ctx, data) => zeichnenFunktion
106
+ */
107
+ start: function (widgetID, data, aufbau) {
108
+ var b = vis.binds.faikout;
109
+ var $div = $('#' + widgetID);
110
+ if (!$div.length) {
111
+ return setTimeout(function () { b.start(widgetID, data, aufbau); }, 100);
112
+ }
113
+
114
+ var prefix = data.deviceOid;
115
+ if (!prefix) {
116
+ $div.html('<div class="fk"><div class="fk-fehler">Kein Gerät gewählt. Im Attribut ' +
117
+ '„Gerät" ein Objekt wie <code>faikout.0.Wohnzimmer</code> eintragen.</div></div>');
118
+ return;
119
+ }
120
+
121
+ vis.conn.getStates(prefix + '.*', function (fehler, zustaende) {
122
+ if (fehler) {
123
+ $div.html('<div class="fk"><div class="fk-fehler">Datenpunkte nicht lesbar: ' + fehler + '</div></div>');
124
+ return;
125
+ }
126
+ zustaende = zustaende || {};
127
+ var ctx = {
128
+ prefix: prefix,
129
+ widgetID: widgetID,
130
+ zustaende: zustaende,
131
+ oid: function (feld) { return prefix + '.' + feld; },
132
+ hat: function (feld) { return Object.prototype.hasOwnProperty.call(zustaende, prefix + '.' + feld); },
133
+ wert: function (feld) {
134
+ var s = vis.states.attr(prefix + '.' + feld + '.val');
135
+ return s === undefined ? null : s;
136
+ },
137
+ /** Farbe der aktuellen Betriebsart - faerbt alle Bausteine einheitlich ein. */
138
+ akzent: function () {
139
+ var an = this.hat('control.power') ? !!this.wert('control.power') : true;
140
+ if (!an) return '#8593a4';
141
+ var m = this.wert('control.mode');
142
+ for (var i = 0; i < b.MODI.length; i++) if (b.MODI[i].wert === m) return b.MODI[i].farbe;
143
+ return '#1e86c4';
144
+ },
145
+ /** Meldet, dass ein Befehl unterwegs ist - die Anlage bestaetigt erst spaeter. */
146
+ wartet: function ($w) {
147
+ var $s = $w.find('.fk-status');
148
+ if (!$s.length) return;
149
+ $s.text('gesendet · warte auf Rückmeldung').addClass('fk-wartet');
150
+ clearTimeout($w.data('fkTimer'));
151
+ $w.data('fkTimer', setTimeout(function () { $s.text('').removeClass('fk-wartet'); }, 65000));
152
+ }
153
+ };
154
+
155
+ var $wurzel = $('<div class="fk"></div>')
156
+ .toggleClass('fk-dunkel', data.theme === 'dunkel')
157
+ .toggleClass('fk-blank', data.rahmen === false);
158
+ $div.empty().append($wurzel);
159
+
160
+ var zeichnen = aufbau($wurzel, ctx, data);
161
+ if (typeof zeichnen !== 'function') return;
162
+
163
+ var gebunden = [];
164
+ Object.keys(zustaende).forEach(function (id) {
165
+ vis.states.bind(id + '.val', zeichnen);
166
+ gebunden.push(id + '.val');
167
+ });
168
+ $div.data('bound', gebunden);
169
+ $div.data('bindHandler', zeichnen);
170
+ zeichnen();
171
+ });
172
+ },
173
+
174
+ /** Hinweis, wenn die Anlage den benoetigten Datenpunkt gar nicht hat. */
175
+ fehlt: function ($wurzel, feld) {
176
+ $wurzel.html('<div class="fk-fehler">Dieses Gerät hat keinen Datenpunkt <code>' + feld + '</code>.</div>');
177
+ return null;
178
+ },
179
+
180
+ /** Grenzen und Schrittweite des Sollwerts - je Anlage verschieden. */
181
+ grenzen: function (ctx, fertig) {
182
+ var b = vis.binds.faikout;
183
+ var g = { min: b.MIN, max: b.MAX, step: b.SCHRITT };
184
+ vis.conn.getObject(ctx.oid('control.target'), function (err, obj) {
185
+ if (!err && obj && obj.common) {
186
+ if (typeof obj.common.min === 'number') g.min = obj.common.min;
187
+ if (typeof obj.common.max === 'number') g.max = obj.common.max;
188
+ if (typeof obj.common.step === 'number') g.step = obj.common.step;
189
+ }
190
+ fertig(g);
191
+ });
192
+ return g;
193
+ },
194
+
195
+ /* ------------------------------------------------------------------ Kopfzeile */
196
+
197
+ kopf: function ($wurzel, ctx, data) {
198
+ if (data.kopf === false) return;
199
+ var name = data.titel || ctx.prefix.split('.').pop().replace(/_/g, ' ');
200
+ var $kopf = $('<div class="fk-kopf"></div>');
201
+ $kopf.append($('<div></div>')
202
+ .append($('<div class="fk-name"></div>').text(name))
203
+ .append(data.untertitel ? $('<div class="fk-ort"></div>').text(data.untertitel) : ''));
204
+ if (ctx.hat('control.power') && data.schalter !== false) {
205
+ $('<button type="button" class="fk-schalter" aria-label="Ein/Aus"></button>')
206
+ .on('click', function () {
207
+ vis.setValue(ctx.oid('control.power'), !ctx.wert('control.power'));
208
+ ctx.wartet($wurzel);
209
+ })
210
+ .appendTo($kopf);
211
+ }
212
+ $wurzel.append($kopf);
213
+ },
214
+
215
+ fuss: function ($wurzel, ctx, data) {
216
+ if (data.fuss === false) return;
217
+ $wurzel.append('<div class="fk-fuss"><span class="fk-punkt">verbunden</span><span class="fk-status"></span></div>');
218
+ },
219
+
220
+ /* ================================================================== Bogen */
221
+
222
+ /**
223
+ * Baut den Bogen in ein vorhandenes Element und liefert die Zeichenfunktion zurueck.
224
+ * Wird sowohl vom eigenstaendigen Bogen-Widget als auch von der kompletten Kachel
225
+ * benutzt - so gibt es nur eine Quelle fuer Geometrie und Bedienung.
226
+ */
227
+ bogenBauen: function ($ziel, ctx, data, gid) {
228
+ var b = vis.binds.faikout;
229
+ var VB_W = 320, VB_H = 258, CX = 160, CY = 152, R = 118, START = 145, SPAN = 250;
230
+ var LAENGE = 2 * Math.PI * R * (SPAN / 360);
231
+ var g = { min: b.MIN, max: b.MAX, step: b.SCHRITT };
232
+ var entwurf = null; // waehrend des Ziehens angezeigter Wert
233
+
234
+ var pkt = function (grad, r) {
235
+ var w = grad * Math.PI / 180;
236
+ return [CX + r * Math.cos(w), CY + r * Math.sin(w)];
237
+ };
238
+ var a0 = pkt(START, R), e0 = pkt(START + SPAN, R);
239
+ var pfad = 'M ' + a0[0].toFixed(1) + ' ' + a0[1].toFixed(1) + ' A ' + R + ' ' + R +
240
+ ' 0 1 1 ' + e0[0].toFixed(1) + ' ' + e0[1].toFixed(1);
241
+
242
+ $ziel.append(
243
+ '<div class="fk-instrument">' +
244
+ ' <div class="fk-bogen">' +
245
+ ' <svg viewBox="0 0 ' + VB_W + ' ' + VB_H + '">' +
246
+ ' <defs><linearGradient id="' + gid + '" x1="0" y1="0" x2="1" y2="0">' +
247
+ ' <stop offset="0" stop-color="var(--fk-t0)"/><stop offset="0.22" stop-color="var(--fk-t1)"/>' +
248
+ ' <stop offset="0.45" stop-color="var(--fk-t2)"/><stop offset="0.66" stop-color="var(--fk-t3)"/>' +
249
+ ' <stop offset="0.85" stop-color="var(--fk-t4)"/><stop offset="1" stop-color="var(--fk-t5)"/>' +
250
+ ' </linearGradient></defs>' +
251
+ ' <path class="fk-spur" d="' + pfad + '" stroke="var(--fk-rand)"/>' +
252
+ ' <path class="fk-skala" d="' + pfad + '" stroke="url(#' + gid + ')"/>' +
253
+ ' <path class="fk-fuellung" d="' + pfad + '" stroke="url(#' + gid + ')" ' +
254
+ ' stroke-dasharray="' + LAENGE.toFixed(1) + '" stroke-dashoffset="' + LAENGE.toFixed(1) + '"/>' +
255
+ ' <line class="fk-raummarke" x1="0" y1="0" x2="0" y2="0"/>' +
256
+ ' <g class="fk-griff"><circle r="11"/><circle class="fk-kern" r="4"/></g>' +
257
+ ' <path class="fk-treffer" d="' + pfad + '"/>' +
258
+ ' </svg>' +
259
+ ' <div class="fk-ablesung">' +
260
+ ' <div class="fk-soll"><span class="fk-soll-zahl">–</span><span class="fk-soll-einheit">°C</span></div>' +
261
+ ' <div class="fk-soll-etikett">Soll</div>' +
262
+ ' <div class="fk-ist">' +
263
+ ' <span class="fk-ist-temp-feld">' + b.SYM.temp + '<b class="fk-ist-temp">–</b> °C</span>' +
264
+ ' <span class="fk-ist-hum-feld">' + b.SYM.hum + '<b class="fk-ist-hum">–</b> %</span>' +
265
+ ' </div>' +
266
+ ' </div>' +
267
+ ' </div>' +
268
+ ' <div class="fk-pm-reihe">' +
269
+ ' <button type="button" class="fk-pm fk-pm-minus" aria-label="Sollwert senken">−</button>' +
270
+ ' <button type="button" class="fk-pm fk-pm-plus" aria-label="Sollwert erhöhen">+</button>' +
271
+ ' </div>' +
272
+ '</div>');
273
+
274
+ var $wurzel = $ziel.closest('.fk');
275
+ var $bogen = $ziel.find('.fk-bogen');
276
+
277
+ var setzen = function (neu) {
278
+ neu = Math.max(g.min, Math.min(g.max, Math.round(neu / g.step) * g.step));
279
+ vis.setValue(ctx.oid('control.target'), neu);
280
+ ctx.wartet($wurzel);
281
+ };
282
+ $ziel.find('.fk-pm-minus').on('click', function () {
283
+ var a = parseFloat(ctx.wert('control.target'));
284
+ if (!isNaN(a)) setzen(a - g.step);
285
+ });
286
+ $ziel.find('.fk-pm-plus').on('click', function () {
287
+ var a = parseFloat(ctx.wert('control.target'));
288
+ if (!isNaN(a)) setzen(a + g.step);
289
+ });
290
+
291
+ if (data.ziehen !== false) {
292
+ var svg = $bogen.find('svg')[0];
293
+ var treffer = $bogen.find('.fk-treffer')[0];
294
+ var zieht = false;
295
+
296
+ var ausEreignis = function (ev) {
297
+ var r = svg.getBoundingClientRect();
298
+ var x = (ev.clientX - r.left) / r.width * VB_W;
299
+ var y = (ev.clientY - r.top) / r.height * VB_H;
300
+ var d = Math.atan2(y - CY, x - CX) * 180 / Math.PI;
301
+ if (d < 0) d += 360;
302
+ var ende = START + SPAN - 360; // Ende der Luecke unten
303
+ if (d > ende && d < START) d = d < (ende + START) / 2 ? ende : START;
304
+ if (d < START) d += 360;
305
+ var t = g.min + ((d - START) / SPAN) * (g.max - g.min);
306
+ return Math.max(g.min, Math.min(g.max, Math.round(t / g.step) * g.step));
307
+ };
308
+
309
+ treffer.addEventListener('pointerdown', function (ev) {
310
+ zieht = true;
311
+ $bogen.addClass('fk-zieht');
312
+ treffer.setPointerCapture(ev.pointerId);
313
+ entwurf = ausEreignis(ev);
314
+ zeichnen();
315
+ ev.preventDefault();
316
+ });
317
+ treffer.addEventListener('pointermove', function (ev) {
318
+ if (!zieht) return;
319
+ entwurf = ausEreignis(ev);
320
+ zeichnen();
321
+ });
322
+ var los = function (ev) {
323
+ if (!zieht) return;
324
+ zieht = false;
325
+ $bogen.removeClass('fk-zieht');
326
+ try { treffer.releasePointerCapture(ev.pointerId); } catch (e) { /* schon frei */ }
327
+ // Erst beim Loslassen senden - sonst laeuft je Mausbewegung ein Befehl.
328
+ var wert = entwurf;
329
+ entwurf = null;
330
+ if (wert !== null) setzen(wert);
331
+ };
332
+ treffer.addEventListener('pointerup', los);
333
+ treffer.addEventListener('pointercancel', los);
334
+ }
335
+
336
+ // Bereich und Schrittweite kommen vom Geraet (eine Anlage nimmt halbe Grad, die
337
+ // andere nur ganze) - nachladen und dann neu zeichnen.
338
+ b.grenzen(ctx, function (neu) { g = neu; zeichnen(); });
339
+
340
+ function zeichnen() {
341
+ var soll = entwurf !== null ? entwurf : parseFloat(ctx.wert('control.target'));
342
+ if (soll !== null && !isNaN(soll)) {
343
+ $ziel.find('.fk-soll-zahl').text(b.zahl(soll, 1));
344
+ var anteil = Math.max(0, Math.min(1, (soll - g.min) / (g.max - g.min)));
345
+ $ziel.find('.fk-fuellung').attr('stroke-dashoffset', String(LAENGE * (1 - anteil)));
346
+ var gp = pkt(START + anteil * SPAN, R);
347
+ $ziel.find('.fk-griff').attr('transform', 'translate(' + gp[0].toFixed(1) + ' ' + gp[1].toFixed(1) + ')');
348
+ }
349
+
350
+ // `temp` ist die Raumtemperatur, NICHT der Sollwert - der steht in `target`.
351
+ var raum = parseFloat(ctx.wert('status.temp'));
352
+ if (!isNaN(raum)) {
353
+ $ziel.find('.fk-ist-temp').text(b.zahl(raum, 1));
354
+ var rel = Math.max(0, Math.min(1, (raum - g.min) / (g.max - g.min)));
355
+ var m1 = pkt(START + rel * SPAN, R + 13), m2 = pkt(START + rel * SPAN, R + 21);
356
+ $ziel.find('.fk-raummarke')
357
+ .attr('x1', m1[0].toFixed(1)).attr('y1', m1[1].toFixed(1))
358
+ .attr('x2', m2[0].toFixed(1)).attr('y2', m2[1].toFixed(1));
359
+ } else {
360
+ $ziel.find('.fk-ist-temp-feld').hide();
361
+ }
362
+
363
+ // Luftfeuchte gibt es nur, wenn die Anlage wirklich eine misst.
364
+ var f = parseFloat(ctx.wert('status.hum'));
365
+ if (ctx.hat('status.hum') && !isNaN(f)) $ziel.find('.fk-ist-hum').text(b.zahl(f, 0));
366
+ else $ziel.find('.fk-ist-hum-feld').hide();
367
+ }
368
+ return zeichnen;
369
+ },
370
+
371
+ bogen: function (widgetID, view, data) {
372
+ vis.binds.faikout.start(widgetID, data, function ($wurzel, ctx) {
373
+ var b = vis.binds.faikout;
374
+ if (!ctx.hat('control.target')) return b.fehlt($wurzel, 'control.target');
375
+
376
+ $wurzel.append('<div class="fk-streifen"></div>');
377
+ b.kopf($wurzel, ctx, data);
378
+ var zeichneBogen = b.bogenBauen($wurzel, ctx, data, 'fkskala_' + widgetID);
379
+ b.fuss($wurzel, ctx, data);
380
+
381
+ return function () {
382
+ var an = ctx.hat('control.power') ? !!ctx.wert('control.power') : true;
383
+ $wurzel.css('--fk-akzent', ctx.akzent()).toggleClass('fk-aus', !an);
384
+ $wurzel.find('.fk-schalter').toggleClass('fk-an', an);
385
+ zeichneBogen();
386
+ var erreichbar = ctx.hat('info.online') ? !!ctx.wert('info.online') : true;
387
+ $wurzel.find('.fk-punkt').toggleClass('fk-weg', !erreichbar).text(erreichbar ? 'verbunden' : 'offline');
388
+ };
389
+ });
390
+ },
391
+ /* ================================================================== Betriebsart */
392
+
393
+ modus: function (widgetID, view, data, senkrecht) {
394
+ vis.binds.faikout.start(widgetID, data, function ($wurzel, ctx) {
395
+ var b = vis.binds.faikout;
396
+ if (!ctx.hat('control.mode')) return b.fehlt($wurzel, 'control.mode');
397
+
398
+ b.kopf($wurzel, ctx, data);
399
+ var $g = $('<div class="fk-gruppe"></div>');
400
+ if (data.etikett !== false) $g.append('<div class="fk-gruppe-etikett">Betriebsart</div>');
401
+ var $p = $('<div class="fk-pillen"></div>').toggleClass('fk-senkrecht', !!senkrecht);
402
+ b.MODI.forEach(function (m) {
403
+ $('<button type="button" class="fk-pille"></button>')
404
+ .html((data.symbole === false ? '' : b.SYM[m.sym]) + '<span>' + m.text + '</span>')
405
+ .attr('data-wert', m.wert)
406
+ .on('click', function () {
407
+ vis.setValue(ctx.oid('control.mode'), m.wert);
408
+ // Eine Betriebsart zu waehlen heisst: die Anlage soll laufen.
409
+ if (ctx.hat('control.power') && !ctx.wert('control.power')) {
410
+ vis.setValue(ctx.oid('control.power'), true);
411
+ }
412
+ ctx.wartet($wurzel);
413
+ })
414
+ .appendTo($p);
415
+ });
416
+ $g.append($p);
417
+ $wurzel.append($g);
418
+ b.fuss($wurzel, ctx, data);
419
+
420
+ return function () {
421
+ var an = ctx.hat('control.power') ? !!ctx.wert('control.power') : true;
422
+ var m = ctx.wert('control.mode');
423
+ $wurzel.css('--fk-akzent', ctx.akzent()).toggleClass('fk-aus', !an);
424
+ $wurzel.find('.fk-schalter').toggleClass('fk-an', an);
425
+ $wurzel.find('.fk-pille').each(function () {
426
+ $(this).toggleClass('fk-gewaehlt', $(this).attr('data-wert') === m && an);
427
+ });
428
+ };
429
+ });
430
+ },
431
+
432
+ /* ================================================================== Luefterstufe */
433
+
434
+ luefter: function (widgetID, view, data, senkrecht) {
435
+ vis.binds.faikout.start(widgetID, data, function ($wurzel, ctx) {
436
+ var b = vis.binds.faikout;
437
+ if (!ctx.hat('control.fan')) return b.fehlt($wurzel, 'control.fan');
438
+
439
+ b.kopf($wurzel, ctx, data);
440
+ var $g = $('<div class="fk-gruppe"></div>');
441
+ if (data.etikett !== false) $g.append('<div class="fk-gruppe-etikett">Lüfter</div>');
442
+ var $p = $('<div class="fk-pillen"></div>').toggleClass('fk-senkrecht', !!senkrecht);
443
+ b.LUEFTER.forEach(function (l) {
444
+ $('<button type="button" class="fk-pille fk-nur-text"></button>')
445
+ .text(l.text).attr('data-wert', l.wert)
446
+ .on('click', function () {
447
+ vis.setValue(ctx.oid('control.fan'), l.wert);
448
+ ctx.wartet($wurzel);
449
+ })
450
+ .appendTo($p);
451
+ });
452
+ $g.append($p);
453
+ $wurzel.append($g);
454
+ b.fuss($wurzel, ctx, data);
455
+
456
+ return function () {
457
+ var f = ctx.wert('control.fan');
458
+ $wurzel.css('--fk-akzent', ctx.akzent());
459
+ $wurzel.find('.fk-pille').each(function () {
460
+ $(this).toggleClass('fk-gewaehlt', $(this).attr('data-wert') === f);
461
+ });
462
+ $wurzel.find('.fk-schalter').toggleClass('fk-an', ctx.hat('control.power') ? !!ctx.wert('control.power') : true);
463
+ };
464
+ });
465
+ },
466
+
467
+ /* ================================================================== Leistungssteuerung */
468
+
469
+ leistung: function (widgetID, view, data) {
470
+ vis.binds.faikout.start(widgetID, data, function ($wurzel, ctx) {
471
+ var b = vis.binds.faikout;
472
+ if (!ctx.hat('control.demand')) return b.fehlt($wurzel, 'control.demand');
473
+
474
+ b.kopf($wurzel, ctx, data);
475
+ var $g = $('<div class="fk-gruppe"></div>');
476
+ if (data.etikett !== false) $g.append('<div class="fk-gruppe-etikett">Leistungsbegrenzung</div>');
477
+ $g.append(
478
+ '<div class="fk-leistung">' +
479
+ ' <div class="fk-leistung-wert"><span class="fk-leistung-zahl">–</span><small>%</small></div>' +
480
+ ' <input type="range" class="fk-schieber" min="30" max="100" step="5" value="100">' +
481
+ ' <div class="fk-leistung-skala"><span>30</span><span>65</span><span>100</span></div>' +
482
+ '</div>');
483
+ $wurzel.append($g);
484
+ b.fuss($wurzel, ctx, data);
485
+
486
+ var $s = $wurzel.find('.fk-schieber');
487
+ // Waehrend des Schiebens nur anzeigen; gesendet wird beim Loslassen, sonst laeuft
488
+ // je Rastung ein Befehl an die Anlage.
489
+ $s.on('input', function () { $wurzel.find('.fk-leistung-zahl').text($s.val()); });
490
+ $s.on('change', function () {
491
+ vis.setValue(ctx.oid('control.demand'), parseInt($s.val(), 10));
492
+ ctx.wartet($wurzel);
493
+ });
494
+
495
+ return function () {
496
+ var w = parseInt(ctx.wert('control.demand'), 10);
497
+ $wurzel.css('--fk-akzent', ctx.akzent());
498
+ if (!isNaN(w)) {
499
+ if (!$s.is(':active')) $s.val(w);
500
+ $wurzel.find('.fk-leistung-zahl').text(w);
501
+ }
502
+ };
503
+ });
504
+ },
505
+
506
+ /* ================================================================== Zusatzfunktionen */
507
+
508
+ zusatz: function (widgetID, view, data, senkrecht) {
509
+ vis.binds.faikout.start(widgetID, data, function ($wurzel, ctx) {
510
+ var b = vis.binds.faikout;
511
+ // `swing` und `swingv`/`swingh` sind zwei Wege zum selben Ziel: hat die Anlage die
512
+ // getrennten Lamellen, ist das Sammel-Feld ueberfluessig und wuerde doppelt wirken.
513
+ var getrennt = ctx.hat('control.swingv') || ctx.hat('control.swingh');
514
+ var liste = b.ZUSATZ.filter(function (z) {
515
+ if (z.feld === 'control.swing' && getrennt) return false;
516
+ return ctx.hat(z.feld);
517
+ });
518
+ if (!liste.length) {
519
+ $wurzel.append('<div class="fk-fehler">Dieses Gerät meldet keine der Zusatzfunktionen.</div>');
520
+ return null;
521
+ }
522
+
523
+ b.kopf($wurzel, ctx, data);
524
+ var $g = $('<div class="fk-gruppe"></div>');
525
+ if (data.etikett !== false) $g.append('<div class="fk-gruppe-etikett">Zusatzfunktionen</div>');
526
+ var $p = $('<div class="fk-pillen"></div>').toggleClass('fk-senkrecht', !!senkrecht);
527
+ liste.forEach(function (z) {
528
+ var mitText = data.beschriftung === true || senkrecht;
529
+ $('<button type="button" class="fk-pille fk-schalt"></button>')
530
+ .addClass(mitText ? '' : 'fk-nur-icon')
531
+ .attr({ 'data-feld': z.feld, title: z.name, 'aria-label': z.name })
532
+ .html(b.SYM[z.sym] + (mitText ? '<span>' + z.name + '</span>' : ''))
533
+ .on('click', function () {
534
+ var alt = ctx.wert(z.feld);
535
+ // `swing` ist eine Liste (off/on), alle anderen sind Schalter.
536
+ // `swing` kennt off/on/V/H/H+V/C (SWING_* in Faikout.c). Umgeschaltet wird nur
537
+ // zwischen aus und ein; die Achsen setzt man ueber swingv/swingh.
538
+ if (z.feld === 'control.swing') vis.setValue(ctx.oid(z.feld), alt === 'off' ? 'on' : 'off');
539
+ else vis.setValue(ctx.oid(z.feld), !alt);
540
+ ctx.wartet($wurzel);
541
+ })
542
+ .appendTo($p);
543
+ });
544
+ $g.append($p);
545
+ $wurzel.append($g);
546
+ b.fuss($wurzel, ctx, data);
547
+
548
+ return function () {
549
+ $wurzel.css('--fk-akzent', ctx.akzent());
550
+ $wurzel.find('.fk-schalt').each(function () {
551
+ var f = $(this).attr('data-feld');
552
+ var v = ctx.wert(f);
553
+ var an = f === 'control.swing' ? !!(v && v !== 'off') : !!v;
554
+ $(this).toggleClass('fk-gewaehlt', an);
555
+ });
556
+ $wurzel.find('.fk-schalter').toggleClass('fk-an', ctx.hat('control.power') ? !!ctx.wert('control.power') : true);
557
+ };
558
+ });
559
+ },
560
+
561
+ /* ================================================================== Messwerte */
562
+
563
+ /** Welche Messwerte sollen erscheinen? Ohne Auswahl die sechs der Standardkachel. */
564
+ gewaehlteWerte: function (data) {
565
+ var b = vis.binds.faikout;
566
+ var aus = b.MESSWERTE.filter(function (m) { return data['zeige_' + m.schluessel] === true; });
567
+ if (aus.length) return aus;
568
+ return b.MESSWERTE.filter(function (m) {
569
+ return ['aussen', 'leistung', 'heute', 'verdichter', 'ventilator', 'kaeltemittel']
570
+ .indexOf(m.schluessel) >= 0;
571
+ });
572
+ },
573
+
574
+ messwerte: function (widgetID, view, data) {
575
+ vis.binds.faikout.start(widgetID, data, function ($wurzel, ctx) {
576
+ var b = vis.binds.faikout;
577
+ b.kopf($wurzel, ctx, data);
578
+ var liste = b.gewaehlteWerte(data).filter(function (m) { return ctx.hat(m.feld); });
579
+ if (!liste.length) {
580
+ $wurzel.append('<div class="fk-fehler">Keiner der gewählten Messwerte ist bei diesem Gerät vorhanden.</div>');
581
+ return null;
582
+ }
583
+ $wurzel.append('<div class="fk-messwerte"></div>');
584
+ b.fuss($wurzel, ctx, data);
585
+
586
+ return function () {
587
+ var html = '';
588
+ liste.forEach(function (m) {
589
+ var v = b.zahl(ctx.wert(m.feld), m.stellen);
590
+ if (v === null) return;
591
+ html += '<div class="fk-mw"><div class="fk-mw-name">' + m.name + '</div>' +
592
+ '<div class="fk-mw-wert">' + v + '<small>' + m.einheit + '</small></div></div>';
593
+ });
594
+ $wurzel.find('.fk-messwerte').html(html);
595
+ $wurzel.css('--fk-akzent', ctx.akzent());
596
+ };
597
+ });
598
+ },
599
+
600
+ einzelwert: function (widgetID, view, data) {
601
+ vis.binds.faikout.start(widgetID, data, function ($wurzel, ctx) {
602
+ var b = vis.binds.faikout;
603
+ var m = null;
604
+ for (var i = 0; i < b.MESSWERTE.length; i++) {
605
+ if (b.MESSWERTE[i].schluessel === data.messwert) m = b.MESSWERTE[i];
606
+ }
607
+ if (!m) {
608
+ $wurzel.html('<div class="fk-fehler">Kein Messwert gewählt.</div>');
609
+ return null;
610
+ }
611
+ if (!ctx.hat(m.feld)) return b.fehlt($wurzel, m.feld);
612
+
613
+ $wurzel.append(
614
+ '<div class="fk-einzel">' +
615
+ (data.etikett === false ? '' : '<div class="fk-einzel-name">' + (data.titel || m.name) + '</div>') +
616
+ ' <div class="fk-einzel-wert">–<small>' + m.einheit + '</small></div>' +
617
+ '</div>');
618
+
619
+ return function () {
620
+ var v = b.zahl(ctx.wert(m.feld), m.stellen);
621
+ $wurzel.find('.fk-einzel-wert').html((v === null ? '–' : v) + '<small>' + m.einheit + '</small>');
622
+ $wurzel.css('--fk-akzent', ctx.akzent());
623
+ };
624
+ });
625
+ },
626
+
627
+ /* ================================================================== Energieverlauf */
628
+
629
+ /** Farben der drei Reihen. Kalt/warm sind gesetzt, Gesamt bleibt neutral dazwischen. */
630
+ VERLAUF_FARBEN: { gesamt: '#a78bfa', kuehlen: '#4fc3f7', heizen: '#ff7043' },
631
+
632
+ /**
633
+ * Liest eine der drei Reihen aus dem Adapter.
634
+ *
635
+ * Alle drei fuehrt der Adapter selbst - Stunden, Tage und Monate. Es ist bewusst kein
636
+ * Fremdskript noetig: sonst blieben Monats- und Jahresansicht bei jedem leer, der nichts
637
+ * weiter einrichtet.
638
+ *
639
+ * @param {function} fertig ([{beschriftung, kuehlen, heizen}], hinweis) => void
640
+ */
641
+ verlaufLesen: function (ctx, bereich, fertig) {
642
+ var b = vis.binds.faikout;
643
+ var FELD = { tag: 'stundenJson', monat: 'tageJson', jahr: 'monateJson' };
644
+ var feld = FELD[bereich] || FELD.tag;
645
+
646
+ // Kuehlen und Heizen stehen getrennt; sie werden ueber ihren Zeitschluessel vereint.
647
+ var reihen = {};
648
+ var offen = 2;
649
+ var gefunden = false;
650
+
651
+ ['kuehlen', 'heizen'].forEach(function (art) {
652
+ var pfad = 'verbrauch.' + art + '.' + feld;
653
+ if (!ctx.hat(pfad)) {
654
+ if (!--offen) b.verlaufFertig(reihen, fertig, bereich, gefunden ? null : feld);
655
+ return;
656
+ }
657
+ gefunden = true;
658
+ vis.conn.getState(ctx.oid(pfad), function (err, zustand) {
659
+ if (!err && zustand && zustand.val) {
660
+ try {
661
+ JSON.parse(zustand.val).forEach(function (p) {
662
+ // Der Adapter benennt den Schluessel je nach Reihe verschieden.
663
+ var k = p.stunde || p.tag || p.monat;
664
+ // Luecken tragen keinen Schluessel - sie gehoeren nicht ins Diagramm.
665
+ if (!k) return;
666
+ if (!reihen[k]) reihen[k] = { schluessel: k, kuehlen: 0, heizen: 0 };
667
+ reihen[k][art] = Number(p.kwh) || 0;
668
+ });
669
+ } catch (e) { /* unbrauchbares JSON wird still uebergangen */ }
670
+ }
671
+ if (!--offen) b.verlaufFertig(reihen, fertig, bereich, null);
672
+ });
673
+ });
674
+ },
675
+
676
+ verlaufFertig: function (reihen, fertig, bereich, fehlendesFeld) {
677
+ var b = vis.binds.faikout;
678
+ if (fehlendesFeld) return fertig(null, fehlendesFeld);
679
+ var schluessel = Object.keys(reihen).sort();
680
+ fertig(schluessel.map(function (k) {
681
+ var e = reihen[k];
682
+ return {
683
+ beschriftung: b.verlaufBeschriftung(k, bereich),
684
+ kuehlen: e.kuehlen,
685
+ heizen: e.heizen
686
+ };
687
+ }));
688
+ },
689
+
690
+ /** Kurze Achsenbeschriftung: Stunde, Tag im Monat oder Monatsname. */
691
+ verlaufBeschriftung: function (schluessel, bereich) {
692
+ var MONATE = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'];
693
+ if (bereich === 'tag') return schluessel.slice(-2); // 2026-08-19T14 -> 14
694
+ if (/^\d{4}-\d{2}-\d{2}$/.test(schluessel)) return String(+schluessel.slice(8)) + '.';
695
+ if (/^\d{4}-\d{2}$/.test(schluessel)) return MONATE[+schluessel.slice(5) - 1] || schluessel;
696
+ return schluessel;
697
+ },
698
+
699
+ /** Zeichnet die Saeulen als SVG. Ohne Fremdbibliothek, damit das Widget eigenstaendig bleibt. */
700
+ verlaufZeichnen: function ($ziel, punkte, arten, breite, hoehe) {
701
+ var b = vis.binds.faikout;
702
+ var LINKS = 34, UNTEN = 18, OBEN = 8, RECHTS = 4;
703
+ var flaecheB = breite - LINKS - RECHTS;
704
+ var flaecheH = hoehe - UNTEN - OBEN;
705
+
706
+ // Gesamt liegt hinten, die Anteile stehen davor - so bleiben beide ablesbar.
707
+ var reihen = [];
708
+ if (arten.gesamt) reihen.push({ art: 'gesamt', werte: punkte.map(function (p) { return p.kuehlen + p.heizen; }) });
709
+ if (arten.heizen) reihen.push({ art: 'heizen', werte: punkte.map(function (p) { return p.heizen; }) });
710
+ if (arten.kuehlen) reihen.push({ art: 'kuehlen', werte: punkte.map(function (p) { return p.kuehlen; }) });
711
+
712
+ var max = 0.1;
713
+ reihen.forEach(function (r) { r.werte.forEach(function (v) { if (v > max) max = v; }); });
714
+ // Auf eine glatte Stufe aufrunden, damit die Achse lesbare Zahlen traegt.
715
+ var stufe = max <= 1 ? 0.25 : max <= 10 ? 2 : max <= 50 ? 10 : max <= 200 ? 50 : 100;
716
+ var spitze = Math.ceil(max / stufe) * stufe;
717
+
718
+ function y(v) { return OBEN + flaecheH - (v / spitze) * flaecheH; }
719
+ var gruppeB = flaecheB / Math.max(punkte.length, 1);
720
+ var balkenB = Math.max(2, gruppeB / Math.max(reihen.length, 1) - 1.5);
721
+
722
+ var svg = '<svg viewBox="0 0 ' + breite + ' ' + hoehe + '" preserveAspectRatio="none" class="fk-verlauf-svg">';
723
+ svg += '<g class="fk-v-raster">';
724
+ for (var i = 0; i <= 2; i++) {
725
+ svg += '<line x1="' + LINKS + '" y1="' + y(spitze * i / 2).toFixed(1) +
726
+ '" x2="' + (breite - RECHTS) + '" y2="' + y(spitze * i / 2).toFixed(1) + '"/>';
727
+ }
728
+ svg += '</g><g class="fk-v-achse">';
729
+ for (var j = 0; j <= 2; j++) {
730
+ var v = spitze * j / 2;
731
+ var t = v >= 10 ? v.toFixed(0) : v.toFixed(v < 1 ? 2 : 1);
732
+ svg += '<text x="' + (LINKS - 4) + '" y="' + (y(v) + 3).toFixed(1) + '" text-anchor="end">' + t + '</text>';
733
+ }
734
+ // Beschriftung ausduennen, sonst kleben 24 oder 31 Werte aneinander.
735
+ var schritt = Math.ceil(punkte.length / 8) || 1;
736
+ punkte.forEach(function (p, k) {
737
+ if (k % schritt) return;
738
+ svg += '<text x="' + (LINKS + gruppeB * (k + 0.5)).toFixed(1) + '" y="' + (hoehe - 5) +
739
+ '" text-anchor="middle">' + p.beschriftung + '</text>';
740
+ });
741
+ svg += '</g><g class="fk-v-balken">';
742
+ reihen.forEach(function (r, ri) {
743
+ r.werte.forEach(function (wert, k) {
744
+ if (wert <= 0) return;
745
+ var x = LINKS + gruppeB * k + (gruppeB - balkenB * reihen.length) / 2 + ri * balkenB;
746
+ var h = Math.max(1, OBEN + flaecheH - y(wert));
747
+ svg += '<rect x="' + x.toFixed(1) + '" y="' + y(wert).toFixed(1) +
748
+ '" width="' + balkenB.toFixed(1) + '" height="' + h.toFixed(1) +
749
+ '" rx="1" fill="' + b.VERLAUF_FARBEN[r.art] + '">' +
750
+ '<title>' + punkte[k].beschriftung + ' · Kühlen ' + punkte[k].kuehlen.toFixed(1) +
751
+ ' kWh · Heizen ' + punkte[k].heizen.toFixed(1) + ' kWh</title></rect>';
752
+ });
753
+ });
754
+ svg += '</g></svg>';
755
+ $ziel.html(svg);
756
+ },
757
+
758
+ verlauf: function (widgetID, view, data) {
759
+ vis.binds.faikout.start(widgetID, data, function ($wurzel, ctx) {
760
+ var b = vis.binds.faikout;
761
+ b.kopf($wurzel, ctx, data);
762
+
763
+ var bereich = data.bereich || 'tag';
764
+ var arten = {
765
+ gesamt: data.zeige_gesamt === true,
766
+ kuehlen: data.zeige_kuehlen !== false,
767
+ heizen: data.zeige_heizen !== false
768
+ };
769
+ var BESCHRIFTUNG = { tag: 'Heute', monat: 'Dieser Monat', jahr: 'Dieses Jahr' };
770
+
771
+ var $box = $('<div class="fk-verlauf"></div>').appendTo($wurzel);
772
+ if (data.umschalter !== false) {
773
+ var $schalter = $('<div class="fk-v-schalter"></div>').appendTo($box);
774
+ ['tag', 'monat', 'jahr'].forEach(function (wahl) {
775
+ $('<button type="button"></button>')
776
+ .text({ tag: 'Tag', monat: 'Monat', jahr: 'Jahr' }[wahl])
777
+ .attr('aria-pressed', wahl === bereich ? 'true' : 'false')
778
+ .on('click', function () {
779
+ bereich = wahl;
780
+ $schalter.find('button').attr('aria-pressed', 'false');
781
+ $(this).attr('aria-pressed', 'true');
782
+ laden();
783
+ })
784
+ .appendTo($schalter);
785
+ });
786
+ }
787
+ if (data.auswahl !== false) {
788
+ var $arten = $('<div class="fk-v-arten"></div>').appendTo($box);
789
+ [['gesamt', 'Gesamt'], ['kuehlen', 'Kühlen'], ['heizen', 'Heizen']].forEach(function (paar) {
790
+ var $l = $('<label></label>').appendTo($arten);
791
+ $('<input type="checkbox">')
792
+ .prop('checked', arten[paar[0]])
793
+ .on('change', function () { arten[paar[0]] = this.checked; laden(); })
794
+ .appendTo($l);
795
+ $('<span class="fk-v-punkt"></span>')
796
+ .css('background', b.VERLAUF_FARBEN[paar[0]]).appendTo($l);
797
+ $l.append(document.createTextNode(paar[1]));
798
+ });
799
+ }
800
+ var $flaeche = $('<div class="fk-v-flaeche"></div>').appendTo($box);
801
+ var $fuss = $('<div class="fk-v-fuss"><span class="fk-v-zeitraum"></span>' +
802
+ '<span class="fk-v-summe"></span></div>').appendTo($box);
803
+ b.fuss($wurzel, ctx, data);
804
+
805
+ function laden() {
806
+ b.verlaufLesen(ctx, bereich, function (punkte, fehlend) {
807
+ $fuss.find('.fk-v-zeitraum').text(BESCHRIFTUNG[bereich] || '');
808
+ if (punkte === null) {
809
+ $flaeche.html('<div class="fk-fehler">Dieses Gerät hat keinen Datenpunkt ' +
810
+ '<code>verbrauch.&lt;art&gt;.' + fehlend + '</code>. Er entsteht, sobald ' +
811
+ 'die Anlage Energiewerte meldet.</div>');
812
+ $fuss.find('.fk-v-summe').text('');
813
+ return;
814
+ }
815
+ if (!punkte.length) {
816
+ $flaeche.html('<div class="fk-v-leer">Noch keine Werte für diesen Zeitraum.</div>');
817
+ $fuss.find('.fk-v-summe').text('');
818
+ return;
819
+ }
820
+ var w = $flaeche.width() || 400;
821
+ b.verlaufZeichnen($flaeche, punkte, arten, w, Math.max(110, Math.round(w * 0.42)));
822
+ var summe = punkte.reduce(function (a, p) { return a + p.kuehlen + p.heizen; }, 0);
823
+ $fuss.find('.fk-v-summe').html('Summe <b>' +
824
+ (summe >= 100 ? summe.toFixed(0) : summe.toFixed(1)) + ' kWh</b>');
825
+ });
826
+ }
827
+
828
+ return function () {
829
+ $wurzel.css('--fk-akzent', ctx.akzent());
830
+ laden();
831
+ };
832
+ });
833
+ },
834
+
835
+ /* ================================================================== Komplette Kachel */
836
+
837
+ kachel: function (widgetID, view, data) {
838
+ vis.binds.faikout.start(widgetID, data, function ($wurzel, ctx) {
839
+ var b = vis.binds.faikout;
840
+ if (!ctx.hat('control.target')) return b.fehlt($wurzel, 'control.target');
841
+ var teile = [];
842
+
843
+ $wurzel.append('<div class="fk-streifen"></div>');
844
+ b.kopf($wurzel, ctx, data);
845
+
846
+ teile.push(b.bogenBauen($wurzel, ctx, data, 'fkskala_' + widgetID));
847
+ if (ctx.hat('control.mode')) {
848
+ teile.push(b.teilPillen($wurzel, ctx, 'Betriebsart', 'control.mode', b.MODI, false, data.symbole !== false));
849
+ }
850
+ if (ctx.hat('control.fan')) {
851
+ teile.push(b.teilPillen($wurzel, ctx, 'Lüfter', 'control.fan', b.LUEFTER, true, false));
852
+ }
853
+
854
+ // Schwenken gehoert auf die grosse Kachel. Die uebrigen Zusatzfunktionen kommen
855
+ // nur dazu, wenn das Attribut `zusatz` gesetzt ist - sonst wird die Kachel zu voll.
856
+ var schalter = b.ZUSATZ.filter(function (z) {
857
+ if (!ctx.hat(z.feld)) return false;
858
+ // `swing` und `swingv`/`swingh` sind zwei Wege zum selben Ziel: hat die Anlage
859
+ // die getrennten Lamellen, waere das Sammel-Feld doppelt.
860
+ if (z.feld === 'control.swing' && (ctx.hat('control.swingv') || ctx.hat('control.swingh'))) return false;
861
+ var istSchwenken = z.feld === 'control.swing' || z.feld === 'control.swingv' || z.feld === 'control.swingh';
862
+ return istSchwenken || data.zusatz === true;
863
+ });
864
+ if (schalter.length) teile.push(b.teilSchalter($wurzel, ctx, schalter, data));
865
+
866
+ teile.push(b.teilMesswerte($wurzel, ctx, data));
867
+ b.fuss($wurzel, ctx, data);
868
+
869
+ return function () {
870
+ var an = ctx.hat('control.power') ? !!ctx.wert('control.power') : true;
871
+ $wurzel.css('--fk-akzent', ctx.akzent()).toggleClass('fk-aus', !an);
872
+ $wurzel.find('.fk-schalter').toggleClass('fk-an', an);
873
+ teile.forEach(function (f) { if (f) f(); });
874
+ var erreichbar = ctx.hat('info.online') ? !!ctx.wert('info.online') : true;
875
+ $wurzel.find('.fk-punkt').toggleClass('fk-weg', !erreichbar).text(erreichbar ? 'verbunden' : 'offline');
876
+ };
877
+ });
878
+ },
879
+
880
+ /* --- Reihen der Kachel (ohne eigenen Rahmen und ohne Kopfzeile) --- */
881
+
882
+ teilPillen: function ($wurzel, ctx, etikett, feld, liste, nurText, symbole) {
883
+ var b = vis.binds.faikout;
884
+ var $g = $('<div class="fk-gruppe" style="flex:none"></div>')
885
+ .append('<div class="fk-gruppe-etikett">' + etikett + '</div>');
886
+ var $p = $('<div class="fk-pillen"></div>');
887
+ liste.forEach(function (e) {
888
+ var $t = $('<button type="button" class="fk-pille"></button>').attr('data-wert', e.wert);
889
+ if (nurText) $t.addClass('fk-nur-text').text(e.text);
890
+ else $t.html((symbole ? b.SYM[e.sym] : '') + '<span>' + e.text + '</span>');
891
+ $t.on('click', function () {
892
+ vis.setValue(ctx.oid(feld), e.wert);
893
+ if (feld === 'control.mode' && ctx.hat('control.power') && !ctx.wert('control.power')) {
894
+ vis.setValue(ctx.oid('control.power'), true);
895
+ }
896
+ ctx.wartet($wurzel);
897
+ });
898
+ $p.append($t);
899
+ });
900
+ $g.append($p);
901
+ $wurzel.append($g);
902
+ return function () {
903
+ var w = ctx.wert(feld);
904
+ var an = ctx.hat('control.power') ? !!ctx.wert('control.power') : true;
905
+ $g.find('.fk-pille').each(function () {
906
+ var passt = $(this).attr('data-wert') === w;
907
+ $(this).toggleClass('fk-gewaehlt', feld === 'control.mode' ? (passt && an) : passt);
908
+ });
909
+ };
910
+ },
911
+
912
+ /** Reihe mit Symbolschaltern (Schwenken, Sparen, Turbo …) fuer die grosse Kachel. */
913
+ teilSchalter: function ($wurzel, ctx, liste, data) {
914
+ var b = vis.binds.faikout;
915
+ var $g = $('<div class="fk-gruppe" style="flex:none"></div>')
916
+ .append('<div class="fk-gruppe-etikett">' +
917
+ (liste.length > 3 ? 'Zusatzfunktionen' : 'Schwenken') + '</div>');
918
+ var $p = $('<div class="fk-pillen"></div>');
919
+ liste.forEach(function (z) {
920
+ var mitText = data && data.beschriftung === true;
921
+ $('<button type="button" class="fk-pille fk-schalt"></button>')
922
+ .addClass(mitText ? '' : 'fk-nur-icon')
923
+ .attr({ 'data-feld': z.feld, title: z.name, 'aria-label': z.name })
924
+ .html(b.SYM[z.sym] + (mitText ? '<span>' + z.name + '</span>' : ''))
925
+ .on('click', function () {
926
+ var alt = ctx.wert(z.feld);
927
+ // `swing` kennt off/on/V/H/H+V/C (SWING_* in Faikout.c). Umgeschaltet wird nur
928
+ // zwischen aus und ein; die Achsen setzt man ueber swingv/swingh.
929
+ if (z.feld === 'control.swing') vis.setValue(ctx.oid(z.feld), alt === 'off' ? 'on' : 'off');
930
+ else vis.setValue(ctx.oid(z.feld), !alt);
931
+ ctx.wartet($wurzel);
932
+ })
933
+ .appendTo($p);
934
+ });
935
+ $g.append($p);
936
+ $wurzel.append($g);
937
+ return function () {
938
+ $g.find('.fk-schalt').each(function () {
939
+ var f = $(this).attr('data-feld');
940
+ var v = ctx.wert(f);
941
+ $(this).toggleClass('fk-gewaehlt', f === 'control.swing' ? !!(v && v !== 'off') : !!v);
942
+ });
943
+ };
944
+ },
945
+
946
+ teilMesswerte: function ($wurzel, ctx, data) {
947
+ var b = vis.binds.faikout;
948
+ var liste = b.gewaehlteWerte(data).filter(function (m) { return ctx.hat(m.feld); });
949
+ if (!liste.length) return null;
950
+ var $m = $('<div class="fk-messwerte" style="flex:none;border-top:1px solid var(--fk-rand)"></div>');
951
+ $wurzel.append($m);
952
+ return function () {
953
+ var html = '';
954
+ liste.forEach(function (m) {
955
+ var v = b.zahl(ctx.wert(m.feld), m.stellen);
956
+ if (v === null) return;
957
+ html += '<div class="fk-mw"><div class="fk-mw-name">' + m.name + '</div>' +
958
+ '<div class="fk-mw-wert">' + v + '<small>' + m.einheit + '</small></div></div>';
959
+ });
960
+ $m.html(html);
961
+ };
962
+ }
963
+ };