Skip to content

Commit 56153f2

Browse files
committed
Add support and troubleshooting utilities
Introduces a new support.py module with log collection, sanitization, and downloadable support file generation. Updates config_flow to provide a support menu, log file generation, diagnostics info, and troubleshooting guide. Translations updated for new support features and error handling. __init__.py registers a new HTTP view for secure log file downloads.
1 parent 61533cb commit 56153f2

File tree

5 files changed

+728
-28
lines changed

5 files changed

+728
-28
lines changed

custom_components/dantherm/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import homeassistant.helpers.config_validation as cv
2323
from homeassistant.helpers.storage import Store
2424
from homeassistant.helpers.translation import async_get_translations
25+
from .support import DanthermLogDownloadView
2526

2627
from .const import DEFAULT_NAME, DEFAULT_SCAN_INTERVAL, DOMAIN
2728
from .device import DanthermDevice
@@ -230,6 +231,8 @@ async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
230231

231232
hass.data[DOMAIN] = {}
232233
await async_setup_services(hass)
234+
if hasattr(hass, "http"):
235+
hass.http.register_view(DanthermLogDownloadView(hass))
233236
return True
234237

235238

custom_components/dantherm/config_flow.py

Lines changed: 235 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
POLLING_OPTIONS_LIST,
3232
)
3333
from .helpers import is_primary_entry
34+
from .support import (
35+
async_toggle_debug_logging,
36+
async_collect_integration_logs,
37+
async_create_downloadable_support_file,
38+
)
3439

3540
DATA_SCHEMA = vol.Schema(
3641
{
@@ -157,6 +162,7 @@ class DanthermOptionsFlowHandler(config_entries.OptionsFlow):
157162

158163
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
159164
"""Initialize options flow."""
165+
self._last_download: dict[str, str] | None = None
160166

161167
def _get_polling_speed_from_interval(self, interval: int) -> str:
162168
"""Convert scan interval to polling speed option."""
@@ -190,29 +196,18 @@ def _get_polling_options_with_custom(self) -> list[str]:
190196
async def async_step_init(
191197
self, user_input: dict[str, Any] | None = None
192198
) -> ConfigFlowResult:
193-
"""Show the initial welcome screen with configuration overview."""
194-
if user_input is not None:
195-
# Check if user wants to continue
196-
if not user_input.get("continue", False):
197-
# User unchecked continue, abort the options flow
198-
return self.async_abort(reason="aborted_by_user")
199-
200-
# User wants to continue, proceed to network configuration
201-
return await self.async_step_network()
202-
203-
# Create a schema with a read-only information field
204-
schema = vol.Schema(
205-
{
206-
vol.Optional("continue", default=True): bool,
207-
}
208-
)
209-
210-
return self.async_show_form(
199+
"""Show the main menu with options."""
200+
return self.async_show_menu(
211201
step_id="init",
212-
data_schema=schema,
213-
description_placeholders={},
202+
menu_options=["continue_setup", "support"],
214203
)
215204

205+
async def async_step_continue_setup(
206+
self, user_input: dict[str, Any] | None = None
207+
) -> ConfigFlowResult:
208+
"""Continue with regular setup."""
209+
return await self.async_step_network()
210+
216211
async def async_step_network(
217212
self, user_input: dict[str, Any] | None = None
218213
) -> ConfigFlowResult:
@@ -372,3 +367,223 @@ async def async_step_advanced(
372367
return self.async_create_entry(title="", data={})
373368

374369
return self.async_show_form(step_id="advanced", data_schema=schema)
370+
371+
async def async_step_support(
372+
self, user_input: dict[str, Any] | None = None
373+
) -> ConfigFlowResult:
374+
"""Show support options menu."""
375+
if user_input is not None:
376+
# Handle debug logging toggle
377+
logger = logging.getLogger(f"custom_components.{DOMAIN}")
378+
current_debug = logger.isEnabledFor(logging.DEBUG)
379+
new_debug_setting = user_input.get("debug_logging", current_debug)
380+
381+
# Toggle debug logging if setting changed
382+
if new_debug_setting != current_debug:
383+
await async_toggle_debug_logging(self.hass, new_debug_setting)
384+
385+
# Handle other actions
386+
action = user_input.get("action")
387+
if action == "collect_logs":
388+
# Generate files using shared helper
389+
try:
390+
result = await async_create_downloadable_support_file(
391+
self.hass,
392+
self.config_entry.entry_id,
393+
self.config_entry.title,
394+
prefix="dantherm_support",
395+
)
396+
397+
self._last_download = {
398+
"filename": result["filename"],
399+
"download_url": result["forced_download_url"],
400+
"file_path": result["file_path"],
401+
"timestamp": result["timestamp"],
402+
}
403+
404+
# Ask frontend to open the download URL in a new tab/window
405+
# while keeping the flow alive; user can then click "Done" to continue
406+
return self.async_external_step(
407+
step_id="support_download",
408+
url=result["forced_download_url"],
409+
)
410+
411+
except (OSError, ValueError) as ex:
412+
return self.async_create_entry(
413+
title="Fejl ved fil generering",
414+
data={"error": f"Kunne ikke generere support filer: {ex}"},
415+
)
416+
if action == "diagnostics_info":
417+
return await self.async_step_diagnostics_info()
418+
if action == "troubleshooting":
419+
return await self.async_step_troubleshooting()
420+
if action == "back_to_main":
421+
return await self.async_step_network()
422+
423+
# If no specific action, stay on support page
424+
return await self.async_step_support()
425+
426+
# Check current debug status
427+
logger = logging.getLogger(f"custom_components.{DOMAIN}")
428+
debug_enabled = logger.isEnabledFor(logging.DEBUG)
429+
430+
schema = vol.Schema(
431+
{
432+
vol.Optional("debug_logging", default=debug_enabled): bool,
433+
vol.Optional("action"): vol.In(
434+
{
435+
"collect_logs": "Collect integration logs",
436+
"diagnostics_info": "Generate diagnostics data",
437+
"troubleshooting": "Troubleshooting guide",
438+
"back_to_main": "Back to main configuration",
439+
}
440+
),
441+
}
442+
)
443+
444+
return self.async_show_form(
445+
step_id="support",
446+
data_schema=schema,
447+
description_placeholders={
448+
"device_name": self.config_entry.title,
449+
"debug_status": "enabled" if debug_enabled else "disabled",
450+
# If a file was just generated, provide a link placeholder
451+
"download_link": (
452+
f"[Klik her for at downloade]({self._last_download['download_url']})"
453+
if self._last_download
454+
else ""
455+
),
456+
},
457+
)
458+
459+
async def async_step_support_download_ready(
460+
self, user_input: dict[str, Any] | None = None
461+
) -> ConfigFlowResult:
462+
"""Show a small form with a clickable download link and a back button."""
463+
if not self._last_download:
464+
return await self.async_step_support()
465+
466+
if user_input is not None:
467+
if user_input.get("back", False):
468+
return await self.async_step_support()
469+
470+
schema = vol.Schema({vol.Optional("back", default=False): bool})
471+
472+
return self.async_show_form(
473+
step_id="support_download_ready",
474+
data_schema=schema,
475+
description_placeholders={
476+
"filename": self._last_download["filename"],
477+
"download_url": self._last_download["download_url"],
478+
"download_link": f"[Klik her for at downloade]({self._last_download['download_url']})",
479+
},
480+
)
481+
482+
async def async_step_support_download(
483+
self, user_input: dict[str, Any] | None = None
484+
) -> ConfigFlowResult:
485+
"""External step 'done' handler: return to the download-ready view."""
486+
# Once the external URL was opened, guide user to the view that also shows the link
487+
return self.async_external_step_done(next_step_id="support_download_ready")
488+
489+
async def async_step_collect_logs(
490+
self, user_input: dict[str, Any] | None = None
491+
) -> ConfigFlowResult:
492+
"""Collect integration logs and generate download file."""
493+
if user_input is not None:
494+
if user_input.get("back", False):
495+
return await self.async_step_support()
496+
if user_input.get("generate", False):
497+
# Generate and trigger the log file download using shared helper
498+
try:
499+
result = await async_create_downloadable_support_file(
500+
self.hass,
501+
self.config_entry.entry_id,
502+
self.config_entry.title,
503+
prefix="dantherm_logs",
504+
)
505+
506+
self._last_download = {
507+
"filename": result["filename"],
508+
"download_url": result["forced_download_url"],
509+
"file_path": result["file_path"],
510+
"timestamp": result["timestamp"],
511+
}
512+
513+
# Open the download in a new tab and then show the ready step
514+
return self.async_external_step(
515+
step_id="support_download",
516+
url=result["forced_download_url"],
517+
)
518+
519+
except (OSError, ValueError) as ex:
520+
return self.async_show_form(
521+
step_id="collect_logs",
522+
data_schema=vol.Schema(
523+
{vol.Optional("back", default=False): bool}
524+
),
525+
errors={"base": "log_generation_failed"},
526+
description_placeholders={"error_message": str(ex)},
527+
)
528+
529+
# Show form to generate logs
530+
try:
531+
# Quick preview of available logs
532+
logs_preview = await async_collect_integration_logs(self.hass)
533+
collection_info = logs_preview.get("collection_info", {})
534+
535+
status_parts = [
536+
f"Debug logging: {'aktiveret' if collection_info.get('debug_enabled', False) else 'deaktiveret'}",
537+
f"Log entries tilgængelige: {logs_preview.get('total_entries', 0)}",
538+
"Følsomme data bliver automatisk fjernet",
539+
]
540+
541+
status = " • ".join(status_parts)
542+
543+
except (AttributeError, ValueError, RuntimeError):
544+
status = "Klar til at generere log fil"
545+
546+
schema = vol.Schema(
547+
{
548+
vol.Optional("generate", default=False): bool,
549+
vol.Optional("back", default=False): bool,
550+
}
551+
)
552+
553+
return self.async_show_form(
554+
step_id="collect_logs",
555+
data_schema=schema,
556+
description_placeholders={"log_status": status},
557+
)
558+
559+
async def async_step_diagnostics_info(
560+
self, user_input: dict[str, Any] | None = None
561+
) -> ConfigFlowResult:
562+
"""Show diagnostics information."""
563+
if user_input is not None:
564+
if user_input.get("back", False):
565+
return await self.async_step_support()
566+
567+
schema = vol.Schema({vol.Optional("back", default=False): bool})
568+
569+
return self.async_show_form(
570+
step_id="diagnostics_info",
571+
data_schema=schema,
572+
description_placeholders={"device_name": self.config_entry.title},
573+
)
574+
575+
async def async_step_troubleshooting(
576+
self, user_input: dict[str, Any] | None = None
577+
) -> ConfigFlowResult:
578+
"""Show troubleshooting guide."""
579+
if user_input is not None:
580+
if user_input.get("back", False):
581+
return await self.async_step_support()
582+
583+
schema = vol.Schema({vol.Optional("back", default=False): bool})
584+
585+
return self.async_show_form(
586+
step_id="troubleshooting",
587+
data_schema=schema,
588+
description_placeholders={"device_name": self.config_entry.title},
589+
)

0 commit comments

Comments
 (0)