mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 06:05:26 +01:00
Control modes for Shelly Cury (#155665)
Co-authored-by: Shay Levy <levyshay1@gmail.com>
This commit is contained in:
@@ -70,6 +70,13 @@
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"cury_away_mode": {
|
||||
"default": "mdi:home-outline",
|
||||
"state": {
|
||||
"off": "mdi:home-import-outline",
|
||||
"on": "mdi:home-export-outline"
|
||||
}
|
||||
},
|
||||
"cury_boost": {
|
||||
"default": "mdi:rocket-launch"
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Final
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
from aioshelly.const import RPC_GENERATIONS
|
||||
|
||||
@@ -37,14 +37,92 @@ PARALLEL_UPDATES = 0
|
||||
class RpcSelectDescription(RpcEntityDescription, SelectEntityDescription):
|
||||
"""Class to describe a RPC select entity."""
|
||||
|
||||
method: str
|
||||
|
||||
|
||||
class RpcSelect(ShellyRpcAttributeEntity, SelectEntity):
|
||||
"""Represent a RPC select entity."""
|
||||
|
||||
entity_description: RpcSelectDescription
|
||||
_id: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ShellyRpcCoordinator,
|
||||
key: str,
|
||||
attribute: str,
|
||||
description: RpcSelectDescription,
|
||||
) -> None:
|
||||
"""Initialize select."""
|
||||
super().__init__(coordinator, key, attribute, description)
|
||||
|
||||
if self.option_map:
|
||||
self._attr_options = list(self.option_map.values())
|
||||
|
||||
if hasattr(self, "_attr_name") and description.role != ROLE_GENERIC:
|
||||
delattr(self, "_attr_name")
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the selected entity option to represent the entity state."""
|
||||
if isinstance(self.attribute_value, str) and self.option_map:
|
||||
return self.option_map[self.attribute_value]
|
||||
|
||||
return None
|
||||
|
||||
@rpc_call
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the value."""
|
||||
method = getattr(self.coordinator.device, self.entity_description.method)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert method is not None
|
||||
|
||||
if self.reversed_option_map:
|
||||
await method(self._id, self.reversed_option_map[option])
|
||||
else:
|
||||
await method(self._id, option)
|
||||
|
||||
|
||||
class RpcCuryModeSelect(RpcSelect):
|
||||
"""Represent a RPC select entity for Cury modes."""
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the selected entity option to represent the entity state."""
|
||||
if self.attribute_value is None:
|
||||
return "none"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(self.attribute_value, str)
|
||||
|
||||
return self.attribute_value
|
||||
|
||||
|
||||
RPC_SELECT_ENTITIES: Final = {
|
||||
"cury_mode": RpcSelectDescription(
|
||||
key="cury",
|
||||
sub_key="mode",
|
||||
translation_key="cury_mode",
|
||||
options=[
|
||||
"hall",
|
||||
"bedroom",
|
||||
"living_room",
|
||||
"lavatory_room",
|
||||
"none",
|
||||
"reception",
|
||||
"workplace",
|
||||
],
|
||||
method="cury_set_mode",
|
||||
entity_class=RpcCuryModeSelect,
|
||||
),
|
||||
"enum_generic": RpcSelectDescription(
|
||||
key="enum",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, SELECT_PLATFORM
|
||||
),
|
||||
method="enum_set",
|
||||
role=ROLE_GENERIC,
|
||||
),
|
||||
}
|
||||
@@ -89,37 +167,3 @@ def _async_setup_rpc_entry(
|
||||
virtual_text_ids,
|
||||
"enum",
|
||||
)
|
||||
|
||||
|
||||
class RpcSelect(ShellyRpcAttributeEntity, SelectEntity):
|
||||
"""Represent a RPC select entity."""
|
||||
|
||||
entity_description: RpcSelectDescription
|
||||
_id: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ShellyRpcCoordinator,
|
||||
key: str,
|
||||
attribute: str,
|
||||
description: RpcSelectDescription,
|
||||
) -> None:
|
||||
"""Initialize select."""
|
||||
super().__init__(coordinator, key, attribute, description)
|
||||
|
||||
self._attr_options = list(self.option_map.values())
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the selected entity option to represent the entity state."""
|
||||
if not isinstance(self.attribute_value, str):
|
||||
return None
|
||||
|
||||
return self.option_map[self.attribute_value]
|
||||
|
||||
@rpc_call
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the value."""
|
||||
await self.coordinator.device.enum_set(
|
||||
self._id, self.reversed_option_map[option]
|
||||
)
|
||||
|
||||
@@ -162,6 +162,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"cury_mode": {
|
||||
"name": "Mode",
|
||||
"state": {
|
||||
"bedroom": "Bedroom",
|
||||
"hall": "Hall",
|
||||
"lavatory_room": "Lavatory room",
|
||||
"living_room": "Living room",
|
||||
"none": "None",
|
||||
"reception": "Reception",
|
||||
"workplace": "Workplace"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"adc": {
|
||||
"name": "ADC"
|
||||
|
||||
@@ -290,6 +290,16 @@ RPC_SWITCHES = {
|
||||
available=lambda status: (right := status["right"]) is not None
|
||||
and right.get("vial", {}).get("level", -1) != -1,
|
||||
),
|
||||
"cury_away_mode": RpcSwitchDescription(
|
||||
key="cury",
|
||||
sub_key="away_mode",
|
||||
name="Away mode",
|
||||
translation_key="cury_away_mode",
|
||||
is_on=lambda status: status["away_mode"],
|
||||
method_on="cury_set_away_mode",
|
||||
method_off="cury_set_away_mode",
|
||||
method_params_fn=lambda id, value: (id, value),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -262,6 +262,73 @@
|
||||
'state': '45',
|
||||
})
|
||||
# ---
|
||||
# name: test_device[cury_gen4][select.test_name_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'hall',
|
||||
'bedroom',
|
||||
'living_room',
|
||||
'lavatory_room',
|
||||
'none',
|
||||
'reception',
|
||||
'workplace',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.test_name_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Mode',
|
||||
'platform': 'shelly',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cury_mode',
|
||||
'unique_id': '123456789ABC-cury:0-cury_mode',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_device[cury_gen4][select.test_name_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test name Mode',
|
||||
'options': list([
|
||||
'hall',
|
||||
'bedroom',
|
||||
'living_room',
|
||||
'lavatory_room',
|
||||
'none',
|
||||
'reception',
|
||||
'workplace',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.test_name_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'living_room',
|
||||
})
|
||||
# ---
|
||||
# name: test_device[cury_gen4][sensor.test_name_last_restart-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -564,6 +631,54 @@
|
||||
'state': '-49',
|
||||
})
|
||||
# ---
|
||||
# name: test_device[cury_gen4][switch.test_name_away_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.test_name_away_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Away mode',
|
||||
'platform': 'shelly',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cury_away_mode',
|
||||
'unique_id': '123456789ABC-cury:0-cury_away_mode',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_device[cury_gen4][switch.test_name_away_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test name Away mode',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.test_name_away_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_device[cury_gen4][switch.test_name_left_slot-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -1,4 +1,52 @@
|
||||
# serializer version: 1
|
||||
# name: test_cury_switch_entity[switch.test_name_away_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.test_name_away_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Away mode',
|
||||
'platform': 'shelly',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cury_away_mode',
|
||||
'unique_id': '123456789ABC-cury:0-cury_away_mode',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_cury_switch_entity[switch.test_name_away_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test name Away mode',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.test_name_away_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_cury_switch_entity[switch.test_name_left_slot-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -14,7 +14,12 @@ from homeassistant.components.select import (
|
||||
)
|
||||
from homeassistant.components.shelly.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
STATE_UNKNOWN,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceRegistry
|
||||
@@ -266,3 +271,59 @@ async def test_select_set_reauth_error(
|
||||
assert "context" in flow
|
||||
assert flow["context"].get("source") == SOURCE_REAUTH
|
||||
assert flow["context"].get("entry_id") == entry.entry_id
|
||||
|
||||
|
||||
async def test_rpc_cury_mode_select(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: EntityRegistry,
|
||||
mock_rpc_device: Mock,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test Cury Mode select entity."""
|
||||
entity_id = f"{SELECT_PLATFORM}.test_name_mode"
|
||||
status = {"cury:0": {"id": 0, "mode": "hall"}}
|
||||
monkeypatch.setattr(mock_rpc_device, "status", status)
|
||||
await init_integration(hass, 3)
|
||||
|
||||
assert (state := hass.states.get(entity_id))
|
||||
assert state.state == "hall"
|
||||
assert state.attributes.get(ATTR_OPTIONS) == [
|
||||
"hall",
|
||||
"bedroom",
|
||||
"living_room",
|
||||
"lavatory_room",
|
||||
"none",
|
||||
"reception",
|
||||
"workplace",
|
||||
]
|
||||
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Test name Mode"
|
||||
|
||||
assert (entry := entity_registry.async_get(entity_id))
|
||||
assert entry.unique_id == "123456789ABC-cury:0-cury_mode"
|
||||
assert entry.translation_key == "cury_mode"
|
||||
|
||||
monkeypatch.setitem(mock_rpc_device.status["cury:0"], "mode", "living_room")
|
||||
mock_rpc_device.mock_update()
|
||||
|
||||
assert (state := hass.states.get(entity_id))
|
||||
assert state.state == "living_room"
|
||||
|
||||
monkeypatch.setitem(mock_rpc_device.status["cury:0"], "mode", "reception")
|
||||
await hass.services.async_call(
|
||||
SELECT_PLATFORM,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "reception"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_rpc_device.cury_set_mode.assert_called_once_with(0, "reception")
|
||||
mock_rpc_device.mock_update()
|
||||
|
||||
assert (state := hass.states.get(entity_id))
|
||||
assert state.state == "reception"
|
||||
|
||||
monkeypatch.setitem(mock_rpc_device.status["cury:0"], "mode", None)
|
||||
mock_rpc_device.mock_update()
|
||||
|
||||
assert (state := hass.states.get(entity_id))
|
||||
assert state.state == "none"
|
||||
|
||||
@@ -829,6 +829,7 @@ async def test_cury_switch_entity(
|
||||
status = {
|
||||
"cury:0": {
|
||||
"id": 0,
|
||||
"away_mode": False,
|
||||
"slots": {
|
||||
"left": {
|
||||
"intensity": 70,
|
||||
@@ -848,7 +849,13 @@ async def test_cury_switch_entity(
|
||||
monkeypatch.setattr(mock_rpc_device, "status", status)
|
||||
await init_integration(hass, 3)
|
||||
|
||||
for entity in ("left_slot", "left_slot_boost", "right_slot", "right_slot_boost"):
|
||||
for entity in (
|
||||
"away_mode",
|
||||
"left_slot",
|
||||
"left_slot_boost",
|
||||
"right_slot",
|
||||
"right_slot_boost",
|
||||
):
|
||||
entity_id = f"{SWITCH_DOMAIN}.test_name_{entity}"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
Reference in New Issue
Block a user