mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Migrate from entry unique id to emoncms unique id (#129133)
* Migrate from entry unique id to emoncms unique id * Use a placeholder for the documentation URL * Use async_set_unique_id in config_flow * use _abort_if_unique_id_configured in config_flow * Avoid single-use variable Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Add async_migrate_entry * Remove commented code * Downgrade version if user add server without uuid * Improve code quality * Move code migrating HA to emoncms uuid to init * Fit doc url in less than 88 chars Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Improve code quality * Only update unique_id with async_update_entry Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Make emoncms_client compulsory to get_feed_list * Improve readability with unique id functions * Rmv test to give more sense to _migrate_unique_id --------- Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
This commit is contained in:
parent
e3dfa84d65
commit
24b47b50ea
@ -5,8 +5,11 @@ from pyemoncms import EmoncmsClient
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
|
||||
from .const import DOMAIN, EMONCMS_UUID_DOC_URL, LOGGER
|
||||
from .coordinator import EmoncmsCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
@ -14,6 +17,49 @@ PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
type EmonCMSConfigEntry = ConfigEntry[EmoncmsCoordinator]
|
||||
|
||||
|
||||
def _migrate_unique_id(
|
||||
hass: HomeAssistant, entry: EmonCMSConfigEntry, emoncms_unique_id: str
|
||||
) -> None:
|
||||
"""Migrate to emoncms unique id if needed."""
|
||||
ent_reg = er.async_get(hass)
|
||||
entry_entities = ent_reg.entities.get_entries_for_config_entry_id(entry.entry_id)
|
||||
for entity in entry_entities:
|
||||
if entity.unique_id.split("-")[0] == entry.entry_id:
|
||||
feed_id = entity.unique_id.split("-")[-1]
|
||||
LOGGER.debug(f"moving feed {feed_id} to hardware uuid")
|
||||
ent_reg.async_update_entity(
|
||||
entity.entity_id, new_unique_id=f"{emoncms_unique_id}-{feed_id}"
|
||||
)
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
unique_id=emoncms_unique_id,
|
||||
)
|
||||
|
||||
|
||||
async def _check_unique_id_migration(
|
||||
hass: HomeAssistant, entry: EmonCMSConfigEntry, emoncms_client: EmoncmsClient
|
||||
) -> None:
|
||||
"""Check if we can migrate to the emoncms uuid."""
|
||||
emoncms_unique_id = await emoncms_client.async_get_uuid()
|
||||
if emoncms_unique_id:
|
||||
if entry.unique_id != emoncms_unique_id:
|
||||
_migrate_unique_id(hass, entry, emoncms_unique_id)
|
||||
else:
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"migrate database",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="migrate_database",
|
||||
translation_placeholders={
|
||||
"url": entry.data[CONF_URL],
|
||||
"doc_url": EMONCMS_UUID_DOC_URL,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: EmonCMSConfigEntry) -> bool:
|
||||
"""Load a config entry."""
|
||||
emoncms_client = EmoncmsClient(
|
||||
@ -21,6 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: EmonCMSConfigEntry) -> b
|
||||
entry.data[CONF_API_KEY],
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
await _check_unique_id_migration(hass, entry, emoncms_client)
|
||||
coordinator = EmoncmsCoordinator(hass, emoncms_client)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.config_entries import (
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import selector
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@ -48,13 +48,10 @@ def sensor_name(url: str) -> str:
|
||||
return f"emoncms@{sensorip}"
|
||||
|
||||
|
||||
async def get_feed_list(hass: HomeAssistant, url: str, api_key: str) -> dict[str, Any]:
|
||||
async def get_feed_list(
|
||||
emoncms_client: EmoncmsClient,
|
||||
) -> dict[str, Any]:
|
||||
"""Check connection to emoncms and return feed list if successful."""
|
||||
emoncms_client = EmoncmsClient(
|
||||
url,
|
||||
api_key,
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
return await emoncms_client.async_request("/feed/list.json")
|
||||
|
||||
|
||||
@ -82,22 +79,25 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
description_placeholders = {}
|
||||
|
||||
if user_input is not None:
|
||||
self.url = user_input[CONF_URL]
|
||||
self.api_key = user_input[CONF_API_KEY]
|
||||
self._async_abort_entries_match(
|
||||
{
|
||||
CONF_API_KEY: user_input[CONF_API_KEY],
|
||||
CONF_URL: user_input[CONF_URL],
|
||||
CONF_API_KEY: self.api_key,
|
||||
CONF_URL: self.url,
|
||||
}
|
||||
)
|
||||
result = await get_feed_list(
|
||||
self.hass, user_input[CONF_URL], user_input[CONF_API_KEY]
|
||||
emoncms_client = EmoncmsClient(
|
||||
self.url, self.api_key, session=async_get_clientsession(self.hass)
|
||||
)
|
||||
result = await get_feed_list(emoncms_client)
|
||||
if not result[CONF_SUCCESS]:
|
||||
errors["base"] = "api_error"
|
||||
description_placeholders = {"details": result[CONF_MESSAGE]}
|
||||
else:
|
||||
self.include_only_feeds = user_input.get(CONF_ONLY_INCLUDE_FEEDID)
|
||||
self.url = user_input[CONF_URL]
|
||||
self.api_key = user_input[CONF_API_KEY]
|
||||
await self.async_set_unique_id(await emoncms_client.async_get_uuid())
|
||||
self._abort_if_unique_id_configured()
|
||||
options = get_options(result[CONF_MESSAGE])
|
||||
self.dropdown = {
|
||||
"options": options,
|
||||
@ -191,7 +191,12 @@ class EmoncmsOptionsFlow(OptionsFlow):
|
||||
self.config_entry.data.get(CONF_ONLY_INCLUDE_FEEDID, []),
|
||||
)
|
||||
options: list = include_only_feeds
|
||||
result = await get_feed_list(self.hass, self._url, self._api_key)
|
||||
emoncms_client = EmoncmsClient(
|
||||
self._url,
|
||||
self._api_key,
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
result = await get_feed_list(emoncms_client)
|
||||
if not result[CONF_SUCCESS]:
|
||||
errors["base"] = "api_error"
|
||||
description_placeholders = {"details": result[CONF_MESSAGE]}
|
||||
|
@ -7,6 +7,10 @@ CONF_ONLY_INCLUDE_FEEDID = "include_only_feed_id"
|
||||
CONF_MESSAGE = "message"
|
||||
CONF_SUCCESS = "success"
|
||||
DOMAIN = "emoncms"
|
||||
EMONCMS_UUID_DOC_URL = (
|
||||
"https://docs.openenergymonitor.org/emoncms/update.html"
|
||||
"#upgrading-to-a-version-producing-a-unique-identifier"
|
||||
)
|
||||
FEED_ID = "id"
|
||||
FEED_NAME = "name"
|
||||
FEED_TAG = "tag"
|
||||
|
@ -148,20 +148,20 @@ async def async_setup_entry(
|
||||
return
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
# uuid was added in emoncms database 11.5.7
|
||||
unique_id = entry.unique_id if entry.unique_id else entry.entry_id
|
||||
elems = coordinator.data
|
||||
if not elems:
|
||||
return
|
||||
|
||||
sensors: list[EmonCmsSensor] = []
|
||||
|
||||
for idx, elem in enumerate(elems):
|
||||
if include_only_feeds is not None and elem[FEED_ID] not in include_only_feeds:
|
||||
continue
|
||||
|
||||
sensors.append(
|
||||
EmonCmsSensor(
|
||||
coordinator,
|
||||
entry.entry_id,
|
||||
unique_id,
|
||||
elem["unit"],
|
||||
name,
|
||||
idx,
|
||||
@ -176,7 +176,7 @@ class EmonCmsSensor(CoordinatorEntity[EmoncmsCoordinator], SensorEntity):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EmoncmsCoordinator,
|
||||
entry_id: str,
|
||||
unique_id: str,
|
||||
unit_of_measurement: str | None,
|
||||
name: str,
|
||||
idx: int,
|
||||
@ -189,7 +189,7 @@ class EmonCmsSensor(CoordinatorEntity[EmoncmsCoordinator], SensorEntity):
|
||||
elem = self.coordinator.data[self.idx]
|
||||
self._attr_name = f"{name} {elem[FEED_NAME]}"
|
||||
self._attr_native_unit_of_measurement = unit_of_measurement
|
||||
self._attr_unique_id = f"{entry_id}-{elem[FEED_ID]}"
|
||||
self._attr_unique_id = f"{unique_id}-{elem[FEED_ID]}"
|
||||
if unit_of_measurement in ("kWh", "Wh"):
|
||||
self._attr_device_class = SensorDeviceClass.ENERGY
|
||||
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
|
@ -19,6 +19,9 @@
|
||||
"include_only_feed_id": "Choose feeds to include"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This server is already configured"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@ -41,6 +44,10 @@
|
||||
"missing_include_only_feed_id": {
|
||||
"title": "No feed synchronized with the {domain} sensor",
|
||||
"description": "Configuring {domain} using YAML is being removed.\n\nPlease add manually the feeds you want to synchronize with the `configure` button of the integration."
|
||||
},
|
||||
"migrate_database": {
|
||||
"title": "Upgrade your emoncms version",
|
||||
"description": "Your [emoncms]({url}) does not ship a unique identifier.\n\n Please upgrade to at least version 11.5.7 and migrate your emoncms database.\n\n More info on [emoncms documentation]({doc_url})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,6 +91,21 @@ def config_entry() -> MockConfigEntry:
|
||||
)
|
||||
|
||||
|
||||
FLOW_RESULT_SECOND_URL = copy.deepcopy(FLOW_RESULT)
|
||||
FLOW_RESULT_SECOND_URL[CONF_URL] = "http://1.1.1.2"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_entry_unique_id() -> MockConfigEntry:
|
||||
"""Mock emoncms config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title=SENSOR_NAME,
|
||||
data=FLOW_RESULT_SECOND_URL,
|
||||
unique_id="123-53535292",
|
||||
)
|
||||
|
||||
|
||||
FLOW_RESULT_NO_FEED = copy.deepcopy(FLOW_RESULT)
|
||||
FLOW_RESULT_NO_FEED[CONF_ONLY_INCLUDE_FEEDID] = None
|
||||
|
||||
@ -143,4 +158,5 @@ async def emoncms_client() -> AsyncGenerator[AsyncMock]:
|
||||
):
|
||||
client = mock_client.return_value
|
||||
client.async_request.return_value = {"success": True, "message": FEEDS}
|
||||
client.async_get_uuid.return_value = "123-53535292"
|
||||
yield client
|
||||
|
@ -30,7 +30,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'XXXXXXXX-1',
|
||||
'unique_id': '123-53535292-1',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
|
@ -142,3 +142,21 @@ async def test_options_flow_failure(
|
||||
assert result["description_placeholders"]["details"] == "failure"
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
|
||||
async def test_unique_id_exists(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
emoncms_client: AsyncMock,
|
||||
config_entry_unique_id: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test when entry with same unique id already exists."""
|
||||
config_entry_unique_id.add_to_hass(hass)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], USER_INPUT
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
@ -4,11 +4,14 @@ from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from homeassistant.components.emoncms.const import DOMAIN, FEED_ID, FEED_NAME
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
|
||||
from . import setup_integration
|
||||
from .conftest import EMONCMS_FAILURE
|
||||
from .conftest import EMONCMS_FAILURE, FEEDS
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@ -38,3 +41,49 @@ async def test_failure(
|
||||
emoncms_client.async_request.return_value = EMONCMS_FAILURE
|
||||
config_entry.add_to_hass(hass)
|
||||
assert not await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_migrate_uuid(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
emoncms_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test migration from home assistant uuid to emoncms uuid."""
|
||||
config_entry.add_to_hass(hass)
|
||||
assert config_entry.unique_id is None
|
||||
for _, feed in enumerate(FEEDS):
|
||||
entity_registry.async_get_or_create(
|
||||
Platform.SENSOR,
|
||||
DOMAIN,
|
||||
f"{config_entry.entry_id}-{feed[FEED_ID]}",
|
||||
config_entry=config_entry,
|
||||
suggested_object_id=f"{DOMAIN}_{feed[FEED_NAME]}",
|
||||
)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
emoncms_uuid = emoncms_client.async_get_uuid.return_value
|
||||
assert config_entry.unique_id == emoncms_uuid
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry.entry_id
|
||||
)
|
||||
|
||||
for nb, feed in enumerate(FEEDS):
|
||||
assert entity_entries[nb].unique_id == f"{emoncms_uuid}-{feed[FEED_ID]}"
|
||||
assert (
|
||||
entity_entries[nb].previous_unique_id
|
||||
== f"{config_entry.entry_id}-{feed[FEED_ID]}"
|
||||
)
|
||||
|
||||
|
||||
async def test_no_uuid(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
emoncms_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test an issue is created when the emoncms server does not ship an uuid."""
|
||||
emoncms_client.async_get_uuid.return_value = None
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
assert issue_registry.async_get_issue(domain=DOMAIN, issue_id="migrate database")
|
||||
|
Loading…
x
Reference in New Issue
Block a user