Skip to content

Commit 31293ea

Browse files
authored
Merge pull request #64 from kwaczek/feat/order-history-sensors
feat: order history & spending analytics sensors
2 parents d89f4be + 338c263 commit 31293ea

17 files changed

Lines changed: 3728 additions & 35 deletions

custom_components/rohlikcz/__init__.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
88
from homeassistant.core import HomeAssistant
99

10-
from .const import DOMAIN
10+
from .const import (
11+
DOMAIN, CONF_ANALYTICS, DEFAULT_ANALYTICS,
12+
CONF_TOP_N, DEFAULT_TOP_N, CONF_HIDE_DISCONTINUED, DEFAULT_HIDE_DISCONTINUED,
13+
)
1114
from .hub import RohlikAccount
1215
from .services import register_services
1316

@@ -16,9 +19,26 @@
1619
PLATFORMS: list[str] = ["sensor", "binary_sensor", "todo", "calendar"]
1720

1821

22+
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
23+
"""Migrate old config entries to new format."""
24+
_LOGGER.debug("Migrating from version %s", entry.version)
25+
26+
if entry.version < 1:
27+
# Pre-analytics entries: set empty analytics (opt-in)
28+
new_options = {**entry.options, CONF_ANALYTICS: DEFAULT_ANALYTICS}
29+
hass.config_entries.async_update_entry(entry, options=new_options, version=1)
30+
_LOGGER.info("Migrated config entry to version 1 (analytics disabled by default)")
31+
32+
return True
33+
34+
1935
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
2036
"""Set up Rohlik integration from a config entry flow."""
21-
rohlik_hub = RohlikAccount(hass, entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD])
37+
analytics = entry.options.get(CONF_ANALYTICS, DEFAULT_ANALYTICS)
38+
39+
top_n = int(entry.options.get(CONF_TOP_N, DEFAULT_TOP_N))
40+
hide_discontinued = entry.options.get(CONF_HIDE_DISCONTINUED, DEFAULT_HIDE_DISCONTINUED)
41+
rohlik_hub = RohlikAccount(hass, entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD], analytics=analytics, top_n=top_n, hide_discontinued=hide_discontinued)
2242
await rohlik_hub.async_update()
2343

2444
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = rohlik_hub
@@ -29,15 +49,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
2949
_LOGGER.info("Setting up platforms: %s", PLATFORMS)
3050
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
3151
_LOGGER.info("Platforms setup complete")
52+
53+
# If analytics enabled, fetch full order history + enrich in background
54+
if analytics:
55+
async def _fetch_history():
56+
try:
57+
if rohlik_hub.order_store:
58+
await rohlik_hub.fetch_full_order_history(hass=hass)
59+
except Exception as err:
60+
_LOGGER.error("Background order history fetch failed: %s", err)
61+
62+
entry.async_create_background_task(hass, _fetch_history(), "rohlik_fetch_history")
63+
64+
# Reload when options change (user reconfigures analytics)
65+
entry.async_on_unload(entry.add_update_listener(_async_reload_entry))
66+
3267
return True
3368

3469

70+
async def _async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
71+
"""Reload the config entry when options change."""
72+
await hass.config_entries.async_reload(entry.entry_id)
73+
74+
3575
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
3676
"""Unload a config entry."""
3777
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
3878
if unload_ok:
3979
hass.data[DOMAIN].pop(entry.entry_id)
4080

4181
return unload_ok
42-
43-

custom_components/rohlikcz/config_flow.py

Lines changed: 116 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,61 +3,155 @@
33

44
from homeassistant.const import CONF_PASSWORD, CONF_EMAIL
55
from homeassistant import config_entries
6-
from homeassistant.core import HomeAssistant
6+
from homeassistant.core import HomeAssistant, callback
7+
from homeassistant.helpers.selector import (
8+
BooleanSelector,
9+
NumberSelector,
10+
NumberSelectorConfig,
11+
NumberSelectorMode,
12+
SelectSelector,
13+
SelectSelectorConfig,
14+
SelectSelectorMode,
15+
)
716
import voluptuous as vol
817

9-
from .const import DOMAIN
18+
from .const import (
19+
DOMAIN, CONF_ANALYTICS, ANALYTICS_OPTIONS, DEFAULT_ANALYTICS,
20+
CONF_TOP_N, DEFAULT_TOP_N, CONF_HIDE_DISCONTINUED, DEFAULT_HIDE_DISCONTINUED,
21+
)
1022
from .errors import InvalidCredentialsError
1123
from .rohlik_api import RohlikCZAPI
1224

13-
14-
1525
_LOGGER = logging.getLogger(__name__)
1626

1727

1828
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[str, dict[str, Any]]:
19-
"""Validate the user input allows us to connect.
20-
Data has the keys from DATA_SCHEMA with values provided by the user.
21-
"""
22-
23-
api = RohlikCZAPI(data[CONF_EMAIL], data[CONF_PASSWORD]) # type: ignore[Any]
24-
29+
"""Validate the user input allows us to connect."""
30+
api = RohlikCZAPI(data[CONF_EMAIL], data[CONF_PASSWORD])
2531
reply = await api.get_data()
26-
2732
title: str = reply["login"]["data"]["user"]["name"]
28-
2933
return title, data
3034

3135

36+
ANALYTICS_SCHEMA = vol.Schema({
37+
vol.Optional(CONF_ANALYTICS, default=DEFAULT_ANALYTICS): SelectSelector(
38+
SelectSelectorConfig(
39+
options=ANALYTICS_OPTIONS,
40+
multiple=True,
41+
mode=SelectSelectorMode.LIST,
42+
translation_key=CONF_ANALYTICS,
43+
)
44+
),
45+
vol.Optional(CONF_TOP_N, default=DEFAULT_TOP_N): NumberSelector(
46+
NumberSelectorConfig(
47+
min=5,
48+
max=200,
49+
step=5,
50+
mode=NumberSelectorMode.BOX,
51+
)
52+
),
53+
vol.Optional(CONF_HIDE_DISCONTINUED, default=DEFAULT_HIDE_DISCONTINUED): BooleanSelector(),
54+
})
55+
56+
3257
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
3358

3459
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
35-
VERSION = 0.1
60+
VERSION = 1
3661

37-
async def async_step_user(self, user_input: dict[str, Any] | None = None) -> config_entries.FlowResult:
62+
def __init__(self) -> None:
63+
super().__init__()
64+
self._user_title: str | None = None
65+
self._user_data: dict[str, Any] = {}
66+
67+
async def async_step_user(
68+
self, user_input: dict[str, Any] | None = None
69+
) -> config_entries.FlowResult:
3870

3971
data_schema: dict[Any, Any] = {
4072
vol.Required(CONF_EMAIL, default="e-mail"): str,
41-
vol.Required(CONF_PASSWORD, default="password"): str
73+
vol.Required(CONF_PASSWORD, default="password"): str,
4274
}
4375

44-
# Set dict for errors
4576
errors: dict[str, str] = {}
4677

47-
# Steps to take if user input is received
4878
if user_input is not None:
4979
try:
5080
info, data = await validate_input(self.hass, user_input)
51-
return self.async_create_entry(title=info, data=data)
81+
self._user_title = info
82+
self._user_data = data
83+
return await self.async_step_analytics()
5284

5385
except InvalidCredentialsError:
54-
errors["base"] = "Invalid credentials provided"
86+
errors["base"] = "invalid_auth"
5587

56-
except Exception: # pylint: disable=broad-except
88+
except Exception:
5789
_LOGGER.exception("Unknown exception")
58-
errors["base"] = "Unknown exception"
90+
errors["base"] = "unknown"
5991

60-
# If there is no user input or there were errors, show the form again, including any errors that were found with the input.
6192
return self.async_show_form(
6293
step_id="user", data_schema=vol.Schema(data_schema), errors=errors
6394
)
95+
96+
async def async_step_analytics(
97+
self, user_input: dict[str, Any] | None = None
98+
) -> config_entries.FlowResult:
99+
"""Second step: choose analytics levels."""
100+
if user_input is not None:
101+
return self.async_create_entry(
102+
title=self._user_title,
103+
data=self._user_data,
104+
options={
105+
CONF_ANALYTICS: user_input.get(CONF_ANALYTICS, []),
106+
CONF_TOP_N: int(user_input.get(CONF_TOP_N, DEFAULT_TOP_N)),
107+
CONF_HIDE_DISCONTINUED: user_input.get(CONF_HIDE_DISCONTINUED, DEFAULT_HIDE_DISCONTINUED),
108+
},
109+
)
110+
111+
return self.async_show_form(
112+
step_id="analytics",
113+
data_schema=ANALYTICS_SCHEMA,
114+
)
115+
116+
@staticmethod
117+
@callback
118+
def async_get_options_flow(config_entry: config_entries.ConfigEntry):
119+
return RohlikOptionsFlowHandler()
120+
121+
122+
class RohlikOptionsFlowHandler(config_entries.OptionsFlow):
123+
"""Handle options for existing entries (reconfigure analytics)."""
124+
125+
async def async_step_init(
126+
self, user_input: dict[str, Any] | None = None
127+
) -> config_entries.FlowResult:
128+
if user_input is not None:
129+
user_input[CONF_TOP_N] = int(user_input.get(CONF_TOP_N, DEFAULT_TOP_N))
130+
return self.async_create_entry(title="", data=user_input)
131+
132+
current = self.config_entry.options.get(CONF_ANALYTICS, DEFAULT_ANALYTICS)
133+
current_top_n = self.config_entry.options.get(CONF_TOP_N, DEFAULT_TOP_N)
134+
current_hide = self.config_entry.options.get(CONF_HIDE_DISCONTINUED, DEFAULT_HIDE_DISCONTINUED)
135+
136+
return self.async_show_form(
137+
step_id="init",
138+
data_schema=vol.Schema({
139+
vol.Optional(CONF_ANALYTICS, default=current): SelectSelector(
140+
SelectSelectorConfig(
141+
options=ANALYTICS_OPTIONS,
142+
multiple=True,
143+
mode=SelectSelectorMode.LIST,
144+
translation_key=CONF_ANALYTICS,
145+
)
146+
),
147+
vol.Optional(CONF_TOP_N, default=current_top_n): NumberSelector(
148+
NumberSelectorConfig(
149+
min=5,
150+
max=200,
151+
step=5,
152+
mode=NumberSelectorMode.BOX,
153+
)
154+
),
155+
vol.Optional(CONF_HIDE_DISCONTINUED, default=current_hide): BooleanSelector(),
156+
}),
157+
)

custom_components/rohlikcz/const.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@
3535
ICON_INFO = "mdi:information-outline"
3636
ICON_DELIVERY_TIME = "mdi:timer-sand"
3737
ICON_MONTHLY_SPENT = "mdi:cash-register"
38+
ICON_YEARLY_SPENT = "mdi:calendar-text"
39+
ICON_ALLTIME_SPENT = "mdi:chart-line"
3840
ICON_DELIVERY_CALENDAR = "mdi:calendar-clock"
41+
ICON_CATEGORY_SPENDING = "mdi:shape"
3942

4043
""" Service attributes """
4144
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
@@ -53,3 +56,19 @@
5356
SERVICE_GET_CART_CONTENT = "get_cart_content"
5457
SERVICE_SEARCH_AND_ADD_PRODUCT = "search_and_add_to_cart"
5558
SERVICE_UPDATE_DATA = "update_data"
59+
SERVICE_FETCH_ORDER_HISTORY = "fetch_order_history"
60+
61+
""" Analytics options """
62+
CONF_ANALYTICS = "analytics"
63+
CONF_TOP_N = "top_n"
64+
ANALYTICS_OPTIONS = [
65+
"categories_l0",
66+
"categories_l1",
67+
"categories_l2",
68+
"categories_l3",
69+
"per_item",
70+
]
71+
DEFAULT_ANALYTICS = [] # Nothing enabled by default (opt-in)
72+
DEFAULT_TOP_N = 10
73+
CONF_HIDE_DISCONTINUED = "hide_discontinued"
74+
DEFAULT_HIDE_DISCONTINUED = True

0 commit comments

Comments
 (0)