Add support for selective config parameter entity discovery (#48336)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Raman Gupta 2021-03-27 13:14:06 -04:00 committed by GitHub
parent 56abe25c1f
commit fbc3f97097
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 3065 additions and 3 deletions

View file

@ -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

View file

@ -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

View file

@ -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}

View file

@ -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"
)

View file

@ -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

View file

@ -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

File diff suppressed because it is too large Load diff