Add script to convert zwave_js device diagnostics to fixture (#102799)
This commit is contained in:
parent
f28c9221e6
commit
13378b4ae2
7 changed files with 3832 additions and 1 deletions
|
@ -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.
|
||||||
|
|
||||||
|
|
1
homeassistant/components/zwave_js/scripts/__init__.py
Normal file
1
homeassistant/components/zwave_js/scripts/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Scripts module for Z-Wave JS."""
|
|
@ -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()
|
2315
tests/components/zwave_js/fixtures/device_diagnostics.json
Normal file
2315
tests/components/zwave_js/fixtures/device_diagnostics.json
Normal file
File diff suppressed because it is too large
Load diff
1330
tests/components/zwave_js/fixtures/zooz_zse44_state.json
Normal file
1330
tests/components/zwave_js/fixtures/zooz_zse44_state.json
Normal file
File diff suppressed because it is too large
Load diff
1
tests/components/zwave_js/scripts/__init__.py
Normal file
1
tests/components/zwave_js/scripts/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for zwave_js scripts."""
|
|
@ -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)
|
Loading…
Add table
Add a link
Reference in a new issue