Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate from entry unique id to emoncms unique id #129133

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions homeassistant/components/emoncms/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ async def async_step_user(
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]
emoncms_client = EmoncmsClient(
self.url, self.api_key, session=async_get_clientsession(self.hass)
)
emoncms_unique_id = await emoncms_client.async_get_uuid()
alexandrecuer marked this conversation as resolved.
Show resolved Hide resolved
if (
emoncms_unique_id
and self.hass.config_entries.async_entry_for_domain_unique_id(
DOMAIN, emoncms_unique_id
)
):
return self.async_abort(reason="already_configured")
options = get_options(result[CONF_MESSAGE])
self.dropdown = {
"options": options,
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/emoncms/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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"
Expand Down
43 changes: 39 additions & 4 deletions homeassistant/components/emoncms/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from typing import Any

from pyemoncms import EmoncmsClient
import voluptuous as vol

from homeassistant.components.sensor import (
Expand All @@ -19,11 +20,13 @@
CONF_UNIT_OF_MEASUREMENT,
CONF_URL,
CONF_VALUE_TEMPLATE,
Platform,
UnitOfPower,
)
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import template
from homeassistant.helpers import entity_registry as er, template
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
Expand All @@ -35,9 +38,11 @@
CONF_EXCLUDE_FEEDID,
CONF_ONLY_INCLUDE_FEEDID,
DOMAIN,
EMONCMS_UUID_DOC_URL,
FEED_ID,
FEED_NAME,
FEED_TAG,
LOGGER,
)
from .coordinator import EmoncmsCoordinator

Expand Down Expand Up @@ -139,27 +144,53 @@ async def async_setup_entry(
) -> None:
"""Set up the emoncms sensors."""
config = entry.options if entry.options else entry.data
url = config[CONF_URL]
apikey = config[CONF_API_KEY]
name = sensor_name(config[CONF_URL])
exclude_feeds = config.get(CONF_EXCLUDE_FEEDID)
include_only_feeds = config.get(CONF_ONLY_INCLUDE_FEEDID)

if exclude_feeds is None and include_only_feeds is None:
return

emoncms_client = EmoncmsClient(url, apikey, session=async_get_clientsession(hass))
emoncms_unique_id = await emoncms_client.async_get_uuid()
if emoncms_unique_id is None:
async_create_issue(
hass,
DOMAIN,
"migrate database",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="migrate_database",
translation_placeholders={"url": url, "doc_url": EMONCMS_UUID_DOC_URL},
)
else:
hass.config_entries.async_update_entry(entry, unique_id=emoncms_unique_id)

coordinator = entry.runtime_data
elems = coordinator.data
if not elems:
return

ent_reg = er.async_get(hass)
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

entity_id = ent_reg.async_get_entity_id(
Platform.SENSOR, DOMAIN, f"{entry.entry_id}-{elem[FEED_ID]}"
)
if entity_id is not None and emoncms_unique_id is not None:
LOGGER.debug(f"{entity_id} exists and needs to be migrated")
ent_reg.async_update_entity(
entity_id, new_unique_id=f"{emoncms_unique_id}-{elem[FEED_ID]}"
)
sensors.append(
EmonCmsSensor(
coordinator,
emoncms_unique_id,
entry.entry_id,
elem["unit"],
name,
Expand All @@ -175,6 +206,7 @@ class EmonCmsSensor(CoordinatorEntity[EmoncmsCoordinator], SensorEntity):
def __init__(
self,
coordinator: EmoncmsCoordinator,
emoncms_unique_id: str,
entry_id: str,
unit_of_measurement: str | None,
name: str,
Expand All @@ -188,7 +220,10 @@ def __init__(
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]}"
if emoncms_unique_id:
self._attr_unique_id = f"{emoncms_unique_id}-{elem[FEED_ID]}"
else:
self._attr_unique_id = f"{entry_id}-{elem[FEED_ID]}"
if unit_of_measurement in ("kWh", "Wh"):
self._attr_device_class = SensorDeviceClass.ENERGY
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/emoncms/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"include_only_feed_id": "Choose feeds to include"
}
}
},
"abort": {
"already_configured": "This server is already configured"
}
},
"options": {
Expand All @@ -35,6 +38,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})"
}
}
}
20 changes: 20 additions & 0 deletions tests/components/emoncms/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -140,7 +155,12 @@ async def emoncms_client() -> AsyncGenerator[AsyncMock]:
"homeassistant.components.emoncms.config_flow.EmoncmsClient",
new=mock_client,
),
patch(
"homeassistant.components.emoncms.sensor.EmoncmsClient",
new=mock_client,
),
):
client = mock_client.return_value
client.async_request.return_value = {"success": True, "message": FEEDS}
client.async_get_uuid.return_value = "123-53535292"
yield client
2 changes: 1 addition & 1 deletion tests/components/emoncms/snapshots/test_sensor.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -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'>,
})
# ---
Expand Down
18 changes: 18 additions & 0 deletions tests/components/emoncms/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,21 @@ async def test_options_flow_failure(
assert result["errors"]["base"] == "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"
48 changes: 46 additions & 2 deletions tests/components/emoncms/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@
import pytest
from syrupy.assertion import SnapshotAssertion

from homeassistant.components.emoncms.const import DOMAIN
from homeassistant.components.emoncms.const import DOMAIN, FEED_ID, FEED_NAME
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import async_setup_component

from . import setup_integration
from .conftest import EMONCMS_FAILURE, get_feed
from .conftest import EMONCMS_FAILURE, FEEDS, get_feed

from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform

Expand Down Expand Up @@ -149,3 +150,46 @@ async def skip_time() -> None:
await skip_time()

assert f"Error fetching {DOMAIN}_coordinator data" in caplog.text


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)
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()
entity_entries = er.async_entries_for_config_entry(
entity_registry, config_entry.entry_id
)
emoncms_uuid = emoncms_client.async_get_uuid.return_value
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")