Skip to content

Commit bdca592

Browse files
authored
Add Shelly event translation (home-assistant#156162)
Signed-off-by: David Rapan <[email protected]>
1 parent 5c0c7b9 commit bdca592

File tree

5 files changed

+144
-38
lines changed

5 files changed

+144
-38
lines changed

homeassistant/components/shelly/event.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@
3030
from .utils import (
3131
async_remove_orphaned_entities,
3232
async_remove_shelly_entity,
33+
get_block_channel,
34+
get_block_custom_name,
3335
get_device_entry_gen,
36+
get_rpc_component_name,
3437
get_rpc_entity_name,
3538
get_rpc_key_instances,
3639
is_block_momentary_input,
@@ -74,7 +77,6 @@ class ShellyRpcEventDescription(EventEntityDescription):
7477
SCRIPT_EVENT: Final = ShellyRpcEventDescription(
7578
key="script",
7679
translation_key="script",
77-
device_class=None,
7880
entity_registry_enabled_default=False,
7981
)
8082

@@ -195,6 +197,17 @@ def __init__(
195197
self._attr_event_types = list(BASIC_INPUTS_EVENTS_TYPES)
196198
self.entity_description = description
197199

200+
if (
201+
hasattr(self, "_attr_name")
202+
and self._attr_name
203+
and not get_block_custom_name(coordinator.device, block)
204+
):
205+
self._attr_translation_placeholders = {
206+
"input_number": get_block_channel(block)
207+
}
208+
209+
delattr(self, "_attr_name")
210+
198211
async def async_added_to_hass(self) -> None:
199212
"""When entity is added to hass."""
200213
await super().async_added_to_hass()
@@ -227,9 +240,20 @@ def __init__(
227240
self.event_id = int(key.split(":")[-1])
228241
self._attr_device_info = get_entity_rpc_device_info(coordinator, key)
229242
self._attr_unique_id = f"{coordinator.mac}-{key}"
230-
self._attr_name = get_rpc_entity_name(coordinator.device, key)
231243
self.entity_description = description
232244

245+
if description.key == "input":
246+
component = key.split(":")[0]
247+
component_id = key.split(":")[-1]
248+
if not get_rpc_component_name(coordinator.device, key) and (
249+
component.lower() == "input" and component_id.isnumeric()
250+
):
251+
self._attr_translation_placeholders = {"input_number": component_id}
252+
else:
253+
self._attr_name = get_rpc_entity_name(coordinator.device, key)
254+
elif description.key == "script":
255+
self._attr_name = get_rpc_entity_name(coordinator.device, key)
256+
233257
async def async_added_to_hass(self) -> None:
234258
"""When entity is added to hass."""
235259
await super().async_added_to_hass()

homeassistant/components/shelly/strings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@
168168
},
169169
"event": {
170170
"input": {
171+
"name": "Input {input_number}",
171172
"state_attributes": {
172173
"event_type": {
173174
"state": {

homeassistant/components/shelly/utils.py

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -120,17 +120,35 @@ def get_number_of_channels(device: BlockDevice, block: Block) -> int:
120120
def get_block_entity_name(
121121
device: BlockDevice,
122122
block: Block | None,
123-
description: str | UndefinedType | None = None,
123+
name: str | UndefinedType | None = None,
124124
) -> str | None:
125125
"""Naming for block based switch and sensors."""
126126
channel_name = get_block_channel_name(device, block)
127127

128-
if description is not UNDEFINED and description:
129-
return f"{channel_name} {description.lower()}" if channel_name else description
128+
if name is not UNDEFINED and name:
129+
return f"{channel_name} {name.lower()}" if channel_name else name
130130

131131
return channel_name
132132

133133

134+
def get_block_custom_name(device: BlockDevice, block: Block | None) -> str | None:
135+
"""Get custom name from device settings."""
136+
if block and (key := cast(str, block.type) + "s") and key in device.settings:
137+
assert block.channel
138+
139+
if name := device.settings[key][int(block.channel)].get("name"):
140+
return cast(str, name)
141+
142+
return None
143+
144+
145+
def get_block_channel(block: Block | None, base: str = "1") -> str:
146+
"""Get block channel."""
147+
assert block and block.channel
148+
149+
return chr(int(block.channel) + ord(base))
150+
151+
134152
def get_block_channel_name(device: BlockDevice, block: Block | None) -> str | None:
135153
"""Get name based on device and channel name."""
136154
if (
@@ -140,38 +158,24 @@ def get_block_channel_name(device: BlockDevice, block: Block | None) -> str | No
140158
):
141159
return None
142160

143-
assert block.channel
161+
if custom_name := get_block_custom_name(device, block):
162+
return custom_name
144163

145-
channel_name: str | None = None
146-
mode = cast(str, block.type) + "s"
147-
if mode in device.settings:
148-
channel_name = device.settings[mode][int(block.channel)].get("name")
149-
150-
if channel_name:
151-
return channel_name
152-
153-
base = ord("1")
154-
155-
return f"Channel {chr(int(block.channel) + base)}"
164+
return f"Channel {get_block_channel(block)}"
156165

157166

158167
def get_block_sub_device_name(device: BlockDevice, block: Block) -> str:
159168
"""Get name of block sub-device."""
160169
if TYPE_CHECKING:
161170
assert block.channel
162171

163-
mode = cast(str, block.type) + "s"
164-
if mode in device.settings:
165-
if channel_name := device.settings[mode][int(block.channel)].get("name"):
166-
return cast(str, channel_name)
172+
if custom_name := get_block_custom_name(device, block):
173+
return custom_name
167174

168175
if device.settings["device"]["type"] == MODEL_EM3:
169-
base = ord("A")
170-
return f"{device.name} Phase {chr(int(block.channel) + base)}"
176+
return f"{device.name} Phase {get_block_channel(block, 'A')}"
171177

172-
base = ord("1")
173-
174-
return f"{device.name} Channel {chr(int(block.channel) + base)}"
178+
return f"{device.name} Channel {get_block_channel(block)}"
175179

176180

177181
def is_block_momentary_input(
@@ -387,6 +391,18 @@ def get_shelly_model_name(
387391
return cast(str, MODEL_NAMES.get(model))
388392

389393

394+
def get_rpc_component_name(device: RpcDevice, key: str) -> str | None:
395+
"""Get component name from device config."""
396+
if (
397+
key in device.config
398+
and key != "em:0" # workaround for Pro 3EM, we don't want to get name for em:0
399+
and (name := device.config[key].get("name"))
400+
):
401+
return cast(str, name)
402+
403+
return None
404+
405+
390406
def get_rpc_channel_name(device: RpcDevice, key: str) -> str | None:
391407
"""Get name based on device and channel name."""
392408
if BLU_TRV_IDENTIFIER in key:
@@ -398,13 +414,11 @@ def get_rpc_channel_name(device: RpcDevice, key: str) -> str | None:
398414
component = key.split(":")[0]
399415
component_id = key.split(":")[-1]
400416

401-
if key in device.config and key != "em:0":
402-
# workaround for Pro 3EM, we don't want to get name for em:0
403-
if component_name := device.config[key].get("name"):
404-
if component in (*VIRTUAL_COMPONENTS, "input", "presencezone", "script"):
405-
return cast(str, component_name)
417+
if component_name := get_rpc_component_name(device, key):
418+
if component in (*VIRTUAL_COMPONENTS, "input", "presencezone", "script"):
419+
return component_name
406420

407-
return cast(str, component_name) if instances == 1 else None
421+
return component_name if instances == 1 else None
408422

409423
if component in (*VIRTUAL_COMPONENTS, "input"):
410424
return f"{component.title()} {component_id}"

tests/components/shelly/conftest.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,28 @@
3636
"mac": MOCK_MAC,
3737
"hostname": "test-host",
3838
"type": MODEL_25,
39+
"num_inputs": 3,
3940
"num_outputs": 2,
4041
},
4142
"coiot": {"update_period": 15},
4243
"fw": "20201124-092159/v1.9.0@57ac4ad8",
44+
"inputs": [
45+
{
46+
"name": "TV LEDs",
47+
"btn_type": "momentary",
48+
"btn_reverse": 0,
49+
},
50+
{
51+
"name": "TV Spots",
52+
"btn_type": "momentary",
53+
"btn_reverse": 0,
54+
},
55+
{
56+
"name": None,
57+
"btn_type": "momentary",
58+
"btn_reverse": 0,
59+
},
60+
],
4361
"relays": [{"btn_type": "momentary"}, {"btn_type": "toggle"}],
4462
"rollers": [{"positioning": True}],
4563
"external_power": 0,
@@ -348,6 +366,7 @@ def mock_white_light_set_state(
348366
"mac": MOCK_MAC,
349367
"auth": False,
350368
"fw": "20210715-092854/v1.11.0@57ac4ad8",
369+
"num_inputs": 3,
351370
"num_outputs": 2,
352371
}
353372

tests/components/shelly/test_event.py

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Tests for Shelly button platform."""
22

3+
import copy
34
from unittest.mock import Mock
45

56
from aioshelly.ble.const import BLE_SCRIPT_NAME
@@ -24,9 +25,14 @@
2425
patch_platforms,
2526
register_entity,
2627
)
28+
from .conftest import MOCK_BLOCKS
2729

2830
DEVICE_BLOCK_ID = 4
2931

32+
UNORDERED_EVENT_TYPES = unordered(
33+
["double", "long", "long_single", "single", "single_long", "triple"]
34+
)
35+
3036

3137
@pytest.fixture(autouse=True)
3238
def fixture_platforms():
@@ -213,15 +219,57 @@ async def test_block_event(
213219
assert state.attributes.get(ATTR_EVENT_TYPE) == "long"
214220

215221

222+
async def test_block_event_single_output(
223+
hass: HomeAssistant, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch
224+
) -> None:
225+
"""Test block device event when num_outputs is 1."""
226+
monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1)
227+
await init_integration(hass, 1)
228+
229+
assert hass.states.get("event.test_name")
230+
231+
216232
async def test_block_event_shix3_1(
217233
hass: HomeAssistant, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch
218234
) -> None:
219235
"""Test block device event for SHIX3-1."""
220-
monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1)
236+
blocks = copy.deepcopy(MOCK_BLOCKS)
237+
blocks[0] = Mock(
238+
sensor_ids={
239+
"inputEvent": "S",
240+
"inputEventCnt": 2,
241+
},
242+
channel="0",
243+
type="input",
244+
description="input_0",
245+
)
246+
blocks[1] = Mock(
247+
sensor_ids={
248+
"inputEvent": "S",
249+
"inputEventCnt": 2,
250+
},
251+
channel="1",
252+
type="input",
253+
description="input_1",
254+
)
255+
blocks[2] = Mock(
256+
sensor_ids={
257+
"inputEvent": "S",
258+
"inputEventCnt": 2,
259+
},
260+
channel="2",
261+
type="input",
262+
description="input_2",
263+
)
264+
monkeypatch.setattr(mock_block_device, "blocks", blocks)
265+
monkeypatch.delitem(mock_block_device.settings, "relays")
221266
await init_integration(hass, 1, model=MODEL_I3)
222-
entity_id = "event.test_name"
223267

224-
assert (state := hass.states.get(entity_id))
225-
assert state.attributes.get(ATTR_EVENT_TYPES) == unordered(
226-
["double", "long", "long_single", "single", "single_long", "triple"]
227-
)
268+
assert (state := hass.states.get("event.test_name_tv_leds"))
269+
assert state.attributes.get(ATTR_EVENT_TYPES) == UNORDERED_EVENT_TYPES
270+
271+
assert (state := hass.states.get("event.test_name_tv_spots"))
272+
assert state.attributes.get(ATTR_EVENT_TYPES) == UNORDERED_EVENT_TYPES
273+
274+
assert (state := hass.states.get("event.test_name_input_3"))
275+
assert state.attributes.get(ATTR_EVENT_TYPES) == UNORDERED_EVENT_TYPES

0 commit comments

Comments
 (0)