@jjlmoya/utils-hardware 1.20.0 → 1.21.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.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +4 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/upsRuntimeCalculator/bibliography.astro +14 -0
- package/src/tool/upsRuntimeCalculator/bibliography.ts +16 -0
- package/src/tool/upsRuntimeCalculator/component.astro +384 -0
- package/src/tool/upsRuntimeCalculator/entry.ts +29 -0
- package/src/tool/upsRuntimeCalculator/i18n/de.ts +208 -0
- package/src/tool/upsRuntimeCalculator/i18n/en.ts +208 -0
- package/src/tool/upsRuntimeCalculator/i18n/es.ts +208 -0
- package/src/tool/upsRuntimeCalculator/i18n/fr.ts +208 -0
- package/src/tool/upsRuntimeCalculator/i18n/id.ts +208 -0
- package/src/tool/upsRuntimeCalculator/i18n/it.ts +208 -0
- package/src/tool/upsRuntimeCalculator/i18n/ja.ts +208 -0
- package/src/tool/upsRuntimeCalculator/i18n/ko.ts +208 -0
- package/src/tool/upsRuntimeCalculator/i18n/nl.ts +208 -0
- package/src/tool/upsRuntimeCalculator/i18n/pl.ts +208 -0
- package/src/tool/upsRuntimeCalculator/i18n/pt.ts +208 -0
- package/src/tool/upsRuntimeCalculator/i18n/ru.ts +208 -0
- package/src/tool/upsRuntimeCalculator/i18n/sv.ts +208 -0
- package/src/tool/upsRuntimeCalculator/i18n/tr.ts +208 -0
- package/src/tool/upsRuntimeCalculator/i18n/zh.ts +208 -0
- package/src/tool/upsRuntimeCalculator/index.ts +11 -0
- package/src/tool/upsRuntimeCalculator/logic.ts +48 -0
- package/src/tool/upsRuntimeCalculator/seo.astro +15 -0
- package/src/tool/upsRuntimeCalculator/ui.ts +31 -0
- package/src/tool/upsRuntimeCalculator/ups-runtime-calculator.css +530 -0
- package/src/tools.ts +2 -1
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -10,10 +10,11 @@ import { toneGenerator } from '../tool/toneGenerator/index';
|
|
|
10
10
|
import { refreshRateDetector } from '../tool/refreshRateDetector/index';
|
|
11
11
|
import { monitorGhostingTest } from '../tool/monitorGhostingTest/index';
|
|
12
12
|
import { spectrumCanvas } from '../tool/colorAccuracyTest/index';
|
|
13
|
+
import { upsRuntimeCalculator } from '../tool/upsRuntimeCalculator/index';
|
|
13
14
|
|
|
14
15
|
export const hardwareCategory: HardwareCategoryEntry = {
|
|
15
16
|
icon: 'mdi:memory',
|
|
16
|
-
tools: [pixelesPantalla, testTeclado, testMando, probadorVibracionMando, testRaton, mouseDoubleClickTest, estimadorSaludBateria, toneGenerator, refreshRateDetector, monitorGhostingTest, spectrumCanvas],
|
|
17
|
+
tools: [pixelesPantalla, testTeclado, testMando, probadorVibracionMando, testRaton, mouseDoubleClickTest, estimadorSaludBateria, toneGenerator, refreshRateDetector, monitorGhostingTest, spectrumCanvas, upsRuntimeCalculator],
|
|
17
18
|
i18n: {
|
|
18
19
|
en: () => import('./i18n/en').then((m) => m.content),
|
|
19
20
|
es: () => import('./i18n/es').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -20,6 +20,8 @@ export { monitorGhostingTest } from './tool/monitorGhostingTest/entry';
|
|
|
20
20
|
export type { MonitorGhostingTestLocaleContent } from './tool/monitorGhostingTest/entry';
|
|
21
21
|
export { spectrumCanvas } from './tool/colorAccuracyTest/entry';
|
|
22
22
|
export type { SpectrumCanvasLocaleContent } from './tool/colorAccuracyTest/entry';
|
|
23
|
+
export { upsRuntimeCalculator } from './tool/upsRuntimeCalculator/entry';
|
|
24
|
+
export type { UpsRuntimeCalculatorLocaleContent } from './tool/upsRuntimeCalculator/entry';
|
|
23
25
|
export { hardwareCategory } from './category';
|
|
24
26
|
import { estimadorSaludBateria } from './tool/batteryHealthEstimator/entry';
|
|
25
27
|
import { pixelesPantalla } from './tool/deadPixelTest/entry';
|
|
@@ -32,4 +34,5 @@ import { toneGenerator } from './tool/toneGenerator/entry';
|
|
|
32
34
|
import { refreshRateDetector } from './tool/refreshRateDetector/entry';
|
|
33
35
|
import { monitorGhostingTest } from './tool/monitorGhostingTest/entry';
|
|
34
36
|
import { spectrumCanvas } from './tool/colorAccuracyTest/entry';
|
|
35
|
-
|
|
37
|
+
import { upsRuntimeCalculator } from './tool/upsRuntimeCalculator/entry';
|
|
38
|
+
export const ALL_ENTRIES = [estimadorSaludBateria, pixelesPantalla, testMando, probadorVibracionMando, testTeclado, testRaton, mouseDoubleClickTest, toneGenerator, refreshRateDetector, monitorGhostingTest, spectrumCanvas, upsRuntimeCalculator];
|
package/src/index.ts
CHANGED
|
@@ -28,3 +28,4 @@ export { TONE_GENERATOR_TOOL } from './tool/toneGenerator/index';
|
|
|
28
28
|
export { REFRESH_RATE_DETECTOR_TOOL } from './tool/refreshRateDetector/index';
|
|
29
29
|
export { MONITOR_GHOSTING_TEST_TOOL } from './tool/monitorGhostingTest/index';
|
|
30
30
|
export { SPECTRUM_CANVAS_TOOL } from './tool/colorAccuracyTest/index';
|
|
31
|
+
export { UPS_RUNTIME_CALCULATOR_TOOL } from './tool/upsRuntimeCalculator/index';
|
|
@@ -4,8 +4,8 @@ import { hardwareCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 12 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(12);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('hardwareCategory should be defined', () => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import type { KnownLocale } from '../../types';
|
|
4
|
+
import { upsRuntimeCalculator } from './index';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const content = await upsRuntimeCalculator.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && content.bibliography.length > 0 && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'APC by Schneider Electric - UPS battery runtime and load behavior',
|
|
6
|
+
url: 'https://www.se.com/us/en/work/products/tools/ups-selector/',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'CyberPower - Understanding watts, volt-amps, and power factor',
|
|
10
|
+
url: 'https://www.cyberpower.com/global/en/blog/watts-vs-volt-amperes-va-key-power-ratings-for-choosing-the-right-ups',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'Eaton - UPS sizing and power protection fundamentals',
|
|
14
|
+
url: 'https://www.eaton.com/us/en-us/catalog/backup-power-ups-surge-it-power-distribution/ups-buying-guide.html',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Icon } from 'astro-icon/components';
|
|
3
|
+
import type { KnownLocale } from '../../types';
|
|
4
|
+
import type { UpsRuntimeCalculatorUI } from './ui';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
ui?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { ui } = Astro.props;
|
|
12
|
+
const t = (ui ?? {}) as UpsRuntimeCalculatorUI;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<div
|
|
16
|
+
class="ups-root"
|
|
17
|
+
data-config={JSON.stringify({
|
|
18
|
+
presetDesktop: t.presetDesktop,
|
|
19
|
+
presetMonitor: t.presetMonitor,
|
|
20
|
+
presetRouter: t.presetRouter,
|
|
21
|
+
presetNas: t.presetNas,
|
|
22
|
+
deviceName: t.deviceName,
|
|
23
|
+
watts: t.watts,
|
|
24
|
+
remove: t.remove,
|
|
25
|
+
bandLight: t.bandLight,
|
|
26
|
+
bandBalanced: t.bandBalanced,
|
|
27
|
+
bandHeavy: t.bandHeavy,
|
|
28
|
+
summaryPrefix: t.summaryPrefix,
|
|
29
|
+
minutes: t.minutes,
|
|
30
|
+
hours: t.hours,
|
|
31
|
+
wattsUnit: t.wattsUnit,
|
|
32
|
+
percentUnit: t.percentUnit,
|
|
33
|
+
assumptionsLabel: t.assumptionsLabel,
|
|
34
|
+
})}
|
|
35
|
+
>
|
|
36
|
+
<section class="ups-panel">
|
|
37
|
+
<div class="ups-console ups-load">
|
|
38
|
+
<div class="ups-strip">
|
|
39
|
+
<span>{t.loadTitle}</span>
|
|
40
|
+
<button class="ups-add" type="button" data-add-device>{t.addDevice}</button>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="ups-devices" data-devices></div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="ups-machine" aria-hidden="true">
|
|
46
|
+
<div class="ups-shell">
|
|
47
|
+
<div class="ups-port-row">
|
|
48
|
+
<i></i><i></i><i></i><i></i>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="ups-screen">
|
|
51
|
+
<span data-runtime-big>0</span>
|
|
52
|
+
<small data-runtime-unit>{t.minutes}</small>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="ups-core">
|
|
55
|
+
<b></b><b></b><b></b><b></b>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="ups-energy-rail">
|
|
59
|
+
<span data-load-bar></span>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="ups-console ups-controls" aria-label={t.assumptionsLabel}>
|
|
64
|
+
<label class="ups-capacity">
|
|
65
|
+
<span>{t.batteryWh}</span>
|
|
66
|
+
<span class="ups-number-field ups-number-field-large">
|
|
67
|
+
<input data-battery type="number" min="50" max="3000" step="10" value="432" />
|
|
68
|
+
<em>{t.whUnit}</em>
|
|
69
|
+
</span>
|
|
70
|
+
</label>
|
|
71
|
+
<label>
|
|
72
|
+
<span>{t.efficiency}</span>
|
|
73
|
+
<input data-efficiency type="range" min="70" max="95" value="86" />
|
|
74
|
+
<strong data-efficiency-label>86%</strong>
|
|
75
|
+
</label>
|
|
76
|
+
<label>
|
|
77
|
+
<span>{t.powerFactor}</span>
|
|
78
|
+
<input data-power-factor type="range" min="50" max="100" value="70" />
|
|
79
|
+
<strong data-power-factor-label>0.70</strong>
|
|
80
|
+
</label>
|
|
81
|
+
<label>
|
|
82
|
+
<span>{t.reserve}</span>
|
|
83
|
+
<input data-reserve type="range" min="0" max="50" value="20" />
|
|
84
|
+
<strong data-reserve-label>20%</strong>
|
|
85
|
+
</label>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="ups-dashboard">
|
|
89
|
+
<div class="ups-metrics">
|
|
90
|
+
<article>
|
|
91
|
+
<span>{t.totalLoad}</span>
|
|
92
|
+
<strong><output data-total-load>0</output> {t.wattsUnit}</strong>
|
|
93
|
+
<i data-load-meter></i>
|
|
94
|
+
</article>
|
|
95
|
+
<article>
|
|
96
|
+
<span>{t.runtime}</span>
|
|
97
|
+
<strong><output data-runtime>0</output></strong>
|
|
98
|
+
<i data-runtime-meter></i>
|
|
99
|
+
</article>
|
|
100
|
+
<article>
|
|
101
|
+
<span>{t.recommendedUps}</span>
|
|
102
|
+
<strong><output data-recommended>0</output> {t.vaUnit}</strong>
|
|
103
|
+
<i data-va-meter></i>
|
|
104
|
+
</article>
|
|
105
|
+
<article>
|
|
106
|
+
<span>{t.usableEnergy}</span>
|
|
107
|
+
<strong><output data-usable-energy>0</output> {t.whUnit}</strong>
|
|
108
|
+
<i data-energy-meter></i>
|
|
109
|
+
</article>
|
|
110
|
+
</div>
|
|
111
|
+
<p class="ups-summary" data-summary></p>
|
|
112
|
+
</div>
|
|
113
|
+
</section>
|
|
114
|
+
<template data-trash-icon>
|
|
115
|
+
<Icon name="mdi:trash-can-outline" class="ups-trash-icon" />
|
|
116
|
+
</template>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<link rel="stylesheet" href="./ups-runtime-calculator.css" />
|
|
120
|
+
|
|
121
|
+
<script>
|
|
122
|
+
interface Device {
|
|
123
|
+
name: string;
|
|
124
|
+
watts: number;
|
|
125
|
+
}
|
|
126
|
+
interface Config {
|
|
127
|
+
presetDesktop: string;
|
|
128
|
+
presetMonitor: string;
|
|
129
|
+
presetRouter: string;
|
|
130
|
+
presetNas: string;
|
|
131
|
+
deviceName: string;
|
|
132
|
+
watts: string;
|
|
133
|
+
remove: string;
|
|
134
|
+
bandLight: string;
|
|
135
|
+
bandBalanced: string;
|
|
136
|
+
bandHeavy: string;
|
|
137
|
+
summaryPrefix: string;
|
|
138
|
+
minutes: string;
|
|
139
|
+
hours: string;
|
|
140
|
+
wattsUnit: string;
|
|
141
|
+
percentUnit: string;
|
|
142
|
+
assumptionsLabel: string;
|
|
143
|
+
}
|
|
144
|
+
interface PersistedState {
|
|
145
|
+
devices?: Device[];
|
|
146
|
+
batteryWh?: string;
|
|
147
|
+
efficiency?: string;
|
|
148
|
+
powerFactor?: string;
|
|
149
|
+
reserve?: string;
|
|
150
|
+
}
|
|
151
|
+
type RuntimeStats = { totalWatts: number; usableWh: number; runtimeMinutes: number; recommendedWatts: number; recommendedVa: number };
|
|
152
|
+
const root = document.querySelector<HTMLElement>('.ups-root');
|
|
153
|
+
const storageKey = 'ups-runtime-calculator-state';
|
|
154
|
+
const config = JSON.parse(root?.dataset.config ?? '{}') as Config;
|
|
155
|
+
const defaultDevices: Device[] = [
|
|
156
|
+
{ name: config.presetDesktop, watts: 260 },
|
|
157
|
+
{ name: config.presetMonitor, watts: 45 },
|
|
158
|
+
{ name: config.presetRouter, watts: 14 },
|
|
159
|
+
{ name: config.presetNas, watts: 38 },
|
|
160
|
+
];
|
|
161
|
+
const savedState = readState();
|
|
162
|
+
const devices: Device[] = Array.isArray(savedState.devices) && savedState.devices.length > 0
|
|
163
|
+
? savedState.devices.map((device) => ({ name: String(device.name), watts: Number(device.watts) || 0 }))
|
|
164
|
+
: defaultDevices;
|
|
165
|
+
const els = {
|
|
166
|
+
devices: root?.querySelector<HTMLElement>('[data-devices]'),
|
|
167
|
+
add: root?.querySelector<HTMLButtonElement>('[data-add-device]'),
|
|
168
|
+
battery: root?.querySelector<HTMLInputElement>('[data-battery]'),
|
|
169
|
+
efficiency: root?.querySelector<HTMLInputElement>('[data-efficiency]'),
|
|
170
|
+
efficiencyLabel: root?.querySelector<HTMLElement>('[data-efficiency-label]'),
|
|
171
|
+
powerFactor: root?.querySelector<HTMLInputElement>('[data-power-factor]'),
|
|
172
|
+
powerFactorLabel: root?.querySelector<HTMLElement>('[data-power-factor-label]'),
|
|
173
|
+
reserve: root?.querySelector<HTMLInputElement>('[data-reserve]'),
|
|
174
|
+
reserveLabel: root?.querySelector<HTMLElement>('[data-reserve-label]'),
|
|
175
|
+
totalLoad: root?.querySelector<HTMLOutputElement>('[data-total-load]'),
|
|
176
|
+
runtime: root?.querySelector<HTMLOutputElement>('[data-runtime]'),
|
|
177
|
+
runtimeBig: root?.querySelector<HTMLElement>('[data-runtime-big]'),
|
|
178
|
+
runtimeUnit: root?.querySelector<HTMLElement>('[data-runtime-unit]'),
|
|
179
|
+
recommended: root?.querySelector<HTMLOutputElement>('[data-recommended]'),
|
|
180
|
+
usableEnergy: root?.querySelector<HTMLOutputElement>('[data-usable-energy]'),
|
|
181
|
+
summary: root?.querySelector<HTMLElement>('[data-summary]'),
|
|
182
|
+
loadBar: root?.querySelector<HTMLElement>('[data-load-bar]'),
|
|
183
|
+
loadMeter: root?.querySelector<HTMLElement>('[data-load-meter]'),
|
|
184
|
+
runtimeMeter: root?.querySelector<HTMLElement>('[data-runtime-meter]'),
|
|
185
|
+
vaMeter: root?.querySelector<HTMLElement>('[data-va-meter]'),
|
|
186
|
+
energyMeter: root?.querySelector<HTMLElement>('[data-energy-meter]'),
|
|
187
|
+
trashIcon: root?.querySelector<HTMLTemplateElement>('[data-trash-icon]'),
|
|
188
|
+
};
|
|
189
|
+
applySavedControls();
|
|
190
|
+
function readState(): PersistedState {
|
|
191
|
+
try {
|
|
192
|
+
return JSON.parse(localStorage.getItem(storageKey) ?? '{}') as PersistedState;
|
|
193
|
+
} catch {
|
|
194
|
+
return {};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function saveState() {
|
|
198
|
+
const nextState: PersistedState = {
|
|
199
|
+
devices,
|
|
200
|
+
batteryWh: els.battery?.value,
|
|
201
|
+
efficiency: els.efficiency?.value,
|
|
202
|
+
powerFactor: els.powerFactor?.value,
|
|
203
|
+
reserve: els.reserve?.value,
|
|
204
|
+
};
|
|
205
|
+
localStorage.setItem(storageKey, JSON.stringify(nextState));
|
|
206
|
+
}
|
|
207
|
+
function applySavedControls() {
|
|
208
|
+
[
|
|
209
|
+
[els.battery, savedState.batteryWh],
|
|
210
|
+
[els.efficiency, savedState.efficiency],
|
|
211
|
+
[els.powerFactor, savedState.powerFactor],
|
|
212
|
+
[els.reserve, savedState.reserve],
|
|
213
|
+
].forEach(([input, value]) => applySavedValue(input as HTMLInputElement | null | undefined, value as string | undefined));
|
|
214
|
+
}
|
|
215
|
+
function applySavedValue(input: HTMLInputElement | null | undefined, value: string | undefined) {
|
|
216
|
+
if (input && value) {
|
|
217
|
+
input.value = value;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function calculate() {
|
|
221
|
+
const stats = getRuntimeStats();
|
|
222
|
+
updateLabels();
|
|
223
|
+
updateOutputs(stats);
|
|
224
|
+
updateMeters(stats);
|
|
225
|
+
saveState();
|
|
226
|
+
}
|
|
227
|
+
function getRuntimeStats(): RuntimeStats {
|
|
228
|
+
const totalWatts = devices.reduce((sum, device) => sum + Math.max(0, device.watts), 0);
|
|
229
|
+
const batteryWh = readNumber(els.battery, 0);
|
|
230
|
+
const efficiency = readNumber(els.efficiency, 86) / 100;
|
|
231
|
+
const reserve = readNumber(els.reserve, 20) / 100;
|
|
232
|
+
const powerFactor = readNumber(els.powerFactor, 70) / 100;
|
|
233
|
+
const usableWh = batteryWh * efficiency * (1 - reserve);
|
|
234
|
+
const runtimeMinutes = totalWatts > 0 ? (usableWh / totalWatts) * 60 : 0;
|
|
235
|
+
const recommendedWatts = Math.ceil((totalWatts * 1.25) / 10) * 10;
|
|
236
|
+
const recommendedVa = Math.ceil((recommendedWatts / powerFactor) / 50) * 50;
|
|
237
|
+
return { totalWatts, usableWh, runtimeMinutes, recommendedWatts, recommendedVa };
|
|
238
|
+
}
|
|
239
|
+
function updateLabels() {
|
|
240
|
+
const efficiency = readNumber(els.efficiency, 86) / 100;
|
|
241
|
+
const reserve = readNumber(els.reserve, 20) / 100;
|
|
242
|
+
const powerFactor = readNumber(els.powerFactor, 70) / 100;
|
|
243
|
+
els.efficiencyLabel!.textContent = `${Math.round(efficiency * 100)}${config.percentUnit}`;
|
|
244
|
+
els.powerFactorLabel!.textContent = powerFactor.toFixed(2);
|
|
245
|
+
els.reserveLabel!.textContent = `${Math.round(reserve * 100)}${config.percentUnit}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function updateOutputs(stats: RuntimeStats) {
|
|
249
|
+
const runtimeValue = getRuntimeValue(stats.runtimeMinutes);
|
|
250
|
+
const band = getBandText(stats.runtimeMinutes);
|
|
251
|
+
els.totalLoad!.textContent = stats.totalWatts.toFixed(0);
|
|
252
|
+
els.usableEnergy!.textContent = stats.usableWh.toFixed(0);
|
|
253
|
+
els.recommended!.textContent = stats.recommendedVa.toFixed(0);
|
|
254
|
+
els.runtime!.textContent = formatRuntime(stats.runtimeMinutes);
|
|
255
|
+
els.runtimeBig!.textContent = runtimeValue.value;
|
|
256
|
+
els.runtimeUnit!.textContent = runtimeValue.unit;
|
|
257
|
+
els.summary!.textContent = `${config.summaryPrefix} ${band} ${stats.recommendedVa.toFixed(0)} ${config.vaUnit} / ${stats.recommendedWatts.toFixed(0)} ${config.wattsUnit}.`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function updateMeters(stats: RuntimeStats) {
|
|
261
|
+
const runtimeFill = clampFill(stats.runtimeMinutes * 2, 6);
|
|
262
|
+
const loadFill = clampFill(stats.totalWatts / 8, 8);
|
|
263
|
+
const vaFill = clampFill(stats.recommendedVa / 12, 8);
|
|
264
|
+
const energyFill = clampFill(stats.usableWh / 8, 8);
|
|
265
|
+
root?.style.setProperty('--ups-fill', `${runtimeFill}%`);
|
|
266
|
+
root?.style.setProperty('--ups-load-fill', `${loadFill}%`);
|
|
267
|
+
els.loadBar!.style.width = `${loadFill}%`;
|
|
268
|
+
els.loadMeter!.style.setProperty('--meter-fill', `${loadFill}%`);
|
|
269
|
+
els.runtimeMeter!.style.setProperty('--meter-fill', `${runtimeFill}%`);
|
|
270
|
+
els.vaMeter!.style.setProperty('--meter-fill', `${vaFill}%`);
|
|
271
|
+
els.energyMeter!.style.setProperty('--meter-fill', `${energyFill}%`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function clampFill(value: number, min: number) {
|
|
275
|
+
return Math.max(min, Math.min(100, value));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function readNumber(input: HTMLInputElement | null | undefined, fallback: number) {
|
|
279
|
+
return input ? Number(input.value) : fallback;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function getRuntimeValue(minutes: number) {
|
|
283
|
+
if (minutes >= 60) {
|
|
284
|
+
return { value: (minutes / 60).toFixed(1), unit: config.hours };
|
|
285
|
+
}
|
|
286
|
+
return { value: minutes.toFixed(0), unit: config.minutes };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getBandText(minutes: number) {
|
|
290
|
+
if (minutes >= 30) {
|
|
291
|
+
return config.bandLight;
|
|
292
|
+
}
|
|
293
|
+
if (minutes >= 12) {
|
|
294
|
+
return config.bandBalanced;
|
|
295
|
+
}
|
|
296
|
+
return config.bandHeavy;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function formatRuntime(minutes: number): string {
|
|
300
|
+
if (minutes >= 60) return `${(minutes / 60).toFixed(1)} ${config.hours}`;
|
|
301
|
+
return `${minutes.toFixed(0)} ${config.minutes}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function renderDevices() {
|
|
305
|
+
els.devices!.innerHTML = '';
|
|
306
|
+
devices.forEach((device, index) => els.devices!.append(createDeviceRow(device, index)));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function createDeviceRow(device: Device, index: number) {
|
|
310
|
+
const row = document.createElement('div');
|
|
311
|
+
const nameInput = createNameInput(device, index);
|
|
312
|
+
const wattsInput = createWattsInput(device, index);
|
|
313
|
+
row.className = 'ups-device';
|
|
314
|
+
row.append(nameInput, createWattsLabel(wattsInput), createRemoveButton(index));
|
|
315
|
+
return row;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function createNameInput(device: Device, index: number) {
|
|
319
|
+
const input = document.createElement('input');
|
|
320
|
+
input.type = 'text';
|
|
321
|
+
input.className = 'ups-device-name';
|
|
322
|
+
input.ariaLabel = config.deviceName;
|
|
323
|
+
input.value = device.name;
|
|
324
|
+
input.addEventListener('input', () => {
|
|
325
|
+
devices[index]!.name = input.value;
|
|
326
|
+
saveState();
|
|
327
|
+
});
|
|
328
|
+
return input;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function createWattsInput(device: Device, index: number) {
|
|
332
|
+
const input = document.createElement('input');
|
|
333
|
+
input.type = 'number';
|
|
334
|
+
input.min = '0';
|
|
335
|
+
input.max = '2000';
|
|
336
|
+
input.step = '5';
|
|
337
|
+
input.value = String(device.watts);
|
|
338
|
+
input.addEventListener('input', () => {
|
|
339
|
+
devices[index]!.watts = Number(input.value);
|
|
340
|
+
calculate();
|
|
341
|
+
});
|
|
342
|
+
return input;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function createWattsLabel(input: HTMLInputElement) {
|
|
346
|
+
const label = document.createElement('label');
|
|
347
|
+
const text = document.createElement('span');
|
|
348
|
+
const field = document.createElement('span');
|
|
349
|
+
const unit = document.createElement('em');
|
|
350
|
+
text.textContent = config.watts;
|
|
351
|
+
field.className = 'ups-number-field';
|
|
352
|
+
unit.textContent = config.wattsUnit;
|
|
353
|
+
field.append(input, unit);
|
|
354
|
+
label.append(text, field);
|
|
355
|
+
return label;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function createRemoveButton(index: number) {
|
|
359
|
+
const button = document.createElement('button');
|
|
360
|
+
button.type = 'button';
|
|
361
|
+
button.ariaLabel = config.remove;
|
|
362
|
+
appendTrashIcon(button);
|
|
363
|
+
button.addEventListener('click', () => {
|
|
364
|
+
devices.splice(index, 1);
|
|
365
|
+
renderDevices();
|
|
366
|
+
calculate();
|
|
367
|
+
});
|
|
368
|
+
return button;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function appendTrashIcon(button: HTMLButtonElement) {
|
|
372
|
+
if (els.trashIcon?.content) button.append(els.trashIcon.content.cloneNode(true));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
els.add?.addEventListener('click', () => {
|
|
376
|
+
devices.push({ name: config.deviceName, watts: 25 });
|
|
377
|
+
renderDevices();
|
|
378
|
+
calculate();
|
|
379
|
+
});
|
|
380
|
+
[els.battery, els.efficiency, els.powerFactor, els.reserve].forEach((input) => input?.addEventListener('input', calculate));
|
|
381
|
+
|
|
382
|
+
renderDevices();
|
|
383
|
+
calculate();
|
|
384
|
+
</script>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HardwareToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { UpsRuntimeCalculatorUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type UpsRuntimeCalculatorLocaleContent = ToolLocaleContent<UpsRuntimeCalculatorUI>;
|
|
5
|
+
|
|
6
|
+
export const upsRuntimeCalculator: HardwareToolEntry<UpsRuntimeCalculatorUI> = {
|
|
7
|
+
id: 'ups-runtime-calculator',
|
|
8
|
+
icons: {
|
|
9
|
+
bg: 'mdi:battery-clock',
|
|
10
|
+
fg: 'mdi:power-plug-battery',
|
|
11
|
+
},
|
|
12
|
+
i18n: {
|
|
13
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
14
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
15
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
16
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
17
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
18
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
19
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
20
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
21
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
22
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
23
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
24
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
25
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
26
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
27
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
28
|
+
},
|
|
29
|
+
};
|