@jjlmoya/utils-aquarium 1.59.0 → 1.60.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/tool/aquariumLightingPhotoperiodPlanner/aquarium-lighting-photoperiod-planner.css +9 -9
- package/src/tool/aquariumLightingPhotoperiodPlanner/component.astro +3 -1
- package/src/tool/aquariumLightingPhotoperiodPlanner/controller.ts +21 -9
- package/src/tool/aquariumLightingPhotoperiodPlanner/dom-views.test.ts +2 -1
- package/src/tool/aquariumLightingPhotoperiodPlanner/dom-views.ts +20 -8
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/de.ts +2 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/en.ts +2 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/es.ts +2 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/fr.ts +2 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/id.ts +2 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/it.ts +2 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/ja.ts +2 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/ko.ts +2 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/nl.ts +2 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/pl.ts +2 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/pt.ts +2 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/ru.ts +2 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/sv.ts +2 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/tr.ts +2 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/i18n/zh.ts +2 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/logic.test.ts +10 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/logic.ts +10 -0
- package/src/tool/aquariumLightingPhotoperiodPlanner/storage.ts +12 -8
- package/src/tool/aquariumLightingPhotoperiodPlanner/ui.ts +2 -0
package/package.json
CHANGED
package/src/tool/aquariumLightingPhotoperiodPlanner/aquarium-lighting-photoperiod-planner.css
CHANGED
|
@@ -259,7 +259,7 @@
|
|
|
259
259
|
|
|
260
260
|
.aqlp-summary-row {
|
|
261
261
|
display: grid;
|
|
262
|
-
grid-template-columns: repeat(
|
|
262
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
263
263
|
gap: 24px;
|
|
264
264
|
margin-top: 22px;
|
|
265
265
|
padding: 14px 0;
|
|
@@ -617,6 +617,14 @@
|
|
|
617
617
|
gap: 5px;
|
|
618
618
|
}
|
|
619
619
|
|
|
620
|
+
.aqlp-summary-row {
|
|
621
|
+
gap: 12px;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.aqlp-summary-row div + div {
|
|
625
|
+
padding-left: 12px;
|
|
626
|
+
}
|
|
627
|
+
|
|
620
628
|
.aqlp-event-row span {
|
|
621
629
|
display: grid;
|
|
622
630
|
grid-template-columns: auto minmax(0, 1fr);
|
|
@@ -633,14 +641,6 @@
|
|
|
633
641
|
text-align: right;
|
|
634
642
|
}
|
|
635
643
|
|
|
636
|
-
.aqlp-summary-row {
|
|
637
|
-
gap: 12px;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
.aqlp-summary-row div + div {
|
|
641
|
-
padding-left: 12px;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
644
|
.aqlp-co2-row {
|
|
645
645
|
grid-template-columns: auto minmax(0, 1fr);
|
|
646
646
|
}
|
|
@@ -40,7 +40,7 @@ const initialFullPhase = initial.phases.find((phase) => phase.phase === 'full');
|
|
|
40
40
|
<path class="aqlp-chart-fill" data-chart-fill d={initialChart.fill}></path>
|
|
41
41
|
<path class="aqlp-chart-line" data-chart-line d={initialChart.line}></path>
|
|
42
42
|
<circle class="aqlp-chart-marker aqlp-chart-marker-edge" data-chart-marker-start cx={(initialMarkers.start / 1440) * 960} cy="220" r="8"></circle>
|
|
43
|
-
<circle class="aqlp-chart-marker aqlp-chart-marker-full" data-chart-marker-full cx={(initialMarkers.full / 1440) * 960} cy=
|
|
43
|
+
<circle class="aqlp-chart-marker aqlp-chart-marker-full" data-chart-marker-full cx={(initialMarkers.full / 1440) * 960} cy={220 - (initial.input.intensityPercent / 100) * 180} r="8"></circle>
|
|
44
44
|
<circle class="aqlp-chart-marker aqlp-chart-marker-edge" data-chart-marker-end cx={(initialMarkers.end / 1440) * 960} cy="220" r="8"></circle>
|
|
45
45
|
<text class="aqlp-chart-y-label" x="12" y="27">100%</text>
|
|
46
46
|
<text class="aqlp-chart-y-label" x="12" y="126">50%</text>
|
|
@@ -62,6 +62,7 @@ const initialFullPhase = initial.phases.find((phase) => phase.phase === 'full');
|
|
|
62
62
|
<div class="aqlp-summary-row">
|
|
63
63
|
<div><span>{ui.targetBandLabel}</span><strong data-target-band>7 to 9 {ui.hoursUnit}</strong></div>
|
|
64
64
|
<div><span>{ui.fullIntensityLabel}</span><strong data-full-intensity>7 {ui.hoursUnit}</strong></div>
|
|
65
|
+
<div><span>{ui.fixtureLevelLabel}</span><strong data-intensity>{initial.input.intensityPercent}%</strong></div>
|
|
65
66
|
</div>
|
|
66
67
|
<div class="aqlp-co2-row" data-co2-row><span class="aqlp-co2-mark">CO₂</span><span><strong>{ui.co2Label}</strong><small>{ui.co2Hint}</small></span><strong data-co2-window>07:00 → 15:00</strong></div>
|
|
67
68
|
<div class="aqlp-signal" aria-live="polite"><span class="aqlp-signal-mark" aria-hidden="true"></span><p data-adjustment>{ui.adjustmentInside}</p></div>
|
|
@@ -76,6 +77,7 @@ const initialFullPhase = initial.phases.find((phase) => phase.phase === 'full');
|
|
|
76
77
|
<fieldset class="aqlp-control aqlp-choice-control"><legend>{ui.plantProfileLabel}</legend><div class="aqlp-choice-group" data-choice-group="plantProfile" role="group" aria-label={ui.plantProfileLabel}><button type="button" class="aqlp-choice" data-choice="plantProfile" data-value="none" aria-pressed="false">{ui.plantProfileNone}</button><button type="button" class="aqlp-choice" data-choice="plantProfile" data-value="low" aria-pressed="true">{ui.plantProfileLow}</button><button type="button" class="aqlp-choice" data-choice="plantProfile" data-value="high" aria-pressed="false">{ui.plantProfileHigh}</button></div></fieldset>
|
|
77
78
|
<div class="aqlp-control"><label for="aqlp-start-time">{ui.startTimeLabel}</label><input id="aqlp-start-time" class="aqlp-input" data-field="startTime" type="time" value={initial.input.startTime} /></div>
|
|
78
79
|
<div class="aqlp-control aqlp-range-control"><div class="aqlp-label-line"><label for="aqlp-photoperiod">{ui.photoperiodLabel}</label><output data-output="photoperiodHours">8 {ui.hoursUnit}</output></div><input id="aqlp-photoperiod" data-field="photoperiodHours" data-unit={ui.hoursUnit} type="range" min="6" max="12" step="0.5" value={initial.input.photoperiodHours} /></div>
|
|
80
|
+
<div class="aqlp-control aqlp-range-control"><div class="aqlp-label-line"><label for="aqlp-intensity">{ui.intensityLabel}</label><output data-output="intensityPercent">70%</output></div><input id="aqlp-intensity" data-field="intensityPercent" data-unit="%" type="range" min="10" max="100" step="5" value={initial.input.intensityPercent} /></div>
|
|
79
81
|
</div>
|
|
80
82
|
|
|
81
83
|
<details class="aqlp-advanced"><summary class="aqlp-details-summary">{ui.optionalRampsLabel}<span>{ui.optionalRampsSummary}</span></summary><div class="aqlp-ramp-grid"><div class="aqlp-control aqlp-range-control"><div class="aqlp-label-line"><label for="aqlp-ramp-up">{ui.rampUpLabel}</label><output data-output="rampUpMinutes">30 {ui.minutesUnit}</output></div><input id="aqlp-ramp-up" data-field="rampUpMinutes" data-unit={ui.minutesUnit} type="range" min="0" max="180" step="15" value={initial.input.rampUpMinutes} /></div><div class="aqlp-control aqlp-range-control"><div class="aqlp-label-line"><label for="aqlp-ramp-down">{ui.rampDownLabel}</label><output data-output="rampDownMinutes">30 {ui.minutesUnit}</output></div><input id="aqlp-ramp-down" data-field="rampDownMinutes" data-unit={ui.minutesUnit} type="range" min="0" max="180" step="15" value={initial.input.rampDownMinutes} /></div></div></details>
|
|
@@ -21,24 +21,29 @@ function readNumber(
|
|
|
21
21
|
return Number.isFinite(value) ? value : fallback;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function readChoice(root: ControllerRoot, field: string): string | undefined {
|
|
25
|
+
return root.querySelector<HTMLButtonElement>(
|
|
26
|
+
`[data-choice-group="${field}"] [aria-pressed="true"]`,
|
|
27
|
+
)?.dataset.value;
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
function readInput(root: ControllerRoot): LightingInput {
|
|
25
|
-
const aquariumType = root
|
|
26
|
-
|
|
27
|
-
)?.dataset.value as AquariumType | undefined;
|
|
28
|
-
const plantProfile = root.querySelector<HTMLButtonElement>(
|
|
29
|
-
'[data-choice-group="plantProfile"] [aria-pressed="true"]',
|
|
30
|
-
)?.dataset.value as PlantProfile | undefined;
|
|
31
|
+
const aquariumType = readChoice(root, "aquariumType") as AquariumType | undefined;
|
|
32
|
+
const plantProfile = readChoice(root, "plantProfile") as PlantProfile | undefined;
|
|
31
33
|
return {
|
|
32
34
|
aquariumType: aquariumType ?? DEFAULT_INPUT.aquariumType,
|
|
33
35
|
plantProfile: plantProfile ?? DEFAULT_INPUT.plantProfile,
|
|
34
|
-
startTime:
|
|
35
|
-
root.querySelector<HTMLInputElement>('[data-field="startTime"]')?.value ??
|
|
36
|
-
DEFAULT_INPUT.startTime,
|
|
36
|
+
startTime: root.querySelector<HTMLInputElement>('[data-field="startTime"]')?.value ?? DEFAULT_INPUT.startTime,
|
|
37
37
|
photoperiodHours: readNumber(
|
|
38
38
|
root,
|
|
39
39
|
"photoperiodHours",
|
|
40
40
|
DEFAULT_INPUT.photoperiodHours,
|
|
41
41
|
),
|
|
42
|
+
intensityPercent: readNumber(
|
|
43
|
+
root,
|
|
44
|
+
"intensityPercent",
|
|
45
|
+
DEFAULT_INPUT.intensityPercent,
|
|
46
|
+
),
|
|
42
47
|
rampUpMinutes: readNumber(
|
|
43
48
|
root,
|
|
44
49
|
"rampUpMinutes",
|
|
@@ -66,6 +71,7 @@ function setFields(root: ControllerRoot, input: LightingInput): void {
|
|
|
66
71
|
const fields = [
|
|
67
72
|
"startTime",
|
|
68
73
|
"photoperiodHours",
|
|
74
|
+
"intensityPercent",
|
|
69
75
|
"rampUpMinutes",
|
|
70
76
|
"rampDownMinutes",
|
|
71
77
|
] as const;
|
|
@@ -88,6 +94,12 @@ function setSliderOutputs(
|
|
|
88
94
|
String(input.photoperiodHours),
|
|
89
95
|
ui.hoursUnit,
|
|
90
96
|
);
|
|
97
|
+
updateSliderOutput(
|
|
98
|
+
root,
|
|
99
|
+
"intensityPercent",
|
|
100
|
+
String(input.intensityPercent),
|
|
101
|
+
"%",
|
|
102
|
+
);
|
|
91
103
|
updateSliderOutput(
|
|
92
104
|
root,
|
|
93
105
|
"rampUpMinutes",
|
|
@@ -19,7 +19,8 @@ describe("aquarium lighting photoperiod planner chart", () => {
|
|
|
19
19
|
|
|
20
20
|
expect(paths.line).toMatch(/^M 0\.00 220\.00/);
|
|
21
21
|
expect(paths.line).toContain("320.00 220.00");
|
|
22
|
-
expect(paths.line).toContain("340.00
|
|
22
|
+
expect(paths.line).toContain("340.00 94.00");
|
|
23
|
+
expect(paths.line).toContain("620.00 94.00");
|
|
23
24
|
expect(paths.line).toContain("640.00 220.00");
|
|
24
25
|
expect(paths.fill).toMatch(/L 960 220 L 0 220 Z$/);
|
|
25
26
|
});
|
|
@@ -18,22 +18,27 @@ function clockMinutes(value: string): number {
|
|
|
18
18
|
return hours * 60 + minutes;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function levelForPhase(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
function levelForPhase(
|
|
22
|
+
phase: PhaseSegment["phase"],
|
|
23
|
+
progress: number,
|
|
24
|
+
peak: number,
|
|
25
|
+
): number {
|
|
26
|
+
if (phase === "ramp-up") return progress * peak;
|
|
27
|
+
if (phase === "ramp-down") return (1 - progress) * peak;
|
|
28
|
+
if (phase === "full") return peak;
|
|
25
29
|
return 0;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
function chartSegment(
|
|
29
33
|
phase: PhaseSegment,
|
|
34
|
+
peak: number,
|
|
30
35
|
): Array<{ time: number; level: number }> {
|
|
31
36
|
const start = clockMinutes(phase.startTime);
|
|
32
37
|
const firstDuration = Math.min(phase.duration, 1440 - start);
|
|
33
38
|
const end = start + firstDuration;
|
|
34
|
-
const startLevel = levelForPhase(phase.phase, 0);
|
|
39
|
+
const startLevel = levelForPhase(phase.phase, 0, peak);
|
|
35
40
|
const endProgress = phase.duration ? firstDuration / phase.duration : 1;
|
|
36
|
-
const endLevel = levelForPhase(phase.phase, endProgress);
|
|
41
|
+
const endLevel = levelForPhase(phase.phase, endProgress, peak);
|
|
37
42
|
const points = [
|
|
38
43
|
{ time: start, level: startLevel },
|
|
39
44
|
{ time: end, level: endLevel },
|
|
@@ -42,7 +47,7 @@ function chartSegment(
|
|
|
42
47
|
if (secondDuration > 0) {
|
|
43
48
|
points.push(
|
|
44
49
|
{ time: 0, level: endLevel },
|
|
45
|
-
{ time: secondDuration, level: levelForPhase(phase.phase, 1) },
|
|
50
|
+
{ time: secondDuration, level: levelForPhase(phase.phase, 1, peak) },
|
|
46
51
|
);
|
|
47
52
|
}
|
|
48
53
|
return points;
|
|
@@ -58,8 +63,9 @@ export function getIntensityChartPaths(schedule: LightingSchedule): {
|
|
|
58
63
|
fill: string;
|
|
59
64
|
line: string;
|
|
60
65
|
} {
|
|
66
|
+
const peak = schedule.input.intensityPercent / 100;
|
|
61
67
|
const points = schedule.phases
|
|
62
|
-
.flatMap(chartSegment)
|
|
68
|
+
.flatMap((phase) => chartSegment(phase, peak))
|
|
63
69
|
.sort((left, right) => left.time - right.time);
|
|
64
70
|
const unique = points.filter(
|
|
65
71
|
(point, index) => index === 0 || point.time !== points[index - 1]?.time,
|
|
@@ -115,6 +121,7 @@ function updateAnswer(
|
|
|
115
121
|
"[data-full-intensity]",
|
|
116
122
|
`${formatHours(schedule.fullIntensityMinutes / 60)} ${ui.hoursUnit}`,
|
|
117
123
|
);
|
|
124
|
+
updateText(root, "[data-intensity]", `${schedule.input.intensityPercent}%`);
|
|
118
125
|
updateText(root, "[data-adjustment]", evaluation.adjustment);
|
|
119
126
|
updateText(
|
|
120
127
|
root,
|
|
@@ -171,6 +178,11 @@ function updateChart(root: RenderRoot, schedule: LightingSchedule): void {
|
|
|
171
178
|
startMarker.setAttribute("cx", String((markers.start / 1440) * 960));
|
|
172
179
|
if (fullMarker)
|
|
173
180
|
fullMarker.setAttribute("cx", String((markers.full / 1440) * 960));
|
|
181
|
+
if (fullMarker)
|
|
182
|
+
fullMarker.setAttribute(
|
|
183
|
+
"cy",
|
|
184
|
+
String(220 - (schedule.input.intensityPercent / 100) * 180),
|
|
185
|
+
);
|
|
174
186
|
if (endMarker)
|
|
175
187
|
endMarker.setAttribute("cx", String((markers.end / 1440) * 960));
|
|
176
188
|
}
|
|
@@ -44,6 +44,7 @@ const ui = {
|
|
|
44
44
|
plantProfileHigh: 'Lichtintensive Pflanzen',
|
|
45
45
|
startTimeLabel: 'Startzeit des Tageszyklus',
|
|
46
46
|
photoperiodLabel: 'Photoperiode Gesamtdauer',
|
|
47
|
+
intensityLabel: 'Einstellung der Lampenleistung',
|
|
47
48
|
rampUpLabel: 'Sonnenaufgang Aufdimmen',
|
|
48
49
|
rampDownLabel: 'Sonnenuntergang Abdimmen',
|
|
49
50
|
optionalRampsLabel: 'Optionale Dimmphasen',
|
|
@@ -56,6 +57,7 @@ const ui = {
|
|
|
56
57
|
targetBandLabel: 'Orientierungsrahmen',
|
|
57
58
|
lightWindowLabel: 'Beleuchtungsfenster',
|
|
58
59
|
fullIntensityLabel: 'Volle Lichtintensität',
|
|
60
|
+
fixtureLevelLabel: 'Lampenstufe',
|
|
59
61
|
lightsOnLabel: 'Licht an',
|
|
60
62
|
fullLightLabel: 'Volllicht',
|
|
61
63
|
lightsOffLabel: 'Licht aus',
|
|
@@ -44,6 +44,7 @@ const ui = {
|
|
|
44
44
|
plantProfileHigh: 'High light plants',
|
|
45
45
|
startTimeLabel: 'Cycle start time',
|
|
46
46
|
photoperiodLabel: 'Photoperiod',
|
|
47
|
+
intensityLabel: 'Fixture output setting',
|
|
47
48
|
rampUpLabel: 'Ramp up',
|
|
48
49
|
rampDownLabel: 'Ramp down',
|
|
49
50
|
optionalRampsLabel: 'Optional dimming ramps',
|
|
@@ -56,6 +57,7 @@ const ui = {
|
|
|
56
57
|
targetBandLabel: 'Planning window',
|
|
57
58
|
lightWindowLabel: 'Light window',
|
|
58
59
|
fullIntensityLabel: 'Full intensity',
|
|
60
|
+
fixtureLevelLabel: 'Fixture level',
|
|
59
61
|
lightsOnLabel: 'lights on',
|
|
60
62
|
fullLightLabel: 'full light',
|
|
61
63
|
lightsOffLabel: 'lights off',
|
|
@@ -29,6 +29,7 @@ const ui = {
|
|
|
29
29
|
plantProfileHigh: 'Plantas exigentes en luz',
|
|
30
30
|
startTimeLabel: 'Hora de inicio del ciclo',
|
|
31
31
|
photoperiodLabel: 'Fotoperiodo total',
|
|
32
|
+
intensityLabel: 'Potencia configurada de la pantalla',
|
|
32
33
|
rampUpLabel: 'Rampa de encendido amanecer',
|
|
33
34
|
rampDownLabel: 'Rampa de apagado atardecer',
|
|
34
35
|
optionalRampsLabel: 'Rampas de regulación opcionales',
|
|
@@ -41,6 +42,7 @@ const ui = {
|
|
|
41
42
|
targetBandLabel: 'Banda de planificación',
|
|
42
43
|
lightWindowLabel: 'Ventana de iluminación',
|
|
43
44
|
fullIntensityLabel: 'Intensidad completa',
|
|
45
|
+
fixtureLevelLabel: 'Nivel de la pantalla',
|
|
44
46
|
lightsOnLabel: 'encendido',
|
|
45
47
|
fullLightLabel: 'luz completa',
|
|
46
48
|
lightsOffLabel: 'apagado',
|
|
@@ -29,6 +29,7 @@ const ui = {
|
|
|
29
29
|
plantProfileHigh: 'Plantes exigeantes en lumiere',
|
|
30
30
|
startTimeLabel: 'Heure de début du cycle',
|
|
31
31
|
photoperiodLabel: 'Photopériode totale',
|
|
32
|
+
intensityLabel: 'Réglage de puissance de la rampe',
|
|
32
33
|
rampUpLabel: 'Rampe d allumage lever',
|
|
33
34
|
rampDownLabel: 'Rampe d extinction coucher',
|
|
34
35
|
optionalRampsLabel: 'Rampes de variation optionnelles',
|
|
@@ -41,6 +42,7 @@ const ui = {
|
|
|
41
42
|
targetBandLabel: 'Plage de référence',
|
|
42
43
|
lightWindowLabel: 'Fenêtre d eclairage',
|
|
43
44
|
fullIntensityLabel: 'Pleine intensité',
|
|
45
|
+
fixtureLevelLabel: 'Niveau de la rampe',
|
|
44
46
|
lightsOnLabel: 'allumage',
|
|
45
47
|
fullLightLabel: 'pleine lumiere',
|
|
46
48
|
lightsOffLabel: 'extinction',
|
|
@@ -29,6 +29,7 @@ const ui = {
|
|
|
29
29
|
plantProfileHigh: 'Tanaman cahaya tinggi',
|
|
30
30
|
startTimeLabel: 'Waktu mulai siklus',
|
|
31
31
|
photoperiodLabel: 'Total fotoperiode',
|
|
32
|
+
intensityLabel: 'Tingkat daya lampu',
|
|
32
33
|
rampUpLabel: 'Transisi redup terbit',
|
|
33
34
|
rampDownLabel: 'Transisi redup terbenam',
|
|
34
35
|
optionalRampsLabel: 'Transisi redup opsional',
|
|
@@ -41,6 +42,7 @@ const ui = {
|
|
|
41
42
|
targetBandLabel: 'Rentang acuan',
|
|
42
43
|
lightWindowLabel: 'Jendela pencahayaan',
|
|
43
44
|
fullIntensityLabel: 'Intensitas penuh',
|
|
45
|
+
fixtureLevelLabel: 'Level lampu',
|
|
44
46
|
lightsOnLabel: 'lampu menyala',
|
|
45
47
|
fullLightLabel: 'cahaya penuh',
|
|
46
48
|
lightsOffLabel: 'lampu padam',
|
|
@@ -29,6 +29,7 @@ const ui = {
|
|
|
29
29
|
plantProfileHigh: 'Piante esigenti di luce',
|
|
30
30
|
startTimeLabel: 'Ora di inizio del ciclo',
|
|
31
31
|
photoperiodLabel: 'Fotoperiodo totale',
|
|
32
|
+
intensityLabel: 'Potenza impostata della lampada',
|
|
32
33
|
rampUpLabel: 'Rampa di alba accensione',
|
|
33
34
|
rampDownLabel: 'Rampa di tramonto spegnimento',
|
|
34
35
|
optionalRampsLabel: 'Rampe di dimmerazione opzionali',
|
|
@@ -41,6 +42,7 @@ const ui = {
|
|
|
41
42
|
targetBandLabel: 'Fascia di riferimento',
|
|
42
43
|
lightWindowLabel: 'Finestra di illuminazione',
|
|
43
44
|
fullIntensityLabel: 'Piena intensità',
|
|
45
|
+
fixtureLevelLabel: 'Livello della lampada',
|
|
44
46
|
lightsOnLabel: 'accensione',
|
|
45
47
|
fullLightLabel: 'piena luce',
|
|
46
48
|
lightsOffLabel: 'spegnimento',
|
|
@@ -29,6 +29,7 @@ const ui = {
|
|
|
29
29
|
plantProfileHigh: '高光量(有茎草など)',
|
|
30
30
|
startTimeLabel: '点灯開始時間',
|
|
31
31
|
photoperiodLabel: '合計照射時間',
|
|
32
|
+
intensityLabel: '照明の出力設定',
|
|
32
33
|
rampUpLabel: '日の出(点灯調光)',
|
|
33
34
|
rampDownLabel: '日の入り(消灯調光)',
|
|
34
35
|
optionalRampsLabel: '調光設定(オプション)',
|
|
@@ -41,6 +42,7 @@ const ui = {
|
|
|
41
42
|
targetBandLabel: '推奨照射範囲',
|
|
42
43
|
lightWindowLabel: '照明タイマースケジュール',
|
|
43
44
|
fullIntensityLabel: '全灯点灯時間',
|
|
45
|
+
fixtureLevelLabel: '出力レベル',
|
|
44
46
|
lightsOnLabel: '点灯開始',
|
|
45
47
|
fullLightLabel: '全灯照射',
|
|
46
48
|
lightsOffLabel: '完全消灯',
|
|
@@ -29,6 +29,7 @@ const ui = {
|
|
|
29
29
|
plantProfileHigh: '양성 수초 (고광량)',
|
|
30
30
|
startTimeLabel: '조명 시작 시간',
|
|
31
31
|
photoperiodLabel: '총 조명 시간',
|
|
32
|
+
intensityLabel: '조명 출력 설정',
|
|
32
33
|
rampUpLabel: '일출 (점등 전환)',
|
|
33
34
|
rampDownLabel: '일몰 (소등 전환)',
|
|
34
35
|
optionalRampsLabel: '디밍 전환 설정 (선택)',
|
|
@@ -41,6 +42,7 @@ const ui = {
|
|
|
41
42
|
targetBandLabel: '권장 시간 범위',
|
|
42
43
|
lightWindowLabel: '조명 일정 시간표',
|
|
43
44
|
fullIntensityLabel: '최대 광량 시간',
|
|
45
|
+
fixtureLevelLabel: '조명 단계',
|
|
44
46
|
lightsOnLabel: '점등 시작',
|
|
45
47
|
fullLightLabel: '최대 광량',
|
|
46
48
|
lightsOffLabel: '완전 소등',
|
|
@@ -29,6 +29,7 @@ const ui = {
|
|
|
29
29
|
plantProfileHigh: 'Lichtbehoeftige planten',
|
|
30
30
|
startTimeLabel: 'Starttijd van de cyclus',
|
|
31
31
|
photoperiodLabel: 'Totale fotoperiode',
|
|
32
|
+
intensityLabel: 'Ingesteld lampvermogen',
|
|
32
33
|
rampUpLabel: 'Zonsopkomst (indimmen)',
|
|
33
34
|
rampDownLabel: 'Zonsondergang (uitdimmen)',
|
|
34
35
|
optionalRampsLabel: 'Optionele dimfasen',
|
|
@@ -41,6 +42,7 @@ const ui = {
|
|
|
41
42
|
targetBandLabel: 'Richtlijn tijdsduur',
|
|
42
43
|
lightWindowLabel: 'Verlichtingsvenster',
|
|
43
44
|
fullIntensityLabel: 'Volle lichtsterkte',
|
|
45
|
+
fixtureLevelLabel: 'Lampniveau',
|
|
44
46
|
lightsOnLabel: 'licht aan',
|
|
45
47
|
fullLightLabel: 'vol licht',
|
|
46
48
|
lightsOffLabel: 'licht uit',
|
|
@@ -29,6 +29,7 @@ const ui = {
|
|
|
29
29
|
plantProfileHigh: 'Rosliny swiatlolubne',
|
|
30
30
|
startTimeLabel: 'Czas startu cyklu',
|
|
31
31
|
photoperiodLabel: 'Calkowity fotoperiod',
|
|
32
|
+
intensityLabel: 'Ustawiona moc lampy',
|
|
32
33
|
rampUpLabel: 'Swit rozjasnianie',
|
|
33
34
|
rampDownLabel: 'Zmierzch sciemnianie',
|
|
34
35
|
optionalRampsLabel: 'Opcjonalne fazy sciemniania',
|
|
@@ -41,6 +42,7 @@ const ui = {
|
|
|
41
42
|
targetBandLabel: 'Zakres docelowy',
|
|
42
43
|
lightWindowLabel: 'Okno oswietlenia',
|
|
43
44
|
fullIntensityLabel: 'Pelna moc oswietlenia',
|
|
45
|
+
fixtureLevelLabel: 'Poziom lampy',
|
|
44
46
|
lightsOnLabel: 'wlaczanie',
|
|
45
47
|
fullLightLabel: 'pelne swiatlo',
|
|
46
48
|
lightsOffLabel: 'wylaczanie',
|
|
@@ -29,6 +29,7 @@ const ui = {
|
|
|
29
29
|
plantProfileHigh: 'Plantas exigentes em luz',
|
|
30
30
|
startTimeLabel: 'Hora de início do ciclo',
|
|
31
31
|
photoperiodLabel: 'Fotoperíodo total',
|
|
32
|
+
intensityLabel: 'Potência definida da luminária',
|
|
32
33
|
rampUpLabel: 'Rampa de amanhecer acendimento',
|
|
33
34
|
rampDownLabel: 'Rampa de anoitecer desligamento',
|
|
34
35
|
optionalRampsLabel: 'Rampas de regulação opcionais',
|
|
@@ -41,6 +42,7 @@ const ui = {
|
|
|
41
42
|
targetBandLabel: 'Faixa de referência',
|
|
42
43
|
lightWindowLabel: 'Janela de iluminação',
|
|
43
44
|
fullIntensityLabel: 'Intensidade total',
|
|
45
|
+
fixtureLevelLabel: 'Nível da luminária',
|
|
44
46
|
lightsOnLabel: 'acendimento',
|
|
45
47
|
fullLightLabel: 'luz total',
|
|
46
48
|
lightsOffLabel: 'desligamento',
|
|
@@ -29,6 +29,7 @@ const ui = {
|
|
|
29
29
|
plantProfileHigh: 'Светолюбивые растения',
|
|
30
30
|
startTimeLabel: 'Время начала светового дня',
|
|
31
31
|
photoperiodLabel: 'Общий фотопериод',
|
|
32
|
+
intensityLabel: 'Настройка мощности светильника',
|
|
32
33
|
rampUpLabel: 'Рассвет (плавное включение)',
|
|
33
34
|
rampDownLabel: 'Закат (плавное выключение)',
|
|
34
35
|
optionalRampsLabel: 'Фазы рассвета и заката (опционально)',
|
|
@@ -41,6 +42,7 @@ const ui = {
|
|
|
41
42
|
targetBandLabel: 'Рекомендуемый диапазон',
|
|
42
43
|
lightWindowLabel: 'График освещения',
|
|
43
44
|
fullIntensityLabel: 'Полная мощность света',
|
|
45
|
+
fixtureLevelLabel: 'Уровень светильника',
|
|
44
46
|
lightsOnLabel: 'включение',
|
|
45
47
|
fullLightLabel: 'полный свет',
|
|
46
48
|
lightsOffLabel: 'выключение',
|
|
@@ -29,6 +29,7 @@ const ui = {
|
|
|
29
29
|
plantProfileHigh: 'Ljuskrävande växter',
|
|
30
30
|
startTimeLabel: 'Starttid för cykeln',
|
|
31
31
|
photoperiodLabel: 'Total fotoperiod',
|
|
32
|
+
intensityLabel: 'Inställd lampeffekt',
|
|
32
33
|
rampUpLabel: 'Soluppgång (uppdimning)',
|
|
33
34
|
rampDownLabel: 'Solnedgång (neddimning)',
|
|
34
35
|
optionalRampsLabel: 'Valfria dimningsfaser',
|
|
@@ -41,6 +42,7 @@ const ui = {
|
|
|
41
42
|
targetBandLabel: 'Rekommenderat intervall',
|
|
42
43
|
lightWindowLabel: 'Belysningsfönster',
|
|
43
44
|
fullIntensityLabel: 'Full ljusstyrka',
|
|
45
|
+
fixtureLevelLabel: 'Lampnivå',
|
|
44
46
|
lightsOnLabel: 'tändning',
|
|
45
47
|
fullLightLabel: 'fullt ljus',
|
|
46
48
|
lightsOffLabel: 'släckning',
|
|
@@ -29,6 +29,7 @@ const ui = {
|
|
|
29
29
|
plantProfileHigh: 'Yüksek ışık isteyen bitkiler',
|
|
30
30
|
startTimeLabel: 'Döngü başlangıç saati',
|
|
31
31
|
photoperiodLabel: 'Toplam fotoperiyod',
|
|
32
|
+
intensityLabel: 'Armatür güç ayarı',
|
|
32
33
|
rampUpLabel: 'Gün doğumu geçişi (açılış)',
|
|
33
34
|
rampDownLabel: 'Gün batımı geçişi (kapanış)',
|
|
34
35
|
optionalRampsLabel: 'İsteğe bağlı karartma geçişleri',
|
|
@@ -41,6 +42,7 @@ const ui = {
|
|
|
41
42
|
targetBandLabel: 'Hedef zaman aralığı',
|
|
42
43
|
lightWindowLabel: 'Aydınlatma penceresi',
|
|
43
44
|
fullIntensityLabel: 'Tam ışık gücü',
|
|
45
|
+
fixtureLevelLabel: 'Armatür seviyesi',
|
|
44
46
|
lightsOnLabel: 'ışıklar açık',
|
|
45
47
|
fullLightLabel: 'tam ışık',
|
|
46
48
|
lightsOffLabel: 'ışıklar kapalı',
|
|
@@ -29,6 +29,7 @@ const ui = {
|
|
|
29
29
|
plantProfileHigh: '高光照水草(阳性草)',
|
|
30
30
|
startTimeLabel: '光照周期起始时间',
|
|
31
31
|
photoperiodLabel: '光照总时长',
|
|
32
|
+
intensityLabel: '灯具输出设置',
|
|
32
33
|
rampUpLabel: '日出渐进(开灯调光)',
|
|
33
34
|
rampDownLabel: '日落渐进(关灯调光)',
|
|
34
35
|
optionalRampsLabel: '可选渐变调光设置',
|
|
@@ -41,6 +42,7 @@ const ui = {
|
|
|
41
42
|
targetBandLabel: '建议光照范围',
|
|
42
43
|
lightWindowLabel: '光照时间窗口',
|
|
43
44
|
fullIntensityLabel: '全功率光照时长',
|
|
45
|
+
fixtureLevelLabel: '灯具档位',
|
|
44
46
|
lightsOnLabel: '开始开灯',
|
|
45
47
|
fullLightLabel: '全光照',
|
|
46
48
|
lightsOffLabel: '完全关灯',
|
|
@@ -16,6 +16,7 @@ describe("aquarium lighting photoperiod planner logic", () => {
|
|
|
16
16
|
expect(result.band).toEqual({ low: 7, high: 9 });
|
|
17
17
|
expect(result.photoperiodMinutes).toBe(480);
|
|
18
18
|
expect(result.fullIntensityMinutes).toBe(420);
|
|
19
|
+
expect(result.input.intensityPercent).toBe(70);
|
|
19
20
|
expect(result.phases.map((phase) => phase.phase)).toEqual([
|
|
20
21
|
"ramp-up",
|
|
21
22
|
"full",
|
|
@@ -112,4 +113,13 @@ describe("aquarium lighting photoperiod planner logic", () => {
|
|
|
112
113
|
expect(formatHours(8)).toBe("8");
|
|
113
114
|
expect(formatHours(8.5)).toBe("8.5");
|
|
114
115
|
});
|
|
116
|
+
|
|
117
|
+
it("normalizes the fixture output setting to safe five percent steps", () => {
|
|
118
|
+
const result = calculateSchedule({
|
|
119
|
+
...DEFAULT_INPUT,
|
|
120
|
+
intensityPercent: 73,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result.input.intensityPercent).toBe(75);
|
|
124
|
+
});
|
|
115
125
|
});
|
|
@@ -8,6 +8,7 @@ export interface LightingInput {
|
|
|
8
8
|
plantProfile: PlantProfile;
|
|
9
9
|
startTime: string;
|
|
10
10
|
photoperiodHours: number;
|
|
11
|
+
intensityPercent: number;
|
|
11
12
|
rampUpMinutes: number;
|
|
12
13
|
rampDownMinutes: number;
|
|
13
14
|
}
|
|
@@ -66,6 +67,7 @@ export const DEFAULT_INPUT: LightingInput = {
|
|
|
66
67
|
plantProfile: "low",
|
|
67
68
|
startTime: "08:00",
|
|
68
69
|
photoperiodHours: 8,
|
|
70
|
+
intensityPercent: 70,
|
|
69
71
|
rampUpMinutes: 30,
|
|
70
72
|
rampDownMinutes: 30,
|
|
71
73
|
};
|
|
@@ -76,6 +78,7 @@ export const PRESETS: Record<string, LightingInput> = {
|
|
|
76
78
|
aquariumType: "tropical",
|
|
77
79
|
plantProfile: "none",
|
|
78
80
|
photoperiodHours: 7,
|
|
81
|
+
intensityPercent: 55,
|
|
79
82
|
rampUpMinutes: 15,
|
|
80
83
|
rampDownMinutes: 15,
|
|
81
84
|
},
|
|
@@ -86,6 +89,7 @@ export const PRESETS: Record<string, LightingInput> = {
|
|
|
86
89
|
plantProfile: "none",
|
|
87
90
|
startTime: "10:00",
|
|
88
91
|
photoperiodHours: 8,
|
|
92
|
+
intensityPercent: 80,
|
|
89
93
|
rampUpMinutes: 60,
|
|
90
94
|
rampDownMinutes: 60,
|
|
91
95
|
},
|
|
@@ -124,6 +128,11 @@ function normalizePlantProfile(value: string): PlantProfile {
|
|
|
124
128
|
|
|
125
129
|
function normalizeInput(input: LightingInput): LightingInput {
|
|
126
130
|
const photoperiodHours = clamp(Number(input.photoperiodHours) || 8, 6, 12);
|
|
131
|
+
const intensityPercent = clamp(
|
|
132
|
+
Math.round((Number(input.intensityPercent) || 70) / 5) * 5,
|
|
133
|
+
10,
|
|
134
|
+
100,
|
|
135
|
+
);
|
|
127
136
|
const photoperiodMinutes = Math.round(photoperiodHours * 60);
|
|
128
137
|
const rampUpMinutes = clamp(Number(input.rampUpMinutes) || 0, 0, 180);
|
|
129
138
|
const rampDownMinutes = clamp(Number(input.rampDownMinutes) || 0, 0, 180);
|
|
@@ -138,6 +147,7 @@ function normalizeInput(input: LightingInput): LightingInput {
|
|
|
138
147
|
plantProfile: normalizePlantProfile(input.plantProfile),
|
|
139
148
|
startTime: formatClock(parseClock(input.startTime)),
|
|
140
149
|
photoperiodHours,
|
|
150
|
+
intensityPercent,
|
|
141
151
|
rampUpMinutes: Math.round(rampUpMinutes * scale),
|
|
142
152
|
rampDownMinutes: Math.round(rampDownMinutes * scale),
|
|
143
153
|
};
|
|
@@ -2,17 +2,21 @@ import type { LightingInput } from "./logic";
|
|
|
2
2
|
|
|
3
3
|
const STORAGE_KEY = "jjlmoya-aquarium-lighting-photoperiod-planner";
|
|
4
4
|
|
|
5
|
+
function hasLightingValues(candidate: Partial<LightingInput>): boolean {
|
|
6
|
+
const strings = [candidate.aquariumType, candidate.plantProfile, candidate.startTime];
|
|
7
|
+
const numbers = [
|
|
8
|
+
candidate.photoperiodHours,
|
|
9
|
+
candidate.intensityPercent,
|
|
10
|
+
candidate.rampUpMinutes,
|
|
11
|
+
candidate.rampDownMinutes,
|
|
12
|
+
];
|
|
13
|
+
return strings.every((value) => typeof value === "string") && numbers.every((value) => typeof value === "number");
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
function isLightingInput(value: unknown): value is LightingInput {
|
|
6
17
|
if (!value || typeof value !== "object") return false;
|
|
7
18
|
const candidate = value as Partial<LightingInput>;
|
|
8
|
-
return (
|
|
9
|
-
typeof candidate.aquariumType === "string" &&
|
|
10
|
-
typeof candidate.plantProfile === "string" &&
|
|
11
|
-
typeof candidate.startTime === "string" &&
|
|
12
|
-
typeof candidate.photoperiodHours === "number" &&
|
|
13
|
-
typeof candidate.rampUpMinutes === "number" &&
|
|
14
|
-
typeof candidate.rampDownMinutes === "number"
|
|
15
|
-
);
|
|
19
|
+
return hasLightingValues(candidate);
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
export function loadInput(): LightingInput | undefined {
|
|
@@ -13,6 +13,7 @@ export interface AquariumLightingPhotoperiodPlannerUI extends Record<
|
|
|
13
13
|
plantProfileHigh: string;
|
|
14
14
|
startTimeLabel: string;
|
|
15
15
|
photoperiodLabel: string;
|
|
16
|
+
intensityLabel: string;
|
|
16
17
|
rampUpLabel: string;
|
|
17
18
|
rampDownLabel: string;
|
|
18
19
|
optionalRampsLabel: string;
|
|
@@ -25,6 +26,7 @@ export interface AquariumLightingPhotoperiodPlannerUI extends Record<
|
|
|
25
26
|
targetBandLabel: string;
|
|
26
27
|
lightWindowLabel: string;
|
|
27
28
|
fullIntensityLabel: string;
|
|
29
|
+
fixtureLevelLabel: string;
|
|
28
30
|
lightsOnLabel: string;
|
|
29
31
|
fullLightLabel: string;
|
|
30
32
|
lightsOffLabel: string;
|