@iobroker/json-config 9.0.24 → 9.1.1

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.
@@ -65,6 +65,10 @@ export default class ConfigGeneric extends Component {
65
65
  reportedHidden = false;
66
66
  calculateTimeout = null;
67
67
  AsyncFunction;
68
+ /** Resolved `dependsOnStates` of this element: alias => state ID */
69
+ stateAliases = null;
70
+ /** True if this element has subscribed on states, so it must unsubscribe if `dependsOnStates` disappears */
71
+ statesSubscribed = false;
68
72
  constructor(props) {
69
73
  super(props);
70
74
  this.AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
@@ -98,7 +102,128 @@ export default class ConfigGeneric extends Component {
98
102
  }
99
103
  return (await this.props.oContext.socket.getObject(id)) || null;
100
104
  }
105
+ /**
106
+ * Bring the `dependsOnStates` attribute into the form `{ alias: stateID }`.
107
+ * In the short array form the state ID is used as an alias too.
108
+ *
109
+ * @param dependsOnStates value of the `dependsOnStates` attribute
110
+ * @returns aliases with the still unresolved state IDs
111
+ */
112
+ static normalizeDependsOnStates(dependsOnStates) {
113
+ const result = {};
114
+ if (!dependsOnStates) {
115
+ return result;
116
+ }
117
+ if (Array.isArray(dependsOnStates)) {
118
+ dependsOnStates.forEach(id => {
119
+ if (id && typeof id === 'string') {
120
+ result[id] = id;
121
+ }
122
+ });
123
+ }
124
+ else if (isObject(dependsOnStates)) {
125
+ Object.keys(dependsOnStates).forEach(alias => {
126
+ if (dependsOnStates[alias] && typeof dependsOnStates[alias] === 'string') {
127
+ result[alias] = dependsOnStates[alias];
128
+ }
129
+ });
130
+ }
131
+ else {
132
+ console.error(`[JsonConfigComponent] Invalid dependsOnStates: ${JSON.stringify(dependsOnStates)}`);
133
+ }
134
+ return result;
135
+ }
136
+ /**
137
+ * Build the real state ID out of a `dependsOnStates` entry: resolve the `${data.xxx}` patterns and
138
+ * expand a leading dot (`.info.running`) to the namespace of the own instance.
139
+ *
140
+ * @param id state ID from the schema
141
+ * @returns the resolved state ID or an empty string if it cannot be used
142
+ */
143
+ async resolveStateId(id) {
144
+ let result = (id || '').toString();
145
+ if (result.includes('${')) {
146
+ result = await this.getPatternAsync(result, null, true);
147
+ }
148
+ result = result.trim();
149
+ if (result.startsWith('.')) {
150
+ result = `${this.props.oContext.adapterName}.${this.props.oContext.instance || 0}${result}`;
151
+ }
152
+ if (result.includes('*')) {
153
+ console.error(`[JsonConfigComponent] Wildcards are not allowed in dependsOnStates: ${result}`);
154
+ return '';
155
+ }
156
+ return result;
157
+ }
158
+ /**
159
+ * Resolve all state IDs of a `dependsOnStates` attribute.
160
+ *
161
+ * @param dependsOnStates value of the `dependsOnStates` attribute
162
+ * @returns aliases with the resolved state IDs
163
+ */
164
+ async resolveDependsOnStates(dependsOnStates) {
165
+ const aliases = ConfigGeneric.normalizeDependsOnStates(dependsOnStates);
166
+ const resolved = {};
167
+ for (const alias of Object.keys(aliases)) {
168
+ const id = await this.resolveStateId(aliases[alias]);
169
+ if (id) {
170
+ resolved[alias] = id;
171
+ }
172
+ }
173
+ return resolved;
174
+ }
175
+ /**
176
+ * Subscribe on the given state IDs. The promise is resolved as soon as all values are known,
177
+ * so the following calculation does not run with unknown states.
178
+ *
179
+ * @param ids resolved state IDs
180
+ */
181
+ async subscribeOnStateIds(ids) {
182
+ if (!ids.length && !this.statesSubscribed) {
183
+ return;
184
+ }
185
+ this.statesSubscribed = !!ids.length;
186
+ await this.props.oContext.subscribeStates?.(this, ids, this.onDependsOnStateChanged);
187
+ }
188
+ /**
189
+ * Resolve the `dependsOnStates` attribute of this element and subscribe on the states.
190
+ *
191
+ * It is called before every calculation and not only once, because a state ID can contain a
192
+ * `${data.xxx}` pattern and so it can change if the user changes the configuration.
193
+ */
194
+ async updateStateSubscriptions() {
195
+ this.stateAliases = await this.resolveDependsOnStates(this.props.schema?.dependsOnStates);
196
+ await this.subscribeOnStateIds(Object.values(this.stateAliases));
197
+ }
198
+ /** True if this element depends on ioBroker states */
199
+ usesDependsOnStates() {
200
+ return !!this.props.schema?.dependsOnStates || this.statesSubscribed;
201
+ }
202
+ /** One of the states from `dependsOnStates` changed => recalculate `hidden`, `disabled`, ... */
203
+ onDependsOnStateChanged = () => {
204
+ this.forceUpdate();
205
+ };
206
+ /**
207
+ * Values of the subscribed states for the JS functions and patterns (`_states.<alias>`).
208
+ *
209
+ * @param aliases alias => state ID. If omitted, the aliases of this element are used
210
+ * @returns alias => state object. `null` if the state does not exist
211
+ */
212
+ getStateValues(aliases) {
213
+ const map = aliases === undefined ? this.stateAliases : aliases;
214
+ const result = {};
215
+ if (map) {
216
+ Object.keys(map).forEach(alias => {
217
+ result[alias] = this.props.oContext.getStateValue?.(map[alias]) ?? null;
218
+ });
219
+ }
220
+ return result;
221
+ }
101
222
  async componentDidMount() {
223
+ if (this.usesDependsOnStates()) {
224
+ // Subscribe before the first calculation, so `defaultFunc` and `hidden` already see the values
225
+ await this.updateStateSubscriptions();
226
+ }
102
227
  if (this.props.schema?.defaultFunc) {
103
228
  if (this.props.custom) {
104
229
  this.defaultValue = await this.executeCustom(this.props.schema.defaultFunc, this.props.data, this.props.customObj, this.props.oContext.instanceObj, this.props.arrayIndex, this.props.globalData, 'defaultFunc');
@@ -195,6 +320,10 @@ export default class ConfigGeneric extends Component {
195
320
  if (this.props.attr) {
196
321
  this.props.oContext.registerOnForceUpdate?.(this.props.attr);
197
322
  }
323
+ // Not only if `statesSubscribed` is true: the element could have released its states before (an ID
324
+ // with a `${data.xxx}` pattern was resolved to nothing), but it is still registered in the pool
325
+ this.props.oContext.unsubscribeStates?.(this);
326
+ this.statesSubscribed = false;
198
327
  if (this.sendToTimeout) {
199
328
  clearTimeout(this.sendToTimeout);
200
329
  this.sendToTimeout = null;
@@ -259,14 +388,16 @@ export default class ConfigGeneric extends Component {
259
388
  ConfigGeneric.setValue(data[part], attr, value);
260
389
  }
261
390
  }
262
- getText(text, noTranslation) {
391
+ getText(text, noTranslation,
392
+ /** Values for `_states`. If omitted, the states of this element are used */
393
+ states) {
263
394
  if (!text) {
264
395
  return '';
265
396
  }
266
397
  if (typeof text === 'string') {
267
398
  const strText = noTranslation ? text : I18n.t(text);
268
399
  if (strText.includes('${')) {
269
- return this.getPattern(strText, undefined, noTranslation);
400
+ return this.getPattern(strText, undefined, noTranslation, states);
270
401
  }
271
402
  return strText;
272
403
  }
@@ -277,9 +408,9 @@ export default class ConfigGeneric extends Component {
277
408
  if (typeof funcText.func === 'object') {
278
409
  return this.getPattern(funcText.func[this.lang] ||
279
410
  funcText.func.en ||
280
- '', undefined, true);
411
+ '', undefined, true, states);
281
412
  }
282
- return this.getPattern(funcText.func, undefined, noTranslation);
413
+ return this.getPattern(funcText.func, undefined, noTranslation, states);
283
414
  }
284
415
  return text[this.lang] || text.en || '';
285
416
  }
@@ -604,7 +735,9 @@ export default class ConfigGeneric extends Component {
604
735
  }
605
736
  return true;
606
737
  }
607
- async execute(func, defaultValue, data, arrayIndex, globalData, funcName) {
738
+ async execute(func, defaultValue, data, arrayIndex, globalData, funcName,
739
+ /** Values for `_states`. If omitted, the states of this element are used */
740
+ states) {
608
741
  let fun;
609
742
  if (isObject(func)) {
610
743
  fun = func.func;
@@ -623,8 +756,8 @@ export default class ConfigGeneric extends Component {
623
756
  fun = ConfigGeneric.ensureAwaitGetObject(fun);
624
757
  }
625
758
  try {
626
- const f = new this.AsyncFunction('data', 'originalData', '_system', '_alive', '_common', '_socket', '_instance', 'arrayIndex', 'globalData', '_changed', '_href', 'getObject', '_os', '_arch', '_host', fun.includes('return') ? fun : `return ${fun}`);
627
- const result = await f(data || this.props.data || {}, this.props.originalData, this.props.oContext.systemConfig, this.props.alive, this.props.common, this.props.oContext.socket, this.props.oContext.instance, arrayIndex, globalData || {}, this.props.changed, window.location.href, this.getObject, this.props.oContext.hostInfo?.os || '', this.props.oContext.hostInfo?.arch || '', this.props.oContext.hostInfo || {});
759
+ const f = new this.AsyncFunction('data', 'originalData', '_system', '_alive', '_common', '_socket', '_instance', 'arrayIndex', 'globalData', '_changed', '_href', 'getObject', '_os', '_arch', '_host', '_states', fun.includes('return') ? fun : `return ${fun}`);
760
+ const result = await f(data || this.props.data || {}, this.props.originalData, this.props.oContext.systemConfig, this.props.alive, this.props.common, this.props.oContext.socket, this.props.oContext.instance, arrayIndex, globalData || {}, this.props.changed, window.location.href, this.getObject, this.props.oContext.hostInfo?.os || '', this.props.oContext.hostInfo?.arch || '', this.props.oContext.hostInfo || {}, states || this.getStateValues());
628
761
  this.debugLog(funcName || 'JS function', fun, result, data);
629
762
  return result;
630
763
  }
@@ -634,7 +767,9 @@ export default class ConfigGeneric extends Component {
634
767
  return defaultValue;
635
768
  }
636
769
  }
637
- async executeCustom(func, data, customObj, instanceObj, arrayIndex, globalData, funcName) {
770
+ async executeCustom(func, data, customObj, instanceObj, arrayIndex, globalData, funcName,
771
+ /** Values for `_states`. If omitted, the states of this element are used */
772
+ states) {
638
773
  let fun;
639
774
  if (isObject(func)) {
640
775
  fun = func.func;
@@ -653,8 +788,8 @@ export default class ConfigGeneric extends Component {
653
788
  fun = ConfigGeneric.ensureAwaitGetObject(fun);
654
789
  }
655
790
  try {
656
- const f = new this.AsyncFunction('data', 'originalData', '_system', 'instanceObj', 'customObj', '_socket', 'arrayIndex', 'globalData', '_changed', '_href', 'getObject', '_os', '_arch', '_host', fun.includes('return') ? fun : `return ${fun}`);
657
- const result = await f(data || this.props.data, this.props.originalData, this.props.oContext.systemConfig, instanceObj || {}, customObj || {}, this.props.oContext.socket, arrayIndex, globalData || {}, this.props.changed, window.location.href, this.getObject, this.props.oContext.hostInfo?.os || '', this.props.oContext.hostInfo?.arch || '', this.props.oContext.hostInfo || {});
791
+ const f = new this.AsyncFunction('data', 'originalData', '_system', 'instanceObj', 'customObj', '_socket', 'arrayIndex', 'globalData', '_changed', '_href', 'getObject', '_os', '_arch', '_host', '_states', fun.includes('return') ? fun : `return ${fun}`);
792
+ const result = await f(data || this.props.data, this.props.originalData, this.props.oContext.systemConfig, instanceObj || {}, customObj || {}, this.props.oContext.socket, arrayIndex, globalData || {}, this.props.changed, window.location.href, this.getObject, this.props.oContext.hostInfo?.os || '', this.props.oContext.hostInfo?.arch || '', this.props.oContext.hostInfo || {}, states || this.getStateValues());
658
793
  this.debugLog(funcName || 'JS function', fun, result, data);
659
794
  return result;
660
795
  }
@@ -801,7 +936,9 @@ export default class ConfigGeneric extends Component {
801
936
  return null;
802
937
  }
803
938
  };
804
- async getPatternAsync(pattern, data, noTranslation) {
939
+ async getPatternAsync(pattern, data, noTranslation,
940
+ /** Values for `_states`. If omitted, the states of this element are used */
941
+ states) {
805
942
  data ||= this.props.data;
806
943
  if (!pattern) {
807
944
  return '';
@@ -825,15 +962,15 @@ export default class ConfigGeneric extends Component {
825
962
  }
826
963
  try {
827
964
  if (this.props.custom) {
828
- const f = new this.AsyncFunction('data', 'originalData', 'arrayIndex', 'globalData', '_system', 'instanceObj', 'customObj', '_socket', '_changed', '_href', 'getObject', '_os', '_arch', '_host', `return \`${ConfigGeneric.escapeString(patternStr, data)}\``);
829
- const text = await f(data, this.props.originalData, this.props.arrayIndex, this.props.globalData, this.props.oContext.systemConfig, this.props.oContext.instanceObj, this.props.customObj, this.props.oContext.socket, this.props.changed, window.location.href, this.getObject, this.props.oContext.hostInfo?.os || '', this.props.oContext.hostInfo?.arch || '', this.props.oContext.hostInfo || {});
965
+ const f = new this.AsyncFunction('data', 'originalData', 'arrayIndex', 'globalData', '_system', 'instanceObj', 'customObj', '_socket', '_changed', '_href', 'getObject', '_os', '_arch', '_host', '_states', `return \`${ConfigGeneric.escapeString(patternStr, data)}\``);
966
+ const text = await f(data, this.props.originalData, this.props.arrayIndex, this.props.globalData, this.props.oContext.systemConfig, this.props.oContext.instanceObj, this.props.customObj, this.props.oContext.socket, this.props.changed, window.location.href, this.getObject, this.props.oContext.hostInfo?.os || '', this.props.oContext.hostInfo?.arch || '', this.props.oContext.hostInfo || {}, states || this.getStateValues());
830
967
  if (noTranslation) {
831
968
  return text;
832
969
  }
833
970
  return I18n.t(text);
834
971
  }
835
- const f = new this.AsyncFunction('data', 'originalData', 'arrayIndex', 'globalData', '_system', '_alive', '_common', '_socket', '_changed', '_href', 'getObject', '_os', '_arch', '_host', `return \`${ConfigGeneric.escapeString(patternStr, data)}\``);
836
- const text = await f(data, this.props.originalData, this.props.arrayIndex, this.props.globalData, this.props.oContext.systemConfig, this.props.alive, this.props.common, this.props.oContext.socket, this.props.changed, window.location.href, this.getObject, this.props.oContext.hostInfo?.os || '', this.props.oContext.hostInfo?.arch || '', this.props.oContext.hostInfo || {});
972
+ const f = new this.AsyncFunction('data', 'originalData', 'arrayIndex', 'globalData', '_system', '_alive', '_common', '_socket', '_changed', '_href', 'getObject', '_os', '_arch', '_host', '_states', `return \`${ConfigGeneric.escapeString(patternStr, data)}\``);
973
+ const text = await f(data, this.props.originalData, this.props.arrayIndex, this.props.globalData, this.props.oContext.systemConfig, this.props.alive, this.props.common, this.props.oContext.socket, this.props.changed, window.location.href, this.getObject, this.props.oContext.hostInfo?.os || '', this.props.oContext.hostInfo?.arch || '', this.props.oContext.hostInfo || {}, states || this.getStateValues());
837
974
  if (noTranslation) {
838
975
  return text;
839
976
  }
@@ -844,7 +981,9 @@ export default class ConfigGeneric extends Component {
844
981
  return patternStr;
845
982
  }
846
983
  }
847
- getPattern(pattern, data, noTranslation) {
984
+ getPattern(pattern, data, noTranslation,
985
+ /** Values for `_states`. If omitted, the states of this element are used */
986
+ states) {
848
987
  data ||= this.props.data;
849
988
  if (!pattern) {
850
989
  return '';
@@ -868,15 +1007,15 @@ export default class ConfigGeneric extends Component {
868
1007
  }
869
1008
  try {
870
1009
  if (this.props.custom) {
871
- const f = new Function('data', 'originalData', 'arrayIndex', 'globalData', '_system', 'instanceObj', 'customObj', '_socket', '_changed', '_href', '_os', '_arch', '_host', `return \`${ConfigGeneric.escapeString(patternStr, data)}\``);
872
- const text = f(data, this.props.originalData, this.props.arrayIndex, this.props.globalData, this.props.oContext.systemConfig, this.props.oContext.instanceObj, this.props.customObj, this.props.oContext.socket, this.props.changed, window.location.href, this.props.oContext.hostInfo?.os || '', this.props.oContext.hostInfo?.arch || '', this.props.oContext.hostInfo || {});
1010
+ const f = new Function('data', 'originalData', 'arrayIndex', 'globalData', '_system', 'instanceObj', 'customObj', '_socket', '_changed', '_href', '_os', '_arch', '_host', '_states', `return \`${ConfigGeneric.escapeString(patternStr, data)}\``);
1011
+ const text = f(data, this.props.originalData, this.props.arrayIndex, this.props.globalData, this.props.oContext.systemConfig, this.props.oContext.instanceObj, this.props.customObj, this.props.oContext.socket, this.props.changed, window.location.href, this.props.oContext.hostInfo?.os || '', this.props.oContext.hostInfo?.arch || '', this.props.oContext.hostInfo || {}, states || this.getStateValues());
873
1012
  if (noTranslation) {
874
1013
  return text;
875
1014
  }
876
1015
  return I18n.t(text);
877
1016
  }
878
- const f = new Function('data', 'originalData', 'arrayIndex', 'globalData', '_system', '_alive', '_common', '_socket', '_changed', '_href', '_os', '_arch', '_host', `return \`${ConfigGeneric.escapeString(patternStr, data)}\``);
879
- const text = f(data, this.props.originalData, this.props.arrayIndex, this.props.globalData, this.props.oContext.systemConfig, this.props.alive, this.props.common, this.props.oContext.socket, this.props.changed, window.location.href, this.props.oContext.hostInfo?.os || '', this.props.oContext.hostInfo?.arch || '', this.props.oContext.hostInfo || {});
1017
+ const f = new Function('data', 'originalData', 'arrayIndex', 'globalData', '_system', '_alive', '_common', '_socket', '_changed', '_href', '_os', '_arch', '_host', '_states', `return \`${ConfigGeneric.escapeString(patternStr, data)}\``);
1018
+ const text = f(data, this.props.originalData, this.props.arrayIndex, this.props.globalData, this.props.oContext.systemConfig, this.props.alive, this.props.common, this.props.oContext.socket, this.props.changed, window.location.href, this.props.oContext.hostInfo?.os || '', this.props.oContext.hostInfo?.arch || '', this.props.oContext.hostInfo || {}, states || this.getStateValues());
880
1019
  if (noTranslation) {
881
1020
  return text;
882
1021
  }
@@ -897,6 +1036,11 @@ export default class ConfigGeneric extends Component {
897
1036
  if (!schema) {
898
1037
  return;
899
1038
  }
1039
+ if (this.usesDependsOnStates()) {
1040
+ // The state IDs can contain `${data.xxx}` patterns, so they must be resolved anew if the data changed.
1041
+ // It waits for the first values, so the element is not calculated with unknown states.
1042
+ await this.updateStateSubscriptions();
1043
+ }
900
1044
  const { error, disabled, hidden, defaultValue } = await this.calculate(schema);
901
1045
  if (!this.state.calculatedValues ||
902
1046
  this.state.calculatedValues.error !== error ||