forked from johanzander/bess-manager-beta
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPricingFormSection.tsx
More file actions
208 lines (197 loc) · 12.4 KB
/
Copy pathPricingFormSection.tsx
File metadata and controls
208 lines (197 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
import React from 'react';
import { numField, txtInput, radioGroup, toggle, SectionCard } from './FormHelpers';
export interface PricingForm {
currency: string;
provider: string;
nordpoolConfigEntryId: string;
nordpoolEntity: string;
octopusImportTodayEntity: string;
octopusImportTomorrowEntity: string;
octopusExportTodayEntity: string;
octopusExportTomorrowEntity: string;
entsoeEntity: string;
area: string;
markupRate: number;
vatMultiplier: number;
additionalCosts: number;
taxReduction: number;
spotMultiplier: number;
exportSpotMultiplier: number;
sellPriceEqualsBuyPrice: boolean;
}
interface Props {
form: PricingForm;
onChange: (f: PricingForm) => void;
}
export function PricingFormSection({ form, onChange }: Props) {
const isOctopus = form.provider === 'octopus';
const isEntsoe = form.provider === 'entsoe';
const currency = isOctopus ? 'GBP' : form.currency;
const sm = form.spotMultiplier ?? 1.0;
const esm = form.exportSpotMultiplier ?? 1.0;
const previewSpot = 1.0;
const previewBuy = Number(
((previewSpot * sm + form.markupRate) * form.vatMultiplier + form.additionalCosts).toFixed(4),
);
const previewSell = form.sellPriceEqualsBuyPrice
? previewBuy
: Number((previewSpot * esm + form.taxReduction).toFixed(4));
return (
<div className="space-y-3">
<SectionCard
title="Price Source"
description="Configure where to fetch your electricity prices from. Choose your provider and the relevant Home Assistant entities."
>
{radioGroup(
'Provider',
[
{ value: 'nordpool_official', label: 'Nord Pool (official HA integration)' },
{ value: 'nordpool_hacs', label: 'Nord Pool (HACS custom sensor)' },
{ value: 'octopus', label: 'Octopus Energy' },
{ value: 'entsoe', label: 'ENTSO-e / Belpex (Transparency Platform)' },
],
form.provider,
v => onChange({ ...form, provider: v }),
)}
{form.provider === 'nordpool_official' && (
<div className="space-y-3">
{txtInput('Config Entry ID', form.nordpoolConfigEntryId,
v => onChange({ ...form, nordpoolConfigEntryId: v }), 'Auto-detected…')}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{txtInput('Price Area', form.area, () => {}, 'Auto-detected…', { readOnly: true })}
{txtInput('Currency', form.currency,
v => onChange({ ...form, currency: v }), 'Auto-detected…',
{ readOnly: !!(form.area && form.area.length <= 5 && form.currency) })}
</div>
</div>
)}
{form.provider === 'nordpool_hacs' && (
<div className="space-y-3">
{txtInput('Sensor', form.nordpoolEntity,
v => onChange({ ...form, nordpoolEntity: v }), 'sensor.nordpool_…')}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{txtInput('Price Area', form.area, () => {}, 'Auto-detected…', { readOnly: true })}
{txtInput('Currency', form.currency,
v => onChange({ ...form, currency: v }), 'Auto-detected…',
{ readOnly: !!(form.area && form.area.length <= 5 && form.currency) })}
</div>
</div>
)}
{isOctopus && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{txtInput('Import today', form.octopusImportTodayEntity,
v => onChange({ ...form, octopusImportTodayEntity: v }))}
{txtInput('Import tomorrow', form.octopusImportTomorrowEntity,
v => onChange({ ...form, octopusImportTomorrowEntity: v }))}
{txtInput('Export today', form.octopusExportTodayEntity,
v => onChange({ ...form, octopusExportTodayEntity: v }))}
{txtInput('Export tomorrow', form.octopusExportTomorrowEntity,
v => onChange({ ...form, octopusExportTomorrowEntity: v }))}
</div>
)}
{form.provider === 'entsoe' && (
<div className="space-y-3">
{txtInput('Sensor', form.entsoeEntity,
v => onChange({ ...form, entsoeEntity: v }), 'sensor.…_average_electricity_price')}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{txtInput('Currency', form.currency, () => {}, 'Auto-detected…', { readOnly: true })}
</div>
</div>
)}
</SectionCard>
<SectionCard
title="Price Calculation"
description={isOctopus
? 'Octopus prices are already final (VAT-inclusive, GBP/kWh). Only tax reduction applies.'
: isEntsoe
? 'Calculate your actual electricity costs from ENTSO-e/Belpex spot prices. Supports both additive markup and multiplicative spot adjustments.'
: 'Calculate your actual electricity costs from spot prices, fees and taxes.'}
>
{!isOctopus && (
<>
{toggle('Sell price equals buy price (net metering / saldering)', form.sellPriceEqualsBuyPrice,
v => onChange({ ...form, sellPriceEqualsBuyPrice: v }))}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{isEntsoe && numField('Import Spot Multiplier', form.spotMultiplier,
v => onChange({ ...form, spotMultiplier: v }),
{ unit: 'factor (1.0 = no adjustment)', min: 0.5, max: 2.0, step: 0.0001 })}
{numField('Markup Rate', form.markupRate,
v => onChange({ ...form, markupRate: v }),
{ unit: `${currency}/kWh (ex-VAT)`, min: 0, step: 0.001 })}
{numField('VAT Multiplier', form.vatMultiplier,
v => onChange({ ...form, vatMultiplier: v }),
{ unit: 'factor', min: 1, step: 0.01 })}
{numField('Additional Costs', form.additionalCosts,
v => onChange({ ...form, additionalCosts: v }),
{ unit: `${currency}/kWh`, min: 0, step: 0.001 })}
{isEntsoe && !form.sellPriceEqualsBuyPrice && numField('Export Spot Multiplier', form.exportSpotMultiplier,
v => onChange({ ...form, exportSpotMultiplier: v }),
{ unit: 'factor (1.0 = no adjustment)', min: 0.5, max: 2.0, step: 0.0001 })}
{!form.sellPriceEqualsBuyPrice && numField('Export Compensation', form.taxReduction,
v => onChange({ ...form, taxReduction: v }),
{ unit: `${currency}/kWh`, min: 0, step: 0.001 })}
</div>
{isEntsoe ? (
<div className="rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800/40 px-4 py-3 space-y-3 text-xs text-gray-600 dark:text-gray-400">
<div className="space-y-2">
<div><span className="font-medium text-blue-900 dark:text-blue-200">Import Spot Multiplier:</span> Contract-specific factor applied to raw spot price. E.g. Luminus Dynamic: 1.0175.</div>
<div><span className="font-medium text-blue-900 dark:text-blue-200">Markup Rate:</span> Fixed costs before VAT: supplier margin, excise duty, grid fees, etc.</div>
<div><span className="font-medium text-blue-900 dark:text-blue-200">VAT Multiplier:</span> VAT factor on import. 1.06 = 6% (Belgium), 1.21 = 21% (Netherlands).</div>
<div><span className="font-medium text-blue-900 dark:text-blue-200">Additional Costs:</span> Post-VAT fixed costs. Set to 0 if all costs are already included above.</div>
<div><span className="font-medium text-blue-900 dark:text-blue-200">Export Spot Multiplier:</span> Contract-specific factor on spot for export/injection. E.g. Luminus: 1.018.</div>
<div><span className="font-medium text-blue-900 dark:text-blue-200">Export Compensation:</span> Fixed per-kWh payment or deduction for exported energy. Use negative values for deductions.</div>
</div>
<div className="space-y-2 pt-2 border-t border-blue-200 dark:border-blue-700">
<p className="font-medium text-blue-900 dark:text-blue-200">How the raw spot price is converted:</p>
<p className="pl-2 border-l-2 border-blue-300 dark:border-blue-600"><strong>Buy price:</strong> (spot × import multiplier + markup) × VAT + additional costs</p>
<p className="pl-2 border-l-2 border-blue-300 dark:border-blue-600"><strong>Sell price:</strong> {form.sellPriceEqualsBuyPrice ? 'same as buy price (net metering)' : 'spot × export multiplier + export compensation'}</p>
<div><span className="font-medium text-blue-900 dark:text-blue-200">Sell price equals buy price:</span> Enable under net metering ("saldering" in the Netherlands, in force through 2026): exported energy offsets imported energy 1:1 on your bill, so every exported kWh is worth the full buy price.</div>
</div>
</div>
) : (
<div className="rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800/40 px-4 py-3 space-y-3 text-xs text-gray-600 dark:text-gray-400">
<div className="space-y-2">
<div><span className="font-medium text-blue-900 dark:text-blue-200">Markup Rate:</span> Energy provider margin fee. E.g. Tibber 0.08 (8 öre/kWh), Ellevio ~0.15. Applied before VAT.</div>
<div><span className="font-medium text-blue-900 dark:text-blue-200">VAT Multiplier:</span> VAT factor. 1.25 = 25% (Sweden/Norway), 1.20 = 20% (UK/EU).</div>
<div><span className="font-medium text-blue-900 dark:text-blue-200">Additional Costs:</span> Grid transfer fee + energy tax (sum ex-VAT, then VAT applied). E.g. E.ON: (0.2584 + 0.3600) × 1.25 = 0.773 SEK/kWh.</div>
<div><span className="font-medium text-blue-900 dark:text-blue-200">Export Compensation:</span> Per-kWh payment from grid operator (Nätnytta) when selling surplus electricity. Check your energy bill under "Producent/Självfaktura". E.g. E.ON: 0.1988 (19.88 öre/kWh).</div>
</div>
<div className="space-y-2 pt-2 border-t border-blue-200 dark:border-blue-700">
<p className="font-medium text-blue-900 dark:text-blue-200">How the raw spot price is converted:</p>
<p className="pl-2 border-l-2 border-blue-300 dark:border-blue-600"><strong>Buy price:</strong> (raw spot + markup) × VAT multiplier + grid fees</p>
<p className="pl-2 border-l-2 border-blue-300 dark:border-blue-600"><strong>Sell price:</strong> {form.sellPriceEqualsBuyPrice ? 'same as buy price (net metering)' : 'raw spot + export compensation'}</p>
<p className="text-gray-500 dark:text-gray-500 italic">Note: Markup is added before VAT (ex-VAT), while grid fees already include VAT.</p>
<div><span className="font-medium text-blue-900 dark:text-blue-200">Sell price equals buy price:</span> Enable under net metering ("saldering" in the Netherlands, in force through 2026): exported energy offsets imported energy 1:1 on your bill, so every exported kWh is worth the full buy price. E.g. Tibber NL.</div>
</div>
</div>
)}
<div className="rounded-lg bg-gray-50 dark:bg-gray-700/50 px-4 py-3 text-sm space-y-1.5">
<p className="text-xs text-gray-500 dark:text-gray-400">
Preview at spot = 1.00
</p>
<div className="flex justify-between font-medium">
<span className="text-gray-700 dark:text-gray-200">Buy price</span>
<span className="text-blue-600 dark:text-blue-400">
{previewBuy.toFixed(2)} {currency}/kWh
</span>
</div>
<div className="flex justify-between font-medium">
<span className="text-gray-700 dark:text-gray-200">Sell price</span>
<span className="text-green-600 dark:text-green-400">
{previewSell.toFixed(2)} {currency}/kWh
</span>
</div>
</div>
</>
)}
{isOctopus && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{numField('Tax Reduction', form.taxReduction,
v => onChange({ ...form, taxReduction: v }),
{ unit: 'GBP/kWh credit on sold energy', min: 0, step: 0.001 })}
</div>
)}
</SectionCard>
</div>
);
}