Add proper support for zwave_js Indicator CC (#90248)

* Add proper support for zwave_js Indicator CC

* remove stale test

* Make all indicators diagnostic

* only set entity category if it is specified

* Only set properties from discovery if specified

* Conditionally set assumed state as well

* fix const name

* Don't create task

* Disable property keys 3-5 by default

* add additional dispatcher_connects so we catch all signals

* be consistent about order

* rename new discovery parameter

* comment

* exclude property keys 3-5

* fix remove logic

* add comment so I don't forget

* Switch entity category to config where necessary

* cut line

* less lines

* Update homeassistant/components/zwave_js/switch.py

Co-authored-by: kpine <keith.pine@gmail.com>

* Move async_remove to respond to interview started event

* Set up listener immediately so we don't wait for platform creation

* remove dupe import

* black

* append

---------

Co-authored-by: kpine <keith.pine@gmail.com>
This commit is contained in:
Raman Gupta 2023-05-24 09:09:38 -04:00 committed by GitHub
parent 66f7218b68
commit 872cd47e87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1862 additions and 80 deletions

View file

@ -321,6 +321,19 @@ class ControllerEvents:
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
@ -455,7 +468,6 @@ class NodeEvents:
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
@ -464,15 +476,6 @@ class NodeEvents:
# 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] = {}

View file

@ -36,6 +36,8 @@ async def async_setup_entry(
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)
@ -63,6 +65,21 @@ async def async_setup_entry(
)
class ZwaveBooleanNodeButton(ZWaveBaseEntity, ButtonEntity):
"""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."""
@ -98,6 +115,9 @@ class ZWaveNodePingButton(ButtonEntity):
)
)
# 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,

View file

@ -44,7 +44,7 @@ from zwave_js_server.model.node import Node as ZwaveNode
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
@ -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
@ -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
@dataclass
@ -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
@ -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(
@ -695,6 +706,18 @@ DISCOVERY_SCHEMAS = [
),
allow_multi=True,
),
# binary sensor for Indicator CC
ZWaveDiscoverySchema(
platform=Platform.BINARY_SENSOR,
hint="boolean",
primary_value=ZWaveValueDiscoverySchema(
command_class={CommandClass.INDICATOR},
type={ValueType.BOOLEAN},
readable=True,
writeable=False,
),
entity_category=EntityCategory.DIAGNOSTIC,
),
# generic text sensors
ZWaveDiscoverySchema(
platform=Platform.SENSOR,
@ -704,15 +727,6 @@ DISCOVERY_SCHEMAS = [
type={ValueType.STRING},
),
),
ZWaveDiscoverySchema(
platform=Platform.SENSOR,
hint="string_sensor",
primary_value=ZWaveValueDiscoverySchema(
command_class={CommandClass.INDICATOR},
type={ValueType.STRING},
),
entity_registry_enabled_default=False,
),
# generic numeric sensors
ZWaveDiscoverySchema(
platform=Platform.SENSOR,
@ -733,9 +747,11 @@ DISCOVERY_SCHEMAS = [
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(
@ -768,9 +784,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.NUMBER,
hint="Basic",
primary_value=ZWaveValueDiscoverySchema(
command_class={
CommandClass.BASIC,
},
command_class={CommandClass.BASIC},
type={ValueType.NUMBER},
property={CURRENT_VALUE_PROPERTY},
),
@ -783,14 +797,48 @@ DISCOVERY_SCHEMAS = [
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(
@ -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:
@ -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

View file

@ -44,10 +44,12 @@ class ZWaveBaseEntity(Entity):
# 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)},
@ -103,7 +105,18 @@ class ZWaveBaseEntity(Entity):
(
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,
)

View file

@ -66,7 +66,9 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity):
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:

View file

@ -615,6 +615,9 @@ class ZWaveNodeStatusSensor(SensorEntity):
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,

View file

@ -41,6 +41,8 @@ async def async_setup_entry(
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))
@ -85,6 +87,18 @@ class ZWaveSwitch(ZWaveBaseEntity, SwitchEntity):
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."""

View file

@ -317,7 +317,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
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,
)
)

View file

@ -11,6 +11,8 @@ from zwave_js_server.model.driver import Driver
from zwave_js_server.model.node import Node
from zwave_js_server.version import VersionInfo
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture
# Add-on fixtures
@ -109,7 +111,7 @@ def mock_addon_options(addon_info):
def set_addon_options_side_effect_fixture(addon_options):
"""Return the set add-on options side effect."""
async def set_addon_options(hass, slug, options):
async def set_addon_options(hass: HomeAssistant, slug, options):
"""Mock set add-on options."""
addon_options.update(options["options"])
@ -130,7 +132,7 @@ def mock_set_addon_options(set_addon_options_side_effect):
def install_addon_side_effect_fixture(addon_store_info, addon_info):
"""Return the install add-on side effect."""
async def install_addon(hass, slug):
async def install_addon(hass: HomeAssistant, slug):
"""Mock install add-on."""
addon_store_info.return_value = {
"available": True,
@ -168,7 +170,7 @@ def mock_update_addon():
def start_addon_side_effect_fixture(addon_store_info, addon_info):
"""Return the start add-on options side effect."""
async def start_addon(hass, slug):
async def start_addon(hass: HomeAssistant, slug):
"""Mock start add-on."""
addon_store_info.return_value = {
"available": True,
@ -365,7 +367,7 @@ def climate_adc_t3000_state_fixture():
@pytest.fixture(name="climate_danfoss_lc_13_state", scope="session")
def climate_danfoss_lc_13_state_fixture():
"""Load the climate Danfoss (LC-13) electronic radiator thermostat node state fixture data."""
"""Load Danfoss (LC-13) electronic radiator thermostat node state fixture data."""
return json.loads(load_fixture("zwave_js/climate_danfoss_lc_13_state.json"))
@ -522,7 +524,7 @@ def climate_radio_thermostat_ct101_multiple_temp_units_state_fixture():
scope="session",
)
def climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state_fixture():
"""Load the climate device with mode and setpoint on different endpoints node state fixture data."""
"""Load climate device w/ mode+setpoint on diff endpoints node state fixture data."""
return json.loads(
load_fixture(
"zwave_js/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json"
@ -604,6 +606,18 @@ def lock_home_connect_620_state_fixture():
return json.loads(load_fixture("zwave_js/lock_home_connect_620_state.json"))
@pytest.fixture(name="switch_zooz_zen72_state", scope="session")
def switch_zooz_zen72_state_fixture():
"""Load the Zooz Zen72 switch node state fixture data."""
return json.loads(load_fixture("zwave_js/switch_zooz_zen72_state.json"))
@pytest.fixture(name="indicator_test_state", scope="session")
def indicator_test_state_fixture():
"""Load the indicator CC test node state fixture data."""
return json.loads(load_fixture("zwave_js/indicator_test_state.json"))
# model fixtures
@ -612,7 +626,6 @@ def mock_client_fixture(
controller_state, controller_node_state, version_state, log_config_state
):
"""Mock a client."""
with patch(
"homeassistant.components.zwave_js.ZwaveClient", autospec=True
) as client_class:
@ -722,7 +735,7 @@ def climate_radio_thermostat_ct100_plus_fixture(
def climate_radio_thermostat_ct100_plus_different_endpoints_fixture(
client, climate_radio_thermostat_ct100_plus_different_endpoints_state
):
"""Mock a climate radio thermostat ct100 plus node with values on different endpoints."""
"""Mock climate radio thermostat ct100 plus node w/ values on diff endpoints."""
node = Node(
client,
copy.deepcopy(climate_radio_thermostat_ct100_plus_different_endpoints_state),
@ -773,7 +786,7 @@ def climate_adc_t3000_missing_mode_fixture(client, climate_adc_t3000_state):
@pytest.fixture(name="climate_adc_t3000_missing_fan_mode_states")
def climate_adc_t3000_missing_fan_mode_states_fixture(client, climate_adc_t3000_state):
"""Mock a climate ADC-T3000 node with missing 'states' metadata on Thermostat Fan Mode."""
"""Mock ADC-T3000 node w/ missing 'states' metadata on Thermostat Fan Mode."""
data = copy.deepcopy(climate_adc_t3000_state)
data["name"] = f"{data['name']} missing fan mode states"
for value in data["values"]:
@ -872,7 +885,7 @@ def nortek_thermostat_removed_event_fixture(client):
@pytest.fixture(name="integration")
async def integration_fixture(hass, client):
async def integration_fixture(hass: HomeAssistant, client):
"""Set up the zwave_js integration."""
entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
entry.add_to_hass(hass)
@ -1146,3 +1159,19 @@ def lock_home_connect_620_fixture(client, lock_home_connect_620_state):
node = Node(client, copy.deepcopy(lock_home_connect_620_state))
client.driver.controller.nodes[node.node_id] = node
return node
@pytest.fixture(name="switch_zooz_zen72")
def switch_zooz_zen72_fixture(client, switch_zooz_zen72_state):
"""Mock a Zooz Zen72 switch node."""
node = Node(client, copy.deepcopy(switch_zooz_zen72_state))
client.driver.controller.nodes[node.node_id] = node
return node
@pytest.fixture(name="indicator_test")
def indicator_test_fixture(client, indicator_test_state):
"""Mock a indicator CC test node."""
node = Node(client, copy.deepcopy(indicator_test_state))
client.driver.controller.nodes[node.node_id] = node
return node

View file

@ -0,0 +1,190 @@
{
"nodeId": 43,
"index": 0,
"installerIcon": 1536,
"userIcon": 1540,
"status": 4,
"ready": true,
"isListening": true,
"isRouting": true,
"isSecure": true,
"manufacturerId": 634,
"productId": 40962,
"productType": 28672,
"firmwareVersion": "10.0.1",
"zwavePlusVersion": 2,
"location": "**REDACTED**",
"deviceConfig": {
"filename": "/usr/src/app/store/.config-db/devices/0x027a/zen72.json",
"isEmbedded": true,
"manufacturer": "Fake",
"manufacturerId": 634,
"label": "Device",
"description": "This is a fake device",
"devices": [
{
"productType": 28672,
"productId": 40962
}
],
"firmwareVersion": {
"min": "0.0",
"max": "255.255"
},
"associations": {},
"paramInformation": {
"_map": {}
},
"compat": {
"skipConfigurationNameQuery": true,
"skipConfigurationInfoQuery": true
},
"metadata": {
"inclusion": "1. Initiate inclusion (pairing) in the app (or web interface).\n2. TAP UP 3 TIMES QUICKLY if using traditional Z-Wave inclusion.\n3. The LED indicator will blink blue to signal communication and turn green for 3 seconds if inclusion is successful or turn red for 3 seconds if the pairing attempt fails",
"exclusion": "1. Bring your Z-Wave gateway (hub) close to the switch if possible\n2. Put the Z-Wave hub into exclusion mode (not sure how to do that? ask@getzooz.com) \n3. Tap the lower paddle on the switch 3 times quickly (the LED indicator will start blinking blue)\n4. Your hub will confirm exclusion, the LED indicator on the switch will turn green for 3 seconds, and the device will disappear from your controller's device list",
"reset": "If your primary controller is missing or inoperable, you may need to reset the device to factory settings. To reset the switch, press and hold the lower paddle for 10 seconds until the LED indicator starts blinking. Release paddle, and immediately after, tap the lower paddle 5 times to complete the reset. The LED indicator will flash blue 3 times and turn red for 3 seconds to confirm successful reset",
"manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=product_documents/4108/zooz-700-series-z-wave-dimmer-zen72-manual.pdf"
}
},
"label": "Device",
"interviewAttempts": 0,
"endpoints": [
{
"nodeId": 43,
"index": 0,
"installerIcon": 1536,
"userIcon": 1540,
"deviceClass": {
"basic": {
"key": 4,
"label": "Routing Slave"
},
"generic": {
"key": 17,
"label": "Multilevel Switch"
},
"specific": {
"key": 1,
"label": "Multilevel Power Switch"
},
"mandatorySupportedCCs": [32, 38, 39],
"mandatoryControlledCCs": []
},
"commandClasses": [
{
"id": 135,
"name": "Indicator",
"version": 3,
"isSecure": true
}
]
}
],
"values": [
{
"endpoint": 0,
"commandClass": 135,
"commandClassName": "Indicator",
"property": "Test",
"propertyKey": "Sensor",
"propertyName": "Test",
"propertyKeyName": "Sensor",
"ccVersion": 3,
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"description": "Tests non-writeable Indicator CC number gets created as a sensor entity",
"label": "Sensor",
"ccSpecific": {
"indicatorId": 80,
"propertyId": 4
}
},
"value": 0
},
{
"endpoint": 0,
"commandClass": 135,
"commandClassName": "Indicator",
"property": "Test",
"propertyKey": "Switch",
"propertyName": "Test",
"propertyKeyName": "Switch",
"ccVersion": 3,
"metadata": {
"type": "boolean",
"readable": true,
"writeable": true,
"description": "Tests writeable Indicator CC boolean gets created as a switch entity",
"label": "Switch",
"ccSpecific": {
"indicatorId": 80,
"propertyId": 5
}
},
"value": false
},
{
"endpoint": 0,
"commandClass": 135,
"commandClassName": "Indicator",
"property": "Test",
"propertyKey": "Binary Sensor",
"propertyName": "Test",
"propertyKeyName": "Binary Sensor",
"ccVersion": 3,
"metadata": {
"type": "boolean",
"readable": true,
"writeable": false,
"description": "Tests non-writeable Indicator CC boolean gets created as a binary sensor entity",
"label": "Binary Sensor",
"ccSpecific": {
"indicatorId": 80,
"propertyId": 5
}
},
"value": false
}
],
"isFrequentListening": false,
"maxDataRate": 100000,
"supportedDataRates": [40000, 100000],
"protocolVersion": 3,
"supportsBeaming": true,
"supportsSecurity": false,
"nodeType": 1,
"zwavePlusNodeType": 0,
"zwavePlusRoleType": 5,
"deviceClass": {
"basic": {
"key": 4,
"label": "Routing Slave"
},
"generic": {
"key": 17,
"label": "Multilevel Switch"
},
"specific": {
"key": 1,
"label": "Multilevel Power Switch"
},
"mandatorySupportedCCs": [32, 38, 39],
"mandatoryControlledCCs": []
},
"interviewStage": "Complete",
"deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x027a:0x7000:0xa002:10.0.1",
"statistics": {
"commandsTX": 64,
"commandsRX": 88,
"commandsDroppedRX": 0,
"commandsDroppedTX": 0,
"timeoutResponse": 0,
"rtt": 74.5,
"rssi": -60
},
"highestSecurityClass": 1,
"isControllerNode": false,
"keepAwake": false
}

File diff suppressed because it is too large Load diff

View file

@ -85,6 +85,8 @@ from homeassistant.helpers import device_registry as dr
from tests.common import MockUser
from tests.typing import ClientSessionGenerator, WebSocketGenerator
CONTROLLER_PATCH_PREFIX = "zwave_js_server.model.controller.Controller"
def get_device(hass: HomeAssistant, node):
"""Get device ID for a node."""
@ -131,7 +133,7 @@ async def test_network_status(
# Try API call with entry ID
with patch(
"zwave_js_server.model.controller.Controller.async_get_state",
f"{CONTROLLER_PATCH_PREFIX}.async_get_state",
return_value=controller_state["controller"],
):
await ws_client.send_json(
@ -155,7 +157,7 @@ async def test_network_status(
)
assert device
with patch(
"zwave_js_server.model.controller.Controller.async_get_state",
f"{CONTROLLER_PATCH_PREFIX}.async_get_state",
return_value=controller_state["controller"],
):
await ws_client.send_json(
@ -410,7 +412,8 @@ async def test_node_metadata(
"controller being inoperable or otherwise unavailable.)"
)
assert result["manual"] == (
"https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/2479/ZP3111-5_R2_20170316.pdf"
"https://products.z-wavealliance.org/ProductManual/File?folder=&filename="
"MarketCertificationFiles/2479/ZP3111-5_R2_20170316.pdf"
)
assert not result["wakeup"]
assert (
@ -885,7 +888,7 @@ async def test_add_node(
# Test FailedZWaveCommand is caught
with patch(
"zwave_js_server.model.controller.Controller.async_begin_inclusion",
f"{CONTROLLER_PATCH_PREFIX}.async_begin_inclusion",
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
):
await ws_client.send_json(
@ -1155,7 +1158,7 @@ async def test_provision_smart_start_node(
# Test FailedZWaveCommand is caught
with patch(
"zwave_js_server.model.controller.Controller.async_provision_smart_start_node",
f"{CONTROLLER_PATCH_PREFIX}.async_provision_smart_start_node",
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
):
await ws_client.send_json(
@ -1163,7 +1166,9 @@ async def test_provision_smart_start_node(
ID: 7,
TYPE: "zwave_js/provision_smart_start_node",
ENTRY_ID: entry.entry_id,
QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest",
QR_CODE_STRING: (
"90testtesttesttesttesttesttesttesttesttesttesttesttest"
),
}
)
msg = await ws_client.receive_json()
@ -1259,7 +1264,7 @@ async def test_unprovision_smart_start_node(
# Test FailedZWaveCommand is caught
with patch(
"zwave_js_server.model.controller.Controller.async_unprovision_smart_start_node",
f"{CONTROLLER_PATCH_PREFIX}.async_unprovision_smart_start_node",
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
):
await ws_client.send_json(
@ -1332,7 +1337,7 @@ async def test_get_provisioning_entries(
# Test FailedZWaveCommand is caught
with patch(
"zwave_js_server.model.controller.Controller.async_get_provisioning_entries",
f"{CONTROLLER_PATCH_PREFIX}.async_get_provisioning_entries",
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
):
await ws_client.send_json(
@ -1432,7 +1437,9 @@ async def test_parse_qr_code_string(
ID: 6,
TYPE: "zwave_js/parse_qr_code_string",
ENTRY_ID: entry.entry_id,
QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest",
QR_CODE_STRING: (
"90testtesttesttesttesttesttesttesttesttesttesttesttest"
),
}
)
msg = await ws_client.receive_json()
@ -1497,7 +1504,9 @@ async def test_try_parse_dsk_from_qr_code_string(
ID: 6,
TYPE: "zwave_js/try_parse_dsk_from_qr_code_string",
ENTRY_ID: entry.entry_id,
QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest",
QR_CODE_STRING: (
"90testtesttesttesttesttesttesttesttesttesttesttesttest"
),
}
)
msg = await ws_client.receive_json()
@ -1572,7 +1581,7 @@ async def test_cancel_inclusion_exclusion(
# Test FailedZWaveCommand is caught
with patch(
"zwave_js_server.model.controller.Controller.async_stop_inclusion",
f"{CONTROLLER_PATCH_PREFIX}.async_stop_inclusion",
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
):
await ws_client.send_json(
@ -1590,7 +1599,7 @@ async def test_cancel_inclusion_exclusion(
# Test FailedZWaveCommand is caught
with patch(
"zwave_js_server.model.controller.Controller.async_stop_exclusion",
f"{CONTROLLER_PATCH_PREFIX}.async_stop_exclusion",
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
):
await ws_client.send_json(
@ -1709,7 +1718,7 @@ async def test_remove_node(
# Test FailedZWaveCommand is caught
with patch(
"zwave_js_server.model.controller.Controller.async_begin_exclusion",
f"{CONTROLLER_PATCH_PREFIX}.async_begin_exclusion",
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
):
await ws_client.send_json(
@ -2054,7 +2063,7 @@ async def test_replace_failed_node(
# Test FailedZWaveCommand is caught
with patch(
"zwave_js_server.model.controller.Controller.async_replace_failed_node",
f"{CONTROLLER_PATCH_PREFIX}.async_replace_failed_node",
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
):
await ws_client.send_json(
@ -2105,7 +2114,7 @@ async def test_remove_failed_node(
# Test FailedZWaveCommand is caught
with patch(
"zwave_js_server.model.controller.Controller.async_remove_failed_node",
f"{CONTROLLER_PATCH_PREFIX}.async_remove_failed_node",
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
):
await ws_client.send_json(
@ -2200,7 +2209,7 @@ async def test_begin_healing_network(
# Test FailedZWaveCommand is caught
with patch(
"zwave_js_server.model.controller.Controller.async_begin_healing_network",
f"{CONTROLLER_PATCH_PREFIX}.async_begin_healing_network",
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
):
await ws_client.send_json(
@ -2342,7 +2351,7 @@ async def test_stop_healing_network(
# Test FailedZWaveCommand is caught
with patch(
"zwave_js_server.model.controller.Controller.async_stop_healing_network",
f"{CONTROLLER_PATCH_PREFIX}.async_stop_healing_network",
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
):
await ws_client.send_json(
@ -2403,7 +2412,7 @@ async def test_heal_node(
# Test FailedZWaveCommand is caught
with patch(
"zwave_js_server.model.controller.Controller.async_heal_node",
f"{CONTROLLER_PATCH_PREFIX}.async_heal_node",
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
):
await ws_client.send_json(
@ -3279,7 +3288,7 @@ async def test_subscribe_log_updates(
async def test_update_log_config(
hass: HomeAssistant, client, integration, hass_ws_client: WebSocketGenerator
) -> None:
"""Test that the update_log_config WS API call works and that schema validation works."""
"""Test that update_log_config WS API call and schema validation works."""
entry = integration
ws_client = await hass_ws_client(hass)
@ -3841,7 +3850,7 @@ async def test_subscribe_firmware_update_status_initial_value(
integration,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test subscribe_firmware_update_status websocket command with in progress update."""
"""Test subscribe_firmware_update_status WS command with in progress update."""
ws_client = await hass_ws_client(hass)
device = get_device(hass, multisensor_6)
@ -4160,7 +4169,7 @@ async def test_is_any_ota_firmware_update_in_progress(
# Test FailedZWaveCommand is caught
with patch(
"zwave_js_server.model.controller.Controller.async_is_any_ota_firmware_update_in_progress",
f"{CONTROLLER_PATCH_PREFIX}.async_is_any_ota_firmware_update_in_progress",
side_effect=FailedZWaveCommand("failed_command", 1, "error message"),
):
await ws_client.send_json(

View file

@ -122,7 +122,7 @@ async def test_device_diagnostics_missing_primary_value(
integration,
hass_client: ClientSessionGenerator,
) -> None:
"""Test that the device diagnostics handles an entity with a missing primary value."""
"""Test that device diagnostics handles an entity with a missing primary value."""
dev_reg = async_get_dev_reg(hass)
device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)})
assert device

View file

@ -1,6 +1,19 @@
"""Test discovery of entities for device-specific schemas for the Z-Wave JS integration."""
"""Test entity discovery for device-specific schemas for the Z-Wave JS integration."""
import pytest
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.components.number import (
ATTR_VALUE,
DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.components.zwave_js.discovery import (
FirmwareVersionRange,
ZWaveDiscoverySchema,
@ -9,6 +22,7 @@ from homeassistant.components.zwave_js.discovery import (
from homeassistant.components.zwave_js.discovery_data_template import (
DynamicCurrentTempClimateDataTemplate,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNKNOWN, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@ -141,3 +155,141 @@ async def test_merten_507801_disabled_enitites(
)
assert updated_entry != entry
assert updated_entry.disabled is False
async def test_zooz_zen72(
hass: HomeAssistant, client, switch_zooz_zen72, integration
) -> None:
"""Test that Zooz ZEN72 Indicators are discovered as number entities."""
ent_reg = er.async_get(hass)
assert len(hass.states.async_entity_ids(NUMBER_DOMAIN)) == 1
assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 2 # includes ping
entity_id = "number.z_wave_plus_700_series_dimmer_switch_indicator_value"
entry = ent_reg.async_get(entity_id)
assert entry
assert entry.entity_category == EntityCategory.CONFIG
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_UNKNOWN
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: entity_id,
ATTR_VALUE: 5,
},
blocking=True,
)
assert len(client.async_send_command.call_args_list) == 1
args = client.async_send_command.call_args[0][0]
assert args["command"] == "node.set_value"
assert args["nodeId"] == switch_zooz_zen72.node_id
assert args["valueId"] == {
"commandClass": 135,
"endpoint": 0,
"property": "value",
}
assert args["value"] == 5
client.async_send_command.reset_mock()
entity_id = "button.z_wave_plus_700_series_dimmer_switch_identify"
entry = ent_reg.async_get(entity_id)
assert entry
assert entry.entity_category == EntityCategory.CONFIG
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
assert len(client.async_send_command.call_args_list) == 1
args = client.async_send_command.call_args[0][0]
assert args["command"] == "node.set_value"
assert args["nodeId"] == switch_zooz_zen72.node_id
assert args["valueId"] == {
"commandClass": 135,
"endpoint": 0,
"property": "identify",
}
assert args["value"] is True
async def test_indicator_test(
hass: HomeAssistant, client, indicator_test, integration
) -> None:
"""Test that Indicators are discovered properly.
This test covers indicators that we don't already have device fixtures for.
"""
ent_reg = er.async_get(hass)
assert len(hass.states.async_entity_ids(NUMBER_DOMAIN)) == 0
assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 1 # only ping
assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 1
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 # include node status
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
entity_id = "binary_sensor.this_is_a_fake_device_binary_sensor"
entry = ent_reg.async_get(entity_id)
assert entry
assert entry.entity_category == EntityCategory.DIAGNOSTIC
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_OFF
client.async_send_command.reset_mock()
entity_id = "sensor.this_is_a_fake_device_sensor"
entry = ent_reg.async_get(entity_id)
assert entry
assert entry.entity_category == EntityCategory.DIAGNOSTIC
state = hass.states.get(entity_id)
assert state
assert state.state == "0.0"
client.async_send_command.reset_mock()
entity_id = "switch.this_is_a_fake_device_switch"
entry = ent_reg.async_get(entity_id)
assert entry
assert entry.entity_category == EntityCategory.CONFIG
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_OFF
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
assert len(client.async_send_command.call_args_list) == 1
args = client.async_send_command.call_args[0][0]
assert args["command"] == "node.set_value"
assert args["nodeId"] == indicator_test.node_id
assert args["valueId"] == {
"commandClass": 135,
"endpoint": 0,
"property": "Test",
"propertyKey": "Switch",
}
assert args["value"] is True
client.async_send_command.reset_mock()
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
assert len(client.async_send_command.call_args_list) == 1
args = client.async_send_command.call_args[0][0]
assert args["command"] == "node.set_value"
assert args["nodeId"] == indicator_test.node_id
assert args["valueId"] == {
"commandClass": 135,
"endpoint": 0,
"property": "Test",
"propertyKey": "Switch",
}
assert args["value"] is False

View file

@ -101,7 +101,7 @@ async def test_disabled_statistics(hass: HomeAssistant, client) -> None:
async def test_noop_statistics(hass: HomeAssistant, client) -> None:
"""Test that we don't make any statistics calls if user hasn't provided preference."""
"""Test that we don't make statistics calls if user hasn't set preference."""
entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
entry.add_to_hass(hass)
@ -1332,7 +1332,7 @@ async def test_node_model_change(
async def test_disabled_node_status_entity_on_node_replaced(
hass: HomeAssistant, zp3111_state, zp3111, client, integration
) -> None:
"""Test that when a node replacement event is received the node status sensor is removed."""
"""Test when node replacement event is received, node status sensor is removed."""
node_status_entity = "sensor.4_in_1_sensor_node_status"
state = hass.states.get(node_status_entity)
assert state

View file

@ -46,7 +46,6 @@ from .common import (
ENERGY_SENSOR,
HUMIDITY_SENSOR,
ID_LOCK_CONFIG_PARAMETER_SENSOR,
INDICATOR_SENSOR,
METER_ENERGY_SENSOR,
NOTIFICATION_MOTION_SENSOR,
POWER_SENSOR,
@ -218,18 +217,6 @@ async def test_disabled_notification_sensor(
assert state.state == STATE_UNKNOWN
async def test_disabled_indcator_sensor(
hass: HomeAssistant, climate_radio_thermostat_ct100_plus, integration
) -> None:
"""Test sensor is created from Indicator CC and is disabled."""
ent_reg = er.async_get(hass)
entity_entry = ent_reg.async_get(INDICATOR_SENSOR)
assert entity_entry
assert entity_entry.disabled
assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
async def test_config_parameter_sensor(
hass: HomeAssistant, lock_id_lock_as_id150, integration
) -> None: