Skip to content

Commit 2f86704

Browse files
committed
v0.3.4: implement more robust error handling and logging
1 parent 548a270 commit 2f86704

3 files changed

Lines changed: 436 additions & 41 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@adametherzlab/tank-level",
3-
"version": "0.3.3",
3+
"version": "0.3.4",
44
"description": "Tank level calculator — volume from height for cylindrical, rectangular, conical & spherical vessels with strapping tables, fill rate, alarms & inventory",
55
"type": "module",
66
"main": "src/index.ts",

src/monitoring.ts

Lines changed: 232 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,243 @@
1-
// Other imports and functions remain unchanged...
1+
import { calculateVolume, convertVolume } from './index';
2+
import type { VesselConfig, StrappingOptions, StrappingEntry, FillRateInput, FillRateResult, AlarmConfig, AlarmResult, TankReading, InventoryEntry, InventoryResult, Unit } from './types';
23

4+
/**
5+
* Generates a strapping table (also known as a calibration chart) for a given vessel.
6+
* This table maps liquid height to volume and percentage fill.
7+
*
8+
* @param vessel The configuration of the vessel.
9+
* @param options Options for generating the table, including number of steps, unit, and precision.
10+
* @returns An array of StrappingEntry objects, each containing height, volume, and percentage.
11+
* @throws {Error} If `steps` is out of the valid range (1-10000).
12+
*/
313
export function strappingTable(vessel: VesselConfig, options?: StrappingOptions): readonly StrappingEntry[] {
414
const steps = options?.steps ?? 20;
515
const unit = options?.unit ?? 'cubicMeters';
616
const precision = options?.precision ?? 4;
717

818
if (steps < 1 || steps > 10000) {
9-
throw new Error('Steps must be between 1 and 10000');
19+
throw new Error(`Invalid steps value: ${steps}. Steps must be between 1 and 10000.`);
1020
}
1121

12-
// Rest of function remains unchanged...
22+
const table: StrappingEntry[] = [];
23+
const totalHeight = vessel.dimensions.height;
24+
25+
// Calculate total volume once for percentage calculation
26+
let totalVolume: number;
27+
try {
28+
totalVolume = calculateVolume(vessel, totalHeight).volume;
29+
} catch (error: any) {
30+
throw new Error(`Failed to calculate total volume for strapping table: ${error.message}`);
31+
}
32+
33+
for (let i = 0; i <= steps; i++) {
34+
const height = (i / steps) * totalHeight;
35+
let volumeResult;
36+
try {
37+
volumeResult = calculateVolume(vessel, height);
38+
} catch (error: any) {
39+
// Log the error but try to continue if possible, or rethrow if critical
40+
console.warn(`Warning: Failed to calculate volume for height ${height} in strapping table. Error: ${error.message}`);
41+
volumeResult = { volume: 0, percentage: 0 }; // Fallback
42+
}
43+
44+
const volumeInTargetUnit = convertVolume(volumeResult.volume, 'cubicMeters', unit);
45+
const percentage = totalVolume > 0 ? (volumeResult.volume / totalVolume) * 100 : 0;
46+
47+
table.push({
48+
height: parseFloat(height.toFixed(precision)),
49+
volume: parseFloat(volumeInTargetUnit.toFixed(precision)),
50+
percentage: parseFloat(percentage.toFixed(precision)),
51+
});
52+
}
53+
54+
return table;
55+
}
56+
57+
/**
58+
* Calculates the fill or drain rate of a vessel based on two height readings over time.
59+
*
60+
* @param input The FillRateInput object containing vessel config, heights, and time.
61+
* @returns A FillRateResult object with rates, direction, and time to full/empty.
62+
* @throws {Error} If `timeMinutes` is not positive.
63+
* @throws {Error} If vessel total volume calculation fails.
64+
*/
65+
export function fillRate(input: FillRateInput): FillRateResult {
66+
const { vessel, heightBefore, heightAfter, timeMinutes } = input;
67+
68+
if (timeMinutes <= 0) {
69+
throw new Error(`Invalid timeMinutes: ${timeMinutes}. Must be a positive number.`);
70+
}
71+
72+
const totalHeight = vessel.dimensions.height;
73+
let totalVolume: number;
74+
try {
75+
totalVolume = calculateVolume(vessel, totalHeight).volume;
76+
} catch (error: any) {
77+
throw new Error(`Failed to calculate total volume for fill rate: ${error.message}`);
78+
}
79+
80+
if (totalVolume === 0) {
81+
console.warn('Warning: Vessel has zero total volume, fill rate calculations may be inaccurate.');
82+
return {
83+
ratePerMinute: 0,
84+
ratePerHour: 0,
85+
direction: 'stable',
86+
volumeChange: 0,
87+
minutesToFull: null,
88+
minutesToEmpty: null,
89+
percentBefore: 0,
90+
percentAfter: 0,
91+
};
92+
}
93+
94+
let volumeBefore: number, volumeAfter: number;
95+
try {
96+
volumeBefore = calculateVolume(vessel, heightBefore).volume;
97+
volumeAfter = calculateVolume(vessel, heightAfter).volume;
98+
} catch (error: any) {
99+
throw new Error(`Failed to calculate volume for fill rate at height ${heightBefore} or ${heightAfter}: ${error.message}`);
100+
}
101+
102+
const volumeChange = volumeAfter - volumeBefore;
103+
const ratePerMinute = volumeChange / timeMinutes;
104+
const ratePerHour = ratePerMinute * 60;
105+
106+
let direction: FillRateResult['direction'] = 'stable';
107+
if (volumeChange > 0) {
108+
direction = 'filling';
109+
} else if (volumeChange < 0) {
110+
direction = 'draining';
111+
}
112+
113+
const remainingVolume = totalVolume - volumeAfter;
114+
const minutesToFull = ratePerMinute > 0 ? remainingVolume / ratePerMinute : null;
115+
116+
const currentVolume = volumeAfter;
117+
const minutesToEmpty = ratePerMinute < 0 ? currentVolume / Math.abs(ratePerMinute) : null;
118+
119+
const percentBefore = (volumeBefore / totalVolume) * 100;
120+
const percentAfter = (volumeAfter / totalVolume) * 100;
121+
122+
return {
123+
ratePerMinute,
124+
ratePerHour,
125+
direction,
126+
volumeChange,
127+
minutesToFull: minutesToFull !== null && minutesToFull >= 0 ? minutesToFull : null,
128+
minutesToEmpty: minutesToEmpty !== null && minutesToEmpty >= 0 ? minutesToEmpty : null,
129+
percentBefore,
130+
percentAfter,
131+
};
132+
}
133+
134+
/**
135+
* Checks the current liquid level against predefined alarm thresholds.
136+
*
137+
* @param vessel The vessel configuration.
138+
* @param liquidHeight The current liquid height in the vessel.
139+
* @param alarmConfig The alarm thresholds (high-high, high, low, low-low) as percentages.
140+
* @returns An AlarmResult object indicating the current alarm status.
141+
* @throws {Error} If `liquidHeight` is negative or exceeds vessel height.
142+
* @throws {Error} If vessel total volume calculation fails.
143+
*/
144+
export function tankAlarms(vessel: VesselConfig, liquidHeight: number, alarmConfig: AlarmConfig): AlarmResult {
145+
const totalHeight = vessel.dimensions.height;
146+
147+
if (liquidHeight < 0) {
148+
throw new Error(`Liquid height cannot be negative: ${liquidHeight}.`);
149+
}
150+
if (liquidHeight > totalHeight) {
151+
console.warn(`Warning: Liquid height (${liquidHeight}) exceeds vessel total height (${totalHeight}).`);
152+
}
153+
154+
let currentVolumeResult;
155+
try {
156+
currentVolumeResult = calculateVolume(vessel, liquidHeight);
157+
} catch (error: any) {
158+
throw new Error(`Failed to calculate volume for alarm check at height ${liquidHeight}: ${error.message}`);
159+
}
160+
161+
const percentage = currentVolumeResult.percentage;
162+
const activeAlarms: AlarmStatus[] = [];
163+
let status: AlarmStatus = 'normal';
164+
165+
if (alarmConfig.highHigh !== undefined && percentage >= alarmConfig.highHigh) {
166+
activeAlarms.push('high-high');
167+
status = 'high-high';
168+
} else if (alarmConfig.high !== undefined && percentage >= alarmConfig.high) {
169+
activeAlarms.push('high');
170+
status = 'high';
171+
} else if (alarmConfig.lowLow !== undefined && percentage <= alarmConfig.lowLow) {
172+
activeAlarms.push('low-low');
173+
status = 'low-low';
174+
} else if (alarmConfig.low !== undefined && percentage <= alarmConfig.low) {
175+
activeAlarms.push('low');
176+
status = 'low';
177+
}
178+
179+
return {
180+
status,
181+
percentage: parseFloat(percentage.toFixed(2)),
182+
activeAlarms,
183+
isAlarmed: activeAlarms.length > 0,
184+
};
13185
}
14186

15-
// Rest of file remains unchanged...
187+
/**
188+
* Calculates the total inventory and average fill percentage for multiple tanks.
189+
*
190+
* @param readings An array of TankReading objects, each representing a tank's current state.
191+
* @param targetUnit The unit to convert all volumes to for the total and individual entries.
192+
* @param precision The number of decimal places for volume and percentage.
193+
* @returns An InventoryResult object with total volume, average percentage, and individual tank entries.
194+
* @throws {Error} If any tank reading fails volume calculation.
195+
*/
196+
export function tankInventory(readings: readonly TankReading[], targetUnit: Unit = 'cubicMeters', precision: number = 4): InventoryResult {
197+
let totalVolume = 0;
198+
let totalPercentage = 0;
199+
const tanks: InventoryEntry[] = [];
200+
201+
if (readings.length === 0) {
202+
return {
203+
tanks: [],
204+
totalVolume: 0,
205+
averagePercentage: 0,
206+
count: 0,
207+
unit: targetUnit,
208+
};
209+
}
210+
211+
for (const reading of readings) {
212+
const { name, vessel, liquidHeight, unit: inputUnit = 'cubicMeters' } = reading;
213+
214+
let volumeResult;
215+
try {
216+
volumeResult = calculateVolume(vessel, liquidHeight);
217+
} catch (error: any) {
218+
throw new Error(`Failed to calculate volume for tank '${name}' at height ${liquidHeight}: ${error.message}`);
219+
}
220+
221+
const volumeInTargetUnit = convertVolume(volumeResult.volume, 'cubicMeters', targetUnit);
222+
223+
tanks.push({
224+
name,
225+
volume: parseFloat(volumeInTargetUnit.toFixed(precision)),
226+
percentage: parseFloat(volumeResult.percentage.toFixed(precision)),
227+
unit: targetUnit,
228+
});
229+
230+
totalVolume += volumeInTargetUnit;
231+
totalPercentage += volumeResult.percentage;
232+
}
233+
234+
const averagePercentage = totalPercentage / readings.length;
235+
236+
return {
237+
tanks,
238+
totalVolume: parseFloat(totalVolume.toFixed(precision)),
239+
averagePercentage: parseFloat(averagePercentage.toFixed(precision)),
240+
count: readings.length,
241+
unit: targetUnit,
242+
};
243+
}

0 commit comments

Comments
 (0)