mirror of
https://github.com/home-assistant/core.git
synced 2026-09-25 17:04:04 -04:00
Fix SleepIQ bed dedup removing real beds that share a sleeper (#182267)
This commit is contained in:
@@ -6,7 +6,6 @@ from typing import Any
|
||||
from asyncsleepiq import (
|
||||
AsyncSleepIQ,
|
||||
SleepIQAPIException,
|
||||
SleepIQBed,
|
||||
SleepIQConnectionException,
|
||||
SleepIQLoginException,
|
||||
SleepIQTimeoutException,
|
||||
@@ -127,57 +126,28 @@ async def async_unload_entry(hass: HomeAssistant, entry: SleepIQConfigEntry) ->
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
def _foundation_feature_count(bed: SleepIQBed) -> int:
|
||||
"""Count foundation features on a bed."""
|
||||
f = bed.foundation
|
||||
return (
|
||||
len(f.lights)
|
||||
+ len(f.actuators)
|
||||
+ len(f.presets)
|
||||
+ len(f.foot_warmers)
|
||||
+ len(f.core_climates)
|
||||
)
|
||||
_FIRMNESS_CONTROL = "firmness control"
|
||||
|
||||
|
||||
def _filter_duplicate_beds(gateway: AsyncSleepIQ) -> None:
|
||||
"""Remove duplicate bed objects that share sleeper IDs.
|
||||
"""Remove "Firmness Control" controller objects that duplicate a real bed.
|
||||
|
||||
Groups beds whose sleeper-ID sets overlap and keeps the one with
|
||||
the most foundation features so the real bed survives regardless
|
||||
of API ordering.
|
||||
The SleepIQ API can return a controller object alongside the real bed,
|
||||
both carrying the same sleeper IDs. The controller is identified by
|
||||
"Firmness Control" in its model string.
|
||||
"""
|
||||
groups: dict[frozenset[str], list[str]] = {}
|
||||
to_remove: list[str] = []
|
||||
for bed_id, bed in gateway.beds.items():
|
||||
bed_sleeper_ids = frozenset(s.sleeper_id for s in bed.sleepers if s.sleeper_id)
|
||||
if not bed_sleeper_ids:
|
||||
continue
|
||||
matched = None
|
||||
for key in groups:
|
||||
if key & bed_sleeper_ids:
|
||||
matched = key
|
||||
break
|
||||
if matched is not None:
|
||||
groups[matched].append(bed_id)
|
||||
else:
|
||||
groups[bed_sleeper_ids] = [bed_id]
|
||||
|
||||
for bed_ids in groups.values():
|
||||
if len(bed_ids) < 2:
|
||||
continue
|
||||
best = max(
|
||||
bed_ids, key=lambda bid: _foundation_feature_count(gateway.beds[bid])
|
||||
if _FIRMNESS_CONTROL in (bed.model or "").lower():
|
||||
to_remove.append(bed_id)
|
||||
for bed_id in to_remove:
|
||||
_LOGGER.debug(
|
||||
"Removing controller duplicate '%s' (id=%s, model=%s)",
|
||||
gateway.beds[bed_id].name,
|
||||
bed_id,
|
||||
gateway.beds[bed_id].model,
|
||||
)
|
||||
for bed_id in bed_ids:
|
||||
if bed_id != best:
|
||||
_LOGGER.debug(
|
||||
"Removing duplicate bed '%s' (id=%s), keeping '%s' (id=%s)"
|
||||
" which has more foundation features",
|
||||
gateway.beds[bed_id].name,
|
||||
bed_id,
|
||||
gateway.beds[best].name,
|
||||
best,
|
||||
)
|
||||
del gateway.beds[bed_id]
|
||||
del gateway.beds[bed_id]
|
||||
|
||||
|
||||
async def _async_migrate_unique_ids(
|
||||
|
||||
@@ -369,3 +369,103 @@ async def test_duplicate_beds_none_sleeper_ids_not_filtered(
|
||||
entry = await setup_platform(hass, "sensor")
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
assert "ghost_001" in mock_asyncsleepiq.beds
|
||||
|
||||
|
||||
async def test_real_beds_sharing_sleeper_not_filtered(
|
||||
hass: HomeAssistant,
|
||||
mock_asyncsleepiq: MagicMock,
|
||||
) -> None:
|
||||
"""Test that two real beds sharing a sleeper profile are both preserved."""
|
||||
second_bed = create_autospec(SleepIQBed)
|
||||
second_bed.name = "Bedroom 2"
|
||||
second_bed.id = "bed_002"
|
||||
second_bed.mac_addr = "AA:BB:CC:DD:EE:03"
|
||||
second_bed.model = "C10"
|
||||
second_bed.paused = False
|
||||
|
||||
shared_sleeper = create_autospec(SleepIQSleeper)
|
||||
shared_sleeper.side = Side.LEFT
|
||||
shared_sleeper.name = SLEEPER_L_NAME
|
||||
shared_sleeper.sleeper_id = SLEEPER_L_ID
|
||||
shared_sleeper.in_bed = False
|
||||
shared_sleeper.sleep_number = 50
|
||||
shared_sleeper.pressure = 1200
|
||||
shared_sleeper.sleep_data = SleepData(
|
||||
duration=0, sleep_score=0, heart_rate=0, respiratory_rate=0, hrv=0
|
||||
)
|
||||
|
||||
other_sleeper = create_autospec(SleepIQSleeper)
|
||||
other_sleeper.side = Side.RIGHT
|
||||
other_sleeper.name = "Other Person"
|
||||
other_sleeper.sleeper_id = "99999"
|
||||
other_sleeper.in_bed = False
|
||||
other_sleeper.sleep_number = 60
|
||||
other_sleeper.pressure = 1100
|
||||
other_sleeper.sleep_data = SleepData(
|
||||
duration=0, sleep_score=0, heart_rate=0, respiratory_rate=0, hrv=0
|
||||
)
|
||||
|
||||
second_bed.sleepers = [shared_sleeper, other_sleeper]
|
||||
second_bed.foundation = create_autospec(SleepIQFoundation)
|
||||
second_bed.foundation.lights = [MagicMock()]
|
||||
second_bed.foundation.actuators = []
|
||||
second_bed.foundation.presets = []
|
||||
second_bed.foundation.foot_warmers = []
|
||||
second_bed.foundation.core_climates = []
|
||||
|
||||
mock_asyncsleepiq.beds["bed_002"] = second_bed
|
||||
|
||||
entry = await setup_platform(hass, "sensor")
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
assert BED_ID in mock_asyncsleepiq.beds
|
||||
assert "bed_002" in mock_asyncsleepiq.beds
|
||||
|
||||
|
||||
async def test_identical_sleepers_both_with_features_preserved(
|
||||
hass: HomeAssistant,
|
||||
mock_asyncsleepiq: MagicMock,
|
||||
) -> None:
|
||||
"""Test that two feature-bearing beds with identical sleeper sets are both kept."""
|
||||
duplicate_bed = create_autospec(SleepIQBed)
|
||||
duplicate_bed.name = "Living Room Bed"
|
||||
duplicate_bed.id = "bed_003"
|
||||
duplicate_bed.mac_addr = "AA:BB:CC:DD:EE:04"
|
||||
duplicate_bed.model = "C10"
|
||||
duplicate_bed.paused = False
|
||||
|
||||
dup_sleeper_l = create_autospec(SleepIQSleeper)
|
||||
dup_sleeper_l.side = Side.LEFT
|
||||
dup_sleeper_l.name = SLEEPER_L_NAME
|
||||
dup_sleeper_l.sleeper_id = SLEEPER_L_ID
|
||||
dup_sleeper_l.in_bed = False
|
||||
dup_sleeper_l.sleep_number = 50
|
||||
dup_sleeper_l.pressure = 1200
|
||||
dup_sleeper_l.sleep_data = SleepData(
|
||||
duration=0, sleep_score=0, heart_rate=0, respiratory_rate=0, hrv=0
|
||||
)
|
||||
|
||||
dup_sleeper_r = create_autospec(SleepIQSleeper)
|
||||
dup_sleeper_r.side = Side.RIGHT
|
||||
dup_sleeper_r.name = SLEEPER_R_NAME
|
||||
dup_sleeper_r.sleeper_id = SLEEPER_R_ID
|
||||
dup_sleeper_r.in_bed = False
|
||||
dup_sleeper_r.sleep_number = 60
|
||||
dup_sleeper_r.pressure = 1100
|
||||
dup_sleeper_r.sleep_data = SleepData(
|
||||
duration=0, sleep_score=0, heart_rate=0, respiratory_rate=0, hrv=0
|
||||
)
|
||||
|
||||
duplicate_bed.sleepers = [dup_sleeper_l, dup_sleeper_r]
|
||||
duplicate_bed.foundation = create_autospec(SleepIQFoundation)
|
||||
duplicate_bed.foundation.lights = [MagicMock()]
|
||||
duplicate_bed.foundation.actuators = []
|
||||
duplicate_bed.foundation.presets = []
|
||||
duplicate_bed.foundation.foot_warmers = []
|
||||
duplicate_bed.foundation.core_climates = []
|
||||
|
||||
mock_asyncsleepiq.beds["bed_003"] = duplicate_bed
|
||||
|
||||
entry = await setup_platform(hass, "sensor")
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
assert BED_ID in mock_asyncsleepiq.beds
|
||||
assert "bed_003" in mock_asyncsleepiq.beds
|
||||
|
||||
Reference in New Issue
Block a user