homematic-manager 3.0.0-beta.25 → 3.0.0-beta.26

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.
@@ -99,8 +99,8 @@ export class Backend {
99
99
  * swallows packets takes 30 s); a store that has not started by then never will.
100
100
  */
101
101
  #metaStarting;
102
- /** Task 36: ReGa's first report per `<interface>|<address>|<datapoint>`, from its pending alarms. */
103
- #regaFirstSeen = new Map();
102
+ /** Task 36, B-74: ReGa's pending alarms per `<interface>|<address>|<datapoint>` - the CCU's own list. */
103
+ #regaAlarms = new Map();
104
104
  /** Task 64: `getParamsetId` answers by `<interface>|<address>`; emptied on every connect. */
105
105
  #paramsetIds = new Map();
106
106
  #serviceMessageTimer;
@@ -665,7 +665,7 @@ export class Backend {
665
665
  });
666
666
  // B-54: the names are waited for only so long; the store starts after them either way,
667
667
  // so its names still land over ReGa's as before (task 27, D-40)
668
- this.#regaFirstSeen.clear();
668
+ this.#regaAlarms.clear();
669
669
  const names = this.#rega.refreshNames().then((changed) => {
670
670
  if (changed && this.#manager === manager) {
671
671
  this.#caches.saveNames();
@@ -1085,7 +1085,8 @@ export class Backend {
1085
1085
  */
1086
1086
  #stickyUnreachMessages() {
1087
1087
  const states = this.#manager?.states() ?? [];
1088
- return this.#caches.listServiceMessages().filter((message) => {
1088
+ // B-74: with the messages only ReGa lists - the WebUI's "war gestört" the interface forgot
1089
+ return this.#serviceMessages().filter((message) => {
1089
1090
  if (message.datapoint !== 'STICKY_UNREACH' || message.value === false) {
1090
1091
  return false;
1091
1092
  }
@@ -1102,9 +1103,7 @@ export class Backend {
1102
1103
  */
1103
1104
  async #acknowledgeExisting(messages) {
1104
1105
  for (const message of messages) {
1105
- const listed = this.#caches
1106
- .listServiceMessages(message.interfaceName)
1107
- .some((entry) => entry.address === message.address && entry.datapoint === message.datapoint);
1106
+ const listed = this.#serviceMessages(message.interfaceName).some((entry) => entry.address === message.address && entry.datapoint === message.datapoint);
1108
1107
  if (!listed || this.#stopped) {
1109
1108
  continue;
1110
1109
  }
@@ -1432,20 +1431,56 @@ export class Backend {
1432
1431
  return matrix;
1433
1432
  }
1434
1433
  #serviceMessages(interfaceName) {
1435
- return this.#withRegaTimes(this.#caches.listServiceMessages(interfaceName));
1434
+ return this.#withRegaAlarms(this.#caches.listServiceMessages(interfaceName), interfaceName);
1436
1435
  }
1437
1436
  /**
1438
1437
  * Task 36: where ReGa knows a message, its first report (the WebUI's *Erste Meldung*) replaces
1439
1438
  * the time this application first saw it, and says so.
1439
+ *
1440
+ * B-74 (#150): and a pending alarm whose datapoint the interface process does not list becomes
1441
+ * a row of its own, as the WebUI shows it - value and first report from ReGa, marked as listed
1442
+ * by the CCU only. rfd forgets a `STICKY_UNREACH` at a restart while ReGa keeps the alarm until
1443
+ * it is receipted, so the WebUI had nine messages this list lacked. Only the connection's own
1444
+ * interfaces and the datapoints that are service messages here: an alarm on a system variable
1445
+ * or a CUxD datapoint stays the WebUI's.
1440
1446
  */
1441
- #withRegaTimes(messages) {
1442
- if (this.#regaFirstSeen.size === 0) {
1447
+ #withRegaAlarms(messages, interfaceName) {
1448
+ if (this.#regaAlarms.size === 0) {
1443
1449
  return messages;
1444
1450
  }
1445
- return messages.map((message) => {
1446
- const first = this.#regaFirstSeen.get(`${message.interfaceName}|${message.address}|${message.datapoint}`);
1447
- return first === undefined ? message : { ...message, since: first, sinceSource: 'rega' };
1451
+ const listed = new Set();
1452
+ const result = messages.map((message) => {
1453
+ const key = `${message.interfaceName}|${message.address}|${message.datapoint}`;
1454
+ listed.add(key);
1455
+ const alarm = this.#regaAlarms.get(key);
1456
+ return alarm === undefined ? message : { ...message, since: alarm.first, sinceSource: 'rega' };
1448
1457
  });
1458
+ const known = new Set(this.#manager?.names() ?? []);
1459
+ for (const [key, alarm] of this.#regaAlarms) {
1460
+ if (listed.has(key) ||
1461
+ !known.has(alarm.interfaceName) ||
1462
+ (interfaceName !== undefined && alarm.interfaceName !== interfaceName) ||
1463
+ !countsAsServiceMessage(alarm.datapoint, alarm.value)) {
1464
+ continue;
1465
+ }
1466
+ result.push({
1467
+ interfaceName: alarm.interfaceName,
1468
+ address: alarm.address,
1469
+ datapoint: alarm.datapoint,
1470
+ value: alarm.value,
1471
+ since: alarm.first,
1472
+ sinceSource: 'rega',
1473
+ source: 'rega',
1474
+ });
1475
+ }
1476
+ return result;
1477
+ }
1478
+ /** B-74: is this message one only ReGa lists - a pending alarm without the interface's datapoint? */
1479
+ #isRegaOnly(interfaceName, address, datapoint) {
1480
+ return (this.#regaAlarms.has(`${interfaceName}|${address}|${datapoint}`) &&
1481
+ !this.#caches
1482
+ .listServiceMessages(interfaceName)
1483
+ .some((message) => message.address === address && message.datapoint === datapoint));
1449
1484
  }
1450
1485
  /**
1451
1486
  * Task 36: reads ReGa's pending service messages (one short script, read-only) and keeps their
@@ -1457,8 +1492,8 @@ export class Backend {
1457
1492
  async #refreshRegaAlarms() {
1458
1493
  const rega = this.#rega;
1459
1494
  if (!rega?.state.enabled) {
1460
- if (this.#regaFirstSeen.size > 0) {
1461
- this.#regaFirstSeen.clear();
1495
+ if (this.#regaAlarms.size > 0) {
1496
+ this.#regaAlarms.clear();
1462
1497
  this.events.emit('serviceMessages.changed', this.#serviceMessages());
1463
1498
  }
1464
1499
  return;
@@ -1467,12 +1502,15 @@ export class Backend {
1467
1502
  if (alarms === undefined || this.#rega !== rega) {
1468
1503
  return;
1469
1504
  }
1470
- const next = new Map(alarms.map((alarm) => [`${alarm.interfaceName}|${alarm.address}|${alarm.datapoint}`, alarm.first]));
1471
- const changed = next.size !== this.#regaFirstSeen.size ||
1472
- [...next].some(([key, first]) => this.#regaFirstSeen.get(key) !== first);
1473
- this.#regaFirstSeen.clear();
1474
- for (const [key, first] of next) {
1475
- this.#regaFirstSeen.set(key, first);
1505
+ const next = new Map(alarms.map((alarm) => [`${alarm.interfaceName}|${alarm.address}|${alarm.datapoint}`, alarm]));
1506
+ const changed = next.size !== this.#regaAlarms.size ||
1507
+ [...next].some(([key, alarm]) => {
1508
+ const before = this.#regaAlarms.get(key);
1509
+ return before === undefined || before.first !== alarm.first || before.value !== alarm.value;
1510
+ });
1511
+ this.#regaAlarms.clear();
1512
+ for (const [key, alarm] of next) {
1513
+ this.#regaAlarms.set(key, alarm);
1476
1514
  }
1477
1515
  if (changed) {
1478
1516
  this.events.emit('serviceMessages.changed', this.#serviceMessages());
@@ -1705,21 +1743,44 @@ export class Backend {
1705
1743
  if (!isAcknowledgeable(datapoint)) {
1706
1744
  throw validationError(`${datapoint} cannot be acknowledged`);
1707
1745
  }
1708
- const description = await this.#describe(interfaceName, address, 'VALUES');
1709
- const parameter = description[datapoint];
1710
- const value = parameter?.TYPE === 'ACTION';
1711
- await this.#writer.setValue(interfaceName, address, datapoint, value);
1746
+ // B-74: a message only ReGa lists has no flag left to clear on the interface; the write is
1747
+ // still sent (rfd takes a `false` for a flag it does not hold), but its failure - a device
1748
+ // the interface no longer knows, say - must not keep the alarm standing in the WebUI
1749
+ const regaOnly = this.#isRegaOnly(interfaceName, address, datapoint);
1750
+ try {
1751
+ const description = await this.#describe(interfaceName, address, 'VALUES');
1752
+ const parameter = description[datapoint];
1753
+ const value = parameter?.TYPE === 'ACTION';
1754
+ await this.#writer.setValue(interfaceName, address, datapoint, value);
1755
+ }
1756
+ catch (error) {
1757
+ if (!regaOnly) {
1758
+ throw error;
1759
+ }
1760
+ this.#notice('info', `${address}: ${datapoint} is listed by the CCU only; the interface did not take the write (${errorMessage(error)}), the alarm is receipted in ReGa`, interfaceName);
1761
+ }
1712
1762
  if (this.#caches.serviceMessages.clear(interfaceName, address, datapoint)) {
1713
1763
  this.events.emit('serviceMessages.changed', this.#serviceMessages());
1714
1764
  }
1715
1765
  // Issue #94: the datapoint write above is the acknowledgement that matters, and it happened
1716
1766
  // whether ReGa exists or not. This clears the CCU's own alarm on top, so the WebUI stops
1717
1767
  // showing a message the user has already dealt with here. D-2: silent when ReGa is off.
1718
- void this.#rega?.acknowledgeAlarm(interfaceName, address, datapoint).then(async (receipted) => {
1768
+ // B-74: for a message only ReGa lists the receipt *is* the acknowledgement, so it is waited
1769
+ // for, and the row goes with the list that follows it.
1770
+ const receipt = (this.#rega?.acknowledgeAlarm(interfaceName, address, datapoint) ?? Promise.resolve(false)).then(async (receipted) => {
1719
1771
  if (receipted) {
1720
1772
  await this.#refreshRegaAlarms();
1721
1773
  }
1774
+ else if (regaOnly) {
1775
+ this.#notice('warn', `${address}: ${datapoint} is listed by the CCU only, and ReGa did not receipt it`, interfaceName);
1776
+ }
1722
1777
  });
1778
+ if (regaOnly) {
1779
+ await receipt;
1780
+ }
1781
+ else {
1782
+ void receipt;
1783
+ }
1723
1784
  return null;
1724
1785
  }
1725
1786
  #recentEvents(interfaceName, limit) {
@@ -20,6 +20,7 @@
20
20
  * the interface process, or is a convenience; a system without ReGa loses nothing but the CCU-side
21
21
  * bookkeeping.
22
22
  */
23
+ import type { ParamsetValue } from '@homematic-manager/core';
23
24
  /** Escapes a string for the ReGa string literal it is written into. */
24
25
  export declare function escapeRegaString(value: string): string;
25
26
  /** One entry of a rename: the ReGa object id and the name it should carry. */
@@ -72,24 +73,35 @@ export declare function parseConfirmedDevices(output: string): ConfirmedDevice[]
72
73
  */
73
74
  export declare function acknowledgeAlarmScript(interfaceName: string, address: string, datapoint: string): string | undefined;
74
75
  /**
75
- * Task 36: the CCU's pending service messages with their times, as the WebUI's service-message page
76
- * reads them: every `OT_ALARMDP` of `ID_SERVICES` that is `asOncoming`, one line per alarm with the
77
- * trigger datapoint's name, `AlOccurrenceTime()` (the WebUI's *Erste Meldung*) and `Timestamp()`
76
+ * Task 36, B-74: the CCU's pending service messages with their times and values, as the WebUI's
77
+ * service-message page lists them: every `OT_ALARMDP` of `ID_SERVICES` that is `Used()`,
78
+ * `Enabled()` and `asOncoming` (`serviceMessages.htm` of OpenCCU 3.89.8), one line per alarm with
79
+ * the trigger datapoint's name, `AlOccurrenceTime()` (the WebUI's *Erste Meldung*), `Timestamp()`
78
80
  * (*Letzte Meldung*), both as Unix seconds (`ToInteger()`, checked against `date +%s` on the lab's
79
- * OpenCCU). Read-only.
81
+ * OpenCCU), and the trigger's `Value()` - ReGa's own last value of the datapoint, which is what
82
+ * the WebUI shows for the message (`true`/`false`, an ENUM as its index). Read-only.
83
+ *
84
+ * B-74 (#150): an alarm stays oncoming until it is receipted, whatever the interface process
85
+ * reports meanwhile - rfd forgets a `STICKY_UNREACH` at a restart, ReGa does not - so the
86
+ * WebUI lists messages `getServiceMessages` does not. The value is what such a message is shown
87
+ * with.
80
88
  */
81
- export declare const REGA_ALARMS_SCRIPT = "string sId;\nforeach (sId, dom.GetObject(ID_SERVICES).EnumIDs()) {\n object oAlarm = dom.GetObject(sId);\n if (oAlarm) {\n if (oAlarm.IsTypeOf(OT_ALARMDP) && (oAlarm.AlState() == asOncoming)) {\n object oTrigger = dom.GetObject(oAlarm.AlTriggerDP());\n if (oTrigger) {\n Write(oTrigger.Name() # \"\\t\" # oAlarm.AlOccurrenceTime().ToInteger() # \"\\t\" # oAlarm.Timestamp().ToInteger() # \"\\n\");\n }\n }\n }\n}\n";
82
- /** One pending ReGa service message: whose datapoint, first and last reported (epoch ms). */
89
+ export declare const REGA_ALARMS_SCRIPT = "string sId;\nforeach (sId, dom.GetObject(ID_SERVICES).EnumIDs()) {\n object oAlarm = dom.GetObject(sId);\n if (oAlarm) {\n if (oAlarm.IsTypeOf(OT_ALARMDP) && (oAlarm.Used() == true) && (oAlarm.Enabled() == true) && (oAlarm.AlState() == asOncoming)) {\n object oTrigger = dom.GetObject(oAlarm.AlTriggerDP());\n if (oTrigger) {\n Write(oTrigger.Name() # \"\\t\" # oAlarm.AlOccurrenceTime().ToInteger() # \"\\t\" # oAlarm.Timestamp().ToInteger() # \"\\t\" # oTrigger.Value() # \"\\n\");\n }\n }\n }\n}\n";
90
+ /** One pending ReGa service message: whose datapoint, its value, first and last reported (epoch ms). */
83
91
  export interface RegaAlarm {
84
92
  readonly interfaceName: string;
85
93
  readonly address: string;
86
94
  readonly datapoint: string;
95
+ /** ReGa's last value of the trigger datapoint; `true` when the line carries none (older script). */
96
+ readonly value: ParamsetValue;
87
97
  readonly first: number;
88
98
  readonly last: number;
89
99
  }
90
100
  /**
91
101
  * Reads what {@link REGA_ALARMS_SCRIPT} wrote. A line whose name is not `<interface>.<channel>.<datapoint>`
92
- * (a system alarm, a CUxD name with more dots) or whose time is no positive number is skipped.
102
+ * (a system alarm, a CUxD name with more dots) or whose time is no positive number is skipped. The
103
+ * value column is ReGa's text of the value: `true`/`false`, a number as it prints it (an ENUM by
104
+ * its index), anything else stays a string; a line without it means `true`.
93
105
  */
94
106
  export declare function parseRegaAlarms(output: string): RegaAlarm[];
95
107
  /**
@@ -124,20 +124,27 @@ Write(iDone);
124
124
  `;
125
125
  }
126
126
  /**
127
- * Task 36: the CCU's pending service messages with their times, as the WebUI's service-message page
128
- * reads them: every `OT_ALARMDP` of `ID_SERVICES` that is `asOncoming`, one line per alarm with the
129
- * trigger datapoint's name, `AlOccurrenceTime()` (the WebUI's *Erste Meldung*) and `Timestamp()`
127
+ * Task 36, B-74: the CCU's pending service messages with their times and values, as the WebUI's
128
+ * service-message page lists them: every `OT_ALARMDP` of `ID_SERVICES` that is `Used()`,
129
+ * `Enabled()` and `asOncoming` (`serviceMessages.htm` of OpenCCU 3.89.8), one line per alarm with
130
+ * the trigger datapoint's name, `AlOccurrenceTime()` (the WebUI's *Erste Meldung*), `Timestamp()`
130
131
  * (*Letzte Meldung*), both as Unix seconds (`ToInteger()`, checked against `date +%s` on the lab's
131
- * OpenCCU). Read-only.
132
+ * OpenCCU), and the trigger's `Value()` - ReGa's own last value of the datapoint, which is what
133
+ * the WebUI shows for the message (`true`/`false`, an ENUM as its index). Read-only.
134
+ *
135
+ * B-74 (#150): an alarm stays oncoming until it is receipted, whatever the interface process
136
+ * reports meanwhile - rfd forgets a `STICKY_UNREACH` at a restart, ReGa does not - so the
137
+ * WebUI lists messages `getServiceMessages` does not. The value is what such a message is shown
138
+ * with.
132
139
  */
133
140
  export const REGA_ALARMS_SCRIPT = `string sId;
134
141
  foreach (sId, dom.GetObject(ID_SERVICES).EnumIDs()) {
135
142
  object oAlarm = dom.GetObject(sId);
136
143
  if (oAlarm) {
137
- if (oAlarm.IsTypeOf(OT_ALARMDP) && (oAlarm.AlState() == asOncoming)) {
144
+ if (oAlarm.IsTypeOf(OT_ALARMDP) && (oAlarm.Used() == true) && (oAlarm.Enabled() == true) && (oAlarm.AlState() == asOncoming)) {
138
145
  object oTrigger = dom.GetObject(oAlarm.AlTriggerDP());
139
146
  if (oTrigger) {
140
- Write(oTrigger.Name() # "\\t" # oAlarm.AlOccurrenceTime().ToInteger() # "\\t" # oAlarm.Timestamp().ToInteger() # "\\n");
147
+ Write(oTrigger.Name() # "\\t" # oAlarm.AlOccurrenceTime().ToInteger() # "\\t" # oAlarm.Timestamp().ToInteger() # "\\t" # oTrigger.Value() # "\\n");
141
148
  }
142
149
  }
143
150
  }
@@ -145,12 +152,14 @@ foreach (sId, dom.GetObject(ID_SERVICES).EnumIDs()) {
145
152
  `;
146
153
  /**
147
154
  * Reads what {@link REGA_ALARMS_SCRIPT} wrote. A line whose name is not `<interface>.<channel>.<datapoint>`
148
- * (a system alarm, a CUxD name with more dots) or whose time is no positive number is skipped.
155
+ * (a system alarm, a CUxD name with more dots) or whose time is no positive number is skipped. The
156
+ * value column is ReGa's text of the value: `true`/`false`, a number as it prints it (an ENUM by
157
+ * its index), anything else stays a string; a line without it means `true`.
149
158
  */
150
159
  export function parseRegaAlarms(output) {
151
160
  const alarms = [];
152
161
  for (const line of output.split('\n')) {
153
- const [name, first, last] = line.split('\t');
162
+ const [name, first, last, value] = line.split('\t');
154
163
  const match = /^([^.\s]+)\.([^.\s]+)\.([^.\s]+)$/u.exec(name?.trim() ?? '');
155
164
  const firstSeconds = Number(first);
156
165
  const lastSeconds = Number(last);
@@ -161,12 +170,25 @@ export function parseRegaAlarms(output) {
161
170
  interfaceName: match[1] ?? '',
162
171
  address: match[2] ?? '',
163
172
  datapoint: match[3] ?? '',
173
+ value: parseRegaValue(value),
164
174
  first: firstSeconds * 1000,
165
175
  last: Number.isFinite(lastSeconds) && lastSeconds > 0 ? lastSeconds * 1000 : firstSeconds * 1000,
166
176
  });
167
177
  }
168
178
  return alarms;
169
179
  }
180
+ /** ReGa's text of a datapoint value, as `Write(oDP.Value())` prints it. */
181
+ function parseRegaValue(text) {
182
+ const trimmed = text?.trim() ?? '';
183
+ if (trimmed === '' || trimmed === 'true') {
184
+ return true;
185
+ }
186
+ if (trimmed === 'false') {
187
+ return false;
188
+ }
189
+ const number = Number(trimmed);
190
+ return Number.isFinite(number) ? number : trimmed;
191
+ }
170
192
  /**
171
193
  * Is this safe to put inside a ReGa string literal *unescaped*?
172
194
  *
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homematic-manager/backend",
3
- "version": "3.0.0-beta.25",
3
+ "version": "3.0.0-beta.26",
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",
@@ -589,6 +589,13 @@ export interface ServiceMessage {
589
589
  * WebUI's *Erste Meldung*); absent when it is when this application first saw it.
590
590
  */
591
591
  sinceSource?: 'rega';
592
+ /**
593
+ * B-74 (#150): `rega` when only the CCU's ReGa lists this message - its alarm is pending while
594
+ * the interface process no longer reports the datapoint (rfd forgets a `STICKY_UNREACH` at a
595
+ * restart, ReGa keeps the alarm until it is receipted). The WebUI shows such a message; so does
596
+ * this list. Absent when the interface reports it.
597
+ */
598
+ source?: 'rega';
592
599
  }
593
600
  export interface EventRecord {
594
601
  /** Milliseconds since epoch. */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homematic-manager/core",
3
- "version": "3.0.0-beta.25",
3
+ "version": "3.0.0-beta.26",
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.25",
3
+ "version": "3.0.0-beta.26",
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.0.0"
68
68
  },
69
69
  "dependencies": {
70
- "@homematic-manager/backend": "3.0.0-beta.25",
71
- "@homematic-manager/core": "3.0.0-beta.25",
70
+ "@homematic-manager/backend": "3.0.0-beta.26",
71
+ "@homematic-manager/core": "3.0.0-beta.26",
72
72
  "binrpc": "^4.2.0",
73
73
  "homematic-rega": "^2.0.0",
74
74
  "homematic-xmlrpc": "^2.0.0",