Add zwave to ozw migration (#39081)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Martin Hjelmare 2021-01-09 15:23:03 +01:00 committed by GitHub
parent 982c42e746
commit 8b72324ae6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 751 additions and 10 deletions

View File

@ -36,6 +36,7 @@ from .const import (
DATA_UNSUBSCRIBE,
DOMAIN,
MANAGER,
NODES_VALUES,
PLATFORMS,
TOPIC_OPENZWAVE,
)
@ -68,7 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
ozw_data[DATA_UNSUBSCRIBE] = []
data_nodes = {}
data_values = {}
hass.data[DOMAIN][NODES_VALUES] = data_values = {}
removed_nodes = []
manager_options = {"topic_prefix": f"{TOPIC_OPENZWAVE}/"}

View File

@ -37,6 +37,15 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.integration_created_addon = False
self.install_task = None
async def async_step_import(self, data):
"""Handle imported data.
This step will be used when importing data during zwave to ozw migration.
"""
self.network_key = data.get(CONF_NETWORK_KEY)
self.usb_path = data.get(CONF_USB_PATH)
return await self.async_step_user()
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
if self._async_current_entries():
@ -163,13 +172,15 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
else:
return self._async_create_entry_from_vars()
self.usb_path = self.addon_config.get(CONF_ADDON_DEVICE, "")
self.network_key = self.addon_config.get(CONF_ADDON_NETWORK_KEY, "")
usb_path = self.addon_config.get(CONF_ADDON_DEVICE, self.usb_path or "")
network_key = self.addon_config.get(
CONF_ADDON_NETWORK_KEY, self.network_key or ""
)
data_schema = vol.Schema(
{
vol.Required(CONF_USB_PATH, default=self.usb_path): str,
vol.Optional(CONF_NETWORK_KEY, default=self.network_key): str,
vol.Required(CONF_USB_PATH, default=usb_path): str,
vol.Optional(CONF_NETWORK_KEY, default=network_key): str,
}
)

View File

@ -25,6 +25,7 @@ PLATFORMS = [
SWITCH_DOMAIN,
]
MANAGER = "manager"
NODES_VALUES = "nodes_values"
# MQTT Topics
TOPIC_OPENZWAVE = "OpenZWave"
@ -40,6 +41,9 @@ ATTR_SCENE_LABEL = "scene_label"
ATTR_SCENE_VALUE_ID = "scene_value_id"
ATTR_SCENE_VALUE_LABEL = "scene_value_label"
# Config entry data and options
MIGRATED = "migrated"
# Service specific
SERVICE_ADD_NODE = "add_node"
SERVICE_REMOVE_NODE = "remove_node"

View File

@ -7,7 +7,8 @@
"python-openzwave-mqtt[mqtt-client]==1.4.0"
],
"after_dependencies": [
"mqtt"
"mqtt",
"zwave"
],
"codeowners": [
"@cgarwood",

View File

@ -0,0 +1,171 @@
"""Provide tools for migrating from the zwave integration."""
from homeassistant.helpers.device_registry import (
async_get_registry as async_get_device_registry,
)
from homeassistant.helpers.entity_registry import (
async_entries_for_config_entry,
async_get_registry as async_get_entity_registry,
)
from .const import DOMAIN, MIGRATED, NODES_VALUES
from .entity import create_device_id, create_value_id
# The following dicts map labels between OpenZWave 1.4 and 1.6.
METER_CC_LABELS = {
"Energy": "Electric - kWh",
"Power": "Electric - W",
"Count": "Electric - Pulses",
"Voltage": "Electric - V",
"Current": "Electric - A",
"Power Factor": "Electric - PF",
}
NOTIFICATION_CC_LABELS = {
"General": "Start",
"Smoke": "Smoke Alarm",
"Carbon Monoxide": "Carbon Monoxide",
"Carbon Dioxide": "Carbon Dioxide",
"Heat": "Heat",
"Flood": "Water",
"Access Control": "Access Control",
"Burglar": "Home Security",
"Power Management": "Power Management",
"System": "System",
"Emergency": "Emergency",
"Clock": "Clock",
"Appliance": "Appliance",
"HomeHealth": "Home Health",
}
CC_ID_LABELS = {
50: METER_CC_LABELS,
113: NOTIFICATION_CC_LABELS,
}
async def async_get_migration_data(hass):
"""Return dict with ozw side migration info."""
data = {}
nodes_values = hass.data[DOMAIN][NODES_VALUES]
ozw_config_entries = hass.config_entries.async_entries(DOMAIN)
config_entry = ozw_config_entries[0] # ozw only has a single config entry
ent_reg = await async_get_entity_registry(hass)
entity_entries = async_entries_for_config_entry(ent_reg, config_entry.entry_id)
unique_entries = {entry.unique_id: entry for entry in entity_entries}
dev_reg = await async_get_device_registry(hass)
for node_id, node_values in nodes_values.items():
for entity_values in node_values:
unique_id = create_value_id(entity_values.primary)
if unique_id not in unique_entries:
continue
node = entity_values.primary.node
device_identifier = (
DOMAIN,
create_device_id(node, entity_values.primary.instance),
)
device_entry = dev_reg.async_get_device({device_identifier}, set())
data[unique_id] = {
"node_id": node_id,
"node_instance": entity_values.primary.instance,
"device_id": device_entry.id,
"command_class": entity_values.primary.command_class.value,
"command_class_label": entity_values.primary.label,
"value_index": entity_values.primary.index,
"unique_id": unique_id,
"entity_entry": unique_entries[unique_id],
}
return data
def map_node_values(zwave_data, ozw_data):
"""Map zwave node values onto ozw node values."""
migration_map = {"device_entries": {}, "entity_entries": {}}
for zwave_entry in zwave_data.values():
node_id = zwave_entry["node_id"]
node_instance = zwave_entry["node_instance"]
cc_id = zwave_entry["command_class"]
zwave_cc_label = zwave_entry["command_class_label"]
if cc_id in CC_ID_LABELS:
labels = CC_ID_LABELS[cc_id]
ozw_cc_label = labels.get(zwave_cc_label, zwave_cc_label)
ozw_entry = next(
(
entry
for entry in ozw_data.values()
if entry["node_id"] == node_id
and entry["node_instance"] == node_instance
and entry["command_class"] == cc_id
and entry["command_class_label"] == ozw_cc_label
),
None,
)
else:
value_index = zwave_entry["value_index"]
ozw_entry = next(
(
entry
for entry in ozw_data.values()
if entry["node_id"] == node_id
and entry["node_instance"] == node_instance
and entry["command_class"] == cc_id
and entry["value_index"] == value_index
),
None,
)
if ozw_entry is None:
continue
# Save the zwave_entry under the ozw entity_id to create the map.
# Check that the mapped entities have the same domain.
if zwave_entry["entity_entry"].domain == ozw_entry["entity_entry"].domain:
migration_map["entity_entries"][
ozw_entry["entity_entry"].entity_id
] = zwave_entry
migration_map["device_entries"][ozw_entry["device_id"]] = zwave_entry[
"device_id"
]
return migration_map
async def async_migrate(hass, migration_map):
"""Perform zwave to ozw migration."""
dev_reg = await async_get_device_registry(hass)
for ozw_device_id, zwave_device_id in migration_map["device_entries"].items():
zwave_device_entry = dev_reg.async_get(zwave_device_id)
dev_reg.async_update_device(
ozw_device_id,
area_id=zwave_device_entry.area_id,
name_by_user=zwave_device_entry.name_by_user,
)
ent_reg = await async_get_entity_registry(hass)
for zwave_entry in migration_map["entity_entries"].values():
zwave_entity_id = zwave_entry["entity_entry"].entity_id
ent_reg.async_remove(zwave_entity_id)
for ozw_entity_id, zwave_entry in migration_map["entity_entries"].items():
entity_entry = zwave_entry["entity_entry"]
ent_reg.async_update_entity(
ozw_entity_id,
new_entity_id=entity_entry.entity_id,
name=entity_entry.name,
icon=entity_entry.icon,
)
zwave_config_entry = hass.config_entries.async_entries("zwave")[0]
await hass.config_entries.async_remove(zwave_config_entry.entry_id)
ozw_config_entry = hass.config_entries.async_entries("ozw")[0]
updates = {
**ozw_config_entry.data,
MIGRATED: True,
}
hass.config_entries.async_update_entry(ozw_config_entry, data=updates)

View File

@ -1,4 +1,6 @@
"""Web socket API for OpenZWave."""
import logging
from openzwavemqtt.const import (
ATTR_CODE_SLOT,
ATTR_LABEL,
@ -23,7 +25,11 @@ from homeassistant.helpers import config_validation as cv
from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER
from .lock import ATTR_USERCODE
from .migration import async_get_migration_data, async_migrate, map_node_values
_LOGGER = logging.getLogger(__name__)
DRY_RUN = "dry_run"
TYPE = "type"
ID = "id"
OZW_INSTANCE = "ozw_instance"
@ -52,6 +58,7 @@ ATTR_NEIGHBORS = "neighbors"
@callback
def async_register_api(hass):
"""Register all of our api endpoints."""
websocket_api.async_register_command(hass, websocket_migrate_zwave)
websocket_api.async_register_command(hass, websocket_get_instances)
websocket_api.async_register_command(hass, websocket_get_nodes)
websocket_api.async_register_command(hass, websocket_network_status)
@ -161,6 +168,63 @@ def _get_config_params(node, *args):
return config_params
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required(TYPE): "ozw/migrate_zwave",
vol.Optional(DRY_RUN, default=True): bool,
}
)
async def websocket_migrate_zwave(hass, connection, msg):
"""Migrate the zwave integration device and entity data to ozw integration."""
if "zwave" not in hass.config.components:
_LOGGER.error("Can not migrate, zwave integration is not loaded")
connection.send_message(
websocket_api.error_message(
msg["id"], "zwave_not_loaded", "Integration zwave is not loaded"
)
)
return
zwave = hass.components.zwave
zwave_data = await zwave.async_get_ozw_migration_data(hass)
_LOGGER.debug("Migration zwave data: %s", zwave_data)
ozw_data = await async_get_migration_data(hass)
_LOGGER.debug("Migration ozw data: %s", ozw_data)
can_migrate = map_node_values(zwave_data, ozw_data)
zwave_entity_ids = [
entry["entity_entry"].entity_id for entry in zwave_data.values()
]
ozw_entity_ids = [entry["entity_entry"].entity_id for entry in ozw_data.values()]
migration_device_map = {
zwave_device_id: ozw_device_id
for ozw_device_id, zwave_device_id in can_migrate["device_entries"].items()
}
migration_entity_map = {
zwave_entry["entity_entry"].entity_id: ozw_entity_id
for ozw_entity_id, zwave_entry in can_migrate["entity_entries"].items()
}
_LOGGER.debug("Migration entity map: %s", migration_entity_map)
if not msg[DRY_RUN]:
await async_migrate(hass, can_migrate)
connection.send_result(
msg[ID],
{
"migration_device_map": migration_device_map,
"zwave_entity_ids": zwave_entity_ids,
"ozw_entity_ids": ozw_entity_ids,
"migration_entity_map": migration_entity_map,
"migrated": not msg[DRY_RUN],
},
)
@websocket_api.websocket_command({vol.Required(TYPE): "ozw/get_instances"})
def websocket_get_instances(hass, connection, msg):
"""Get a list of OZW instances."""

View File

@ -28,6 +28,7 @@ from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL
from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.entity_registry import (
async_entries_for_config_entry,
async_get_registry as async_get_entity_registry,
)
from homeassistant.helpers.entity_values import EntityValues
@ -81,6 +82,8 @@ CONF_DEVICE_CONFIG = "device_config"
CONF_DEVICE_CONFIG_GLOB = "device_config_glob"
CONF_DEVICE_CONFIG_DOMAIN = "device_config_domain"
DATA_ZWAVE_CONFIG_YAML_PRESENT = "zwave_config_yaml_present"
DEFAULT_CONF_IGNORED = False
DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False
DEFAULT_CONF_INVERT_PERCENT = False
@ -250,6 +253,64 @@ CONFIG_SCHEMA = vol.Schema(
)
async def async_get_ozw_migration_data(hass):
"""Return dict with info for migration to ozw integration."""
data_to_migrate = {}
zwave_config_entries = hass.config_entries.async_entries(DOMAIN)
if not zwave_config_entries:
_LOGGER.error("Config entry not set up")
return data_to_migrate
if hass.data.get(DATA_ZWAVE_CONFIG_YAML_PRESENT):
_LOGGER.warning(
"Remove %s from configuration.yaml "
"to avoid setting up this integration on restart "
"after completing migration to ozw",
DOMAIN,
)
config_entry = zwave_config_entries[0] # zwave only has a single config entry
ent_reg = await async_get_entity_registry(hass)
entity_entries = async_entries_for_config_entry(ent_reg, config_entry.entry_id)
unique_entries = {entry.unique_id: entry for entry in entity_entries}
dev_reg = await async_get_device_registry(hass)
for entity_values in hass.data[DATA_ENTITY_VALUES]:
node = entity_values.primary.node
unique_id = compute_value_unique_id(node, entity_values.primary)
if unique_id not in unique_entries:
continue
device_identifier, _ = node_device_id_and_name(
node, entity_values.primary.instance
)
device_entry = dev_reg.async_get_device({device_identifier}, set())
data_to_migrate[unique_id] = {
"node_id": node.node_id,
"node_instance": entity_values.primary.instance,
"device_id": device_entry.id,
"command_class": entity_values.primary.command_class,
"command_class_label": entity_values.primary.label,
"value_index": entity_values.primary.index,
"unique_id": unique_id,
"entity_entry": unique_entries[unique_id],
}
return data_to_migrate
@callback
def async_is_ozw_migrated(hass):
"""Return True if migration to ozw is done."""
ozw_config_entries = hass.config_entries.async_entries("ozw")
if not ozw_config_entries:
return False
ozw_config_entry = ozw_config_entries[0] # only one ozw entry is allowed
migrated = bool(ozw_config_entry.data.get("migrated"))
return migrated
def _obj_to_dict(obj):
"""Convert an object into a hash for debug."""
return {
@ -312,6 +373,7 @@ async def async_setup(hass, config):
conf = config[DOMAIN]
hass.data[DATA_ZWAVE_CONFIG] = conf
hass.data[DATA_ZWAVE_CONFIG_YAML_PRESENT] = True
if not hass.config_entries.async_entries(DOMAIN):
hass.async_create_task(
@ -343,6 +405,12 @@ async def async_setup_entry(hass, config_entry):
# pylint: enable=import-error
from pydispatch import dispatcher
if async_is_ozw_migrated(hass):
_LOGGER.error(
"Migration to ozw has been done. Please remove the zwave integration"
)
return False
# Merge config entry and yaml config
config = config_entry.data
if DATA_ZWAVE_CONFIG in hass.data:

View File

@ -4,5 +4,6 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave",
"requirements": ["homeassistant-pyozw==0.1.10", "pydispatcher==2.0.5"],
"after_dependencies": ["ozw"],
"codeowners": ["@home-assistant/z-wave"]
}

View File

@ -2,6 +2,8 @@
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.components.ozw.const import DOMAIN as OZW_DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.core import callback
from .const import (
@ -56,9 +58,32 @@ def websocket_get_migration_config(hass, connection, msg):
)
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command({vol.Required(TYPE): "zwave/start_ozw_config_flow"})
async def websocket_start_ozw_config_flow(hass, connection, msg):
"""Start the ozw integration config flow (for migration wizard).
Return data with the flow id of the started ozw config flow.
"""
config = hass.data[DATA_ZWAVE_CONFIG]
data = {
"usb_path": config[CONF_USB_STICK_PATH],
"network_key": config[CONF_NETWORK_KEY],
}
result = await hass.config_entries.flow.async_init(
OZW_DOMAIN, context={"source": SOURCE_IMPORT}, data=data
)
connection.send_result(
msg[ID],
{"flow_id": result["flow_id"]},
)
@callback
def async_load_websocket_api(hass):
"""Set up the web socket API."""
websocket_api.async_register_command(hass, websocket_network_status)
websocket_api.async_register_command(hass, websocket_get_config)
websocket_api.async_register_command(hass, websocket_get_migration_config)
websocket_api.async_register_command(hass, websocket_start_ozw_config_flow)

View File

@ -16,6 +16,12 @@ def generic_data_fixture():
return load_fixture("ozw/generic_network_dump.csv")
@pytest.fixture(name="migration_data", scope="session")
def migration_data_fixture():
"""Load migration MQTT data and return it."""
return load_fixture("ozw/migration_fixture.csv")
@pytest.fixture(name="fan_data", scope="session")
def fan_data_fixture():
"""Load fan MQTT data and return it."""

View File

@ -535,3 +535,52 @@ async def test_discovery_addon_not_installed(
assert result["type"] == "form"
assert result["step_id"] == "start_addon"
async def test_import_addon_installed(
hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon
):
"""Test add-on already installed but not running on Supervisor."""
hass.config.components.add("mqtt")
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={"usb_path": "/test/imported", "network_key": "imported123"},
)
assert result["type"] == "form"
assert result["step_id"] == "on_supervisor"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"use_addon": True}
)
assert result["type"] == "form"
assert result["step_id"] == "start_addon"
# the default input should be the imported data
default_input = result["data_schema"]({})
with patch(
"homeassistant.components.ozw.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.ozw.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"], default_input
)
await hass.async_block_till_done()
assert result["type"] == "create_entry"
assert result["title"] == TITLE
assert result["data"] == {
"usb_path": "/test/imported",
"network_key": "imported123",
"use_addon": True,
"integration_created_addon": False,
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1

View File

@ -0,0 +1,292 @@
"""Test zwave to ozw migration."""
from unittest.mock import patch
import pytest
from homeassistant.components.ozw.websocket_api import ID, TYPE
from homeassistant.helpers.device_registry import (
DeviceEntry,
async_get_registry as async_get_device_registry,
)
from homeassistant.helpers.entity_registry import (
RegistryEntry,
async_get_registry as async_get_entity_registry,
)
from .common import setup_ozw
from tests.common import MockConfigEntry, mock_device_registry, mock_registry
ZWAVE_SOURCE_NODE_DEVICE_ID = "zwave_source_node_device_id"
ZWAVE_SOURCE_NODE_DEVICE_NAME = "Z-Wave Source Node Device"
ZWAVE_SOURCE_NODE_DEVICE_AREA = "Z-Wave Source Node Area"
ZWAVE_SOURCE_ENTITY = "sensor.zwave_source_node"
ZWAVE_SOURCE_NODE_UNIQUE_ID = "10-4321"
ZWAVE_BATTERY_DEVICE_ID = "zwave_battery_device_id"
ZWAVE_BATTERY_DEVICE_NAME = "Z-Wave Battery Device"
ZWAVE_BATTERY_DEVICE_AREA = "Z-Wave Battery Area"
ZWAVE_BATTERY_ENTITY = "sensor.zwave_battery_level"
ZWAVE_BATTERY_UNIQUE_ID = "36-1234"
ZWAVE_BATTERY_NAME = "Z-Wave Battery Level"
ZWAVE_BATTERY_ICON = "mdi:zwave-test-battery"
ZWAVE_POWER_DEVICE_ID = "zwave_power_device_id"
ZWAVE_POWER_DEVICE_NAME = "Z-Wave Power Device"
ZWAVE_POWER_DEVICE_AREA = "Z-Wave Power Area"
ZWAVE_POWER_ENTITY = "binary_sensor.zwave_power"
ZWAVE_POWER_UNIQUE_ID = "32-5678"
ZWAVE_POWER_NAME = "Z-Wave Power"
ZWAVE_POWER_ICON = "mdi:zwave-test-power"
@pytest.fixture(name="zwave_migration_data")
def zwave_migration_data_fixture(hass):
"""Return mock zwave migration data."""
zwave_source_node_device = DeviceEntry(
id=ZWAVE_SOURCE_NODE_DEVICE_ID,
name_by_user=ZWAVE_SOURCE_NODE_DEVICE_NAME,
area_id=ZWAVE_SOURCE_NODE_DEVICE_AREA,
)
zwave_source_node_entry = RegistryEntry(
entity_id=ZWAVE_SOURCE_ENTITY,
unique_id=ZWAVE_SOURCE_NODE_UNIQUE_ID,
platform="zwave",
name="Z-Wave Source Node",
)
zwave_battery_device = DeviceEntry(
id=ZWAVE_BATTERY_DEVICE_ID,
name_by_user=ZWAVE_BATTERY_DEVICE_NAME,
area_id=ZWAVE_BATTERY_DEVICE_AREA,
)
zwave_battery_entry = RegistryEntry(
entity_id=ZWAVE_BATTERY_ENTITY,
unique_id=ZWAVE_BATTERY_UNIQUE_ID,
platform="zwave",
name=ZWAVE_BATTERY_NAME,
icon=ZWAVE_BATTERY_ICON,
)
zwave_power_device = DeviceEntry(
id=ZWAVE_POWER_DEVICE_ID,
name_by_user=ZWAVE_POWER_DEVICE_NAME,
area_id=ZWAVE_POWER_DEVICE_AREA,
)
zwave_power_entry = RegistryEntry(
entity_id=ZWAVE_POWER_ENTITY,
unique_id=ZWAVE_POWER_UNIQUE_ID,
platform="zwave",
name=ZWAVE_POWER_NAME,
icon=ZWAVE_POWER_ICON,
)
zwave_migration_data = {
ZWAVE_SOURCE_NODE_UNIQUE_ID: {
"node_id": 10,
"node_instance": 1,
"device_id": zwave_source_node_device.id,
"command_class": 113,
"command_class_label": "SourceNodeId",
"value_index": 2,
"unique_id": ZWAVE_SOURCE_NODE_UNIQUE_ID,
"entity_entry": zwave_source_node_entry,
},
ZWAVE_BATTERY_UNIQUE_ID: {
"node_id": 36,
"node_instance": 1,
"device_id": zwave_battery_device.id,
"command_class": 128,
"command_class_label": "Battery Level",
"value_index": 0,
"unique_id": ZWAVE_BATTERY_UNIQUE_ID,
"entity_entry": zwave_battery_entry,
},
ZWAVE_POWER_UNIQUE_ID: {
"node_id": 32,
"node_instance": 1,
"device_id": zwave_power_device.id,
"command_class": 50,
"command_class_label": "Power",
"value_index": 8,
"unique_id": ZWAVE_POWER_UNIQUE_ID,
"entity_entry": zwave_power_entry,
},
}
mock_device_registry(
hass,
{
zwave_source_node_device.id: zwave_source_node_device,
zwave_battery_device.id: zwave_battery_device,
zwave_power_device.id: zwave_power_device,
},
)
mock_registry(
hass,
{
ZWAVE_SOURCE_ENTITY: zwave_source_node_entry,
ZWAVE_BATTERY_ENTITY: zwave_battery_entry,
ZWAVE_POWER_ENTITY: zwave_power_entry,
},
)
return zwave_migration_data
@pytest.fixture(name="zwave_integration")
def zwave_integration_fixture(hass, zwave_migration_data):
"""Mock the zwave integration."""
hass.config.components.add("zwave")
zwave_config_entry = MockConfigEntry(domain="zwave", data={"usb_path": "/dev/test"})
zwave_config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.zwave.async_get_ozw_migration_data",
return_value=zwave_migration_data,
):
yield zwave_config_entry
async def test_migrate_zwave(hass, migration_data, hass_ws_client, zwave_integration):
"""Test the zwave to ozw migration websocket api."""
await setup_ozw(hass, fixture=migration_data)
client = await hass_ws_client(hass)
assert hass.config_entries.async_entries("zwave")
await client.send_json({ID: 5, TYPE: "ozw/migrate_zwave", "dry_run": False})
msg = await client.receive_json()
result = msg["result"]
migration_entity_map = {
ZWAVE_BATTERY_ENTITY: "sensor.water_sensor_6_battery_level",
}
assert result["zwave_entity_ids"] == [
ZWAVE_SOURCE_ENTITY,
ZWAVE_BATTERY_ENTITY,
ZWAVE_POWER_ENTITY,
]
assert result["ozw_entity_ids"] == [
"sensor.smart_plug_electric_w",
"sensor.water_sensor_6_battery_level",
]
assert result["migration_entity_map"] == migration_entity_map
assert result["migrated"] is True
dev_reg = await async_get_device_registry(hass)
ent_reg = await async_get_entity_registry(hass)
# check the device registry migration
# check that the migrated entries have correct attributes
battery_entry = dev_reg.async_get_device(
identifiers={("ozw", "1.36.1")}, connections=set()
)
assert battery_entry.name_by_user == ZWAVE_BATTERY_DEVICE_NAME
assert battery_entry.area_id == ZWAVE_BATTERY_DEVICE_AREA
power_entry = dev_reg.async_get_device(
identifiers={("ozw", "1.32.1")}, connections=set()
)
assert power_entry.name_by_user == ZWAVE_POWER_DEVICE_NAME
assert power_entry.area_id == ZWAVE_POWER_DEVICE_AREA
migration_device_map = {
ZWAVE_BATTERY_DEVICE_ID: battery_entry.id,
ZWAVE_POWER_DEVICE_ID: power_entry.id,
}
assert result["migration_device_map"] == migration_device_map
# check the entity registry migration
# this should have been migrated and no longer present under that id
assert not ent_reg.async_is_registered("sensor.water_sensor_6_battery_level")
# these should not have been migrated and is still in the registry
assert ent_reg.async_is_registered(ZWAVE_SOURCE_ENTITY)
source_entry = ent_reg.async_get(ZWAVE_SOURCE_ENTITY)
assert source_entry.unique_id == ZWAVE_SOURCE_NODE_UNIQUE_ID
assert ent_reg.async_is_registered(ZWAVE_POWER_ENTITY)
source_entry = ent_reg.async_get(ZWAVE_POWER_ENTITY)
assert source_entry.unique_id == ZWAVE_POWER_UNIQUE_ID
assert ent_reg.async_is_registered("sensor.smart_plug_electric_w")
# this is the new entity_id of the ozw entity
assert ent_reg.async_is_registered(ZWAVE_BATTERY_ENTITY)
# check that the migrated entries have correct attributes
battery_entry = ent_reg.async_get(ZWAVE_BATTERY_ENTITY)
assert battery_entry.unique_id == "1-36-610271249"
assert battery_entry.name == ZWAVE_BATTERY_NAME
assert battery_entry.icon == ZWAVE_BATTERY_ICON
# check that the zwave config entry has been removed
assert not hass.config_entries.async_entries("zwave")
# Check that the zwave integration fails entry setup after migration
zwave_config_entry = MockConfigEntry(domain="zwave")
zwave_config_entry.add_to_hass(hass)
assert not await hass.config_entries.async_setup(zwave_config_entry.entry_id)
async def test_migrate_zwave_dry_run(
hass, migration_data, hass_ws_client, zwave_integration
):
"""Test the zwave to ozw migration websocket api dry run."""
await setup_ozw(hass, fixture=migration_data)
client = await hass_ws_client(hass)
await client.send_json({ID: 5, TYPE: "ozw/migrate_zwave"})
msg = await client.receive_json()
result = msg["result"]
migration_entity_map = {
ZWAVE_BATTERY_ENTITY: "sensor.water_sensor_6_battery_level",
}
assert result["zwave_entity_ids"] == [
ZWAVE_SOURCE_ENTITY,
ZWAVE_BATTERY_ENTITY,
ZWAVE_POWER_ENTITY,
]
assert result["ozw_entity_ids"] == [
"sensor.smart_plug_electric_w",
"sensor.water_sensor_6_battery_level",
]
assert result["migration_entity_map"] == migration_entity_map
assert result["migrated"] is False
ent_reg = await async_get_entity_registry(hass)
# no real migration should have been done
assert ent_reg.async_is_registered("sensor.water_sensor_6_battery_level")
assert ent_reg.async_is_registered("sensor.smart_plug_electric_w")
assert ent_reg.async_is_registered(ZWAVE_SOURCE_ENTITY)
source_entry = ent_reg.async_get(ZWAVE_SOURCE_ENTITY)
assert source_entry.unique_id == ZWAVE_SOURCE_NODE_UNIQUE_ID
assert ent_reg.async_is_registered(ZWAVE_BATTERY_ENTITY)
battery_entry = ent_reg.async_get(ZWAVE_BATTERY_ENTITY)
assert battery_entry.unique_id == ZWAVE_BATTERY_UNIQUE_ID
assert ent_reg.async_is_registered(ZWAVE_POWER_ENTITY)
power_entry = ent_reg.async_get(ZWAVE_POWER_ENTITY)
assert power_entry.unique_id == ZWAVE_POWER_UNIQUE_ID
# check that the zwave config entry has not been removed
assert hass.config_entries.async_entries("zwave")
# Check that the zwave integration can be setup after dry run
zwave_config_entry = zwave_integration
with patch("openzwave.option.ZWaveOption"), patch("openzwave.network.ZWaveNetwork"):
assert await hass.config_entries.async_setup(zwave_config_entry.entry_id)
async def test_migrate_zwave_not_setup(hass, migration_data, hass_ws_client):
"""Test the zwave to ozw migration websocket without zwave setup."""
await setup_ozw(hass, fixture=migration_data)
client = await hass_ws_client(hass)
await client.send_json({ID: 5, TYPE: "ozw/migrate_zwave"})
msg = await client.receive_json()
assert not msg["success"]
assert msg["error"]["code"] == "zwave_not_loaded"
assert msg["error"]["message"] == "Integration zwave is not loaded"

View File

@ -1,4 +1,6 @@
"""Test Z-Wave Websocket API."""
from unittest.mock import call, patch
from homeassistant.bootstrap import async_setup_component
from homeassistant.components.zwave.const import (
CONF_AUTOHEAL,
@ -8,6 +10,8 @@ from homeassistant.components.zwave.const import (
)
from homeassistant.components.zwave.websocket_api import ID, TYPE
NETWORK_KEY = "0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST"
async def test_zwave_ws_api(hass, mock_openzwave, hass_ws_client):
"""Test Z-Wave websocket API."""
@ -20,7 +24,7 @@ async def test_zwave_ws_api(hass, mock_openzwave, hass_ws_client):
CONF_AUTOHEAL: False,
CONF_USB_STICK_PATH: "/dev/zwave",
CONF_POLLING_INTERVAL: 6000,
CONF_NETWORK_KEY: "0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST",
CONF_NETWORK_KEY: NETWORK_KEY,
}
},
)
@ -38,12 +42,47 @@ async def test_zwave_ws_api(hass, mock_openzwave, hass_ws_client):
assert not result[CONF_AUTOHEAL]
assert result[CONF_POLLING_INTERVAL] == 6000
async def test_zwave_ozw_migration_api(hass, mock_openzwave, hass_ws_client):
"""Test Z-Wave to OpenZWave websocket migration API."""
await async_setup_component(
hass,
"zwave",
{
"zwave": {
CONF_AUTOHEAL: False,
CONF_USB_STICK_PATH: "/dev/zwave",
CONF_POLLING_INTERVAL: 6000,
CONF_NETWORK_KEY: NETWORK_KEY,
}
},
)
await hass.async_block_till_done()
client = await hass_ws_client(hass)
await client.send_json({ID: 6, TYPE: "zwave/get_migration_config"})
msg = await client.receive_json()
result = msg["result"]
assert result[CONF_USB_STICK_PATH] == "/dev/zwave"
assert (
result[CONF_NETWORK_KEY]
== "0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST"
assert result[CONF_NETWORK_KEY] == NETWORK_KEY
with patch(
"homeassistant.config_entries.ConfigEntriesFlowManager.async_init"
) as async_init:
async_init.return_value = {"flow_id": "mock_flow_id"}
await client.send_json({ID: 7, TYPE: "zwave/start_ozw_config_flow"})
msg = await client.receive_json()
result = msg["result"]
assert result["flow_id"] == "mock_flow_id"
assert async_init.call_args == call(
"ozw",
context={"source": "import"},
data={"usb_path": "/dev/zwave", "network_key": NETWORK_KEY},
)

View File

@ -0,0 +1,9 @@
OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"}
OpenZWave/1/node/32/,{ "NodeID": 32, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0208:0005:0101", "ZWAProductURL": "", "ProductPic": "images/hank/hkzw-so01-smartplug.png", "Description": "fixture description", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "Smart Plug", "ProductPicBase64": "iVBORggg==" }, "Event": "nodeQueriesComplete", "TimeStamp": 1579566933, "NodeManufacturerName": "HANK Electronics Ltd", "NodeProductName": "HKZW-SO01 Smart Plug", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Binary Switch", "NodeGeneric": 16, "NodeSpecificString": "Binary Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0208", "NodeProductType": "0x0101", "NodeProductID": "0x0005", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "On/Off Power Switch", "NodeDeviceType": 1792, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 33, 36, 37, 39 ]}
OpenZWave/1/node/32/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891}
OpenZWave/1/node/32/instance/1/commandclass/50/,{ "Instance": 1, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "TimeStamp": 1579566891}
OpenZWave/1/node/32/instance/1/commandclass/50/value/562950495305746/,{ "Label": "Electric - W", "Value": 0.0, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 562950495305746, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891}
OpenZWave/1/node/36/,{ "NodeID": 36, "NodeQueryStage": "CacheLoad", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0086:007A:0102", "ZWAProductURL": "", "ProductPic": "images/aeotec/zw122.png", "Description": "fixture description", "WakeupHelp": "Pressing the Action Button once will trigger sending the Wake up notification command. If press and hold the Z-Wave button for 3 seconds, the Water Sensor will wake up for 10 minutes.", "ProductSupportURL": "", "Frequency": "", "Name": "Water Sensor 6", "ProductPicBase64": "kSuQmCC" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "AEON Labs", "NodeProductName": "ZW122 Water Sensor 6", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Notification Sensor", "NodeGeneric": 7, "NodeSpecificString": "Notification Sensor", "NodeSpecific": 1, "NodeManufacturerID": "0x0086", "NodeProductType": "0x0102", "NodeProductID": "0x007a", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 4}
OpenZWave/1/node/36/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891}
OpenZWave/1/node/36/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1579566891}
OpenZWave/1/node/36/instance/1/commandclass/128/value/610271249/,{ "Label": "Battery Level", "Value": 100, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 36, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 610271249, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891}
Can't render this file because it contains an unexpected character in line 1 and column 26.