Add button entity to ping zwave_js node (#66129)
* Add button entity to ping zwave_js node * Fix docstring * Fix docstrings * Fix and simplify tests * Fix name * Update homeassistant/components/zwave_js/button.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zwave_js/services.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zwave_js/button.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zwave_js/button.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zwave_js/button.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * review comments * Update homeassistant/components/zwave_js/button.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zwave_js/button.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Remove self.client line * Add callback to remove entity * Add extra dispatch signal on replacement * Combine signals for valueless entities Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
113c3149c4
commit
152dbfd2fe
8 changed files with 147 additions and 45 deletions
|
@ -15,6 +15,7 @@ from zwave_js_server.model.notification import (
|
|||
)
|
||||
from zwave_js_server.model.value import Value, ValueNotification
|
||||
|
||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
|
@ -93,6 +94,7 @@ from .helpers import (
|
|||
get_device_id,
|
||||
get_device_id_ext,
|
||||
get_unique_id,
|
||||
get_valueless_base_unique_id,
|
||||
)
|
||||
from .migrate import async_migrate_discovered_value
|
||||
from .services import ZWaveServices
|
||||
|
@ -171,11 +173,19 @@ async def async_setup_entry( # noqa: C901
|
|||
entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {})
|
||||
|
||||
entry_hass_data[DATA_CLIENT] = client
|
||||
entry_hass_data[DATA_PLATFORM_SETUP] = {}
|
||||
platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP] = {}
|
||||
|
||||
registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict)
|
||||
discovered_value_ids: dict[str, set[str]] = defaultdict(set)
|
||||
|
||||
async def async_setup_platform(platform: str) -> None:
|
||||
"""Set up platform if needed."""
|
||||
if platform not in platform_setup_tasks:
|
||||
platform_setup_tasks[platform] = hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, platform)
|
||||
)
|
||||
await platform_setup_tasks[platform]
|
||||
|
||||
@callback
|
||||
def remove_device(device: device_registry.DeviceEntry) -> None:
|
||||
"""Remove device from registry."""
|
||||
|
@ -202,13 +212,8 @@ async def async_setup_entry( # noqa: C901
|
|||
disc_info,
|
||||
)
|
||||
|
||||
platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP]
|
||||
platform = disc_info.platform
|
||||
if platform not in platform_setup_tasks:
|
||||
platform_setup_tasks[platform] = hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, platform)
|
||||
)
|
||||
await platform_setup_tasks[platform]
|
||||
await async_setup_platform(platform)
|
||||
|
||||
LOGGER.debug("Discovered entity: %s", disc_info)
|
||||
async_dispatcher_send(
|
||||
|
@ -256,6 +261,12 @@ async def async_setup_entry( # noqa: C901
|
|||
)
|
||||
)
|
||||
|
||||
# Create a ping button for each device
|
||||
await async_setup_platform(BUTTON_DOMAIN)
|
||||
async_dispatcher_send(
|
||||
hass, f"{DOMAIN}_{entry.entry_id}_add_ping_button_entity", node
|
||||
)
|
||||
|
||||
# add listeners to handle new values that get added later
|
||||
for event in ("value added", "value updated", "metadata updated"):
|
||||
entry.async_on_unload(
|
||||
|
@ -284,19 +295,7 @@ async def async_setup_entry( # noqa: C901
|
|||
|
||||
async def async_on_node_added(node: ZwaveNode) -> None:
|
||||
"""Handle node added event."""
|
||||
platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP]
|
||||
|
||||
# We need to set up the sensor platform if it hasn't already been setup in
|
||||
# order to create the node status sensor
|
||||
if SENSOR_DOMAIN not in platform_setup_tasks:
|
||||
platform_setup_tasks[SENSOR_DOMAIN] = hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, SENSOR_DOMAIN)
|
||||
)
|
||||
|
||||
# This guard ensures that concurrent runs of this function all await the
|
||||
# platform setup task
|
||||
if not platform_setup_tasks[SENSOR_DOMAIN].done():
|
||||
await platform_setup_tasks[SENSOR_DOMAIN]
|
||||
await async_setup_platform(SENSOR_DOMAIN)
|
||||
|
||||
# Create a node status sensor for each device
|
||||
async_dispatcher_send(
|
||||
|
@ -358,7 +357,7 @@ async def async_setup_entry( # noqa: C901
|
|||
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
f"{DOMAIN}_{client.driver.controller.home_id}.{node.node_id}.node_status_remove_entity",
|
||||
f"{DOMAIN}_{get_valueless_base_unique_id(client, node)}_remove_entity",
|
||||
)
|
||||
else:
|
||||
remove_device(device)
|
||||
|
|
73
homeassistant/components/zwave_js/button.py
Normal file
73
homeassistant/components/zwave_js/button.py
Normal file
|
@ -0,0 +1,73 @@
|
|||
"""Representation of Z-Wave buttons."""
|
||||
from __future__ import annotations
|
||||
|
||||
from zwave_js_server.client import Client as ZwaveClient
|
||||
from zwave_js_server.model.node import Node as ZwaveNode
|
||||
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DATA_CLIENT, DOMAIN
|
||||
from .helpers import get_device_id, get_valueless_base_unique_id
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Z-Wave button from config entry."""
|
||||
client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
|
||||
|
||||
@callback
|
||||
def async_add_ping_button_entity(node: ZwaveNode) -> None:
|
||||
"""Add ping button entity."""
|
||||
async_add_entities([ZWaveNodePingButton(client, node)])
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
f"{DOMAIN}_{config_entry.entry_id}_add_ping_button_entity",
|
||||
async_add_ping_button_entity,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ZWaveNodePingButton(ButtonEntity):
|
||||
"""Representation of a ping button entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
|
||||
def __init__(self, client: ZwaveClient, node: ZwaveNode) -> None:
|
||||
"""Initialize a ping Z-Wave device button entity."""
|
||||
self.node = node
|
||||
name: str = (
|
||||
node.name or node.device_config.description or f"Node {node.node_id}"
|
||||
)
|
||||
# Entity class attributes
|
||||
self._attr_name = f"{name}: Ping"
|
||||
self._base_unique_id = get_valueless_base_unique_id(client, node)
|
||||
self._attr_unique_id = f"{self._base_unique_id}.ping"
|
||||
# device is precreated in main handler
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={get_device_id(client, node)},
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._base_unique_id}_remove_entity",
|
||||
self.async_remove,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
self.hass.async_create_task(self.node.async_ping())
|
|
@ -55,6 +55,11 @@ def update_data_collection_preference(
|
|||
|
||||
|
||||
@callback
|
||||
def get_valueless_base_unique_id(client: ZwaveClient, node: ZwaveNode) -> str:
|
||||
"""Return the base unique ID for an entity that is not based on a value."""
|
||||
return f"{client.driver.controller.home_id}.{node.node_id}"
|
||||
|
||||
|
||||
def get_unique_id(client: ZwaveClient, value_id: str) -> str:
|
||||
"""Get unique ID from client and value ID."""
|
||||
return f"{client.driver.controller.home_id}.{value_id}"
|
||||
|
|
|
@ -61,7 +61,7 @@ from .discovery_data_template import (
|
|||
NumericSensorDataTemplateData,
|
||||
)
|
||||
from .entity import ZWaveBaseEntity
|
||||
from .helpers import get_device_id
|
||||
from .helpers import get_device_id, get_valueless_base_unique_id
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -477,9 +477,8 @@ class ZWaveNodeStatusSensor(SensorEntity):
|
|||
)
|
||||
# Entity class attributes
|
||||
self._attr_name = f"{name}: Node Status"
|
||||
self._attr_unique_id = (
|
||||
f"{self.client.driver.controller.home_id}.{node.node_id}.node_status"
|
||||
)
|
||||
self._base_unique_id = get_valueless_base_unique_id(client, node)
|
||||
self._attr_unique_id = f"{self._base_unique_id}.node_status"
|
||||
# device is precreated in main handler
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={get_device_id(self.client, self.node)},
|
||||
|
@ -517,7 +516,7 @@ class ZWaveNodeStatusSensor(SensorEntity):
|
|||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self.unique_id}_remove_entity",
|
||||
f"{DOMAIN}_{self._base_unique_id}_remove_entity",
|
||||
self.async_remove,
|
||||
)
|
||||
)
|
||||
|
|
|
@ -457,7 +457,7 @@ class ZWaveServices:
|
|||
options = service.data.get(const.ATTR_OPTIONS)
|
||||
|
||||
if not broadcast and len(nodes) == 1:
|
||||
const.LOGGER.warning(
|
||||
const.LOGGER.info(
|
||||
"Passing the zwave_js.multicast_set_value service call to the "
|
||||
"zwave_js.set_value service since only one node was targeted"
|
||||
)
|
||||
|
@ -520,5 +520,10 @@ class ZWaveServices:
|
|||
async def async_ping(self, service: ServiceCall) -> None:
|
||||
"""Ping node(s)."""
|
||||
# pylint: disable=no-self-use
|
||||
const.LOGGER.warning(
|
||||
"This service is deprecated in favor of the ping button entity. Service "
|
||||
"calls will still work for now but the service will be removed in a "
|
||||
"future release"
|
||||
)
|
||||
nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
|
||||
await asyncio.gather(*(node.async_ping() for node in nodes))
|
||||
|
|
|
@ -737,18 +737,6 @@ def null_name_check_fixture(client, null_name_check_state):
|
|||
return node
|
||||
|
||||
|
||||
@pytest.fixture(name="multiple_devices")
|
||||
def multiple_devices_fixture(
|
||||
client, climate_radio_thermostat_ct100_plus_state, lock_schlage_be469_state
|
||||
):
|
||||
"""Mock a client with multiple devices."""
|
||||
node = Node(client, copy.deepcopy(climate_radio_thermostat_ct100_plus_state))
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
node = Node(client, copy.deepcopy(lock_schlage_be469_state))
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
return client.driver.controller.nodes
|
||||
|
||||
|
||||
@pytest.fixture(name="gdc_zw062")
|
||||
def motorized_barrier_cover_fixture(client, gdc_zw062_state):
|
||||
"""Mock a motorized barrier node."""
|
||||
|
|
33
tests/components/zwave_js/test_button.py
Normal file
33
tests/components/zwave_js/test_button.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
"""Test the Z-Wave JS button entities."""
|
||||
from homeassistant.components.button.const import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
|
||||
|
||||
async def test_ping_entity(
|
||||
hass,
|
||||
client,
|
||||
climate_radio_thermostat_ct100_plus_different_endpoints,
|
||||
integration,
|
||||
):
|
||||
"""Test ping entity."""
|
||||
client.async_send_command.return_value = {"responded": True}
|
||||
|
||||
# Test successful ping call
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{
|
||||
ATTR_ENTITY_ID: "button.z_wave_thermostat_ping",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
args = client.async_send_command.call_args_list[0][0][0]
|
||||
assert args["command"] == "node.ping"
|
||||
assert (
|
||||
args["nodeId"]
|
||||
== climate_radio_thermostat_ct100_plus_different_endpoints.node_id
|
||||
)
|
||||
|
||||
client.async_send_command.reset_mock()
|
|
@ -788,10 +788,10 @@ async def test_remove_entry(
|
|||
assert "Failed to uninstall the Z-Wave JS add-on" in caplog.text
|
||||
|
||||
|
||||
async def test_removed_device(hass, client, multiple_devices, integration):
|
||||
async def test_removed_device(
|
||||
hass, client, climate_radio_thermostat_ct100_plus, lock_schlage_be469, integration
|
||||
):
|
||||
"""Test that the device registry gets updated when a device gets removed."""
|
||||
nodes = multiple_devices
|
||||
|
||||
# Verify how many nodes are available
|
||||
assert len(client.driver.controller.nodes) == 2
|
||||
|
||||
|
@ -803,10 +803,10 @@ async def test_removed_device(hass, client, multiple_devices, integration):
|
|||
# Check how many entities there are
|
||||
ent_reg = er.async_get(hass)
|
||||
entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id)
|
||||
assert len(entity_entries) == 26
|
||||
assert len(entity_entries) == 28
|
||||
|
||||
# Remove a node and reload the entry
|
||||
old_node = nodes.pop(13)
|
||||
old_node = client.driver.controller.nodes.pop(13)
|
||||
await hass.config_entries.async_reload(integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
@ -815,7 +815,7 @@ async def test_removed_device(hass, client, multiple_devices, integration):
|
|||
device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id)
|
||||
assert len(device_entries) == 1
|
||||
entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id)
|
||||
assert len(entity_entries) == 16
|
||||
assert len(entity_entries) == 17
|
||||
assert dev_reg.async_get_device({get_device_id(client, old_node)}) is None
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue