Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 4 additions & 43 deletions homeassistant/components/zwave_js/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import DOMAIN, LOGGER
from .const import DOMAIN
from .discovery import ZwaveDiscoveryInfo
from .entity import ZWaveBaseEntity
from .helpers import get_device_info, get_valueless_base_unique_id
from .entity import ZWaveBaseEntity, ZWaveNodeBaseEntity
from .models import ZwaveJSConfigEntry

PARALLEL_UPDATES = 0
Expand Down Expand Up @@ -78,54 +77,16 @@ async def async_press(self) -> None:
await self._async_set_value(self.info.primary_value, True)


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

_attr_should_poll = False
_attr_entity_category = EntityCategory.CONFIG
_attr_has_entity_name = True
_attr_translation_key = "ping"

def __init__(self, driver: Driver, node: ZwaveNode) -> None:
"""Initialize a ping Z-Wave device button entity."""
self.node = node

# Entity class attributes
self._base_unique_id = get_valueless_base_unique_id(driver, node)
super().__init__(driver, node)
self._attr_unique_id = f"{self._base_unique_id}.ping"
# device may not be precreated in main handler yet
self._attr_device_info = get_device_info(driver, node)

async def async_poll_value(self, _: bool) -> None:
"""Poll a value."""
# We log an error instead of raising an exception because this service call occurs
# in a separate task since it is called via the dispatcher and we don't want to
# raise the exception in that separate task because it is confusing to the user.
LOGGER.error(
"There is no value to refresh for this entity so the zwave_js.refresh_value"
" service won't work for it"
)

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.unique_id}_poll_value",
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,
f"{DOMAIN}_{self._base_unique_id}_remove_entity",
self.async_remove,
)
)

async def async_press(self) -> None:
"""Press the button."""
Expand Down
70 changes: 69 additions & 1 deletion homeassistant/components/zwave_js/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from zwave_js_server.exceptions import BaseZwaveJSServerError
from zwave_js_server.model.driver import Driver
from zwave_js_server.model.node import Node as ZwaveNode
from zwave_js_server.model.value import (
SetValueResult,
Value as ZwaveValue,
Expand All @@ -28,7 +29,12 @@
LOGGER,
)
from .discovery_data_template import BaseDiscoverySchemaDataTemplate
from .helpers import get_device_id, get_unique_id, get_valueless_base_unique_id
from .helpers import (
get_device_id,
get_device_info,
get_unique_id,
get_valueless_base_unique_id,
)
from .models import PlatformZwaveDiscoveryInfo, ZwaveDiscoveryInfo, ZwaveJSConfigEntry


Expand Down Expand Up @@ -426,3 +432,65 @@ async def _async_set_value(
raise HomeAssistantError(
f"Unable to set value {value.value_id}: {err}"
) from err


class ZWaveNodeBaseEntity(Entity):
"""Base entity class for Z-Wave node-level (non-value) entities.

Used for entities that exist for the whole node rather than a specific
Z-Wave Value (e.g. firmware update, ping button, node status sensor).
"""

_attr_has_entity_name = True
_attr_should_poll = False

# Subclasses can opt in to also being removed when a node starts a
# reinterview. Useful for entities whose existence depends on CCs that
# may disappear during reinterview.
_remove_on_reinterview = False

def __init__(self, driver: Driver, node: ZwaveNode) -> None:
"""Initialize a Z-Wave node-level entity."""
self.driver = driver
self.node = node

self._base_unique_id = get_valueless_base_unique_id(driver, node)
# device may not be precreated in main handler yet
self._attr_device_info = get_device_info(driver, node)

async def async_poll_value(self, _: bool) -> None:
"""Poll a value (no-op for entities not backed by a Z-Wave Value)."""
# We log an error instead of raising an exception because this service
# call occurs in a separate task since it is called via the dispatcher
# and we don't want to raise the exception in that separate task because
# it is confusing to the user.
LOGGER.error(
"There is no value to refresh for %s so the zwave_js.refresh_value"
" service won't work for it",
self.entity_id,
)

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.unique_id}_poll_value",
self.async_poll_value,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._base_unique_id}_remove_entity",
self.async_remove,
)
)
if self._remove_on_reinterview:
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._base_unique_id}_remove_entity_on_interview_started",
self.async_remove,
)
)
122 changes: 13 additions & 109 deletions homeassistant/components/zwave_js/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,7 @@
NumericSensorDataTemplate,
NumericSensorDataTemplateData,
)
from .entity import NewZwaveDiscoveryInfo, ZWaveBaseEntity
from .helpers import get_device_info, get_valueless_base_unique_id
from .entity import NewZwaveDiscoveryInfo, ZWaveBaseEntity, ZWaveNodeBaseEntity
from .migrate import async_migrate_statistics_sensors
from .models import (
NewZWaveDiscoverySchema,
Expand Down Expand Up @@ -1033,36 +1032,19 @@ def extra_state_attributes(self) -> dict[str, str] | None:
return {ATTR_VALUE: value}


class ZWaveNodeStatusSensor(SensorEntity):
class ZWaveNodeStatusSensor(ZWaveNodeBaseEntity, SensorEntity):
"""Representation of a node status sensor."""

_attr_should_poll = False
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_has_entity_name = True
_attr_translation_key = "node_status"

def __init__(
self, config_entry: ZwaveJSConfigEntry, driver: Driver, node: ZwaveNode
) -> None:
"""Initialize a generic Z-Wave device entity."""
super().__init__(driver, node)
self.config_entry = config_entry
self.node = node

# Entity class attributes
self._base_unique_id = get_valueless_base_unique_id(driver, node)
self._attr_unique_id = f"{self._base_unique_id}.node_status"
# device may not be precreated in main handler yet
self._attr_device_info = get_device_info(driver, node)

async def async_poll_value(self, _: bool) -> None:
"""Poll a value."""
# We log an error instead of raising an exception because this service call occurs
# in a separate task since it is called via the dispatcher and we don't want to
# raise the exception in that separate task because it is confusing to the user.
LOGGER.error(
"There is no value to refresh for this entity so the zwave_js.refresh_value"
" service won't work for it"
)

@callback
def _status_changed(self, _: dict) -> None:
Expand All @@ -1072,60 +1054,27 @@ def _status_changed(self, _: dict) -> None:

async def async_added_to_hass(self) -> None:
"""Call when entity is added."""
# Add value_changed callbacks.
await super().async_added_to_hass()
for evt in ("wake up", "sleep", "dead", "alive"):
self.async_on_remove(self.node.on(evt, self._status_changed))
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self.unique_id}_poll_value",
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,
f"{DOMAIN}_{self._base_unique_id}_remove_entity",
self.async_remove,
)
)
self._attr_native_value: str = self.node.status.name.lower()
self.async_write_ha_state()


class ZWaveControllerStatusSensor(SensorEntity):
class ZWaveControllerStatusSensor(ZWaveNodeBaseEntity, SensorEntity):
"""Representation of a controller status sensor."""

_attr_should_poll = False
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_has_entity_name = True
_attr_translation_key = "controller_status"

def __init__(self, config_entry: ZwaveJSConfigEntry, driver: Driver) -> None:
"""Initialize a generic Z-Wave device entity."""
self.config_entry = config_entry
self.controller = driver.controller
node = self.controller.own_node
assert node

# Entity class attributes
self._base_unique_id = get_valueless_base_unique_id(driver, node)
super().__init__(driver, node)
self.config_entry = config_entry
self._attr_unique_id = f"{self._base_unique_id}.controller_status"
# device may not be precreated in main handler yet
self._attr_device_info = get_device_info(driver, node)

async def async_poll_value(self, _: bool) -> None:
"""Poll a value."""
# We log an error instead of raising an exception because this service call occurs
# in a separate task since it is called via the dispatcher and we don't want to
# raise the exception in that separate task because it is confusing to the user.
LOGGER.error(
"There is no value to refresh for this entity so the zwave_js.refresh_value"
" service won't work for it"
)

@callback
def _status_changed(self, _: dict) -> None:
Expand All @@ -1135,34 +1084,16 @@ def _status_changed(self, _: dict) -> None:

async def async_added_to_hass(self) -> None:
"""Call when entity is added."""
# Add value_changed callbacks.
await super().async_added_to_hass()
self.async_on_remove(self.controller.on("status changed", self._status_changed))
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self.unique_id}_poll_value",
self.async_poll_value,
)
)
# we don't listen for `remove_entity_on_ready_node` signal because this is not
# a regular node
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._base_unique_id}_remove_entity",
self.async_remove,
)
)
self._attr_native_value: str = self.controller.status.name.lower()


class ZWaveStatisticsSensor(SensorEntity):
class ZWaveStatisticsSensor(ZWaveNodeBaseEntity, SensorEntity):
"""Representation of a node/controller statistics sensor."""

entity_description: ZWaveJSStatisticsSensorEntityDescription
_attr_should_poll = False
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_has_entity_name = True

def __init__(
self,
Expand All @@ -1172,31 +1103,17 @@ def __init__(
description: ZWaveJSStatisticsSensorEntityDescription,
) -> None:
"""Initialize a Z-Wave statistics entity."""
self.entity_description = description
self.config_entry = config_entry
self.statistics_src = statistics_src
node = (
statistics_src.own_node
if isinstance(statistics_src, Controller)
else statistics_src
)
assert node

# Entity class attributes
self._base_unique_id = get_valueless_base_unique_id(driver, node)
super().__init__(driver, node)
self.entity_description = description
self.config_entry = config_entry
self._attr_unique_id = f"{self._base_unique_id}.statistics_{description.key}"
# device may not be precreated in main handler yet
self._attr_device_info = get_device_info(driver, node)

async def async_poll_value(self, _: bool) -> None:
"""Poll a value."""
# We log an error instead of raising an exception because this service call occurs
# in a separate task since it is called via the dispatcher and we don't want to
# raise the exception in that separate task because it is confusing to the user.
LOGGER.error(
"There is no value to refresh for this entity so the zwave_js.refresh_value"
" service won't work for it"
)

@callback
def _statistics_updated(self, event_data: dict) -> None:
Expand Down Expand Up @@ -1226,20 +1143,7 @@ def _set_statistics(

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.unique_id}_poll_value",
self.async_poll_value,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._base_unique_id}_remove_entity",
self.async_remove,
)
)
await super().async_added_to_hass()
self.async_on_remove(
self.statistics_src.on("statistics updated", self._statistics_updated)
)
Expand Down
Loading