@7365admin1/layer-common 4.121.0 → 4.122.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.
- package/CHANGELOG.md +20 -4
- package/assets/css/robot-hero.css +12 -0
- package/components/Robot/LiftQueue.vue +97 -3
- package/composables/useRobots.ts +21 -0
- package/package.json +1 -1
- package/utils/notification-target.ts +12 -0
- package/utils/robot-building.ts +60 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.122.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 811b0e5: Lift queue: after Connect gateway / New code, the card shows the ready-to-paste gateway settings (this app's address and the code) with Copy settings, Done, and three numbered setup steps; the Waiting line says what to check.
|
|
8
|
+
|
|
9
|
+
## 4.122.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 0a37446: Lift queue: each lift card shows its real lift gateway (Connected / Offline / lift not answering, floor and door once decoded, raw readings), and Seven365 Super Admin can create a gateway code (shown once), make a new one, or disconnect. At a HERO Fleet Core site the gateway is read from the saved map's lift of the same name.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 5df1cfd: A Security Ops client meeting notification is now clickable on the web. `client-meeting` joins `SITE_PAGES` and opens the site's Ops Review page, where the meetings are listed. Without the entry `notificationTarget` answered null for the kind and the row opened nothing at all, the same dead end the `security-ops` entry fixed for action alerts. Its own kind rather than `security-ops`, because the phone opens the meeting record itself while the web has no per-meeting page.
|
|
18
|
+
|
|
3
19
|
## 4.121.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
|
@@ -3218,11 +3234,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
|
|
|
3218
3234
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
3219
3235
|
own rule from two endpoints the console already calls, unprojected:
|
|
3220
3236
|
|
|
3221
|
-
|
|
3222
|
-
|
|
3237
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
3238
|
+
GET /api/roles/id/:role that membership's role document
|
|
3223
3239
|
|
|
3224
|
-
|
|
3225
|
-
|
|
3240
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
3241
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
3226
3242
|
|
|
3227
3243
|
`role.default` is the marker because it is the only property of a platform
|
|
3228
3244
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -1264,6 +1264,18 @@ div.hero-editor__wp.hero-editor__sel, div.hero-editor__fid.hero-editor__sel { ou
|
|
|
1264
1264
|
.hero .hero-lift-row__release { border: 0; background: none; padding: 0; font: inherit; font-size: 12px; font-weight: 600; color: var(--hero-accent); cursor: pointer; white-space: nowrap; }
|
|
1265
1265
|
.hero-lift__ask { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 10px; font-size: 13px; }
|
|
1266
1266
|
.hero-lift__simnote { font-size: 12px; }
|
|
1267
|
+
.hero-lift__gw { grid-column: 1 / -1; display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; padding: 0 0 10px; font-size: 13px; min-width: 0; }
|
|
1268
|
+
.hero-lift__gwread { font-weight: 700; }
|
|
1269
|
+
.hero-lift__gwhelp { font-size: 12px; flex-basis: 100%; }
|
|
1270
|
+
.hero-lift__gwraw { flex-basis: 100%; font-size: 12px; }
|
|
1271
|
+
.hero-lift__gwraw summary { cursor: pointer; opacity: 0.7; }
|
|
1272
|
+
.hero-lift__gwreg { display: inline-block; margin: 4px 12px 0 0; font-family: monospace; }
|
|
1273
|
+
.hero-lift__gwact { margin-left: auto; display: flex; gap: 6px; }
|
|
1274
|
+
.hero-lift__gwcode { flex-basis: 100%; display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; padding: 8px 10px; border: 1px solid rgb(var(--v-theme-warning)); }
|
|
1275
|
+
.hero-lift__gwini { flex-basis: 100%; margin: 0; padding: 8px 10px; font-family: monospace; font-size: 12px; white-space: pre-wrap; word-break: break-all; user-select: all; background: rgba(var(--v-theme-on-surface), 0.06); }
|
|
1276
|
+
.hero-lift__gwsteps { flex-basis: 100%; margin: 0; padding-left: 20px; font-size: 13px; }
|
|
1277
|
+
.hero-lift__gwsteps li { margin: 2px 0; }
|
|
1278
|
+
.hero-lift__gwcode code { font-family: monospace; word-break: break-all; user-select: all; }
|
|
1267
1279
|
@media (max-width: 600px) {
|
|
1268
1280
|
.hero-lift { grid-template-columns: 1fr; }
|
|
1269
1281
|
.hero-bmap__bar { right: 14px; }
|
|
@@ -37,6 +37,41 @@
|
|
|
37
37
|
<span class="hero-lift__car"><span class="hero-lift__cardot" :class="[`hero-lift__cardot--${l.carTone}`, { 'hero-lift__cardot--blink': l.riding }]" aria-hidden="true" />{{ l.carText }}</span>
|
|
38
38
|
</div>
|
|
39
39
|
|
|
40
|
+
<!-- The real lift's read-only gateway link (Inovance B0S over RS-485): status, readings, setup. -->
|
|
41
|
+
<div v-if="l.gateway || (canEditMap && setupMapId)" class="hero-lift__gw" :data-status="l.gateway?.status ?? 'none'">
|
|
42
|
+
<template v-if="l.gateway">
|
|
43
|
+
<span class="hero-chip" :class="`hero-tone--${l.gateway.tone}`" role="status">{{ l.gateway.text }}</span>
|
|
44
|
+
<span v-if="l.gateway.decoded" class="hero-lift__gwread">{{ l.gateway.decoded }}</span>
|
|
45
|
+
<span v-else-if="l.gateway.undecoded" class="hero-muted hero-lift__gwread">Real lift answering · floor/door not decoded yet (fill [decode] in the gateway settings)</span>
|
|
46
|
+
<span v-if="l.gateway.help" class="hero-muted hero-lift__gwhelp">{{ l.gateway.help }}</span>
|
|
47
|
+
<details v-if="l.gateway.registers.length" class="hero-lift__gwraw">
|
|
48
|
+
<summary>Raw readings ({{ l.gateway.registers.length }})</summary>
|
|
49
|
+
<span v-for="r in l.gateway.registers" :key="r.reg" class="hero-lift__gwreg">{{ r.reg }} = {{ r.value }}</span>
|
|
50
|
+
</details>
|
|
51
|
+
</template>
|
|
52
|
+
<span v-else class="hero-muted">No real lift connected · the car below is simulated.</span>
|
|
53
|
+
<span v-if="canEditMap && setupMapId" class="hero-lift__gwact">
|
|
54
|
+
<v-btn size="small" variant="outlined" :loading="busy === `gw:${l.name}`" @click="connectGateway(l.name, !!l.gateway)">
|
|
55
|
+
{{ l.gateway ? "New code" : "Connect gateway" }}
|
|
56
|
+
</v-btn>
|
|
57
|
+
<v-btn v-if="l.gateway" size="small" variant="text" :disabled="busy === `gw:${l.name}`" @click="disconnectGateway(l.name)">Disconnect</v-btn>
|
|
58
|
+
</span>
|
|
59
|
+
<div v-if="newCode?.lift === l.name" class="hero-lift__gwcode" role="alert">
|
|
60
|
+
<b>Gateway settings for {{ l.name }} - shown once, copy them now:</b>
|
|
61
|
+
<pre class="hero-lift__gwini">{{ gatewayIni }}</pre>
|
|
62
|
+
<span class="hero-lift__gwact">
|
|
63
|
+
<v-btn size="small" variant="tonal" @click="copyCode">Copy settings</v-btn>
|
|
64
|
+
<v-btn size="small" variant="text" @click="newCode = null">Done</v-btn>
|
|
65
|
+
</span>
|
|
66
|
+
<ol class="hero-lift__gwsteps">
|
|
67
|
+
<li>On the gateway (Raspberry Pi / PC), paste these two lines into the <code>[server]</code> section of <code>lift-gateway.ini</code>.</li>
|
|
68
|
+
<li>Check the link without the lift: <code>python3 lift_gateway.py -c lift-gateway.ini --simulate</code>. It prints "iService365 OK" and this card turns <b>Connected</b>.</li>
|
|
69
|
+
<li>Wire the lift board's MOD1 port, then run it without <code>--simulate</code>. Floor and door show once the lift's register list is in the <code>[decode]</code> section.</li>
|
|
70
|
+
</ol>
|
|
71
|
+
<span class="hero-muted">Lost the code? Press New code - the old one stops working.</span>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
40
75
|
<div class="hero-lift__shaft" :style="{ height: `${l.shaftH}px` }" :title="l.simulated ? 'Simulated lift - no real lift is controlled yet' : undefined">
|
|
41
76
|
<div class="hero-lift__labels">
|
|
42
77
|
<div v-for="f in l.rails" :key="f.floorNo" class="hero-lift__floor" :class="{ 'hero-lift__floor--lit': f.lit }" :style="{ top: `${f.top}px` }">
|
|
@@ -139,13 +174,13 @@ const emit = defineEmits<{
|
|
|
139
174
|
status: [status: { connected: boolean; faults: number; siteName: string; clientName: string }];
|
|
140
175
|
}>();
|
|
141
176
|
|
|
142
|
-
const { rows, all, map, fleet, names, loading: mapLoading, error: mapError } = useRobotBuilding(
|
|
177
|
+
const { rows, all, map, saved, fleet, names, loading: mapLoading, error: mapError } = useRobotBuilding(
|
|
143
178
|
toRef(props, "organization"),
|
|
144
179
|
toRef(props, "site"),
|
|
145
180
|
toRef(props, "type"),
|
|
146
181
|
);
|
|
147
182
|
const kicker = computed(() => [props.clientName || names.value.clientName, props.siteName || names.value.siteName].filter(Boolean).join(" · "));
|
|
148
|
-
const { getLiftQueue, requestLift, releaseLiftSession } = useRobots();
|
|
183
|
+
const { getLiftQueue, requestLift, releaseLiftSession, connectLiftGateway, disconnectLiftGateway } = useRobots();
|
|
149
184
|
|
|
150
185
|
/* ---- the queue, every 3 s while the page is visible ---- */
|
|
151
186
|
const items = ref<Array<Record<string, any>>>([]);
|
|
@@ -157,7 +192,16 @@ const error = computed(() => mapError.value);
|
|
|
157
192
|
|
|
158
193
|
async function poll() {
|
|
159
194
|
// The fleet building has no lift sessions of ours: its lifts come live with the building (5 s refresh).
|
|
160
|
-
if (fleet.value)
|
|
195
|
+
if (fleet.value) {
|
|
196
|
+
// The fleet building's lifts, plus each real lift's gateway from the saved map (same lift name).
|
|
197
|
+
const list = fleetLiftItems(map.value, all.value);
|
|
198
|
+
const gw = saved.value?._id ? await getLiftQueue(String(saved.value._id)).catch(() => null) : null;
|
|
199
|
+
const byName = new Map<string, any>((gw?.items ?? []).map((x: any) => [String(x.liftName), x.gateway ?? null]));
|
|
200
|
+
items.value = list.map((x: Record<string, any>) => ({ ...x, gateway: byName.get(String(x.liftName)) ?? null }));
|
|
201
|
+
queueLoading.value = false;
|
|
202
|
+
now.value = Date.now();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
161
205
|
const id = map.value?._id;
|
|
162
206
|
if (!id) return void (items.value = []);
|
|
163
207
|
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
@@ -217,6 +261,56 @@ watch(cards, (list) => {
|
|
|
217
261
|
});
|
|
218
262
|
const busy = ref("");
|
|
219
263
|
const toast = ref("");
|
|
264
|
+
/*
|
|
265
|
+
* ---- the real lift's gateway (Seven365 Super Admin = canEditMap) ----
|
|
266
|
+
* Set up on the SAVED map's lift (at a fleet site, the imported Fleet Core building);
|
|
267
|
+
* the server re-checks Super Admin and stores only the code's sha256.
|
|
268
|
+
*/
|
|
269
|
+
const setupMapId = computed(() => String((fleet.value ? saved.value?._id : map.value?._id) ?? ""));
|
|
270
|
+
const newCode = ref<{ lift: string; code: string } | null>(null);
|
|
271
|
+
async function connectGateway(lift: string, replacing: boolean) {
|
|
272
|
+
if (replacing && typeof window !== "undefined" && !window.confirm(`Make a new code for ${lift}? The gateway's current code stops working until you paste the new one.`)) return;
|
|
273
|
+
busy.value = `gw:${lift}`;
|
|
274
|
+
try {
|
|
275
|
+
const res = await connectLiftGateway(setupMapId.value, lift);
|
|
276
|
+
newCode.value = { lift, code: res.code };
|
|
277
|
+
await poll();
|
|
278
|
+
} catch (e) {
|
|
279
|
+
toast.value = robotErrorMessage(
|
|
280
|
+
e,
|
|
281
|
+
fleet.value ? "Could not create the code. Import the HERO Fleet Core building on Building map → Edit map first, so this lift is on the saved map." : "Could not create the gateway code.",
|
|
282
|
+
);
|
|
283
|
+
} finally {
|
|
284
|
+
busy.value = "";
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async function disconnectGateway(lift: string) {
|
|
288
|
+
if (typeof window !== "undefined" && !window.confirm(`Disconnect the gateway from ${lift}? Its code stops working.`)) return;
|
|
289
|
+
busy.value = `gw:${lift}`;
|
|
290
|
+
try {
|
|
291
|
+
await disconnectLiftGateway(setupMapId.value, lift);
|
|
292
|
+
if (newCode.value?.lift === lift) newCode.value = null;
|
|
293
|
+
toast.value = `Gateway disconnected · ${lift}`;
|
|
294
|
+
await poll();
|
|
295
|
+
} catch (e) {
|
|
296
|
+
toast.value = robotErrorMessage(e, "The gateway could not be disconnected.");
|
|
297
|
+
} finally {
|
|
298
|
+
busy.value = "";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/** The gateway's [server] lines: this app's own address (it forwards /api to iService365) and the code. */
|
|
302
|
+
const gatewayIni = computed(() =>
|
|
303
|
+
newCode.value ? `url = ${typeof window !== "undefined" ? window.location.origin : ""}\ncode = ${newCode.value.code}` : "",
|
|
304
|
+
);
|
|
305
|
+
async function copyCode() {
|
|
306
|
+
try {
|
|
307
|
+
await navigator.clipboard.writeText(gatewayIni.value);
|
|
308
|
+
toast.value = "Settings copied";
|
|
309
|
+
} catch {
|
|
310
|
+
toast.value = "Select the code and copy it by hand.";
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
220
314
|
async function ask(l: { name: string }) {
|
|
221
315
|
const robotId = askRobot.value[l.name];
|
|
222
316
|
if (!robotId || !map.value?._id) return;
|
package/composables/useRobots.ts
CHANGED
|
@@ -559,6 +559,25 @@ export default function useRobots() {
|
|
|
559
559
|
);
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
+
/**
|
|
563
|
+
* A real lift's gateway code (Seven365 Super Admin): a new `lgw_...` code,
|
|
564
|
+
* shown ONCE in the answer; asking again replaces it (the old one stops working).
|
|
565
|
+
*/
|
|
566
|
+
function connectLiftGateway(mapId: string, liftName: string) {
|
|
567
|
+
return useNuxtApp().$api<{ code: string; codeHint: string }>(
|
|
568
|
+
`/api/robots/building-maps/${mapId}/lifts/${encodeURIComponent(liftName)}/gateway`,
|
|
569
|
+
{ method: "POST" },
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** The gateway's code stops working; the card goes back to the simulated car. */
|
|
574
|
+
function disconnectLiftGateway(mapId: string, liftName: string) {
|
|
575
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
576
|
+
`/api/robots/building-maps/${mapId}/lifts/${encodeURIComponent(liftName)}/gateway`,
|
|
577
|
+
{ method: "DELETE" },
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
562
581
|
/** Give the lift back, or leave the queue. */
|
|
563
582
|
function releaseLiftSession(sessionId: string) {
|
|
564
583
|
return useNuxtApp().$api<Record<string, any>>(`/api/robots/lift-sessions/${sessionId}/release`, {
|
|
@@ -595,6 +614,8 @@ export default function useRobots() {
|
|
|
595
614
|
getLiftQueue,
|
|
596
615
|
requestLift,
|
|
597
616
|
releaseLiftSession,
|
|
617
|
+
connectLiftGateway,
|
|
618
|
+
disconnectLiftGateway,
|
|
598
619
|
getRobotLiftSession,
|
|
599
620
|
getRobots,
|
|
600
621
|
getSiteFleet,
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.122.1",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|
|
@@ -214,6 +214,18 @@ const SITE_PAGES: Record<string, TSitePage[]> = {
|
|
|
214
214
|
* opened nothing at all. Adding it repairs those too, not only the new alerts.
|
|
215
215
|
*/
|
|
216
216
|
"security-ops": [{ apps: ["security"], page: () => "security-ops" }],
|
|
217
|
+
/**
|
|
218
|
+
* Security Ops: a client meeting was scheduled, moved or called off, or one
|
|
219
|
+
* is happening today.
|
|
220
|
+
*
|
|
221
|
+
* ITS OWN KIND, NOT `security-ops`, because the two land in different places:
|
|
222
|
+
* an action belongs to the register and a meeting belongs to the meeting
|
|
223
|
+
* record. The web has no per-meeting page, so this opens Ops Review, where
|
|
224
|
+
* the meetings tab lists them; the phone, which does have one, opens the
|
|
225
|
+
* meeting itself. Same kind, different answer per surface, which is what this
|
|
226
|
+
* table is for.
|
|
227
|
+
*/
|
|
228
|
+
"client-meeting": [{ apps: ["security"], page: () => "security-ops" }],
|
|
217
229
|
};
|
|
218
230
|
SITE_PAGES.myVisitors = SITE_PAGES.visitors;
|
|
219
231
|
|
package/utils/robot-building.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
pushUndo,
|
|
19
19
|
type TMapPointM,
|
|
20
20
|
} from "./building-map";
|
|
21
|
-
import { floorName } from "./robot-display";
|
|
21
|
+
import { agoText, floorName } from "./robot-display";
|
|
22
22
|
|
|
23
23
|
/* ================================================================ camera */
|
|
24
24
|
|
|
@@ -1475,5 +1475,64 @@ export function liftCard(lift: Record<string, any>, now = Date.now()) {
|
|
|
1475
1475
|
})),
|
|
1476
1476
|
rows,
|
|
1477
1477
|
simulated: lift?.simulated === true,
|
|
1478
|
+
gateway: liftGatewayLine(lift?.gateway ?? null),
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
const DOOR_TEXT = ["Doors closed", "Doors moving", "Doors open"];
|
|
1483
|
+
const MOTION_TEXT = ["Stopped", "Going up", "Going down", "Moving"];
|
|
1484
|
+
const MODE_TEXT = ["Mode unknown", "Normal", "Robot mode", "FIRE mode", "Out of service", "EMERGENCY"];
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* The real lift's gateway (read-only link to the Inovance board), in words:
|
|
1488
|
+
* one status line, a tone, and the decoded floor/door/motion/mode once the
|
|
1489
|
+
* gateway's [decode] settings are filled in. `null` = no gateway: the car is simulated.
|
|
1490
|
+
*/
|
|
1491
|
+
export function liftGatewayLine(g: Record<string, any> | null) {
|
|
1492
|
+
if (!g) return null;
|
|
1493
|
+
const ago = g.ageS == null ? "" : agoText(0, Number(g.ageS) * 1000);
|
|
1494
|
+
const byStatus: Record<string, { text: string; tone: string; help: string }> = {
|
|
1495
|
+
waiting: {
|
|
1496
|
+
text: `Waiting for the gateway (code …${g.codeHint ?? ""})`,
|
|
1497
|
+
tone: "neutral",
|
|
1498
|
+
help: "The gateway has not sent anything yet. Check its lift-gateway.ini (url and code from Connect gateway), its network, and that the program is running.",
|
|
1499
|
+
},
|
|
1500
|
+
connected: { text: `Connected · last reading ${ago}`, tone: "success", help: "" },
|
|
1501
|
+
"no-lift": {
|
|
1502
|
+
text: "Gateway online · lift not answering",
|
|
1503
|
+
tone: "warning",
|
|
1504
|
+
help: g.lastError || "Check the RS-485 wiring (A/B), baud, parity and unit address.",
|
|
1505
|
+
},
|
|
1506
|
+
offline: {
|
|
1507
|
+
text: `Offline · last heard ${ago}`,
|
|
1508
|
+
tone: "error",
|
|
1509
|
+
help: "Nothing from the gateway for 30 s: check its power, network, and that the program is running.",
|
|
1510
|
+
},
|
|
1511
|
+
};
|
|
1512
|
+
const line = byStatus[g.status] ?? byStatus.offline!;
|
|
1513
|
+
const st = g.status === "connected" ? g.state : null;
|
|
1514
|
+
const decoded = st
|
|
1515
|
+
? [
|
|
1516
|
+
st.currentFloor ? `Floor ${st.currentFloor}` : "",
|
|
1517
|
+
st.doorState != null ? DOOR_TEXT[st.doorState] ?? "" : "",
|
|
1518
|
+
st.motionState != null ? MOTION_TEXT[st.motionState] ?? "" : "",
|
|
1519
|
+
st.currentMode != null ? MODE_TEXT[st.currentMode] ?? "" : "",
|
|
1520
|
+
].filter(Boolean).join(" · ")
|
|
1521
|
+
: "";
|
|
1522
|
+
const registers = Object.entries((g.registers ?? {}) as Record<string, number>)
|
|
1523
|
+
.map(([reg, value]) => ({ reg, value }))
|
|
1524
|
+
.sort((a, b) => Number(a.reg) - Number(b.reg));
|
|
1525
|
+
return {
|
|
1526
|
+
...line,
|
|
1527
|
+
status: String(g.status ?? "offline"),
|
|
1528
|
+
connected: g.status === "connected",
|
|
1529
|
+
decoded,
|
|
1530
|
+
// Until the Inovance register list is in the gateway's settings, only raw numbers exist.
|
|
1531
|
+
undecoded: g.status === "connected" && !decoded,
|
|
1532
|
+
registers,
|
|
1533
|
+
port: String(g.port ?? ""),
|
|
1534
|
+
version: String(g.gatewayVersion ?? ""),
|
|
1535
|
+
codeHint: String(g.codeHint ?? ""),
|
|
1536
|
+
readings: Number(g.readings ?? 0),
|
|
1478
1537
|
};
|
|
1479
1538
|
}
|