jmri-client 5.0.0 → 5.1.0

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.
@@ -723,8 +723,8 @@ var mockData = {
723
723
  { "name": "F4", "label": "Dynamic Brake", "lockable": true, "icon": null, "selectedIcon": null },
724
724
  { "name": "F5", "label": null, "lockable": false, "icon": null, "selectedIcon": null }
725
725
  ],
726
- "attributes": [],
727
- "rosterGroups": []
726
+ "attributes": [{ "name": "RosterGroup:diesels", "value": "yes" }],
727
+ "rosterGroups": ["diesels"]
728
728
  },
729
729
  "id": 1
730
730
  },
@@ -754,8 +754,8 @@ var mockData = {
754
754
  { "name": "F3", "label": "Steam", "lockable": true, "icon": null, "selectedIcon": null },
755
755
  { "name": "F4", "label": null, "lockable": false, "icon": null, "selectedIcon": null }
756
756
  ],
757
- "attributes": [],
758
- "rosterGroups": []
757
+ "attributes": [{ "name": "RosterGroup:steam", "value": "yes" }],
758
+ "rosterGroups": ["steam"]
759
759
  },
760
760
  "id": 2
761
761
  },
@@ -786,13 +786,19 @@ var mockData = {
786
786
  { "name": "F4", "label": null, "lockable": false, "icon": null, "selectedIcon": null },
787
787
  { "name": "F5", "label": "Mars Light", "lockable": true, "icon": null, "selectedIcon": null }
788
788
  ],
789
- "attributes": [],
790
- "rosterGroups": []
789
+ "attributes": [{ "name": "RosterGroup:diesels", "value": "yes" }],
790
+ "rosterGroups": ["diesels"]
791
791
  },
792
792
  "id": 3
793
793
  }
794
794
  ]
795
795
  },
796
+ "rosterGroup": {
797
+ "list": [
798
+ { "type": "rosterGroup", "data": { "name": "diesels", "length": 2 } },
799
+ { "type": "rosterGroup", "data": { "name": "steam", "length": 1 } }
800
+ ]
801
+ },
796
802
  "throttle": {
797
803
  "acquire": {
798
804
  "success": {
@@ -891,6 +897,7 @@ var mockData = {
891
897
  // src/mocks/mock-response-manager.ts
892
898
  var MockResponseManager = class {
893
899
  constructor(options = {}) {
900
+ this.powerStateByPrefix = /* @__PURE__ */ new Map();
894
901
  this.throttles = /* @__PURE__ */ new Map();
895
902
  this.lights = /* @__PURE__ */ new Map([
896
903
  ["IL1", 4 /* OFF */],
@@ -919,6 +926,8 @@ var MockResponseManager = class {
919
926
  return this.getPowerResponse(message);
920
927
  case "roster":
921
928
  return this.getRosterResponse(message);
929
+ case "rosterGroup":
930
+ return this.getRosterGroupResponse();
922
931
  case "throttle":
923
932
  return this.getThrottleResponse(message);
924
933
  case "light":
@@ -940,34 +949,48 @@ var MockResponseManager = class {
940
949
  return JSON.parse(JSON.stringify(mockData.hello));
941
950
  }
942
951
  /**
943
- * Get power response
952
+ * Get power response, with optional per-prefix state tracking
944
953
  */
945
954
  getPowerResponse(message) {
946
- if (message.data?.state !== void 0) {
955
+ const prefix = message.data?.prefix;
956
+ if (message.method === "post" && message.data?.state !== void 0) {
957
+ if (prefix !== void 0) {
958
+ this.powerStateByPrefix.set(prefix, message.data.state);
959
+ return { type: "power", data: { state: message.data.state, prefix } };
960
+ }
947
961
  this.powerState = message.data.state;
948
- return {
949
- type: "power",
950
- data: { state: this.powerState }
951
- };
962
+ return { type: "power", data: { state: this.powerState } };
952
963
  }
953
- return {
954
- type: "power",
955
- data: { state: this.powerState }
956
- };
964
+ if (prefix !== void 0) {
965
+ const state = this.powerStateByPrefix.get(prefix) ?? 0 /* UNKNOWN */;
966
+ return { type: "power", data: { state, prefix } };
967
+ }
968
+ return { type: "power", data: { state: this.powerState } };
957
969
  }
958
970
  /**
959
- * Get roster response
971
+ * Get roster response, optionally filtered by group
960
972
  */
961
973
  getRosterResponse(message) {
962
974
  if (message.type === "roster" && message.method === "list") {
963
- return {
964
- type: "roster",
965
- data: JSON.parse(JSON.stringify(mockData.roster.list))
966
- };
975
+ const all = JSON.parse(JSON.stringify(mockData.roster.list));
976
+ const group = message.params?.group;
977
+ if (group) {
978
+ return {
979
+ type: "roster",
980
+ data: all.filter((e) => e.data.rosterGroups?.includes(group))
981
+ };
982
+ }
983
+ return { type: "roster", data: all };
967
984
  }
985
+ return { type: "roster", data: [] };
986
+ }
987
+ /**
988
+ * Get roster group response
989
+ */
990
+ getRosterGroupResponse() {
968
991
  return {
969
- type: "roster",
970
- data: []
992
+ type: "rosterGroup",
993
+ data: JSON.parse(JSON.stringify(mockData.rosterGroup.list))
971
994
  };
972
995
  }
973
996
  /**
@@ -986,13 +1009,11 @@ var MockResponseManager = class {
986
1009
  F1: false,
987
1010
  F2: false,
988
1011
  F3: false,
989
- F4: false
1012
+ F4: false,
1013
+ ...data.prefix !== void 0 && { prefix: data.prefix }
990
1014
  };
991
1015
  this.throttles.set(throttleId, throttleState);
992
- return {
993
- type: "throttle",
994
- data: { ...throttleState }
995
- };
1016
+ return { type: "throttle", data: { ...throttleState } };
996
1017
  }
997
1018
  if (data.release !== void 0 && data.throttle) {
998
1019
  this.throttles.delete(data.throttle);
@@ -1125,6 +1146,7 @@ var MockResponseManager = class {
1125
1146
  */
1126
1147
  reset() {
1127
1148
  this.powerState = 4 /* OFF */;
1149
+ this.powerStateByPrefix.clear();
1128
1150
  this.throttles.clear();
1129
1151
  this.lights = /* @__PURE__ */ new Map([
1130
1152
  ["IL1", 4 /* OFF */],
@@ -1608,6 +1630,32 @@ var RosterManager = class {
1608
1630
  }
1609
1631
  return results;
1610
1632
  }
1633
+ /**
1634
+ * Get all roster groups
1635
+ */
1636
+ async getRosterGroups() {
1637
+ const message = {
1638
+ type: "rosterGroup",
1639
+ method: "list"
1640
+ };
1641
+ const response = await this.client.request(message);
1642
+ if (!response.data) return [];
1643
+ return response.data.map((wrapper) => wrapper.data);
1644
+ }
1645
+ /**
1646
+ * Get roster entries belonging to a specific group
1647
+ */
1648
+ async getRosterEntriesByGroup(group) {
1649
+ const message = {
1650
+ type: "roster",
1651
+ method: "list",
1652
+ params: { group }
1653
+ };
1654
+ const response = await this.client.request(message);
1655
+ if (!response.data) return [];
1656
+ const entries = response.data;
1657
+ return entries.filter((e) => e.type === "rosterEntry");
1658
+ }
1611
1659
  /**
1612
1660
  * Get cached roster (no network request)
1613
1661
  */
@@ -2315,6 +2363,18 @@ var JmriClient = class extends import_index.default {
2315
2363
  async searchRoster(query) {
2316
2364
  return this.rosterManager.searchRoster(query);
2317
2365
  }
2366
+ /**
2367
+ * Get all roster groups
2368
+ */
2369
+ async getRosterGroups() {
2370
+ return this.rosterManager.getRosterGroups();
2371
+ }
2372
+ /**
2373
+ * Get roster entries belonging to a specific group
2374
+ */
2375
+ async getRosterEntriesByGroup(group) {
2376
+ return this.rosterManager.getRosterEntriesByGroup(group);
2377
+ }
2318
2378
  // ============================================================================
2319
2379
  // Turnout Control
2320
2380
  // ============================================================================
package/dist/cjs/index.js CHANGED
@@ -581,8 +581,8 @@ var mockData = {
581
581
  { "name": "F4", "label": "Dynamic Brake", "lockable": true, "icon": null, "selectedIcon": null },
582
582
  { "name": "F5", "label": null, "lockable": false, "icon": null, "selectedIcon": null }
583
583
  ],
584
- "attributes": [],
585
- "rosterGroups": []
584
+ "attributes": [{ "name": "RosterGroup:diesels", "value": "yes" }],
585
+ "rosterGroups": ["diesels"]
586
586
  },
587
587
  "id": 1
588
588
  },
@@ -612,8 +612,8 @@ var mockData = {
612
612
  { "name": "F3", "label": "Steam", "lockable": true, "icon": null, "selectedIcon": null },
613
613
  { "name": "F4", "label": null, "lockable": false, "icon": null, "selectedIcon": null }
614
614
  ],
615
- "attributes": [],
616
- "rosterGroups": []
615
+ "attributes": [{ "name": "RosterGroup:steam", "value": "yes" }],
616
+ "rosterGroups": ["steam"]
617
617
  },
618
618
  "id": 2
619
619
  },
@@ -644,13 +644,19 @@ var mockData = {
644
644
  { "name": "F4", "label": null, "lockable": false, "icon": null, "selectedIcon": null },
645
645
  { "name": "F5", "label": "Mars Light", "lockable": true, "icon": null, "selectedIcon": null }
646
646
  ],
647
- "attributes": [],
648
- "rosterGroups": []
647
+ "attributes": [{ "name": "RosterGroup:diesels", "value": "yes" }],
648
+ "rosterGroups": ["diesels"]
649
649
  },
650
650
  "id": 3
651
651
  }
652
652
  ]
653
653
  },
654
+ "rosterGroup": {
655
+ "list": [
656
+ { "type": "rosterGroup", "data": { "name": "diesels", "length": 2 } },
657
+ { "type": "rosterGroup", "data": { "name": "steam", "length": 1 } }
658
+ ]
659
+ },
654
660
  "throttle": {
655
661
  "acquire": {
656
662
  "success": {
@@ -749,6 +755,7 @@ var mockData = {
749
755
  // src/mocks/mock-response-manager.ts
750
756
  var MockResponseManager = class {
751
757
  constructor(options = {}) {
758
+ this.powerStateByPrefix = /* @__PURE__ */ new Map();
752
759
  this.throttles = /* @__PURE__ */ new Map();
753
760
  this.lights = /* @__PURE__ */ new Map([
754
761
  ["IL1", 4 /* OFF */],
@@ -777,6 +784,8 @@ var MockResponseManager = class {
777
784
  return this.getPowerResponse(message);
778
785
  case "roster":
779
786
  return this.getRosterResponse(message);
787
+ case "rosterGroup":
788
+ return this.getRosterGroupResponse();
780
789
  case "throttle":
781
790
  return this.getThrottleResponse(message);
782
791
  case "light":
@@ -798,34 +807,48 @@ var MockResponseManager = class {
798
807
  return JSON.parse(JSON.stringify(mockData.hello));
799
808
  }
800
809
  /**
801
- * Get power response
810
+ * Get power response, with optional per-prefix state tracking
802
811
  */
803
812
  getPowerResponse(message) {
804
- if (message.data?.state !== void 0) {
813
+ const prefix = message.data?.prefix;
814
+ if (message.method === "post" && message.data?.state !== void 0) {
815
+ if (prefix !== void 0) {
816
+ this.powerStateByPrefix.set(prefix, message.data.state);
817
+ return { type: "power", data: { state: message.data.state, prefix } };
818
+ }
805
819
  this.powerState = message.data.state;
806
- return {
807
- type: "power",
808
- data: { state: this.powerState }
809
- };
820
+ return { type: "power", data: { state: this.powerState } };
810
821
  }
811
- return {
812
- type: "power",
813
- data: { state: this.powerState }
814
- };
822
+ if (prefix !== void 0) {
823
+ const state = this.powerStateByPrefix.get(prefix) ?? 0 /* UNKNOWN */;
824
+ return { type: "power", data: { state, prefix } };
825
+ }
826
+ return { type: "power", data: { state: this.powerState } };
815
827
  }
816
828
  /**
817
- * Get roster response
829
+ * Get roster response, optionally filtered by group
818
830
  */
819
831
  getRosterResponse(message) {
820
832
  if (message.type === "roster" && message.method === "list") {
821
- return {
822
- type: "roster",
823
- data: JSON.parse(JSON.stringify(mockData.roster.list))
824
- };
833
+ const all = JSON.parse(JSON.stringify(mockData.roster.list));
834
+ const group = message.params?.group;
835
+ if (group) {
836
+ return {
837
+ type: "roster",
838
+ data: all.filter((e) => e.data.rosterGroups?.includes(group))
839
+ };
840
+ }
841
+ return { type: "roster", data: all };
825
842
  }
843
+ return { type: "roster", data: [] };
844
+ }
845
+ /**
846
+ * Get roster group response
847
+ */
848
+ getRosterGroupResponse() {
826
849
  return {
827
- type: "roster",
828
- data: []
850
+ type: "rosterGroup",
851
+ data: JSON.parse(JSON.stringify(mockData.rosterGroup.list))
829
852
  };
830
853
  }
831
854
  /**
@@ -844,13 +867,11 @@ var MockResponseManager = class {
844
867
  F1: false,
845
868
  F2: false,
846
869
  F3: false,
847
- F4: false
870
+ F4: false,
871
+ ...data.prefix !== void 0 && { prefix: data.prefix }
848
872
  };
849
873
  this.throttles.set(throttleId, throttleState);
850
- return {
851
- type: "throttle",
852
- data: { ...throttleState }
853
- };
874
+ return { type: "throttle", data: { ...throttleState } };
854
875
  }
855
876
  if (data.release !== void 0 && data.throttle) {
856
877
  this.throttles.delete(data.throttle);
@@ -983,6 +1004,7 @@ var MockResponseManager = class {
983
1004
  */
984
1005
  reset() {
985
1006
  this.powerState = 4 /* OFF */;
1007
+ this.powerStateByPrefix.clear();
986
1008
  this.throttles.clear();
987
1009
  this.lights = /* @__PURE__ */ new Map([
988
1010
  ["IL1", 4 /* OFF */],
@@ -1467,6 +1489,32 @@ var RosterManager = class {
1467
1489
  }
1468
1490
  return results;
1469
1491
  }
1492
+ /**
1493
+ * Get all roster groups
1494
+ */
1495
+ async getRosterGroups() {
1496
+ const message = {
1497
+ type: "rosterGroup",
1498
+ method: "list"
1499
+ };
1500
+ const response = await this.client.request(message);
1501
+ if (!response.data) return [];
1502
+ return response.data.map((wrapper) => wrapper.data);
1503
+ }
1504
+ /**
1505
+ * Get roster entries belonging to a specific group
1506
+ */
1507
+ async getRosterEntriesByGroup(group) {
1508
+ const message = {
1509
+ type: "roster",
1510
+ method: "list",
1511
+ params: { group }
1512
+ };
1513
+ const response = await this.client.request(message);
1514
+ if (!response.data) return [];
1515
+ const entries = response.data;
1516
+ return entries.filter((e) => e.type === "rosterEntry");
1517
+ }
1470
1518
  /**
1471
1519
  * Get cached roster (no network request)
1472
1520
  */
@@ -2179,6 +2227,18 @@ var JmriClient = class extends import_eventemitter310.EventEmitter {
2179
2227
  async searchRoster(query) {
2180
2228
  return this.rosterManager.searchRoster(query);
2181
2229
  }
2230
+ /**
2231
+ * Get all roster groups
2232
+ */
2233
+ async getRosterGroups() {
2234
+ return this.rosterManager.getRosterGroups();
2235
+ }
2236
+ /**
2237
+ * Get roster entries belonging to a specific group
2238
+ */
2239
+ async getRosterEntriesByGroup(group) {
2240
+ return this.rosterManager.getRosterEntriesByGroup(group);
2241
+ }
2182
2242
  // ============================================================================
2183
2243
  // Turnout Control
2184
2244
  // ============================================================================
package/dist/esm/index.js CHANGED
@@ -532,8 +532,8 @@ var mockData = {
532
532
  { "name": "F4", "label": "Dynamic Brake", "lockable": true, "icon": null, "selectedIcon": null },
533
533
  { "name": "F5", "label": null, "lockable": false, "icon": null, "selectedIcon": null }
534
534
  ],
535
- "attributes": [],
536
- "rosterGroups": []
535
+ "attributes": [{ "name": "RosterGroup:diesels", "value": "yes" }],
536
+ "rosterGroups": ["diesels"]
537
537
  },
538
538
  "id": 1
539
539
  },
@@ -563,8 +563,8 @@ var mockData = {
563
563
  { "name": "F3", "label": "Steam", "lockable": true, "icon": null, "selectedIcon": null },
564
564
  { "name": "F4", "label": null, "lockable": false, "icon": null, "selectedIcon": null }
565
565
  ],
566
- "attributes": [],
567
- "rosterGroups": []
566
+ "attributes": [{ "name": "RosterGroup:steam", "value": "yes" }],
567
+ "rosterGroups": ["steam"]
568
568
  },
569
569
  "id": 2
570
570
  },
@@ -595,13 +595,19 @@ var mockData = {
595
595
  { "name": "F4", "label": null, "lockable": false, "icon": null, "selectedIcon": null },
596
596
  { "name": "F5", "label": "Mars Light", "lockable": true, "icon": null, "selectedIcon": null }
597
597
  ],
598
- "attributes": [],
599
- "rosterGroups": []
598
+ "attributes": [{ "name": "RosterGroup:diesels", "value": "yes" }],
599
+ "rosterGroups": ["diesels"]
600
600
  },
601
601
  "id": 3
602
602
  }
603
603
  ]
604
604
  },
605
+ "rosterGroup": {
606
+ "list": [
607
+ { "type": "rosterGroup", "data": { "name": "diesels", "length": 2 } },
608
+ { "type": "rosterGroup", "data": { "name": "steam", "length": 1 } }
609
+ ]
610
+ },
605
611
  "throttle": {
606
612
  "acquire": {
607
613
  "success": {
@@ -700,6 +706,7 @@ var mockData = {
700
706
  // src/mocks/mock-response-manager.ts
701
707
  var MockResponseManager = class {
702
708
  constructor(options = {}) {
709
+ this.powerStateByPrefix = /* @__PURE__ */ new Map();
703
710
  this.throttles = /* @__PURE__ */ new Map();
704
711
  this.lights = /* @__PURE__ */ new Map([
705
712
  ["IL1", 4 /* OFF */],
@@ -728,6 +735,8 @@ var MockResponseManager = class {
728
735
  return this.getPowerResponse(message);
729
736
  case "roster":
730
737
  return this.getRosterResponse(message);
738
+ case "rosterGroup":
739
+ return this.getRosterGroupResponse();
731
740
  case "throttle":
732
741
  return this.getThrottleResponse(message);
733
742
  case "light":
@@ -749,34 +758,48 @@ var MockResponseManager = class {
749
758
  return JSON.parse(JSON.stringify(mockData.hello));
750
759
  }
751
760
  /**
752
- * Get power response
761
+ * Get power response, with optional per-prefix state tracking
753
762
  */
754
763
  getPowerResponse(message) {
755
- if (message.data?.state !== void 0) {
764
+ const prefix = message.data?.prefix;
765
+ if (message.method === "post" && message.data?.state !== void 0) {
766
+ if (prefix !== void 0) {
767
+ this.powerStateByPrefix.set(prefix, message.data.state);
768
+ return { type: "power", data: { state: message.data.state, prefix } };
769
+ }
756
770
  this.powerState = message.data.state;
757
- return {
758
- type: "power",
759
- data: { state: this.powerState }
760
- };
771
+ return { type: "power", data: { state: this.powerState } };
761
772
  }
762
- return {
763
- type: "power",
764
- data: { state: this.powerState }
765
- };
773
+ if (prefix !== void 0) {
774
+ const state = this.powerStateByPrefix.get(prefix) ?? 0 /* UNKNOWN */;
775
+ return { type: "power", data: { state, prefix } };
776
+ }
777
+ return { type: "power", data: { state: this.powerState } };
766
778
  }
767
779
  /**
768
- * Get roster response
780
+ * Get roster response, optionally filtered by group
769
781
  */
770
782
  getRosterResponse(message) {
771
783
  if (message.type === "roster" && message.method === "list") {
772
- return {
773
- type: "roster",
774
- data: JSON.parse(JSON.stringify(mockData.roster.list))
775
- };
784
+ const all = JSON.parse(JSON.stringify(mockData.roster.list));
785
+ const group = message.params?.group;
786
+ if (group) {
787
+ return {
788
+ type: "roster",
789
+ data: all.filter((e) => e.data.rosterGroups?.includes(group))
790
+ };
791
+ }
792
+ return { type: "roster", data: all };
776
793
  }
794
+ return { type: "roster", data: [] };
795
+ }
796
+ /**
797
+ * Get roster group response
798
+ */
799
+ getRosterGroupResponse() {
777
800
  return {
778
- type: "roster",
779
- data: []
801
+ type: "rosterGroup",
802
+ data: JSON.parse(JSON.stringify(mockData.rosterGroup.list))
780
803
  };
781
804
  }
782
805
  /**
@@ -795,13 +818,11 @@ var MockResponseManager = class {
795
818
  F1: false,
796
819
  F2: false,
797
820
  F3: false,
798
- F4: false
821
+ F4: false,
822
+ ...data.prefix !== void 0 && { prefix: data.prefix }
799
823
  };
800
824
  this.throttles.set(throttleId, throttleState);
801
- return {
802
- type: "throttle",
803
- data: { ...throttleState }
804
- };
825
+ return { type: "throttle", data: { ...throttleState } };
805
826
  }
806
827
  if (data.release !== void 0 && data.throttle) {
807
828
  this.throttles.delete(data.throttle);
@@ -934,6 +955,7 @@ var MockResponseManager = class {
934
955
  */
935
956
  reset() {
936
957
  this.powerState = 4 /* OFF */;
958
+ this.powerStateByPrefix.clear();
937
959
  this.throttles.clear();
938
960
  this.lights = /* @__PURE__ */ new Map([
939
961
  ["IL1", 4 /* OFF */],
@@ -1418,6 +1440,32 @@ var RosterManager = class {
1418
1440
  }
1419
1441
  return results;
1420
1442
  }
1443
+ /**
1444
+ * Get all roster groups
1445
+ */
1446
+ async getRosterGroups() {
1447
+ const message = {
1448
+ type: "rosterGroup",
1449
+ method: "list"
1450
+ };
1451
+ const response = await this.client.request(message);
1452
+ if (!response.data) return [];
1453
+ return response.data.map((wrapper) => wrapper.data);
1454
+ }
1455
+ /**
1456
+ * Get roster entries belonging to a specific group
1457
+ */
1458
+ async getRosterEntriesByGroup(group) {
1459
+ const message = {
1460
+ type: "roster",
1461
+ method: "list",
1462
+ params: { group }
1463
+ };
1464
+ const response = await this.client.request(message);
1465
+ if (!response.data) return [];
1466
+ const entries = response.data;
1467
+ return entries.filter((e) => e.type === "rosterEntry");
1468
+ }
1421
1469
  /**
1422
1470
  * Get cached roster (no network request)
1423
1471
  */
@@ -2130,6 +2178,18 @@ var JmriClient = class extends EventEmitter10 {
2130
2178
  async searchRoster(query) {
2131
2179
  return this.rosterManager.searchRoster(query);
2132
2180
  }
2181
+ /**
2182
+ * Get all roster groups
2183
+ */
2184
+ async getRosterGroups() {
2185
+ return this.rosterManager.getRosterGroups();
2186
+ }
2187
+ /**
2188
+ * Get roster entries belonging to a specific group
2189
+ */
2190
+ async getRosterEntriesByGroup(group) {
2191
+ return this.rosterManager.getRosterEntriesByGroup(group);
2192
+ }
2133
2193
  // ============================================================================
2134
2194
  // Turnout Control
2135
2195
  // ============================================================================
@@ -4,7 +4,7 @@
4
4
  import { EventEmitter } from 'eventemitter3';
5
5
  import { WebSocketClient } from './core/websocket-client.js';
6
6
  import { PartialClientOptions } from './types/client-options.js';
7
- import { PowerState, RosterEntryWrapper, TurnoutState, TurnoutData, LightState, LightData, SystemConnectionData } from './types/jmri-messages.js';
7
+ import { PowerState, RosterEntryWrapper, RosterGroup, TurnoutState, TurnoutData, LightState, LightData, SystemConnectionData } from './types/jmri-messages.js';
8
8
  import { ConnectionState } from './types/events.js';
9
9
  import { ThrottleAcquireOptions, ThrottleFunctionKey, ThrottleState } from './types/throttle.js';
10
10
  /**
@@ -104,6 +104,14 @@ export declare class JmriClient extends EventEmitter {
104
104
  * Search roster by partial name match
105
105
  */
106
106
  searchRoster(query: string): Promise<RosterEntryWrapper[]>;
107
+ /**
108
+ * Get all roster groups
109
+ */
110
+ getRosterGroups(): Promise<RosterGroup[]>;
111
+ /**
112
+ * Get roster entries belonging to a specific group
113
+ */
114
+ getRosterEntriesByGroup(group: string): Promise<RosterEntryWrapper[]>;
107
115
  /**
108
116
  * Get the current state of a turnout
109
117
  */
@@ -4,7 +4,7 @@
4
4
  */
5
5
  export { JmriClient } from './client.js';
6
6
  export { WebSocketClient } from './core/websocket-client.js';
7
- export { JmriClientOptions, PartialClientOptions, ReconnectionOptions, HeartbeatOptions, MockOptions, PowerState, TurnoutState, LightState, RosterEntry, TurnoutData, LightData, JmriMessage, PowerMessage, TurnoutMessage, LightMessage, ThrottleMessage, RosterMessage, ConnectionState, EventPayloads, ThrottleAcquireOptions, ThrottleFunctionKey, ThrottleState } from './types/index.js';
7
+ export { JmriClientOptions, PartialClientOptions, ReconnectionOptions, HeartbeatOptions, MockOptions, PowerState, TurnoutState, LightState, RosterEntry, RosterGroup, TurnoutData, LightData, JmriMessage, PowerMessage, TurnoutMessage, LightMessage, ThrottleMessage, RosterMessage, ConnectionState, EventPayloads, ThrottleAcquireOptions, ThrottleFunctionKey, ThrottleState } from './types/index.js';
8
8
  export { isThrottleFunctionKey, isValidSpeed } from './types/throttle.js';
9
9
  export { powerStateToString, turnoutStateToString, lightStateToString } from './types/jmri-messages.js';
10
10
  export { MockResponseManager, mockResponseManager, mockData } from './mocks/index.js';
@@ -2,7 +2,7 @@
2
2
  * Roster management
3
3
  */
4
4
  import { WebSocketClient } from '../core/websocket-client.js';
5
- import { RosterEntryWrapper } from '../types/jmri-messages.js';
5
+ import { RosterEntryWrapper, RosterGroup } from '../types/jmri-messages.js';
6
6
  /**
7
7
  * Manages locomotive roster
8
8
  */
@@ -26,6 +26,14 @@ export declare class RosterManager {
26
26
  * Search roster by partial name match
27
27
  */
28
28
  searchRoster(query: string): Promise<RosterEntryWrapper[]>;
29
+ /**
30
+ * Get all roster groups
31
+ */
32
+ getRosterGroups(): Promise<RosterGroup[]>;
33
+ /**
34
+ * Get roster entries belonging to a specific group
35
+ */
36
+ getRosterEntriesByGroup(group: string): Promise<RosterEntryWrapper[]>;
29
37
  /**
30
38
  * Get cached roster (no network request)
31
39
  */
@@ -96,8 +96,11 @@ export declare const mockData: {
96
96
  readonly icon: null;
97
97
  readonly selectedIcon: null;
98
98
  }];
99
- readonly attributes: readonly [];
100
- readonly rosterGroups: readonly [];
99
+ readonly attributes: readonly [{
100
+ readonly name: "RosterGroup:diesels";
101
+ readonly value: "yes";
102
+ }];
103
+ readonly rosterGroups: readonly ["diesels"];
101
104
  };
102
105
  readonly id: 1;
103
106
  }, {
@@ -150,8 +153,11 @@ export declare const mockData: {
150
153
  readonly icon: null;
151
154
  readonly selectedIcon: null;
152
155
  }];
153
- readonly attributes: readonly [];
154
- readonly rosterGroups: readonly [];
156
+ readonly attributes: readonly [{
157
+ readonly name: "RosterGroup:steam";
158
+ readonly value: "yes";
159
+ }];
160
+ readonly rosterGroups: readonly ["steam"];
155
161
  };
156
162
  readonly id: 2;
157
163
  }, {
@@ -210,12 +216,30 @@ export declare const mockData: {
210
216
  readonly icon: null;
211
217
  readonly selectedIcon: null;
212
218
  }];
213
- readonly attributes: readonly [];
214
- readonly rosterGroups: readonly [];
219
+ readonly attributes: readonly [{
220
+ readonly name: "RosterGroup:diesels";
221
+ readonly value: "yes";
222
+ }];
223
+ readonly rosterGroups: readonly ["diesels"];
215
224
  };
216
225
  readonly id: 3;
217
226
  }];
218
227
  };
228
+ readonly rosterGroup: {
229
+ readonly list: readonly [{
230
+ readonly type: "rosterGroup";
231
+ readonly data: {
232
+ readonly name: "diesels";
233
+ readonly length: 2;
234
+ };
235
+ }, {
236
+ readonly type: "rosterGroup";
237
+ readonly data: {
238
+ readonly name: "steam";
239
+ readonly length: 1;
240
+ };
241
+ }];
242
+ };
219
243
  readonly throttle: {
220
244
  readonly acquire: {
221
245
  readonly success: {
@@ -20,6 +20,7 @@ export interface MockResponseManagerOptions {
20
20
  export declare class MockResponseManager {
21
21
  private responseDelay;
22
22
  private powerState;
23
+ private powerStateByPrefix;
23
24
  private throttles;
24
25
  private lights;
25
26
  private turnouts;
@@ -33,13 +34,17 @@ export declare class MockResponseManager {
33
34
  */
34
35
  private getHelloResponse;
35
36
  /**
36
- * Get power response
37
+ * Get power response, with optional per-prefix state tracking
37
38
  */
38
39
  private getPowerResponse;
39
40
  /**
40
- * Get roster response
41
+ * Get roster response, optionally filtered by group
41
42
  */
42
43
  private getRosterResponse;
44
+ /**
45
+ * Get roster group response
46
+ */
47
+ private getRosterGroupResponse;
43
48
  /**
44
49
  * Get throttle response
45
50
  */
@@ -10,6 +10,7 @@ export interface JmriMessage {
10
10
  method?: 'get' | 'post' | 'put' | 'delete' | 'list';
11
11
  data?: any;
12
12
  id?: number;
13
+ params?: Record<string, string>;
13
14
  }
14
15
  /**
15
16
  * Power state values (from JMRI JSON protocol constants)
@@ -166,6 +167,27 @@ export interface RosterMessage extends JmriMessage {
166
167
  export interface RosterData {
167
168
  [key: string]: RosterEntry;
168
169
  }
170
+ export interface RosterGroup {
171
+ name: string;
172
+ length: number;
173
+ }
174
+ export interface RosterGroupWrapper {
175
+ type: 'rosterGroup';
176
+ data: RosterGroup;
177
+ }
178
+ export interface RosterGroupMessage extends JmriMessage {
179
+ type: 'rosterGroup';
180
+ method: 'list';
181
+ data?: RosterGroupWrapper[];
182
+ }
183
+ export interface RosterMessageWithParams extends JmriMessage {
184
+ type: 'roster';
185
+ method: 'list';
186
+ params?: {
187
+ group: string;
188
+ };
189
+ data?: RosterResponse;
190
+ }
169
191
  /**
170
192
  * Turnout state values (from JMRI JSON protocol constants)
171
193
  * UNKNOWN = 0 (state cannot be determined)
package/docs/API.md CHANGED
@@ -141,6 +141,13 @@ const entry = await client.getRosterEntryByAddress(3);
141
141
 
142
142
  // Search roster
143
143
  const results = await client.searchRoster('steam');
144
+
145
+ // Get all roster groups
146
+ const groups: RosterGroup[] = await client.getRosterGroups();
147
+ // [{ name: 'locos', length: 3 }, { name: 'trams', length: 2 }]
148
+
149
+ // Get roster entries for a specific group
150
+ const locos = await client.getRosterEntriesByGroup('locos');
144
151
  ```
145
152
 
146
153
  ## Light Control
@@ -267,6 +274,7 @@ import {
267
274
  LightState,
268
275
  LightData,
269
276
  RosterEntry,
277
+ RosterGroup,
270
278
  TurnoutState,
271
279
  TurnoutData,
272
280
  ThrottleState,
package/docs/MOCK_MODE.md CHANGED
@@ -65,13 +65,18 @@ Connection establishment response with JMRI version info:
65
65
  Track power ON/OFF states
66
66
 
67
67
  ### Roster
68
- Three sample locomotives:
69
- - **CSX754** - GP38-2 diesel locomotive
70
- - **UP3985** - Challenger 4-6-6-4 steam locomotive
71
- - **BNSF5240** - SD40-2 diesel locomotive
68
+ Three sample locomotives across two groups:
69
+ - **CSX754** - GP38-2 diesel locomotive (`diesels` group)
70
+ - **UP3985** - Challenger 4-6-6-4 steam locomotive (`steam` group)
71
+ - **BNSF5240** - SD40-2 diesel locomotive (`diesels` group)
72
72
 
73
73
  Each includes realistic function key mappings (F0-F4).
74
74
 
75
+ ### Roster Groups
76
+ Two sample groups:
77
+ - **diesels** - CSX754, BNSF5240 (length: 2)
78
+ - **steam** - UP3985 (length: 1)
79
+
75
80
  ### Lights
76
81
  Three sample lights:
77
82
  - **IL1** - Yard Light (OFF)
@@ -163,10 +168,10 @@ const response = await mockManager.getMockResponse({
163
168
 
164
169
  The mock system maintains state for realistic behavior:
165
170
 
166
- - **Power state** - Remembers if power is ON or OFF
171
+ - **Power state** - Remembers if power is ON or OFF; tracked independently per connection prefix when one is supplied
167
172
  - **Light states** - Tracks ON/OFF state per light
168
173
  - **Turnout states** - Tracks CLOSED/THROWN state per turnout
169
- - **Throttles** - Tracks acquired throttles and their states
174
+ - **Throttles** - Tracks acquired throttles and their states, including connection prefix
170
175
  - **Speed/Direction** - Maintains current speed and direction per throttle
171
176
  - **Functions** - Tracks function key states (F0-F28)
172
177
 
@@ -208,11 +213,12 @@ The mock data structure follows the JMRI JSON protocol specification.
208
213
  Mock mode implements the full JMRI client API. All methods work identically:
209
214
 
210
215
  - ✅ `connect()` / `disconnect()`
211
- - ✅ `getPower()` / `powerOn()` / `powerOff()`
212
- - ✅ `getRoster()`
216
+ - ✅ `getPower()` / `powerOn()` / `powerOff()` (with optional prefix)
217
+ - ✅ `getRoster()` / `getRosterEntryByName()` / `getRosterEntryByAddress()` / `searchRoster()`
218
+ - ✅ `getRosterGroups()` / `getRosterEntriesByGroup()`
213
219
  - ✅ `getLight()` / `setLight()` / `turnOnLight()` / `turnOffLight()` / `listLights()`
214
220
  - ✅ `getTurnout()` / `setTurnout()` / `throwTurnout()` / `closeTurnout()` / `listTurnouts()`
215
- - ✅ `acquireThrottle()` / `releaseThrottle()`
221
+ - ✅ `acquireThrottle()` / `releaseThrottle()` (with optional prefix)
216
222
  - ✅ `setThrottleSpeed()`
217
223
  - ✅ `setThrottleDirection()`
218
224
  - ✅ `setThrottleFunction()`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jmri-client",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "WebSocket client for JMRI with real-time updates and throttle control - works in both Node.js and browsers",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",