homematic-manager 3.0.0-beta.30 → 3.0.0-beta.31

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homematic-manager/backend",
3
- "version": "3.0.0-beta.30",
3
+ "version": "3.0.0-beta.31",
4
4
  "type": "module",
5
5
  "description": "Homematic Manager backend: XML-RPC/BIN-RPC clients and callback servers, caches, optional ReGa",
6
6
  "license": "AGPL-3.0-or-later",
@@ -143,9 +143,22 @@ export declare class EasyModeEngine {
143
143
  */
144
144
  applyProfile(profile: LinkProfile, current: Paramset, description: ParamsetDescription, options?: ApplyProfileOptions): AppliedProfile;
145
145
  /**
146
- * Which profile a link paramset currently follows: by `UI_HINT` when the CCU wrote one,
147
- * otherwise by matching the profiles' `fixed` parameters against the current values - the more
148
- * fixed parameters a profile matches, the better the fit.
146
+ * Which profile a link paramset currently follows, the way the WebUI's `get_cur_profile2`
147
+ * decides it: a profile fits when every parameter it names holds on the link - a `fixed` value
148
+ * equal, a `list` value one of its values, a `range` value inside it - leaving out the
149
+ * parameters the link does not have (B-59, "Gibt es diesen Parameter im Aktor überhaupt?").
150
+ *
151
+ * B-77: only the `fixed` parameters used to count, and the profile with the most of them won.
152
+ * The profiles of one channel pair often differ only in their lists - an HmIP switch's
153
+ * "Schalter ein" and "Schalter ein / aus" differ in the jump table (`*_JT_ON` 1/3 or 4/6) -
154
+ * so a toggle link was shown as "switch on", which has more fixed parameters.
155
+ *
156
+ * Order: `UI_HINT` when its profile fits (`0`, the expert view, is taken as it is); otherwise
157
+ * the fitting profile the link has the most named parameters of, then the one with more fixed
158
+ * parameters (a staircase light is a "switch on" with a fixed time: the more specific one), then
159
+ * the one that lacks fewer, then the lower id - the WebUI takes the first fitting one by id, and
160
+ * where only one fits, as for B-77's pair, the two agree; last a `UI_HINT` whose profile no
161
+ * longer fits, as before.
149
162
  *
150
163
  * `undefined` means "none of them"; the caller then shows the expert view.
151
164
  */
@@ -153,43 +153,43 @@ export class EasyModeEngine {
153
153
  return { values, problems };
154
154
  }
155
155
  /**
156
- * Which profile a link paramset currently follows: by `UI_HINT` when the CCU wrote one,
157
- * otherwise by matching the profiles' `fixed` parameters against the current values - the more
158
- * fixed parameters a profile matches, the better the fit.
156
+ * Which profile a link paramset currently follows, the way the WebUI's `get_cur_profile2`
157
+ * decides it: a profile fits when every parameter it names holds on the link - a `fixed` value
158
+ * equal, a `list` value one of its values, a `range` value inside it - leaving out the
159
+ * parameters the link does not have (B-59, "Gibt es diesen Parameter im Aktor überhaupt?").
160
+ *
161
+ * B-77: only the `fixed` parameters used to count, and the profile with the most of them won.
162
+ * The profiles of one channel pair often differ only in their lists - an HmIP switch's
163
+ * "Schalter ein" and "Schalter ein / aus" differ in the jump table (`*_JT_ON` 1/3 or 4/6) -
164
+ * so a toggle link was shown as "switch on", which has more fixed parameters.
165
+ *
166
+ * Order: `UI_HINT` when its profile fits (`0`, the expert view, is taken as it is); otherwise
167
+ * the fitting profile the link has the most named parameters of, then the one with more fixed
168
+ * parameters (a staircase light is a "switch on" with a fixed time: the more specific one), then
169
+ * the one that lacks fewer, then the lower id - the WebUI takes the first fitting one by id, and
170
+ * where only one fits, as for B-77's pair, the two agree; last a `UI_HINT` whose profile no
171
+ * longer fits, as before.
159
172
  *
160
173
  * `undefined` means "none of them"; the caller then shows the expert view.
161
174
  */
162
175
  detectProfile(linkParamset, profiles) {
163
176
  const hint = linkParamset[UI_HINT];
164
- if (hint !== undefined && hint !== '') {
165
- const byHint = profiles.find((profile) => String(profile.id) === String(hint));
166
- if (byHint) {
167
- return byHint;
168
- }
177
+ const byHint = hint === undefined || hint === ''
178
+ ? undefined
179
+ : profiles.find((profile) => String(profile.id) === String(hint));
180
+ if (byHint && (byHint.id === EXPERT_PROFILE_ID || profileFits(byHint, linkParamset) !== undefined)) {
181
+ return byHint;
169
182
  }
170
183
  let best;
171
- let bestScore = 0;
172
- let bestMissing = 0;
173
- for (const profile of profiles) {
174
- const all = Object.entries(profile.params).filter((entry) => entry[1].kind === 'fixed');
175
- // B-59: a parameter the link does not have (this firmware lacks it) is left out, as the
176
- // WebUI's get_cur_profile2 does ("Gibt es diesen Parameter im Aktor überhaupt?")
177
- const fixed = all.filter(([param]) => param in linkParamset);
178
- const missing = all.length - fixed.length;
179
- if (fixed.length === 0) {
180
- continue;
181
- }
182
- if (!fixed.every(([param, constraint]) => sameProfileValue(linkParamset[param], constraint.value))) {
183
- continue;
184
- }
185
- // the most fixed parameters matched; on a tie the profile that asks for less the link lacks
186
- if (fixed.length > bestScore || (fixed.length === bestScore && missing < bestMissing)) {
187
- bestScore = fixed.length;
188
- bestMissing = missing;
184
+ let bestFit;
185
+ for (const profile of [...profiles].sort((a, b) => a.id - b.id)) {
186
+ const fit = profileFits(profile, linkParamset);
187
+ if (fit !== undefined && betterFit(fit, bestFit)) {
188
+ bestFit = fit;
189
189
  best = profile;
190
190
  }
191
191
  }
192
- return best;
192
+ return best ?? byHint;
193
193
  }
194
194
  /**
195
195
  * The MASTER paramset of a channel type as the dialog should show it: ordered, with the
@@ -250,6 +250,58 @@ export function resolveAlias(receiverType, aliases) {
250
250
  }
251
251
  return current;
252
252
  }
253
+ /**
254
+ * Whether every parameter a profile names holds on the link, as `get_cur_profile2` checks it, and
255
+ * how many of them the link has and lacks. `undefined` when one does not hold, or when the link
256
+ * has none of them (the expert profile, or a profile of another firmware).
257
+ */
258
+ function profileFits(profile, linkParamset) {
259
+ let present = 0;
260
+ let fixed = 0;
261
+ let missing = 0;
262
+ for (const [param, constraint] of Object.entries(profile.params)) {
263
+ if (!(param in linkParamset)) {
264
+ missing += 1;
265
+ continue;
266
+ }
267
+ if (!constraintHolds(constraint, linkParamset[param])) {
268
+ return undefined;
269
+ }
270
+ present += 1;
271
+ if (constraint.kind === 'fixed') {
272
+ fixed += 1;
273
+ }
274
+ }
275
+ return present === 0 ? undefined : { present, fixed, missing };
276
+ }
277
+ /** Whether a fit is the better one: more named parameters, then more fixed ones, then fewer lacking. */
278
+ function betterFit(fit, best) {
279
+ if (best === undefined)
280
+ return true;
281
+ if (fit.present !== best.present)
282
+ return fit.present > best.present;
283
+ if (fit.fixed !== best.fixed)
284
+ return fit.fixed > best.fixed;
285
+ return fit.missing < best.missing;
286
+ }
287
+ function constraintHolds(constraint, value) {
288
+ switch (constraint.kind) {
289
+ case 'fixed':
290
+ return sameProfileValue(value, constraint.value);
291
+ case 'list':
292
+ return constraint.values.some((entry) => sameProfileValue(value, entry));
293
+ case 'range': {
294
+ const number = Number(value);
295
+ // an enum name where the range counts indices cannot be judged here; it rules nothing out
296
+ if (value === '' || Number.isNaN(number)) {
297
+ return true;
298
+ }
299
+ // the WebUI also accepts the range's default outside it: `{31 range 0 - 7}` of an HmIP
300
+ // "Schalter aus" holds for 31, because its last check compares against the first element
301
+ return (number >= constraint.min && number <= constraint.max) || number === constraint.default;
302
+ }
303
+ }
304
+ }
253
305
  function applyConstraint(constraint, current) {
254
306
  switch (constraint.kind) {
255
307
  case 'fixed':
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homematic-manager/core",
3
- "version": "3.0.0-beta.30",
3
+ "version": "3.0.0-beta.31",
4
4
  "type": "module",
5
5
  "description": "Pure domain logic of the Homematic Manager: no I/O, no DOM",
6
6
  "license": "AGPL-3.0-or-later",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homematic-manager",
3
- "version": "3.0.0-beta.30",
3
+ "version": "3.0.0-beta.31",
4
4
  "type": "module",
5
5
  "description": "Configure and administer Homematic and HomematicIP devices - devices and channels, direct links, paramsets, RSSI, service messages, events and an RPC console - from a local HTTP/WebSocket server that also serves the web UI",
6
6
  "keywords": [
@@ -67,8 +67,8 @@
67
67
  "hm-simulator": "^1.1.0"
68
68
  },
69
69
  "dependencies": {
70
- "@homematic-manager/backend": "3.0.0-beta.30",
71
- "@homematic-manager/core": "3.0.0-beta.30",
70
+ "@homematic-manager/backend": "3.0.0-beta.31",
71
+ "@homematic-manager/core": "3.0.0-beta.31",
72
72
  "binrpc": "^4.2.0",
73
73
  "homematic-rega": "^2.0.0",
74
74
  "homematic-xmlrpc": "^2.0.0",