| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- """Tests for the write-path device-setting key aliases (issue #18 / #20).
- Several inverter firmwares expose the same logical control under a different
- writable-config key name than the one this integration was written against —
- the same class of bug #13 fixed for sensor reads, here for control writes.
- Confirmed by lukaszkwapien's full writable-config dump for a FCHAO inverter
- (#18): Output Source Priority is `setOutputSourcePriority` on that firmware,
- not the documented `outputSourcePrioritySetting`, and several other controls
- (battery charge/discharge limit, grid charge limit) aren't exposed under any
- key name at all on that device.
- """
- from __future__ import annotations
- import pytest
- from custom_components.solar_of_things.api import (
- SolarOfThingsAPI,
- resolve_setting_key,
- )
- from custom_components.solar_of_things.const import SETTING_KEY_ALIASES
- # Trimmed from lukaszkwapien's writable-config dump for a FCHAO inverter
- # (#18) — a real device that exposes Output Source Priority under the
- # alternate key and does not expose the three limit settings at all.
- FCHAO_SETTINGS = {
- "setOutputSourcePriority": {"key": "setOutputSourcePriority", "value": 0, "valueDisplay": "Utilty"},
- "chargeSourcePrioirty": {"key": "chargeSourcePrioirty", "value": 0, "valueDisplay": "Solar And Utility"},
- "flotingChargingVoltageSetting": {"value": 54.0},
- }
- # A device that uses every documented key name as-is.
- DOCUMENTED_SETTINGS = {
- "outputSourcePrioritySetting": {"key": "outputSourcePrioritySetting", "value": 1},
- "chargerSourcePrioritySetting": {"key": "chargerSourcePrioritySetting", "value": 0},
- "acInputRangeSetting": {"key": "acInputRangeSetting", "value": 0},
- "batteryPowerLimitingSetting": {"key": "batteryPowerLimitingSetting", "value": 1},
- "batteryChargeLimit": {"key": "batteryChargeLimit", "value": 80},
- "batteryDischargeLimit": {"key": "batteryDischargeLimit", "value": 20},
- "gridChargeLimit": {"key": "gridChargeLimit", "value": 3000},
- }
- # ─── resolve_setting_key ────────────────────────────────────────────────────
- def test_resolves_the_documented_key_when_present():
- assert resolve_setting_key(DOCUMENTED_SETTINGS, "outputSourcePrioritySetting") == "outputSourcePrioritySetting"
- def test_resolves_the_fchao_alternate_key_for_output_source_priority():
- assert resolve_setting_key(FCHAO_SETTINGS, "outputSourcePrioritySetting") == "setOutputSourcePriority"
- def test_documented_key_wins_when_a_device_has_both():
- """The canonical name is tried first in SETTING_KEY_ALIASES — if a device
- somehow exposed both, prefer the documented one rather than the alias."""
- both = {**DOCUMENTED_SETTINGS, "setOutputSourcePriority": {"value": 9}}
- assert resolve_setting_key(both, "outputSourcePrioritySetting") == "outputSourcePrioritySetting"
- @pytest.mark.parametrize(
- "canonical",
- ["batteryChargeLimit", "batteryDischargeLimit", "gridChargeLimit"],
- )
- def test_returns_none_when_a_device_exposes_neither_name(canonical):
- """FCHAO's dump does not contain these keys under any known spelling —
- must resolve to None so the entity can report unavailable, rather than
- fall back to a guess that would send a write guaranteed to fail with
- code=70134 "Config attribute not exists"."""
- assert resolve_setting_key(FCHAO_SETTINGS, canonical) is None
- @pytest.mark.parametrize("settings", [None, {}, [], "text", 0])
- def test_hostile_input_resolves_to_none(settings):
- assert resolve_setting_key(settings, "outputSourcePrioritySetting") is None
- def test_every_alias_group_lists_the_canonical_name_first():
- """SETTING_KEY_ALIASES documents preference order; the canonical
- (documented) key must always be tried before any alternate."""
- for canonical, aliases in SETTING_KEY_ALIASES.items():
- assert aliases[0] == canonical, canonical
- # ─── API write methods pick up the resolved key ────────────────────────────
- @pytest.fixture
- def api_stub():
- """An API instance with _write_setting stubbed to record what it was
- called with, instead of making a network request."""
- api = SolarOfThingsAPI(iot_token="test-token")
- calls = []
- api._write_setting = lambda device_id, key, value: calls.append((device_id, key, value))
- return api, calls
- def test_set_operating_mode_uses_the_fchao_alternate_key(api_stub):
- api, calls = api_stub
- api.set_operating_mode("device-1", "Utility First (USO)", settings=FCHAO_SETTINGS)
- assert calls == [("device-1", "setOutputSourcePriority", 0)]
- def test_set_operating_mode_uses_the_documented_key_by_default(api_stub):
- api, calls = api_stub
- api.set_operating_mode("device-2", "Utility First (USO)", settings=DOCUMENTED_SETTINGS)
- assert calls == [("device-2", "outputSourcePrioritySetting", 0)]
- def test_set_operating_mode_falls_back_to_the_documented_key_when_settings_is_none(api_stub):
- """No settings passed (e.g. an older caller) -> unchanged pre-#18 behavior."""
- api, calls = api_stub
- api.set_operating_mode("device-3", "Solar First (SUB)", settings=None)
- assert calls == [("device-3", "outputSourcePrioritySetting", 1)]
- def test_set_backup_mode_uses_the_fchao_alternate_key_too(api_stub):
- """set_backup_mode controls the same underlying setting as
- set_operating_mode and must resolve through the same alias."""
- api, calls = api_stub
- api.set_backup_mode("device-4", True, settings=FCHAO_SETTINGS)
- assert calls == [("device-4", "setOutputSourcePriority", 2)]
- def test_set_battery_charge_limit_falls_back_when_key_is_confirmed_absent(api_stub):
- """No known alternate exists for this key on FCHAO — _resolve_write_key
- falls back to the documented name rather than raising, since preventing
- the doomed call is the entity's `available=False` job, not this one's."""
- api, calls = api_stub
- api.set_battery_charge_limit("device-5", 80, settings=FCHAO_SETTINGS)
- assert calls == [("device-5", "batteryChargeLimit", 80)]
|