Skip to content

Commit 8c86924

Browse files
committed
Resolve a currency symbol to a code wherever it arrives
AlphaESS has started answering moneyType with the symbol rather than an ISO code - "€" where it used to send "EUR". Home Assistant's monetary device class only accepts a code, so the symbol had to be resolved before it reached a unit. The mapping already existed in the sensor platform, so the monetary units were never wrong, but two things around it were. The Currency Code sensor reports whatever the API sent, so it now reads "€" - a symbol, from a sensor named for a code, and the same value goes into the diagnostics download. The parser now resolves it once, so both report a code. The other is a symbol that names more than one currency. "$" resolved to USD whatever the owner's currency, which is wrong for every AUD, NZD and CAD system - a good share of them - and "¥" resolved to JPY for a Chinese one. An ambiguous symbol now resolves to the currency Home Assistant is configured for when that is one of the candidates, and to the most common otherwise. An unrecognised value still falls back rather than being guessed at. The mapping moves to its own module so the parser and the sensor platform share one answer, and it gains 元 and Kč on the way through.
1 parent edb85b5 commit 8c86924

6 files changed

Lines changed: 128 additions & 67 deletions

File tree

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,14 @@ discarding the list shape.
440440

441441
### Currency and daily history sensors
442442

443-
- Monetary sensors now use ISO 4217 currency codes when provided by the API, and fall back to Home Assistant's configured currency when not available.
443+
- Monetary sensors report an ISO 4217 currency code, which is what Home
444+
Assistant's monetary device class accepts. AlphaESS answers `moneyType` with
445+
either a code or the symbol and has changed which, so a symbol is resolved to
446+
a code. Where a symbol names more than one currency (`$`, `¥`, `kr`) the
447+
currency Home Assistant is configured for wins if it is one of them,
448+
otherwise the most common is used. An unrecognised value falls back to the
449+
configured currency rather than being guessed at. The `Currency Code`
450+
diagnostic sensor reports the resolved code, not the raw value.
444451
- Diagnostic currency sensors are available for troubleshooting:
445452
- `Currency Code`
446453
- Daily history energy breakdown sensors are exposed:

custom_components/alphaess/coordinator.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
SUBENTRY_TYPE_EV_CHARGER,
3232
SUBENTRY_TYPE_INVERTER,
3333
)
34+
from .currency import normalize_currency_unit
3435
from .enums import AlphaESSNames
3536

3637
_LOGGER = logging.getLogger(__name__)
@@ -277,7 +278,10 @@ def parse_summary_data(self, sum_data: dict, fallback_currency: str | None = Non
277278
AlphaESSNames.TodayIncome: self.dp.safe_get(sum_data, "todayIncome"),
278279
}
279280

280-
resolved = currency or fallback_currency or "Unknown"
281+
# moneyType is sometimes an ISO code and sometimes the symbol; the
282+
# sensor named "Currency Code" should report a code either way, and so
283+
# should the diagnostics download.
284+
resolved = normalize_currency_unit(currency, fallback_currency) or "Unknown"
281285
data[AlphaESSNames.CurrencyCode] = resolved
282286
data["Currency"] = resolved
283287

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Currency handling for the monetary sensors.
2+
3+
AlphaESS answers `moneyType` with either an ISO 4217 code or the currency
4+
symbol, and has changed which one it sends. Home Assistant's monetary device
5+
class only accepts a code, so a symbol has to be resolved to one.
6+
7+
Some symbols name more than one currency, and there the only honest tiebreak
8+
available is the currency Home Assistant is already configured for: a "$" from
9+
an inverter belonging to someone running AUD is far more likely to be AUD than
10+
USD.
11+
"""
12+
from __future__ import annotations
13+
14+
# Symbol -> the ISO codes that share it, most common first. A single-entry list
15+
# is unambiguous and is used whatever Home Assistant is configured for.
16+
SYMBOL_TO_ISO: dict[str, list[str]] = {
17+
"$": ["USD", "AUD", "CAD", "NZD", "SGD", "HKD", "MXN"],
18+
"€": ["EUR"],
19+
"£": ["GBP"],
20+
"¥": ["JPY", "CNY"],
21+
"元": ["CNY"],
22+
"₩": ["KRW"],
23+
"₹": ["INR"],
24+
"₽": ["RUB"],
25+
"₺": ["TRY"],
26+
"R$": ["BRL"],
27+
"₫": ["VND"],
28+
"₴": ["UAH"],
29+
"₱": ["PHP"],
30+
"₦": ["NGN"],
31+
"Fr": ["CHF"],
32+
"kr": ["SEK", "NOK", "DKK", "ISK"],
33+
"zł": ["PLN"],
34+
"Kč": ["CZK"],
35+
"A$": ["AUD"],
36+
"C$": ["CAD"],
37+
"NZ$": ["NZD"],
38+
"R": ["ZAR"],
39+
}
40+
41+
42+
def normalize_currency_unit(value: str | None, fallback: str | None) -> str | None:
43+
"""Return an ISO 4217 code for a `moneyType`, or the fallback.
44+
45+
A three-letter code is taken as-is. A symbol naming one currency resolves to
46+
it. A symbol naming several resolves to the configured currency when that is
47+
one of them, and otherwise to the most common. Anything unrecognised falls
48+
back rather than guessing.
49+
"""
50+
if value is None:
51+
return fallback
52+
53+
normalized = value.strip()
54+
if not normalized:
55+
return fallback
56+
57+
# Already a 3-letter ISO code
58+
if len(normalized) == 3 and normalized.isalpha():
59+
return normalized.upper()
60+
61+
candidates = SYMBOL_TO_ISO.get(normalized)
62+
if not candidates:
63+
# Unknown symbol - the configured currency is a better answer than a guess.
64+
return fallback
65+
66+
if len(candidates) > 1 and fallback:
67+
configured = fallback.strip().upper()
68+
if configured in candidates:
69+
return configured
70+
71+
return candidates[0]

custom_components/alphaess/sensor.py

Lines changed: 4 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
WIFI_STATUS_KEYS,
2020
)
2121
from .coordinator import AlphaESSDataUpdateCoordinator
22+
from .currency import normalize_currency_unit
2223
from .device import build_ev_charger_device_info, build_inverter_device_info
2324
from .enums import AlphaESSNames
2425
from .sensorlist import (
@@ -37,58 +38,6 @@
3738
# Map common currency symbols to ISO 4217 codes.
3839
# SensorDeviceClass.MONETARY expects an ISO 4217 currency code; raw symbols
3940
# (e.g. '€') would produce invalid-unit warnings and broken statistics.
40-
_SYMBOL_TO_ISO: dict[str, str] = {
41-
"$": "USD",
42-
"€": "EUR",
43-
"£": "GBP",
44-
"¥": "JPY",
45-
"₩": "KRW",
46-
"₹": "INR",
47-
"₽": "RUB",
48-
"₺": "TRY",
49-
"R$": "BRL",
50-
"₫": "VND",
51-
"₴": "UAH",
52-
"₱": "PHP",
53-
"₦": "NGN",
54-
"Fr": "CHF",
55-
"kr": "SEK",
56-
"zł": "PLN",
57-
"A$": "AUD",
58-
"C$": "CAD",
59-
"NZ$": "NZD",
60-
"R": "ZAR",
61-
}
62-
63-
64-
def _normalize_currency_unit(value: str | None, fallback: str | None) -> str | None:
65-
"""
66-
Normalize currency for monetary units to an ISO 4217 code.
67-
68-
SensorDeviceClass.MONETARY expects an ISO 4217 currency code.
69-
If the API returns a known symbol we map it; otherwise we fall back
70-
to the HA-configured currency.
71-
"""
72-
if value is None:
73-
return fallback
74-
75-
normalized = value.strip()
76-
if not normalized:
77-
return fallback
78-
79-
# Already a 3-letter ISO code
80-
if len(normalized) == 3 and normalized.isalpha():
81-
return normalized.upper()
82-
83-
# Try to map a symbol to its ISO code
84-
iso = _SYMBOL_TO_ISO.get(normalized)
85-
if iso:
86-
return iso
87-
88-
# Unknown symbol — fall back to HA's configured currency
89-
return fallback
90-
91-
9241
EV_RELATED_KEYS = {
9342
AlphaESSNames.evchargersn,
9443
AlphaESSNames.evchargermodel,
@@ -192,7 +141,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None:
192141

193142
data = coordinator.data[serial]
194143
model = data.get("Model")
195-
currency = _normalize_currency_unit(
144+
currency = normalize_currency_unit(
196145
data.get(AlphaESSNames.CurrencyCode) or data.get("Currency"),
197146
hass.config.currency,
198147
)
@@ -273,7 +222,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None:
273222
if not ev_charger:
274223
continue
275224

276-
currency = _normalize_currency_unit(
225+
currency = normalize_currency_unit(
277226
data.get(AlphaESSNames.CurrencyCode) or data.get("Currency"),
278227
hass.config.currency,
279228
)
@@ -306,7 +255,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None:
306255
if not ev_charger or ev_charger in ev_subentry_serials:
307256
continue
308257

309-
currency = _normalize_currency_unit(
258+
currency = normalize_currency_unit(
310259
data.get(AlphaESSNames.CurrencyCode) or data.get("Currency"),
311260
hass.config.currency,
312261
)

tests/test_coordinator_parsers.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,19 @@ def test_currency_unknown(self, parser):
110110
data = parser.parse_summary_data({})
111111
assert data[AlphaESSNames.CurrencyCode] == "Unknown"
112112

113+
def test_a_symbol_is_reported_as_a_code(self, parser):
114+
"""AlphaESS switched moneyType from "EUR" to "€"; the sensor is named
115+
Currency Code and Home Assistant's monetary class needs one."""
116+
data = parser.parse_summary_data({"moneyType": "€", "eload": 5})
117+
assert data[AlphaESSNames.CurrencyCode] == "EUR"
118+
assert data["Currency"] == "EUR"
119+
120+
def test_an_ambiguous_symbol_uses_the_configured_currency(self, parser):
121+
data = parser.parse_summary_data(
122+
{"moneyType": "$"}, fallback_currency="AUD",
123+
)
124+
assert data[AlphaESSNames.CurrencyCode] == "AUD"
125+
113126
def test_self_consumption_scaling(self, parser):
114127
data = parser.parse_summary_data(
115128
{"eselfConsumption": 0.5, "eselfSufficiency": 0.25}

tests/test_helpers.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
SUBENTRY_TYPE_EV_CHARGER,
1010
SUBENTRY_TYPE_INVERTER,
1111
)
12-
from custom_components.alphaess.sensor import _normalize_currency_unit
12+
from custom_components.alphaess.currency import normalize_currency_unit
1313

1414

1515
def _entry_with_subentries(subentries):
@@ -29,21 +29,38 @@ def _inverter_subentry(serial, ip="", model=""):
2929

3030
class TestNormalizeCurrencyUnit:
3131
def test_none_falls_back(self):
32-
assert _normalize_currency_unit(None, "EUR") == "EUR"
33-
assert _normalize_currency_unit("", "EUR") == "EUR"
34-
assert _normalize_currency_unit(" ", "EUR") == "EUR"
32+
assert normalize_currency_unit(None, "EUR") == "EUR"
33+
assert normalize_currency_unit("", "EUR") == "EUR"
34+
assert normalize_currency_unit(" ", "EUR") == "EUR"
3535

3636
def test_iso_code_passthrough(self):
37-
assert _normalize_currency_unit("gbp", "EUR") == "GBP"
38-
assert _normalize_currency_unit("USD", "EUR") == "USD"
37+
assert normalize_currency_unit("gbp", "EUR") == "GBP"
38+
assert normalize_currency_unit("USD", "EUR") == "USD"
3939

4040
def test_symbol_mapping(self):
41-
assert _normalize_currency_unit("€", "USD") == "EUR"
42-
assert _normalize_currency_unit("£", "USD") == "GBP"
43-
assert _normalize_currency_unit("$", "EUR") == "USD"
41+
assert normalize_currency_unit("€", "USD") == "EUR"
42+
assert normalize_currency_unit("£", "USD") == "GBP"
43+
assert normalize_currency_unit("$", "EUR") == "USD"
4444

4545
def test_unknown_symbol_falls_back(self):
46-
assert _normalize_currency_unit("☃", "AUD") == "AUD"
46+
assert normalize_currency_unit("☃", "AUD") == "AUD"
47+
48+
def test_an_ambiguous_symbol_prefers_the_configured_currency(self):
49+
"""AlphaESS sells into plenty of dollar and yen markets, and the symbol
50+
alone cannot tell them apart. What Home Assistant is set to can."""
51+
assert normalize_currency_unit("$", "AUD") == "AUD"
52+
assert normalize_currency_unit("$", "nzd") == "NZD"
53+
assert normalize_currency_unit("¥", "CNY") == "CNY"
54+
assert normalize_currency_unit("kr", "NOK") == "NOK"
55+
56+
def test_an_ambiguous_symbol_falls_to_the_common_one(self):
57+
assert normalize_currency_unit("$", "GBP") == "USD"
58+
assert normalize_currency_unit("$", None) == "USD"
59+
assert normalize_currency_unit("¥", "GBP") == "JPY"
60+
61+
def test_an_unambiguous_symbol_ignores_the_configured_currency(self):
62+
assert normalize_currency_unit("€", "AUD") == "EUR"
63+
assert normalize_currency_unit("A$", "USD") == "AUD"
4764

4865

4966
class TestBuildIpAddressMap:

0 commit comments

Comments
 (0)