Add zwave to zwave_js migration (#56159)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Martin Hjelmare 2021-09-29 17:55:27 +02:00 committed by GitHub
parent d5c3d234ec
commit 50fffe48f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1219 additions and 687 deletions

View File

@ -35,15 +35,6 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.integration_created_addon = False self.integration_created_addon = False
self.install_task = None 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): async def async_step_user(self, user_input=None):
"""Handle the initial step.""" """Handle the initial step."""
if self._async_current_entries(): if self._async_current_entries():

View File

@ -1,171 +0,0 @@
"""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

@ -25,7 +25,6 @@ from homeassistant.helpers import config_validation as cv
from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER
from .lock import ATTR_USERCODE from .lock import ATTR_USERCODE
from .migration import async_get_migration_data, async_migrate, map_node_values
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -58,7 +57,6 @@ ATTR_NEIGHBORS = "neighbors"
@callback @callback
def async_register_api(hass): def async_register_api(hass):
"""Register all of our api endpoints.""" """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_instances)
websocket_api.async_register_command(hass, websocket_get_nodes) websocket_api.async_register_command(hass, websocket_get_nodes)
websocket_api.async_register_command(hass, websocket_network_status) websocket_api.async_register_command(hass, websocket_network_status)
@ -168,63 +166,6 @@ def _get_config_params(node, *args):
return config_params 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"}) @websocket_api.websocket_command({vol.Required(TYPE): "ozw/get_instances"})
def websocket_get_instances(hass, connection, msg): def websocket_get_instances(hass, connection, msg):
"""Get a list of OZW instances.""" """Get a list of OZW instances."""

View File

@ -29,7 +29,6 @@ from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL
from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.entity_registry import ( from homeassistant.helpers.entity_registry import (
async_entries_for_config_entry,
async_get_registry as async_get_entity_registry, async_get_registry as async_get_entity_registry,
) )
from homeassistant.helpers.entity_values import EntityValues from homeassistant.helpers.entity_values import EntityValues
@ -56,11 +55,18 @@ from .const import (
DOMAIN, DOMAIN,
) )
from .discovery_schemas import DISCOVERY_SCHEMAS from .discovery_schemas import DISCOVERY_SCHEMAS
from .migration import ( # noqa: F401 pylint: disable=unused-import
async_add_migration_entity_value,
async_get_migration_data,
async_is_ozw_migrated,
async_is_zwave_js_migrated,
)
from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity
from .util import ( from .util import (
check_has_unique_id, check_has_unique_id,
check_node_schema, check_node_schema,
check_value_schema, check_value_schema,
compute_value_unique_id,
is_node_parsed, is_node_parsed,
node_device_id_and_name, node_device_id_and_name,
node_name, node_name,
@ -253,64 +259,6 @@ 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): def _obj_to_dict(obj):
"""Convert an object into a hash for debug.""" """Convert an object into a hash for debug."""
return { return {
@ -404,9 +352,22 @@ async def async_setup_entry(hass, config_entry): # noqa: C901
# pylint: enable=import-error # pylint: enable=import-error
from pydispatch import dispatcher from pydispatch import dispatcher
if async_is_ozw_migrated(hass): if async_is_ozw_migrated(hass) or async_is_zwave_js_migrated(hass):
if hass.data.get(DATA_ZWAVE_CONFIG_YAML_PRESENT):
config_yaml_message = (
", and remove %s from configuration.yaml "
"to avoid setting up this integration on restart ",
DOMAIN,
)
else:
config_yaml_message = ""
_LOGGER.error( _LOGGER.error(
"Migration to ozw has been done. Please remove the zwave integration" "Migration away from legacy Z-Wave has been done. "
"Please remove the %s integration%s",
DOMAIN,
config_yaml_message,
) )
return False return False
@ -1307,6 +1268,9 @@ class ZWaveDeviceEntity(ZWaveBaseEntity):
self.refresh_from_network, self.refresh_from_network,
) )
# Add legacy Z-Wave migration data.
await async_add_migration_entity_value(self.hass, self.entity_id, self.values)
def _update_attributes(self): def _update_attributes(self):
"""Update the node attributes. May only be used inside callback.""" """Update the node attributes. May only be used inside callback."""
self.node_id = self.node.node_id self.node_id = self.node.node_id
@ -1386,8 +1350,3 @@ class ZWaveDeviceEntity(ZWaveBaseEntity):
) or self.node.is_ready: ) or self.node.is_ready:
return compute_value_unique_id(self.node, self.values.primary) return compute_value_unique_id(self.node, self.values.primary)
return None return None
def compute_value_unique_id(node, value):
"""Compute unique_id a value would get if it were to get one."""
return f"{node.node_id}-{value.object_id}"

View File

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

View File

@ -0,0 +1,167 @@
"""Handle migration from legacy Z-Wave to OpenZWave and Z-Wave JS."""
from __future__ import annotations
from typing import TYPE_CHECKING, TypedDict, cast
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import async_get as async_get_device_registry
from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.storage import Store
from .const import DOMAIN
from .util import node_device_id_and_name
if TYPE_CHECKING:
from . import ZWaveDeviceEntityValues
LEGACY_ZWAVE_MIGRATION = f"{DOMAIN}_legacy_zwave_migration"
STORAGE_WRITE_DELAY = 30
STORAGE_KEY = f"{DOMAIN}.legacy_zwave_migration"
STORAGE_VERSION = 1
class ZWaveMigrationData(TypedDict):
"""Represent the Z-Wave migration data dict."""
node_id: int
node_instance: int
command_class: int
command_class_label: str
value_index: int
device_id: str
domain: str
entity_id: str
unique_id: str
unit_of_measurement: str | None
@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
@callback
def async_is_zwave_js_migrated(hass):
"""Return True if migration to Z-Wave JS is done."""
zwave_js_config_entries = hass.config_entries.async_entries("zwave_js")
if not zwave_js_config_entries:
return False
migrated = any(
config_entry.data.get("migrated") for config_entry in zwave_js_config_entries
)
return migrated
async def async_add_migration_entity_value(
hass: HomeAssistant,
entity_id: str,
entity_values: ZWaveDeviceEntityValues,
) -> None:
"""Add Z-Wave entity value for legacy Z-Wave migration."""
migration_handler: LegacyZWaveMigration = await get_legacy_zwave_migration(hass)
migration_handler.add_entity_value(entity_id, entity_values)
async def async_get_migration_data(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, ZWaveMigrationData]:
"""Return Z-Wave migration data."""
migration_handler: LegacyZWaveMigration = await get_legacy_zwave_migration(hass)
return await migration_handler.get_data(config_entry)
@singleton(LEGACY_ZWAVE_MIGRATION)
async def get_legacy_zwave_migration(hass: HomeAssistant) -> LegacyZWaveMigration:
"""Return legacy Z-Wave migration handler."""
migration_handler = LegacyZWaveMigration(hass)
await migration_handler.load_data()
return migration_handler
class LegacyZWaveMigration:
"""Handle the migration from zwave to ozw and zwave_js."""
def __init__(self, hass: HomeAssistant) -> None:
"""Set up migration instance."""
self._hass = hass
self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
self._data: dict[str, dict[str, ZWaveMigrationData]] = {}
async def load_data(self) -> None:
"""Load Z-Wave migration data."""
stored = cast(dict, await self._store.async_load())
if stored:
self._data = stored
@callback
def save_data(
self, config_entry_id: str, entity_id: str, data: ZWaveMigrationData
) -> None:
"""Save Z-Wave migration data."""
if config_entry_id not in self._data:
self._data[config_entry_id] = {}
self._data[config_entry_id][entity_id] = data
self._store.async_delay_save(self._data_to_save, STORAGE_WRITE_DELAY)
@callback
def _data_to_save(self) -> dict[str, dict[str, ZWaveMigrationData]]:
"""Return data to save."""
return self._data
@callback
def add_entity_value(
self,
entity_id: str,
entity_values: ZWaveDeviceEntityValues,
) -> None:
"""Add info for one entity and Z-Wave value."""
ent_reg = async_get_entity_registry(self._hass)
dev_reg = async_get_device_registry(self._hass)
node = entity_values.primary.node
entity_entry = ent_reg.async_get(entity_id)
assert entity_entry
device_identifier, _ = node_device_id_and_name(
node, entity_values.primary.instance
)
device_entry = dev_reg.async_get_device({device_identifier}, set())
assert device_entry
# Normalize unit of measurement.
if unit := entity_entry.unit_of_measurement:
unit = unit.lower()
if unit == "":
unit = None
data: ZWaveMigrationData = {
"node_id": node.node_id,
"node_instance": entity_values.primary.instance,
"command_class": entity_values.primary.command_class,
"command_class_label": entity_values.primary.label,
"value_index": entity_values.primary.index,
"device_id": device_entry.id,
"domain": entity_entry.domain,
"entity_id": entity_id,
"unique_id": entity_entry.unique_id,
"unit_of_measurement": unit,
}
self.save_data(entity_entry.config_entry_id, entity_id, data)
async def get_data(
self, config_entry: ConfigEntry
) -> dict[str, ZWaveMigrationData]:
"""Return Z-Wave migration data."""
await self.load_data()
data = self._data.get(config_entry.entry_id)
return data or {}

View File

@ -88,6 +88,11 @@ def check_value_schema(value, schema):
return True return True
def compute_value_unique_id(node, value):
"""Compute unique_id a value would get if it were to get one."""
return f"{node.node_id}-{value.object_id}"
def node_name(node): def node_name(node):
"""Return the name of the node.""" """Return the name of the node."""
if is_node_parsed(node): if is_node_parsed(node):

View File

@ -2,7 +2,6 @@
import voluptuous as vol import voluptuous as vol
from homeassistant.components import websocket_api 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.config_entries import SOURCE_IMPORT
from homeassistant.core import callback from homeassistant.core import callback
@ -59,12 +58,14 @@ def websocket_get_migration_config(hass, connection, msg):
@websocket_api.require_admin @websocket_api.require_admin
@websocket_api.websocket_command(
{vol.Required(TYPE): "zwave/start_zwave_js_config_flow"}
)
@websocket_api.async_response @websocket_api.async_response
@websocket_api.websocket_command({vol.Required(TYPE): "zwave/start_ozw_config_flow"}) async def websocket_start_zwave_js_config_flow(hass, connection, msg):
async def websocket_start_ozw_config_flow(hass, connection, msg): """Start the Z-Wave JS integration config flow (for migration wizard).
"""Start the ozw integration config flow (for migration wizard).
Return data with the flow id of the started ozw config flow. Return data with the flow id of the started Z-Wave JS config flow.
""" """
config = hass.data[DATA_ZWAVE_CONFIG] config = hass.data[DATA_ZWAVE_CONFIG]
data = { data = {
@ -72,7 +73,7 @@ async def websocket_start_ozw_config_flow(hass, connection, msg):
"network_key": config[CONF_NETWORK_KEY], "network_key": config[CONF_NETWORK_KEY],
} }
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
OZW_DOMAIN, context={"source": SOURCE_IMPORT}, data=data "zwave_js", context={"source": SOURCE_IMPORT}, data=data
) )
connection.send_result( connection.send_result(
msg[ID], msg[ID],
@ -86,4 +87,4 @@ def async_load_websocket_api(hass):
websocket_api.async_register_command(hass, websocket_network_status) 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_config)
websocket_api.async_register_command(hass, websocket_get_migration_config) websocket_api.async_register_command(hass, websocket_get_migration_config)
websocket_api.async_register_command(hass, websocket_start_ozw_config_flow) websocket_api.async_register_command(hass, websocket_start_zwave_js_config_flow)

View File

@ -58,8 +58,15 @@ from .const import (
DATA_CLIENT, DATA_CLIENT,
DOMAIN, DOMAIN,
EVENT_DEVICE_ADDED_TO_REGISTRY, EVENT_DEVICE_ADDED_TO_REGISTRY,
LOGGER,
) )
from .helpers import async_enable_statistics, update_data_collection_preference from .helpers import async_enable_statistics, update_data_collection_preference
from .migrate import (
ZWaveMigrationData,
async_get_migration_data,
async_map_legacy_zwave_values,
async_migrate_legacy_zwave,
)
DATA_UNSUBSCRIBE = "unsubs" DATA_UNSUBSCRIBE = "unsubs"
@ -96,6 +103,9 @@ OPTED_IN = "opted_in"
SECURITY_CLASSES = "security_classes" SECURITY_CLASSES = "security_classes"
CLIENT_SIDE_AUTH = "client_side_auth" CLIENT_SIDE_AUTH = "client_side_auth"
# constants for migration
DRY_RUN = "dry_run"
def async_get_entry(orig_func: Callable) -> Callable: def async_get_entry(orig_func: Callable) -> Callable:
"""Decorate async function to get entry.""" """Decorate async function to get entry."""
@ -218,6 +228,8 @@ def async_register_api(hass: HomeAssistant) -> None:
hass, websocket_subscribe_controller_statistics hass, websocket_subscribe_controller_statistics
) )
websocket_api.async_register_command(hass, websocket_subscribe_node_statistics) websocket_api.async_register_command(hass, websocket_subscribe_node_statistics)
websocket_api.async_register_command(hass, websocket_node_ready)
websocket_api.async_register_command(hass, websocket_migrate_zwave)
hass.http.register_view(DumpView()) hass.http.register_view(DumpView())
hass.http.register_view(FirmwareUploadView()) hass.http.register_view(FirmwareUploadView())
@ -272,6 +284,42 @@ async def websocket_network_status(
) )
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zwave_js/node_ready",
vol.Required(ENTRY_ID): str,
vol.Required(NODE_ID): int,
}
)
@websocket_api.async_response
@async_get_node
async def websocket_node_ready(
hass: HomeAssistant,
connection: ActiveConnection,
msg: dict,
node: Node,
) -> None:
"""Subscribe to the node ready event of a Z-Wave JS node."""
@callback
def forward_event(event: dict) -> None:
"""Forward the event."""
connection.send_message(
websocket_api.event_message(msg[ID], {"event": event["event"]})
)
@callback
def async_cleanup() -> None:
"""Remove signal listeners."""
for unsub in unsubs:
unsub()
connection.subscriptions[msg["id"]] = async_cleanup
msg[DATA_UNSUBSCRIBE] = unsubs = [node.on("ready", forward_event)]
connection.send_result(msg[ID])
@websocket_api.websocket_command( @websocket_api.websocket_command(
{ {
vol.Required(TYPE): "zwave_js/node_status", vol.Required(TYPE): "zwave_js/node_status",
@ -1743,3 +1791,72 @@ async def websocket_subscribe_node_statistics(
connection.subscriptions[msg["id"]] = async_cleanup connection.subscriptions[msg["id"]] = async_cleanup
connection.send_result(msg[ID], _get_node_statistics_dict(node.statistics)) connection.send_result(msg[ID], _get_node_statistics_dict(node.statistics))
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zwave_js/migrate_zwave",
vol.Required(ENTRY_ID): str,
vol.Optional(DRY_RUN, default=True): bool,
}
)
@websocket_api.async_response
@async_get_entry
async def websocket_migrate_zwave(
hass: HomeAssistant,
connection: ActiveConnection,
msg: dict,
entry: ConfigEntry,
client: Client,
) -> None:
"""Migrate Z-Wave device and entity data to Z-Wave JS integration."""
if "zwave" not in hass.config.components:
connection.send_message(
websocket_api.error_message(
msg["id"], "zwave_not_loaded", "Integration zwave is not loaded"
)
)
return
zwave = hass.components.zwave
zwave_config_entries = hass.config_entries.async_entries("zwave")
zwave_config_entry = zwave_config_entries[0] # zwave only has a single config entry
zwave_data: dict[str, ZWaveMigrationData] = await zwave.async_get_migration_data(
hass, zwave_config_entry
)
LOGGER.debug("Migration zwave data: %s", zwave_data)
zwave_js_config_entry = entry
zwave_js_data = await async_get_migration_data(hass, zwave_js_config_entry)
LOGGER.debug("Migration zwave_js data: %s", zwave_js_data)
migration_map = async_map_legacy_zwave_values(zwave_data, zwave_js_data)
zwave_entity_ids = [entry["entity_id"] for entry in zwave_data.values()]
zwave_js_entity_ids = [entry["entity_id"] for entry in zwave_js_data.values()]
migration_device_map = {
zwave_device_id: zwave_js_device_id
for zwave_js_device_id, zwave_device_id in migration_map.device_entries.items()
}
migration_entity_map = {
zwave_entry["entity_id"]: zwave_js_entity_id
for zwave_js_entity_id, zwave_entry in migration_map.entity_entries.items()
}
LOGGER.debug("Migration entity map: %s", migration_entity_map)
if not msg[DRY_RUN]:
await async_migrate_legacy_zwave(
hass, zwave_config_entry, zwave_js_config_entry, migration_map
)
connection.send_result(
msg[ID],
{
"migration_device_map": migration_device_map,
"zwave_entity_ids": zwave_entity_ids,
"zwave_js_entity_ids": zwave_js_entity_ids,
"migration_entity_map": migration_entity_map,
"migrated": not msg[DRY_RUN],
},
)

View File

@ -302,6 +302,16 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN):
"""Return the options flow.""" """Return the options flow."""
return OptionsFlowHandler(config_entry) return OptionsFlowHandler(config_entry)
async def async_step_import(self, data: dict[str, Any]) -> FlowResult:
"""Handle imported data.
This step will be used when importing data
during Z-Wave to Z-Wave JS 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( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:

View File

@ -15,6 +15,7 @@ from homeassistant.helpers.entity import Entity
from .const import DOMAIN from .const import DOMAIN
from .discovery import ZwaveDiscoveryInfo from .discovery import ZwaveDiscoveryInfo
from .helpers import get_device_id, get_unique_id from .helpers import get_device_id, get_unique_id
from .migrate import async_add_migration_entity_value
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -109,6 +110,11 @@ class ZWaveBaseEntity(Entity):
) )
) )
# Add legacy Z-Wave migration data.
await async_add_migration_entity_value(
self.hass, self.config_entry, self.entity_id, self.info
)
def generate_name( def generate_name(
self, self,
include_value_name: bool = False, include_value_name: bool = False,

View File

@ -1,27 +1,355 @@
"""Functions used to migrate unique IDs for Z-Wave JS entities.""" """Functions used to migrate unique IDs for Z-Wave JS entities."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass, field
import logging import logging
from typing import TypedDict, cast
from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.model.value import Value as ZwaveValue from zwave_js_server.model.value import Value as ZwaveValue
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_UNAVAILABLE from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.device_registry import (
DeviceEntry,
async_get as async_get_device_registry,
)
from homeassistant.helpers.entity_registry import ( from homeassistant.helpers.entity_registry import (
EntityRegistry, EntityRegistry,
RegistryEntry, RegistryEntry,
async_entries_for_device, async_entries_for_device,
async_get as async_get_entity_registry,
) )
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.storage import Store
from .const import DOMAIN from .const import DOMAIN
from .discovery import ZwaveDiscoveryInfo from .discovery import ZwaveDiscoveryInfo
from .helpers import get_unique_id from .helpers import get_device_id, get_unique_id
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
LEGACY_ZWAVE_MIGRATION = f"{DOMAIN}_legacy_zwave_migration"
MIGRATED = "migrated"
STORAGE_WRITE_DELAY = 30
STORAGE_KEY = f"{DOMAIN}.legacy_zwave_migration"
STORAGE_VERSION = 1
NOTIFICATION_CC_LABEL_TO_PROPERTY_NAME = {
"Smoke": "Smoke Alarm",
"Carbon Monoxide": "CO Alarm",
"Carbon Dioxide": "CO2 Alarm",
"Heat": "Heat Alarm",
"Flood": "Water Alarm",
"Access Control": "Access Control",
"Burglar": "Home Security",
"Power Management": "Power Management",
"System": "System",
"Emergency": "Siren",
"Clock": "Clock",
"Appliance": "Appliance",
"HomeHealth": "Home Health",
}
SENSOR_MULTILEVEL_CC_LABEL_TO_PROPERTY_NAME = {
"Temperature": "Air temperature",
"General": "General purpose",
"Luminance": "Illuminance",
"Power": "Power",
"Relative Humidity": "Humidity",
"Velocity": "Velocity",
"Direction": "Direction",
"Atmospheric Pressure": "Atmospheric pressure",
"Barometric Pressure": "Barometric pressure",
"Solar Radiation": "Solar radiation",
"Dew Point": "Dew point",
"Rain Rate": "Rain rate",
"Tide Level": "Tide level",
"Weight": "Weight",
"Voltage": "Voltage",
"Current": "Current",
"CO2 Level": "Carbon dioxide (CO₂) level",
"Air Flow": "Air flow",
"Tank Capacity": "Tank capacity",
"Distance": "Distance",
"Angle Position": "Angle position",
"Rotation": "Rotation",
"Water Temperature": "Water temperature",
"Soil Temperature": "Soil temperature",
"Seismic Intensity": "Seismic Intensity",
"Seismic Magnitude": "Seismic magnitude",
"Ultraviolet": "Ultraviolet",
"Electrical Resistivity": "Electrical resistivity",
"Electrical Conductivity": "Electrical conductivity",
"Loudness": "Loudness",
"Moisture": "Moisture",
}
CC_ID_LABEL_TO_PROPERTY = {
49: SENSOR_MULTILEVEL_CC_LABEL_TO_PROPERTY_NAME,
113: NOTIFICATION_CC_LABEL_TO_PROPERTY_NAME,
}
class ZWaveMigrationData(TypedDict):
"""Represent the Z-Wave migration data dict."""
node_id: int
node_instance: int
command_class: int
command_class_label: str
value_index: int
device_id: str
domain: str
entity_id: str
unique_id: str
unit_of_measurement: str | None
class ZWaveJSMigrationData(TypedDict):
"""Represent the Z-Wave JS migration data dict."""
node_id: int
endpoint_index: int
command_class: int
value_property_name: str
value_property_key_name: str | None
value_id: str
device_id: str
domain: str
entity_id: str
unique_id: str
unit_of_measurement: str | None
@dataclass
class LegacyZWaveMappedData:
"""Represent the mapped data between Z-Wave and Z-Wave JS."""
entity_entries: dict[str, ZWaveMigrationData] = field(default_factory=dict)
device_entries: dict[str, str] = field(default_factory=dict)
async def async_add_migration_entity_value(
hass: HomeAssistant,
config_entry: ConfigEntry,
entity_id: str,
discovery_info: ZwaveDiscoveryInfo,
) -> None:
"""Add Z-Wave JS entity value for legacy Z-Wave migration."""
migration_handler: LegacyZWaveMigration = await get_legacy_zwave_migration(hass)
migration_handler.add_entity_value(config_entry, entity_id, discovery_info)
async def async_get_migration_data(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, ZWaveJSMigrationData]:
"""Return Z-Wave JS migration data."""
migration_handler: LegacyZWaveMigration = await get_legacy_zwave_migration(hass)
return await migration_handler.get_data(config_entry)
@singleton(LEGACY_ZWAVE_MIGRATION)
async def get_legacy_zwave_migration(hass: HomeAssistant) -> LegacyZWaveMigration:
"""Return legacy Z-Wave migration handler."""
migration_handler = LegacyZWaveMigration(hass)
await migration_handler.load_data()
return migration_handler
class LegacyZWaveMigration:
"""Handle the migration from zwave to zwave_js."""
def __init__(self, hass: HomeAssistant) -> None:
"""Set up migration instance."""
self._hass = hass
self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
self._data: dict[str, dict[str, ZWaveJSMigrationData]] = {}
async def load_data(self) -> None:
"""Load Z-Wave JS migration data."""
stored = cast(dict, await self._store.async_load())
if stored:
self._data = stored
@callback
def save_data(
self, config_entry_id: str, entity_id: str, data: ZWaveJSMigrationData
) -> None:
"""Save Z-Wave JS migration data."""
if config_entry_id not in self._data:
self._data[config_entry_id] = {}
self._data[config_entry_id][entity_id] = data
self._store.async_delay_save(self._data_to_save, STORAGE_WRITE_DELAY)
@callback
def _data_to_save(self) -> dict[str, dict[str, ZWaveJSMigrationData]]:
"""Return data to save."""
return self._data
@callback
def add_entity_value(
self,
config_entry: ConfigEntry,
entity_id: str,
discovery_info: ZwaveDiscoveryInfo,
) -> None:
"""Add info for one entity and Z-Wave JS value."""
ent_reg = async_get_entity_registry(self._hass)
dev_reg = async_get_device_registry(self._hass)
node = discovery_info.node
primary_value = discovery_info.primary_value
entity_entry = ent_reg.async_get(entity_id)
assert entity_entry
device_identifier = get_device_id(node.client, node)
device_entry = dev_reg.async_get_device({device_identifier}, set())
assert device_entry
# Normalize unit of measurement.
if unit := entity_entry.unit_of_measurement:
unit = unit.lower()
if unit == "":
unit = None
data: ZWaveJSMigrationData = {
"node_id": node.node_id,
"endpoint_index": node.index,
"command_class": primary_value.command_class,
"value_property_name": primary_value.property_name,
"value_property_key_name": primary_value.property_key_name,
"value_id": primary_value.value_id,
"device_id": device_entry.id,
"domain": entity_entry.domain,
"entity_id": entity_id,
"unique_id": entity_entry.unique_id,
"unit_of_measurement": unit,
}
self.save_data(config_entry.entry_id, entity_id, data)
async def get_data(
self, config_entry: ConfigEntry
) -> dict[str, ZWaveJSMigrationData]:
"""Return Z-Wave JS migration data for a config entry."""
await self.load_data()
data = self._data.get(config_entry.entry_id)
return data or {}
@callback
def async_map_legacy_zwave_values(
zwave_data: dict[str, ZWaveMigrationData],
zwave_js_data: dict[str, ZWaveJSMigrationData],
) -> LegacyZWaveMappedData:
"""Map Z-Wave node values onto Z-Wave JS node values."""
migration_map = LegacyZWaveMappedData()
zwave_proc_data: dict[
tuple[int, int, int, str, str | None, str | None],
ZWaveMigrationData | None,
] = {}
zwave_js_proc_data: dict[
tuple[int, int, int, str, str | None, str | None],
ZWaveJSMigrationData | None,
] = {}
for zwave_item in zwave_data.values():
zwave_js_property_name = CC_ID_LABEL_TO_PROPERTY.get(
zwave_item["command_class"], {}
).get(zwave_item["command_class_label"])
item_id = (
zwave_item["node_id"],
zwave_item["command_class"],
zwave_item["node_instance"] - 1,
zwave_item["domain"],
zwave_item["unit_of_measurement"],
zwave_js_property_name,
)
# Filter out duplicates that are not resolvable.
if item_id in zwave_proc_data:
zwave_proc_data[item_id] = None
continue
zwave_proc_data[item_id] = zwave_item
for zwave_js_item in zwave_js_data.values():
# Only identify with property name if there is a command class label map.
if zwave_js_item["command_class"] in CC_ID_LABEL_TO_PROPERTY:
zwave_js_property_name = zwave_js_item["value_property_name"]
else:
zwave_js_property_name = None
item_id = (
zwave_js_item["node_id"],
zwave_js_item["command_class"],
zwave_js_item["endpoint_index"],
zwave_js_item["domain"],
zwave_js_item["unit_of_measurement"],
zwave_js_property_name,
)
# Filter out duplicates that are not resolvable.
if item_id in zwave_js_proc_data:
zwave_js_proc_data[item_id] = None
continue
zwave_js_proc_data[item_id] = zwave_js_item
for item_id, zwave_entry in zwave_proc_data.items():
zwave_js_entry = zwave_js_proc_data.pop(item_id, None)
if zwave_entry is None or zwave_js_entry is None:
continue
migration_map.entity_entries[zwave_js_entry["entity_id"]] = zwave_entry
migration_map.device_entries[zwave_js_entry["device_id"]] = zwave_entry[
"device_id"
]
return migration_map
async def async_migrate_legacy_zwave(
hass: HomeAssistant,
zwave_config_entry: ConfigEntry,
zwave_js_config_entry: ConfigEntry,
migration_map: LegacyZWaveMappedData,
) -> None:
"""Perform Z-Wave to Z-Wave JS migration."""
dev_reg = async_get_device_registry(hass)
for zwave_js_device_id, zwave_device_id in migration_map.device_entries.items():
zwave_device_entry = dev_reg.async_get(zwave_device_id)
if not zwave_device_entry:
continue
dev_reg.async_update_device(
zwave_js_device_id,
area_id=zwave_device_entry.area_id,
name_by_user=zwave_device_entry.name_by_user,
)
ent_reg = async_get_entity_registry(hass)
for zwave_js_entity_id, zwave_entry in migration_map.entity_entries.items():
zwave_entity_id = zwave_entry["entity_id"]
entity_entry = ent_reg.async_get(zwave_entity_id)
if not entity_entry:
continue
ent_reg.async_remove(zwave_entity_id)
ent_reg.async_update_entity(
zwave_js_entity_id,
new_entity_id=entity_entry.entity_id,
name=entity_entry.name,
icon=entity_entry.icon,
)
await hass.config_entries.async_remove(zwave_config_entry.entry_id)
updates = {
**zwave_js_config_entry.data,
MIGRATED: True,
}
hass.config_entries.async_update_entry(zwave_js_config_entry, data=updates)
@dataclass @dataclass
class ValueID: class ValueID:

View File

@ -134,14 +134,14 @@ IGNORE_VIOLATIONS = {
# Demo # Demo
("demo", "manual"), ("demo", "manual"),
("demo", "openalpr_local"), ("demo", "openalpr_local"),
# Migration wizard from zwave to ozw.
"ozw",
# Migration of settings from zeroconf to network # Migration of settings from zeroconf to network
("network", "zeroconf"), ("network", "zeroconf"),
# This should become a helper method that integrations can submit data to # This should become a helper method that integrations can submit data to
("websocket_api", "lovelace"), ("websocket_api", "lovelace"),
("websocket_api", "shopping_list"), ("websocket_api", "shopping_list"),
"logbook", "logbook",
# Migration wizard from zwave to zwave_js.
"zwave_js",
} }

View File

@ -512,49 +512,3 @@ async def test_discovery_addon_not_installed(
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "start_addon" 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_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_entry.mock_calls) == 1

View File

@ -1,285 +0,0 @@
"""Test zwave to ozw migration."""
from unittest.mock import patch
import pytest
from homeassistant.components.ozw.websocket_api import ID, TYPE
from homeassistant.helpers import device_registry as dr, entity_registry as er
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 = dr.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 = er.RegistryEntry(
entity_id=ZWAVE_SOURCE_ENTITY,
unique_id=ZWAVE_SOURCE_NODE_UNIQUE_ID,
platform="zwave",
name="Z-Wave Source Node",
)
zwave_battery_device = dr.DeviceEntry(
id=ZWAVE_BATTERY_DEVICE_ID,
name_by_user=ZWAVE_BATTERY_DEVICE_NAME,
area_id=ZWAVE_BATTERY_DEVICE_AREA,
)
zwave_battery_entry = er.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 = dr.DeviceEntry(
id=ZWAVE_POWER_DEVICE_ID,
name_by_user=ZWAVE_POWER_DEVICE_NAME,
area_id=ZWAVE_POWER_DEVICE_AREA,
)
zwave_power_entry = er.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 = dr.async_get(hass)
ent_reg = er.async_get(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 = er.async_get(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

@ -15,8 +15,7 @@ from homeassistant.components.zwave import (
DATA_NETWORK, DATA_NETWORK,
const, const,
) )
from homeassistant.components.zwave.binary_sensor import get_device from homeassistant.const import ATTR_NAME
from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -1854,38 +1853,6 @@ async def test_remove_association(hass, mock_openzwave, zwave_setup_ready):
assert group.remove_association.mock_calls[0][1][1] == 5 assert group.remove_association.mock_calls[0][1][1] == 5
async def test_refresh_entity(hass, mock_openzwave, zwave_setup_ready):
"""Test zwave refresh_entity service."""
node = MockNode()
value = MockValue(
data=False, node=node, command_class=const.COMMAND_CLASS_SENSOR_BINARY
)
power_value = MockValue(data=50, node=node, command_class=const.COMMAND_CLASS_METER)
values = MockEntityValues(primary=value, power=power_value)
device = get_device(node=node, values=values, node_config={})
device.hass = hass
device.entity_id = "binary_sensor.mock_entity_id"
await device.async_added_to_hass()
await hass.async_block_till_done()
await hass.services.async_call(
"zwave", "refresh_entity", {ATTR_ENTITY_ID: "binary_sensor.mock_entity_id"}
)
await hass.async_block_till_done()
assert node.refresh_value.called
assert len(node.refresh_value.mock_calls) == 2
assert (
sorted(
[
node.refresh_value.mock_calls[0][1][0],
node.refresh_value.mock_calls[1][1][0],
]
)
== sorted([value.value_id, power_value.value_id])
)
async def test_refresh_node(hass, mock_openzwave, zwave_setup_ready): async def test_refresh_node(hass, mock_openzwave, zwave_setup_ready):
"""Test zwave refresh_node service.""" """Test zwave refresh_node service."""
zwave_network = hass.data[DATA_NETWORK] zwave_network = hass.data[DATA_NETWORK]

View File

@ -44,8 +44,8 @@ async def test_zwave_ws_api(hass, mock_openzwave, hass_ws_client):
assert result[CONF_POLLING_INTERVAL] == 6000 assert result[CONF_POLLING_INTERVAL] == 6000
async def test_zwave_ozw_migration_api(hass, mock_openzwave, hass_ws_client): async def test_zwave_zwave_js_migration_api(hass, mock_openzwave, hass_ws_client):
"""Test Z-Wave to OpenZWave websocket migration API.""" """Test Z-Wave to Z-Wave JS websocket migration API."""
await async_setup_component( await async_setup_component(
hass, hass,
@ -76,14 +76,14 @@ async def test_zwave_ozw_migration_api(hass, mock_openzwave, hass_ws_client):
) as async_init: ) as async_init:
async_init.return_value = {"flow_id": "mock_flow_id"} async_init.return_value = {"flow_id": "mock_flow_id"}
await client.send_json({ID: 7, TYPE: "zwave/start_ozw_config_flow"}) await client.send_json({ID: 7, TYPE: "zwave/start_zwave_js_config_flow"})
msg = await client.receive_json() msg = await client.receive_json()
result = msg["result"] result = msg["result"]
assert result["flow_id"] == "mock_flow_id" assert result["flow_id"] == "mock_flow_id"
assert async_init.call_args == call( assert async_init.call_args == call(
"ozw", "zwave_js",
context={"source": config_entries.SOURCE_IMPORT}, context={"source": config_entries.SOURCE_IMPORT},
data={"usb_path": "/dev/zwave", "network_key": NETWORK_KEY}, data={"usb_path": "/dev/zwave", "network_key": NETWORK_KEY},
) )

View File

@ -1,4 +1,5 @@
"""Test the Z-Wave JS Websocket API.""" """Test the Z-Wave JS Websocket API."""
from copy import deepcopy
import json import json
from unittest.mock import patch from unittest.mock import patch
@ -17,6 +18,7 @@ from zwave_js_server.exceptions import (
NotFoundError, NotFoundError,
SetValueFailed, SetValueFailed,
) )
from zwave_js_server.model.node import Node
from zwave_js_server.model.value import _get_value_id_from_dict, get_value_id from zwave_js_server.model.value import _get_value_id_from_dict, get_value_id
from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.components.websocket_api.const import ERR_NOT_FOUND
@ -78,6 +80,51 @@ async def test_network_status(hass, integration, hass_ws_client):
assert msg["error"]["code"] == ERR_NOT_LOADED assert msg["error"]["code"] == ERR_NOT_LOADED
async def test_node_ready(
hass,
multisensor_6_state,
client,
integration,
hass_ws_client,
):
"""Test the node ready websocket command."""
entry = integration
ws_client = await hass_ws_client(hass)
node_data = deepcopy(multisensor_6_state) # Copy to allow modification in tests.
node = Node(client, node_data)
node.data["ready"] = False
client.driver.controller.nodes[node.node_id] = node
await ws_client.send_json(
{
ID: 3,
TYPE: "zwave_js/node_ready",
ENTRY_ID: entry.entry_id,
"node_id": node.node_id,
}
)
msg = await ws_client.receive_json()
assert msg["success"]
node.data["ready"] = True
event = Event(
"ready",
{
"source": "node",
"event": "ready",
"nodeId": node.node_id,
"nodeState": node.data,
},
)
node.receive_event(event)
await hass.async_block_till_done()
msg = await ws_client.receive_json()
assert msg["event"]["event"] == "ready"
async def test_node_status(hass, multisensor_6, integration, hass_ws_client): async def test_node_status(hass, multisensor_6, integration, hass_ws_client):
"""Test the node status websocket command.""" """Test the node status websocket command."""
entry = integration entry = integration

View File

@ -2053,3 +2053,71 @@ async def test_options_addon_not_installed(
assert entry.data["integration_created_addon"] is True assert entry.data["integration_created_addon"] is True
assert client.connect.call_count == 2 assert client.connect.call_count == 2
assert client.disconnect.call_count == 1 assert client.disconnect.call_count == 1
@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}])
async def test_import_addon_installed(
hass,
supervisor,
addon_installed,
addon_options,
set_addon_options,
start_addon,
get_addon_discovery_info,
):
"""Test import step while add-on already installed on Supervisor."""
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"] == "configure_addon"
# the default input should be the imported data
default_input = result["data_schema"]({})
result = await hass.config_entries.flow.async_configure(
result["flow_id"], default_input
)
assert set_addon_options.call_args == call(
hass,
"core_zwave_js",
{"options": {"device": "/test/imported", "network_key": "imported123"}},
)
assert result["type"] == "progress"
assert result["step_id"] == "start_addon"
with patch(
"homeassistant.components.zwave_js.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.zwave_js.async_setup_entry",
return_value=True,
) as mock_setup_entry:
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_configure(result["flow_id"])
await hass.async_block_till_done()
assert start_addon.call_args == call(hass, "core_zwave_js")
assert result["type"] == "create_entry"
assert result["title"] == TITLE
assert result["data"] == {
"url": "ws://host1:3001",
"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

@ -1,15 +1,443 @@
"""Test the Z-Wave JS migration module.""" """Test the Z-Wave JS migration module."""
import copy import copy
from unittest.mock import patch
import pytest import pytest
from zwave_js_server.model.node import Node from zwave_js_server.model.node import Node
from homeassistant.components.zwave_js.api import ENTRY_ID, ID, TYPE
from homeassistant.components.zwave_js.const import DOMAIN from homeassistant.components.zwave_js.const import DOMAIN
from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.components.zwave_js.helpers import get_device_id
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from .common import AIR_TEMPERATURE_SENSOR, NOTIFICATION_MOTION_BINARY_SENSOR from .common import AIR_TEMPERATURE_SENSOR, NOTIFICATION_MOTION_BINARY_SENSOR
from tests.common import MockConfigEntry, mock_device_registry, mock_registry
# Switch device
ZWAVE_SWITCH_DEVICE_ID = "zwave_switch_device_id"
ZWAVE_SWITCH_DEVICE_NAME = "Z-Wave Switch Device"
ZWAVE_SWITCH_DEVICE_AREA = "Z-Wave Switch Area"
ZWAVE_SWITCH_ENTITY = "switch.zwave_switch_node"
ZWAVE_SWITCH_UNIQUE_ID = "102-6789"
ZWAVE_SWITCH_NAME = "Z-Wave Switch"
ZWAVE_SWITCH_ICON = "mdi:zwave-test-switch"
ZWAVE_POWER_ENTITY = "sensor.zwave_power"
ZWAVE_POWER_UNIQUE_ID = "102-5678"
ZWAVE_POWER_NAME = "Z-Wave Power"
ZWAVE_POWER_ICON = "mdi:zwave-test-power"
# Multisensor device
ZWAVE_MULTISENSOR_DEVICE_ID = "zwave_multisensor_device_id"
ZWAVE_MULTISENSOR_DEVICE_NAME = "Z-Wave Multisensor Device"
ZWAVE_MULTISENSOR_DEVICE_AREA = "Z-Wave Multisensor Area"
ZWAVE_SOURCE_NODE_ENTITY = "sensor.zwave_source_node"
ZWAVE_SOURCE_NODE_UNIQUE_ID = "52-4321"
ZWAVE_BATTERY_ENTITY = "sensor.zwave_battery_level"
ZWAVE_BATTERY_UNIQUE_ID = "52-1234"
ZWAVE_BATTERY_NAME = "Z-Wave Battery Level"
ZWAVE_BATTERY_ICON = "mdi:zwave-test-battery"
ZWAVE_TAMPERING_ENTITY = "sensor.zwave_tampering"
ZWAVE_TAMPERING_UNIQUE_ID = "52-3456"
ZWAVE_TAMPERING_NAME = "Z-Wave Tampering"
ZWAVE_TAMPERING_ICON = "mdi:zwave-test-tampering"
@pytest.fixture(name="zwave_migration_data")
def zwave_migration_data_fixture(hass):
"""Return mock zwave migration data."""
zwave_switch_device = dr.DeviceEntry(
id=ZWAVE_SWITCH_DEVICE_ID,
name_by_user=ZWAVE_SWITCH_DEVICE_NAME,
area_id=ZWAVE_SWITCH_DEVICE_AREA,
)
zwave_switch_entry = er.RegistryEntry(
entity_id=ZWAVE_SWITCH_ENTITY,
unique_id=ZWAVE_SWITCH_UNIQUE_ID,
platform="zwave",
name=ZWAVE_SWITCH_NAME,
icon=ZWAVE_SWITCH_ICON,
)
zwave_multisensor_device = dr.DeviceEntry(
id=ZWAVE_MULTISENSOR_DEVICE_ID,
name_by_user=ZWAVE_MULTISENSOR_DEVICE_NAME,
area_id=ZWAVE_MULTISENSOR_DEVICE_AREA,
)
zwave_source_node_entry = er.RegistryEntry(
entity_id=ZWAVE_SOURCE_NODE_ENTITY,
unique_id=ZWAVE_SOURCE_NODE_UNIQUE_ID,
platform="zwave",
name="Z-Wave Source Node",
)
zwave_battery_entry = er.RegistryEntry(
entity_id=ZWAVE_BATTERY_ENTITY,
unique_id=ZWAVE_BATTERY_UNIQUE_ID,
platform="zwave",
name=ZWAVE_BATTERY_NAME,
icon=ZWAVE_BATTERY_ICON,
unit_of_measurement="%",
)
zwave_power_entry = er.RegistryEntry(
entity_id=ZWAVE_POWER_ENTITY,
unique_id=ZWAVE_POWER_UNIQUE_ID,
platform="zwave",
name=ZWAVE_POWER_NAME,
icon=ZWAVE_POWER_ICON,
unit_of_measurement="W",
)
zwave_tampering_entry = er.RegistryEntry(
entity_id=ZWAVE_TAMPERING_ENTITY,
unique_id=ZWAVE_TAMPERING_UNIQUE_ID,
platform="zwave",
name=ZWAVE_TAMPERING_NAME,
icon=ZWAVE_TAMPERING_ICON,
unit_of_measurement="", # Test empty string unit normalization.
)
zwave_migration_data = {
ZWAVE_SWITCH_ENTITY: {
"node_id": 102,
"node_instance": 1,
"command_class": 37,
"command_class_label": "",
"value_index": 1,
"device_id": zwave_switch_device.id,
"domain": zwave_switch_entry.domain,
"entity_id": zwave_switch_entry.entity_id,
"unique_id": ZWAVE_SWITCH_UNIQUE_ID,
"unit_of_measurement": zwave_switch_entry.unit_of_measurement,
},
ZWAVE_POWER_ENTITY: {
"node_id": 102,
"node_instance": 1,
"command_class": 50,
"command_class_label": "Power",
"value_index": 8,
"device_id": zwave_switch_device.id,
"domain": zwave_power_entry.domain,
"entity_id": zwave_power_entry.entity_id,
"unique_id": ZWAVE_POWER_UNIQUE_ID,
"unit_of_measurement": zwave_power_entry.unit_of_measurement,
},
ZWAVE_SOURCE_NODE_ENTITY: {
"node_id": 52,
"node_instance": 1,
"command_class": 113,
"command_class_label": "SourceNodeId",
"value_index": 1,
"device_id": zwave_multisensor_device.id,
"domain": zwave_source_node_entry.domain,
"entity_id": zwave_source_node_entry.entity_id,
"unique_id": ZWAVE_SOURCE_NODE_UNIQUE_ID,
"unit_of_measurement": zwave_source_node_entry.unit_of_measurement,
},
ZWAVE_BATTERY_ENTITY: {
"node_id": 52,
"node_instance": 1,
"command_class": 128,
"command_class_label": "Battery Level",
"value_index": 0,
"device_id": zwave_multisensor_device.id,
"domain": zwave_battery_entry.domain,
"entity_id": zwave_battery_entry.entity_id,
"unique_id": ZWAVE_BATTERY_UNIQUE_ID,
"unit_of_measurement": zwave_battery_entry.unit_of_measurement,
},
ZWAVE_TAMPERING_ENTITY: {
"node_id": 52,
"node_instance": 1,
"command_class": 113,
"command_class_label": "Burglar",
"value_index": 10,
"device_id": zwave_multisensor_device.id,
"domain": zwave_tampering_entry.domain,
"entity_id": zwave_tampering_entry.entity_id,
"unique_id": ZWAVE_TAMPERING_UNIQUE_ID,
"unit_of_measurement": zwave_tampering_entry.unit_of_measurement,
},
}
mock_device_registry(
hass,
{
zwave_switch_device.id: zwave_switch_device,
zwave_multisensor_device.id: zwave_multisensor_device,
},
)
mock_registry(
hass,
{
ZWAVE_SWITCH_ENTITY: zwave_switch_entry,
ZWAVE_SOURCE_NODE_ENTITY: zwave_source_node_entry,
ZWAVE_BATTERY_ENTITY: zwave_battery_entry,
ZWAVE_POWER_ENTITY: zwave_power_entry,
ZWAVE_TAMPERING_ENTITY: zwave_tampering_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_migration_data",
return_value=zwave_migration_data,
):
yield zwave_config_entry
async def test_migrate_zwave(
hass,
zwave_integration,
aeon_smart_switch_6,
multisensor_6,
integration,
hass_ws_client,
):
"""Test the Z-Wave to Z-Wave JS migration websocket api."""
entry = integration
client = await hass_ws_client(hass)
assert hass.config_entries.async_entries("zwave")
await client.send_json(
{
ID: 5,
TYPE: "zwave_js/migrate_zwave",
ENTRY_ID: entry.entry_id,
"dry_run": False,
}
)
msg = await client.receive_json()
result = msg["result"]
migration_entity_map = {
ZWAVE_SWITCH_ENTITY: "switch.smart_switch_6",
ZWAVE_BATTERY_ENTITY: "sensor.multisensor_6_battery_level",
}
assert result["zwave_entity_ids"] == [
ZWAVE_SWITCH_ENTITY,
ZWAVE_POWER_ENTITY,
ZWAVE_SOURCE_NODE_ENTITY,
ZWAVE_BATTERY_ENTITY,
ZWAVE_TAMPERING_ENTITY,
]
expected_zwave_js_entities = [
"switch.smart_switch_6",
"sensor.multisensor_6_air_temperature",
"sensor.multisensor_6_illuminance",
"sensor.multisensor_6_humidity",
"sensor.multisensor_6_ultraviolet",
"binary_sensor.multisensor_6_home_security_tampering_product_cover_removed",
"binary_sensor.multisensor_6_home_security_motion_detection",
"sensor.multisensor_6_battery_level",
"binary_sensor.multisensor_6_low_battery_level",
"light.smart_switch_6",
"sensor.smart_switch_6_electric_consumed_kwh",
"sensor.smart_switch_6_electric_consumed_w",
"sensor.smart_switch_6_electric_consumed_v",
"sensor.smart_switch_6_electric_consumed_a",
]
# Assert that both lists have the same items without checking order
assert not set(result["zwave_js_entity_ids"]) ^ set(expected_zwave_js_entities)
assert result["migration_entity_map"] == migration_entity_map
assert result["migrated"] is True
dev_reg = dr.async_get(hass)
ent_reg = er.async_get(hass)
# check the device registry migration
# check that the migrated entries have correct attributes
multisensor_device_entry = dev_reg.async_get_device(
identifiers={("zwave_js", "3245146787-52")}, connections=set()
)
assert multisensor_device_entry
assert multisensor_device_entry.name_by_user == ZWAVE_MULTISENSOR_DEVICE_NAME
assert multisensor_device_entry.area_id == ZWAVE_MULTISENSOR_DEVICE_AREA
switch_device_entry = dev_reg.async_get_device(
identifiers={("zwave_js", "3245146787-102")}, connections=set()
)
assert switch_device_entry
assert switch_device_entry.name_by_user == ZWAVE_SWITCH_DEVICE_NAME
assert switch_device_entry.area_id == ZWAVE_SWITCH_DEVICE_AREA
migration_device_map = {
ZWAVE_SWITCH_DEVICE_ID: switch_device_entry.id,
ZWAVE_MULTISENSOR_DEVICE_ID: multisensor_device_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.multisensor_6_battery_level")
# these should not have been migrated and is still in the registry
assert ent_reg.async_is_registered(ZWAVE_SOURCE_NODE_ENTITY)
source_entry = ent_reg.async_get(ZWAVE_SOURCE_NODE_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(ZWAVE_TAMPERING_ENTITY)
tampering_entry = ent_reg.async_get(ZWAVE_TAMPERING_ENTITY)
assert tampering_entry.unique_id == ZWAVE_TAMPERING_UNIQUE_ID
assert ent_reg.async_is_registered("sensor.smart_switch_6_electric_consumed_w")
# this is the new entity_ids of the zwave_js entities
assert ent_reg.async_is_registered(ZWAVE_SWITCH_ENTITY)
assert ent_reg.async_is_registered(ZWAVE_BATTERY_ENTITY)
# check that the migrated entries have correct attributes
switch_entry = ent_reg.async_get(ZWAVE_SWITCH_ENTITY)
assert switch_entry
assert switch_entry.unique_id == "3245146787.102-37-0-currentValue"
assert switch_entry.name == ZWAVE_SWITCH_NAME
assert switch_entry.icon == ZWAVE_SWITCH_ICON
battery_entry = ent_reg.async_get(ZWAVE_BATTERY_ENTITY)
assert battery_entry
assert battery_entry.unique_id == "3245146787.52-128-0-level"
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,
zwave_integration,
aeon_smart_switch_6,
multisensor_6,
integration,
hass_ws_client,
):
"""Test the zwave to zwave_js migration websocket api dry run."""
entry = integration
client = await hass_ws_client(hass)
await client.send_json(
{ID: 5, TYPE: "zwave_js/migrate_zwave", ENTRY_ID: entry.entry_id}
)
msg = await client.receive_json()
result = msg["result"]
migration_entity_map = {
ZWAVE_SWITCH_ENTITY: "switch.smart_switch_6",
ZWAVE_BATTERY_ENTITY: "sensor.multisensor_6_battery_level",
}
assert result["zwave_entity_ids"] == [
ZWAVE_SWITCH_ENTITY,
ZWAVE_POWER_ENTITY,
ZWAVE_SOURCE_NODE_ENTITY,
ZWAVE_BATTERY_ENTITY,
ZWAVE_TAMPERING_ENTITY,
]
expected_zwave_js_entities = [
"switch.smart_switch_6",
"sensor.multisensor_6_air_temperature",
"sensor.multisensor_6_illuminance",
"sensor.multisensor_6_humidity",
"sensor.multisensor_6_ultraviolet",
"binary_sensor.multisensor_6_home_security_tampering_product_cover_removed",
"binary_sensor.multisensor_6_home_security_motion_detection",
"sensor.multisensor_6_battery_level",
"binary_sensor.multisensor_6_low_battery_level",
"light.smart_switch_6",
"sensor.smart_switch_6_electric_consumed_kwh",
"sensor.smart_switch_6_electric_consumed_w",
"sensor.smart_switch_6_electric_consumed_v",
"sensor.smart_switch_6_electric_consumed_a",
]
# Assert that both lists have the same items without checking order
assert not set(result["zwave_js_entity_ids"]) ^ set(expected_zwave_js_entities)
assert result["migration_entity_map"] == migration_entity_map
dev_reg = dr.async_get(hass)
multisensor_device_entry = dev_reg.async_get_device(
identifiers={("zwave_js", "3245146787-52")}, connections=set()
)
assert multisensor_device_entry
assert multisensor_device_entry.name_by_user is None
assert multisensor_device_entry.area_id is None
switch_device_entry = dev_reg.async_get_device(
identifiers={("zwave_js", "3245146787-102")}, connections=set()
)
assert switch_device_entry
assert switch_device_entry.name_by_user is None
assert switch_device_entry.area_id is None
migration_device_map = {
ZWAVE_SWITCH_DEVICE_ID: switch_device_entry.id,
ZWAVE_MULTISENSOR_DEVICE_ID: multisensor_device_entry.id,
}
assert result["migration_device_map"] == migration_device_map
assert result["migrated"] is False
ent_reg = er.async_get(hass)
# no real migration should have been done
assert ent_reg.async_is_registered("switch.smart_switch_6")
assert ent_reg.async_is_registered("sensor.multisensor_6_battery_level")
assert ent_reg.async_is_registered("sensor.smart_switch_6_electric_consumed_w")
assert ent_reg.async_is_registered(ZWAVE_SOURCE_NODE_ENTITY)
source_entry = ent_reg.async_get(ZWAVE_SOURCE_NODE_ENTITY)
assert source_entry
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
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
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, aeon_smart_switch_6, multisensor_6, integration, hass_ws_client
):
"""Test the zwave to zwave_js migration websocket without zwave setup."""
entry = integration
client = await hass_ws_client(hass)
await client.send_json(
{ID: 5, TYPE: "zwave_js/migrate_zwave", ENTRY_ID: entry.entry_id}
)
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"
async def test_unique_id_migration_dupes( async def test_unique_id_migration_dupes(
hass, multisensor_6_state, client, integration hass, multisensor_6_state, client, integration