-
Notifications
You must be signed in to change notification settings - Fork 1
/
get-state-data.ts
430 lines (384 loc) · 12.1 KB
/
get-state-data.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
/**
* The {@link GetStateData} class is parser and access helper for the CSV response
* data of the `/GetState.csv` endpoint (see {@link GetStateService}). The
* {@link GetStateCategory} enum can be used to retrieve data objects categorized
* according to the endpoint description (see [ProCon.IP manual](http://www.pooldigital.de/trm/TRM_ProConIP.pdf)).
* @packageDocumentation
*/
import { GetStateDataObject } from './get-state-data-object';
import { GetStateDataSysInfo } from './get-state-data-sys-info';
import { RelayDataObject } from './relay-data-object';
/**
* Enum of valid categories that can be used with
* {@link GetStateData.getDataObjectsByCategory}.
*
* Categories are based on the official API documentation.
*
* See manual (search for _GetState.csv_): http://www.pooldigital.de/trm/TRM_ProConIP.pdf
*/
export enum GetStateCategory {
/**
* Internal time of the ProCon.IP when processing the corresponding request.
* Hence, there is only one item in this category.
*/
TIME = 'time', // eslint-disable-line no-unused-vars
/**
* Category for analog channels.
*/
ANALOG = 'analog', // eslint-disable-line no-unused-vars
/**
* Category for electrode readings.
*/
ELECTRODES = 'electrodes', // eslint-disable-line no-unused-vars
/**
* Category for temperature sensor values.
*/
TEMPERATURES = 'temperatures', // eslint-disable-line no-unused-vars
/**
* Category for internal relays.
*/
RELAYS = 'relays', // eslint-disable-line no-unused-vars
/**
* Category for digital inputs.
*/
DIGITAL_INPUT = 'digitalInput', // eslint-disable-line no-unused-vars
/**
* Category for external relays.
*/
EXTERNAL_RELAYS = 'externalRelays', // eslint-disable-line no-unused-vars
/**
* Category for canister filling levels.
*/
CANISTER = 'canister', // eslint-disable-line no-unused-vars
/**
* Category for canister consumptions.
*/
CANISTER_CONSUMPTION = 'canisterConsumptions', // eslint-disable-line no-unused-vars
}
export interface IGetStateCategories {
time: number[];
analog: number[];
electrodes: number[];
temperatures: number[];
relays: number[];
digitalInput: number[];
externalRelays: number[];
canister: number[];
canisterConsumptions: number[];
}
/**
* This class is parser and access helper at once with integrated object
* representation for the response CSV of the {@link GetStateService}.
* (_This might be changed/split in seperate classes in a future refactoring_)
*/
export class GetStateData {
/**
* Extend the data object instances as you like.
*/
[key: string]: any; // eslint-disable-line no-undef
/**
* Raw CSV input string (retrieved by the {@link GetStateService}).
*/
public raw: string;
/**
* CSV input parsed to a simple 2-dimensional array.
*
* Structure:
* ```
* [
* 0: [ // line one
* 0: // line one, column one
* 1: // line one, column two
* ],
* 1: [ // line two
* 0: // line two, column one
* ...
* ...
* ]
* ```
*/
public parsed: string[][];
/**
* SysInfo column data.
*
* The first line of the csv has no relation to the rest of the CSV. So it
* is stored seperately in here.
*/
public sysInfo: GetStateDataSysInfo;
/**
* Actual data objects for further processing.
*
* Ordered by CSV column position starting at 0.
*/
public objects: GetStateDataObject[];
/**
* Lists all indices of objects that are not labeled with 'n.a.' and therefore
* considered to be active.
*/
public active: number[];
public readonly categories: IGetStateCategories = GetStateData.categories;
/**
* Data categories as array of objects.
*
* Category names as keys and arrays as values. These arrays list columns
* (referencing the {@link parsed} CSV) which fall into this category.
* The array values might contain simple listings of the column positions or
* another array containing the starting and ending index of a slice/range.
* Counting columns starts at 0. The value is of type `any` to simplify
* dynamic iteration without linting or parsing errors.
*/
public static readonly categories: IGetStateCategories = {
/**
* Internal time of the ProCon.IP when processing the corresponding request.
* Hence, there is only one item in this category. _Read from **column 0**
* of the CSV._
*/
time: [0],
/**
* Category for analog channels.
*
* _Read from **column 1 to 5** of the CSV._
*/
analog: GetStateData.expandSlice([[1, 5]]),
/**
* Category for electrode readings.
*
* _Read from **columns 6 and 7** of the CSV._
*/
electrodes: GetStateData.expandSlice([[6, 7]]),
/**
* Category for temperature sensor values.
*
* _Read from **column 8 to 15** of the CSV._
*/
temperatures: GetStateData.expandSlice([[8, 15]]),
/**
* Category for internal relay values.
*
* _Read from **column 16 to 23** of the CSV._
*/
relays: GetStateData.expandSlice([[16, 23]]),
/**
* Category for digital input values.
*
* _Read from **column 24 to 27** of the CSV._
*/
digitalInput: GetStateData.expandSlice([[24, 27]]),
/**
* Category for external relay values.
*
* _Read from **column 28 to 35** of the CSV._
*/
externalRelays: GetStateData.expandSlice([[28, 35]]),
/**
* Category for canister values.
*
* _Read from **column 36 to 38** of the CSV._
*/
canister: GetStateData.expandSlice([[36, 38]]),
/**
* Category for canister consumptions.
*
* _Read from **column 39 to 41** of the CSV._
*/
canisterConsumptions: GetStateData.expandSlice([[39, 41]]),
};
/**
* Initialize new {@link GetStateData} instance.
*
* @param rawData Plain response string of the {@link GetStateService} or the
* `/GetState.csv` API endpoint.
*/
public constructor(rawData?: string) {
this.objects = [];
this.active = [];
if (rawData === undefined) {
this.raw = '';
this.parsed = [[]];
this.sysInfo = new GetStateDataSysInfo();
} else {
// Save raw input string.
this.raw = rawData;
// Parse csv into 2-dimensional array of strings.
this.parsed = this.raw
.split(/[\r\n]+/) // split rows
.map((row) => row.split(/[,]/)) // split columns
.filter((row) => row.length > 1 || (row.length === 1 && row[0].trim().length > 1)); // remove blank lines
// Save common system information.
this.sysInfo = new GetStateDataSysInfo(this.parsed);
this.resolveObjects();
}
}
/**
* Get the category of a data item by its column index.
*
* @param index Column index
* @returns Category name or string `none` if no category could be identified.
*/
public getCategory(index: number): string {
for (const category in GetStateData.categories) {
if (GetStateData.categories[category as keyof IGetStateCategories].indexOf(index) >= 0) {
return category;
}
}
return 'none';
}
/**
* Get {@link GetStateDataObject} objects by index.
*
* @param indices An array of object indices specifying the return objects.
* @param activeOnly Optionally filter for active objects only.
*/
public getDataObjects(indices: number[], activeOnly = false): GetStateDataObject[] {
return activeOnly
? this.objects.filter((obj, idx) => indices.indexOf(idx) >= 0 && this.active.indexOf(idx) >= 0)
: this.objects.filter((obj, idx) => indices.indexOf(idx) >= 0);
}
/**
* Get a single {@link GetStateDataObject} by id aka column index.
*
* @param id Object column index.
*/
public getDataObject(id: number): GetStateDataObject {
return this.objects[id] ? this.objects[id] : new GetStateDataObject(id, '', '', '', '', '');
}
/**
* Get all data objects of a given category.
*
* @param category A valid category string (see {@link GetStateCategory})
* @param activeOnly Optionally filter for active objects only.
*/
public getDataObjectsByCategory(category: string, activeOnly = false): GetStateDataObject[] {
return this.getDataObjects(GetStateData.categories[category as GetStateCategory], activeOnly);
}
/**
* Get the object id aka column index of the chlorine dosage control relay.
*/
public getChlorineDosageControlId(): number {
return this.sysInfo.chlorineDosageRelay;
}
/**
* Get the object id aka column index of the pH minus dosage control relay.
*/
public getPhMinusDosageControlId(): number {
return this.sysInfo.phMinusDosageRelay;
}
/**
* Get the object id aka column index of the pH plus dosage control relay.
*/
public getPhPlusDosageControlId(): number {
return this.sysInfo.phPlusDosageRelay;
}
/**
* Get the chlorine dosage control {@link RelayDataObject}.
*/
public getChlorineDosageControl(): RelayDataObject {
return new RelayDataObject(this.getDataObject(this.getChlorineDosageControlId()));
}
/**
* Get the pH- dosage control {@link RelayDataObject}.
*/
public getPhMinusDosageControl(): RelayDataObject {
return new RelayDataObject(this.getDataObject(this.getPhMinusDosageControlId()));
}
/**
* Get the pH+ dosage control {@link RelayDataObject}.
*/
public getPhPlusDosageControl(): RelayDataObject {
return new RelayDataObject(this.getDataObject(this.getPhPlusDosageControlId()));
}
/**
* Check whether the given id refers to a dosage control {@link RelayDataObject}.
*/
public isDosageControl(id: number): boolean {
return (
[this.getChlorineDosageControlId(), this.getPhMinusDosageControlId(), this.getPhPlusDosageControlId()].indexOf(
id,
) >= 0
);
}
/**
* Parse the CSV string into a 2-dimensional array structure and into
* {@link GetStateDataObject} and {@link RelayDataObject} objects.
*
* @param csv Raw CSV input string (response of the `/GetState.csv` endpoint)
*/
public parseCsv(csv: string): void {
// Save raw input string.
this.raw = csv;
// Parse csv into 2-dimensional array of strings.
this.parsed = csv
.split(/[\r\n]+/) // split rows
.map((row) => row.split(/[,]/)) // split columns
.filter((row) => row.length > 1 || (row.length === 1 && row[0].trim().length > 1)); // remove blank lines
// Save common system information.
this.sysInfo = new GetStateDataSysInfo(this.parsed);
this.resolveObjects();
}
/**
* @internal
*/
private resolveObjects(): void {
// Iterate data columns.
this.active.length = 0;
this.parsed[1].forEach((name, index) => {
if (this.objects[index] === undefined) {
// Add object to the objects array.
this.objects[index] = new GetStateDataObject(
index,
name,
this.parsed[2][index],
this.parsed[3][index],
this.parsed[4][index],
this.parsed[5][index],
);
} else {
this.objects[index].set(
index,
name,
this.parsed[2][index],
this.parsed[3][index],
this.parsed[4][index],
this.parsed[5][index],
);
}
if (this.objects[index].active) {
this.active.push(index);
}
});
this.categorize();
}
/**
* @internal
*/
private categorize(): void {
Object.keys(GetStateData.categories).forEach((category) => {
let catId = 0;
GetStateData.categories[category as keyof IGetStateCategories].forEach((id: number) => {
if (this.objects[id] !== undefined) {
this.objects[id].categoryId = catId++;
this.objects[id].category = category;
}
});
});
}
/**
* @param input
* @internal
*/
private static expandSlice(input: number[][]): number[] {
const output: number[] = [];
input.forEach((def) => {
if (Number.isInteger(Number(def))) {
output.push(Number(def));
}
if (Array.isArray(def)) {
def.map((subDef) => Number(subDef));
for (let i = Number(def[0]); i <= Number(def[1]); i++) {
output.push(i);
}
}
});
return output;
}
}