Skip to content

Commit 2b5a12f

Browse files
Improve user experience for person entity configuration.
1 parent 1e6b94a commit 2b5a12f

4 files changed

Lines changed: 131 additions & 26 deletions

File tree

custom_components/renpho_fitness_scale_ble/config_flow.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
async_discovered_service_info,
1717
)
1818
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
19-
from homeassistant.const import CONF_ADDRESS, UnitOfLength, UnitOfMass
19+
from homeassistant.const import (
20+
CONF_ADDRESS,
21+
STATE_UNKNOWN,
22+
UnitOfLength,
23+
UnitOfMass,
24+
)
2025
from homeassistant.data_entry_flow import FlowResult
2126
from homeassistant.helpers import (
2227
config_validation as cv,
@@ -166,6 +171,26 @@ def _validate_person_entity_unique(
166171
return True
167172

168173

174+
def _validate_person_entity_has_tracker(hass, person_entity: str | None) -> bool:
175+
"""A person entity is only useful here if it can report a location.
176+
177+
A ``person.X`` with no device trackers assigned sits in state
178+
``unknown``; linking it would do nothing (location-based matching only
179+
skips a user when their person is ``not_home``). Reject that so users
180+
don't configure an inert link. ``unavailable`` is treated as
181+
acceptable — it's typically transient (HA restart, brief tracker
182+
outage), and blocking on it would be a flaky validation. A missing
183+
state is also accepted (the entity selector only offers existing
184+
entities, so this is purely defensive).
185+
"""
186+
if not person_entity:
187+
return True
188+
state = hass.states.get(person_entity)
189+
if state is None:
190+
return True
191+
return state.state != STATE_UNKNOWN
192+
193+
169194
def _get_mobile_notify_services(hass) -> dict[str, str]:
170195
services: dict[str, str] = {}
171196
for service_name in hass.services.async_services().get("notify", {}):
@@ -435,9 +460,14 @@ async def async_step_add_first_user(
435460
) -> FlowResult:
436461
if user_input is not None:
437462
user_name = user_input[CONF_USER_NAME]
463+
person_entity = user_input.get(CONF_PERSON_ENTITY)
438464
errors: dict[str, str] = {}
439465
if not _validate_user_name_not_empty(user_name):
440466
errors["base"] = "empty_user_name"
467+
if person_entity and not _validate_person_entity_has_tracker(
468+
self.hass, person_entity
469+
):
470+
errors["person_entity"] = "person_entity_no_tracker"
441471
if errors:
442472
return self.async_show_form(
443473
step_id="add_first_user",
@@ -447,14 +477,14 @@ async def async_step_add_first_user(
447477
body_metrics = user_input.get(CONF_BODY_METRICS_ENABLED, False)
448478
if body_metrics:
449479
self.context[CONF_USER_NAME] = user_name
450-
self.context[CONF_PERSON_ENTITY] = user_input.get(CONF_PERSON_ENTITY)
480+
self.context[CONF_PERSON_ENTITY] = person_entity
451481
self.context[CONF_MOBILE_NOTIFY_SERVICES] = user_input.get(
452482
CONF_MOBILE_NOTIFY_SERVICES, []
453483
)
454484
return await self.async_step_add_first_user_body_metrics()
455485
return self._create_entry_with_first_user(
456486
user_name=user_name,
457-
person_entity=user_input.get(CONF_PERSON_ENTITY),
487+
person_entity=person_entity,
458488
mobile_services=user_input.get(CONF_MOBILE_NOTIFY_SERVICES, []),
459489
body_metrics_enabled=False,
460490
body_metrics_fields=None,
@@ -568,6 +598,10 @@ async def async_step_add_user(
568598
person_entity, self.user_profiles
569599
):
570600
errors["person_entity"] = "duplicate_person_entity"
601+
elif person_entity and not _validate_person_entity_has_tracker(
602+
self.hass, person_entity
603+
):
604+
errors["person_entity"] = "person_entity_no_tracker"
571605
if errors:
572606
return self.async_show_form(
573607
step_id="add_user",
@@ -679,6 +713,10 @@ async def async_step_edit_user_details(
679713
exclude_user_id=selected_user_id,
680714
):
681715
errors["person_entity"] = "duplicate_person_entity"
716+
elif person_entity and not _validate_person_entity_has_tracker(
717+
self.hass, person_entity
718+
):
719+
errors["person_entity"] = "person_entity_no_tracker"
682720
if errors:
683721
return self.async_show_form(
684722
step_id="edit_user_details",

custom_components/renpho_fitness_scale_ble/repairs.py

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,18 @@
1212
mean the location signal the user *configured* isn't doing anything,
1313
and the integration falls back to weight-only matching for that user.
1414
15-
``mobile_service_missing`` and ``person_entity_missing`` are fixable
16-
in-place — the right one-click action is unambiguous (drop the stale
17-
entry / clear the broken link). ``person_entity_unknown`` is left
18-
non-fixable because the right remediation usually isn't clearing the
19-
link, it's assigning a tracker to the existing person; that's a
20-
decision the user has to make.
15+
``mobile_service_missing``, ``person_entity_missing``, and
16+
``person_entity_unknown`` are all fixable in-place with a one-click
17+
"clear the link / drop the stale entry" action. For the two person
18+
cases, clearing isn't the *only* remediation (relinking, or assigning a
19+
device tracker, also resolves them), but it's a safe unambiguous default
20+
we can offer directly — the fix-flow text points at the alternatives.
21+
22+
The person-entity issues are only raised when the integration has 2+
23+
users configured. The location filter that consumes the link only runs
24+
in the multi-user resolver path (see ``_select_library_mode_argument``),
25+
so with a single user a linked person is inert and warning about it
26+
would be pure noise.
2127
2228
Issue types:
2329
@@ -30,16 +36,17 @@
3036
Configure remains available for users who want to point at a new
3137
entity instead.
3238
33-
- ``person_entity_unknown_<entry_id>_<user_id>`` *(non-fixable)*
39+
- ``person_entity_unknown_<entry_id>_<user_id>`` *(fixable)*
3440
The linked ``person.X`` exists but its state is ``unknown`` — the
3541
person was created in HA but no device trackers have been assigned
3642
to it. Same runtime impact as the missing case (not excluded, just
3743
no useful location signal). Surfaced as a warning so the user knows
38-
their configuration is degraded. Left non-fixable because the right
39-
remediation is usually "assign a tracker", not "clear the link".
40-
``unavailable`` is deliberately *not* flagged because it's typically
41-
transient (HA restart, brief tracker outage); raising a flapping
42-
issue every reload would just be noise.
44+
their configuration is degraded. The fix flow clears the link (the
45+
right move for a household that doesn't use location-based matching);
46+
the fix-flow text also points at the alternative of assigning a
47+
device tracker instead. ``unavailable`` is deliberately *not* flagged
48+
because it's typically transient (HA restart, brief tracker outage);
49+
raising a flapping issue every reload would just be noise.
4350
4451
- ``mobile_service_missing_<entry_id>_<user_id>_<service_slug>`` *(fixable)*
4552
A configured ``notify.mobile_app_*`` service no longer exists.
@@ -78,6 +85,7 @@
7885

7986
_REPAIR_KIND_MOBILE_SERVICE_MISSING = "mobile_service_missing"
8087
_REPAIR_KIND_PERSON_ENTITY_MISSING = "person_entity_missing"
88+
_REPAIR_KIND_PERSON_ENTITY_UNKNOWN = "person_entity_unknown"
8189

8290

8391
def _person_entity_missing_issue_id(entry_id: str, user_id: str) -> str:
@@ -118,6 +126,12 @@ def async_scan_repair_issues(hass: HomeAssistant, entry: ConfigEntry) -> None:
118126
# currently in the registry to figure out what to add/remove.
119127
desired: dict[str, dict[str, Any]] = {}
120128

129+
# The location filter that consumes a linked person entity only runs in
130+
# the multi-user resolver path. With a single user the link is inert, so
131+
# the person-entity issues below would be pure noise — only raise them
132+
# when 2+ users are configured.
133+
multi_user = sum(1 for p in profiles if p.get(CONF_USER_ID)) >= 2
134+
121135
for profile in profiles:
122136
user_id = profile.get(CONF_USER_ID, "")
123137
user_name = profile.get(CONF_USER_NAME, user_id)
@@ -129,7 +143,7 @@ def async_scan_repair_issues(hass: HomeAssistant, entry: ConfigEntry) -> None:
129143
# exact "not_home"), but the configured link isn't providing the
130144
# location signal the user expected.
131145
person_entity = profile.get(CONF_PERSON_ENTITY)
132-
if person_entity:
146+
if multi_user and person_entity:
133147
person_state = hass.states.get(person_entity)
134148
if person_state is None:
135149
# Entity gone (deleted in Settings → People). Fixable:
@@ -153,7 +167,10 @@ def async_scan_repair_issues(hass: HomeAssistant, entry: ConfigEntry) -> None:
153167
elif person_state.state == STATE_UNKNOWN:
154168
# Entity exists but has no working tracker — almost
155169
# always permanent. (We don't flag STATE_UNAVAILABLE
156-
# because that's typically transient.)
170+
# because that's typically transient.) Fixable: clearing
171+
# the inert link is the right move for a household that
172+
# doesn't use location-based matching; the fix-flow text
173+
# points at assigning a tracker as the alternative.
157174
issue_id = _person_entity_unknown_issue_id(entry.entry_id, user_id)
158175
desired[issue_id] = {
159176
"translation_key": "person_entity_unknown",
@@ -162,6 +179,12 @@ def async_scan_repair_issues(hass: HomeAssistant, entry: ConfigEntry) -> None:
162179
"person_entity": person_entity,
163180
},
164181
"severity": ir.IssueSeverity.WARNING,
182+
"is_fixable": True,
183+
"data": {
184+
"kind": _REPAIR_KIND_PERSON_ENTITY_UNKNOWN,
185+
"entry_id": entry.entry_id,
186+
"user_id": user_id,
187+
},
165188
}
166189

167190
# 2. Mobile notify services that no longer exist. Fixable in-place:
@@ -237,12 +260,15 @@ def async_clear_repair_issues_for_entry(
237260

238261

239262
class _ClearPersonEntityRepairFlow(RepairsFlow):
240-
"""Clear a broken person_entity link from a user profile.
263+
"""Clear a person_entity link from a user profile.
241264
242265
Called from the Repairs UI when the user clicks **Submit** on a
243-
``person_entity_missing`` issue. Clearing is unambiguous; users who
244-
want to point at a different person entity instead can do that via
245-
Configure → Edit User.
266+
``person_entity_missing`` issue (entity deleted) or a
267+
``person_entity_unknown`` issue (entity exists but has no working
268+
tracker). In both cases the link is inert, so clearing it is a safe
269+
unambiguous action; users who want to point at a different person
270+
entity, or to assign a tracker instead, can do that via
271+
Configure → Edit User / Settings → People.
246272
"""
247273

248274
def __init__(self, entry_id: str, user_id: str) -> None:
@@ -372,7 +398,10 @@ async def async_create_fix_flow(
372398
user_id=data["user_id"],
373399
service=data["service"],
374400
)
375-
if kind == _REPAIR_KIND_PERSON_ENTITY_MISSING:
401+
if kind in (
402+
_REPAIR_KIND_PERSON_ENTITY_MISSING,
403+
_REPAIR_KIND_PERSON_ENTITY_UNKNOWN,
404+
):
376405
return _ClearPersonEntityRepairFlow(
377406
entry_id=data["entry_id"],
378407
user_id=data["user_id"],

custom_components/renpho_fitness_scale_ble/strings.json

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
"person_entity": "Person entity (optional)",
2626
"mobile_notify_services": "Mobile devices for notifications",
2727
"body_metrics_enabled": "Calculate body composition metrics"
28+
},
29+
"data_description": {
30+
"person_entity": "Only used when this scale has multiple users, to skip this user when their linked person is away from home at weigh-in time. Requires the person to have a working device tracker (e.g. their phone via the Home Assistant companion app). Leave blank to match by weight only."
2831
}
2932
},
3033
"add_first_user_body_metrics": {
@@ -52,6 +55,7 @@
5255
"error": {
5356
"empty_user_name": "User name cannot be empty.",
5457
"duplicate_person_entity": "Another user is already linked to this person entity.",
58+
"person_entity_no_tracker": "This person has no device tracker, so location-based matching can't use it. Assign a device tracker in Settings → People first (typically their phone via the Home Assistant companion app), or leave this field blank.",
5559
"user_id_generation_failed": "Could not generate a unique user ID. Try a different name.",
5660
"invalid_user_name": "Invalid user name."
5761
}
@@ -76,6 +80,9 @@
7680
"person_entity": "Person entity (optional)",
7781
"mobile_notify_services": "Mobile devices for notifications",
7882
"body_metrics_enabled": "Calculate body composition metrics"
83+
},
84+
"data_description": {
85+
"person_entity": "Only used when this scale has multiple users, to skip this user when their linked person is away from home at weigh-in time. Requires the person to have a working device tracker (e.g. their phone via the Home Assistant companion app). Leave blank to match by weight only."
7986
}
8087
},
8188
"add_user_body_metrics": {
@@ -107,6 +114,9 @@
107114
"person_entity": "Person entity (optional)",
108115
"mobile_notify_services": "Mobile devices for notifications",
109116
"body_metrics_enabled": "Calculate body composition metrics"
117+
},
118+
"data_description": {
119+
"person_entity": "Only used when this scale has multiple users, to skip this user when their linked person is away from home at weigh-in time. Requires the person to have a working device tracker (e.g. their phone via the Home Assistant companion app). Leave blank to match by weight only."
110120
}
111121
},
112122
"edit_user_body_metrics": {
@@ -149,7 +159,8 @@
149159
},
150160
"error": {
151161
"empty_user_name": "User name cannot be empty.",
152-
"duplicate_person_entity": "Another user is already linked to this person entity."
162+
"duplicate_person_entity": "Another user is already linked to this person entity.",
163+
"person_entity_no_tracker": "This person has no device tracker, so location-based matching can't use it. Assign a device tracker in Settings → People first (typically their phone via the Home Assistant companion app), or leave this field blank."
153164
}
154165
},
155166
"selector": {
@@ -175,7 +186,15 @@
175186
},
176187
"person_entity_unknown": {
177188
"title": "{user_name}: linked person has no location",
178-
"description": "User **{user_name}** is linked to person entity `{person_entity}`, but that person has no working device trackers, so its state is `unknown`. Auto-assignment continues using weight matching — the link doesn't actively cause a problem — but location-based filtering for this user is effectively disabled until a tracker is assigned.\n\nFix it by either:\n\n- Going to **Settings → People**, opening `{person_entity}`, and assigning a device tracker to it (typically the user's phone via the Home Assistant companion app), **or**\n- Going to **Settings → Devices & Services → Renpho Fitness Scale BLE → Configure** and clearing the **Person Entity** field on this user's profile if you don't plan to set up a tracker."
189+
"fix_flow": {
190+
"step": {
191+
"confirm": {
192+
"title": "Clear the person entity link?",
193+
"description": "User **{user_name}** is linked to person entity `{person_entity}`, but that person has no working device trackers, so its state is `unknown`. Location-based matching only skips a user when their person is `not_home`, so with no tracker this link does nothing — auto-assignment is already using weight-only matching for this user.\n\nIf you don't use location-based matching (for example, everyone is usually home at weigh-in time), clearing this link is safe and removes this warning.\n\nPrefer to keep it? Assign a device tracker to `{person_entity}` in **Settings → People** instead (typically the user's phone via the Home Assistant companion app), and this warning will clear on its own.\n\nThis will clear the link from the profile.",
194+
"submit": "Clear"
195+
}
196+
}
197+
}
179198
},
180199
"mobile_service_missing": {
181200
"title": "{user_name}: mobile notify service `{service}` not found",

0 commit comments

Comments
 (0)