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.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/admin/faikout.png +0 -0
- package/admin/i18n/de/translations.json +25 -0
- package/admin/i18n/en/translations.json +25 -0
- package/admin/i18n/es/translations.json +25 -0
- package/admin/i18n/fr/translations.json +25 -0
- package/admin/i18n/it/translations.json +25 -0
- package/admin/i18n/nl/translations.json +25 -0
- package/admin/i18n/pl/translations.json +25 -0
- package/admin/i18n/pt/translations.json +25 -0
- package/admin/i18n/ru/translations.json +25 -0
- package/admin/i18n/uk/translations.json +25 -0
- package/admin/i18n/zh-cn/translations.json +25 -0
- package/admin/jsonConfig.json +187 -0
- package/io-package.json +185 -0
- package/lib/broker.js +129 -0
- package/lib/fields.js +266 -0
- package/lib/verbrauch.js +214 -0
- package/main.js +451 -0
- package/package.json +56 -0
- package/widgets/faikout/css/faikout.css +233 -0
- package/widgets/faikout/js/faikout.js +963 -0
- package/widgets/faikout.html +240 -0
package/io-package.json
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
{
|
|
2
|
+
"common": {
|
|
3
|
+
"name": "faikout",
|
|
4
|
+
"version": "0.0.5",
|
|
5
|
+
"titleLang": {
|
|
6
|
+
"en": "Faikout",
|
|
7
|
+
"de": "Faikout",
|
|
8
|
+
"ru": "Faikout",
|
|
9
|
+
"pt": "Faikout",
|
|
10
|
+
"nl": "Faikout",
|
|
11
|
+
"fr": "Faikout",
|
|
12
|
+
"it": "Faikout",
|
|
13
|
+
"es": "Faikout",
|
|
14
|
+
"pl": "Faikout",
|
|
15
|
+
"uk": "Faikout",
|
|
16
|
+
"zh-cn": "Faikout"
|
|
17
|
+
},
|
|
18
|
+
"desc": {
|
|
19
|
+
"en": "Connect Daikin air conditioners with a faikout module locally via MQTT.",
|
|
20
|
+
"de": "Verbindet Daikin-Klimaanlagen mit faikout-Modul lokal über MQTT.",
|
|
21
|
+
"ru": "Локальное подключение кондиционеров Daikin с модулем faikout через MQTT.",
|
|
22
|
+
"pt": "Liga ar condicionados Daikin com módulo faikout localmente via MQTT.",
|
|
23
|
+
"nl": "Verbindt Daikin-airco's met faikout-module lokaal via MQTT.",
|
|
24
|
+
"fr": "Connecte localement les climatiseurs Daikin équipés d'un module faikout via MQTT.",
|
|
25
|
+
"it": "Collega localmente i condizionatori Daikin con modulo faikout tramite MQTT.",
|
|
26
|
+
"es": "Conecta localmente aires acondicionados Daikin con módulo faikout mediante MQTT.",
|
|
27
|
+
"pl": "Łączy lokalnie klimatyzatory Daikin z modułem faikout przez MQTT.",
|
|
28
|
+
"uk": "Локальне підключення кондиціонерів Daikin з модулем faikout через MQTT.",
|
|
29
|
+
"zh-cn": "通过 MQTT 在本地连接带 faikout 模块的大金空调。"
|
|
30
|
+
},
|
|
31
|
+
"authors": [
|
|
32
|
+
"Immanuel"
|
|
33
|
+
],
|
|
34
|
+
"keywords": [
|
|
35
|
+
"faikout",
|
|
36
|
+
"faikin",
|
|
37
|
+
"daikin",
|
|
38
|
+
"mqtt",
|
|
39
|
+
"climate"
|
|
40
|
+
],
|
|
41
|
+
"licenseInformation": {
|
|
42
|
+
"type": "free",
|
|
43
|
+
"license": "MIT"
|
|
44
|
+
},
|
|
45
|
+
"platform": "Javascript/Node.js",
|
|
46
|
+
"icon": "faikout.png",
|
|
47
|
+
"enabled": true,
|
|
48
|
+
"extIcon": "https://raw.githubusercontent.com/SmarthomeElektroniker/ioBroker.faikout/main/admin/faikout.png",
|
|
49
|
+
"readme": "https://github.com/SmarthomeElektroniker/ioBroker.faikout/blob/main/README.md",
|
|
50
|
+
"loglevel": "info",
|
|
51
|
+
"tier": 2,
|
|
52
|
+
"mode": "daemon",
|
|
53
|
+
"type": "climate-control",
|
|
54
|
+
"compact": true,
|
|
55
|
+
"connectionType": "local",
|
|
56
|
+
"dataSource": "push",
|
|
57
|
+
"adminUI": {
|
|
58
|
+
"config": "json"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": [
|
|
61
|
+
{
|
|
62
|
+
"js-controller": ">=6.0.11"
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"globalDependencies": [
|
|
66
|
+
{
|
|
67
|
+
"admin": ">=7.6.20"
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"news": {
|
|
71
|
+
"0.0.5": {
|
|
72
|
+
"en": "Meet the ioBroker repository requirements",
|
|
73
|
+
"de": "Anforderungen des ioBroker-Repositorys erfuellt",
|
|
74
|
+
"ru": "Соответствие требованиям репозитория ioBroker",
|
|
75
|
+
"pt": "Cumpre os requisitos do repositório ioBroker",
|
|
76
|
+
"nl": "Voldoet aan de eisen van het ioBroker-repository",
|
|
77
|
+
"fr": "Conforme aux exigences du dépôt ioBroker",
|
|
78
|
+
"it": "Conforme ai requisiti del repository ioBroker",
|
|
79
|
+
"es": "Cumple los requisitos del repositorio ioBroker",
|
|
80
|
+
"pl": "Spełnia wymagania repozytorium ioBroker",
|
|
81
|
+
"uk": "Відповідає вимогам репозиторію ioBroker",
|
|
82
|
+
"zh-cn": "满足 ioBroker 仓库的收录要求"
|
|
83
|
+
},
|
|
84
|
+
"0.0.4": {
|
|
85
|
+
"en": "Fix: admin rejected the configuration (\"invalid jsonConfig\")",
|
|
86
|
+
"de": "Fehlerbehebung: Admin verwarf die Konfiguration (\"invalid jsonConfig\")",
|
|
87
|
+
"ru": "Исправление: admin отклонял конфигурацию",
|
|
88
|
+
"pt": "Correção: o admin rejeitava a configuração",
|
|
89
|
+
"nl": "Fix: admin weigerde de configuratie",
|
|
90
|
+
"fr": "Correctif : l'admin rejetait la configuration",
|
|
91
|
+
"it": "Correzione: admin rifiutava la configurazione",
|
|
92
|
+
"es": "Corrección: admin rechazaba la configuración",
|
|
93
|
+
"pl": "Poprawka: admin odrzucał konfigurację",
|
|
94
|
+
"uk": "Виправлення: admin відхиляв конфігурацію",
|
|
95
|
+
"zh-cn": "修复:admin 拒绝了配置"
|
|
96
|
+
},
|
|
97
|
+
"0.0.3": {
|
|
98
|
+
"en": "Fix: consumption crashed after updating from 0.0.1 (missing day/month rings)",
|
|
99
|
+
"de": "Fehlerbehebung: Verbrauch lief nach dem Update von 0.0.1 auf einen Fehler (fehlende Tages-/Monatsringe)",
|
|
100
|
+
"ru": "Исправление: расход давал сбой после обновления с 0.0.1",
|
|
101
|
+
"pt": "Correção: o consumo falhava após a atualização a partir de 0.0.1",
|
|
102
|
+
"nl": "Fix: verbruik liep vast na de update vanaf 0.0.1",
|
|
103
|
+
"fr": "Correctif : la consommation échouait après la mise à jour depuis 0.0.1",
|
|
104
|
+
"it": "Correzione: il consumo si interrompeva dopo l'aggiornamento da 0.0.1",
|
|
105
|
+
"es": "Corrección: el consumo fallaba tras actualizar desde 0.0.1",
|
|
106
|
+
"pl": "Poprawka: zużycie zawodziło po aktualizacji z 0.0.1",
|
|
107
|
+
"uk": "Виправлення: споживання давало збій після оновлення з 0.0.1",
|
|
108
|
+
"zh-cn": "修复:从 0.0.1 更新后能耗计算出错"
|
|
109
|
+
},
|
|
110
|
+
"0.0.2": {
|
|
111
|
+
"en": "New VIS widget: energy history, switchable between day, month and year",
|
|
112
|
+
"de": "Neuer VIS-Baustein: Energieverlauf, umschaltbar zwischen Tag, Monat und Jahr",
|
|
113
|
+
"ru": "Отображение энергопотребления как VIS-виджета, месячные значения в адаптере",
|
|
114
|
+
"pt": "Consumo de energia como widget VIS, valores mensais no adaptador",
|
|
115
|
+
"nl": "Energieverloop als VIS-widget, maandwaarden in de adapter",
|
|
116
|
+
"fr": "Historique d'énergie comme widget VIS, valeurs mensuelles dans l'adaptateur",
|
|
117
|
+
"it": "Andamento energetico come widget VIS, valori mensili nell'adapter",
|
|
118
|
+
"es": "Histórico de energía como widget VIS, valores mensuales en el adaptador",
|
|
119
|
+
"pl": "Przebieg energii jako widget VIS, wartości miesięczne w adapterze",
|
|
120
|
+
"uk": "Графік енергоспоживання як VIS-віджет, місячні значення в адаптері",
|
|
121
|
+
"zh-cn": "能耗曲线 VIS 控件,适配器内月度数值"
|
|
122
|
+
},
|
|
123
|
+
"0.0.1": {
|
|
124
|
+
"en": "Initial version: own MQTT broker, dynamic data points, writable controls.",
|
|
125
|
+
"de": "Erste Version: eigener MQTT-Broker, dynamische Datenpunkte, schreibbare Steuerung.",
|
|
126
|
+
"ru": "Первая версия: собственный MQTT-брокер, динамические точки данных, управление.",
|
|
127
|
+
"pt": "Primeira versão: broker MQTT próprio, pontos de dados dinâmicos, controlo gravável.",
|
|
128
|
+
"nl": "Eerste versie: eigen MQTT-broker, dynamische datapunten, schrijfbare besturing.",
|
|
129
|
+
"fr": "Première version : courtier MQTT intégré, points de données dynamiques, commandes inscriptibles.",
|
|
130
|
+
"it": "Prima versione: broker MQTT proprio, punti dati dinamici, controlli scrivibili.",
|
|
131
|
+
"es": "Primera versión: broker MQTT propio, puntos de datos dinámicos, controles escribibles.",
|
|
132
|
+
"pl": "Pierwsza wersja: własny broker MQTT, dynamiczne punkty danych, sterowanie zapisywalne.",
|
|
133
|
+
"uk": "Перша версія: власний MQTT-брокер, динамічні точки даних, керування.",
|
|
134
|
+
"zh-cn": "首个版本:自带 MQTT 代理、动态数据点、可写控制。"
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"restartAdapters": [
|
|
138
|
+
"vis",
|
|
139
|
+
"vis-2"
|
|
140
|
+
]
|
|
141
|
+
},
|
|
142
|
+
"native": {
|
|
143
|
+
"port": 1888,
|
|
144
|
+
"user": "",
|
|
145
|
+
"pass": "",
|
|
146
|
+
"historyAnmelden": true,
|
|
147
|
+
"historyInstanz": "history.0"
|
|
148
|
+
},
|
|
149
|
+
"encryptedNative": [
|
|
150
|
+
"pass"
|
|
151
|
+
],
|
|
152
|
+
"protectedNative": [
|
|
153
|
+
"pass"
|
|
154
|
+
],
|
|
155
|
+
"objects": [],
|
|
156
|
+
"instanceObjects": [
|
|
157
|
+
{
|
|
158
|
+
"_id": "info",
|
|
159
|
+
"type": "channel",
|
|
160
|
+
"common": {
|
|
161
|
+
"name": {
|
|
162
|
+
"en": "Information",
|
|
163
|
+
"de": "Informationen"
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
"native": {}
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"_id": "info.connection",
|
|
170
|
+
"type": "state",
|
|
171
|
+
"common": {
|
|
172
|
+
"role": "indicator.connected",
|
|
173
|
+
"name": {
|
|
174
|
+
"en": "At least one faikout module connected",
|
|
175
|
+
"de": "Mindestens ein faikout-Modul verbunden"
|
|
176
|
+
},
|
|
177
|
+
"type": "boolean",
|
|
178
|
+
"read": true,
|
|
179
|
+
"write": false,
|
|
180
|
+
"def": false
|
|
181
|
+
},
|
|
182
|
+
"native": {}
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
}
|
package/lib/broker.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Eigener MQTT-Broker fuer die faikout-Module.
|
|
5
|
+
*
|
|
6
|
+
* Bewusst ein eigener Broker auf eigenem Port statt Mitbenutzung eines vorhandenen
|
|
7
|
+
* (z. B. mqtt.0): Die Klimaanlagen sollen von allem anderen getrennt bleiben, und der
|
|
8
|
+
* Adapter kennt so jede Nachricht seiner Geraete, ohne fremden Verkehr mitzulesen.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const net = require('node:net');
|
|
12
|
+
// aedes 1.x exportiert die Klasse benannt; der Aufruf als Funktion (aedes(...)) faellt weg.
|
|
13
|
+
const { Aedes } = require('aedes');
|
|
14
|
+
|
|
15
|
+
class FaikoutBroker {
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} opts
|
|
18
|
+
* @param {number} opts.port Port, auf dem gelauscht wird
|
|
19
|
+
* @param {string} [opts.user] Optionaler Benutzername; leer = keine Anmeldung noetig
|
|
20
|
+
* @param {string} [opts.pass] Passwort dazu
|
|
21
|
+
* @param {object} opts.log Logger des Adapters
|
|
22
|
+
* @param {(topic:string, payload:Buffer)=>void} opts.onMessage
|
|
23
|
+
* @param {(clientId:string, verbunden:boolean)=>void} [opts.onClient]
|
|
24
|
+
*/
|
|
25
|
+
constructor(opts) {
|
|
26
|
+
this.port = opts.port;
|
|
27
|
+
this.user = opts.user || '';
|
|
28
|
+
this.pass = opts.pass || '';
|
|
29
|
+
this.log = opts.log;
|
|
30
|
+
this.onMessage = opts.onMessage;
|
|
31
|
+
this.onClient = opts.onClient || (() => {});
|
|
32
|
+
this.broker = null;
|
|
33
|
+
this.server = null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async start() {
|
|
37
|
+
// aedes 1.x wird ueber eine asynchrone Fabrik erzeugt. Ein blosses `new Aedes()`
|
|
38
|
+
// liefert zwar eine Instanz, deren `handle` aber nie antwortet - der Client laeuft
|
|
39
|
+
// dann in "connack timeout".
|
|
40
|
+
this.broker = await Aedes.createBroker({
|
|
41
|
+
id: 'iobroker-faikout',
|
|
42
|
+
// Anmeldung nur pruefen, wenn in den Instanzeinstellungen etwas hinterlegt ist.
|
|
43
|
+
// Ohne Benutzer bleibt der Broker offen - im eigenen LAN der uebliche Fall, und
|
|
44
|
+
// die faikout-Module koennen ohne Anmeldung senden.
|
|
45
|
+
authenticate: (client, username, password, done) => {
|
|
46
|
+
if (!this.user) return done(null, true);
|
|
47
|
+
const u = username ? String(username) : '';
|
|
48
|
+
const p = password ? password.toString() : '';
|
|
49
|
+
if (u !== this.user || p !== this.pass) {
|
|
50
|
+
this.log.warn(`MQTT-Anmeldung abgelehnt für "${client && client.id}" (Benutzer "${u}")`);
|
|
51
|
+
const err = new Error('Anmeldung abgelehnt');
|
|
52
|
+
err.returnCode = 4; // bad user name or password
|
|
53
|
+
return done(err, false);
|
|
54
|
+
}
|
|
55
|
+
done(null, true);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
this.broker.on('client', client => {
|
|
61
|
+
this.log.info(`MQTT-Client verbunden: ${client.id}`);
|
|
62
|
+
this.onClient(client.id, true);
|
|
63
|
+
});
|
|
64
|
+
this.broker.on('clientDisconnect', client => {
|
|
65
|
+
this.log.info(`MQTT-Client getrennt: ${client.id}`);
|
|
66
|
+
this.onClient(client.id, false);
|
|
67
|
+
});
|
|
68
|
+
this.broker.on('clientError', (client, err) => {
|
|
69
|
+
this.log.debug(`MQTT-Clientfehler ${client && client.id}: ${err.message}`);
|
|
70
|
+
});
|
|
71
|
+
this.broker.on('publish', (packet, client) => {
|
|
72
|
+
// Ohne client sind es die internen $SYS-Nachrichten des Brokers.
|
|
73
|
+
if (!client || !packet || !packet.topic) return;
|
|
74
|
+
try {
|
|
75
|
+
this.onMessage(packet.topic, packet.payload);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
this.log.warn(`Fehler beim Verarbeiten von "${packet.topic}": ${e.message}`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
this.server = net.createServer(stream => this.broker.handle(stream));
|
|
82
|
+
this.server.on('error', err => {
|
|
83
|
+
if (err.code === 'EADDRINUSE') {
|
|
84
|
+
this.log.error(`Port ${this.port} ist belegt - anderen Port in den Instanzeinstellungen wählen.`);
|
|
85
|
+
}
|
|
86
|
+
reject(err);
|
|
87
|
+
});
|
|
88
|
+
this.server.listen(this.port, () => {
|
|
89
|
+
this.log.info(`MQTT-Broker lauscht auf Port ${this.port}`);
|
|
90
|
+
resolve();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Veroeffentlicht ein Kommando an ein Geraet. */
|
|
96
|
+
publish(topic, payload) {
|
|
97
|
+
return new Promise(resolve => {
|
|
98
|
+
if (!this.broker) return resolve(false);
|
|
99
|
+
this.broker.publish(
|
|
100
|
+
{ topic, payload: Buffer.from(String(payload)), qos: 0, retain: false, cmd: 'publish' },
|
|
101
|
+
err => {
|
|
102
|
+
if (err) this.log.warn(`Senden an "${topic}" fehlgeschlagen: ${err.message}`);
|
|
103
|
+
resolve(!err);
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
stop() {
|
|
110
|
+
return new Promise(resolve => {
|
|
111
|
+
const fertig = () => {
|
|
112
|
+
if (this.broker) {
|
|
113
|
+
this.broker.close(() => resolve());
|
|
114
|
+
this.broker = null;
|
|
115
|
+
} else {
|
|
116
|
+
resolve();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
if (this.server) {
|
|
120
|
+
this.server.close(fertig);
|
|
121
|
+
this.server = null;
|
|
122
|
+
} else {
|
|
123
|
+
fertig();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { FaikoutBroker };
|
package/lib/fields.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Feldkunde der faikout-Module.
|
|
5
|
+
*
|
|
6
|
+
* Grundlage sind die Makrolisten des Originals (ESP/main/acfields.m, accontrols.m,
|
|
7
|
+
* acextras.m) und die Funktion revk_state_extra() in Faikout.c. Wichtig: Ein Modul sendet
|
|
8
|
+
* ein Feld nur, wenn es die angeschlossene Klimaanlage tatsaechlich kann
|
|
9
|
+
* (`daikin.status_known & CONTROL_<name>`), zusaetzlich blenden die no*-Einstellungen aus.
|
|
10
|
+
* Es gibt deshalb KEINE feste Feldliste je Geraet - der Adapter legt an, was ankommt, und
|
|
11
|
+
* benutzt diese Tabelle nur, um bekannten Feldern Rolle, Einheit und Schreibbarkeit zu geben.
|
|
12
|
+
* Unbekannte Felder landen ueber ableiten() trotzdem im Objektbaum.
|
|
13
|
+
*
|
|
14
|
+
* Als Grundlage dient das `state/<Name>`-Topic, nicht `Faikout/<Name>`: Ersteres liefert
|
|
15
|
+
* Klartext (mode "cool", fan "medium") und passt damit zu den Kommandotopics; letzteres das
|
|
16
|
+
* native Format mit Kuerzeln (mode "C", fan "3") und Energie als Whoutside/Whheating.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Betriebsarten, wie sie state/ meldet und command/<Name>/mode entgegennimmt. */
|
|
20
|
+
const MODES = ['off', 'heat_cool', 'cool', 'heat', 'dry', 'fan_only'];
|
|
21
|
+
|
|
22
|
+
/** Luefterstufen in der Reihenfolge der Originaloberflaeche. */
|
|
23
|
+
const FAN_MODES = ['auto', 'night', 'low', 'lowMedium', 'medium', 'mediumHigh', 'high'];
|
|
24
|
+
|
|
25
|
+
function liste(werte) {
|
|
26
|
+
const s = {};
|
|
27
|
+
for (const w of werte) s[w] = w;
|
|
28
|
+
return s;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Bekannte Felder. `w: true` heisst: es gibt ein Kommandotopic command/<Name>/<Feld>.
|
|
33
|
+
* `kanal` sortiert den Datenpunkt in den Objektbaum ein.
|
|
34
|
+
*/
|
|
35
|
+
const FELDER = {
|
|
36
|
+
// ---------------------------------------------------------------- Steuerung
|
|
37
|
+
power: { kanal: 'control', type: 'boolean', role: 'switch.power', w: true, name: { en: 'Power', de: 'Ein/Aus' } },
|
|
38
|
+
mode: {
|
|
39
|
+
kanal: 'control', type: 'string', role: 'level.mode.airconditioner', w: true,
|
|
40
|
+
states: liste(MODES), name: { en: 'Operating mode', de: 'Betriebsart' },
|
|
41
|
+
},
|
|
42
|
+
// ACHTUNG, Asymmetrie: Gelesen wird der Sollwert als `target`, geschrieben aber auf
|
|
43
|
+
// command/<Name>/temp. Belegt durch die HA-Discovery des Geraets selbst
|
|
44
|
+
// (temp_stat_tpl = {{value_json.target}}, temp_cmd_t = ~/temp) und durch die Anlage im
|
|
45
|
+
// Wohnzimmer: temp 23 bei target 18 - 23 war die Raumtemperatur, 18 der Sollwert.
|
|
46
|
+
target: {
|
|
47
|
+
kanal: 'control', type: 'number', role: 'level.temperature', unit: '°C', w: true,
|
|
48
|
+
cmd: 'temp', min: 16, max: 29,
|
|
49
|
+
name: { en: 'Target temperature', de: 'Solltemperatur' },
|
|
50
|
+
},
|
|
51
|
+
fan: {
|
|
52
|
+
kanal: 'control', type: 'string', role: 'level.mode.fan', w: true,
|
|
53
|
+
states: liste(FAN_MODES), name: { en: 'Fan speed', de: 'Lüfterstufe' },
|
|
54
|
+
},
|
|
55
|
+
// ACHTUNG: `swing` kennt sechs Werte, nicht nur off/on. Die HA-Discovery meldet zwar nur
|
|
56
|
+
// off|on, das Geraet selbst sendet aber H, V, H+V (beide Achsen) und C (Komfortbetrieb) -
|
|
57
|
+
// siehe SWING_* in Faikout.c. Am Wohnzimmer-Geraet stand live "V"; mit einer Liste aus
|
|
58
|
+
// nur off/on waere das im VIS ein unbekannter Zustand gewesen.
|
|
59
|
+
swing: {
|
|
60
|
+
kanal: 'control', type: 'string', role: 'level.mode.swing', w: true,
|
|
61
|
+
states: {
|
|
62
|
+
off: 'aus', on: 'ein', V: 'senkrecht', H: 'waagerecht',
|
|
63
|
+
'H+V': 'beide', C: 'Komfort',
|
|
64
|
+
},
|
|
65
|
+
name: { en: 'Swing', de: 'Luftverteilung' },
|
|
66
|
+
},
|
|
67
|
+
swingv: { kanal: 'control', type: 'boolean', role: 'switch', w: true, name: { en: 'Swing vertical', de: 'Lamellen senkrecht' } },
|
|
68
|
+
swingh: { kanal: 'control', type: 'boolean', role: 'switch', w: true, name: { en: 'Swing horizontal', de: 'Lamellen waagerecht' } },
|
|
69
|
+
preset: {
|
|
70
|
+
kanal: 'control', type: 'string', role: 'level.mode.preset', w: true,
|
|
71
|
+
states: liste(['home', 'eco', 'boost']), name: { en: 'Preset', de: 'Voreinstellung' },
|
|
72
|
+
},
|
|
73
|
+
// Kein `states`: Das Geraet fuehrt demand als Schieber 30..100 in 5er-Schritten
|
|
74
|
+
// (addslider(NULL, "Demand", "demand", 30, 100, "5") in Faikout.c). Eine Zustandsliste
|
|
75
|
+
// machte daraus eine Auswahlliste - und weil ihre Texte "30 %" lauteten, haengte ioBroker
|
|
76
|
+
// die Einheit ein zweites Mal an ("100 % %"). `step` sagt dasselbe ohne beide Nachteile.
|
|
77
|
+
demand: {
|
|
78
|
+
kanal: 'control', type: 'number', role: 'level', unit: '%', w: true,
|
|
79
|
+
min: 30, max: 100, step: 5,
|
|
80
|
+
name: { en: 'Demand control', de: 'Leistungsbegrenzung' },
|
|
81
|
+
},
|
|
82
|
+
econo: { kanal: 'control', type: 'boolean', role: 'switch', w: true, name: { en: 'Econo mode', de: 'Sparbetrieb' } },
|
|
83
|
+
powerful: { kanal: 'control', type: 'boolean', role: 'switch', w: true, name: { en: 'Powerful', de: 'Turbo' } },
|
|
84
|
+
comfort: { kanal: 'control', type: 'boolean', role: 'switch', w: true, name: { en: 'Comfort mode', de: 'Komfortbetrieb' } },
|
|
85
|
+
streamer: { kanal: 'control', type: 'boolean', role: 'switch', w: true, name: { en: 'Streamer', de: 'Luftreinigung' } },
|
|
86
|
+
sensor: { kanal: 'control', type: 'boolean', role: 'switch', w: true, name: { en: 'Sensor mode', de: 'Anwesenheitssensor' } },
|
|
87
|
+
led: { kanal: 'control', type: 'boolean', role: 'switch', w: true, name: { en: 'LED', de: 'Anzeige-LED' } },
|
|
88
|
+
quiet: { kanal: 'control', type: 'boolean', role: 'switch', w: true, name: { en: 'Quiet outdoor', de: 'Leiser Aussenbetrieb' } },
|
|
89
|
+
humidify: { kanal: 'control', type: 'string', role: 'level.mode.humidity', w: true, name: { en: 'Humidify', de: 'Befeuchtung' } },
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------- Messwerte
|
|
92
|
+
// Zwei Raumtemperaturen, die man nicht verwechseln darf:
|
|
93
|
+
// `temp` = der Wert, mit dem das Modul REGELT. Reihenfolge im Quellcode
|
|
94
|
+
// (revk_state_extra): env (externer Referenzsensor), sonst home (Sensor der
|
|
95
|
+
// Anlage), sonst inlet (Ansaugtemperatur). Ohne externen Sensor ist er
|
|
96
|
+
// deckungsgleich mit achome - sobald einer eingebunden ist, laufen sie
|
|
97
|
+
// auseinander.
|
|
98
|
+
// `achome` = IMMER der Sensor in der Klimaanlage selbst, ohne Ersatz.
|
|
99
|
+
temp: {
|
|
100
|
+
kanal: 'status', type: 'number', role: 'value.temperature', unit: '°C',
|
|
101
|
+
name: {
|
|
102
|
+
en: 'Room temperature (external sensor if present)',
|
|
103
|
+
de: 'Raumtemperatur (externer Sensor falls vorhanden)',
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
achome: {
|
|
107
|
+
kanal: 'status', type: 'number', role: 'value.temperature', unit: '°C',
|
|
108
|
+
name: {
|
|
109
|
+
en: "Room temperature (unit's own sensor)",
|
|
110
|
+
de: 'Raumtemperatur (Sensor in der Klimaanlage)',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
inlet: { kanal: 'status', type: 'number', role: 'value.temperature', unit: '°C', name: { en: 'Inlet temperature', de: 'Ansaugtemperatur' } },
|
|
114
|
+
outside: { kanal: 'status', type: 'number', role: 'value.temperature', unit: '°C', name: { en: 'Outside temperature', de: 'Aussentemperatur' } },
|
|
115
|
+
liquid: { kanal: 'status', type: 'number', role: 'value.temperature', unit: '°C', name: { en: 'Refrigerant temperature', de: 'Kältemitteltemperatur' } },
|
|
116
|
+
env: { kanal: 'status', type: 'number', role: 'value.temperature', unit: '°C', name: { en: 'External reference temperature', de: 'Externe Referenztemperatur' } },
|
|
117
|
+
hum: { kanal: 'status', type: 'number', role: 'value.humidity', unit: '%', name: { en: 'Indoor humidity', de: 'Luftfeuchte innen' } },
|
|
118
|
+
// Der Sollwert, wie ihn die Anlage selbst fuehrt. Deckt sich normalerweise mit `target`;
|
|
119
|
+
// im Faikout-Automatikbetrieb weicht er ab, weil dann ein Regelziel vorgegeben wird.
|
|
120
|
+
actarget: { kanal: 'status', type: 'number', role: 'value.temperature', unit: '°C', name: { en: 'Unit setpoint', de: 'Sollwert der Anlage' } },
|
|
121
|
+
comp: { kanal: 'status', type: 'number', role: 'value', unit: 'rpm', name: { en: 'Compressor speed', de: 'Verdichterdrehzahl' } },
|
|
122
|
+
fanfreq: { kanal: 'status', type: 'number', role: 'value', unit: 'rpm', name: { en: 'Fan speed', de: 'Lüfterdrehzahl' } },
|
|
123
|
+
fanrpm: { kanal: 'status', type: 'number', role: 'value', unit: 'rpm', name: { en: 'Fan speed', de: 'Lüfterdrehzahl' } },
|
|
124
|
+
anglev: { kanal: 'status', type: 'number', role: 'value', unit: '°', name: { en: 'Louvre angle', de: 'Lamellenwinkel' } },
|
|
125
|
+
consumption: { kanal: 'status', type: 'number', role: 'value.power', unit: 'W', name: { en: 'Power consumption', de: 'Momentanleistung' } },
|
|
126
|
+
// Das Geraet zaehlt in Wh; der Adapter rechnet in kWh um (siehe umrechnen()).
|
|
127
|
+
energy: { kanal: 'status', type: 'number', role: 'value.energy', unit: 'kWh', wh: true, name: { en: 'Lifetime energy', de: 'Energie gesamt' } },
|
|
128
|
+
energyheat: { kanal: 'status', type: 'number', role: 'value.energy', unit: 'kWh', wh: true, name: { en: 'Lifetime heating energy', de: 'Energie Heizen' } },
|
|
129
|
+
energycool: { kanal: 'status', type: 'number', role: 'value.energy', unit: 'kWh', wh: true, name: { en: 'Lifetime cooling energy', de: 'Energie Kühlen' } },
|
|
130
|
+
heat: { kanal: 'status', type: 'boolean', role: 'indicator', name: { en: 'Heating', de: 'Heizt' } },
|
|
131
|
+
slave: { kanal: 'status', type: 'boolean', role: 'indicator', name: { en: 'Controlled by other unit', de: 'Von anderer Einheit gesteuert' } },
|
|
132
|
+
antifreeze: { kanal: 'status', type: 'boolean', role: 'indicator', name: { en: 'Anti-freeze active', de: 'Frostschutz aktiv' } },
|
|
133
|
+
flap: { kanal: 'status', type: 'boolean', role: 'indicator', name: { en: 'Flap', de: 'Klappe' } },
|
|
134
|
+
control: { kanal: 'status', type: 'boolean', role: 'indicator', name: { en: 'Automatic control', de: 'Automatiksteuerung' } },
|
|
135
|
+
model: { kanal: 'status', type: 'string', role: 'info.name', name: { en: 'Model', de: 'Modell' } },
|
|
136
|
+
protocol: { kanal: 'status', type: 'string', role: 'info', name: { en: 'Protocol', de: 'Protokoll' } },
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------- Faikout-Automatik
|
|
139
|
+
// Eine eigene Schaltautomatik IM MODUL, unabhaengig von der Klimaanlage. `autoe` ist der
|
|
140
|
+
// Hauptschalter dafuer; er erlaubt zweierlei: Ein-/Ausschalten zu festen Uhrzeiten
|
|
141
|
+
// (auto1/auto0) und Ein-/Ausschalten nach Temperatur (autop mit autot ± autor).
|
|
142
|
+
//
|
|
143
|
+
// ACHTUNG, Bedeutungswechsel: Sind `autoe` UND `autor` gesetzt (autor > 0), regelt das
|
|
144
|
+
// Modul selbst. Dann meldet `target` nicht mehr den Sollwert der Anlage, sondern das
|
|
145
|
+
// Regelziel des Moduls (autot) - und ein Schreibbefehl auf command/<Name>/temp setzt
|
|
146
|
+
// ebenfalls autot statt des Anlagen-Sollwerts (siehe Faikout.c, Zeilen 1679 und 3197).
|
|
147
|
+
// Solange autor = 0 ist, verhaelt sich alles wie gewohnt.
|
|
148
|
+
autoe: {
|
|
149
|
+
kanal: 'status', type: 'boolean', role: 'switch', w: true,
|
|
150
|
+
name: {
|
|
151
|
+
en: 'Faikout automation (timed and temperature switching)',
|
|
152
|
+
de: 'Faikout-Automatik (Zeit- und Temperaturschaltung)',
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
autop: {
|
|
156
|
+
kanal: 'status', type: 'boolean', role: 'indicator',
|
|
157
|
+
name: {
|
|
158
|
+
en: 'Automation: switch by temperature',
|
|
159
|
+
de: 'Automatik: Schalten nach Temperatur',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
autor: {
|
|
163
|
+
kanal: 'status', type: 'number', role: 'value.temperature', unit: '°C',
|
|
164
|
+
name: {
|
|
165
|
+
en: 'Automation: tolerance around target (0 = module does not control)',
|
|
166
|
+
de: 'Automatik: Toleranz um das Ziel (0 = Modul regelt nicht)',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
autot: {
|
|
170
|
+
kanal: 'status', type: 'number', role: 'value.temperature', unit: '°C',
|
|
171
|
+
name: { en: 'Automation: target temperature', de: 'Automatik: Zieltemperatur' },
|
|
172
|
+
},
|
|
173
|
+
auto0: {
|
|
174
|
+
kanal: 'status', type: 'string', role: 'text',
|
|
175
|
+
name: { en: 'Automation: switch-off time (HHMM)', de: 'Automatik: Ausschaltzeit (HHMM)' },
|
|
176
|
+
},
|
|
177
|
+
auto1: {
|
|
178
|
+
kanal: 'status', type: 'string', role: 'text',
|
|
179
|
+
name: { en: 'Automation: switch-on time (HHMM)', de: 'Automatik: Einschaltzeit (HHMM)' },
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------- Geraetediagnose
|
|
183
|
+
online: { kanal: 'info', type: 'boolean', role: 'indicator.reachable', name: { en: 'Air conditioner online', de: 'Klimaanlage erreichbar' } },
|
|
184
|
+
ipv4: { kanal: 'info', type: 'string', role: 'info.ip', name: { en: 'IP address', de: 'IP-Adresse' } },
|
|
185
|
+
rssi: { kanal: 'info', type: 'number', role: 'value.rssi', unit: 'dBm', name: { en: 'WiFi signal', de: 'WLAN-Signal' } },
|
|
186
|
+
ssid: { kanal: 'info', type: 'string', role: 'info', name: { en: 'WiFi network', de: 'WLAN-Netz' } },
|
|
187
|
+
bssid: { kanal: 'info', type: 'string', role: 'info', name: { en: 'WiFi BSSID', de: 'WLAN-BSSID' } },
|
|
188
|
+
chan: { kanal: 'info', type: 'number', role: 'value', name: { en: 'WiFi channel', de: 'WLAN-Kanal' } },
|
|
189
|
+
uptime: { kanal: 'info', type: 'number', role: 'value', unit: 's', name: { en: 'Uptime', de: 'Laufzeit' } },
|
|
190
|
+
'mqtt-up': { kanal: 'info', type: 'number', role: 'value', unit: 's', id: 'mqttUp', name: { en: 'MQTT uptime', de: 'MQTT-Laufzeit' } },
|
|
191
|
+
version: { kanal: 'info', type: 'string', role: 'info.firmware', name: { en: 'Firmware version', de: 'Firmware-Version' } },
|
|
192
|
+
build: { kanal: 'info', type: 'string', role: 'info', name: { en: 'Firmware build', de: 'Firmware-Stand' } },
|
|
193
|
+
'build-suffix': { kanal: 'info', type: 'string', role: 'info', id: 'buildSuffix', name: { en: 'Hardware variant', de: 'Hardware-Variante' } },
|
|
194
|
+
app: { kanal: 'info', type: 'string', role: 'info', name: { en: 'Application', de: 'Anwendung' } },
|
|
195
|
+
id: { kanal: 'info', type: 'string', role: 'info.mac', name: { en: 'Device ID', de: 'Geräte-ID' } },
|
|
196
|
+
mem: { kanal: 'info', type: 'number', role: 'value', unit: 'B', name: { en: 'Free memory', de: 'Freier Speicher' } },
|
|
197
|
+
spi: { kanal: 'info', type: 'number', role: 'value', unit: 'B', name: { en: 'Free SPI RAM', de: 'Freier SPI-Speicher' } },
|
|
198
|
+
flash: { kanal: 'info', type: 'number', role: 'value', unit: 'B', name: { en: 'Flash size', de: 'Flash-Größe' } },
|
|
199
|
+
rst: { kanal: 'info', type: 'number', role: 'value', name: { en: 'Reset reason', de: 'Neustartgrund' } },
|
|
200
|
+
up: { kanal: 'info', type: 'boolean', role: 'indicator.reachable', name: { en: 'Module online', de: 'Modul erreichbar' } },
|
|
201
|
+
ts: { kanal: 'info', type: 'string', role: 'date', name: { en: 'Last message', de: 'Letzte Meldung' } },
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/** Felder, die der Adapter nie als Datenpunkt anlegt (Struktur statt Wert). */
|
|
205
|
+
const UEBERSPRINGEN = new Set(['ble']);
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Beschreibung eines Feldes - bekannt aus [FELDER], sonst aus dem Wert abgeleitet.
|
|
209
|
+
* Damit landet auch ein Feld im Baum, das eine kuenftige Firmware neu einfuehrt.
|
|
210
|
+
*/
|
|
211
|
+
function beschreibe(feld, wert) {
|
|
212
|
+
const bekannt = FELDER[feld];
|
|
213
|
+
if (bekannt) return bekannt;
|
|
214
|
+
const type = typeof wert === 'boolean' ? 'boolean' : typeof wert === 'number' ? 'number' : 'string';
|
|
215
|
+
return {
|
|
216
|
+
kanal: 'status',
|
|
217
|
+
type,
|
|
218
|
+
role: type === 'boolean' ? 'indicator' : type === 'number' ? 'value' : 'text',
|
|
219
|
+
name: { en: feld, de: feld },
|
|
220
|
+
unbekannt: true,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Wh -> kWh, wo die Beschreibung es verlangt. Alles andere unveraendert. */
|
|
225
|
+
function umrechnen(def, wert) {
|
|
226
|
+
if (def && def.wh && typeof wert === 'number') return Math.round(wert / 10) / 100;
|
|
227
|
+
return wert;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Platzhalterwert der Luftfeuchte.
|
|
232
|
+
*
|
|
233
|
+
* Anlagen ohne Feuchtesensor melden dauerhaft `hum: 50` - das ist kein Messwert, sondern der
|
|
234
|
+
* Ersatzwert. Ein blindes Uebernehmen taeuscht eine Messung vor, die es nicht gibt (am
|
|
235
|
+
* Wohnzimmer-Geraet des Nutzers genau so aufgetreten).
|
|
236
|
+
*/
|
|
237
|
+
const FEUCHTE_PLATZHALTER = 50;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Entscheidet, ob ein Feuchtewert echt ist.
|
|
241
|
+
*
|
|
242
|
+
* Solange nur 50 kommt, gilt er als Platzhalter und wird verworfen. Sobald das Geraet EINMAL
|
|
243
|
+
* etwas anderes gemeldet hat, ist ein Sensor da - ab dann zaehlt auch die 50 als gueltiger
|
|
244
|
+
* Messwert. Der Merker muss deshalb dauerhaft am Geraet haengen, nicht nur zur Laufzeit.
|
|
245
|
+
*
|
|
246
|
+
* @param {number} wert gemeldete Luftfeuchte
|
|
247
|
+
* @param {boolean} bekannt hat dieses Geraet frueher schon einen anderen Wert als 50 gemeldet?
|
|
248
|
+
* @returns {{nehmen: boolean, echtAbJetzt: boolean}}
|
|
249
|
+
*/
|
|
250
|
+
function feuchteBewerten(wert, bekannt) {
|
|
251
|
+
if (bekannt) return { nehmen: true, echtAbJetzt: true };
|
|
252
|
+
const echt = typeof wert === 'number' && wert !== FEUCHTE_PLATZHALTER;
|
|
253
|
+
return { nehmen: echt, echtAbJetzt: echt };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Objekt-ID eines Feldes; Bindestriche taugen nicht als ioBroker-ID-Bestandteil. */
|
|
257
|
+
function objektId(feld) {
|
|
258
|
+
const def = FELDER[feld];
|
|
259
|
+
if (def && def.id) return def.id;
|
|
260
|
+
return feld.replace(/-/g, '_');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = {
|
|
264
|
+
FELDER, MODES, FAN_MODES, UEBERSPRINGEN, FEUCHTE_PLATZHALTER,
|
|
265
|
+
beschreibe, umrechnen, objektId, feuchteBewerten,
|
|
266
|
+
};
|