@energiok/node-red-contrib-pricecontrol-thermal 1.0.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/.eslintrc.js +15 -0
- package/CHANGELOG.md +1 -0
- package/COMPARISON-BEST-SAVE-VS-SMART-THERMAL.md +264 -0
- package/DASHBOARD-TEMPLATE-3-MODES.md +288 -0
- package/LICENSE +1 -0
- package/README.md +10 -0
- package/SIMPLIFIED-CONFIG.md +92 -0
- package/package.json +53 -0
- package/src/strategy-smart-thermal-functions.js +1017 -0
- package/src/strategy-smart-thermal.html +245 -0
- package/src/strategy-smart-thermal.js +289 -0
- package/test/strategy-smart-thermal-functions.test.js +874 -0
- package/test/strategy-smart-thermal-node.test.js +390 -0
- package/test-3day-prices.js +96 -0
- package/test-cold-weather.js +43 -0
- package/test-day-transition.js +161 -0
- package/test-find-params.js +78 -0
- package/test-hourly-alignment.js +114 -0
- package/test-price-analysis.js +77 -0
- package/test-runway.js +118 -0
- package/test-saturday-peak.js +132 -0
- package/test-step-by-step.js +213 -0
- package/test-temp-scaling.js +62 -0
- package/test-thermal-budget.js +174 -0
- package/test-timezone.js +83 -0
- package/test-utc-machine.js +90 -0
package/.eslintrc.js
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[The changelog has moved here](https://powersaver.no/changelog/#change-log)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Comparison: Best Save vs Smart Thermal Strategy
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Both nodes solve the same fundamental problem: **minimize electricity cost while maintaining system requirements**. However, they use different approaches.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Best Save Strategy (Deleted/Old)
|
|
10
|
+
|
|
11
|
+
### Core Algorithm
|
|
12
|
+
Uses a **greedy optimization** approach that:
|
|
13
|
+
1. Calculates "saving per minute" for every possible off-sequence
|
|
14
|
+
2. Sorts sequences by total savings (highest first)
|
|
15
|
+
3. Iteratively picks the best sequences that satisfy constraints
|
|
16
|
+
|
|
17
|
+
### Key Features
|
|
18
|
+
|
|
19
|
+
#### 1. Recovery Mechanism
|
|
20
|
+
```javascript
|
|
21
|
+
// After being off, system must recover
|
|
22
|
+
recoveryPercentage: 100 // % of off-time that must be on afterward
|
|
23
|
+
recoveryMaxMinutes: 60 // Cap on recovery time
|
|
24
|
+
```
|
|
25
|
+
**Example**: If system is off for 4 hours (240 min), it must be on for 2-4 hours after (depending on recoveryPercentage).
|
|
26
|
+
|
|
27
|
+
#### 2. Constraint Validation
|
|
28
|
+
```javascript
|
|
29
|
+
function isOnOffSequencesOk(onOff, maxMinutesOff, minMinutesOff, recoveryPercentage, recoveryMaxMinutes)
|
|
30
|
+
```
|
|
31
|
+
- **maxMinutesOff**: Maximum continuous off time (prevents damage)
|
|
32
|
+
- **minMinutesOff**: Minimum off duration (prevents short-cycling)
|
|
33
|
+
- **recoveryPercentage**: Ensures sufficient recovery after long off periods
|
|
34
|
+
- **recoveryMaxMinutes**: Caps recovery time (prevents excessive on-time)
|
|
35
|
+
|
|
36
|
+
#### 3. Minimum Savings Threshold
|
|
37
|
+
```javascript
|
|
38
|
+
minSaving: 0.05 // Only turn off if saving > 5 øre/kWh
|
|
39
|
+
```
|
|
40
|
+
Prevents turning off for trivial savings.
|
|
41
|
+
|
|
42
|
+
#### 4. Day-Before Context
|
|
43
|
+
```javascript
|
|
44
|
+
lastValueDayBefore = undefined
|
|
45
|
+
lastCountDayBefore = 0
|
|
46
|
+
```
|
|
47
|
+
Considers previous day's state to ensure constraints span across days.
|
|
48
|
+
|
|
49
|
+
### Pros
|
|
50
|
+
- **Optimal** for the given constraints (maximizes savings)
|
|
51
|
+
- **Flexible** - works for any device (water heater, heat pump, etc.)
|
|
52
|
+
- **Recovery logic** ensures system doesn't stay off too long
|
|
53
|
+
- **Cross-day awareness** prevents constraint violations at midnight
|
|
54
|
+
|
|
55
|
+
### Cons
|
|
56
|
+
- **Complex algorithm** (nested loops, sorting, filtering)
|
|
57
|
+
- **No temperature awareness** (doesn't consider thermal dynamics)
|
|
58
|
+
- **Binary on/off** (no intermediate states)
|
|
59
|
+
- **Assumes constant load** when on
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Smart Thermal Strategy (Current)
|
|
64
|
+
|
|
65
|
+
### Core Algorithm
|
|
66
|
+
Uses a **physics-based** approach that:
|
|
67
|
+
1. Calculates how long building can coast based on outdoor temperature
|
|
68
|
+
2. Identifies price peaks (top 20%)
|
|
69
|
+
3. Schedules coast during peaks, boost before peaks
|
|
70
|
+
|
|
71
|
+
### Key Features
|
|
72
|
+
|
|
73
|
+
#### 1. Thermal Modeling
|
|
74
|
+
```javascript
|
|
75
|
+
function calculateCoastTime(outdoorTemp, heatLossCoefficient = 0.05) {
|
|
76
|
+
const indoorTemp = 22;
|
|
77
|
+
const comfortMin = 20;
|
|
78
|
+
const tempDelta = indoorTemp - outdoorTemp;
|
|
79
|
+
const tempBuffer = indoorTemp - comfortMin;
|
|
80
|
+
const heatLossRate = tempDelta * heatLossCoefficient;
|
|
81
|
+
return (tempBuffer / heatLossRate) * 60; // minutes
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
**Adapts to weather**: Colder outdoor = shorter coast time allowed.
|
|
85
|
+
|
|
86
|
+
#### 2. Peak Detection
|
|
87
|
+
```javascript
|
|
88
|
+
function findPeaks(prices, topPercentage = 20)
|
|
89
|
+
```
|
|
90
|
+
- Identifies top 20% of prices as "expensive"
|
|
91
|
+
- Coasts during these periods only
|
|
92
|
+
|
|
93
|
+
#### 3. Anti-Short-Cycling
|
|
94
|
+
```javascript
|
|
95
|
+
minCoastMinutes: 60
|
|
96
|
+
minBoostMinutes: 60
|
|
97
|
+
function enforceMinimumDurations(modes, intervalMinutes, minCoastMinutes, minBoostMinutes)
|
|
98
|
+
```
|
|
99
|
+
Prevents rapid mode switching (similar to minMinutesOff in best-save).
|
|
100
|
+
|
|
101
|
+
#### 4. Simple Output
|
|
102
|
+
```javascript
|
|
103
|
+
output: "boost" or "coast" // Just mode strings
|
|
104
|
+
```
|
|
105
|
+
User decides actual temperatures/settings.
|
|
106
|
+
|
|
107
|
+
### Pros
|
|
108
|
+
- **Temperature-aware** - considers outdoor temp and thermal mass
|
|
109
|
+
- **Simple to understand** - coast during expensive periods
|
|
110
|
+
- **User-friendly config** - only 5 parameters
|
|
111
|
+
- **Weather adaptive** - automatically adjusts to conditions
|
|
112
|
+
|
|
113
|
+
### Cons
|
|
114
|
+
- **Not optimal** - doesn't maximize savings, just avoids peaks
|
|
115
|
+
- **Assumes thermal model** - may not fit all buildings
|
|
116
|
+
- **No recovery logic** - doesn't ensure minimum on-time after long coast
|
|
117
|
+
- **No minimum savings** - will coast even for small price differences
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## What Smart Thermal Could Learn from Best Save
|
|
122
|
+
|
|
123
|
+
### 1. **Recovery Logic** ⭐ MOST VALUABLE
|
|
124
|
+
The best-save recovery mechanism ensures the system doesn't stay off too long and has adequate recovery time.
|
|
125
|
+
|
|
126
|
+
**How to add to Smart Thermal**:
|
|
127
|
+
```javascript
|
|
128
|
+
// After coasting, ensure minimum boost time for recovery
|
|
129
|
+
const coastDuration = calculateCoastDuration(schedule);
|
|
130
|
+
const minBoostAfterCoast = Math.max(
|
|
131
|
+
coastDuration * (recoveryPercentage / 100),
|
|
132
|
+
minBoostMinutes
|
|
133
|
+
);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Example**: If system coasts for 4 hours during a price peak, require at least 2-4 hours of boost afterward to ensure building is properly heated.
|
|
137
|
+
|
|
138
|
+
### 2. **Minimum Savings Threshold** ⭐ VALUABLE
|
|
139
|
+
Prevents turning off for trivial price differences.
|
|
140
|
+
|
|
141
|
+
**How to add to Smart Thermal**:
|
|
142
|
+
```javascript
|
|
143
|
+
// Only coast if price difference justifies it
|
|
144
|
+
const avgPrice = calculateAverage(prices);
|
|
145
|
+
const peakPrice = item.price;
|
|
146
|
+
if (peakPrice - avgPrice < minSaving) {
|
|
147
|
+
mode = 'boost'; // Not worth coasting
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 3. **Cross-Day State Tracking**
|
|
152
|
+
Best-save considers yesterday's final state to ensure constraints don't break at midnight.
|
|
153
|
+
|
|
154
|
+
**How to add to Smart Thermal**:
|
|
155
|
+
```javascript
|
|
156
|
+
// Store last mode and duration in context
|
|
157
|
+
const lastMode = node.context().get('lastMode') || 'boost';
|
|
158
|
+
const lastModeDuration = node.context().get('lastModeDuration') || 0;
|
|
159
|
+
|
|
160
|
+
// Use when enforcing minimum durations
|
|
161
|
+
const effectiveModes = [
|
|
162
|
+
...fillArray(lastMode, lastModeDuration),
|
|
163
|
+
...todaysModes
|
|
164
|
+
];
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### 4. **Greedy Optimization** (Optional)
|
|
168
|
+
Best-save's algorithm finds the truly optimal solution, not just "avoid peaks".
|
|
169
|
+
|
|
170
|
+
**Trade-off**: Much more complex, probably not worth it for the thermal use case since weather/temperature dynamics dominate over pure price optimization.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Recommended Improvements to Smart Thermal
|
|
175
|
+
|
|
176
|
+
### Priority 1: Add Recovery Logic ⭐⭐⭐
|
|
177
|
+
```javascript
|
|
178
|
+
// Add to config
|
|
179
|
+
recoveryPercentage: 100 // Default: equal recovery time
|
|
180
|
+
recoveryMaxMinutes: 120 // Default: cap at 2 hours
|
|
181
|
+
|
|
182
|
+
// In createSchedule function
|
|
183
|
+
function enforceRecovery(schedule) {
|
|
184
|
+
let coastMinutes = 0;
|
|
185
|
+
for (let i = 0; i < schedule.length; i++) {
|
|
186
|
+
if (schedule[i].mode === 'coast') {
|
|
187
|
+
coastMinutes++;
|
|
188
|
+
} else if (coastMinutes > 0) {
|
|
189
|
+
// Just finished a coast period, ensure recovery
|
|
190
|
+
const minRecovery = Math.min(
|
|
191
|
+
coastMinutes * (recoveryPercentage / 100),
|
|
192
|
+
recoveryMaxMinutes / intervalMinutes
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Force next minRecovery intervals to be boost
|
|
196
|
+
for (let j = 0; j < minRecovery && i + j < schedule.length; j++) {
|
|
197
|
+
schedule[i + j].mode = 'boost';
|
|
198
|
+
}
|
|
199
|
+
coastMinutes = 0;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return schedule;
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Priority 2: Add Minimum Savings Threshold ⭐⭐
|
|
207
|
+
```javascript
|
|
208
|
+
// Add to config
|
|
209
|
+
minSaving: 5 // Only coast if saving > 5 øre/kWh
|
|
210
|
+
|
|
211
|
+
// In createSchedule function
|
|
212
|
+
const avgPrice = prices.reduce((sum, p) => sum + p.value, 0) / prices.length;
|
|
213
|
+
|
|
214
|
+
schedule.forEach(item => {
|
|
215
|
+
if (item.mode === 'coast' && item.price - avgPrice < minSaving) {
|
|
216
|
+
item.mode = 'boost'; // Not worth the savings
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Priority 3: Cross-Day Context ⭐
|
|
222
|
+
```javascript
|
|
223
|
+
// Store state at end of each schedule creation
|
|
224
|
+
node.context().set('lastScheduleEnd', {
|
|
225
|
+
mode: schedule[schedule.length - 1].mode,
|
|
226
|
+
duration: calculateDurationInSameMode(schedule)
|
|
227
|
+
}, contextStore);
|
|
228
|
+
|
|
229
|
+
// Use when enforcing minimum durations
|
|
230
|
+
const lastState = node.context().get('lastScheduleEnd', contextStore);
|
|
231
|
+
if (lastState) {
|
|
232
|
+
const combinedModes = [
|
|
233
|
+
...fillArray(lastState.mode, lastState.duration),
|
|
234
|
+
...currentModes
|
|
235
|
+
];
|
|
236
|
+
enforceMinimumDurations(combinedModes, ...);
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Conclusion
|
|
243
|
+
|
|
244
|
+
**Best Save** is an optimal general-purpose scheduler that maximizes savings for any binary on/off device.
|
|
245
|
+
|
|
246
|
+
**Smart Thermal** is a simpler, temperature-aware scheduler specifically designed for heating systems.
|
|
247
|
+
|
|
248
|
+
**Best additions from Best Save to Smart Thermal**:
|
|
249
|
+
1. ✅ **Recovery logic** - Ensure adequate heating after coast periods
|
|
250
|
+
2. ✅ **Minimum savings threshold** - Don't coast for trivial savings
|
|
251
|
+
3. ✅ **Cross-day context** - Better constraint enforcement at day boundaries
|
|
252
|
+
|
|
253
|
+
The greedy optimization algorithm from Best Save is **not recommended** because thermal dynamics (heat loss, outdoor temp) are more important than pure price optimization for heating systems.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Implementation Status
|
|
258
|
+
|
|
259
|
+
- [ ] Recovery logic
|
|
260
|
+
- [ ] Minimum savings threshold
|
|
261
|
+
- [ ] Cross-day context
|
|
262
|
+
- [x] Basic thermal modeling
|
|
263
|
+
- [x] Anti-short-cycling
|
|
264
|
+
- [x] Simple user interface
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# Dashboard Template for 3-Mode Smart Thermal
|
|
2
|
+
|
|
3
|
+
This template visualizes the three-state thermal optimizer: **normal**, **boost**, and **coast**.
|
|
4
|
+
|
|
5
|
+
## Colors
|
|
6
|
+
- **Blue** - Normal operation (default state)
|
|
7
|
+
- **Green** - Boost (pre-heat before peaks)
|
|
8
|
+
- **Red** - Coast (turn off during expensive periods)
|
|
9
|
+
|
|
10
|
+
## Template Code for ui-template node
|
|
11
|
+
|
|
12
|
+
```html
|
|
13
|
+
<template>
|
|
14
|
+
<div class="thermal-chart">
|
|
15
|
+
<div class="chart-header">
|
|
16
|
+
<h3>Price & Mode Schedule</h3>
|
|
17
|
+
<div class="legend">
|
|
18
|
+
<span class="legend-item normal"><span class="legend-color"></span> Normal</span>
|
|
19
|
+
<span class="legend-item boost"><span class="legend-color"></span> Boost (Pre-heat)</span>
|
|
20
|
+
<span class="legend-item coast"><span class="legend-color"></span> Coast (Turn Off)</span>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div v-if="msg.payload && msg.payload.schedule" class="schedule-container">
|
|
25
|
+
<div class="schedule-bar">
|
|
26
|
+
<div
|
|
27
|
+
v-for="(item, index) in msg.payload.schedule"
|
|
28
|
+
:key="index"
|
|
29
|
+
class="schedule-item"
|
|
30
|
+
:class="item.mode"
|
|
31
|
+
:style="{width: (100 / msg.payload.schedule.length) + '%'}"
|
|
32
|
+
:title="formatTime(item.time) + ': ' + item.price + ' øre/kWh (' + item.mode + ')'"
|
|
33
|
+
>
|
|
34
|
+
<div class="time-label" v-if="index % 4 === 0">{{ formatTime(item.time) }}</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="price-chart">
|
|
39
|
+
<svg viewBox="0 0 1000 200" preserveAspectRatio="none">
|
|
40
|
+
<!-- Price line -->
|
|
41
|
+
<polyline
|
|
42
|
+
:points="getPriceLinePoints()"
|
|
43
|
+
fill="none"
|
|
44
|
+
stroke="#2196F3"
|
|
45
|
+
stroke-width="2"
|
|
46
|
+
/>
|
|
47
|
+
<!-- Price points with mode colors -->
|
|
48
|
+
<circle
|
|
49
|
+
v-for="(item, index) in msg.payload.schedule"
|
|
50
|
+
:key="'point-' + index"
|
|
51
|
+
:cx="(index / (msg.payload.schedule.length - 1)) * 1000"
|
|
52
|
+
:cy="200 - ((item.price - minPrice) / priceRange) * 180"
|
|
53
|
+
r="3"
|
|
54
|
+
:fill="getModeColor(item.mode)"
|
|
55
|
+
/>
|
|
56
|
+
</svg>
|
|
57
|
+
|
|
58
|
+
<div class="y-axis">
|
|
59
|
+
<span class="y-label">{{ Math.round(maxPrice) }} øre</span>
|
|
60
|
+
<span class="y-label">{{ Math.round((maxPrice + minPrice) / 2) }} øre</span>
|
|
61
|
+
<span class="y-label">{{ Math.round(minPrice) }} øre</span>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="info-bar">
|
|
66
|
+
<span>Outdoor: {{ msg.payload.outdoorTemp }}°C</span>
|
|
67
|
+
<span>Current: {{ msg.payload.currentMode }}</span>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div v-else class="no-data">
|
|
72
|
+
Waiting for data...
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<script>
|
|
78
|
+
export default {
|
|
79
|
+
data() {
|
|
80
|
+
return {
|
|
81
|
+
msg: { payload: null }
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
computed: {
|
|
85
|
+
minPrice() {
|
|
86
|
+
if (!this.msg.payload?.schedule) return 0;
|
|
87
|
+
return Math.min(...this.msg.payload.schedule.map(s => s.price || 0));
|
|
88
|
+
},
|
|
89
|
+
maxPrice() {
|
|
90
|
+
if (!this.msg.payload?.schedule) return 100;
|
|
91
|
+
return Math.max(...this.msg.payload.schedule.map(s => s.price || 0));
|
|
92
|
+
},
|
|
93
|
+
priceRange() {
|
|
94
|
+
return this.maxPrice - this.minPrice || 1;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
methods: {
|
|
98
|
+
formatTime(timestamp) {
|
|
99
|
+
const date = new Date(timestamp);
|
|
100
|
+
return date.toLocaleTimeString('no-NO', { hour: '2-digit', minute: '2-digit' });
|
|
101
|
+
},
|
|
102
|
+
getPriceLinePoints() {
|
|
103
|
+
if (!this.msg.payload?.schedule) return '';
|
|
104
|
+
|
|
105
|
+
return this.msg.payload.schedule.map((item, index) => {
|
|
106
|
+
const x = (index / (this.msg.payload.schedule.length - 1)) * 1000;
|
|
107
|
+
const y = 200 - ((item.price - this.minPrice) / this.priceRange) * 180;
|
|
108
|
+
return `${x},${y}`;
|
|
109
|
+
}).join(' ');
|
|
110
|
+
},
|
|
111
|
+
getModeColor(mode) {
|
|
112
|
+
switch(mode) {
|
|
113
|
+
case 'boost': return '#4CAF50'; // Green
|
|
114
|
+
case 'coast': return '#FF5722'; // Red
|
|
115
|
+
case 'normal':
|
|
116
|
+
default: return '#2196F3'; // Blue
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
watch: {
|
|
121
|
+
msg: {
|
|
122
|
+
handler(newMsg) {
|
|
123
|
+
console.log('Received data:', newMsg);
|
|
124
|
+
},
|
|
125
|
+
deep: true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<style scoped>
|
|
132
|
+
.thermal-chart {
|
|
133
|
+
padding: 16px;
|
|
134
|
+
background: #fafafa;
|
|
135
|
+
border-radius: 8px;
|
|
136
|
+
font-family: Arial, sans-serif;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.chart-header {
|
|
140
|
+
display: flex;
|
|
141
|
+
justify-content: space-between;
|
|
142
|
+
align-items: center;
|
|
143
|
+
margin-bottom: 20px;
|
|
144
|
+
flex-wrap: wrap;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.chart-header h3 {
|
|
148
|
+
margin: 0;
|
|
149
|
+
color: #333;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.legend {
|
|
153
|
+
display: flex;
|
|
154
|
+
gap: 15px;
|
|
155
|
+
flex-wrap: wrap;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.legend-item {
|
|
159
|
+
display: flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
gap: 6px;
|
|
162
|
+
font-size: 13px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.legend-color {
|
|
166
|
+
width: 18px;
|
|
167
|
+
height: 18px;
|
|
168
|
+
border-radius: 3px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.legend-item.normal .legend-color {
|
|
172
|
+
background: #2196F3;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.legend-item.boost .legend-color {
|
|
176
|
+
background: #4CAF50;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.legend-item.coast .legend-color {
|
|
180
|
+
background: #FF5722;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.schedule-container {
|
|
184
|
+
background: white;
|
|
185
|
+
border-radius: 8px;
|
|
186
|
+
padding: 16px;
|
|
187
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.schedule-bar {
|
|
191
|
+
display: flex;
|
|
192
|
+
height: 60px;
|
|
193
|
+
border-radius: 4px;
|
|
194
|
+
overflow: hidden;
|
|
195
|
+
margin-bottom: 20px;
|
|
196
|
+
border: 1px solid #ddd;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.schedule-item {
|
|
200
|
+
position: relative;
|
|
201
|
+
cursor: pointer;
|
|
202
|
+
transition: opacity 0.2s;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.schedule-item:hover {
|
|
206
|
+
opacity: 0.8;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.schedule-item.normal {
|
|
210
|
+
background: linear-gradient(180deg, #42A5F5 0%, #2196F3 100%);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.schedule-item.boost {
|
|
214
|
+
background: linear-gradient(180deg, #66BB6A 0%, #4CAF50 100%);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.schedule-item.coast {
|
|
218
|
+
background: linear-gradient(180deg, #FF7043 0%, #FF5722 100%);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.time-label {
|
|
222
|
+
position: absolute;
|
|
223
|
+
bottom: -20px;
|
|
224
|
+
left: 0;
|
|
225
|
+
font-size: 10px;
|
|
226
|
+
color: #666;
|
|
227
|
+
white-space: nowrap;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.price-chart {
|
|
231
|
+
position: relative;
|
|
232
|
+
height: 200px;
|
|
233
|
+
margin: 20px 0;
|
|
234
|
+
padding-left: 60px;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.price-chart svg {
|
|
238
|
+
width: 100%;
|
|
239
|
+
height: 100%;
|
|
240
|
+
background: #f5f5f5;
|
|
241
|
+
border-radius: 4px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.y-axis {
|
|
245
|
+
position: absolute;
|
|
246
|
+
left: 0;
|
|
247
|
+
top: 0;
|
|
248
|
+
height: 100%;
|
|
249
|
+
display: flex;
|
|
250
|
+
flex-direction: column;
|
|
251
|
+
justify-content: space-between;
|
|
252
|
+
padding: 10px 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.y-label {
|
|
256
|
+
font-size: 12px;
|
|
257
|
+
color: #666;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.info-bar {
|
|
261
|
+
display: flex;
|
|
262
|
+
justify-content: space-between;
|
|
263
|
+
padding-top: 16px;
|
|
264
|
+
border-top: 1px solid #e0e0e0;
|
|
265
|
+
font-size: 14px;
|
|
266
|
+
color: #666;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.no-data {
|
|
270
|
+
text-align: center;
|
|
271
|
+
padding: 40px;
|
|
272
|
+
color: #999;
|
|
273
|
+
font-style: italic;
|
|
274
|
+
}
|
|
275
|
+
</style>
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Summary
|
|
279
|
+
|
|
280
|
+
**Three modes now supported:**
|
|
281
|
+
1. **Normal** (Blue) - Default operation, most of the time
|
|
282
|
+
2. **Boost** (Green) - Pre-heat when prices are significantly cheap AND there's an upcoming peak
|
|
283
|
+
3. **Coast** (Red) - Turn off when prices are significantly expensive
|
|
284
|
+
|
|
285
|
+
**Thresholds that control behavior:**
|
|
286
|
+
- `minCoastSaving`: Only coast if price is 10+ øre/kWh above average
|
|
287
|
+
- `minBoostSaving`: Only boost if price is 10+ øre/kWh below average
|
|
288
|
+
- This prevents mode switching for trivial price differences!
|
package/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# node-red-contrib-power-saver
|
|
2
|
+
|
|
3
|
+
A Node-RED node to save money when power prices are changing by the hour.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Please read more in the [documentation](https://powersaver.no/).
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
[](https://powersaver.no/contribute/#donate)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Simplified Configuration
|
|
2
|
+
|
|
3
|
+
The smart thermal node can run with three primary parameters. Everything else has sensible defaults.
|
|
4
|
+
|
|
5
|
+
## Configuration Parameters
|
|
6
|
+
|
|
7
|
+
### 1. Heat Loss Coefficient (0.03 - 0.08)
|
|
8
|
+
**Default:** 0.05
|
|
9
|
+
|
|
10
|
+
Controls how quickly your building loses heat. This is the only parameter you need to tune based on your building.
|
|
11
|
+
|
|
12
|
+
- **0.03** = Very well insulated modern house (slow heat loss)
|
|
13
|
+
- **0.05** = Average insulation (default)
|
|
14
|
+
- **0.08** = Older house, poor insulation (fast heat loss)
|
|
15
|
+
|
|
16
|
+
**How to tune:** Run it for a few days. If temperature drops too much during coast periods, increase the coefficient. If it does not coast enough, decrease it.
|
|
17
|
+
|
|
18
|
+
### 2. Minimum Mode Duration (minutes)
|
|
19
|
+
**Default:** 60 minutes
|
|
20
|
+
|
|
21
|
+
Minimum time the system stays in any mode (coast, boost, or normal) before switching. Prevents short-cycling that damages equipment.
|
|
22
|
+
|
|
23
|
+
- **30-45 min** = More responsive (switches modes more often)
|
|
24
|
+
- **60 min** = Balanced (default)
|
|
25
|
+
- **90+ min** = More stable (prioritizes comfort over optimization)
|
|
26
|
+
|
|
27
|
+
### 3. Minimum Savings Percent
|
|
28
|
+
**Default:** 5
|
|
29
|
+
|
|
30
|
+
Minimum price variation required before coast/boost can activate.
|
|
31
|
+
|
|
32
|
+
- **2-3%** = Aggressive (more mode changes)
|
|
33
|
+
- **5%** = Balanced (default)
|
|
34
|
+
- **8-12%** = Conservative (fewer mode changes)
|
|
35
|
+
|
|
36
|
+
## What Was Removed
|
|
37
|
+
|
|
38
|
+
The following legacy parameters are no longer supported:
|
|
39
|
+
|
|
40
|
+
- `peakThreshold`
|
|
41
|
+
- `troughThreshold`
|
|
42
|
+
- `minCoastSavingPercent`
|
|
43
|
+
- `minBoostSavingPercent`
|
|
44
|
+
- `minCoastSaving`
|
|
45
|
+
- `minBoostSaving`
|
|
46
|
+
- `minCoastMinutes`
|
|
47
|
+
- `minBoostMinutes`
|
|
48
|
+
- `minNormalMinutes`
|
|
49
|
+
|
|
50
|
+
## Example Configuration
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
{
|
|
54
|
+
heatLossCoefficient: 0.05, // Average insulation
|
|
55
|
+
minModeDuration: 60, // 1 hour minimum per mode
|
|
56
|
+
minSavingsPercent: 5 // Balanced price sensitivity
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## How It Works
|
|
61
|
+
|
|
62
|
+
1. Prices are clustered into cheap, normal, and expensive groups.
|
|
63
|
+
2. `minSavingsPercent` gates clustering when variation is too small.
|
|
64
|
+
3. Heat loss coefficient limits coast duration based on outdoor temperature.
|
|
65
|
+
4. Minimum mode duration prevents excessive mode switching.
|
|
66
|
+
|
|
67
|
+
## Migration from Old Config
|
|
68
|
+
|
|
69
|
+
**Old (many parameters):**
|
|
70
|
+
```javascript
|
|
71
|
+
{
|
|
72
|
+
heatLossCoefficient: 0.05,
|
|
73
|
+
peakThreshold: 20,
|
|
74
|
+
troughThreshold: 20,
|
|
75
|
+
minCoastSavingPercent: 5,
|
|
76
|
+
minBoostSavingPercent: 5,
|
|
77
|
+
minCoastSaving: undefined,
|
|
78
|
+
minBoostSaving: undefined,
|
|
79
|
+
minCoastMinutes: 60,
|
|
80
|
+
minBoostMinutes: 60,
|
|
81
|
+
minNormalMinutes: 60
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**New (3 parameters):**
|
|
86
|
+
```javascript
|
|
87
|
+
{
|
|
88
|
+
heatLossCoefficient: 0.05,
|
|
89
|
+
minModeDuration: 60,
|
|
90
|
+
minSavingsPercent: 5
|
|
91
|
+
}
|
|
92
|
+
```
|