iobroker.zigbee2mqtt 3.1.1 → 3.1.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/README.md +14 -0
- package/io-package.json +53 -27
- package/lib/check.js +6 -4
- package/lib/colors.js +9 -9
- package/lib/deviceController.js +92 -39
- package/lib/exposes.js +19 -7
- package/lib/imageController.js +56 -26
- package/lib/messages.js +15 -13
- package/lib/mqttServerController.js +14 -8
- package/lib/nonGenericDevicesExtension.js +2 -3
- package/lib/rgb.js +52 -63
- package/lib/states.js +138 -28
- package/lib/statesController.js +102 -44
- package/lib/utils.js +36 -47
- package/lib/websocketController.js +84 -52
- package/lib/z2mController.js +28 -16
- package/main.js +103 -33
- package/package.json +1 -1
package/lib/imageController.js
CHANGED
|
@@ -2,42 +2,49 @@ const axios = require('axios').default;
|
|
|
2
2
|
const sharp = require('sharp');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Lädt Geräte-Icons von zigbee2mqtt.io herunter, cacht sie im ioBroker-Dateisystem
|
|
6
|
+
* und skaliert sie auf die konfigurierte Größe.
|
|
6
7
|
*/
|
|
7
8
|
class ImageController {
|
|
8
9
|
/**
|
|
10
|
+
* Erstellt eine neue ImageController-Instanz.
|
|
9
11
|
*
|
|
10
|
-
* @param adapter
|
|
12
|
+
* @param {object} adapter Die ioBroker-Adapter-Instanz
|
|
11
13
|
*/
|
|
12
14
|
constructor(adapter) {
|
|
13
15
|
this.adapter = adapter;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
|
19
|
+
* Bereinigt eine Model-ID für die Verwendung in einer URL (ersetzt '/' durch '_').
|
|
17
20
|
*
|
|
18
|
-
* @param modelName
|
|
21
|
+
* @param {string} modelName Die zu bereinigende Model-ID
|
|
22
|
+
* @returns {string} Bereinigte Model-ID oder 'NA' wenn leer
|
|
19
23
|
*/
|
|
20
24
|
sanitizeModelIDForImageUrl(modelName) {
|
|
21
|
-
if (!modelName) {return 'NA';}
|
|
22
|
-
const modelNameString = modelName.replace('/', '_');
|
|
25
|
+
if (!modelName) { return 'NA'; }
|
|
23
26
|
// eslint-disable-next-line no-control-regex
|
|
24
|
-
return
|
|
27
|
+
return modelName.replace(/\//g, '_').replace(/\u0000/g, '');
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
/**
|
|
31
|
+
* Bereinigt einen Z2M-Gerätenamen für die Verwendung in einer URL.
|
|
28
32
|
*
|
|
29
|
-
* @param deviceName
|
|
33
|
+
* @param {string} deviceName Der zu bereinigende Gerätename
|
|
34
|
+
* @returns {string} Bereinigter Name oder 'NA' wenn leer
|
|
30
35
|
*/
|
|
31
36
|
sanitizeZ2MDeviceName(deviceName) {
|
|
32
|
-
if (!deviceName) {return 'NA';}
|
|
33
|
-
const deviceNameString = deviceName.replace(/:|\s|\//g, '-');
|
|
37
|
+
if (!deviceName) { return 'NA'; }
|
|
34
38
|
// eslint-disable-next-line no-control-regex
|
|
35
|
-
return
|
|
39
|
+
return deviceName.replace(/:|\s|\//g, '-').replace(/\u0000/g, '');
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
43
|
+
* Erstellt die zigbee2mqtt.io-Bild-URL für ein Gerät.
|
|
44
|
+
*
|
|
45
|
+
* @param {object} device Das Geräteobjekt aus dem Z2M-Payload
|
|
46
|
+
* @param {string} ext Dateiendung ('jpg' oder 'png')
|
|
47
|
+
* @returns {string|undefined} Die Bild-URL oder undefined wenn kein Modell vorhanden
|
|
41
48
|
*/
|
|
42
49
|
_buildZ2mImageUrl(device, ext) {
|
|
43
50
|
if (device && device.definition && device.definition.model) {
|
|
@@ -49,14 +56,20 @@ class ImageController {
|
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
/**
|
|
52
|
-
*
|
|
59
|
+
* Gibt die JPG-Bild-URL des Geräts auf zigbee2mqtt.io zurück.
|
|
60
|
+
*
|
|
61
|
+
* @param {object} device Das Geräteobjekt
|
|
62
|
+
* @returns {string|undefined} Die JPG-URL oder undefined
|
|
53
63
|
*/
|
|
54
64
|
getZ2mDeviceImageModelJPG(device) {
|
|
55
65
|
return this._buildZ2mImageUrl(device, 'jpg');
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
/**
|
|
59
|
-
*
|
|
69
|
+
* Gibt die PNG-Bild-URL des Geräts auf zigbee2mqtt.io zurück.
|
|
70
|
+
*
|
|
71
|
+
* @param {object} device Das Geräteobjekt
|
|
72
|
+
* @returns {string|undefined} Die PNG-URL oder undefined
|
|
60
73
|
*/
|
|
61
74
|
getZ2mDeviceImageModelPNG(device) {
|
|
62
75
|
return this._buildZ2mImageUrl(device, 'png');
|
|
@@ -64,8 +77,10 @@ class ImageController {
|
|
|
64
77
|
|
|
65
78
|
|
|
66
79
|
/**
|
|
80
|
+
* Gibt die SLS-Bild-URL des Geräts (alternative Quelle) zurück.
|
|
67
81
|
*
|
|
68
|
-
* @param device
|
|
82
|
+
* @param {object} device Das Geräteobjekt mit model_id-Feld
|
|
83
|
+
* @returns {string|undefined} Die Bild-URL oder undefined
|
|
69
84
|
*/
|
|
70
85
|
getSlsDeviceImage(device) {
|
|
71
86
|
if (device && device.model_id) {
|
|
@@ -73,11 +88,15 @@ class ImageController {
|
|
|
73
88
|
// eslint-disable-next-line no-control-regex
|
|
74
89
|
return icoString.replace(/\u0000/g, '');
|
|
75
90
|
}
|
|
91
|
+
return undefined;
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
/**
|
|
95
|
+
* Lädt das Icon eines Geräts herunter (falls noch nicht gecacht), skaliert es
|
|
96
|
+
* auf die konfigurierte Größe und gibt es als Base64-Data-URL zurück.
|
|
79
97
|
*
|
|
80
|
-
* @param device
|
|
98
|
+
* @param {object} device Das Geräteobjekt aus dem Z2M-bridge/devices-Payload
|
|
99
|
+
* @returns {Promise<string>} Base64-kodiertes Icon als Data-URL oder ''
|
|
81
100
|
*/
|
|
82
101
|
async getDeviceIcon(device) {
|
|
83
102
|
if (!this.adapter.config.useDeviceIcons) {
|
|
@@ -111,7 +130,7 @@ class ImageController {
|
|
|
111
130
|
break;
|
|
112
131
|
}
|
|
113
132
|
} catch (ex) {
|
|
114
|
-
|
|
133
|
+
this.adapter.log.debug(`Icon download failed for ${iconUrl}: ${ex && ex.message ? ex.message : ex}`);
|
|
115
134
|
}
|
|
116
135
|
}
|
|
117
136
|
}
|
|
@@ -122,6 +141,10 @@ class ImageController {
|
|
|
122
141
|
}
|
|
123
142
|
|
|
124
143
|
const icon = await this.adapter.readFileAsync(this.adapter.namespace, iconFileName);
|
|
144
|
+
if (!icon || !icon.file) {
|
|
145
|
+
this.adapter.log.warn(`Failed to read icon file: ${iconFileName}`);
|
|
146
|
+
return '';
|
|
147
|
+
}
|
|
125
148
|
const origIconMeta = await sharp(icon.file).metadata();
|
|
126
149
|
if (
|
|
127
150
|
(origIconMeta.height && origIconMeta.height > imageSize) ||
|
|
@@ -143,9 +166,12 @@ class ImageController {
|
|
|
143
166
|
|
|
144
167
|
return `data:image/png;base64,${icon.file.toString('base64')}`;
|
|
145
168
|
}
|
|
169
|
+
|
|
146
170
|
/**
|
|
171
|
+
* Extrahiert den Dateinamen (mit Erweiterung) aus einer URL.
|
|
147
172
|
*
|
|
148
|
-
* @param url
|
|
173
|
+
* @param {string} url Die vollständige URL
|
|
174
|
+
* @returns {string} Dateiname (z.B. "TRADFRI_bulb.jpg")
|
|
149
175
|
*/
|
|
150
176
|
getFileNameWithExtension(url) {
|
|
151
177
|
const path = new URL(url).pathname;
|
|
@@ -155,26 +181,30 @@ class ImageController {
|
|
|
155
181
|
}
|
|
156
182
|
|
|
157
183
|
/**
|
|
184
|
+
* Lädt ein Icon von der angegebenen URL herunter und speichert es im ioBroker-Dateisystem.
|
|
158
185
|
*
|
|
159
|
-
* @param adapter
|
|
160
|
-
* @param url
|
|
161
|
-
* @param namespace
|
|
186
|
+
* @param {object} adapter Die ioBroker-Adapter-Instanz
|
|
187
|
+
* @param {string} url Download-URL des Icons
|
|
188
|
+
* @param {string} namespace ioBroker-Namespace (z.B. "zigbee2mqtt.0")
|
|
189
|
+
* @returns {Promise<boolean>} true bei Erfolg, false bei Fehler
|
|
162
190
|
*/
|
|
163
191
|
async downloadIcon(adapter, url, namespace) {
|
|
164
192
|
try {
|
|
165
|
-
const res = await axios.get(url, { responseType: 'arraybuffer' });
|
|
193
|
+
const res = await axios.get(url, { responseType: 'arraybuffer', timeout: 10000 });
|
|
166
194
|
await adapter.writeFileAsync(namespace, this.getFileNameWithExtension(url), res.data);
|
|
167
195
|
return true;
|
|
168
196
|
} catch (ex) {
|
|
169
|
-
//adapter.log.warn(ex);
|
|
170
197
|
return false;
|
|
171
198
|
}
|
|
172
199
|
}
|
|
200
|
+
|
|
173
201
|
/**
|
|
202
|
+
* Prüft welcher lokale Icon-Dateiname bereits im ioBroker-Dateisystem existiert.
|
|
174
203
|
*
|
|
175
|
-
* @param z2mIconFileNameJPG
|
|
176
|
-
* @param z2mIconFileNamePNG
|
|
177
|
-
* @param slsIconFileName
|
|
204
|
+
* @param {string} z2mIconFileNameJPG JPG-Dateiname (Z2M-Format)
|
|
205
|
+
* @param {string} z2mIconFileNamePNG PNG-Dateiname (Z2M-Format)
|
|
206
|
+
* @param {string} slsIconFileName PNG-Dateiname (SLS-Format)
|
|
207
|
+
* @returns {Promise<string|null>} Erster gefundener Dateiname oder null
|
|
178
208
|
*/
|
|
179
209
|
async getExistingIconFileName(z2mIconFileNameJPG, z2mIconFileNamePNG, slsIconFileName) {
|
|
180
210
|
if (await this.adapter.fileExistsAsync(this.adapter.namespace, z2mIconFileNameJPG)) {
|
package/lib/messages.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* Gibt die aktuelle Adapter-Konfiguration formatiert als Log-Block aus.
|
|
2
3
|
*
|
|
3
|
-
* @param config
|
|
4
|
-
* @param log
|
|
4
|
+
* @param {object} config Die Adapter-Konfiguration (this.config)
|
|
5
|
+
* @param {object} log Das ioBroker-Log-Objekt (this.log)
|
|
5
6
|
*/
|
|
6
|
-
|
|
7
|
+
function adapterInfo(config, log) {
|
|
7
8
|
if (!config || !log) {
|
|
8
9
|
return;
|
|
9
10
|
}
|
|
@@ -12,23 +13,23 @@ async function adapterInfo(config, log) {
|
|
|
12
13
|
log.info(`|| Zigbee2MQTT Frontend Server: ${config.webUIServer}`);
|
|
13
14
|
log.info(`|| Zigbee2MQTT Frontend Port: ${config.webUIPort}`);
|
|
14
15
|
log.info(`|| Zigbee2MQTT Connection Type: ${config.connectionType}`);
|
|
15
|
-
if (config.connectionType
|
|
16
|
+
if (config.connectionType === 'ws') {
|
|
16
17
|
log.info(`|| Zigbee2MQTT Websocket Scheme: ${config.wsScheme}`);
|
|
17
18
|
log.info(`|| Zigbee2MQTT Websocket Server: ${config.wsServerIP}`);
|
|
18
19
|
log.info(`|| Zigbee2MQTT Websocket Port: ${config.wsServerPort}`);
|
|
19
20
|
log.info(`|| Zigbee2MQTT Websocket Auth-Token: ${config.wsTokenEnabled ? 'use' : 'unused'}`);
|
|
20
21
|
log.info(`|| Zigbee2MQTT Websocket Dummy MQTT-Server: ${config.dummyMqtt ? 'activated' : 'deactivated'}`);
|
|
21
|
-
if (config.dummyMqtt
|
|
22
|
+
if (config.dummyMqtt === true) {
|
|
22
23
|
log.info(`|| Zigbee2MQTT Dummy MQTT IP-Bind: ${config.mqttServerIPBind}`);
|
|
23
24
|
log.info(`|| Zigbee2MQTT Dummy MQTT Port: ${config.mqttServerPort}`);
|
|
24
25
|
}
|
|
25
|
-
} else if (config.connectionType
|
|
26
|
-
log.info(`|| Zigbee2MQTT
|
|
27
|
-
log.info(`|| Zigbee2MQTT
|
|
26
|
+
} else if (config.connectionType === 'exmqtt') {
|
|
27
|
+
log.info(`|| Zigbee2MQTT External MQTT Server: ${config.externalMqttServerIP}`);
|
|
28
|
+
log.info(`|| Zigbee2MQTT External MQTT Port: ${config.externalMqttServerPort}`);
|
|
28
29
|
log.info(
|
|
29
|
-
`|| Zigbee2MQTT
|
|
30
|
+
`|| Zigbee2MQTT External MQTT Credentials: ${config.externalMqttServerCredentials ? 'use' : 'unused'}`
|
|
30
31
|
);
|
|
31
|
-
} else if (config.connectionType
|
|
32
|
+
} else if (config.connectionType === 'intmqtt') {
|
|
32
33
|
log.info(`|| Zigbee2MQTT Internal MQTT IP-Bind: ${config.mqttServerIPBind}`);
|
|
33
34
|
log.info(`|| Zigbee2MQTT Internal MQTT Port: ${config.mqttServerPort}`);
|
|
34
35
|
}
|
|
@@ -50,11 +51,12 @@ async function adapterInfo(config, log) {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
/**
|
|
54
|
+
* Gibt die Zigbee2MQTT-Bridge-Informationen formatiert als Log-Block aus.
|
|
53
55
|
*
|
|
54
|
-
* @param payload
|
|
55
|
-
* @param log
|
|
56
|
+
* @param {object} payload Der Payload des bridge/info-Topics
|
|
57
|
+
* @param {object} log Das ioBroker-Log-Objekt (this.log)
|
|
56
58
|
*/
|
|
57
|
-
|
|
59
|
+
function zigbee2mqttInfo(payload, log) {
|
|
58
60
|
if (!payload || !log) {
|
|
59
61
|
return;
|
|
60
62
|
}
|
|
@@ -5,11 +5,14 @@ const { Aedes } = require('aedes');
|
|
|
5
5
|
const net = require('node:net');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Verwaltet den internen oder Dummy-MQTT-Server (Aedes) für den Zigbee2MQTT-Adapter.
|
|
9
|
+
* Wird im intmqtt- und ws-Modus (dummyMqtt) verwendet.
|
|
9
10
|
*/
|
|
10
11
|
class MqttServerController {
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
+
* Erstellt eine neue MqttServerController-Instanz.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} adapter Die ioBroker-Adapter-Instanz
|
|
13
16
|
*/
|
|
14
17
|
constructor(adapter) {
|
|
15
18
|
this.adapter = adapter;
|
|
@@ -20,7 +23,7 @@ class MqttServerController {
|
|
|
20
23
|
/**
|
|
21
24
|
* Prüft ob Port und Bind-Adresse konfiguriert sind.
|
|
22
25
|
*
|
|
23
|
-
* @returns {boolean}
|
|
26
|
+
* @returns {boolean} true wenn die Konfiguration vollständig ist, sonst false
|
|
24
27
|
*/
|
|
25
28
|
_checkConfig() {
|
|
26
29
|
if (!this.adapter.config.mqttServerPort || !this.adapter.config.mqttServerIPBind) {
|
|
@@ -31,9 +34,9 @@ class MqttServerController {
|
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/**
|
|
34
|
-
* Hängt error- und close-
|
|
37
|
+
* Hängt error- und close-Event-Handler an den internen net.Server.
|
|
35
38
|
*
|
|
36
|
-
* @param {string} label
|
|
39
|
+
* @param {string} label Bezeichnung des Servers für Log-Ausgaben (z.B. "MQTT-Server")
|
|
37
40
|
*/
|
|
38
41
|
_attachServerEvents(label) {
|
|
39
42
|
if (!this.mqttServer) {
|
|
@@ -48,7 +51,8 @@ class MqttServerController {
|
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
/**
|
|
51
|
-
* Startet den internen MQTT-Server mit Persistenz (aedes + nedb).
|
|
54
|
+
* Startet den internen MQTT-Server mit nedb-Persistenz (aedes + aedes-persistence-nedb).
|
|
55
|
+
* Wartet auf den erfolgreichen listen()-Aufruf bevor die Methode zurückkehrt.
|
|
52
56
|
*/
|
|
53
57
|
async createMQTTServer() {
|
|
54
58
|
if (!this._checkConfig()) {
|
|
@@ -90,7 +94,8 @@ class MqttServerController {
|
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
/**
|
|
93
|
-
* Startet einen Dummy-MQTT-Server ohne Persistenz
|
|
97
|
+
* Startet einen Dummy-MQTT-Server ohne Persistenz.
|
|
98
|
+
* Wird im WebSocket-Modus (dummyMqtt=true) benötigt damit Z2M einen MQTT-Broker findet.
|
|
94
99
|
*/
|
|
95
100
|
async createDummyMQTTServer() {
|
|
96
101
|
if (!this._checkConfig()) {
|
|
@@ -121,7 +126,8 @@ class MqttServerController {
|
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
/**
|
|
124
|
-
* Schließt den MQTT-Server und den Aedes-Broker.
|
|
129
|
+
* Schließt den MQTT-Server und den Aedes-Broker sicher.
|
|
130
|
+
* Wird beim Adapter-Stop oder nach einem Fehler beim Start aufgerufen.
|
|
125
131
|
*/
|
|
126
132
|
closeServer() {
|
|
127
133
|
if (this.mqttServer) {
|
package/lib/rgb.js
CHANGED
|
@@ -115,10 +115,15 @@ function cie_to_rgb(x, y, brightness) {
|
|
|
115
115
|
* @returns {Array} Array that contains the CIE color values for x and y
|
|
116
116
|
*/
|
|
117
117
|
function rgb_to_cie(red, green, blue) {
|
|
118
|
+
// Normalize RGB values from 0-255 to 0-1 before gamma correction
|
|
119
|
+
red = red / 255;
|
|
120
|
+
green = green / 255;
|
|
121
|
+
blue = blue / 255;
|
|
122
|
+
|
|
118
123
|
// Apply a gamma correction to the RGB values, which makes the color more vivid and more the like the color displayed on the screen of your device
|
|
119
|
-
red
|
|
124
|
+
red = red > 0.04045 ? Math.pow((red + 0.055) / (1.0 + 0.055), 2.4) : red / 12.92;
|
|
120
125
|
green = green > 0.04045 ? Math.pow((green + 0.055) / (1.0 + 0.055), 2.4) : green / 12.92;
|
|
121
|
-
blue
|
|
126
|
+
blue = blue > 0.04045 ? Math.pow((blue + 0.055) / (1.0 + 0.055), 2.4) : blue / 12.92;
|
|
122
127
|
|
|
123
128
|
// RGB values to XYZ using the Wide RGB D65 conversion formula
|
|
124
129
|
const X = red * 0.664511 + green * 0.154324 + blue * 0.162028;
|
|
@@ -155,7 +160,9 @@ function hsvToRGB(h, s, v) {
|
|
|
155
160
|
let g;
|
|
156
161
|
let b;
|
|
157
162
|
if (arguments.length === 1) {
|
|
158
|
-
|
|
163
|
+
s = h.s;
|
|
164
|
+
v = h.v;
|
|
165
|
+
h = h.h;
|
|
159
166
|
}
|
|
160
167
|
const i = Math.floor(h * 6);
|
|
161
168
|
const f = h * 6 - i;
|
|
@@ -163,24 +170,13 @@ function hsvToRGB(h, s, v) {
|
|
|
163
170
|
const q = v * (1 - f * s);
|
|
164
171
|
const t = v * (1 - (1 - f) * s);
|
|
165
172
|
switch (i % 6) {
|
|
166
|
-
case 0:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
case
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
(r = p), (g = v), (b = t);
|
|
174
|
-
break;
|
|
175
|
-
case 3:
|
|
176
|
-
(r = p), (g = q), (b = v);
|
|
177
|
-
break;
|
|
178
|
-
case 4:
|
|
179
|
-
(r = t), (g = p), (b = v);
|
|
180
|
-
break;
|
|
181
|
-
case 5:
|
|
182
|
-
(r = v), (g = p), (b = q);
|
|
183
|
-
break;
|
|
173
|
+
case 0: r = v; g = t; b = p; break;
|
|
174
|
+
case 1: r = q; g = v; b = p; break;
|
|
175
|
+
case 2: r = p; g = v; b = t; break;
|
|
176
|
+
case 3: r = p; g = q; b = v; break;
|
|
177
|
+
case 4: r = t; g = p; b = v; break;
|
|
178
|
+
case 5: r = v; g = p; b = q; break;
|
|
179
|
+
default: r = 0; g = 0; b = 0; break;
|
|
184
180
|
}
|
|
185
181
|
return {
|
|
186
182
|
r: Math.round(r * 255),
|
|
@@ -198,7 +194,9 @@ function hsvToRGB(h, s, v) {
|
|
|
198
194
|
*/
|
|
199
195
|
function rgbToHSV(r, g, b, numeric) {
|
|
200
196
|
if (arguments.length === 1) {
|
|
201
|
-
|
|
197
|
+
g = r.g;
|
|
198
|
+
b = r.b;
|
|
199
|
+
r = r.r;
|
|
202
200
|
}
|
|
203
201
|
const max = Math.max(r, g, b);
|
|
204
202
|
const min = Math.min(r, g, b);
|
|
@@ -225,20 +223,18 @@ function rgbToHSV(r, g, b, numeric) {
|
|
|
225
223
|
break;
|
|
226
224
|
}
|
|
227
225
|
if (numeric) {
|
|
228
|
-
|
|
229
|
-
|
|
226
|
+
return {
|
|
230
227
|
h: Math.round(h * 360),
|
|
231
228
|
s: Math.round(s * 100),
|
|
232
229
|
v: Math.round(v * 100),
|
|
233
230
|
};
|
|
234
231
|
}
|
|
235
|
-
|
|
236
|
-
|
|
232
|
+
return {
|
|
237
233
|
h: (h * 360).toFixed(3),
|
|
238
234
|
s: (s * 100).toFixed(3),
|
|
239
235
|
v: (v * 100).toFixed(3),
|
|
240
|
-
|
|
241
|
-
|
|
236
|
+
};
|
|
237
|
+
}
|
|
242
238
|
|
|
243
239
|
/**
|
|
244
240
|
*
|
|
@@ -281,20 +277,20 @@ function hsv_to_cie(h, s, v) {
|
|
|
281
277
|
function rgb_to_rgbstring(element) {
|
|
282
278
|
let col = '#';
|
|
283
279
|
if (element && element.hasOwnProperty('r')) {
|
|
284
|
-
col
|
|
285
|
-
} else {
|
|
286
|
-
col
|
|
287
|
-
}
|
|
280
|
+
col += element.r.toString(16).padStart(2, '0');
|
|
281
|
+
} else {
|
|
282
|
+
col += '00';
|
|
283
|
+
}
|
|
288
284
|
if (element && element.hasOwnProperty('g')) {
|
|
289
|
-
col
|
|
290
|
-
} else {
|
|
291
|
-
col
|
|
292
|
-
}
|
|
285
|
+
col += element.g.toString(16).padStart(2, '0');
|
|
286
|
+
} else {
|
|
287
|
+
col += '00';
|
|
288
|
+
}
|
|
293
289
|
if (element && element.hasOwnProperty('b')) {
|
|
294
|
-
col
|
|
295
|
-
} else {
|
|
296
|
-
col
|
|
297
|
-
}
|
|
290
|
+
col += element.b.toString(16).padStart(2, '0');
|
|
291
|
+
} else {
|
|
292
|
+
col += '00';
|
|
293
|
+
}
|
|
298
294
|
return col;
|
|
299
295
|
}
|
|
300
296
|
|
|
@@ -306,30 +302,23 @@ col = `${col }00`;
|
|
|
306
302
|
*/
|
|
307
303
|
function hsbToRGB(h, s, b) {
|
|
308
304
|
const br = Math.round(b * 2.55);
|
|
309
|
-
if (s
|
|
305
|
+
if (s === 0) {
|
|
310
306
|
return [br, br, br];
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
case 4:
|
|
327
|
-
return [t, p, br];
|
|
328
|
-
case 5:
|
|
329
|
-
return [br, p, q];
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return false;
|
|
307
|
+
}
|
|
308
|
+
const hue = h % 360;
|
|
309
|
+
const f = hue % 60;
|
|
310
|
+
const p = Math.round(b * (100 - s) * 0.0255);
|
|
311
|
+
const q = Math.round(b * (6000 - s * f) * 0.000425);
|
|
312
|
+
const t = Math.round(b * (6000 - s * (60 - f)) * 0.000425);
|
|
313
|
+
switch (Math.floor(hue / 60)) {
|
|
314
|
+
case 0: return [br, t, p];
|
|
315
|
+
case 1: return [q, br, p];
|
|
316
|
+
case 2: return [p, br, t];
|
|
317
|
+
case 3: return [p, q, br];
|
|
318
|
+
case 4: return [t, p, br];
|
|
319
|
+
case 5: return [br, p, q];
|
|
320
|
+
}
|
|
321
|
+
return [br, br, br]; // fallback – sollte nie erreicht werden
|
|
333
322
|
}
|
|
334
323
|
|
|
335
324
|
/**
|