Redact user codes from zwave_js diagnostics (#68515)
* Redact user codes from zwave_js diagnostics * simplify test * Remove unused logic * revert change and make all inputs to ZwaveValueID optional * revert change and make all inputs to ZwaveValueID optional * Remove unused diagnostics data from fixture and test location redaction * Add empty ZwaveValueID check * Improve coverage * Simplify post_init check * Use dataclasses.astuple for checks instead
This commit is contained in:
parent
ccd8c7d5f8
commit
8293430e25
9 changed files with 2064 additions and 44 deletions
|
@ -1,12 +1,16 @@
|
||||||
"""Provides diagnostics for Z-Wave JS."""
|
"""Provides diagnostics for Z-Wave JS."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import astuple
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from zwave_js_server.client import Client
|
from zwave_js_server.client import Client
|
||||||
|
from zwave_js_server.const import CommandClass
|
||||||
from zwave_js_server.dump import dump_msgs
|
from zwave_js_server.dump import dump_msgs
|
||||||
from zwave_js_server.model.node import Node, NodeDataType
|
from zwave_js_server.model.node import Node, NodeDataType
|
||||||
|
from zwave_js_server.model.value import ValueDataType
|
||||||
|
|
||||||
|
from homeassistant.components.diagnostics.const import REDACTED
|
||||||
from homeassistant.components.diagnostics.util import async_redact_data
|
from homeassistant.components.diagnostics.util import async_redact_data
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_URL
|
from homeassistant.const import CONF_URL
|
||||||
|
@ -17,9 +21,43 @@ from homeassistant.helpers.device_registry import DeviceEntry
|
||||||
from homeassistant.helpers.entity_registry import async_entries_for_device, async_get
|
from homeassistant.helpers.entity_registry import async_entries_for_device, async_get
|
||||||
|
|
||||||
from .const import DATA_CLIENT, DOMAIN
|
from .const import DATA_CLIENT, DOMAIN
|
||||||
from .helpers import get_home_and_node_id_from_device_entry
|
from .helpers import ZwaveValueID, get_home_and_node_id_from_device_entry
|
||||||
|
|
||||||
TO_REDACT = {"homeId", "location"}
|
KEYS_TO_REDACT = {"homeId", "location"}
|
||||||
|
|
||||||
|
VALUES_TO_REDACT = (
|
||||||
|
ZwaveValueID(property_="userCode", command_class=CommandClass.USER_CODE),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def redact_value_of_zwave_value(zwave_value: ValueDataType) -> ValueDataType:
|
||||||
|
"""Redact value of a Z-Wave value."""
|
||||||
|
for value_to_redact in VALUES_TO_REDACT:
|
||||||
|
zwave_value_id = ZwaveValueID(
|
||||||
|
property_=zwave_value["property"],
|
||||||
|
command_class=CommandClass(zwave_value["commandClass"]),
|
||||||
|
endpoint=zwave_value["endpoint"],
|
||||||
|
property_key=zwave_value.get("propertyKey"),
|
||||||
|
)
|
||||||
|
if all(
|
||||||
|
redacted_field_val is None or redacted_field_val == zwave_value_field_val
|
||||||
|
for redacted_field_val, zwave_value_field_val in zip(
|
||||||
|
astuple(value_to_redact), astuple(zwave_value_id)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return {**zwave_value, "value": REDACTED}
|
||||||
|
return zwave_value
|
||||||
|
|
||||||
|
|
||||||
|
def redact_node_state(node_state: NodeDataType) -> NodeDataType:
|
||||||
|
"""Redact node state."""
|
||||||
|
return {
|
||||||
|
**node_state,
|
||||||
|
"values": [
|
||||||
|
redact_value_of_zwave_value(zwave_value)
|
||||||
|
for zwave_value in node_state["values"]
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_device_entities(
|
def get_device_entities(
|
||||||
|
@ -79,10 +117,16 @@ async def async_get_config_entry_diagnostics(
|
||||||
hass: HomeAssistant, config_entry: ConfigEntry
|
hass: HomeAssistant, config_entry: ConfigEntry
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""Return diagnostics for a config entry."""
|
"""Return diagnostics for a config entry."""
|
||||||
msgs: list[dict] = await dump_msgs(
|
msgs: list[dict] = async_redact_data(
|
||||||
config_entry.data[CONF_URL], async_get_clientsession(hass)
|
await dump_msgs(config_entry.data[CONF_URL], async_get_clientsession(hass)),
|
||||||
|
KEYS_TO_REDACT,
|
||||||
)
|
)
|
||||||
return async_redact_data(msgs, TO_REDACT)
|
handshake_msgs = msgs[:-1]
|
||||||
|
network_state = msgs[-1]
|
||||||
|
network_state["result"]["state"]["nodes"] = [
|
||||||
|
redact_node_state(node) for node in network_state["result"]["state"]["nodes"]
|
||||||
|
]
|
||||||
|
return [*handshake_msgs, network_state]
|
||||||
|
|
||||||
|
|
||||||
async def async_get_device_diagnostics(
|
async def async_get_device_diagnostics(
|
||||||
|
@ -104,5 +148,5 @@ async def async_get_device_diagnostics(
|
||||||
"maxSchemaVersion": client.version.max_schema_version,
|
"maxSchemaVersion": client.version.max_schema_version,
|
||||||
},
|
},
|
||||||
"entities": entities,
|
"entities": entities,
|
||||||
"state": async_redact_data(node.data, TO_REDACT),
|
"state": redact_node_state(async_redact_data(node.data, KEYS_TO_REDACT)),
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,8 +55,8 @@ from .discovery_data_template import (
|
||||||
FanValueMapping,
|
FanValueMapping,
|
||||||
FixedFanValueMappingDataTemplate,
|
FixedFanValueMappingDataTemplate,
|
||||||
NumericSensorDataTemplate,
|
NumericSensorDataTemplate,
|
||||||
ZwaveValueID,
|
|
||||||
)
|
)
|
||||||
|
from .helpers import ZwaveValueID
|
||||||
|
|
||||||
|
|
||||||
class DataclassMustHaveAtLeastOne:
|
class DataclassMustHaveAtLeastOne:
|
||||||
|
@ -307,7 +307,7 @@ DISCOVERY_SCHEMAS = [
|
||||||
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
||||||
data_template=ConfigurableFanValueMappingDataTemplate(
|
data_template=ConfigurableFanValueMappingDataTemplate(
|
||||||
configuration_option=ZwaveValueID(
|
configuration_option=ZwaveValueID(
|
||||||
5, CommandClass.CONFIGURATION, endpoint=0
|
property_=5, command_class=CommandClass.CONFIGURATION, endpoint=0
|
||||||
),
|
),
|
||||||
configuration_value_to_fan_value_mapping={
|
configuration_value_to_fan_value_mapping={
|
||||||
0: FanValueMapping(speeds=[(1, 33), (34, 66), (67, 99)]),
|
0: FanValueMapping(speeds=[(1, 33), (34, 66), (67, 99)]),
|
||||||
|
@ -325,8 +325,8 @@ DISCOVERY_SCHEMAS = [
|
||||||
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
||||||
data_template=CoverTiltDataTemplate(
|
data_template=CoverTiltDataTemplate(
|
||||||
tilt_value_id=ZwaveValueID(
|
tilt_value_id=ZwaveValueID(
|
||||||
"fibaro",
|
property_="fibaro",
|
||||||
CommandClass.MANUFACTURER_PROPRIETARY,
|
command_class=CommandClass.MANUFACTURER_PROPRIETARY,
|
||||||
endpoint=0,
|
endpoint=0,
|
||||||
property_key="venetianBlindsTilt",
|
property_key="venetianBlindsTilt",
|
||||||
)
|
)
|
||||||
|
@ -391,34 +391,36 @@ DISCOVERY_SCHEMAS = [
|
||||||
lookup_table={
|
lookup_table={
|
||||||
# Internal Sensor
|
# Internal Sensor
|
||||||
"A": ZwaveValueID(
|
"A": ZwaveValueID(
|
||||||
THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
property_=THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
||||||
CommandClass.SENSOR_MULTILEVEL,
|
command_class=CommandClass.SENSOR_MULTILEVEL,
|
||||||
endpoint=2,
|
endpoint=2,
|
||||||
),
|
),
|
||||||
"AF": ZwaveValueID(
|
"AF": ZwaveValueID(
|
||||||
THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
property_=THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
||||||
CommandClass.SENSOR_MULTILEVEL,
|
command_class=CommandClass.SENSOR_MULTILEVEL,
|
||||||
endpoint=2,
|
endpoint=2,
|
||||||
),
|
),
|
||||||
# External Sensor
|
# External Sensor
|
||||||
"A2": ZwaveValueID(
|
"A2": ZwaveValueID(
|
||||||
THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
property_=THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
||||||
CommandClass.SENSOR_MULTILEVEL,
|
command_class=CommandClass.SENSOR_MULTILEVEL,
|
||||||
endpoint=3,
|
endpoint=3,
|
||||||
),
|
),
|
||||||
"A2F": ZwaveValueID(
|
"A2F": ZwaveValueID(
|
||||||
THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
property_=THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
||||||
CommandClass.SENSOR_MULTILEVEL,
|
command_class=CommandClass.SENSOR_MULTILEVEL,
|
||||||
endpoint=3,
|
endpoint=3,
|
||||||
),
|
),
|
||||||
# Floor sensor
|
# Floor sensor
|
||||||
"F": ZwaveValueID(
|
"F": ZwaveValueID(
|
||||||
THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
property_=THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
||||||
CommandClass.SENSOR_MULTILEVEL,
|
command_class=CommandClass.SENSOR_MULTILEVEL,
|
||||||
endpoint=4,
|
endpoint=4,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
dependent_value=ZwaveValueID(2, CommandClass.CONFIGURATION, endpoint=0),
|
dependent_value=ZwaveValueID(
|
||||||
|
property_=2, command_class=CommandClass.CONFIGURATION, endpoint=0
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
# Heatit Z-TRM2fx
|
# Heatit Z-TRM2fx
|
||||||
|
@ -438,23 +440,25 @@ DISCOVERY_SCHEMAS = [
|
||||||
lookup_table={
|
lookup_table={
|
||||||
# External Sensor
|
# External Sensor
|
||||||
"A2": ZwaveValueID(
|
"A2": ZwaveValueID(
|
||||||
THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
property_=THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
||||||
CommandClass.SENSOR_MULTILEVEL,
|
command_class=CommandClass.SENSOR_MULTILEVEL,
|
||||||
endpoint=2,
|
endpoint=2,
|
||||||
),
|
),
|
||||||
"A2F": ZwaveValueID(
|
"A2F": ZwaveValueID(
|
||||||
THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
property_=THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
||||||
CommandClass.SENSOR_MULTILEVEL,
|
command_class=CommandClass.SENSOR_MULTILEVEL,
|
||||||
endpoint=2,
|
endpoint=2,
|
||||||
),
|
),
|
||||||
# Floor sensor
|
# Floor sensor
|
||||||
"F": ZwaveValueID(
|
"F": ZwaveValueID(
|
||||||
THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
property_=THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
||||||
CommandClass.SENSOR_MULTILEVEL,
|
command_class=CommandClass.SENSOR_MULTILEVEL,
|
||||||
endpoint=3,
|
endpoint=3,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
dependent_value=ZwaveValueID(2, CommandClass.CONFIGURATION, endpoint=0),
|
dependent_value=ZwaveValueID(
|
||||||
|
property_=2, command_class=CommandClass.CONFIGURATION, endpoint=0
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
# FortrezZ SSA1/SSA2/SSA3
|
# FortrezZ SSA1/SSA2/SSA3
|
||||||
|
|
|
@ -148,6 +148,7 @@ from .const import (
|
||||||
ENTITY_DESC_KEY_TOTAL_INCREASING,
|
ENTITY_DESC_KEY_TOTAL_INCREASING,
|
||||||
ENTITY_DESC_KEY_VOLTAGE,
|
ENTITY_DESC_KEY_VOLTAGE,
|
||||||
)
|
)
|
||||||
|
from .helpers import ZwaveValueID
|
||||||
|
|
||||||
METER_DEVICE_CLASS_MAP: dict[str, set[MeterScaleType]] = {
|
METER_DEVICE_CLASS_MAP: dict[str, set[MeterScaleType]] = {
|
||||||
ENTITY_DESC_KEY_CURRENT: CURRENT_METER_TYPES,
|
ENTITY_DESC_KEY_CURRENT: CURRENT_METER_TYPES,
|
||||||
|
@ -226,16 +227,6 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = {
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ZwaveValueID:
|
|
||||||
"""Class to represent a value ID."""
|
|
||||||
|
|
||||||
property_: str | int
|
|
||||||
command_class: int
|
|
||||||
endpoint: int | None = None
|
|
||||||
property_key: str | int | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BaseDiscoverySchemaDataTemplate:
|
class BaseDiscoverySchemaDataTemplate:
|
||||||
"""Base class for discovery schema data templates."""
|
"""Base class for discovery schema data templates."""
|
||||||
|
@ -486,7 +477,7 @@ class ConfigurableFanValueMappingDataTemplate(
|
||||||
...
|
...
|
||||||
data_template=ConfigurableFanValueMappingDataTemplate(
|
data_template=ConfigurableFanValueMappingDataTemplate(
|
||||||
configuration_option=ZwaveValueID(
|
configuration_option=ZwaveValueID(
|
||||||
5, CommandClass.CONFIGURATION, endpoint=0
|
property_=5, command_class=CommandClass.CONFIGURATION, endpoint=0
|
||||||
),
|
),
|
||||||
configuration_value_to_fan_value_mapping={
|
configuration_value_to_fan_value_mapping={
|
||||||
0: FanValueMapping(speeds=[(1,33), (34,66), (67,99)]),
|
0: FanValueMapping(speeds=[(1,33), (34,66), (67,99)]),
|
||||||
|
|
|
@ -223,7 +223,7 @@ class ZWaveBaseEntity(Entity):
|
||||||
value_property: str | int,
|
value_property: str | int,
|
||||||
command_class: int | None = None,
|
command_class: int | None = None,
|
||||||
endpoint: int | None = None,
|
endpoint: int | None = None,
|
||||||
value_property_key: int | None = None,
|
value_property_key: int | str | None = None,
|
||||||
add_to_watched_value_ids: bool = True,
|
add_to_watched_value_ids: bool = True,
|
||||||
check_all_endpoints: bool = False,
|
check_all_endpoints: bool = False,
|
||||||
) -> ZwaveValue | None:
|
) -> ZwaveValue | None:
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from dataclasses import astuple, dataclass
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -41,6 +42,21 @@ from .const import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ZwaveValueID:
|
||||||
|
"""Class to represent a value ID."""
|
||||||
|
|
||||||
|
property_: str | int | None = None
|
||||||
|
command_class: int | None = None
|
||||||
|
endpoint: int | None = None
|
||||||
|
property_key: str | int | None = None
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
"""Post initialization check."""
|
||||||
|
if all(val is None for val in astuple(self)):
|
||||||
|
raise ValueError("At least one of the fields must be set.")
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def get_value_of_zwave_value(value: ZwaveValue | None) -> Any | None:
|
def get_value_of_zwave_value(value: ZwaveValue | None) -> Any | None:
|
||||||
"""Return the value of a ZwaveValue."""
|
"""Return the value of a ZwaveValue."""
|
||||||
|
|
|
@ -210,6 +210,12 @@ def log_config_state_fixture():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="config_entry_diagnostics", scope="session")
|
||||||
|
def config_entry_diagnostics_fixture():
|
||||||
|
"""Load the config entry diagnostics fixture data."""
|
||||||
|
return json.loads(load_fixture("zwave_js/config_entry_diagnostics.json"))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="multisensor_6_state", scope="session")
|
@pytest.fixture(name="multisensor_6_state", scope="session")
|
||||||
def multisensor_6_state_fixture():
|
def multisensor_6_state_fixture():
|
||||||
"""Load the multisensor 6 node state fixture data."""
|
"""Load the multisensor 6 node state fixture data."""
|
||||||
|
|
1936
tests/components/zwave_js/fixtures/config_entry_diagnostics.json
Normal file
1936
tests/components/zwave_js/fixtures/config_entry_diagnostics.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -4,6 +4,7 @@ from unittest.mock import patch
|
||||||
import pytest
|
import pytest
|
||||||
from zwave_js_server.event import Event
|
from zwave_js_server.event import Event
|
||||||
|
|
||||||
|
from homeassistant.components.diagnostics.const import REDACTED
|
||||||
from homeassistant.components.zwave_js.diagnostics import async_get_device_diagnostics
|
from homeassistant.components.zwave_js.diagnostics import async_get_device_diagnostics
|
||||||
from homeassistant.components.zwave_js.discovery import async_discover_node_values
|
from homeassistant.components.zwave_js.discovery import async_discover_node_values
|
||||||
from homeassistant.components.zwave_js.helpers import get_device_id
|
from homeassistant.components.zwave_js.helpers import get_device_id
|
||||||
|
@ -17,15 +18,27 @@ from tests.components.diagnostics import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_config_entry_diagnostics(hass, hass_client, integration):
|
async def test_config_entry_diagnostics(
|
||||||
|
hass, hass_client, integration, config_entry_diagnostics
|
||||||
|
):
|
||||||
"""Test the config entry level diagnostics data dump."""
|
"""Test the config entry level diagnostics data dump."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.zwave_js.diagnostics.dump_msgs",
|
"homeassistant.components.zwave_js.diagnostics.dump_msgs",
|
||||||
return_value=[{"hello": "world"}, {"second": "msg"}],
|
return_value=config_entry_diagnostics,
|
||||||
):
|
):
|
||||||
assert await get_diagnostics_for_config_entry(
|
diagnostics = await get_diagnostics_for_config_entry(
|
||||||
hass, hass_client, integration
|
hass, hass_client, integration
|
||||||
) == [{"hello": "world"}, {"second": "msg"}]
|
)
|
||||||
|
assert len(diagnostics) == 3
|
||||||
|
assert diagnostics[0]["homeId"] == REDACTED
|
||||||
|
nodes = diagnostics[2]["result"]["state"]["nodes"]
|
||||||
|
for node in nodes:
|
||||||
|
assert "location" not in node or node["location"] == REDACTED
|
||||||
|
for value in node["values"]:
|
||||||
|
if value["commandClass"] == 99 and value["property"] == "userCode":
|
||||||
|
assert value["value"] == REDACTED
|
||||||
|
else:
|
||||||
|
assert value.get("value") != REDACTED
|
||||||
|
|
||||||
|
|
||||||
async def test_device_diagnostics(
|
async def test_device_diagnostics(
|
||||||
|
|
10
tests/components/zwave_js/test_helpers.py
Normal file
10
tests/components/zwave_js/test_helpers.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
"""Test Z-Wave JS helpers module."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.zwave_js.helpers import ZwaveValueID
|
||||||
|
|
||||||
|
|
||||||
|
async def test_empty_zwave_value_id():
|
||||||
|
"""Test empty ZwaveValueID is invalid."""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ZwaveValueID()
|
Loading…
Add table
Add a link
Reference in a new issue