Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
27 changes: 27 additions & 0 deletions custom_components/owlet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,33 @@
_LOGGER = logging.getLogger(__name__)


async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry to new format."""
_LOGGER.debug(
"Migrating configuration from version %s.%s",
config_entry.version,
config_entry.minor_version,
)

if config_entry.version == 1:
# Version 1 used email-only unique_id, version 2 includes region prefix
region = config_entry.data.get(CONF_REGION, "world")
old_unique_id = config_entry.unique_id
new_unique_id = f"{region}_{old_unique_id}"

hass.config_entries.async_update_entry(
config_entry, unique_id=new_unique_id, version=2
)

_LOGGER.debug(
"Migration to version 2 successful: unique_id %s -> %s",
old_unique_id,
new_unique_id,
)

return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Owlet Smart Sock from a config entry."""
hass.data.setdefault(DOMAIN, {})
Expand Down
6 changes: 4 additions & 2 deletions custom_components/owlet/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Owlet Smart Sock."""

VERSION = 1
VERSION = 2
reauth_entry: ConfigEntry | None = None

def __init__(self) -> None:
Expand All @@ -61,7 +61,9 @@ async def async_step_user(
session=async_get_clientsession(self.hass),
)

await self.async_set_unique_id(user_input[CONF_USERNAME].lower())
await self.async_set_unique_id(
f"{user_input[CONF_REGION]}_{user_input[CONF_USERNAME].lower()}"
)
self._abort_if_unique_id_configured()

try:
Expand Down
3 changes: 2 additions & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ async def async_init_integration(
"""Set up integration entry."""
entry = MockConfigEntry(
domain=DOMAIN,
version=2,
title="sample@gmail.com",
unique_id="sample@gmail.com",
unique_id="europe_sample@gmail.com",
data={
CONF_REGION: "europe",
CONF_USERNAME: "sample@gmail.com",
Expand Down
6 changes: 6 additions & 0 deletions tests/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@
"username": "sample@gmail.com",
"password": "sample",
}

CONF_INPUT_WORLD = {
"region": "world",
"username": "sample@gmail.com",
"password": "sample",
}
75 changes: 74 additions & 1 deletion tests/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from homeassistant.data_entry_flow import FlowResultType

from . import async_init_integration
from .const import AUTH_RETURN, CONF_INPUT
from .const import AUTH_RETURN, CONF_INPUT, CONF_INPUT_WORLD


async def test_form(hass: HomeAssistant) -> None:
Expand Down Expand Up @@ -217,6 +217,79 @@ async def test_reauth_unknown_error(hass: HomeAssistant) -> None:
assert result["step_id"] == "reauth_confirm"


async def test_flow_same_email_different_region(hass: HomeAssistant) -> None:
"""Test that the same email can be added for different regions."""
# Create first entry for europe region
with patch(
"homeassistant.components.owlet.config_flow.OwletAPI.authenticate",
return_value=AUTH_RETURN,
), patch(
"homeassistant.components.owlet.config_flow.OwletAPI.validate_authentication"
):
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=CONF_INPUT,
)
await hass.async_block_till_done()

assert result["type"] == FlowResultType.CREATE_ENTRY

# Create second entry for world region with same email
with patch(
"homeassistant.components.owlet.config_flow.OwletAPI.authenticate",
return_value=AUTH_RETURN,
), patch(
"homeassistant.components.owlet.config_flow.OwletAPI.validate_authentication"
):
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=CONF_INPUT_WORLD,
)
await hass.async_block_till_done()

assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"]["region"] == "world"


async def test_flow_duplicate_region_and_email(hass: HomeAssistant) -> None:
"""Test that duplicate email + region is still blocked."""
# Create first entry for europe region
with patch(
"homeassistant.components.owlet.config_flow.OwletAPI.authenticate",
return_value=AUTH_RETURN,
), patch(
"homeassistant.components.owlet.config_flow.OwletAPI.validate_authentication"
):
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=CONF_INPUT,
)
await hass.async_block_till_done()

assert result["type"] == FlowResultType.CREATE_ENTRY

# Try to create duplicate entry for same region + email
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=CONF_INPUT,
)

assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"


async def test_options_flow(hass: HomeAssistant) -> None:
"""Test that the form is served with no input."""
entry = await async_init_integration(hass, skip_setup=True)
Expand Down
46 changes: 45 additions & 1 deletion tests/test_init.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test Owlet init."""
from __future__ import annotations

import json
from unittest.mock import patch

from pyowletapi.exceptions import (
Expand All @@ -14,13 +15,21 @@
CONF_OWLET_EXPIRY,
CONF_OWLET_REFRESH,
DOMAIN,
POLLING_INTERVAL,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_API_TOKEN, CONF_REGION, CONF_USERNAME, Platform
from homeassistant.const import (
CONF_API_TOKEN,
CONF_REGION,
CONF_SCAN_INTERVAL,
CONF_USERNAME,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er

from . import async_init_integration
from tests.common import MockConfigEntry, load_fixture

PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]

Expand Down Expand Up @@ -134,3 +143,38 @@ async def test_async_setup_entry_error(hass: HomeAssistant) -> None:
assert entry.state == ConfigEntryState.SETUP_ERROR

await entry.async_unload(hass)


async def test_migrate_entry_v1_to_v2(hass: HomeAssistant) -> None:
"""Test migration from v1 (email-only unique_id) to v2 (region_email)."""
entry = MockConfigEntry(
domain=DOMAIN,
version=1,
title="sample@gmail.com",
unique_id="sample@gmail.com",
data={
CONF_REGION: "europe",
CONF_USERNAME: "sample@gmail.com",
CONF_API_TOKEN: "api_token",
CONF_OWLET_EXPIRY: 100,
CONF_OWLET_REFRESH: "refresh_token",
},
options={CONF_SCAN_INTERVAL: POLLING_INTERVAL},
)

entry.add_to_hass(hass)

with patch(
"homeassistant.components.owlet.OwletAPI.get_properties",
return_value={},
), patch(
"homeassistant.components.owlet.OwletAPI.authenticate", return_value=None
), patch(
"homeassistant.components.owlet.OwletAPI.get_devices",
return_value=json.loads(load_fixture("get_devices.json", "owlet")),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

assert entry.version == 2
assert entry.unique_id == "europe_sample@gmail.com"