Add support for selective config parameter entity discovery (#48336)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
56abe25c1f
commit
fbc3f97097
7 changed files with 3065 additions and 3 deletions
|
@ -2,7 +2,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Generator
|
from typing import Any, Generator
|
||||||
|
|
||||||
from zwave_js_server.const import CommandClass
|
from zwave_js_server.const import CommandClass
|
||||||
from zwave_js_server.model.device_class import DeviceClassItem
|
from zwave_js_server.model.device_class import DeviceClassItem
|
||||||
|
@ -41,6 +41,12 @@ class ZWaveValueDiscoverySchema:
|
||||||
endpoint: set[int] | None = None
|
endpoint: set[int] | None = None
|
||||||
# [optional] the value's property must match ANY of these values
|
# [optional] the value's property must match ANY of these values
|
||||||
property: set[str | int] | None = None
|
property: set[str | int] | None = None
|
||||||
|
# [optional] the value's property name must match ANY of these values
|
||||||
|
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
|
||||||
|
# [optional] the value's property key name must match ANY of these values
|
||||||
|
property_key_name: set[str] | 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
|
||||||
|
|
||||||
|
@ -82,6 +88,34 @@ class ZWaveDiscoverySchema:
|
||||||
allow_multi: bool = False
|
allow_multi: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_parameter_discovery_schema(
|
||||||
|
property_: set[str | int] | None = None,
|
||||||
|
property_name: set[str] | None = None,
|
||||||
|
property_key: set[str | int] | None = None,
|
||||||
|
property_key_name: set[str] | None = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> ZWaveDiscoverySchema:
|
||||||
|
"""
|
||||||
|
Return a discovery schema for a config parameter.
|
||||||
|
|
||||||
|
Supports all keyword arguments to ZWaveValueDiscoverySchema except platform, hint,
|
||||||
|
and primary_value.
|
||||||
|
"""
|
||||||
|
return ZWaveDiscoverySchema(
|
||||||
|
platform="sensor",
|
||||||
|
hint="config_parameter",
|
||||||
|
primary_value=ZWaveValueDiscoverySchema(
|
||||||
|
command_class={CommandClass.CONFIGURATION},
|
||||||
|
property=property_,
|
||||||
|
property_name=property_name,
|
||||||
|
property_key=property_key,
|
||||||
|
property_key_name=property_key_name,
|
||||||
|
type={"number"},
|
||||||
|
),
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema(
|
SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema(
|
||||||
command_class={CommandClass.SWITCH_MULTILEVEL},
|
command_class={CommandClass.SWITCH_MULTILEVEL},
|
||||||
property={"currentValue"},
|
property={"currentValue"},
|
||||||
|
@ -162,6 +196,19 @@ DISCOVERY_SCHEMAS = [
|
||||||
product_type={0x0003},
|
product_type={0x0003},
|
||||||
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
||||||
),
|
),
|
||||||
|
# ====== START OF CONFIG PARAMETER SPECIFIC MAPPING SCHEMAS =======
|
||||||
|
# Door lock mode config parameter. Functionality equivalent to Notification CC
|
||||||
|
# list sensors.
|
||||||
|
get_config_parameter_discovery_schema(
|
||||||
|
property_name={"Door lock mode"},
|
||||||
|
device_class_generic={"Entry Control"},
|
||||||
|
device_class_specific={
|
||||||
|
"Door Lock",
|
||||||
|
"Advanced Door Lock",
|
||||||
|
"Secure Keypad Door Lock",
|
||||||
|
"Secure Lockbox",
|
||||||
|
},
|
||||||
|
),
|
||||||
# ====== START OF GENERIC MAPPING SCHEMAS =======
|
# ====== START OF GENERIC MAPPING SCHEMAS =======
|
||||||
# locks
|
# locks
|
||||||
ZWaveDiscoverySchema(
|
ZWaveDiscoverySchema(
|
||||||
|
@ -489,6 +536,24 @@ def check_value(value: ZwaveValue, schema: ZWaveValueDiscoverySchema) -> bool:
|
||||||
# check property
|
# check property
|
||||||
if schema.property is not None and value.property_ not in schema.property:
|
if schema.property is not None and value.property_ not in schema.property:
|
||||||
return False
|
return False
|
||||||
|
# check property_name
|
||||||
|
if (
|
||||||
|
schema.property_name is not None
|
||||||
|
and value.property_name not in schema.property_name
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
# check property_key
|
||||||
|
if (
|
||||||
|
schema.property_key is not None
|
||||||
|
and value.property_key not in schema.property_key
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
# check property_key_name
|
||||||
|
if (
|
||||||
|
schema.property_key_name is not None
|
||||||
|
and value.property_key_name not in schema.property_key_name
|
||||||
|
):
|
||||||
|
return False
|
||||||
# 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
|
||||||
|
|
|
@ -99,6 +99,7 @@ class ZWaveBaseEntity(Entity):
|
||||||
include_value_name: bool = False,
|
include_value_name: bool = False,
|
||||||
alternate_value_name: str | None = None,
|
alternate_value_name: str | None = None,
|
||||||
additional_info: list[str] | None = None,
|
additional_info: list[str] | None = None,
|
||||||
|
name_suffix: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Generate entity name."""
|
"""Generate entity name."""
|
||||||
if additional_info is None:
|
if additional_info is None:
|
||||||
|
@ -108,6 +109,8 @@ class ZWaveBaseEntity(Entity):
|
||||||
or self.info.node.device_config.description
|
or self.info.node.device_config.description
|
||||||
or f"Node {self.info.node.node_id}"
|
or f"Node {self.info.node.node_id}"
|
||||||
)
|
)
|
||||||
|
if name_suffix:
|
||||||
|
name = f"{name} {name_suffix}"
|
||||||
if include_value_name:
|
if include_value_name:
|
||||||
value_name = (
|
value_name = (
|
||||||
alternate_value_name
|
alternate_value_name
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Callable
|
from typing import Callable, cast
|
||||||
|
|
||||||
from zwave_js_server.client import Client as ZwaveClient
|
from zwave_js_server.client import Client as ZwaveClient
|
||||||
from zwave_js_server.const import CommandClass
|
from zwave_js_server.const import CommandClass, ConfigurationValueType
|
||||||
|
from zwave_js_server.model.value import ConfigurationValue
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
DEVICE_CLASS_BATTERY,
|
DEVICE_CLASS_BATTERY,
|
||||||
|
@ -49,6 +50,8 @@ async def async_setup_entry(
|
||||||
entities.append(ZWaveNumericSensor(config_entry, client, info))
|
entities.append(ZWaveNumericSensor(config_entry, client, info))
|
||||||
elif info.platform_hint == "list_sensor":
|
elif info.platform_hint == "list_sensor":
|
||||||
entities.append(ZWaveListSensor(config_entry, client, info))
|
entities.append(ZWaveListSensor(config_entry, client, info))
|
||||||
|
elif info.platform_hint == "config_parameter":
|
||||||
|
entities.append(ZWaveConfigParameterSensor(config_entry, client, info))
|
||||||
else:
|
else:
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"Sensor not implemented for %s/%s",
|
"Sensor not implemented for %s/%s",
|
||||||
|
@ -118,6 +121,7 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity):
|
||||||
# We hide some of the more advanced sensors by default to not overwhelm users
|
# We hide some of the more advanced sensors by default to not overwhelm users
|
||||||
if self.info.primary_value.command_class in [
|
if self.info.primary_value.command_class in [
|
||||||
CommandClass.BASIC,
|
CommandClass.BASIC,
|
||||||
|
CommandClass.CONFIGURATION,
|
||||||
CommandClass.INDICATOR,
|
CommandClass.INDICATOR,
|
||||||
CommandClass.NOTIFICATION,
|
CommandClass.NOTIFICATION,
|
||||||
]:
|
]:
|
||||||
|
@ -221,3 +225,48 @@ class ZWaveListSensor(ZwaveSensorBase):
|
||||||
"""Return the device specific state attributes."""
|
"""Return the device specific state attributes."""
|
||||||
# add the value's int value as property for multi-value (list) items
|
# add the value's int value as property for multi-value (list) items
|
||||||
return {"value": self.info.primary_value.value}
|
return {"value": self.info.primary_value.value}
|
||||||
|
|
||||||
|
|
||||||
|
class ZWaveConfigParameterSensor(ZwaveSensorBase):
|
||||||
|
"""Representation of a Z-Wave config parameter sensor."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
client: ZwaveClient,
|
||||||
|
info: ZwaveDiscoveryInfo,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize a ZWaveConfigParameterSensor entity."""
|
||||||
|
super().__init__(config_entry, client, info)
|
||||||
|
self._name = self.generate_name(
|
||||||
|
include_value_name=True,
|
||||||
|
alternate_value_name=self.info.primary_value.property_name,
|
||||||
|
additional_info=[self.info.primary_value.property_key_name],
|
||||||
|
name_suffix="Config Parameter",
|
||||||
|
)
|
||||||
|
self._primary_value = cast(ConfigurationValue, self.info.primary_value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> str | None:
|
||||||
|
"""Return state of the sensor."""
|
||||||
|
if self.info.primary_value.value is None:
|
||||||
|
return None
|
||||||
|
if (
|
||||||
|
self._primary_value.configuration_value_type == ConfigurationValueType.RANGE
|
||||||
|
or (
|
||||||
|
not str(self.info.primary_value.value)
|
||||||
|
in self.info.primary_value.metadata.states
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return str(self.info.primary_value.value)
|
||||||
|
return str(
|
||||||
|
self.info.primary_value.metadata.states[str(self.info.primary_value.value)]
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_state_attributes(self) -> dict[str, str] | None:
|
||||||
|
"""Return the device specific state attributes."""
|
||||||
|
if self._primary_value.configuration_value_type == ConfigurationValueType.RANGE:
|
||||||
|
return None
|
||||||
|
# add the value's int value as property for multi-value (list) items
|
||||||
|
return {"value": self.info.primary_value.value}
|
||||||
|
|
|
@ -22,3 +22,6 @@ CLIMATE_MAIN_HEAT_ACTIONNER = "climate.main_heat_actionner"
|
||||||
BULB_6_MULTI_COLOR_LIGHT_ENTITY = "light.bulb_6_multi_color"
|
BULB_6_MULTI_COLOR_LIGHT_ENTITY = "light.bulb_6_multi_color"
|
||||||
EATON_RF9640_ENTITY = "light.allloaddimmer"
|
EATON_RF9640_ENTITY = "light.allloaddimmer"
|
||||||
AEON_SMART_SWITCH_LIGHT_ENTITY = "light.smart_switch_6"
|
AEON_SMART_SWITCH_LIGHT_ENTITY = "light.smart_switch_6"
|
||||||
|
ID_LOCK_CONFIG_PARAMETER_SENSOR = (
|
||||||
|
"sensor.z_wave_module_for_id_lock_150_and_101_config_parameter_door_lock_mode"
|
||||||
|
)
|
||||||
|
|
|
@ -306,6 +306,12 @@ def null_name_check_state_fixture():
|
||||||
return json.loads(load_fixture("zwave_js/null_name_check_state.json"))
|
return json.loads(load_fixture("zwave_js/null_name_check_state.json"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="lock_id_lock_as_id150_state", scope="session")
|
||||||
|
def lock_id_lock_as_id150_state_fixture():
|
||||||
|
"""Load the id lock id-150 lock node state fixture data."""
|
||||||
|
return json.loads(load_fixture("zwave_js/lock_id_lock_as_id150_state.json"))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="client")
|
@pytest.fixture(name="client")
|
||||||
def mock_client_fixture(controller_state, version_state):
|
def mock_client_fixture(controller_state, version_state):
|
||||||
"""Mock a client."""
|
"""Mock a client."""
|
||||||
|
@ -568,3 +574,11 @@ def inovelli_lzw36_fixture(client, inovelli_lzw36_state):
|
||||||
node = Node(client, copy.deepcopy(inovelli_lzw36_state))
|
node = Node(client, copy.deepcopy(inovelli_lzw36_state))
|
||||||
client.driver.controller.nodes[node.node_id] = node
|
client.driver.controller.nodes[node.node_id] = node
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="lock_id_lock_as_id150")
|
||||||
|
def lock_id_lock_as_id150(client, lock_id_lock_as_id150_state):
|
||||||
|
"""Mock an id lock id-150 lock node."""
|
||||||
|
node = Node(client, copy.deepcopy(lock_id_lock_as_id150_state))
|
||||||
|
client.driver.controller.nodes[node.node_id] = node
|
||||||
|
return node
|
||||||
|
|
|
@ -14,6 +14,7 @@ from .common import (
|
||||||
AIR_TEMPERATURE_SENSOR,
|
AIR_TEMPERATURE_SENSOR,
|
||||||
ENERGY_SENSOR,
|
ENERGY_SENSOR,
|
||||||
HUMIDITY_SENSOR,
|
HUMIDITY_SENSOR,
|
||||||
|
ID_LOCK_CONFIG_PARAMETER_SENSOR,
|
||||||
NOTIFICATION_MOTION_SENSOR,
|
NOTIFICATION_MOTION_SENSOR,
|
||||||
POWER_SENSOR,
|
POWER_SENSOR,
|
||||||
)
|
)
|
||||||
|
@ -76,3 +77,11 @@ async def test_disabled_notification_sensor(hass, multisensor_6, integration):
|
||||||
state = hass.states.get(NOTIFICATION_MOTION_SENSOR)
|
state = hass.states.get(NOTIFICATION_MOTION_SENSOR)
|
||||||
assert state.state == "Motion detection"
|
assert state.state == "Motion detection"
|
||||||
assert state.attributes["value"] == 8
|
assert state.attributes["value"] == 8
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_parameter_sensor(hass, lock_id_lock_as_id150, integration):
|
||||||
|
"""Test config parameter sensor is created."""
|
||||||
|
ent_reg = er.async_get(hass)
|
||||||
|
entity_entry = ent_reg.async_get(ID_LOCK_CONFIG_PARAMETER_SENSOR)
|
||||||
|
assert entity_entry
|
||||||
|
assert entity_entry.disabled
|
||||||
|
|
2919
tests/fixtures/zwave_js/lock_id_lock_as_id150_state.json
vendored
Normal file
2919
tests/fixtures/zwave_js/lock_id_lock_as_id150_state.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue