kirby-ocpp 0.1.0 → 0.1.2

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 CHANGED
@@ -88,6 +88,7 @@ Global shortcuts:
88
88
  - `q`: quit.
89
89
 
90
90
  Forms prompt for values such as `idTag`, connector id, measurands, and `DataTransfer` payloads. There is no built-in default RFID/idTag.
91
+ On the Connector tab, `s` cycles the selected connector status and `S` opens a list of all OCPP connector statuses.
91
92
 
92
93
  ## Persistence
93
94
 
package/dist/cli.js CHANGED
File without changes
@@ -54,9 +54,14 @@ export class OcppClient extends EventEmitter {
54
54
  }
55
55
  this.rejectPending(new Error("Connection closed"));
56
56
  this.stopPingTimer();
57
- this.socket.removeAllListeners();
58
- if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) {
59
- this.socket.close();
57
+ const socket = this.socket;
58
+ socket.removeAllListeners();
59
+ socket.on("error", () => { });
60
+ if (socket.readyState === WebSocket.OPEN) {
61
+ socket.close();
62
+ }
63
+ else if (socket.readyState === WebSocket.CONNECTING) {
64
+ socket.terminate();
60
65
  }
61
66
  this.socket = undefined;
62
67
  }
package/dist/station.d.ts CHANGED
@@ -57,6 +57,7 @@ export declare class Station extends EventEmitter {
57
57
  setDiagnosticsOutcome(outcome: DiagnosticsOutcome): void;
58
58
  setFirmwareOutcome(outcome: FirmwareOutcome): void;
59
59
  cycleStatus(connectorId?: number): Promise<void>;
60
+ setStatus(connectorId: number | undefined, status: ConnectorStatus): Promise<void>;
60
61
  addConnector(): number;
61
62
  private bindClientEvents;
62
63
  private handleCentralSystemCall;
package/dist/station.js CHANGED
@@ -495,14 +495,17 @@ export class Station extends EventEmitter {
495
495
  async cycleStatus(connectorId = this.config.connectorId) {
496
496
  const connector = this.connectors.get(connectorId);
497
497
  const next = nextConnectorStatus(connector.status);
498
+ await this.setStatus(connectorId, next);
499
+ }
500
+ async setStatus(connectorId = this.config.connectorId, status) {
498
501
  this.connectors.patch(connectorId, {
499
- status: next,
500
- errorCode: next === "Faulted" ? "OtherError" : "NoError",
501
- info: next === "Faulted" ? "Manual fault" : undefined,
502
- vendorId: next === "Faulted" ? this.config.vendor : undefined,
502
+ status,
503
+ errorCode: status === "Faulted" ? "OtherError" : "NoError",
504
+ info: status === "Faulted" ? "Manual fault" : undefined,
505
+ vendorId: status === "Faulted" ? this.config.vendor : undefined,
503
506
  vendorErrorCode: undefined
504
507
  });
505
- await this.statusNotification(connectorId, next);
508
+ await this.statusNotification(connectorId, status);
506
509
  }
507
510
  addConnector() {
508
511
  const { id: nextId } = this.connectors.add();
package/dist/tui.d.ts CHANGED
@@ -49,6 +49,7 @@ export declare class Tui {
49
49
  private tabLabel;
50
50
  private togglePlug;
51
51
  private stopTransactionFlow;
52
+ private setStatusFlow;
52
53
  private startTransactionFlow;
53
54
  private customMeterValuesFlow;
54
55
  private dataTransferFlow;
package/dist/tui.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import blessed from "blessed";
3
+ import { CONNECTOR_STATUS_SEQUENCE } from "./station/connector-status.js";
3
4
  const TABS = [
4
5
  { id: "station", label: "Station" },
5
6
  { id: "connector", label: "Connector" },
@@ -10,7 +11,7 @@ const TABS = [
10
11
  { id: "scenarios", label: "Scenarios" }
11
12
  ];
12
13
  const LEVEL_STYLE = {
13
- info: "{blue-fg}info{/blue-fg}",
14
+ info: "{white-fg}info{/white-fg}",
14
15
  success: "{green-fg}ok{/green-fg}",
15
16
  warn: "{yellow-fg}warn{/yellow-fg}",
16
17
  error: "{red-fg}err{/red-fg}",
@@ -19,14 +20,27 @@ const LEVEL_STYLE = {
19
20
  };
20
21
  const DEFAULT_MEASURANDS = "Energy.Active.Import.Register,Power.Active.Import,Current.Import,Voltage,Temperature,SoC";
21
22
  const THEME = {
23
+ background: "black",
24
+ foreground: "white",
22
25
  panelBorder: "cyan",
23
26
  headerBg: "black",
24
27
  headerFg: "white",
25
28
  footerBg: "black",
26
29
  footerFg: "white",
30
+ mutedFg: "cyan",
27
31
  selectedBg: "white",
28
- selectedFg: "black"
32
+ selectedFg: "black",
33
+ focusBorder: "yellow"
29
34
  };
35
+ function fgTag(color, content) {
36
+ return `{${color}-fg}${content}{/${color}-fg}`;
37
+ }
38
+ function muted(content) {
39
+ return fgTag(THEME.mutedFg, content);
40
+ }
41
+ function disabledLine(content) {
42
+ return muted(`${content} (disabled)`);
43
+ }
30
44
  export class Tui {
31
45
  station;
32
46
  screen;
@@ -71,6 +85,8 @@ export class Tui {
71
85
  border: "line",
72
86
  padding: { left: 1, right: 1 },
73
87
  style: {
88
+ fg: THEME.foreground,
89
+ bg: THEME.background,
74
90
  selected: { bg: THEME.selectedBg, fg: THEME.selectedFg },
75
91
  border: { fg: THEME.panelBorder }
76
92
  }
@@ -85,6 +101,8 @@ export class Tui {
85
101
  border: "line",
86
102
  padding: { left: 1, right: 1 },
87
103
  style: {
104
+ fg: THEME.foreground,
105
+ bg: THEME.background,
88
106
  border: { fg: THEME.panelBorder }
89
107
  }
90
108
  });
@@ -100,6 +118,8 @@ export class Tui {
100
118
  border: "line",
101
119
  padding: { left: 1, right: 1 },
102
120
  style: {
121
+ fg: THEME.foreground,
122
+ bg: THEME.background,
103
123
  selected: { bg: THEME.selectedBg, fg: THEME.selectedFg },
104
124
  border: { fg: THEME.panelBorder }
105
125
  }
@@ -123,6 +143,8 @@ export class Tui {
123
143
  style: { bg: THEME.panelBorder }
124
144
  },
125
145
  style: {
146
+ fg: THEME.foreground,
147
+ bg: THEME.background,
126
148
  border: { fg: THEME.panelBorder }
127
149
  }
128
150
  });
@@ -281,7 +303,7 @@ export class Tui {
281
303
  const compact = this.screenColumns() < 110;
282
304
  const tabLine = TABS.map((tab, index) => {
283
305
  const label = compact ? `${index + 1}:${tab.label.slice(0, 4)}` : `${index + 1}:${tab.label}`;
284
- return tab.id === this.activeTab ? `{black-fg}{white-bg}${label}{/white-bg}{/black-fg}` : `{gray-fg}${label}{/gray-fg}`;
306
+ return tab.id === this.activeTab ? `{black-fg}{white-bg}${label}{/white-bg}{/black-fg}` : muted(label);
285
307
  }).join(" ");
286
308
  this.header.setContent([
287
309
  ` {bold}kirby-ocpp{/bold} ${this.station.config.chargePointId} -> ${this.station.config.centralSystemUrl}`,
@@ -357,9 +379,11 @@ export class Tui {
357
379
  this.actions = this.actionsForTab();
358
380
  const width = this.centerContentWidth();
359
381
  const items = this.actions.map((action) => {
360
- const prefix = action.disabled?.() ? "{gray-fg}" : "";
361
- const suffix = action.disabled?.() ? "{/gray-fg}" : "";
362
- return `${prefix}${action.key.padEnd(2)} ${truncateText(action.label, Math.max(12, width - 6))}${suffix}`;
382
+ const disabled = action.disabled?.() ?? false;
383
+ const suffix = disabled ? " (disabled)" : "";
384
+ const labelWidth = Math.max(12, width - 6 - suffix.length);
385
+ const line = `${action.key.padEnd(2)} ${truncateText(action.label, labelWidth)}`;
386
+ return disabled ? disabledLine(line) : line;
363
387
  });
364
388
  this.actionList.setLabel(` ${this.tabLabel(this.activeTab)} actions `);
365
389
  this.actionList.setItems(items);
@@ -394,6 +418,7 @@ export class Tui {
394
418
  return [
395
419
  action("p", "Plug / unplug EV", "Toggle EV plug state on the selected connector.", () => this.togglePlug()),
396
420
  action("s", "Cycle status", "Cycle StatusNotification status on the selected connector.", () => this.runStationTask(() => this.station.cycleStatus(this.selectedConnectorId))),
421
+ action("S", "Set status", "Choose any StatusNotification status for the selected connector.", () => this.setStatusFlow()),
397
422
  action("n", "Send StatusNotification", "Send current selected connector status now.", () => this.runStationTask(() => this.station.statusNotification(this.selectedConnectorId, this.selectedConnector()?.status, true))),
398
423
  action("f", "Fault / clear fault", "Inject or clear a fault on the selected connector.", () => this.faultFlow()),
399
424
  action("]", "Next connector", "Select next connector.", () => this.selectNextConnector()),
@@ -460,6 +485,8 @@ export class Tui {
460
485
  "d",
461
486
  "D",
462
487
  "S-d",
488
+ "S",
489
+ "S-s",
463
490
  "e",
464
491
  "f",
465
492
  "h",
@@ -542,6 +569,8 @@ export class Tui {
542
569
  tags: true,
543
570
  keys: true,
544
571
  style: {
572
+ fg: THEME.foreground,
573
+ bg: THEME.background,
545
574
  border: { fg: THEME.panelBorder }
546
575
  }
547
576
  });
@@ -555,6 +584,8 @@ export class Tui {
555
584
  keys: true,
556
585
  tags: true,
557
586
  style: {
587
+ fg: THEME.foreground,
588
+ bg: THEME.background,
558
589
  border: { fg: THEME.panelBorder }
559
590
  }
560
591
  });
@@ -570,6 +601,8 @@ export class Tui {
570
601
  vi: false,
571
602
  items: [],
572
603
  style: {
604
+ fg: THEME.foreground,
605
+ bg: THEME.background,
573
606
  selected: { bg: THEME.selectedBg, fg: THEME.selectedFg },
574
607
  border: { fg: THEME.panelBorder }
575
608
  }
@@ -581,7 +614,8 @@ export class Tui {
581
614
  right: 2,
582
615
  height: 1,
583
616
  tags: true,
584
- content: "{gray-fg}Type to filter Up/Down select Enter run Esc cancel{/gray-fg}"
617
+ style: { fg: THEME.foreground, bg: THEME.background },
618
+ content: muted("Type to filter Up/Down select Enter run Esc cancel")
585
619
  });
586
620
  let entries = [];
587
621
  let selectedIndex = 0;
@@ -608,7 +642,7 @@ export class Tui {
608
642
  selectedIndex = clamp(selectedIndex, 0, Math.max(0, entries.length - 1));
609
643
  box.setLabel(` Commands${trimmedQuery ? `: ${truncateText(trimmedQuery, 28)}` : ""} `);
610
644
  if (entries.length === 0) {
611
- list.setItems(["{gray-fg}No matching commands{/gray-fg}"]);
645
+ list.setItems([muted("No matching commands")]);
612
646
  list.select(0);
613
647
  this.screen.render();
614
648
  return;
@@ -697,6 +731,8 @@ export class Tui {
697
731
  alwaysScroll: false,
698
732
  padding: { left: 2, right: 2 },
699
733
  style: {
734
+ fg: THEME.foreground,
735
+ bg: THEME.background,
700
736
  border: { fg: THEME.panelBorder }
701
737
  },
702
738
  scrollbar: {
@@ -720,7 +756,7 @@ export class Tui {
720
756
  ` id=${this.station.config.chargePointId}`,
721
757
  ` url=${this.station.config.centralSystemUrl}`,
722
758
  "",
723
- "{gray-fg}Esc or q closes this help.{/gray-fg}"
759
+ muted("Esc or q closes this help.")
724
760
  ].join("\n"));
725
761
  const close = () => {
726
762
  box.destroy();
@@ -738,9 +774,8 @@ export class Tui {
738
774
  paletteEntryLine(entry) {
739
775
  const tab = this.tabLabel(entry.tab).padEnd(11);
740
776
  const key = entry.action.key.padEnd(2);
741
- const state = entry.action.disabled?.() ? "{gray-fg}" : "";
742
- const endState = entry.action.disabled?.() ? "{/gray-fg}" : "";
743
- return `${state}${tab} ${key} ${entry.action.label}${endState}`;
777
+ const line = `${tab} ${key} ${entry.action.label}`;
778
+ return entry.action.disabled?.() ? disabledLine(line) : line;
744
779
  }
745
780
  paletteEntries(query) {
746
781
  const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
@@ -798,6 +833,9 @@ export class Tui {
798
833
  stopTransactionFlow() {
799
834
  this.promptChoice("StopTransaction reason", STOP_REASONS, (reason) => this.runStationTask(() => this.station.stopTransaction(this.selectedConnectorId, readStopReason(reason), this.selectedConnector()?.lastIdTag)));
800
835
  }
836
+ setStatusFlow() {
837
+ this.promptChoice("Connector status", CONNECTOR_STATUS_SEQUENCE, (status) => this.runStationTask(() => this.station.setStatus(this.selectedConnectorId, readConnectorStatus(status))));
838
+ }
801
839
  startTransactionFlow() {
802
840
  this.promptForm("StartTransaction", [
803
841
  { name: "connectorId", label: "connectorId", initialValue: String(this.selectedConnectorId) },
@@ -956,6 +994,8 @@ export class Tui {
956
994
  tags: true,
957
995
  keys: true,
958
996
  style: {
997
+ fg: THEME.foreground,
998
+ bg: THEME.background,
959
999
  border: { fg: THEME.panelBorder }
960
1000
  }
961
1001
  });
@@ -972,6 +1012,8 @@ export class Tui {
972
1012
  keys: true,
973
1013
  tags: true,
974
1014
  style: {
1015
+ fg: THEME.foreground,
1016
+ bg: THEME.background,
975
1017
  border: { fg: THEME.panelBorder }
976
1018
  }
977
1019
  }));
@@ -982,7 +1024,8 @@ export class Tui {
982
1024
  right: 2,
983
1025
  height: 1,
984
1026
  tags: true,
985
- content: "{gray-fg}Tab/Up/Down field Left/Right cursor C-u clear Enter submit Esc cancel{/gray-fg}"
1027
+ style: { fg: THEME.foreground, bg: THEME.background },
1028
+ content: muted("Tab/Up/Down field Left/Right cursor C-u clear Enter submit Esc cancel")
986
1029
  });
987
1030
  let focusedIndex = 0;
988
1031
  let closed = false;
@@ -995,7 +1038,7 @@ export class Tui {
995
1038
  const value = values[index] ?? "";
996
1039
  const cursor = clamp(cursors[index] ?? value.length, 0, value.length);
997
1040
  input.setContent(renderFieldValue(value, cursor, width, focusedIndex === index));
998
- input.style.border = { fg: focusedIndex === index ? "yellow" : THEME.panelBorder };
1041
+ input.style.border = { fg: focusedIndex === index ? THEME.focusBorder : THEME.panelBorder };
999
1042
  };
1000
1043
  const renderInputs = () => {
1001
1044
  inputBoxes.forEach((_input, index) => renderInput(index));
@@ -1128,6 +1171,8 @@ export class Tui {
1128
1171
  vi: true,
1129
1172
  items: choices.map((choice) => ` ${choice}`),
1130
1173
  style: {
1174
+ fg: THEME.foreground,
1175
+ bg: THEME.background,
1131
1176
  selected: { bg: THEME.selectedBg, fg: THEME.selectedFg },
1132
1177
  border: { fg: THEME.panelBorder }
1133
1178
  }
@@ -1205,12 +1250,12 @@ export class Tui {
1205
1250
  alwaysScroll: false,
1206
1251
  content: this.plainLogContent(),
1207
1252
  style: {
1208
- fg: "white",
1209
- bg: "black"
1253
+ fg: THEME.foreground,
1254
+ bg: THEME.background
1210
1255
  },
1211
1256
  scrollbar: {
1212
1257
  ch: " ",
1213
- style: { bg: "cyan" }
1258
+ style: { bg: THEME.panelBorder }
1214
1259
  }
1215
1260
  });
1216
1261
  this.bindLogNavigationKeys(this.plainLogView);
@@ -1275,7 +1320,7 @@ export class Tui {
1275
1320
  appendLogEntry(entry) {
1276
1321
  this.logBox.log(this.formatLogEntryForTui(entry));
1277
1322
  if (entry.details) {
1278
- this.logBox.log(` {gray-fg}${entry.details}{/gray-fg}`);
1323
+ this.logBox.log(` ${muted(entry.details)}`);
1279
1324
  }
1280
1325
  }
1281
1326
  filteredLogs() {
@@ -1432,6 +1477,10 @@ function readStopReason(value) {
1432
1477
  const normalized = value.trim();
1433
1478
  return isStopReason(normalized) ? normalized : "Local";
1434
1479
  }
1480
+ function readConnectorStatus(value) {
1481
+ const normalized = value.trim();
1482
+ return isConnectorStatus(normalized) ? normalized : "Available";
1483
+ }
1435
1484
  function parseObjectJson(value) {
1436
1485
  try {
1437
1486
  const parsed = JSON.parse(value);
@@ -1503,6 +1552,9 @@ function normalizeActionKey(keyName) {
1503
1552
  if (keyName === "S-d") {
1504
1553
  return "D";
1505
1554
  }
1555
+ if (keyName === "S-s") {
1556
+ return "S";
1557
+ }
1506
1558
  if (keyName === "S-u") {
1507
1559
  return "U";
1508
1560
  }
@@ -1515,7 +1567,7 @@ function legacyActionTab(key) {
1515
1567
  if (["b", "h", "x"].includes(key)) {
1516
1568
  return "station";
1517
1569
  }
1518
- if (["p", "s", "n", "f"].includes(key)) {
1570
+ if (["p", "s", "S", "n", "f"].includes(key)) {
1519
1571
  return "connector";
1520
1572
  }
1521
1573
  if (["a", "t", "e", "m", "M"].includes(key)) {
@@ -1554,6 +1606,9 @@ const STOP_REASONS = [
1554
1606
  function isStopReason(value) {
1555
1607
  return STOP_REASONS.includes(value);
1556
1608
  }
1609
+ function isConnectorStatus(value) {
1610
+ return CONNECTOR_STATUS_SEQUENCE.includes(value);
1611
+ }
1557
1612
  const CHARGE_POINT_ERROR_CODES = [
1558
1613
  "ConnectorLockFailure",
1559
1614
  "EVCommunicationError",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kirby-ocpp",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A terminal OCPP charge point emulator.",
5
5
  "type": "module",
6
6
  "author": "archhaze24",