iobroker.poolcontrol 1.3.14 → 1.3.17
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 +76 -100
- package/io-package.json +40 -40
- package/lib/helpers/chemistryOrpHelper.js +805 -0
- package/lib/helpers/chemistryPhHelper.js +274 -21
- package/lib/helpers/pumpHelper2.js +21 -11
- package/lib/helpers/speechTextHelper.js +83 -8
- package/lib/i18n/de.json +35 -1
- package/lib/i18n/en.json +35 -1
- package/lib/i18n/es.json +31 -1
- package/lib/i18n/fr.json +31 -1
- package/lib/i18n/it.json +31 -1
- package/lib/i18n/nl.json +31 -1
- package/lib/i18n/pl.json +31 -1
- package/lib/i18n/pt.json +31 -1
- package/lib/i18n/ru.json +31 -1
- package/lib/i18n/uk.json +31 -1
- package/lib/i18n/zh-cn.json +31 -1
- package/lib/stateDefinitions/chemistryOrpStates.js +1068 -0
- package/lib/stateDefinitions/chemistryPhStates.js +214 -0
- package/lib/stateDefinitions/speechStates.js +151 -0
- package/main.js +14 -0
- package/package.json +1 -1
|
@@ -651,6 +651,220 @@ async function createChemistryPhStates(adapter) {
|
|
|
651
651
|
persist: true,
|
|
652
652
|
});
|
|
653
653
|
|
|
654
|
+
// -------------------------------------------------------------
|
|
655
|
+
// History
|
|
656
|
+
// -------------------------------------------------------------
|
|
657
|
+
await createChannel(adapter, 'chemistry.ph.history', {
|
|
658
|
+
en: 'History',
|
|
659
|
+
de: 'Historie',
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
await createState(adapter, 'chemistry.ph.history.samples_json', {
|
|
663
|
+
name: {
|
|
664
|
+
en: 'Samples JSON',
|
|
665
|
+
de: 'Messwerte JSON',
|
|
666
|
+
},
|
|
667
|
+
desc: {
|
|
668
|
+
en: 'Internal JSON list of valid pH samples for up to 30 days.',
|
|
669
|
+
de: 'Interne JSON-Liste gültiger pH-Messpunkte für bis zu 30 Tage.',
|
|
670
|
+
},
|
|
671
|
+
type: 'string',
|
|
672
|
+
role: 'json',
|
|
673
|
+
read: true,
|
|
674
|
+
write: false,
|
|
675
|
+
def: '[]',
|
|
676
|
+
persist: true,
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
await createState(adapter, 'chemistry.ph.history.samples_count', {
|
|
680
|
+
name: {
|
|
681
|
+
en: 'Samples count',
|
|
682
|
+
de: 'Anzahl Messwerte',
|
|
683
|
+
},
|
|
684
|
+
desc: {
|
|
685
|
+
en: 'Number of stored valid pH samples.',
|
|
686
|
+
de: 'Anzahl gespeicherter gültiger pH-Messpunkte.',
|
|
687
|
+
},
|
|
688
|
+
type: 'number',
|
|
689
|
+
role: 'value',
|
|
690
|
+
read: true,
|
|
691
|
+
write: false,
|
|
692
|
+
def: 0,
|
|
693
|
+
persist: true,
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
await createState(adapter, 'chemistry.ph.history.oldest_sample_at', {
|
|
697
|
+
name: {
|
|
698
|
+
en: 'Oldest sample time',
|
|
699
|
+
de: 'Ältester Messwert',
|
|
700
|
+
},
|
|
701
|
+
type: 'string',
|
|
702
|
+
role: 'value.time',
|
|
703
|
+
read: true,
|
|
704
|
+
write: false,
|
|
705
|
+
def: '',
|
|
706
|
+
persist: true,
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
await createState(adapter, 'chemistry.ph.history.newest_sample_at', {
|
|
710
|
+
name: {
|
|
711
|
+
en: 'Newest sample time',
|
|
712
|
+
de: 'Neuester Messwert',
|
|
713
|
+
},
|
|
714
|
+
type: 'string',
|
|
715
|
+
role: 'value.time',
|
|
716
|
+
read: true,
|
|
717
|
+
write: false,
|
|
718
|
+
def: '',
|
|
719
|
+
persist: true,
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// -------------------------------------------------------------
|
|
723
|
+
// Trend
|
|
724
|
+
// -------------------------------------------------------------
|
|
725
|
+
await createChannel(adapter, 'chemistry.ph.trend', {
|
|
726
|
+
en: 'Trend',
|
|
727
|
+
de: 'Trend',
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
const phTrendStates = [
|
|
731
|
+
['reference_24h_value', '24h reference value', '24h-Referenzwert'],
|
|
732
|
+
['reference_7d_value', '7 day reference value', '7-Tage-Referenzwert'],
|
|
733
|
+
['reference_30d_value', '30 day reference value', '30-Tage-Referenzwert'],
|
|
734
|
+
['delta_24h', '24h delta', '24h-Differenz'],
|
|
735
|
+
['delta_7d', '7 day delta', '7-Tage-Differenz'],
|
|
736
|
+
['delta_30d', '30 day delta', '30-Tage-Differenz'],
|
|
737
|
+
];
|
|
738
|
+
|
|
739
|
+
for (const [stateId, enName, deName] of phTrendStates) {
|
|
740
|
+
await createState(adapter, `chemistry.ph.trend.${stateId}`, {
|
|
741
|
+
name: {
|
|
742
|
+
en: enName,
|
|
743
|
+
de: deName,
|
|
744
|
+
},
|
|
745
|
+
type: 'number',
|
|
746
|
+
role: 'value',
|
|
747
|
+
read: true,
|
|
748
|
+
write: false,
|
|
749
|
+
def: 0,
|
|
750
|
+
persist: true,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const phTrendTimeStates = [
|
|
755
|
+
['reference_24h_at', '24h reference time', 'Zeitpunkt des 24h-Referenzwerts'],
|
|
756
|
+
['reference_7d_at', '7 day reference time', 'Zeitpunkt des 7-Tage-Referenzwerts'],
|
|
757
|
+
['reference_30d_at', '30 day reference time', 'Zeitpunkt des 30-Tage-Referenzwerts'],
|
|
758
|
+
];
|
|
759
|
+
|
|
760
|
+
for (const [stateId, enName, deName] of phTrendTimeStates) {
|
|
761
|
+
await createState(adapter, `chemistry.ph.trend.${stateId}`, {
|
|
762
|
+
name: {
|
|
763
|
+
en: enName,
|
|
764
|
+
de: deName,
|
|
765
|
+
},
|
|
766
|
+
type: 'string',
|
|
767
|
+
role: 'value.time',
|
|
768
|
+
read: true,
|
|
769
|
+
write: false,
|
|
770
|
+
def: '',
|
|
771
|
+
persist: true,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
await createState(adapter, 'chemistry.ph.trend.direction', {
|
|
776
|
+
name: {
|
|
777
|
+
en: 'Trend direction',
|
|
778
|
+
de: 'Trendrichtung',
|
|
779
|
+
},
|
|
780
|
+
desc: {
|
|
781
|
+
en: 'Overall trend direction: stable, rising, falling or not enough data.',
|
|
782
|
+
de: 'Gesamte Trendrichtung: stabil, steigend, fallend oder noch nicht genug Daten.',
|
|
783
|
+
},
|
|
784
|
+
type: 'string',
|
|
785
|
+
role: 'text',
|
|
786
|
+
read: true,
|
|
787
|
+
write: false,
|
|
788
|
+
def: 'not_enough_data',
|
|
789
|
+
persist: true,
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
await createState(adapter, 'chemistry.ph.trend.status', {
|
|
793
|
+
name: {
|
|
794
|
+
en: 'Trend status',
|
|
795
|
+
de: 'Trendstatus',
|
|
796
|
+
},
|
|
797
|
+
desc: {
|
|
798
|
+
en: 'Readable status of the pH trend development.',
|
|
799
|
+
de: 'Lesbarer Status der pH-Trendentwicklung.',
|
|
800
|
+
},
|
|
801
|
+
type: 'string',
|
|
802
|
+
role: 'text',
|
|
803
|
+
read: true,
|
|
804
|
+
write: false,
|
|
805
|
+
def: 'not_enough_data',
|
|
806
|
+
persist: true,
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
// -------------------------------------------------------------
|
|
810
|
+
// Outputs
|
|
811
|
+
// -------------------------------------------------------------
|
|
812
|
+
await createChannel(adapter, 'chemistry.ph.outputs', {
|
|
813
|
+
en: 'Outputs',
|
|
814
|
+
de: 'Ausgaben',
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
await createState(adapter, 'chemistry.ph.outputs.summary_text', {
|
|
818
|
+
name: {
|
|
819
|
+
en: 'Summary text',
|
|
820
|
+
de: 'Zusammenfassung Text',
|
|
821
|
+
},
|
|
822
|
+
desc: {
|
|
823
|
+
en: 'Readable pH summary text.',
|
|
824
|
+
de: 'Lesbare pH-Zusammenfassung.',
|
|
825
|
+
},
|
|
826
|
+
type: 'string',
|
|
827
|
+
role: 'text',
|
|
828
|
+
read: true,
|
|
829
|
+
write: false,
|
|
830
|
+
def: '',
|
|
831
|
+
persist: true,
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
await createState(adapter, 'chemistry.ph.outputs.summary_html', {
|
|
835
|
+
name: {
|
|
836
|
+
en: 'Summary HTML',
|
|
837
|
+
de: 'Zusammenfassung HTML',
|
|
838
|
+
},
|
|
839
|
+
desc: {
|
|
840
|
+
en: 'HTML summary for VIS or widgets.',
|
|
841
|
+
de: 'HTML-Zusammenfassung für VIS oder Widgets.',
|
|
842
|
+
},
|
|
843
|
+
type: 'string',
|
|
844
|
+
role: 'html',
|
|
845
|
+
read: true,
|
|
846
|
+
write: false,
|
|
847
|
+
def: '',
|
|
848
|
+
persist: true,
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
await createState(adapter, 'chemistry.ph.outputs.summary_json', {
|
|
852
|
+
name: {
|
|
853
|
+
en: 'Summary JSON',
|
|
854
|
+
de: 'Zusammenfassung JSON',
|
|
855
|
+
},
|
|
856
|
+
desc: {
|
|
857
|
+
en: 'Structured pH summary as JSON.',
|
|
858
|
+
de: 'Strukturierte pH-Zusammenfassung als JSON.',
|
|
859
|
+
},
|
|
860
|
+
type: 'string',
|
|
861
|
+
role: 'json',
|
|
862
|
+
read: true,
|
|
863
|
+
write: false,
|
|
864
|
+
def: '',
|
|
865
|
+
persist: true,
|
|
866
|
+
});
|
|
867
|
+
|
|
654
868
|
// -------------------------------------------------------------
|
|
655
869
|
// Debug
|
|
656
870
|
// -------------------------------------------------------------
|
|
@@ -285,6 +285,157 @@ async function createSpeechStates(adapter) {
|
|
|
285
285
|
|
|
286
286
|
adapter.log.debug('[speechStates] Amazon Alexa quiet-time states checked and created.');
|
|
287
287
|
|
|
288
|
+
// ------------------------------------------------------------------
|
|
289
|
+
// FIX: Speech sources - user configurable message sources
|
|
290
|
+
// ------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
await adapter.setObjectNotExistsAsync('speech.sources', {
|
|
293
|
+
type: 'channel',
|
|
294
|
+
common: {
|
|
295
|
+
name: {
|
|
296
|
+
en: 'Speech sources',
|
|
297
|
+
de: 'Sprachquellen',
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
native: {},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const speechSources = [
|
|
304
|
+
{
|
|
305
|
+
id: 'solar',
|
|
306
|
+
name: {
|
|
307
|
+
en: 'Solar control messages',
|
|
308
|
+
de: 'Solarsteuerung-Meldungen',
|
|
309
|
+
},
|
|
310
|
+
enabledName: {
|
|
311
|
+
en: 'Enable solar control messages',
|
|
312
|
+
de: 'Solarsteuerung-Meldungen aktivieren',
|
|
313
|
+
},
|
|
314
|
+
enabledDesc: {
|
|
315
|
+
en: 'Allows speech messages when the solar control switches the pump on or off',
|
|
316
|
+
de: 'Erlaubt Sprachmeldungen, wenn die Solarsteuerung die Pumpe ein- oder ausschaltet',
|
|
317
|
+
},
|
|
318
|
+
cooldownName: {
|
|
319
|
+
en: 'Solar message cooldown',
|
|
320
|
+
de: 'Solar-Meldungssperre',
|
|
321
|
+
},
|
|
322
|
+
cooldownDesc: {
|
|
323
|
+
en: 'Minimum time in minutes between solar control speech messages',
|
|
324
|
+
de: 'Mindestzeit in Minuten zwischen Solarsteuerung-Sprachmeldungen',
|
|
325
|
+
},
|
|
326
|
+
cooldownDefault: 30,
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: 'time',
|
|
330
|
+
name: {
|
|
331
|
+
en: 'Time control messages',
|
|
332
|
+
de: 'Zeitsteuerung-Meldungen',
|
|
333
|
+
},
|
|
334
|
+
enabledName: {
|
|
335
|
+
en: 'Enable time control messages',
|
|
336
|
+
de: 'Zeitsteuerung-Meldungen aktivieren',
|
|
337
|
+
},
|
|
338
|
+
enabledDesc: {
|
|
339
|
+
en: 'Allows speech messages when the time control switches the pump on or off',
|
|
340
|
+
de: 'Erlaubt Sprachmeldungen, wenn die Zeitsteuerung die Pumpe ein- oder ausschaltet',
|
|
341
|
+
},
|
|
342
|
+
cooldownName: {
|
|
343
|
+
en: 'Time control message cooldown',
|
|
344
|
+
de: 'Zeitsteuerung-Meldungssperre',
|
|
345
|
+
},
|
|
346
|
+
cooldownDesc: {
|
|
347
|
+
en: 'Minimum time in minutes between time control speech messages',
|
|
348
|
+
de: 'Mindestzeit in Minuten zwischen Zeitsteuerung-Sprachmeldungen',
|
|
349
|
+
},
|
|
350
|
+
cooldownDefault: 0,
|
|
351
|
+
},
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
for (const source of speechSources) {
|
|
355
|
+
const baseId = `speech.sources.${source.id}`;
|
|
356
|
+
|
|
357
|
+
await adapter.setObjectNotExistsAsync(baseId, {
|
|
358
|
+
type: 'channel',
|
|
359
|
+
common: {
|
|
360
|
+
name: source.name,
|
|
361
|
+
},
|
|
362
|
+
native: {},
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
await adapter.setObjectNotExistsAsync(`${baseId}.enabled`, {
|
|
366
|
+
type: 'state',
|
|
367
|
+
common: {
|
|
368
|
+
name: source.enabledName,
|
|
369
|
+
desc: source.enabledDesc,
|
|
370
|
+
type: 'boolean',
|
|
371
|
+
role: 'switch',
|
|
372
|
+
read: true,
|
|
373
|
+
write: true,
|
|
374
|
+
def: true,
|
|
375
|
+
persist: true,
|
|
376
|
+
},
|
|
377
|
+
native: {},
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const enabledState = await adapter.getStateAsync(`${baseId}.enabled`);
|
|
381
|
+
if (!enabledState || enabledState.val === null || enabledState.val === undefined) {
|
|
382
|
+
await adapter.setStateAsync(`${baseId}.enabled`, { val: true, ack: true });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
await adapter.setObjectNotExistsAsync(`${baseId}.cooldown_minutes`, {
|
|
386
|
+
type: 'state',
|
|
387
|
+
common: {
|
|
388
|
+
name: source.cooldownName,
|
|
389
|
+
desc: source.cooldownDesc,
|
|
390
|
+
type: 'number',
|
|
391
|
+
role: 'level',
|
|
392
|
+
unit: 'min',
|
|
393
|
+
read: true,
|
|
394
|
+
write: true,
|
|
395
|
+
min: 0,
|
|
396
|
+
def: source.cooldownDefault,
|
|
397
|
+
persist: true,
|
|
398
|
+
},
|
|
399
|
+
native: {},
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const cooldownState = await adapter.getStateAsync(`${baseId}.cooldown_minutes`);
|
|
403
|
+
if (!cooldownState || cooldownState.val === null || cooldownState.val === undefined) {
|
|
404
|
+
await adapter.setStateAsync(`${baseId}.cooldown_minutes`, {
|
|
405
|
+
val: source.cooldownDefault,
|
|
406
|
+
ack: true,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
await adapter.setObjectNotExistsAsync(`${baseId}.last_sent`, {
|
|
411
|
+
type: 'state',
|
|
412
|
+
common: {
|
|
413
|
+
name: {
|
|
414
|
+
en: 'Last message sent',
|
|
415
|
+
de: 'Letzte gesendete Meldung',
|
|
416
|
+
},
|
|
417
|
+
desc: {
|
|
418
|
+
en: 'Timestamp of the last speech message for this source',
|
|
419
|
+
de: 'Zeitstempel der letzten Sprachmeldung dieser Quelle',
|
|
420
|
+
},
|
|
421
|
+
type: 'string',
|
|
422
|
+
role: 'value.time',
|
|
423
|
+
read: true,
|
|
424
|
+
write: false,
|
|
425
|
+
def: '',
|
|
426
|
+
persist: true,
|
|
427
|
+
},
|
|
428
|
+
native: {},
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const lastSentState = await adapter.getStateAsync(`${baseId}.last_sent`);
|
|
432
|
+
if (!lastSentState || lastSentState.val === null || lastSentState.val === undefined) {
|
|
433
|
+
await adapter.setStateAsync(`${baseId}.last_sent`, { val: '', ack: true });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
adapter.log.debug('[speechStates] Speech source states checked and created');
|
|
438
|
+
|
|
288
439
|
// versteckte Dateien
|
|
289
440
|
|
|
290
441
|
await adapter.setObjectNotExistsAsync('speech.solar_active', {
|
package/main.js
CHANGED
|
@@ -30,6 +30,7 @@ const aiForecastHelper = require('./lib/helpers/aiForecastHelper');
|
|
|
30
30
|
const aiChemistryHelpHelper = require('./lib/helpers/aiChemistryHelpHelper');
|
|
31
31
|
const chemistryPhHelper = require('./lib/helpers/chemistryPhHelper');
|
|
32
32
|
const chemistryTdsHelper = require('./lib/helpers/chemistryTdsHelper');
|
|
33
|
+
const chemistryOrpHelper = require('./lib/helpers/chemistryOrpHelper');
|
|
33
34
|
const controlHelper = require('./lib/helpers/controlHelper');
|
|
34
35
|
const controlHelper2 = require('./lib/helpers/controlHelper2');
|
|
35
36
|
const debugLogHelper = require('./lib/helpers/debugLogHelper');
|
|
@@ -63,6 +64,7 @@ const { createAiStates } = require('./lib/stateDefinitions/aiStates'); // NEU: K
|
|
|
63
64
|
const { createAiChemistryHelpStates } = require('./lib/stateDefinitions/aiChemistryHelpStates'); // NEU: KI-Chemie-Hilfe
|
|
64
65
|
const { createChemistryPhStates } = require('./lib/stateDefinitions/chemistryPhStates');
|
|
65
66
|
const { createChemistryTdsStates } = require('./lib/stateDefinitions/chemistryTdsStates');
|
|
67
|
+
const { createChemistryOrpStates } = require('./lib/stateDefinitions/chemistryOrpStates');
|
|
66
68
|
const { createHeatStates } = require('./lib/stateDefinitions/heatStates');
|
|
67
69
|
const { createActuatorsStates } = require('./lib/stateDefinitions/actuatorsStates');
|
|
68
70
|
const { createSolarInsightsStates } = require('./lib/stateDefinitions/solarInsightsStates');
|
|
@@ -172,6 +174,9 @@ class Poolcontrol extends utils.Adapter {
|
|
|
172
174
|
// --- Chemistry / TDS evaluation ---
|
|
173
175
|
await createChemistryTdsStates(this);
|
|
174
176
|
|
|
177
|
+
// --- Chemistry / ORP evaluation ---
|
|
178
|
+
await createChemistryOrpStates(this);
|
|
179
|
+
|
|
175
180
|
// --- Zusatz-Aktoren (Beleuchtung & Zusatzpumpen) ---
|
|
176
181
|
await createActuatorsStates(this);
|
|
177
182
|
|
|
@@ -202,6 +207,7 @@ class Poolcontrol extends utils.Adapter {
|
|
|
202
207
|
aiChemistryHelpHelper.init(this);
|
|
203
208
|
chemistryPhHelper.init(this);
|
|
204
209
|
chemistryTdsHelper.init(this);
|
|
210
|
+
chemistryOrpHelper.init(this);
|
|
205
211
|
frostHelper.init(this);
|
|
206
212
|
statusHelper.init(this);
|
|
207
213
|
infoHelper.init(this);
|
|
@@ -300,6 +306,9 @@ class Poolcontrol extends utils.Adapter {
|
|
|
300
306
|
if (chemistryTdsHelper.cleanup) {
|
|
301
307
|
chemistryTdsHelper.cleanup();
|
|
302
308
|
}
|
|
309
|
+
if (chemistryOrpHelper.cleanup) {
|
|
310
|
+
chemistryOrpHelper.cleanup();
|
|
311
|
+
}
|
|
303
312
|
if (aiChemistryHelpHelper.cleanup) {
|
|
304
313
|
aiChemistryHelpHelper.cleanup();
|
|
305
314
|
}
|
|
@@ -438,6 +447,11 @@ class Poolcontrol extends utils.Adapter {
|
|
|
438
447
|
} catch (e) {
|
|
439
448
|
this.log.warn(`[chemistryTdsHelper] Error in handleStateChange: ${e.message}`);
|
|
440
449
|
}
|
|
450
|
+
try {
|
|
451
|
+
await chemistryOrpHelper.handleStateChange(id, state);
|
|
452
|
+
} catch (e) {
|
|
453
|
+
this.log.warn(`[chemistryOrpHelper] Error in handleStateChange: ${e.message}`);
|
|
454
|
+
}
|
|
441
455
|
try {
|
|
442
456
|
statusHelper.handleStateChange(id, state);
|
|
443
457
|
} catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.poolcontrol",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.17",
|
|
4
4
|
"description": "Steuerung & Automatisierung für den Pool (Pumpe, Heizung, Ventile, Sensoren).",
|
|
5
5
|
"author": "DasBo1975 <dasbo1975@outlook.de>",
|
|
6
6
|
"homepage": "https://github.com/DasBo1975/ioBroker.poolcontrol",
|