mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Normalise unique ID in Axis integration (#45203)
* Move adding unique id to config entry from setup_entry to migrate_entry * Normalise unique ID * MQTT subscribe should still use the serial number in the way the device itself expects
This commit is contained in:
parent
b3764da912
commit
598a0d19b1
@ -2,7 +2,10 @@
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import CONF_DEVICE, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.const import CONF_DEVICE, CONF_MAC, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.entity_registry import async_migrate_entries
|
||||
|
||||
from .const import DOMAIN as AXIS_DOMAIN
|
||||
from .device import AxisNetworkDevice
|
||||
@ -24,12 +27,6 @@ async def async_setup_entry(hass, config_entry):
|
||||
if not await device.async_setup():
|
||||
return False
|
||||
|
||||
# 0.104 introduced config entry unique id, this makes upgrading possible
|
||||
if config_entry.unique_id is None:
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, unique_id=device.api.vapix.serial_number
|
||||
)
|
||||
|
||||
hass.data[AXIS_DOMAIN][config_entry.unique_id] = device
|
||||
|
||||
await device.async_update_device_registry()
|
||||
@ -52,9 +49,28 @@ async def async_migrate_entry(hass, config_entry):
|
||||
# Flatten configuration but keep old data if user rollbacks HASS prior to 0.106
|
||||
if config_entry.version == 1:
|
||||
config_entry.data = {**config_entry.data, **config_entry.data[CONF_DEVICE]}
|
||||
|
||||
config_entry.unique_id = config_entry.data[CONF_MAC]
|
||||
config_entry.version = 2
|
||||
|
||||
# Normalise MAC address of device which also affects entity unique IDs
|
||||
if config_entry.version == 2:
|
||||
old_unique_id = config_entry.unique_id
|
||||
new_unique_id = format_mac(old_unique_id)
|
||||
|
||||
@callback
|
||||
def update_unique_id(entity_entry):
|
||||
"""Update unique ID of entity entry."""
|
||||
return {
|
||||
"new_unique_id": entity_entry.unique_id.replace(
|
||||
old_unique_id, new_unique_id
|
||||
)
|
||||
}
|
||||
|
||||
await async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
|
||||
|
||||
config_entry.unique_id = new_unique_id
|
||||
config_entry.version = 3
|
||||
|
||||
_LOGGER.info("Migration to version %s successful", config_entry.version)
|
||||
|
||||
return True
|
||||
|
@ -30,7 +30,7 @@ class AxisEntityBase(Entity):
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return a device description for device registry."""
|
||||
return {"identifiers": {(AXIS_DOMAIN, self.device.serial)}}
|
||||
return {"identifiers": {(AXIS_DOMAIN, self.device.unique_id)}}
|
||||
|
||||
@callback
|
||||
def update_callback(self, no_delay=None):
|
||||
@ -73,4 +73,4 @@ class AxisEventBase(AxisEntityBase):
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique identifier for this device."""
|
||||
return f"{self.device.serial}-{self.event.topic}-{self.event.id}"
|
||||
return f"{self.device.unique_id}-{self.event.topic}-{self.event.id}"
|
||||
|
@ -73,7 +73,7 @@ class AxisCamera(AxisEntityBase, MjpegCamera):
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique identifier for this device."""
|
||||
return f"{self.device.serial}-camera"
|
||||
return f"{self.device.unique_id}-camera"
|
||||
|
||||
@property
|
||||
def image_source(self):
|
||||
|
@ -8,13 +8,13 @@ from homeassistant import config_entries
|
||||
from homeassistant.config_entries import SOURCE_IGNORE
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_MAC,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.util.network import is_link_local
|
||||
|
||||
from .const import (
|
||||
@ -26,7 +26,7 @@ from .const import (
|
||||
from .device import get_device
|
||||
from .errors import AuthenticationRequired, CannotConnect
|
||||
|
||||
AXIS_OUI = {"00408C", "ACCC8E", "B8A44F"}
|
||||
AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f"}
|
||||
|
||||
CONFIG_FILE = "axis.conf"
|
||||
|
||||
@ -42,7 +42,7 @@ DEFAULT_PORT = 80
|
||||
class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN):
|
||||
"""Handle a Axis config flow."""
|
||||
|
||||
VERSION = 2
|
||||
VERSION = 3
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
|
||||
|
||||
@staticmethod
|
||||
@ -74,7 +74,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN):
|
||||
password=user_input[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
await self.async_set_unique_id(device.vapix.serial_number)
|
||||
await self.async_set_unique_id(format_mac(device.vapix.serial_number))
|
||||
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
@ -88,7 +88,6 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN):
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
CONF_MAC: device.vapix.serial_number,
|
||||
CONF_MODEL: device.vapix.product_number,
|
||||
}
|
||||
|
||||
@ -134,14 +133,14 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN):
|
||||
|
||||
self.device_config[CONF_NAME] = name
|
||||
|
||||
title = f"{model} - {self.device_config[CONF_MAC]}"
|
||||
title = f"{model} - {self.unique_id}"
|
||||
return self.async_create_entry(title=title, data=self.device_config)
|
||||
|
||||
async def async_step_zeroconf(self, discovery_info):
|
||||
"""Prepare configuration for a discovered Axis device."""
|
||||
serial_number = discovery_info["properties"]["macaddress"]
|
||||
serial_number = format_mac(discovery_info["properties"]["macaddress"])
|
||||
|
||||
if serial_number[:6] not in AXIS_OUI:
|
||||
if serial_number[:8] not in AXIS_OUI:
|
||||
return self.async_abort(reason="not_axis_device")
|
||||
|
||||
if is_link_local(ip_address(discovery_info[CONF_HOST])):
|
||||
|
@ -74,8 +74,8 @@ class AxisNetworkDevice:
|
||||
return self.config_entry.data[CONF_NAME]
|
||||
|
||||
@property
|
||||
def serial(self):
|
||||
"""Return the serial number of this device."""
|
||||
def unique_id(self):
|
||||
"""Return the unique ID (serial number) of this device."""
|
||||
return self.config_entry.unique_id
|
||||
|
||||
# Options
|
||||
@ -102,17 +102,17 @@ class AxisNetworkDevice:
|
||||
@property
|
||||
def signal_reachable(self):
|
||||
"""Device specific event to signal a change in connection status."""
|
||||
return f"axis_reachable_{self.serial}"
|
||||
return f"axis_reachable_{self.unique_id}"
|
||||
|
||||
@property
|
||||
def signal_new_event(self):
|
||||
"""Device specific event to signal new device event available."""
|
||||
return f"axis_new_event_{self.serial}"
|
||||
return f"axis_new_event_{self.unique_id}"
|
||||
|
||||
@property
|
||||
def signal_new_address(self):
|
||||
"""Device specific event to signal a change in device address."""
|
||||
return f"axis_new_address_{self.serial}"
|
||||
return f"axis_new_address_{self.unique_id}"
|
||||
|
||||
# Callbacks
|
||||
|
||||
@ -151,8 +151,8 @@ class AxisNetworkDevice:
|
||||
device_registry = await self.hass.helpers.device_registry.async_get_registry()
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=self.config_entry.entry_id,
|
||||
connections={(CONNECTION_NETWORK_MAC, self.serial)},
|
||||
identifiers={(AXIS_DOMAIN, self.serial)},
|
||||
connections={(CONNECTION_NETWORK_MAC, self.unique_id)},
|
||||
identifiers={(AXIS_DOMAIN, self.unique_id)},
|
||||
manufacturer=ATTR_MANUFACTURER,
|
||||
model=f"{self.model} {self.product_type}",
|
||||
name=self.name,
|
||||
@ -169,7 +169,9 @@ class AxisNetworkDevice:
|
||||
|
||||
if status.get("data", {}).get("status", {}).get("state") == "active":
|
||||
self.listeners.append(
|
||||
await mqtt.async_subscribe(hass, f"{self.serial}/#", self.mqtt_message)
|
||||
await mqtt.async_subscribe(
|
||||
hass, f"{self.api.vapix.serial_number}/#", self.mqtt_message
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
|
@ -15,7 +15,6 @@ from homeassistant.components.axis.const import (
|
||||
from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_USER, SOURCE_ZEROCONF
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_MAC,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
@ -26,6 +25,7 @@ from homeassistant.data_entry_flow import (
|
||||
RESULT_TYPE_CREATE_ENTRY,
|
||||
RESULT_TYPE_FORM,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from .test_device import (
|
||||
MAC,
|
||||
@ -62,13 +62,12 @@ async def test_flow_manual_configuration(hass):
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == f"M1065-LW - {MAC}"
|
||||
assert result["title"] == f"M1065-LW - {format_mac(MAC)}"
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "1.2.3.4",
|
||||
CONF_USERNAME: "user",
|
||||
CONF_PASSWORD: "pass",
|
||||
CONF_PORT: 80,
|
||||
CONF_MAC: MAC,
|
||||
CONF_MODEL: "M1065-LW",
|
||||
CONF_NAME: "M1065-LW 0",
|
||||
}
|
||||
@ -220,13 +219,12 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass):
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == f"M1065-LW - {MAC}"
|
||||
assert result["title"] == f"M1065-LW - {format_mac(MAC)}"
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "1.2.3.4",
|
||||
CONF_USERNAME: "user",
|
||||
CONF_PASSWORD: "pass",
|
||||
CONF_PORT: 80,
|
||||
CONF_MAC: MAC,
|
||||
CONF_MODEL: "M1065-LW",
|
||||
CONF_NAME: "M1065-LW 2",
|
||||
}
|
||||
@ -263,13 +261,12 @@ async def test_zeroconf_flow(hass):
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == f"M1065-LW - {MAC}"
|
||||
assert result["title"] == f"M1065-LW - {format_mac(MAC)}"
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "1.2.3.4",
|
||||
CONF_USERNAME: "user",
|
||||
CONF_PASSWORD: "pass",
|
||||
CONF_PORT: 80,
|
||||
CONF_MAC: MAC,
|
||||
CONF_MODEL: "M1065-LW",
|
||||
CONF_NAME: "M1065-LW 0",
|
||||
}
|
||||
@ -309,7 +306,6 @@ async def test_zeroconf_flow_updated_configuration(hass):
|
||||
CONF_PORT: 80,
|
||||
CONF_USERNAME: "root",
|
||||
CONF_PASSWORD: "pass",
|
||||
CONF_MAC: MAC,
|
||||
CONF_MODEL: MODEL,
|
||||
CONF_NAME: NAME,
|
||||
}
|
||||
@ -338,7 +334,6 @@ async def test_zeroconf_flow_updated_configuration(hass):
|
||||
CONF_PORT: 8080,
|
||||
CONF_USERNAME: "root",
|
||||
CONF_PASSWORD: "pass",
|
||||
CONF_MAC: MAC,
|
||||
CONF_MODEL: MODEL,
|
||||
CONF_NAME: NAME,
|
||||
}
|
||||
|
@ -19,7 +19,6 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAI
|
||||
from homeassistant.config_entries import SOURCE_ZEROCONF
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_MAC,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
@ -29,7 +28,8 @@ from homeassistant.const import (
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_mqtt_message
|
||||
|
||||
MAC = "00408C12345"
|
||||
MAC = "00408C123456"
|
||||
FORMATTED_MAC = "00:40:8c:12:34:56"
|
||||
MODEL = "model"
|
||||
NAME = "name"
|
||||
|
||||
@ -42,7 +42,6 @@ ENTRY_CONFIG = {
|
||||
CONF_USERNAME: "root",
|
||||
CONF_PASSWORD: "pass",
|
||||
CONF_PORT: 80,
|
||||
CONF_MAC: MAC,
|
||||
CONF_MODEL: MODEL,
|
||||
CONF_NAME: NAME,
|
||||
}
|
||||
@ -80,7 +79,7 @@ BASIC_DEVICE_INFO_RESPONSE = {
|
||||
"propertyList": {
|
||||
"ProdNbr": "M1065-LW",
|
||||
"ProdType": "Network Camera",
|
||||
"SerialNumber": "00408C12345",
|
||||
"SerialNumber": MAC,
|
||||
"Version": "9.80.1",
|
||||
}
|
||||
},
|
||||
@ -170,7 +169,7 @@ root.IOPort.I0.Input.Trig=closed
|
||||
root.Output.NbrOfOutputs=0
|
||||
"""
|
||||
|
||||
PROPERTIES_RESPONSE = """root.Properties.API.HTTP.Version=3
|
||||
PROPERTIES_RESPONSE = f"""root.Properties.API.HTTP.Version=3
|
||||
root.Properties.API.Metadata.Metadata=yes
|
||||
root.Properties.API.Metadata.Version=1.0
|
||||
root.Properties.EmbeddedDevelopment.Version=2.16
|
||||
@ -181,7 +180,7 @@ root.Properties.Image.Format=jpeg,mjpeg,h264
|
||||
root.Properties.Image.NbrOfViews=2
|
||||
root.Properties.Image.Resolution=1920x1080,1280x960,1280x720,1024x768,1024x576,800x600,640x480,640x360,352x240,320x240
|
||||
root.Properties.Image.Rotation=0,180
|
||||
root.Properties.System.SerialNumber=00408C12345
|
||||
root.Properties.System.SerialNumber={MAC}
|
||||
"""
|
||||
|
||||
PTZ_RESPONSE = ""
|
||||
@ -284,7 +283,8 @@ async def setup_axis_integration(hass, config=ENTRY_CONFIG, options=ENTRY_OPTION
|
||||
data=deepcopy(config),
|
||||
connection_class=config_entries.CONN_CLASS_LOCAL_PUSH,
|
||||
options=deepcopy(options),
|
||||
version=2,
|
||||
version=3,
|
||||
unique_id=FORMATTED_MAC,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
@ -308,7 +308,7 @@ async def test_device_setup(hass):
|
||||
assert device.api.vapix.firmware_version == "9.10.1"
|
||||
assert device.api.vapix.product_number == "M1065-LW"
|
||||
assert device.api.vapix.product_type == "Network Camera"
|
||||
assert device.api.vapix.serial_number == "00408C12345"
|
||||
assert device.api.vapix.serial_number == "00408C123456"
|
||||
|
||||
entry = device.config_entry
|
||||
|
||||
@ -321,7 +321,7 @@ async def test_device_setup(hass):
|
||||
assert device.host == ENTRY_CONFIG[CONF_HOST]
|
||||
assert device.model == ENTRY_CONFIG[CONF_MODEL]
|
||||
assert device.name == ENTRY_CONFIG[CONF_NAME]
|
||||
assert device.serial == ENTRY_CONFIG[CONF_MAC]
|
||||
assert device.unique_id == FORMATTED_MAC
|
||||
|
||||
|
||||
async def test_device_info(hass):
|
||||
@ -336,7 +336,7 @@ async def test_device_info(hass):
|
||||
assert device.api.vapix.firmware_version == "9.80.1"
|
||||
assert device.api.vapix.product_number == "M1065-LW"
|
||||
assert device.api.vapix.product_type == "Network Camera"
|
||||
assert device.api.vapix.serial_number == "00408C12345"
|
||||
assert device.api.vapix.serial_number == "00408C123456"
|
||||
|
||||
|
||||
async def test_device_support_mqtt(hass, mqtt_mock):
|
||||
|
@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from homeassistant.components import axis
|
||||
from homeassistant.components.axis.const import CONF_MODEL, DOMAIN as AXIS_DOMAIN
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE,
|
||||
CONF_HOST,
|
||||
@ -12,6 +13,8 @@ from homeassistant.const import (
|
||||
CONF_PORT,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.helpers import entity_registry
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .test_device import MAC, setup_axis_integration
|
||||
@ -29,13 +32,13 @@ async def test_setup_entry(hass):
|
||||
"""Test successful setup of entry."""
|
||||
await setup_axis_integration(hass)
|
||||
assert len(hass.data[AXIS_DOMAIN]) == 1
|
||||
assert MAC in hass.data[AXIS_DOMAIN]
|
||||
assert format_mac(MAC) in hass.data[AXIS_DOMAIN]
|
||||
|
||||
|
||||
async def test_setup_entry_fails(hass):
|
||||
"""Test successful setup of entry."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=AXIS_DOMAIN, data={CONF_MAC: "0123"}, version=2
|
||||
domain=AXIS_DOMAIN, data={CONF_MAC: "0123"}, version=3
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
@ -69,7 +72,7 @@ async def test_migrate_entry(hass):
|
||||
CONF_PASSWORD: "password",
|
||||
CONF_PORT: 80,
|
||||
},
|
||||
CONF_MAC: "mac",
|
||||
CONF_MAC: "00408C123456",
|
||||
CONF_MODEL: "model",
|
||||
CONF_NAME: "name",
|
||||
}
|
||||
@ -77,6 +80,17 @@ async def test_migrate_entry(hass):
|
||||
|
||||
assert entry.data == legacy_config
|
||||
assert entry.version == 1
|
||||
assert not entry.unique_id
|
||||
|
||||
# Create entity entry to migrate to new unique ID
|
||||
registry = await entity_registry.async_get_registry(hass)
|
||||
registry.async_get_or_create(
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
AXIS_DOMAIN,
|
||||
"00408C123456-vmd4-0",
|
||||
suggested_object_id="vmd4",
|
||||
config_entry=entry,
|
||||
)
|
||||
|
||||
await entry.async_migrate(hass)
|
||||
|
||||
@ -91,8 +105,12 @@ async def test_migrate_entry(hass):
|
||||
CONF_USERNAME: "username",
|
||||
CONF_PASSWORD: "password",
|
||||
CONF_PORT: 80,
|
||||
CONF_MAC: "mac",
|
||||
CONF_MAC: "00408C123456",
|
||||
CONF_MODEL: "model",
|
||||
CONF_NAME: "name",
|
||||
}
|
||||
assert entry.version == 2
|
||||
assert entry.version == 3
|
||||
assert entry.unique_id == "00:40:8c:12:34:56"
|
||||
|
||||
vmd4_entity = registry.async_get("binary_sensor.vmd4")
|
||||
assert vmd4_entity.unique_id == "00:40:8c:12:34:56-vmd4-0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user