Add script to convert zwave_js device diagnostics to fixture (#102799)

This commit is contained in:
Raman Gupta 2023-10-25 16:07:22 -04:00 committed by GitHub
parent f28c9221e6
commit 13378b4ae2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 3832 additions and 1 deletions

View file

@ -10,7 +10,20 @@ The Z-Wave integration uses a discovery mechanism to create the necessary entiti
In cases where an entity's functionality requires interaction with multiple Values, the discovery rule for that particular entity type is based on the primary Value, or the Value that must be there to indicate that this entity needs to be created, and then the rest of the Values required are discovered by the class instance for that entity. A good example of this is the discovery logic for the `climate` entity. Currently, the discovery logic is tied to the discovery of a Value with a property of `mode` and a command class of `Thermostat Mode`, but the actual entity uses many more Values than that to be fully functional as evident in the [code](./climate.py). In cases where an entity's functionality requires interaction with multiple Values, the discovery rule for that particular entity type is based on the primary Value, or the Value that must be there to indicate that this entity needs to be created, and then the rest of the Values required are discovered by the class instance for that entity. A good example of this is the discovery logic for the `climate` entity. Currently, the discovery logic is tied to the discovery of a Value with a property of `mode` and a command class of `Thermostat Mode`, but the actual entity uses many more Values than that to be fully functional as evident in the [code](./climate.py).
There are several ways that device support can be improved within Home Assistant, but regardless of the reason, it is important to add device specific tests in these use cases. To do so, add the device's data (from device diagnostics) to the [fixtures folder](../../../tests/components/zwave_js/fixtures) and then define the new fixtures in [conftest.py](../../../tests/components/zwave_js/conftest.py). Use existing tests as the model but the tests can go in the [test_discovery.py module](../../../tests/components/zwave_js/test_discovery.py). There are several ways that device support can be improved within Home Assistant, but regardless of the reason, it is important to add device specific tests in these use cases. To do so, add the device's data to the [fixtures folder](../../../tests/components/zwave_js/fixtures) and then define the new fixtures in [conftest.py](../../../tests/components/zwave_js/conftest.py). Use existing tests as the model but the tests can go in the [test_discovery.py module](../../../tests/components/zwave_js/test_discovery.py). To learn how to generate fixtures, see the following section.
### Generating device fixtures
To generate a device fixture, download a diagnostics dump of the device from your Home Assistant instance. The dumped data will need to be modified to match the expected format. You can always do this transformation by hand, but the integration provides a [helper script](scripts/convert_device_diagnostics_to_fixture.py) that will generate the appropriate fixture data from a device diagnostics dump for you. To use it, run the script with the path to the diagnostics dump you downloaded:
`python homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py <path/to/diagnostics/dump>`
The script will print the fixture data to standard output, and you can use Unix piping to create a file from the fixture data:
`python homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py <path/to/diagnostics/dump> > <path_to_fixture_output>`
You can alternatively pass the `--file` flag to the script and it will create the file for you in the [fixtures folder](../../../tests/components/zwave_js/fixtures):
`python homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py <path/to/diagnostics/dump> --file`
### Switching HA support for a device from one entity type to another. ### Switching HA support for a device from one entity type to another.

View file

@ -0,0 +1 @@
"""Scripts module for Z-Wave JS."""

View file

@ -0,0 +1,91 @@
"""Script to convert a device diagnostics file to a fixture."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
from homeassistant.util import slugify
def get_arguments() -> argparse.Namespace:
"""Get parsed passed in arguments."""
parser = argparse.ArgumentParser(description="Z-Wave JS Fixture generator")
parser.add_argument(
"diagnostics_file", type=Path, help="Device diagnostics file to convert"
)
parser.add_argument(
"--file",
action="store_true",
help=(
"Dump fixture to file in fixtures folder. By default, the fixture will be "
"printed to standard output."
),
)
arguments = parser.parse_args()
return arguments
def get_fixtures_dir_path(data: dict) -> Path:
"""Get path to fixtures directory."""
device_config = data["deviceConfig"]
filename = slugify(
f"{device_config['manufacturer']}-{device_config['label']}_state"
)
path = Path(__file__).parents[1]
index = path.parts.index("homeassistant")
return Path(
*path.parts[:index],
"tests",
*path.parts[index + 1 :],
"fixtures",
f"{filename}.json",
)
def load_file(path: Path) -> Any:
"""Load file from path."""
return json.loads(path.read_text("utf8"))
def extract_fixture_data(diagnostics_data: Any) -> dict:
"""Extract fixture data from file."""
if (
not isinstance(diagnostics_data, dict)
or "data" not in diagnostics_data
or "state" not in diagnostics_data["data"]
):
raise ValueError("Invalid diagnostics file format")
state: dict = diagnostics_data["data"]["state"]
if isinstance(state["values"], list):
return state
values_dict: dict[str, dict] = state.pop("values")
state["values"] = list(values_dict.values())
return state
def create_fixture_file(path: Path, state_text: str) -> None:
"""Create a file for the state dump in the fixtures directory."""
path.write_text(state_text, "utf8")
def main() -> None:
"""Run the main script."""
args = get_arguments()
diagnostics_path: Path = args.diagnostics_file
diagnostics = load_file(diagnostics_path)
fixture_data = extract_fixture_data(diagnostics)
fixture_text = json.dumps(fixture_data, indent=2)
if args.file:
fixture_path = get_fixtures_dir_path(fixture_data)
create_fixture_file(fixture_path, fixture_text)
return
print(fixture_text) # noqa: T201
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
"""Tests for zwave_js scripts."""

View file

@ -0,0 +1,80 @@
"""Test convert_device_diagnostics_to_fixture script."""
import copy
import json
from pathlib import Path
import sys
from unittest.mock import patch
import pytest
from homeassistant.components.zwave_js.scripts.convert_device_diagnostics_to_fixture import (
extract_fixture_data,
get_fixtures_dir_path,
load_file,
main,
)
from tests.common import load_fixture
def _minify(text: str) -> str:
"""Minify string by removing whitespace and new lines."""
return text.replace(" ", "").replace("\n", "")
def test_fixture_functions() -> None:
"""Test functions related to the fixture."""
diagnostics_data = json.loads(load_fixture("zwave_js/device_diagnostics.json"))
state = extract_fixture_data(copy.deepcopy(diagnostics_data))
assert isinstance(state["values"], list)
assert (
get_fixtures_dir_path(state)
== Path(__file__).parents[1] / "fixtures" / "zooz_zse44_state.json"
)
old_diagnostics_format_data = copy.deepcopy(diagnostics_data)
old_diagnostics_format_data["data"]["state"]["values"] = list(
old_diagnostics_format_data["data"]["state"]["values"].values()
)
assert (
extract_fixture_data(old_diagnostics_format_data)
== old_diagnostics_format_data["data"]["state"]
)
with pytest.raises(ValueError):
extract_fixture_data({})
def test_load_file() -> None:
"""Test load file."""
assert load_file(
Path(__file__).parents[1] / "fixtures" / "device_diagnostics.json"
) == json.loads(load_fixture("zwave_js/device_diagnostics.json"))
def test_main(capfd: pytest.CaptureFixture[str]) -> None:
"""Test main function."""
Path(__file__).parents[1] / "fixtures" / "zooz_zse44_state.json"
fixture_str = load_fixture("zwave_js/zooz_zse44_state.json")
fixture_dict = json.loads(fixture_str)
# Test dump to stdout
args = [
"homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py",
str(Path(__file__).parents[1] / "fixtures" / "device_diagnostics.json"),
]
with patch.object(sys, "argv", args):
main()
captured = capfd.readouterr()
assert _minify(captured.out) == _minify(fixture_str)
# Check file dump
args.append("--file")
with patch.object(sys, "argv", args), patch(
"homeassistant.components.zwave_js.scripts.convert_device_diagnostics_to_fixture.Path.write_text"
) as write_text_mock:
main()
assert len(write_text_mock.call_args_list) == 1
assert write_text_mock.call_args[0][0] == json.dumps(fixture_dict, indent=2)