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
46 changes: 42 additions & 4 deletions custom_components/rohlikcz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant

from .const import DOMAIN
from .const import (
DOMAIN, CONF_ANALYTICS, DEFAULT_ANALYTICS,
CONF_TOP_N, DEFAULT_TOP_N, CONF_HIDE_DISCONTINUED, DEFAULT_HIDE_DISCONTINUED,
)
from .hub import RohlikAccount
from .services import register_services

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


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

if entry.version < 1:
# Pre-analytics entries: set empty analytics (opt-in)
new_options = {**entry.options, CONF_ANALYTICS: DEFAULT_ANALYTICS}
hass.config_entries.async_update_entry(entry, options=new_options, version=1)
_LOGGER.info("Migrated config entry to version 1 (analytics disabled by default)")

return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Rohlik integration from a config entry flow."""
rohlik_hub = RohlikAccount(hass, entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD])
analytics = entry.options.get(CONF_ANALYTICS, DEFAULT_ANALYTICS)

top_n = int(entry.options.get(CONF_TOP_N, DEFAULT_TOP_N))
hide_discontinued = entry.options.get(CONF_HIDE_DISCONTINUED, DEFAULT_HIDE_DISCONTINUED)
rohlik_hub = RohlikAccount(hass, entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD], analytics=analytics, top_n=top_n, hide_discontinued=hide_discontinued)
await rohlik_hub.async_update()

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = rohlik_hub
Expand All @@ -29,15 +49,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.info("Setting up platforms: %s", PLATFORMS)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
_LOGGER.info("Platforms setup complete")

# If analytics enabled, fetch full order history + enrich in background
if analytics:
async def _fetch_history():
try:
if rohlik_hub.order_store:
await rohlik_hub.fetch_full_order_history(hass=hass)
except Exception as err:
_LOGGER.error("Background order history fetch failed: %s", err)

entry.async_create_background_task(hass, _fetch_history(), "rohlik_fetch_history")

# Reload when options change (user reconfigures analytics)
entry.async_on_unload(entry.add_update_listener(_async_reload_entry))

return True


async def _async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload the config entry when options change."""
await hass.config_entries.async_reload(entry.entry_id)


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok


138 changes: 116 additions & 22 deletions custom_components/rohlikcz/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,61 +3,155 @@

from homeassistant.const import CONF_PASSWORD, CONF_EMAIL
from homeassistant import config_entries
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.selector import (
BooleanSelector,
NumberSelector,
NumberSelectorConfig,
NumberSelectorMode,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
import voluptuous as vol

from .const import DOMAIN
from .const import (
DOMAIN, CONF_ANALYTICS, ANALYTICS_OPTIONS, DEFAULT_ANALYTICS,
CONF_TOP_N, DEFAULT_TOP_N, CONF_HIDE_DISCONTINUED, DEFAULT_HIDE_DISCONTINUED,
)
from .errors import InvalidCredentialsError
from .rohlik_api import RohlikCZAPI



_LOGGER = logging.getLogger(__name__)


async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[str, dict[str, Any]]:
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
"""

api = RohlikCZAPI(data[CONF_EMAIL], data[CONF_PASSWORD]) # type: ignore[Any]

"""Validate the user input allows us to connect."""
api = RohlikCZAPI(data[CONF_EMAIL], data[CONF_PASSWORD])
reply = await api.get_data()

title: str = reply["login"]["data"]["user"]["name"]

return title, data


ANALYTICS_SCHEMA = vol.Schema({
vol.Optional(CONF_ANALYTICS, default=DEFAULT_ANALYTICS): SelectSelector(
SelectSelectorConfig(
options=ANALYTICS_OPTIONS,
multiple=True,
mode=SelectSelectorMode.LIST,
translation_key=CONF_ANALYTICS,
)
),
vol.Optional(CONF_TOP_N, default=DEFAULT_TOP_N): NumberSelector(
NumberSelectorConfig(
min=5,
max=200,
step=5,
mode=NumberSelectorMode.BOX,
)
),
vol.Optional(CONF_HIDE_DISCONTINUED, default=DEFAULT_HIDE_DISCONTINUED): BooleanSelector(),
})


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):

CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
VERSION = 0.1
VERSION = 1

async def async_step_user(self, user_input: dict[str, Any] | None = None) -> config_entries.FlowResult:
def __init__(self) -> None:
super().__init__()
self._user_title: str | None = None
self._user_data: dict[str, Any] = {}

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> config_entries.FlowResult:

data_schema: dict[Any, Any] = {
vol.Required(CONF_EMAIL, default="e-mail"): str,
vol.Required(CONF_PASSWORD, default="password"): str
vol.Required(CONF_PASSWORD, default="password"): str,
}

# Set dict for errors
errors: dict[str, str] = {}

# Steps to take if user input is received
if user_input is not None:
try:
info, data = await validate_input(self.hass, user_input)
return self.async_create_entry(title=info, data=data)
self._user_title = info
self._user_data = data
return await self.async_step_analytics()

except InvalidCredentialsError:
errors["base"] = "Invalid credentials provided"
errors["base"] = "invalid_auth"

except Exception: # pylint: disable=broad-except
except Exception:
_LOGGER.exception("Unknown exception")
errors["base"] = "Unknown exception"
errors["base"] = "unknown"

# If there is no user input or there were errors, show the form again, including any errors that were found with the input.
return self.async_show_form(
step_id="user", data_schema=vol.Schema(data_schema), errors=errors
)

async def async_step_analytics(
self, user_input: dict[str, Any] | None = None
) -> config_entries.FlowResult:
"""Second step: choose analytics levels."""
if user_input is not None:
return self.async_create_entry(
title=self._user_title,
data=self._user_data,
options={
CONF_ANALYTICS: user_input.get(CONF_ANALYTICS, []),
CONF_TOP_N: int(user_input.get(CONF_TOP_N, DEFAULT_TOP_N)),
CONF_HIDE_DISCONTINUED: user_input.get(CONF_HIDE_DISCONTINUED, DEFAULT_HIDE_DISCONTINUED),
},
)

return self.async_show_form(
step_id="analytics",
data_schema=ANALYTICS_SCHEMA,
)

@staticmethod
@callback
def async_get_options_flow(config_entry: config_entries.ConfigEntry):
return RohlikOptionsFlowHandler()


class RohlikOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options for existing entries (reconfigure analytics)."""

async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> config_entries.FlowResult:
if user_input is not None:
user_input[CONF_TOP_N] = int(user_input.get(CONF_TOP_N, DEFAULT_TOP_N))
return self.async_create_entry(title="", data=user_input)

current = self.config_entry.options.get(CONF_ANALYTICS, DEFAULT_ANALYTICS)
current_top_n = self.config_entry.options.get(CONF_TOP_N, DEFAULT_TOP_N)
current_hide = self.config_entry.options.get(CONF_HIDE_DISCONTINUED, DEFAULT_HIDE_DISCONTINUED)

return self.async_show_form(
step_id="init",
data_schema=vol.Schema({
vol.Optional(CONF_ANALYTICS, default=current): SelectSelector(
SelectSelectorConfig(
options=ANALYTICS_OPTIONS,
multiple=True,
mode=SelectSelectorMode.LIST,
translation_key=CONF_ANALYTICS,
)
),
vol.Optional(CONF_TOP_N, default=current_top_n): NumberSelector(
NumberSelectorConfig(
min=5,
max=200,
step=5,
mode=NumberSelectorMode.BOX,
)
),
vol.Optional(CONF_HIDE_DISCONTINUED, default=current_hide): BooleanSelector(),
}),
)
19 changes: 19 additions & 0 deletions custom_components/rohlikcz/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
ICON_INFO = "mdi:information-outline"
ICON_DELIVERY_TIME = "mdi:timer-sand"
ICON_MONTHLY_SPENT = "mdi:cash-register"
ICON_YEARLY_SPENT = "mdi:calendar-text"
ICON_ALLTIME_SPENT = "mdi:chart-line"
ICON_DELIVERY_CALENDAR = "mdi:calendar-clock"
ICON_CATEGORY_SPENDING = "mdi:shape"

""" Service attributes """
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
Expand All @@ -53,3 +56,19 @@
SERVICE_GET_CART_CONTENT = "get_cart_content"
SERVICE_SEARCH_AND_ADD_PRODUCT = "search_and_add_to_cart"
SERVICE_UPDATE_DATA = "update_data"
SERVICE_FETCH_ORDER_HISTORY = "fetch_order_history"

""" Analytics options """
CONF_ANALYTICS = "analytics"
CONF_TOP_N = "top_n"
ANALYTICS_OPTIONS = [
"categories_l0",
"categories_l1",
"categories_l2",
"categories_l3",
"per_item",
]
DEFAULT_ANALYTICS = [] # Nothing enabled by default (opt-in)
DEFAULT_TOP_N = 10
CONF_HIDE_DISCONTINUED = "hide_discontinued"
DEFAULT_HIDE_DISCONTINUED = True
Loading