Change naming of Shelly entities to correspond with HA guidelines (#97533)

* Do not use the device name to create the entity name

* Remove unnecessary return

* Fix mypy complains

* Gen1

* Uncapitalize description.name if channel name is used

* Fix for climate and button

* switch_3 -> switch 3

* Add _attr_has_entity_name to ShellyRestAttributeEntity

* Capitalize channel name
pull/98682/head
Maciej Bieniek 2023-08-19 09:13:22 +00:00 committed by GitHub
parent 7059252164
commit c526d23686
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 66 additions and 59 deletions

View File

@ -154,6 +154,7 @@ class ShellyButton(
entity_description: ShellyButtonDescription[
ShellyRpcCoordinator | ShellyBlockCoordinator
]
_attr_has_entity_name = True
def __init__(
self,
@ -166,7 +167,6 @@ class ShellyButton(
super().__init__(coordinator)
self.entity_description = description
self._attr_name = f"{coordinator.device.name} {description.name}"
self._attr_unique_id = f"{coordinator.mac}_{description.key}"
self._attr_device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)}

View File

@ -130,6 +130,7 @@ class BlockSleepingClimate(
)
_attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"]
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_has_entity_name = True
def __init__(
self,
@ -173,11 +174,6 @@ class BlockSleepingClimate(
"""Set unique id of entity."""
return self._unique_id
@property
def name(self) -> str:
"""Name of entity."""
return self.coordinator.name
@property
def target_temperature(self) -> float | None:
"""Set target temperature."""
@ -354,7 +350,7 @@ class BlockSleepingClimate(
severity=ir.IssueSeverity.ERROR,
translation_key="device_not_calibrated",
translation_placeholders={
"device_name": self.name,
"device_name": self.coordinator.name,
"ip_address": self.coordinator.device.ip_address,
},
)

View File

@ -321,6 +321,8 @@ class RestEntityDescription(EntityDescription):
class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]):
"""Helper class to represent a block entity."""
_attr_has_entity_name = True
def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None:
"""Initialize Shelly entity."""
super().__init__(coordinator)
@ -359,6 +361,8 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]):
class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]):
"""Helper class to represent a rpc entity."""
_attr_has_entity_name = True
def __init__(self, coordinator: ShellyRpcCoordinator, key: str) -> None:
"""Initialize Shelly entity."""
super().__init__(coordinator)
@ -462,6 +466,7 @@ class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]):
"""Class to load info from REST."""
entity_description: RestEntityDescription
_attr_has_entity_name = True
def __init__(
self,

View File

@ -42,7 +42,8 @@ def async_describe_events(
rpc_coordinator = get_rpc_coordinator_by_device_id(hass, device_id)
if rpc_coordinator and rpc_coordinator.device.initialized:
key = f"input:{channel-1}"
input_name = get_rpc_entity_name(rpc_coordinator.device, key)
if iname := get_rpc_entity_name(rpc_coordinator.device, key):
input_name = iname
elif click_type in BLOCK_INPUTS_EVENTS_TYPES:
block_coordinator = get_block_coordinator_by_device_id(hass, device_id)

View File

@ -72,26 +72,26 @@ def get_block_entity_name(
device: BlockDevice,
block: Block | None,
description: str | None = None,
) -> str:
) -> str | None:
"""Naming for block based switch and sensors."""
channel_name = get_block_channel_name(device, block)
if description and channel_name:
return f"{channel_name} {uncapitalize(description)}"
if description:
return f"{channel_name} {description.lower()}"
return description
return channel_name
def get_block_channel_name(device: BlockDevice, block: Block | None) -> str:
def get_block_channel_name(device: BlockDevice, block: Block | None) -> str | None:
"""Get name based on device and channel name."""
entity_name = device.name
if (
not block
or block.type == "device"
or get_number_of_channels(device, block) == 1
):
return entity_name
return None
assert block.channel
@ -108,7 +108,7 @@ def get_block_channel_name(device: BlockDevice, block: Block | None) -> str:
else:
base = ord("1")
return f"{entity_name} channel {chr(int(block.channel)+base)}"
return f"Channel {chr(int(block.channel)+base)}"
def is_block_momentary_input(
@ -285,32 +285,32 @@ def get_model_name(info: dict[str, Any]) -> str:
return cast(str, MODEL_NAMES.get(info["type"], info["type"]))
def get_rpc_channel_name(device: RpcDevice, key: str) -> str:
def get_rpc_channel_name(device: RpcDevice, key: str) -> str | None:
"""Get name based on device and channel name."""
key = key.replace("emdata", "em")
if device.config.get("switch:0"):
key = key.replace("input", "switch")
device_name = device.name
entity_name: str | None = None
if key in device.config:
entity_name = device.config[key].get("name", device_name)
entity_name = device.config[key].get("name")
if entity_name is None:
if key.startswith(("input:", "light:", "switch:")):
return f"{device_name} {key.replace(':', '_')}"
return device_name
return key.replace(":", " ").capitalize()
return entity_name
def get_rpc_entity_name(
device: RpcDevice, key: str, description: str | None = None
) -> str:
) -> str | None:
"""Naming for RPC based switch and sensors."""
channel_name = get_rpc_channel_name(device, key)
if description and channel_name:
return f"{channel_name} {uncapitalize(description)}"
if description:
return f"{channel_name} {description.lower()}"
return description
return channel_name
@ -405,3 +405,8 @@ def mac_address_from_name(name: str) -> str | None:
"""Convert a name to a mac address."""
mac = name.partition(".")[0].partition("-")[-1]
return mac.upper() if len(mac) == 12 else None
def uncapitalize(description: str) -> str:
"""Uncapitalize the first letter of a description."""
return description[:1].lower() + description[1:]

View File

@ -165,7 +165,7 @@ async def test_rpc_binary_sensor(
hass: HomeAssistant, mock_rpc_device, monkeypatch
) -> None:
"""Test RPC binary sensor."""
entity_id = f"{BINARY_SENSOR_DOMAIN}.test_cover_0_overpowering"
entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_test_cover_0_overpowering"
await init_integration(hass, 2)
assert hass.states.get(entity_id).state == STATE_OFF

View File

@ -353,7 +353,7 @@ async def test_rpc_reload_on_cfg_change(
)
await hass.async_block_till_done()
assert hass.states.get("switch.test_switch_0") is not None
assert hass.states.get("switch.test_name_test_switch_0") is not None
# Wait for debouncer
async_fire_time_changed(
@ -361,7 +361,7 @@ async def test_rpc_reload_on_cfg_change(
)
await hass.async_block_till_done()
assert hass.states.get("switch.test_switch_0") is None
assert hass.states.get("switch.test_name_test_switch_0") is None
async def test_rpc_reload_with_invalid_auth(
@ -588,7 +588,7 @@ async def test_rpc_reconnect_error(
"""Test RPC reconnect error."""
await init_integration(hass, 2)
assert hass.states.get("switch.test_switch_0").state == STATE_ON
assert hass.states.get("switch.test_name_test_switch_0").state == STATE_ON
monkeypatch.setattr(mock_rpc_device, "connected", False)
monkeypatch.setattr(
@ -605,7 +605,7 @@ async def test_rpc_reconnect_error(
)
await hass.async_block_till_done()
assert hass.states.get("switch.test_switch_0").state == STATE_UNAVAILABLE
assert hass.states.get("switch.test_name_test_switch_0").state == STATE_UNAVAILABLE
async def test_rpc_polling_connection_error(

View File

@ -97,10 +97,10 @@ async def test_rpc_device_services(
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_SET_COVER_POSITION,
{ATTR_ENTITY_ID: "cover.test_cover_0", ATTR_POSITION: 50},
{ATTR_ENTITY_ID: "cover.test_name_test_cover_0", ATTR_POSITION: 50},
blocking=True,
)
state = hass.states.get("cover.test_cover_0")
state = hass.states.get("cover.test_name_test_cover_0")
assert state.attributes[ATTR_CURRENT_POSITION] == 50
mutate_rpc_device_status(
@ -109,11 +109,11 @@ async def test_rpc_device_services(
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER,
{ATTR_ENTITY_ID: "cover.test_cover_0"},
{ATTR_ENTITY_ID: "cover.test_name_test_cover_0"},
blocking=True,
)
mock_rpc_device.mock_update()
assert hass.states.get("cover.test_cover_0").state == STATE_OPENING
assert hass.states.get("cover.test_name_test_cover_0").state == STATE_OPENING
mutate_rpc_device_status(
monkeypatch, mock_rpc_device, "cover:0", "state", "closing"
@ -121,21 +121,21 @@ async def test_rpc_device_services(
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_CLOSE_COVER,
{ATTR_ENTITY_ID: "cover.test_cover_0"},
{ATTR_ENTITY_ID: "cover.test_name_test_cover_0"},
blocking=True,
)
mock_rpc_device.mock_update()
assert hass.states.get("cover.test_cover_0").state == STATE_CLOSING
assert hass.states.get("cover.test_name_test_cover_0").state == STATE_CLOSING
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "closed")
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_STOP_COVER,
{ATTR_ENTITY_ID: "cover.test_cover_0"},
{ATTR_ENTITY_ID: "cover.test_name_test_cover_0"},
blocking=True,
)
mock_rpc_device.mock_update()
assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED
assert hass.states.get("cover.test_name_test_cover_0").state == STATE_CLOSED
async def test_rpc_device_no_cover_keys(
@ -144,7 +144,7 @@ async def test_rpc_device_no_cover_keys(
"""Test RPC device without cover keys."""
monkeypatch.delitem(mock_rpc_device.status, "cover:0")
await init_integration(hass, 2)
assert hass.states.get("cover.test_cover_0") is None
assert hass.states.get("cover.test_name_test_cover_0") is None
async def test_rpc_device_update(
@ -153,11 +153,11 @@ async def test_rpc_device_update(
"""Test RPC device update."""
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "closed")
await init_integration(hass, 2)
assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED
assert hass.states.get("cover.test_name_test_cover_0").state == STATE_CLOSED
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "open")
mock_rpc_device.mock_update()
assert hass.states.get("cover.test_cover_0").state == STATE_OPEN
assert hass.states.get("cover.test_name_test_cover_0").state == STATE_OPEN
async def test_rpc_device_no_position_control(
@ -168,4 +168,4 @@ async def test_rpc_device_no_position_control(
monkeypatch, mock_rpc_device, "cover:0", "pos_control", False
)
await init_integration(hass, 2)
assert hass.states.get("cover.test_cover_0").state == STATE_OPEN
assert hass.states.get("cover.test_name_test_cover_0").state == STATE_OPEN

View File

@ -175,7 +175,7 @@ async def test_sleeping_rpc_device_online_new_firmware(
("gen", "entity_id"),
[
(1, "switch.test_name_channel_1"),
(2, "switch.test_switch_0"),
(2, "switch.test_name_test_switch_0"),
],
)
async def test_entry_unload(
@ -198,7 +198,7 @@ async def test_entry_unload(
("gen", "entity_id"),
[
(1, "switch.test_name_channel_1"),
(2, "switch.test_switch_0"),
(2, "switch.test_name_test_switch_0"),
],
)
async def test_entry_unload_device_not_ready(
@ -226,7 +226,7 @@ async def test_entry_unload_not_connected(
entry = await init_integration(
hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE}
)
entity_id = "switch.test_switch_0"
entity_id = "switch.test_name_test_switch_0"
assert entry.state is ConfigEntryState.LOADED
assert hass.states.get(entity_id).state is STATE_ON
@ -252,7 +252,7 @@ async def test_entry_unload_not_connected_but_we_think_we_are(
entry = await init_integration(
hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE}
)
entity_id = "switch.test_switch_0"
entity_id = "switch.test_name_test_switch_0"
assert entry.state is ConfigEntryState.LOADED
assert hass.states.get(entity_id).state is STATE_ON

View File

@ -385,25 +385,25 @@ async def test_rpc_device_switch_type_lights_mode(
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "light.test_switch_0"},
{ATTR_ENTITY_ID: "light.test_name_test_switch_0"},
blocking=True,
)
assert hass.states.get("light.test_switch_0").state == STATE_ON
assert hass.states.get("light.test_name_test_switch_0").state == STATE_ON
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "switch:0", "output", False)
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "light.test_switch_0"},
{ATTR_ENTITY_ID: "light.test_name_test_switch_0"},
blocking=True,
)
mock_rpc_device.mock_update()
assert hass.states.get("light.test_switch_0").state == STATE_OFF
assert hass.states.get("light.test_name_test_switch_0").state == STATE_OFF
async def test_rpc_light(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> None:
"""Test RPC light."""
entity_id = f"{LIGHT_DOMAIN}.test_light_0"
entity_id = f"{LIGHT_DOMAIN}.test_name_test_light_0"
monkeypatch.delitem(mock_rpc_device.status, "switch:0")
await init_integration(hass, 2)

View File

@ -262,7 +262,7 @@ async def test_block_sensor_unknown_value(
async def test_rpc_sensor(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> None:
"""Test RPC sensor."""
entity_id = f"{SENSOR_DOMAIN}.test_cover_0_power"
entity_id = f"{SENSOR_DOMAIN}.test_name_test_cover_0_power"
await init_integration(hass, 2)
assert hass.states.get(entity_id).state == "85.3"

View File

@ -149,20 +149,20 @@ async def test_rpc_device_services(
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "switch.test_switch_0"},
{ATTR_ENTITY_ID: "switch.test_name_test_switch_0"},
blocking=True,
)
assert hass.states.get("switch.test_switch_0").state == STATE_ON
assert hass.states.get("switch.test_name_test_switch_0").state == STATE_ON
monkeypatch.setitem(mock_rpc_device.status["switch:0"], "output", False)
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.test_switch_0"},
{ATTR_ENTITY_ID: "switch.test_name_test_switch_0"},
blocking=True,
)
mock_rpc_device.mock_update()
assert hass.states.get("switch.test_switch_0").state == STATE_OFF
assert hass.states.get("switch.test_name_test_switch_0").state == STATE_OFF
async def test_rpc_device_switch_type_lights_mode(
@ -173,7 +173,7 @@ async def test_rpc_device_switch_type_lights_mode(
mock_rpc_device.config["sys"]["ui_data"], "consumption_types", ["lights"]
)
await init_integration(hass, 2)
assert hass.states.get("switch.test_switch_0") is None
assert hass.states.get("switch.test_name_test_switch_0") is None
@pytest.mark.parametrize("exc", [DeviceConnectionError, RpcCallError(-1, "error")])
@ -188,7 +188,7 @@ async def test_rpc_set_state_errors(
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.test_switch_0"},
{ATTR_ENTITY_ID: "switch.test_name_test_switch_0"},
blocking=True,
)
@ -209,7 +209,7 @@ async def test_rpc_auth_error(
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.test_switch_0"},
{ATTR_ENTITY_ID: "switch.test_name_test_switch_0"},
blocking=True,
)
await hass.async_block_till_done()

View File

@ -58,7 +58,7 @@ async def test_block_get_block_channel_name(mock_block_device, monkeypatch) -> N
mock_block_device,
mock_block_device.blocks[DEVICE_BLOCK_ID],
)
== "Test name channel 1"
== "Channel 1"
)
monkeypatch.setitem(mock_block_device.settings["device"], "type", "SHEM-3")
@ -68,7 +68,7 @@ async def test_block_get_block_channel_name(mock_block_device, monkeypatch) -> N
mock_block_device,
mock_block_device.blocks[DEVICE_BLOCK_ID],
)
== "Test name channel A"
== "Channel A"
)
monkeypatch.setitem(
@ -207,7 +207,7 @@ async def test_get_block_input_triggers(mock_block_device, monkeypatch) -> None:
async def test_get_rpc_channel_name(mock_rpc_device) -> None:
"""Test get RPC channel name."""
assert get_rpc_channel_name(mock_rpc_device, "input:0") == "test switch_0"
assert get_rpc_channel_name(mock_rpc_device, "input:3") == "Test name switch_3"
assert get_rpc_channel_name(mock_rpc_device, "input:3") == "Switch 3"
async def test_get_rpc_input_triggers(mock_rpc_device, monkeypatch) -> None: