Skip to content

Commit d607323

Browse files
authored
Add support for tracking stats of party members in Habitica integration (home-assistant#151885)
1 parent 31f595a commit d607323

File tree

14 files changed

+1230
-69
lines changed

14 files changed

+1230
-69
lines changed

homeassistant/components/habitica/__init__.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,7 @@ async def async_setup_entry(
7272
config_entry.runtime_data = coordinator
7373

7474
party = coordinator.data.user.party.id
75-
if HABITICA_KEY not in hass.data:
76-
hass.data[HABITICA_KEY] = {}
75+
hass.data.setdefault(HABITICA_KEY, {})
7776

7877
if party is not None and party not in hass.data[HABITICA_KEY]:
7978
party_coordinator = HabiticaPartyCoordinator(hass, config_entry, api)
@@ -117,9 +116,20 @@ def _party_update_listener() -> None:
117116
coordinator.async_add_listener(_party_update_listener)
118117

119118
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
119+
120+
config_entry.async_on_unload(
121+
config_entry.add_update_listener(_async_update_listener)
122+
)
120123
return True
121124

122125

126+
async def _async_update_listener(
127+
hass: HomeAssistant, entry: HabiticaConfigEntry
128+
) -> None:
129+
"""Handle update."""
130+
await hass.config_entries.async_reload(entry.entry_id)
131+
132+
123133
async def shutdown_party_coordinator(hass: HomeAssistant, party_added: UUID) -> None:
124134
"""Handle party coordinator shutdown."""
125135
await hass.data[HABITICA_KEY][party_added].async_shutdown()

homeassistant/components/habitica/config_flow.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from collections.abc import Mapping
66
import logging
77
from typing import TYPE_CHECKING, Any
8+
from uuid import UUID
89

910
from aiohttp import ClientError
1011
from habiticalib import (
@@ -17,7 +18,14 @@
1718
import voluptuous as vol
1819

1920
from homeassistant import data_entry_flow
20-
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
21+
from homeassistant.config_entries import (
22+
ConfigEntry,
23+
ConfigEntryState,
24+
ConfigFlow,
25+
ConfigFlowResult,
26+
ConfigSubentryFlow,
27+
SubentryFlowResult,
28+
)
2129
from homeassistant.const import (
2230
CONF_API_KEY,
2331
CONF_NAME,
@@ -26,15 +34,21 @@
2634
CONF_USERNAME,
2735
CONF_VERIFY_SSL,
2836
)
37+
from homeassistant.core import callback
2938
from homeassistant.helpers.aiohttp_client import async_get_clientsession
3039
from homeassistant.helpers.selector import (
40+
SelectOptionDict,
41+
SelectSelector,
42+
SelectSelectorConfig,
3143
TextSelector,
3244
TextSelectorConfig,
3345
TextSelectorType,
3446
)
3547

48+
from . import HABITICA_KEY
3649
from .const import (
3750
CONF_API_USER,
51+
CONF_PARTY_MEMBER,
3852
DEFAULT_URL,
3953
DOMAIN,
4054
FORGOT_PASSWORD_URL,
@@ -374,3 +388,66 @@ async def validate_api_key(
374388
return errors, user.data
375389

376390
return errors, None
391+
392+
@classmethod
393+
@callback
394+
def async_get_supported_subentry_types(
395+
cls, config_entry: ConfigEntry
396+
) -> dict[str, type[ConfigSubentryFlow]]:
397+
"""Return subentries supported by this integration."""
398+
return {"party_member": PartyMembersSubentryFlowHandler}
399+
400+
401+
class PartyMembersSubentryFlowHandler(ConfigSubentryFlow):
402+
"""Handle subentry flow for adding party members."""
403+
404+
async def async_step_user(
405+
self, user_input: dict[str, Any] | None = None
406+
) -> SubentryFlowResult:
407+
"""Subentry user flow."""
408+
409+
entry: HabiticaConfigEntry = self._get_entry()
410+
if entry.state is not ConfigEntryState.LOADED:
411+
return self.async_abort(reason="config_entry_disabled")
412+
if (party := entry.runtime_data.data.user.party.id) is None:
413+
return self.async_abort(reason="not_in_a_party")
414+
415+
party_members = self.hass.data[HABITICA_KEY][party].data.members
416+
417+
if user_input is not None:
418+
config_entries = self.hass.config_entries.async_entries(DOMAIN)
419+
420+
for entry in config_entries:
421+
if user_input[CONF_PARTY_MEMBER] == entry.unique_id:
422+
return self.async_abort(reason="already_configured_as_entry")
423+
if user_input[CONF_PARTY_MEMBER] in {
424+
subentry.unique_id for subentry in entry.subentries.values()
425+
}:
426+
return self.async_abort(reason="already_configured")
427+
428+
return self.async_create_entry(
429+
title=party_members[UUID(user_input[CONF_PARTY_MEMBER])].profile.name,
430+
data={},
431+
unique_id=user_input[CONF_PARTY_MEMBER],
432+
)
433+
434+
options = [
435+
SelectOptionDict(
436+
value=str(member_id),
437+
label=f"{member.profile.name} (@{member.auth.local.username})",
438+
)
439+
for member_id, member in party_members.items()
440+
if member_id != str(entry.runtime_data.data.user.id)
441+
and member.profile.name
442+
and member.auth.local.username
443+
]
444+
return self.async_show_form(
445+
step_id="user",
446+
data_schema=vol.Schema(
447+
{
448+
vol.Required(CONF_PARTY_MEMBER): SelectSelector(
449+
SelectSelectorConfig(options=options)
450+
)
451+
}
452+
),
453+
)

homeassistant/components/habitica/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from homeassistant.const import APPLICATION_NAME, __version__
44

55
CONF_API_USER = "api_user"
6+
CONF_PARTY_MEMBER = "party_member"
67

78
DEFAULT_URL = "https://habitica.com"
89
ASSETS_URL = "https://habitica-assets.s3.amazonaws.com/mobileApp/images/"

homeassistant/components/habitica/coordinator.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,9 @@ async def _update_data(self) -> HabiticaPartyData:
213213
party=(await self.habitica.get_group()).data,
214214
members={
215215
member.id: member
216-
for member in (await self.habitica.get_group_members()).data
216+
for member in (
217+
await self.habitica.get_group_members(public_fields=True)
218+
).data
217219
if member.id
218220
},
219221
)

homeassistant/components/habitica/entity.py

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
from __future__ import annotations
44

55
from typing import TYPE_CHECKING
6+
from uuid import UUID
67

7-
from habiticalib import ContentData
8+
from habiticalib import ContentData, UserData
89
from yarl import URL
910

11+
from homeassistant.config_entries import ConfigSubentry
1012
from homeassistant.const import CONF_URL
1113
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
1214
from homeassistant.helpers.entity import EntityDescription
@@ -29,26 +31,84 @@ def __init__(
2931
self,
3032
coordinator: HabiticaDataUpdateCoordinator,
3133
entity_description: EntityDescription,
34+
subentry: ConfigSubentry | None = None,
3235
) -> None:
3336
"""Initialize a Habitica entity."""
3437
super().__init__(coordinator)
3538
if TYPE_CHECKING:
3639
assert coordinator.config_entry.unique_id
40+
assert self.user
3741
self.entity_description = entity_description
38-
self._attr_unique_id = (
39-
f"{coordinator.config_entry.unique_id}_{entity_description.key}"
42+
self.subentry = subentry
43+
unique_id = (
44+
subentry.unique_id
45+
if subentry is not None and subentry.unique_id
46+
else coordinator.config_entry.unique_id
4047
)
48+
49+
self._attr_unique_id = f"{unique_id}_{entity_description.key}"
4150
self._attr_device_info = DeviceInfo(
4251
entry_type=DeviceEntryType.SERVICE,
4352
manufacturer=MANUFACTURER,
4453
model=NAME,
45-
name=coordinator.data.user.profile.name,
54+
name=self.user.profile.name,
4655
configuration_url=(
47-
URL(coordinator.config_entry.data[CONF_URL])
48-
/ "profile"
49-
/ coordinator.config_entry.unique_id
56+
URL(coordinator.config_entry.data[CONF_URL]) / "profile" / unique_id
5057
),
51-
identifiers={(DOMAIN, coordinator.config_entry.unique_id)},
58+
identifiers={(DOMAIN, unique_id)},
59+
)
60+
61+
if subentry:
62+
self._attr_device_info.update(
63+
DeviceInfo(
64+
via_device=(
65+
(
66+
DOMAIN,
67+
f"{coordinator.config_entry.unique_id}_{self.user.party.id}",
68+
)
69+
)
70+
)
71+
)
72+
73+
@property
74+
def user(self) -> UserData | None:
75+
"""Return the user data."""
76+
return self.coordinator.data.user
77+
78+
79+
class HabiticaPartyMemberBase(HabiticaBase):
80+
"""Base Habitica party member entity."""
81+
82+
def __init__(
83+
self,
84+
coordinator: HabiticaDataUpdateCoordinator,
85+
party_coordinator: HabiticaPartyCoordinator,
86+
entity_description: EntityDescription,
87+
subentry: ConfigSubentry | None = None,
88+
) -> None:
89+
"""Initialize a Habitica entity."""
90+
self.party_coordinator = party_coordinator
91+
super().__init__(coordinator, entity_description, subentry)
92+
93+
@property
94+
def user(self) -> UserData | None:
95+
"""Return the user data of the party member."""
96+
if TYPE_CHECKING:
97+
assert self.subentry
98+
assert self.subentry.unique_id
99+
return self.party_coordinator.data.members.get(UUID(self.subentry.unique_id))
100+
101+
@property
102+
def available(self) -> bool:
103+
"""Return True if entity is available."""
104+
105+
return super().available and self.user is not None
106+
107+
async def async_added_to_hass(self) -> None:
108+
"""When entity is added to hass."""
109+
await super().async_added_to_hass()
110+
self.async_on_remove(
111+
self.party_coordinator.async_add_listener(self._handle_coordinator_update)
52112
)
53113

54114

homeassistant/components/habitica/image.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
from __future__ import annotations
44

55
from enum import StrEnum
6+
from typing import TYPE_CHECKING
7+
from uuid import UUID
68

79
from habiticalib import Avatar, ContentData, extract_avatar
810

911
from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription
12+
from homeassistant.config_entries import ConfigSubentry
1013
from homeassistant.core import HomeAssistant
1114
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
1215
from homeassistant.util import dt as dt_util
@@ -18,7 +21,7 @@
1821
HabiticaDataUpdateCoordinator,
1922
HabiticaPartyCoordinator,
2023
)
21-
from .entity import HabiticaBase, HabiticaPartyBase
24+
from .entity import HabiticaBase, HabiticaPartyBase, HabiticaPartyMemberBase
2225

2326
PARALLEL_UPDATES = 1
2427

@@ -47,6 +50,22 @@ async def async_setup_entry(
4750
hass, party_coordinator, config_entry, coordinator.content
4851
)
4952
)
53+
for subentry_id, subentry in config_entry.subentries.items():
54+
if (
55+
subentry.unique_id
56+
and UUID(subentry.unique_id) in party_coordinator.data.members
57+
):
58+
async_add_entities(
59+
[
60+
HabiticaPartyMemberImage(
61+
hass,
62+
coordinator,
63+
party_coordinator,
64+
subentry,
65+
)
66+
],
67+
config_subentry_id=subentry_id,
68+
)
5069

5170
async_add_entities(entities)
5271

@@ -66,18 +85,21 @@ def __init__(
6685
self,
6786
hass: HomeAssistant,
6887
coordinator: HabiticaDataUpdateCoordinator,
88+
subentry: ConfigSubentry | None = None,
6989
) -> None:
7090
"""Initialize the image entity."""
71-
super().__init__(coordinator, self.entity_description)
91+
HabiticaBase.__init__(self, coordinator, self.entity_description, subentry)
7292
ImageEntity.__init__(self, hass)
7393
self._attr_image_last_updated = dt_util.utcnow()
74-
self._avatar = extract_avatar(self.coordinator.data.user)
94+
if TYPE_CHECKING:
95+
assert self.user
96+
self._avatar = extract_avatar(self.user)
7597

7698
def _handle_coordinator_update(self) -> None:
7799
"""Check if equipped gear and other things have changed since last avatar image generation."""
78100

79-
if self._avatar != self.coordinator.data.user:
80-
self._avatar = extract_avatar(self.coordinator.data.user)
101+
if self.user is not None and self._avatar != self.user:
102+
self._avatar = extract_avatar(self.user)
81103
self._attr_image_last_updated = dt_util.utcnow()
82104
self._cache = None
83105

@@ -90,6 +112,24 @@ async def async_image(self) -> bytes | None:
90112
return self._cache
91113

92114

115+
class HabiticaPartyMemberImage(HabiticaImage, HabiticaPartyMemberBase):
116+
"""A Habitica party member image entity."""
117+
118+
def __init__(
119+
self,
120+
hass: HomeAssistant,
121+
coordinator: HabiticaDataUpdateCoordinator,
122+
party_coordinator: HabiticaPartyCoordinator,
123+
subentry: ConfigSubentry | None = None,
124+
) -> None:
125+
"""Initialize the image entity."""
126+
127+
HabiticaPartyMemberBase.__init__(
128+
self, coordinator, party_coordinator, self.entity_description, subentry
129+
)
130+
super().__init__(hass, coordinator, subentry)
131+
132+
93133
class HabiticaPartyImage(HabiticaPartyBase, ImageEntity):
94134
"""A Habitica image entity of a party."""
95135

0 commit comments

Comments
 (0)