Skip to content

Commit

Permalink
Fallback to "digit modes" if mode mapping failed (close dext0r#501)
Browse files Browse the repository at this point in the history
  • Loading branch information
dext0r committed Nov 6, 2024
1 parent 2d2d7b8 commit 4c4b81c
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 59 deletions.
52 changes: 19 additions & 33 deletions custom_components/yandex_smart_home/capability_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,18 @@ class ModeCapability(Capability[ModeCapabilityInstanceActionState], Protocol):
instance: ModeCapabilityInstance

_modes_map_default: dict[ModeCapabilityMode, list[str]] = {}
_modes_map_index_fallback: dict[int, ModeCapabilityMode] = {}
_modes_map_index_fallback: dict[int, ModeCapabilityMode] = {
0: ModeCapabilityMode.ONE,
1: ModeCapabilityMode.TWO,
2: ModeCapabilityMode.THREE,
3: ModeCapabilityMode.FOUR,
4: ModeCapabilityMode.FIVE,
5: ModeCapabilityMode.SIX,
6: ModeCapabilityMode.SEVEN,
7: ModeCapabilityMode.EIGHT,
8: ModeCapabilityMode.NINE,
9: ModeCapabilityMode.TEN,
}

@property
def supported(self) -> bool:
Expand Down Expand Up @@ -87,18 +98,19 @@ def get_yandex_mode_by_ha_mode(self, ha_mode: str, hide_warnings: bool = False)
mode = yandex_mode
break

if mode is None and self.modes_map_config is None and self._modes_map_index_fallback:
try:
mode = self._modes_map_index_fallback[self.supported_ha_modes.index(ha_mode)]
except (IndexError, ValueError, KeyError):
pass

if mode is not None and ha_mode not in self.supported_ha_modes:
raise APIError(
ResponseCode.INVALID_VALUE,
f"Unsupported HA mode '{ha_mode}' for {self}: not in {self.supported_ha_modes}",
)

if mode is None:
if self.modes_map_config is None and ha_mode.lower() != STATE_OFF:
try:
mode = self._modes_map_index_fallback[self.supported_ha_modes.index(ha_mode)]
except (IndexError, ValueError, KeyError):
pass

if mode is None and not hide_warnings:
if ha_mode.lower() not in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_NONE):
if ha_mode.lower() in [m.lower() for m in self.supported_ha_modes]:
Expand Down Expand Up @@ -247,19 +259,6 @@ class ProgramCapability(StateModeCapability, ABC):

instance = ModeCapabilityInstance.PROGRAM

_modes_map_index_fallback = {
0: ModeCapabilityMode.ONE,
1: ModeCapabilityMode.TWO,
2: ModeCapabilityMode.THREE,
3: ModeCapabilityMode.FOUR,
4: ModeCapabilityMode.FIVE,
5: ModeCapabilityMode.SIX,
6: ModeCapabilityMode.SEVEN,
7: ModeCapabilityMode.EIGHT,
8: ModeCapabilityMode.NINE,
9: ModeCapabilityMode.TEN,
}


@STATE_CAPABILITIES_REGISTRY.register
class ProgramCapabilityHumidifier(ProgramCapability):
Expand Down Expand Up @@ -420,19 +419,6 @@ class InputSourceCapability(StateModeCapability):

instance = ModeCapabilityInstance.INPUT_SOURCE

_modes_map_index_fallback = {
0: ModeCapabilityMode.ONE,
1: ModeCapabilityMode.TWO,
2: ModeCapabilityMode.THREE,
3: ModeCapabilityMode.FOUR,
4: ModeCapabilityMode.FIVE,
5: ModeCapabilityMode.SIX,
6: ModeCapabilityMode.SEVEN,
7: ModeCapabilityMode.EIGHT,
8: ModeCapabilityMode.NINE,
9: ModeCapabilityMode.TEN,
}

@property
def supported(self) -> bool:
"""Test if the capability is supported."""
Expand Down
4 changes: 2 additions & 2 deletions docs/config/modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
Со стороны УДЯ нет жесткой привязки значений режимов к типам устройств. Другими словами, у режима "Скорость вентиляции"
(`fan_speed`) значения могут быть не только "низкое", "высокое", но и совсем от другого типа устройств, например "дичь" или "эспрессо".

Если маппинг не удался - управление функцией через УДЯ будет недоступно.
Если маппинг не удался - управление функцией через УДЯ будет недоступно либо вместо реальных названий режимов будут использованы цифры от 1 до 10.

Если часть режимов не удалось связать, в [`Журнале сервера`](https://my.home-assistant.io/redirect/logs/) будут появляться ошибки `Unsupported mode "XXX"`.
Если часть режимов не удалось связать, в [`Журнале сервера`](https://my.home-assistant.io/redirect/logs/) могут появляться ошибки `Unsupported mode "XXX"`.

Для их устранения задайте соответствия режимов УДЯ <--> HA используя пример ниже,
или напишите в [чат](https://t.me/yandex_smart_home) в Telegram, и эта ошибка будет устранена в следующей версии.
Expand Down
2 changes: 1 addition & 1 deletion docs/troubleshoot.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@

## Unsupported mode "XXX" { id=unsupported-mode }

Не удалось связать режимы между УДЯ и Home Assistant. Если нет необходимости выбирать проблемные режимы - ошибку можно игнорировать.
Не удалось сопоставить режим из Home Assistant c режимом в УДЯ. Если нет необходимости выбирать проблемные режимы - ошибку можно игнорировать.

Для исправления: настройте [связь режимов](./config/modes.md) УДЯ <--> HA

Expand Down
47 changes: 24 additions & 23 deletions tests/test_capability_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,11 @@ def _ha_value(self) -> str | None:
return self.state.attributes.get("current_mode")


class MockFallbackModeCapability(MockModeCapabilityA):
class MockModeCapabilityAShortIndexFallback(MockModeCapabilityA):
_modes_map_index_fallback = {
0: ModeCapabilityMode.ONE,
1: ModeCapabilityMode.TWO,
2: ModeCapabilityMode.THREE,
3: ModeCapabilityMode.FOUR,
4: ModeCapabilityMode.FIVE,
5: ModeCapabilityMode.SIX,
6: ModeCapabilityMode.SEVEN,
7: ModeCapabilityMode.EIGHT,
8: ModeCapabilityMode.NINE,
9: ModeCapabilityMode.TEN,
}


Expand All @@ -70,29 +63,33 @@ async def test_capability_mode_unsupported(hass):

state = State("switch.test", STATE_OFF, {"modes_list": ["foo", "bar"]})
cap = MockModeCapabilityA(hass, BASIC_ENTRY_DATA, state)
assert cap.supported is False
assert cap.supported is True


async def test_capability_mode_auto_mapping(hass, caplog):
state = State("switch.test", STATE_OFF, {"modes_list": ["mode_1", "mode_3", "mode_4"]})
cap = MockModeCapabilityA(hass, BASIC_ENTRY_DATA, state)
state = State("switch.test", STATE_OFF, {"modes_list": ["mode_1", "mode_3", "mode_4", "mode_5"]})
cap = MockModeCapabilityAShortIndexFallback(hass, BASIC_ENTRY_DATA, state)

assert cap.supported is True
assert cap.supported_ha_modes == ["mode_1", "mode_3", "mode_4"]
assert cap.supported_yandex_modes == [ModeCapabilityMode.FOWL, ModeCapabilityMode.PUERH_TEA]
assert cap.supported_ha_modes == ["mode_1", "mode_3", "mode_4", "mode_5"]
assert cap.supported_yandex_modes == [
ModeCapabilityMode.FOWL,
ModeCapabilityMode.PUERH_TEA,
ModeCapabilityMode.THREE,
]
assert cap.parameters.dict() == {
"instance": "swing",
"modes": [{"value": "fowl"}, {"value": "puerh_tea"}],
"modes": [{"value": "fowl"}, {"value": "puerh_tea"}, {"value": "three"}],
}

assert cap.get_yandex_mode_by_ha_mode("invalid") is None
assert len(caplog.records) == 0

assert cap.get_yandex_mode_by_ha_mode("mode_4") is None
assert cap.get_yandex_mode_by_ha_mode("mode_5") is None
assert len(caplog.records) == 1
for record in caplog.records:
assert record.message == (
"Failed to get Yandex mode for mode 'mode_4' for instance swing of mode "
"Failed to get Yandex mode for mode 'mode_5' for instance swing of mode "
"capability of switch.test. It may cause inconsistencies between Yandex and "
"HA. See https://docs.yaha-cloud.ru/master/config/modes/"
)
Expand All @@ -103,12 +100,13 @@ async def test_capability_mode_auto_mapping(hass, caplog):

assert cap.get_yandex_mode_by_ha_mode("mode_1") == ModeCapabilityMode.FOWL
assert cap.get_yandex_mode_by_ha_mode("mode_3") == ModeCapabilityMode.PUERH_TEA
assert cap.get_yandex_mode_by_ha_mode("mode_4") == ModeCapabilityMode.THREE
with pytest.raises(APIError) as e: # strange case o_O
assert cap.get_yandex_mode_by_ha_mode("MODE_1")
assert e.value.code == ResponseCode.INVALID_VALUE
assert e.value.message == (
"Unsupported HA mode 'MODE_1' for instance swing of mode capability of "
"switch.test: not in ['mode_1', 'mode_3', 'mode_4']"
"switch.test: not in ['mode_1', 'mode_3', 'mode_4', 'mode_5']"
)

with pytest.raises(APIError) as e:
Expand Down Expand Up @@ -144,10 +142,10 @@ async def test_capability_mode_custom_mapping(hass):


async def test_capability_mode_fallback_index(hass):
state = State("switch.test", STATE_OFF, {"modes_list": ["some", "mode_1", "foo"]})
cap = MockFallbackModeCapability(hass, BASIC_ENTRY_DATA, state)
state = State("switch.test", STATE_OFF, {"modes_list": ["some", "mode_1", "foo", "off"]})
cap = MockModeCapabilityA(hass, BASIC_ENTRY_DATA, state)
assert cap.supported is True
assert cap.supported_ha_modes == ["some", "mode_1", "foo"]
assert cap.supported_ha_modes == ["some", "mode_1", "foo", "off"]
assert cap.supported_yandex_modes == [
ModeCapabilityMode.FOWL,
ModeCapabilityMode.ONE,
Expand All @@ -159,7 +157,7 @@ async def test_capability_mode_fallback_index(hass):
assert cap.get_yandex_mode_by_ha_mode("mode_1") == ModeCapabilityMode.FOWL

state = State("switch.test", STATE_OFF, {"modes_list": [f"mode_{v}" for v in range(0, 11)]})
cap = MockFallbackModeCapability(hass, BASIC_ENTRY_DATA, state)
cap = MockModeCapabilityA(hass, BASIC_ENTRY_DATA, state)
assert cap.supported is True
assert cap.get_yandex_mode_by_ha_mode("mode_9") == "ten"
assert cap.get_yandex_mode_by_ha_mode("mode_11") is None
Expand All @@ -176,7 +174,7 @@ async def test_capability_mode_fallback_index(hass):
}
}
)
cap = MockFallbackModeCapability(hass, entry_data, state)
cap = MockModeCapabilityA(hass, entry_data, state)
assert cap.supported is True
assert cap.supported_yandex_modes == ["americano", "baby_food"]

Expand Down Expand Up @@ -209,7 +207,10 @@ async def test_capability_mode_thermostat(hass):
get_exact_one_capability(hass, BASIC_ENTRY_DATA, state, CapabilityType.MODE, ModeCapabilityInstance.THERMOSTAT),
)
assert cap.retrievable is True
assert cap.parameters.dict() == {"instance": "thermostat", "modes": [{"value": "auto"}, {"value": "fan_only"}]}
assert cap.parameters.dict() == {
"instance": "thermostat",
"modes": [{"value": "auto"}, {"value": "fan_only"}],
}
assert cap.get_value() is None

cap.state.state = climate.HVACMode.FAN_ONLY
Expand Down

0 comments on commit 4c4b81c

Please sign in to comment.