test_setting_key_aliases.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. """Tests for the write-path device-setting key aliases (issue #18 / #20).
  2. Several inverter firmwares expose the same logical control under a different
  3. writable-config key name than the one this integration was written against —
  4. the same class of bug #13 fixed for sensor reads, here for control writes.
  5. Confirmed by lukaszkwapien's full writable-config dump for a FCHAO inverter
  6. (#18): Output Source Priority is `setOutputSourcePriority` on that firmware,
  7. not the documented `outputSourcePrioritySetting`, and several other controls
  8. (battery charge/discharge limit, grid charge limit) aren't exposed under any
  9. key name at all on that device.
  10. """
  11. from __future__ import annotations
  12. import pytest
  13. from custom_components.solar_of_things.api import (
  14. SolarOfThingsAPI,
  15. resolve_setting_key,
  16. )
  17. from custom_components.solar_of_things.const import SETTING_KEY_ALIASES
  18. # Trimmed from lukaszkwapien's writable-config dump for a FCHAO inverter
  19. # (#18) — a real device that exposes Output Source Priority under the
  20. # alternate key and does not expose the three limit settings at all.
  21. FCHAO_SETTINGS = {
  22. "setOutputSourcePriority": {"key": "setOutputSourcePriority", "value": 0, "valueDisplay": "Utilty"},
  23. "chargeSourcePrioirty": {"key": "chargeSourcePrioirty", "value": 0, "valueDisplay": "Solar And Utility"},
  24. "flotingChargingVoltageSetting": {"value": 54.0},
  25. }
  26. # A device that uses every documented key name as-is.
  27. DOCUMENTED_SETTINGS = {
  28. "outputSourcePrioritySetting": {"key": "outputSourcePrioritySetting", "value": 1},
  29. "chargerSourcePrioritySetting": {"key": "chargerSourcePrioritySetting", "value": 0},
  30. "acInputRangeSetting": {"key": "acInputRangeSetting", "value": 0},
  31. "batteryPowerLimitingSetting": {"key": "batteryPowerLimitingSetting", "value": 1},
  32. "batteryChargeLimit": {"key": "batteryChargeLimit", "value": 80},
  33. "batteryDischargeLimit": {"key": "batteryDischargeLimit", "value": 20},
  34. "gridChargeLimit": {"key": "gridChargeLimit", "value": 3000},
  35. }
  36. # ─── resolve_setting_key ────────────────────────────────────────────────────
  37. def test_resolves_the_documented_key_when_present():
  38. assert resolve_setting_key(DOCUMENTED_SETTINGS, "outputSourcePrioritySetting") == "outputSourcePrioritySetting"
  39. def test_resolves_the_fchao_alternate_key_for_output_source_priority():
  40. assert resolve_setting_key(FCHAO_SETTINGS, "outputSourcePrioritySetting") == "setOutputSourcePriority"
  41. def test_documented_key_wins_when_a_device_has_both():
  42. """The canonical name is tried first in SETTING_KEY_ALIASES — if a device
  43. somehow exposed both, prefer the documented one rather than the alias."""
  44. both = {**DOCUMENTED_SETTINGS, "setOutputSourcePriority": {"value": 9}}
  45. assert resolve_setting_key(both, "outputSourcePrioritySetting") == "outputSourcePrioritySetting"
  46. @pytest.mark.parametrize(
  47. "canonical",
  48. ["batteryChargeLimit", "batteryDischargeLimit", "gridChargeLimit"],
  49. )
  50. def test_returns_none_when_a_device_exposes_neither_name(canonical):
  51. """FCHAO's dump does not contain these keys under any known spelling —
  52. must resolve to None so the entity can report unavailable, rather than
  53. fall back to a guess that would send a write guaranteed to fail with
  54. code=70134 "Config attribute not exists"."""
  55. assert resolve_setting_key(FCHAO_SETTINGS, canonical) is None
  56. @pytest.mark.parametrize("settings", [None, {}, [], "text", 0])
  57. def test_hostile_input_resolves_to_none(settings):
  58. assert resolve_setting_key(settings, "outputSourcePrioritySetting") is None
  59. def test_every_alias_group_lists_the_canonical_name_first():
  60. """SETTING_KEY_ALIASES documents preference order; the canonical
  61. (documented) key must always be tried before any alternate."""
  62. for canonical, aliases in SETTING_KEY_ALIASES.items():
  63. assert aliases[0] == canonical, canonical
  64. # ─── API write methods pick up the resolved key ────────────────────────────
  65. @pytest.fixture
  66. def api_stub():
  67. """An API instance with _write_setting stubbed to record what it was
  68. called with, instead of making a network request."""
  69. api = SolarOfThingsAPI(iot_token="test-token")
  70. calls = []
  71. api._write_setting = lambda device_id, key, value: calls.append((device_id, key, value))
  72. return api, calls
  73. def test_set_operating_mode_uses_the_fchao_alternate_key(api_stub):
  74. api, calls = api_stub
  75. api.set_operating_mode("device-1", "Utility First (USO)", settings=FCHAO_SETTINGS)
  76. assert calls == [("device-1", "setOutputSourcePriority", 0)]
  77. def test_set_operating_mode_uses_the_documented_key_by_default(api_stub):
  78. api, calls = api_stub
  79. api.set_operating_mode("device-2", "Utility First (USO)", settings=DOCUMENTED_SETTINGS)
  80. assert calls == [("device-2", "outputSourcePrioritySetting", 0)]
  81. def test_set_operating_mode_falls_back_to_the_documented_key_when_settings_is_none(api_stub):
  82. """No settings passed (e.g. an older caller) -> unchanged pre-#18 behavior."""
  83. api, calls = api_stub
  84. api.set_operating_mode("device-3", "Solar First (SUB)", settings=None)
  85. assert calls == [("device-3", "outputSourcePrioritySetting", 1)]
  86. def test_set_backup_mode_uses_the_fchao_alternate_key_too(api_stub):
  87. """set_backup_mode controls the same underlying setting as
  88. set_operating_mode and must resolve through the same alias."""
  89. api, calls = api_stub
  90. api.set_backup_mode("device-4", True, settings=FCHAO_SETTINGS)
  91. assert calls == [("device-4", "setOutputSourcePriority", 2)]
  92. def test_set_battery_charge_limit_falls_back_when_key_is_confirmed_absent(api_stub):
  93. """No known alternate exists for this key on FCHAO — _resolve_write_key
  94. falls back to the documented name rather than raising, since preventing
  95. the doomed call is the entity's `available=False` job, not this one's."""
  96. api, calls = api_stub
  97. api.set_battery_charge_limit("device-5", 80, settings=FCHAO_SETTINGS)
  98. assert calls == [("device-5", "batteryChargeLimit", 80)]