@goodandready/dsh-cost-meter 0.8.25 → 0.8.27

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/lib/alerts.js ADDED
@@ -0,0 +1,100 @@
1
+ // dsh-cost-meter — tariff transition alerts logic (Issue #117).
2
+
3
+ export function inWindow(minute, w) {
4
+ return w.start < w.end
5
+ ? minute >= w.start && minute < w.end
6
+ : minute >= w.start || minute < w.end
7
+ }
8
+
9
+ export function ratesAt(schedule, minute) {
10
+ if (!schedule) return { cacheHit: 0, input: 0, cacheWrite: 0, output: 0 }
11
+ const m = ((minute % 1440) + 1440) % 1440
12
+ for (const w of schedule.windows || []) {
13
+ if (inWindow(m, w)) return w.rates
14
+ }
15
+ return schedule.base || { cacheHit: 0, input: 0, cacheWrite: 0, output: 0 }
16
+ }
17
+
18
+ export function minutesUntilChange(schedule, minute) {
19
+ if (!schedule || !schedule.windows || schedule.windows.length === 0) return 0
20
+ const m = ((minute % 1440) + 1440) % 1440
21
+ const edges = []
22
+ for (const w of schedule.windows) {
23
+ if (typeof w.start === 'number') edges.push(w.start)
24
+ if (typeof w.end === 'number') edges.push(w.end)
25
+ }
26
+ if (edges.length === 0) return 0
27
+ edges.sort((a, b) => a - b)
28
+ for (const edge of edges) {
29
+ if (edge > m) return edge - m
30
+ }
31
+ return 1440 - m + edges[0]
32
+ }
33
+
34
+ /**
35
+ * Check whether the current minute falls into the pre-switch alert window.
36
+ * Returns an alert descriptor or null if no alert is triggered.
37
+ */
38
+ export function checkTariffAlert(options) {
39
+ const {
40
+ schedule,
41
+ minute,
42
+ leadMinutes = 5,
43
+ enabled = true,
44
+ } = options || {}
45
+
46
+ if (!enabled || !schedule || !Array.isArray(schedule.windows) || schedule.windows.length === 0) {
47
+ return null
48
+ }
49
+
50
+ const m = ((minute % 1440) + 1440) % 1440
51
+ const left = minutesUntilChange(schedule, m)
52
+ const lead = Math.max(1, Math.min(30, Number(leadMinutes) || 5))
53
+
54
+ if (left <= 0 || left > lead) {
55
+ return null
56
+ }
57
+
58
+ const currentRates = ratesAt(schedule, m)
59
+ const nextMinute = (m + left + 1) % 1440
60
+ const nextRates = ratesAt(schedule, nextMinute)
61
+
62
+ const isSwitchingToPeak = (nextRates.output || 0) > (currentRates.output || 0)
63
+ const isSwitchingToOffPeak = (nextRates.output || 0) < (currentRates.output || 0)
64
+
65
+ // If rates do not change between current and next window, no alert
66
+ if (!isSwitchingToPeak && !isSwitchingToOffPeak) {
67
+ return null
68
+ }
69
+
70
+ const direction = isSwitchingToPeak ? 'peak' : 'offpeak'
71
+ // Stable transition window key to deduplicate notifications within the same switch
72
+ const targetMinute = (m + left) % 1440
73
+ const transitionKey = `${targetMinute}:${direction}`
74
+
75
+ return {
76
+ direction,
77
+ leftMinutes: left,
78
+ transitionKey,
79
+ currentRates,
80
+ nextRates,
81
+ targetMinute,
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Generate human-readable alert message in English and Chinese.
87
+ */
88
+ export function formatAlertMessage(alert, locale = 'en') {
89
+ if (!alert) return ''
90
+ const isPeak = alert.direction === 'peak'
91
+ const min = alert.leftMinutes
92
+ if (locale === 'zh') {
93
+ return isPeak
94
+ ? `高峰期计费将在 ${min} 分钟后开始`
95
+ : `特惠期将在 ${min} 分钟后开始(5折优惠)`
96
+ }
97
+ return isPeak
98
+ ? `Peak tariff starts in ${min} min (higher rates)`
99
+ : `Off-peak discount starts in ${min} min (-50% savings)`
100
+ }