Skip to content

Commit

Permalink
Add zwave to ozw migration (#39081)
Browse files Browse the repository at this point in the history
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
  • Loading branch information
3 people committed Jan 9, 2021
1 parent 982c42e commit 8b72324
Show file tree
Hide file tree
Showing 14 changed files with 751 additions and 10 deletions.
3 changes: 2 additions & 1 deletion homeassistant/components/ozw/__init__.py
Expand Up @@ -36,6 +36,7 @@
DATA_UNSUBSCRIBE,
DOMAIN,
MANAGER,
NODES_VALUES,
PLATFORMS,
TOPIC_OPENZWAVE,
)
Expand Down Expand Up @@ -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}/"}

Expand Down
19 changes: 15 additions & 4 deletions homeassistant/components/ozw/config_flow.py
Expand Up @@ -37,6 +37,15 @@ def __init__(self):
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():
Expand Down Expand Up @@ -163,13 +172,15 @@ async def async_step_start_addon(self, user_input=None):
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,
}
)

Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/ozw/const.py
Expand Up @@ -25,6 +25,7 @@
SWITCH_DOMAIN,
]
MANAGER = "manager"
NODES_VALUES = "nodes_values"

# MQTT Topics
TOPIC_OPENZWAVE = "OpenZWave"
Expand All @@ -40,6 +41,9 @@
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"
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/ozw/manifest.json
Expand Up @@ -7,7 +7,8 @@
"python-openzwave-mqtt[mqtt-client]==1.4.0"
],
"after_dependencies": [
"mqtt"
"mqtt",
"zwave"
],
"codeowners": [
"@cgarwood",
Expand Down
171 changes: 171 additions & 0 deletions homeassistant/components/ozw/migration.py
@@ -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)
64 changes: 64 additions & 0 deletions homeassistant/components/ozw/websocket_api.py
@@ -1,4 +1,6 @@
"""Web socket API for OpenZWave."""
import logging

from openzwavemqtt.const import (
ATTR_CODE_SLOT,
ATTR_LABEL,
Expand All @@ -23,7 +25,11 @@

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"
Expand Down Expand Up @@ -52,6 +58,7 @@
@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)
Expand Down Expand Up @@ -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."""
Expand Down

0 comments on commit 8b72324

Please sign in to comment.