mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +00:00
Add button entities to manually idle zwave notification values (#91446)
* Add button entities to manually idle zwave notification values * Update discovery.py * Improve discovery check * fix tests * make unique ID more clear
This commit is contained in:
parent
57a59d808b
commit
3190e5d7cf
@ -5,7 +5,7 @@ from zwave_js_server.client import Client as ZwaveClient
|
|||||||
from zwave_js_server.model.driver import Driver
|
from zwave_js_server.model.driver import Driver
|
||||||
from zwave_js_server.model.node import Node as ZwaveNode
|
from zwave_js_server.model.node import Node as ZwaveNode
|
||||||
|
|
||||||
from homeassistant.components.button import ButtonEntity
|
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, ButtonEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
@ -13,6 +13,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DATA_CLIENT, DOMAIN, LOGGER
|
from .const import DATA_CLIENT, DOMAIN, LOGGER
|
||||||
|
from .discovery import ZwaveDiscoveryInfo
|
||||||
|
from .entity import ZWaveBaseEntity
|
||||||
from .helpers import get_device_info, get_valueless_base_unique_id
|
from .helpers import get_device_info, get_valueless_base_unique_id
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
@ -26,6 +28,17 @@ async def async_setup_entry(
|
|||||||
"""Set up Z-Wave button from config entry."""
|
"""Set up Z-Wave button from config entry."""
|
||||||
client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
|
client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_add_button(info: ZwaveDiscoveryInfo) -> None:
|
||||||
|
"""Add Z-Wave Button."""
|
||||||
|
driver = client.driver
|
||||||
|
assert driver is not None # Driver is ready before platforms are loaded.
|
||||||
|
entities: list[ZWaveBaseEntity] = []
|
||||||
|
if info.platform_hint == "notification idle":
|
||||||
|
entities.append(ZWaveNotificationIdleButton(config_entry, driver, info))
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_add_ping_button_entity(node: ZwaveNode) -> None:
|
def async_add_ping_button_entity(node: ZwaveNode) -> None:
|
||||||
"""Add ping button entity."""
|
"""Add ping button entity."""
|
||||||
@ -41,6 +54,14 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
config_entry.async_on_unload(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
hass,
|
||||||
|
f"{DOMAIN}_{config_entry.entry_id}_add_{BUTTON_DOMAIN}",
|
||||||
|
async_add_button,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ZWaveNodePingButton(ButtonEntity):
|
class ZWaveNodePingButton(ButtonEntity):
|
||||||
"""Representation of a ping button entity."""
|
"""Representation of a ping button entity."""
|
||||||
@ -88,3 +109,25 @@ class ZWaveNodePingButton(ButtonEntity):
|
|||||||
async def async_press(self) -> None:
|
async def async_press(self) -> None:
|
||||||
"""Press the button."""
|
"""Press the button."""
|
||||||
self.hass.async_create_task(self.node.async_ping())
|
self.hass.async_create_task(self.node.async_ping())
|
||||||
|
|
||||||
|
|
||||||
|
class ZWaveNotificationIdleButton(ZWaveBaseEntity, ButtonEntity):
|
||||||
|
"""Button to idle Notification CC values."""
|
||||||
|
|
||||||
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo
|
||||||
|
) -> None:
|
||||||
|
"""Initialize a ZWaveNotificationIdleButton entity."""
|
||||||
|
super().__init__(config_entry, driver, info)
|
||||||
|
self._attr_name = self.generate_name(
|
||||||
|
include_value_name=True, name_prefix="Idle"
|
||||||
|
)
|
||||||
|
self._attr_unique_id = f"{self._attr_unique_id}.notification_idle"
|
||||||
|
|
||||||
|
async def async_press(self) -> None:
|
||||||
|
"""Press the button."""
|
||||||
|
await self.info.node.async_manually_idle_notification_value(
|
||||||
|
self.info.primary_value
|
||||||
|
)
|
||||||
|
@ -147,6 +147,8 @@ class ZWaveValueDiscoverySchema(DataclassMustHaveAtLeastOne):
|
|||||||
property_key_name: set[str | None] | None = None
|
property_key_name: set[str | None] | None = None
|
||||||
# [optional] the value's metadata_type must match ANY of these values
|
# [optional] the value's metadata_type must match ANY of these values
|
||||||
type: set[str] | None = None
|
type: set[str] | None = None
|
||||||
|
# [optional] the value's states map must include ANY of these key/value pairs
|
||||||
|
any_available_states: set[tuple[int, str]] | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -897,6 +899,17 @@ DISCOVERY_SCHEMAS = [
|
|||||||
type={ValueType.NUMBER},
|
type={ValueType.NUMBER},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
# button
|
||||||
|
# Notification CC idle
|
||||||
|
ZWaveDiscoverySchema(
|
||||||
|
platform=Platform.BUTTON,
|
||||||
|
hint="notification idle",
|
||||||
|
primary_value=ZWaveValueDiscoverySchema(
|
||||||
|
command_class={CommandClass.NOTIFICATION},
|
||||||
|
type={ValueType.NUMBER},
|
||||||
|
any_available_states={(0, "idle")},
|
||||||
|
),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -1072,6 +1085,16 @@ def check_value(value: ZwaveValue, schema: ZWaveValueDiscoverySchema) -> bool:
|
|||||||
# check metadata_type
|
# check metadata_type
|
||||||
if schema.type is not None and value.metadata.type not in schema.type:
|
if schema.type is not None and value.metadata.type not in schema.type:
|
||||||
return False
|
return False
|
||||||
|
# check available states
|
||||||
|
if (
|
||||||
|
schema.any_available_states is not None
|
||||||
|
and value.metadata.states is not None
|
||||||
|
and not any(
|
||||||
|
str(key) in value.metadata.states and value.metadata.states[str(key)] == val
|
||||||
|
for key, val in schema.any_available_states
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,3 +60,35 @@ async def test_ping_entity(
|
|||||||
)
|
)
|
||||||
is None
|
is None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_notification_idle_button(
|
||||||
|
hass: HomeAssistant, client, multisensor_6, integration
|
||||||
|
) -> None:
|
||||||
|
"""Test Notification idle button."""
|
||||||
|
node = multisensor_6
|
||||||
|
state = hass.states.get("button.multisensor_6_idle_cover_status")
|
||||||
|
assert state
|
||||||
|
assert state.state == "unknown"
|
||||||
|
assert state.attributes["friendly_name"] == "Multisensor 6 Idle Cover status"
|
||||||
|
|
||||||
|
# Test successful idle call
|
||||||
|
await hass.services.async_call(
|
||||||
|
BUTTON_DOMAIN,
|
||||||
|
SERVICE_PRESS,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: "button.multisensor_6_idle_cover_status",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(client.async_send_command_no_wait.call_args_list) == 1
|
||||||
|
args = client.async_send_command_no_wait.call_args_list[0][0][0]
|
||||||
|
assert args["command"] == "node.manually_idle_notification_value"
|
||||||
|
assert args["nodeId"] == node.node_id
|
||||||
|
assert args["valueId"] == {
|
||||||
|
"commandClass": 113,
|
||||||
|
"endpoint": 0,
|
||||||
|
"property": "Home Security",
|
||||||
|
"propertyKey": "Cover status",
|
||||||
|
}
|
||||||
|
@ -963,7 +963,7 @@ async def test_removed_device(
|
|||||||
# Check how many entities there are
|
# Check how many entities there are
|
||||||
ent_reg = er.async_get(hass)
|
ent_reg = er.async_get(hass)
|
||||||
entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id)
|
entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id)
|
||||||
assert len(entity_entries) == 31
|
assert len(entity_entries) == 36
|
||||||
|
|
||||||
# Remove a node and reload the entry
|
# Remove a node and reload the entry
|
||||||
old_node = driver.controller.nodes.pop(13)
|
old_node = driver.controller.nodes.pop(13)
|
||||||
@ -975,7 +975,7 @@ async def test_removed_device(
|
|||||||
device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id)
|
device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id)
|
||||||
assert len(device_entries) == 2
|
assert len(device_entries) == 2
|
||||||
entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id)
|
entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id)
|
||||||
assert len(entity_entries) == 18
|
assert len(entity_entries) == 23
|
||||||
assert dev_reg.async_get_device({get_device_id(driver, old_node)}) is None
|
assert dev_reg.async_get_device({get_device_id(driver, old_node)}) is None
|
||||||
|
|
||||||
|
|
||||||
@ -1363,6 +1363,7 @@ async def test_disabled_entity_on_value_removed(
|
|||||||
|
|
||||||
# re-enable this default-disabled entity
|
# re-enable this default-disabled entity
|
||||||
sensor_cover_entity = "sensor.4_in_1_sensor_cover_status"
|
sensor_cover_entity = "sensor.4_in_1_sensor_cover_status"
|
||||||
|
idle_cover_status_button_entity = "button.4_in_1_sensor_idle_cover_status"
|
||||||
er_reg.async_update_entity(entity_id=sensor_cover_entity, disabled_by=None)
|
er_reg.async_update_entity(entity_id=sensor_cover_entity, disabled_by=None)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
@ -1379,6 +1380,10 @@ async def test_disabled_entity_on_value_removed(
|
|||||||
assert state
|
assert state
|
||||||
assert state.state != STATE_UNAVAILABLE
|
assert state.state != STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
state = hass.states.get(idle_cover_status_button_entity)
|
||||||
|
assert state
|
||||||
|
assert state.state != STATE_UNAVAILABLE
|
||||||
|
|
||||||
# check for expected entities
|
# check for expected entities
|
||||||
binary_cover_entity = "binary_sensor.4_in_1_sensor_tampering_product_cover_removed"
|
binary_cover_entity = "binary_sensor.4_in_1_sensor_tampering_product_cover_removed"
|
||||||
state = hass.states.get(binary_cover_entity)
|
state = hass.states.get(binary_cover_entity)
|
||||||
@ -1472,6 +1477,10 @@ async def test_disabled_entity_on_value_removed(
|
|||||||
assert state
|
assert state
|
||||||
assert state.state == STATE_UNAVAILABLE
|
assert state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
state = hass.states.get(idle_cover_status_button_entity)
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
# existing entities and the entities with removed values should be unavailable
|
# existing entities and the entities with removed values should be unavailable
|
||||||
new_unavailable_entities = {
|
new_unavailable_entities = {
|
||||||
state.entity_id
|
state.entity_id
|
||||||
@ -1480,6 +1489,11 @@ async def test_disabled_entity_on_value_removed(
|
|||||||
}
|
}
|
||||||
assert (
|
assert (
|
||||||
unavailable_entities
|
unavailable_entities
|
||||||
| {battery_level_entity, binary_cover_entity, sensor_cover_entity}
|
| {
|
||||||
|
battery_level_entity,
|
||||||
|
binary_cover_entity,
|
||||||
|
sensor_cover_entity,
|
||||||
|
idle_cover_status_button_entity,
|
||||||
|
}
|
||||||
== new_unavailable_entities
|
== new_unavailable_entities
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user