@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 +100 -0
- package/lib/client.js +608 -7
- package/lib/commands.js +260 -0
- package/lib/export.js +378 -0
- package/lib/index.js +29 -22
- package/lib/projection.js +19 -0
- package/lib/routes.js +5 -0
- package/package.json +1 -1
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
|
+
}
|