Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add proper support for zwave_js Indicator CC #90248

Merged
merged 27 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
fea76a0
Add proper support for zwave_js Indicator CC
raman325 Mar 24, 2023
4bf798d
remove stale test
raman325 Mar 24, 2023
d7d6219
Make all indicators diagnostic
raman325 Mar 24, 2023
e8f500c
only set entity category if it is specified
raman325 Mar 24, 2023
5b87a63
Only set properties from discovery if specified
raman325 Mar 24, 2023
1747bc4
Conditionally set assumed state as well
raman325 Mar 24, 2023
e7cda14
fix const name
raman325 Mar 28, 2023
8dcafae
Don't create task
raman325 Mar 28, 2023
e707f1a
Disable property keys 3-5 by default
raman325 Mar 28, 2023
b743764
add additional dispatcher_connects so we catch all signals
raman325 Mar 28, 2023
90723e7
be consistent about order
raman325 Mar 28, 2023
6347558
rename new discovery parameter
raman325 Mar 28, 2023
e2cb376
comment
raman325 Mar 28, 2023
6c0c70b
exclude property keys 3-5
raman325 Mar 28, 2023
3158f5a
fix remove logic
raman325 Mar 28, 2023
92720f9
add comment so I don't forget
raman325 Mar 28, 2023
833a973
Switch entity category to config where necessary
raman325 Mar 29, 2023
622884b
cut line
raman325 Mar 29, 2023
d386f7f
less lines
raman325 Mar 29, 2023
c35b0cc
Update homeassistant/components/zwave_js/switch.py
raman325 Mar 29, 2023
b2aa722
Move async_remove to respond to interview started event
raman325 Mar 29, 2023
cab8db8
Set up listener immediately so we don't wait for platform creation
raman325 Mar 29, 2023
526b502
Merge branch 'dev' into indicator
raman325 Apr 30, 2023
74f5885
remove dupe import
raman325 Apr 30, 2023
7e47bf4
black
raman325 Apr 30, 2023
f0d885a
append
raman325 May 24, 2023
6d22501
Merge branch 'dev' of https://github.com/home-assistant/core into ind…
raman325 May 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 13 additions & 10 deletions homeassistant/components/zwave_js/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,19 @@ def remove_device(self, device: dr.DeviceEntry) -> None:

async def async_on_node_added(self, node: ZwaveNode) -> None:
"""Handle node added event."""
# Remove stale entities that may exist from a previous interview when an
# interview is started.
base_unique_id = get_valueless_base_unique_id(self.driver_events.driver, node)
self.config_entry.async_on_unload(
node.on(
"interview started",
lambda _: async_dispatcher_send(
self.hass,
f"{DOMAIN}_{base_unique_id}_remove_entity_on_interview_started",
),
)
)

# No need for a ping button or node status sensor for controller nodes
if not node.is_controller_node:
# Create a node status sensor for each device
Expand Down Expand Up @@ -455,7 +468,6 @@ def __init__(
async def async_on_node_ready(self, node: ZwaveNode) -> None:
"""Handle node ready event."""
LOGGER.debug("Processing node %s", node)
driver = self.controller_events.driver_events.driver
# register (or update) node in device registry
device = self.controller_events.register_node_in_dev_reg(node)
# We only want to create the defaultdict once, even on reinterviews
Expand All @@ -464,15 +476,6 @@ async def async_on_node_ready(self, node: ZwaveNode) -> None:

# Remove any old value ids if this is a reinterview.
self.controller_events.discovered_value_ids.pop(device.id, None)
# Remove stale entities that may exist from a previous interview.
async_dispatcher_send(
self.hass,
(
f"{DOMAIN}_"
f"{get_valueless_base_unique_id(driver, node)}_"
"remove_entity_on_ready_node"
),
)

value_updates_disc_info: dict[str, ZwaveDiscoveryInfo] = {}

Expand Down
20 changes: 20 additions & 0 deletions homeassistant/components/zwave_js/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ def async_add_button(info: ZwaveDiscoveryInfo) -> None:
entities: list[ZWaveBaseEntity] = []
if info.platform_hint == "notification idle":
entities.append(ZWaveNotificationIdleButton(config_entry, driver, info))
else:
entities.append(ZwaveBooleanNodeButton(config_entry, driver, info))

async_add_entities(entities)

Expand Down Expand Up @@ -63,6 +65,21 @@ def async_add_ping_button_entity(node: ZwaveNode) -> None:
)


class ZwaveBooleanNodeButton(ZWaveBaseEntity, ButtonEntity):
raman325 marked this conversation as resolved.
Show resolved Hide resolved
"""Representation of a ZWave button entity for a boolean value."""

def __init__(
self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo
) -> None:
"""Initialize entity."""
super().__init__(config_entry, driver, info)
self._attr_name = self.generate_name(include_value_name=True)

async def async_press(self) -> None:
"""Press the button."""
await self.info.node.async_set_value(self.info.primary_value, True)


class ZWaveNodePingButton(ButtonEntity):
"""Representation of a ping button entity."""

Expand Down Expand Up @@ -98,6 +115,9 @@ async def async_added_to_hass(self) -> None:
)
)

# we don't listen for `remove_entity_on_ready_node` signal because this entity
# is created when the node is added which occurs before ready. It only needs to
# be removed if the node is removed from the network.
self.async_on_remove(
async_dispatcher_connect(
self.hass,
Expand Down
89 changes: 75 additions & 14 deletions homeassistant/components/zwave_js/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
from zwave_js_server.model.value import Value as ZwaveValue

from homeassistant.backports.enum import StrEnum
from homeassistant.const import Platform
from homeassistant.const import EntityCategory, Platform
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceEntry

Expand Down Expand Up @@ -108,7 +108,8 @@ class ZwaveDiscoveryInfo:
node: ZwaveNode
# the value object itself for primary value
primary_value: ZwaveValue
# bool to specify whether state is assumed and events should be fired on value update
# bool to specify whether state is assumed and events should be fired on value
# update
assumed_state: bool
# the home assistant platform for which an entity should be created
platform: Platform
Expand All @@ -122,6 +123,8 @@ class ZwaveDiscoveryInfo:
platform_data_template: BaseDiscoverySchemaDataTemplate | None = None
# bool to specify whether entity should be enabled by default
entity_registry_enabled_default: bool = True
# the entity category for the discovered entity
entity_category: EntityCategory | None = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note: I think we should refactor the discovery and design it like we do in the matter integration. There we attach an Entity class and an EntityDescription to each schema instance and define the schema instances in each platform.

entity_category and entity_registry_enabled_default are entity details and belong in the entity description not in the discovery info.

It's a bigger refactor so something for a separate PR of course.



@dataclass
Expand All @@ -143,8 +146,14 @@ class ZWaveValueDiscoverySchema(DataclassMustHaveAtLeastOne):
property_name: set[str] | None = None
# [optional] the value's property key must match ANY of these values
property_key: set[str | int | None] | None = None
# [optional] the value's property key must NOT match ANY of these values
not_property_key: set[str | int | None] | None = None
# [optional] the value's metadata_type must match ANY of these values
type: set[str] | None = None
# [optional] the value's metadata_readable must match this value
readable: bool | None = None
# [optional] the value's metadata_writeable must match this value
writeable: bool | 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

Expand Down Expand Up @@ -192,6 +201,8 @@ class ZWaveDiscoverySchema:
assumed_state: bool = False
# [optional] bool to specify whether entity should be enabled by default
entity_registry_enabled_default: bool = True
# [optional] the entity category for the discovered entity
entity_category: EntityCategory | None = None


def get_config_parameter_discovery_schema(
Expand Down Expand Up @@ -695,23 +706,26 @@ def get_config_parameter_discovery_schema(
),
allow_multi=True,
),
# generic text sensors
# binary sensor for Indicator CC
ZWaveDiscoverySchema(
platform=Platform.SENSOR,
hint="string_sensor",
platform=Platform.BINARY_SENSOR,
hint="boolean",
primary_value=ZWaveValueDiscoverySchema(
command_class={CommandClass.SENSOR_ALARM},
type={ValueType.STRING},
command_class={CommandClass.INDICATOR},
type={ValueType.BOOLEAN},
readable=True,
writeable=False,
),
entity_category=EntityCategory.DIAGNOSTIC,
),
# generic text sensors
ZWaveDiscoverySchema(
platform=Platform.SENSOR,
hint="string_sensor",
primary_value=ZWaveValueDiscoverySchema(
command_class={CommandClass.INDICATOR},
command_class={CommandClass.SENSOR_ALARM},
type={ValueType.STRING},
),
entity_registry_enabled_default=False,
),
# generic numeric sensors
ZWaveDiscoverySchema(
Expand All @@ -733,9 +747,11 @@ def get_config_parameter_discovery_schema(
primary_value=ZWaveValueDiscoverySchema(
command_class={CommandClass.INDICATOR},
type={ValueType.NUMBER},
readable=True,
writeable=False,
),
data_template=NumericSensorDataTemplate(),
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
# Meter sensors for Meter CC
ZWaveDiscoverySchema(
Expand Down Expand Up @@ -768,9 +784,7 @@ def get_config_parameter_discovery_schema(
platform=Platform.NUMBER,
hint="Basic",
primary_value=ZWaveValueDiscoverySchema(
command_class={
CommandClass.BASIC,
},
command_class={CommandClass.BASIC},
type={ValueType.NUMBER},
property={CURRENT_VALUE_PROPERTY},
),
Expand All @@ -783,14 +797,48 @@ def get_config_parameter_discovery_schema(
property={TARGET_VALUE_PROPERTY},
)
],
data_template=NumericSensorDataTemplate(),
entity_registry_enabled_default=False,
),
# number for Indicator CC (exclude property keys 3-5)
ZWaveDiscoverySchema(
platform=Platform.NUMBER,
primary_value=ZWaveValueDiscoverySchema(
command_class={CommandClass.INDICATOR},
type={ValueType.NUMBER},
not_property_key={3, 4, 5},
readable=True,
writeable=True,
),
entity_category=EntityCategory.CONFIG,
),
# button for Indicator CC
ZWaveDiscoverySchema(
platform=Platform.BUTTON,
primary_value=ZWaveValueDiscoverySchema(
command_class={CommandClass.INDICATOR},
type={ValueType.BOOLEAN},
readable=False,
writeable=True,
),
entity_category=EntityCategory.CONFIG,
),
# binary switches
ZWaveDiscoverySchema(
platform=Platform.SWITCH,
primary_value=SWITCH_BINARY_CURRENT_VALUE_SCHEMA,
),
# switch for Indicator CC
ZWaveDiscoverySchema(
platform=Platform.SWITCH,
hint="indicator",
primary_value=ZWaveValueDiscoverySchema(
command_class={CommandClass.INDICATOR},
type={ValueType.BOOLEAN},
readable=True,
writeable=True,
),
entity_category=EntityCategory.CONFIG,
),
# binary switch
# barrier operator signaling states
ZWaveDiscoverySchema(
Expand Down Expand Up @@ -1023,6 +1071,7 @@ def async_discover_single_value(
platform_data=resolved_data,
additional_value_ids_to_watch=additional_value_ids_to_watch,
entity_registry_enabled_default=schema.entity_registry_enabled_default,
entity_category=schema.entity_category,
)

if not schema.allow_multi:
Expand Down Expand Up @@ -1058,9 +1107,21 @@ def check_value(value: ZwaveValue, schema: ZWaveValueDiscoverySchema) -> bool:
and value.property_key not in schema.property_key
):
return False
# check property_key against not_property_key set
if (
schema.not_property_key is not None
and value.property_key in schema.not_property_key
):
return False
# check metadata_type
if schema.type is not None and value.metadata.type not in schema.type:
return False
# check metadata_readable
if schema.readable is not None and value.metadata.readable != schema.readable:
return False
# check metadata_writeable
if schema.writeable is not None and value.metadata.writeable != schema.writeable:
return False
# check available states
if (
schema.any_available_states is not None
Expand Down
23 changes: 18 additions & 5 deletions homeassistant/components/zwave_js/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@ def __init__(
# Entity class attributes
self._attr_name = self.generate_name()
self._attr_unique_id = get_unique_id(driver, self.info.primary_value.value_id)
self._attr_entity_registry_enabled_default = (
self.info.entity_registry_enabled_default
)
self._attr_assumed_state = self.info.assumed_state
if self.info.entity_registry_enabled_default is False:
self._attr_entity_registry_enabled_default = False
if self.info.entity_category is not None:
self._attr_entity_category = self.info.entity_category
if self.info.assumed_state:
self._attr_assumed_state = True
# device is precreated in main handler
self._attr_device_info = DeviceInfo(
identifiers={get_device_id(driver, self.info.node)},
Expand Down Expand Up @@ -103,7 +105,18 @@ async def async_added_to_hass(self) -> None:
(
f"{DOMAIN}_"
f"{get_valueless_base_unique_id(self.driver, self.info.node)}_"
"remove_entity_on_ready_node"
"remove_entity"
),
self.async_remove,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
(
f"{DOMAIN}_"
f"{get_valueless_base_unique_id(self.driver, self.info.node)}_"
"remove_entity_on_interview_started"
),
self.async_remove,
)
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/zwave_js/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ def __init__(
self._target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY)

# Entity class attributes
self._attr_name = self.generate_name(alternate_value_name=info.platform_hint)
self._attr_name = self.generate_name(
include_value_name=True, alternate_value_name=info.platform_hint
)

@property
def native_min_value(self) -> float:
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/zwave_js/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,9 @@ async def async_added_to_hass(self) -> None:
self.async_poll_value,
)
)
# we don't listen for `remove_entity_on_ready_node` signal because this entity
# is created when the node is added which occurs before ready. It only needs to
# be removed if the node is removed from the network.
self.async_on_remove(
async_dispatcher_connect(
self.hass,
Expand Down
14 changes: 14 additions & 0 deletions homeassistant/components/zwave_js/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def async_add_switch(info: ZwaveDiscoveryInfo) -> None:
entities.append(
ZWaveBarrierEventSignalingSwitch(config_entry, driver, info)
)
elif info.platform_hint == "indicator":
entities.append(ZWaveIndicatorSwitch(config_entry, driver, info))
else:
entities.append(ZWaveSwitch(config_entry, driver, info))

Expand Down Expand Up @@ -85,6 +87,18 @@ async def async_turn_off(self, **kwargs: Any) -> None:
await self.info.node.async_set_value(self._target_value, False)


class ZWaveIndicatorSwitch(ZWaveSwitch):
"""Representation of a Z-Wave Indicator CC switch."""

def __init__(
self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo
) -> None:
"""Initialize the switch."""
super().__init__(config_entry, driver, info)
self._target_value = self.info.primary_value
self._attr_name = self.generate_name(include_value_name=True)


class ZWaveBarrierEventSignalingSwitch(ZWaveBaseEntity, SwitchEntity):
"""Switch is used to turn on/off a barrier device's event signaling subsystem."""

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/zwave_js/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ async def async_added_to_hass(self) -> None:
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._base_unique_id}_remove_entity_on_ready_node",
f"{DOMAIN}_{self._base_unique_id}_remove_entity_on_interview_started",
self.async_remove,
)
)
Expand Down