Deprecate UniFi Protect HDR switch and package sensor (#113636)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Christopher Bailey 2024-03-16 22:15:32 -04:00 committed by GitHub
parent 0a26829ffc
commit 43652a4ace
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 305 additions and 321 deletions

View file

@ -458,6 +458,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
name="Package Detected",
icon="mdi:package-variant-closed",
ufp_value="is_package_currently_detected",
entity_registry_enabled_default=False,
ufp_required_field="can_detect_package",
ufp_enabled="is_package_detection_on",
ufp_event_obj="last_package_detect_event",

View file

@ -2,19 +2,103 @@
from __future__ import annotations
from itertools import chain
import logging
from pyunifiprotect import ProtectApiClient
from pyunifiprotect.data import NVR, Bootstrap, ProtectAdoptableDeviceModel
from pyunifiprotect.data import Bootstrap
from typing_extensions import TypedDict
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.script import scripts_with_entity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.helpers.issue_registry import IssueSeverity
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class EntityRef(TypedDict):
"""Entity ref parameter variable."""
id: str
platform: Platform
class EntityUsage(TypedDict):
"""Entity usages response variable."""
automations: dict[str, list[str]]
scripts: dict[str, list[str]]
@callback
def check_if_used(
hass: HomeAssistant, entry: ConfigEntry, entities: dict[str, EntityRef]
) -> dict[str, EntityUsage]:
"""Check for usages of entities and return them."""
entity_registry = er.async_get(hass)
refs: dict[str, EntityUsage] = {
ref: {"automations": {}, "scripts": {}} for ref in entities
}
for entity in er.async_entries_for_config_entry(entity_registry, entry.entry_id):
for ref_id, ref in entities.items():
if (
entity.domain == ref["platform"]
and entity.disabled_by is None
and ref["id"] in entity.unique_id
):
entity_automations = automations_with_entity(hass, entity.entity_id)
entity_scripts = scripts_with_entity(hass, entity.entity_id)
if entity_automations:
refs[ref_id]["automations"][entity.entity_id] = entity_automations
if entity_scripts:
refs[ref_id]["scripts"][entity.entity_id] = entity_scripts
return refs
@callback
def create_repair_if_used(
hass: HomeAssistant,
entry: ConfigEntry,
breaks_in: str,
entities: dict[str, EntityRef],
) -> None:
"""Create repairs for used entities that are deprecated."""
usages = check_if_used(hass, entry, entities)
for ref_id, refs in usages.items():
issue_id = f"deprecate_{ref_id}"
automations = refs["automations"]
scripts = refs["scripts"]
if automations or scripts:
items = sorted(
set(chain.from_iterable(chain(automations.values(), scripts.values())))
)
ir.async_create_issue(
hass,
DOMAIN,
issue_id,
is_fixable=False,
breaks_in_ha_version=breaks_in,
severity=IssueSeverity.WARNING,
translation_key=issue_id,
translation_placeholders={
"items": "* `" + "`\n* `".join(items) + "`\n"
},
)
else:
_LOGGER.debug("No found usages of %s", ref_id)
ir.async_delete_issue(hass, DOMAIN, issue_id)
async def async_migrate_data(
hass: HomeAssistant,
entry: ConfigEntry,
@ -23,132 +107,32 @@ async def async_migrate_data(
) -> None:
"""Run all valid UniFi Protect data migrations."""
_LOGGER.debug("Start Migrate: async_migrate_buttons")
await async_migrate_buttons(hass, entry, protect, bootstrap)
_LOGGER.debug("Completed Migrate: async_migrate_buttons")
_LOGGER.debug("Start Migrate: async_migrate_device_ids")
await async_migrate_device_ids(hass, entry, protect, bootstrap)
_LOGGER.debug("Completed Migrate: async_migrate_device_ids")
_LOGGER.debug("Start Migrate: async_deprecate_hdr_package")
async_deprecate_hdr_package(hass, entry)
_LOGGER.debug("Completed Migrate: async_deprecate_hdr_package")
async def async_migrate_buttons(
hass: HomeAssistant,
entry: ConfigEntry,
protect: ProtectApiClient,
bootstrap: Bootstrap,
) -> None:
"""Migrate existing Reboot button unique IDs from {device_id} to {deivce_id}_reboot.
@callback
def async_deprecate_hdr_package(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Check for usages of hdr_mode switch and package sensor and raise repair if it is used.
This allows for additional types of buttons that are outside of just a reboot button.
UniFi Protect v3.0.22 changed how HDR works so it is no longer a simple on/off toggle. There is
Always On, Always Off and Auto. So it has been migrated to a select. The old switch is now deprecated.
Added in 2022.6.0.
Additionally, the Package sensor is no longer functional due to how events work so a repair to notify users.
Added in 2024.4.0
"""
registry = er.async_get(hass)
to_migrate = []
for entity in er.async_entries_for_config_entry(registry, entry.entry_id):
if entity.domain == Platform.BUTTON and "_" not in entity.unique_id:
_LOGGER.debug("Button %s needs migration", entity.entity_id)
to_migrate.append(entity)
if len(to_migrate) == 0:
_LOGGER.debug("No button entities need migration")
return
count = 0
for button in to_migrate:
device = bootstrap.get_device_from_id(button.unique_id)
if device is None:
continue
new_unique_id = f"{device.id}_reboot"
_LOGGER.debug(
"Migrating entity %s (old unique_id: %s, new unique_id: %s)",
button.entity_id,
button.unique_id,
new_unique_id,
)
try:
registry.async_update_entity(button.entity_id, new_unique_id=new_unique_id)
except ValueError:
_LOGGER.warning(
"Could not migrate entity %s (old unique_id: %s, new unique_id: %s)",
button.entity_id,
button.unique_id,
new_unique_id,
)
else:
count += 1
if count < len(to_migrate):
_LOGGER.warning("Failed to migate %s reboot buttons", len(to_migrate) - count)
async def async_migrate_device_ids(
hass: HomeAssistant,
entry: ConfigEntry,
protect: ProtectApiClient,
bootstrap: Bootstrap,
) -> None:
"""Migrate unique IDs from {device_id}_{name} format to {mac}_{name} format.
This makes devices persist better with in HA. Anything a device is unadopted/readopted or
the Protect instance has to rebuild the disk array, the device IDs of Protect devices
can change. This causes a ton of orphaned entities and loss of historical data. MAC
addresses are the one persistent identifier a device has that does not change.
Added in 2022.7.0.
"""
registry = er.async_get(hass)
to_migrate = []
for entity in er.async_entries_for_config_entry(registry, entry.entry_id):
parts = entity.unique_id.split("_")
# device ID = 24 characters, MAC = 12
if len(parts[0]) == 24:
_LOGGER.debug("Entity %s needs migration", entity.entity_id)
to_migrate.append(entity)
if len(to_migrate) == 0:
_LOGGER.debug("No entities need migration to MAC address ID")
return
count = 0
for entity in to_migrate:
parts = entity.unique_id.split("_")
if parts[0] == bootstrap.nvr.id:
device: NVR | ProtectAdoptableDeviceModel | None = bootstrap.nvr
else:
device = bootstrap.get_device_from_id(parts[0])
if device is None:
continue
new_unique_id = device.mac
if len(parts) > 1:
new_unique_id = f"{device.mac}_{'_'.join(parts[1:])}"
_LOGGER.debug(
"Migrating entity %s (old unique_id: %s, new unique_id: %s)",
entity.entity_id,
entity.unique_id,
new_unique_id,
)
try:
registry.async_update_entity(entity.entity_id, new_unique_id=new_unique_id)
except ValueError as err:
_LOGGER.warning(
(
"Could not migrate entity %s (old unique_id: %s, new unique_id:"
" %s): %s"
),
entity.entity_id,
entity.unique_id,
new_unique_id,
err,
)
else:
count += 1
if count < len(to_migrate):
_LOGGER.warning("Failed to migrate %s entities", len(to_migrate) - count)
create_repair_if_used(
hass,
entry,
"2024.10.0",
{
"hdr_switch": {"id": "hdr_mode", "platform": Platform.SWITCH},
"package_sensor": {
"id": "smart_obj_package",
"platform": Platform.BINARY_SENSOR,
},
},
)

View file

@ -90,6 +90,14 @@
}
}
}
},
"deprecate_hdr_switch": {
"title": "HDR Mode Switch Deprecated",
"description": "UniFi Protect v3 added a new state for HDR (auto). As a result, the HDR Mode Switch has been replaced with an HDR Mode Select, and it is deprecated.\n\nBelow are the detected automations or scripts that use one or more of the deprecated entities:\n{items}\nThe above list may be incomplete and it does not include any template usages inside of dashboards. Please update any templates, automations or scripts accordingly."
},
"deprecate_package_sensor": {
"title": "Package Event Sensor Deprecated",
"description": "The package event sensor never tripped because of the way events are reported in UniFi Protect. As a result, the sensor is deprecated and will be removed.\n\nBelow are the detected automations or scripts that use one or more of the deprecated entities:\n{items}\nThe above list may be incomplete and it does not include any template usages inside of dashboards. Please update any templates, automations or scripts accordingly."
}
},
"entity": {

View file

@ -5,7 +5,7 @@
"canAutoUpdate": true,
"isStatsGatheringEnabled": true,
"timezone": "America/New_York",
"version": "1.21.0-beta.2",
"version": "2.2.6",
"ucoreVersion": "2.3.26",
"firmwareVersion": "2.3.10",
"uiVersion": null,
@ -40,7 +40,7 @@
"enableStatsReporting": false,
"isSshEnabled": false,
"errorCode": null,
"releaseChannel": "beta",
"releaseChannel": "release",
"ssoChannel": null,
"hosts": ["192.168.216.198"],
"enableBridgeAutoAdoption": true,

View file

@ -2,238 +2,225 @@
from __future__ import annotations
from unittest.mock import AsyncMock
from unittest.mock import patch
from pyunifiprotect.data import Light
from pyunifiprotect.exceptions import NvrError
from pyunifiprotect.data import Camera
from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN
from homeassistant.components.repairs.issue_handler import (
async_process_repairs_platforms,
)
from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN
from homeassistant.components.unifiprotect.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import Platform
from homeassistant.const import SERVICE_RELOAD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from .utils import (
MockUFPFixture,
generate_random_ids,
init_entry,
regenerate_device_ids,
)
from .utils import MockUFPFixture, init_entry
from tests.typing import WebSocketGenerator
async def test_migrate_reboot_button(
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
) -> None:
"""Test migrating unique ID of reboot button."""
async def test_deprecated_entity(
hass: HomeAssistant, ufp: MockUFPFixture, hass_ws_client, doorbell: Camera
):
"""Test Deprecate entity repair does not exist by default (new installs)."""
light1 = light.copy()
light1.name = "Test Light 1"
regenerate_device_ids(light1)
await init_entry(hass, ufp, [doorbell])
light2 = light.copy()
light2.name = "Test Light 2"
regenerate_device_ids(light2)
await async_process_repairs_platforms(hass)
ws_client = await hass_ws_client(hass)
await ws_client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await ws_client.receive_json()
assert msg["success"]
issue = None
for i in msg["result"]["issues"]:
if i["issue_id"] == "deprecate_hdr_switch":
issue = i
assert issue is None
async def test_deprecated_entity_no_automations(
hass: HomeAssistant, ufp: MockUFPFixture, hass_ws_client, doorbell: Camera
):
"""Test Deprecate entity repair exists for existing installs."""
registry = er.async_get(hass)
registry.async_get_or_create(
Platform.BUTTON, DOMAIN, light1.id, config_entry=ufp.entry
)
registry.async_get_or_create(
Platform.BUTTON,
Platform.SWITCH,
DOMAIN,
f"{light2.mac}_reboot",
f"{doorbell.mac}_hdr_mode",
config_entry=ufp.entry,
)
ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap)
await init_entry(hass, ufp, [light1, light2], regenerate_ids=False)
await init_entry(hass, ufp, [doorbell])
assert ufp.entry.state == ConfigEntryState.LOADED
assert ufp.api.update.called
assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac
await async_process_repairs_platforms(hass)
ws_client = await hass_ws_client(hass)
buttons = [
entity
for entity in er.async_entries_for_config_entry(registry, ufp.entry.entry_id)
if entity.domain == Platform.BUTTON.value
]
assert len(buttons) == 4
await ws_client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await ws_client.receive_json()
assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device") is None
assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device_2") is None
light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_{light1.id.lower()}")
assert light is not None
assert light.unique_id == f"{light1.mac}_reboot"
assert msg["success"]
issue = None
for i in msg["result"]["issues"]:
if i["issue_id"] == "deprecate_hdr_switch":
issue = i
assert issue is None
assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device") is None
assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device_2") is None
light = registry.async_get(
f"{Platform.BUTTON}.unifiprotect_{light2.mac.lower()}_reboot"
async def _load_automation(hass: HomeAssistant, entity_id: str):
assert await async_setup_component(
hass,
AUTOMATION_DOMAIN,
{
AUTOMATION_DOMAIN: [
{
"alias": "test1",
"trigger": [
{"platform": "state", "entity_id": entity_id},
{
"platform": "event",
"event_type": "state_changed",
"event_data": {"entity_id": entity_id},
},
],
"condition": {
"condition": "state",
"entity_id": entity_id,
"state": "on",
},
"action": [
{
"service": "test.script",
"data": {"entity_id": entity_id},
},
],
},
]
},
)
assert light is not None
assert light.unique_id == f"{light2.mac}_reboot"
async def test_migrate_nvr_mac(
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
async def test_deprecate_entity_automation(
hass: HomeAssistant,
ufp: MockUFPFixture,
hass_ws_client: WebSocketGenerator,
doorbell: Camera,
) -> None:
"""Test migrating unique ID of NVR to use MAC address."""
light1 = light.copy()
light1.name = "Test Light 1"
regenerate_device_ids(light1)
light2 = light.copy()
light2.name = "Test Light 2"
regenerate_device_ids(light2)
nvr = ufp.api.bootstrap.nvr
regenerate_device_ids(nvr)
registry = er.async_get(hass)
registry.async_get_or_create(
Platform.SENSOR,
DOMAIN,
f"{nvr.id}_storage_utilization",
config_entry=ufp.entry,
)
ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap)
await init_entry(hass, ufp, [light1, light2], regenerate_ids=False)
assert ufp.entry.state == ConfigEntryState.LOADED
assert ufp.api.update.called
assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac
assert registry.async_get(f"{Platform.SENSOR}.{DOMAIN}_storage_utilization") is None
assert (
registry.async_get(f"{Platform.SENSOR}.{DOMAIN}_storage_utilization_2") is None
)
sensor = registry.async_get(
f"{Platform.SENSOR}.{DOMAIN}_{nvr.id}_storage_utilization"
)
assert sensor is not None
assert sensor.unique_id == f"{nvr.mac}_storage_utilization"
async def test_migrate_reboot_button_no_device(
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
) -> None:
"""Test migrating unique ID of reboot button if UniFi Protect device ID changed."""
light2_id, _ = generate_random_ids()
"""Test Deprecate entity repair exists for existing installs."""
registry = er.async_get(hass)
registry.async_get_or_create(
Platform.BUTTON, DOMAIN, light2_id, config_entry=ufp.entry
entry = registry.async_get_or_create(
Platform.SWITCH,
DOMAIN,
f"{doorbell.mac}_hdr_mode",
config_entry=ufp.entry,
)
await _load_automation(hass, entry.entity_id)
await init_entry(hass, ufp, [doorbell])
await async_process_repairs_platforms(hass)
ws_client = await hass_ws_client(hass)
await ws_client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await ws_client.receive_json()
assert msg["success"]
issue = None
for i in msg["result"]["issues"]:
if i["issue_id"] == "deprecate_hdr_switch":
issue = i
assert issue is not None
with patch(
"homeassistant.config.load_yaml_config_file",
autospec=True,
return_value={AUTOMATION_DOMAIN: []},
):
await hass.services.async_call(AUTOMATION_DOMAIN, SERVICE_RELOAD, blocking=True)
await hass.config_entries.async_reload(ufp.entry.entry_id)
await hass.async_block_till_done()
await ws_client.send_json({"id": 2, "type": "repairs/list_issues"})
msg = await ws_client.receive_json()
assert msg["success"]
issue = None
for i in msg["result"]["issues"]:
if i["issue_id"] == "deprecate_hdr_switch":
issue = i
assert issue is None
async def _load_script(hass: HomeAssistant, entity_id: str):
assert await async_setup_component(
hass,
SCRIPT_DOMAIN,
{
SCRIPT_DOMAIN: {
"test": {
"sequence": {
"service": "test.script",
"data": {"entity_id": entity_id},
}
}
},
},
)
ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap)
await init_entry(hass, ufp, [light], regenerate_ids=False)
assert ufp.entry.state == ConfigEntryState.LOADED
assert ufp.api.update.called
assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac
buttons = [
entity
for entity in er.async_entries_for_config_entry(registry, ufp.entry.entry_id)
if entity.domain == Platform.BUTTON.value
]
assert len(buttons) == 3
entity = registry.async_get(f"{Platform.BUTTON}.unifiprotect_{light2_id.lower()}")
assert entity is not None
assert entity.unique_id == light2_id
async def test_migrate_reboot_button_fail(
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
async def test_deprecate_entity_script(
hass: HomeAssistant,
ufp: MockUFPFixture,
hass_ws_client: WebSocketGenerator,
doorbell: Camera,
) -> None:
"""Test migrating unique ID of reboot button."""
"""Test Deprecate entity repair exists for existing installs."""
registry = er.async_get(hass)
registry.async_get_or_create(
Platform.BUTTON,
entry = registry.async_get_or_create(
Platform.SWITCH,
DOMAIN,
light.id,
f"{doorbell.mac}_hdr_mode",
config_entry=ufp.entry,
suggested_object_id=light.display_name,
)
registry.async_get_or_create(
Platform.BUTTON,
DOMAIN,
f"{light.id}_reboot",
config_entry=ufp.entry,
suggested_object_id=light.display_name,
)
await _load_script(hass, entry.entity_id)
await init_entry(hass, ufp, [doorbell])
ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap)
await init_entry(hass, ufp, [light], regenerate_ids=False)
await async_process_repairs_platforms(hass)
ws_client = await hass_ws_client(hass)
assert ufp.entry.state == ConfigEntryState.LOADED
assert ufp.api.update.called
assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac
await ws_client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await ws_client.receive_json()
entity = registry.async_get(f"{Platform.BUTTON}.test_light")
assert entity is not None
assert entity.unique_id == f"{light.mac}"
assert msg["success"]
issue = None
for i in msg["result"]["issues"]:
if i["issue_id"] == "deprecate_hdr_switch":
issue = i
assert issue is not None
with patch(
"homeassistant.config.load_yaml_config_file",
autospec=True,
return_value={SCRIPT_DOMAIN: {}},
):
await hass.services.async_call(SCRIPT_DOMAIN, SERVICE_RELOAD, blocking=True)
async def test_migrate_device_mac_button_fail(
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
) -> None:
"""Test migrating unique ID to MAC format."""
await hass.config_entries.async_reload(ufp.entry.entry_id)
await hass.async_block_till_done()
registry = er.async_get(hass)
registry.async_get_or_create(
Platform.BUTTON,
DOMAIN,
f"{light.id}_reboot",
config_entry=ufp.entry,
suggested_object_id=light.display_name,
)
registry.async_get_or_create(
Platform.BUTTON,
DOMAIN,
f"{light.mac}_reboot",
config_entry=ufp.entry,
suggested_object_id=light.display_name,
)
await ws_client.send_json({"id": 2, "type": "repairs/list_issues"})
msg = await ws_client.receive_json()
ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap)
await init_entry(hass, ufp, [light], regenerate_ids=False)
assert ufp.entry.state == ConfigEntryState.LOADED
assert ufp.api.update.called
assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac
entity = registry.async_get(f"{Platform.BUTTON}.test_light")
assert entity is not None
assert entity.unique_id == f"{light.id}_reboot"
async def test_migrate_device_mac_bootstrap_fail(
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
) -> None:
"""Test migrating with a network error."""
registry = er.async_get(hass)
registry.async_get_or_create(
Platform.BUTTON,
DOMAIN,
f"{light.id}_reboot",
config_entry=ufp.entry,
suggested_object_id=light.name,
)
registry.async_get_or_create(
Platform.BUTTON,
DOMAIN,
f"{light.mac}_reboot",
config_entry=ufp.entry,
suggested_object_id=light.name,
)
ufp.api.get_bootstrap = AsyncMock(side_effect=NvrError)
await init_entry(hass, ufp, [light], regenerate_ids=False)
assert ufp.entry.state == ConfigEntryState.SETUP_RETRY
assert msg["success"]
issue = None
for i in msg["result"]["issues"]:
if i["issue_id"] == "deprecate_hdr_switch":
issue = i
assert issue is None

View file

@ -32,6 +32,8 @@ async def test_ea_warning_ignore(
) -> None:
"""Test EA warning is created if using prerelease version of Protect."""
ufp.api.bootstrap.nvr.release_channel = "beta"
ufp.api.bootstrap.nvr.version = Version("1.21.0-beta.2")
version = ufp.api.bootstrap.nvr.version
assert version.is_prerelease
await init_entry(hass, ufp, [])
@ -92,6 +94,8 @@ async def test_ea_warning_fix(
) -> None:
"""Test EA warning is created if using prerelease version of Protect."""
ufp.api.bootstrap.nvr.release_channel = "beta"
ufp.api.bootstrap.nvr.version = Version("1.21.0-beta.2")
version = ufp.api.bootstrap.nvr.version
assert version.is_prerelease
await init_entry(hass, ufp, [])
@ -125,8 +129,8 @@ async def test_ea_warning_fix(
assert data["step_id"] == "start"
new_nvr = copy(ufp.api.bootstrap.nvr)
new_nvr.version = Version("2.2.6")
new_nvr.release_channel = "release"
new_nvr.version = Version("2.2.6")
mock_msg = Mock()
mock_msg.changed_data = {"version": "2.2.6", "releaseChannel": "release"}
mock_msg.new_obj = new_nvr