Skip to content
Merged
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
35 changes: 16 additions & 19 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,38 +33,33 @@ body:
validations:
required: true

- type: dropdown
# Text boxes rather than drop-downs: GitHub only pre-fills text fields from
# a link, and the panel's Help & feedback page fills these in for you.

- type: input
id: ha_install_type
attributes:
label: Home Assistant Installation Type
options:
- Home Assistant OS (HAOS)
- Home Assistant Supervised
- Home Assistant Container (Docker)
- Home Assistant Core
- Not sure
description: "One of: Home Assistant OS (HAOS), Home Assistant Supervised, Home Assistant Container (Docker), Home Assistant Core, Not sure. Found in Settings → System → About"
placeholder: e.g. Home Assistant OS (HAOS)
validations:
required: true

- type: dropdown
- type: input
id: install_method
attributes:
label: TaskMate Installation Method
options:
- HACS (custom repository)
- Manual (copied files)
description: "One of: HACS (default), HACS (custom repository), Manual (copied files)"
placeholder: e.g. HACS (default)
validations:
required: true

- type: dropdown
- type: input
id: lovelace_mode
attributes:
label: Lovelace Mode
description: Found in Settings → Dashboards → ⋮ menu → Edit → Manage Resources
options:
- Storage (default — managed via UI)
- YAML (configuration.yaml)
- Not sure
description: "One of: Storage (default — managed via UI), YAML (configuration.yaml), Not sure. Found in Settings → Dashboards → ⋮ menu → Edit → Manage Resources"
placeholder: e.g. Storage (default — managed via UI)
validations:
required: true

Expand Down Expand Up @@ -111,7 +106,9 @@ body:
- Sometimes — happens intermittently
- Once — only happened one time
validations:
required: true
# Optional so a report from the panel (which can't pre-select a
# drop-down) isn't asked twice; the panel puts its answer in the text.
required: false

# ── Affected Area ─────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -149,7 +146,7 @@ body:
- Bonus points (weekend / streak / perfect week)
- Other
validations:
required: true
required: false

# ── Sensor & Entity State ─────────────────────────────────────────────────────

Expand Down
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ body:
- New card (not listed above)
- Other / not sure
validations:
required: true
required: false

# ── Alternatives ─────────────────────────────────────────────────────────────

Expand All @@ -114,7 +114,7 @@ body:
- Important — a gap that affects my daily use
- Critical — a blocker for me using TaskMate properly
validations:
required: true
required: false

# ── Use Case ──────────────────────────────────────────────────────────────────

Expand Down
128 changes: 128 additions & 0 deletions custom_components/taskmate/feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""What the admin panel's Help & feedback page fills into a GitHub issue.

The page builds a pre-filled ``issues/new`` link; nothing is sent from Home
Assistant. This gathers the environment half of a bug report — the bit people
get wrong or leave out by hand: versions, how HA and TaskMate are installed,
the Lovelace resource mode, rough sizes and the health check's issue codes.

It never says who the family are: counts only, no names, and the health
check contributes its issue *codes*, not its messages (those name chores).
"""

from __future__ import annotations

import logging
from typing import Any

import homeassistant.const as ha_const
from homeassistant.core import HomeAssistant
from homeassistant.loader import async_get_integration

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

REPO_FULL_NAME = "tempus2016/taskmate"

# HA's system_info installation_type → the bug template's dropdown options.
_INSTALL_TYPES = {
"Home Assistant OS": "haos",
"Home Assistant Supervised": "supervised",
"Home Assistant Container": "container",
"Unsupported Third Party Container": "container",
"Home Assistant Core": "core",
}


async def _installation_type(hass: HomeAssistant) -> tuple[str, str]:
"""(template key, HA's own wording, e.g. "Home Assistant OS")."""
try:
from homeassistant.helpers.system_info import async_get_system_info

info = await async_get_system_info(hass)
except Exception: # noqa: BLE001 — a report without it is still a report
_LOGGER.debug("TaskMate feedback: system info unavailable", exc_info=True)
return "unknown", ""
raw = str(info.get("installation_type") or "")
return _INSTALL_TYPES.get(raw, "unknown"), raw


def _install_method(hass: HomeAssistant) -> str:
"""How TaskMate itself was installed: hacs_default, hacs_custom, manual or unknown.

Reads HACS's in-memory repository list. That is HACS's internal API, so
any surprise in its shape degrades to "unknown" rather than failing.
"""
if "hacs" not in hass.config.components:
return "manual"
try:
hacs = hass.data.get("hacs")
repo = hacs.repositories.get_by_full_name(REPO_FULL_NAME)
if repo is None:
return "manual"
if not getattr(repo.data, "installed", True):
return "manual"
return "hacs_default" if hacs.repositories.is_default(str(repo.data.id)) else "hacs_custom"
except Exception: # noqa: BLE001
_LOGGER.debug("TaskMate feedback: could not read HACS repositories", exc_info=True)
return "unknown"


def _lovelace_mode(hass: HomeAssistant) -> str:
"""storage, yaml or unknown — where Lovelace resources are managed.

HA keeps this as a dataclass since 2024.12 (``mode``, later renamed
``resource_mode``) and as a plain dict before it.
"""
data = hass.data.get("lovelace")
if data is None:
return "unknown"
if isinstance(data, dict):
mode = data.get("resource_mode") or data.get("mode")
else:
mode = getattr(data, "resource_mode", None) or getattr(data, "mode", None)
mode = str(mode or "").lower()
return mode if mode in ("storage", "yaml") else "unknown"


def _entry_id(hass: HomeAssistant, coordinator) -> str:
entry = getattr(coordinator, "config_entry", None)
if entry is not None and getattr(entry, "entry_id", None):
return str(entry.entry_id)
for key, value in hass.data.get(DOMAIN, {}).items():
if value is coordinator:
return str(key)
return ""


async def async_feedback_info(hass: HomeAssistant, coordinator) -> dict[str, Any]:
"""The environment summary for a bug report or feature request."""
integration = await async_get_integration(hass, DOMAIN)
install_type, install_type_raw = await _installation_type(hass)
storage = coordinator.storage
children = storage.get_children()
try:
health = coordinator.health_report()
health_codes = sorted({str(i.get("code", "")) for i in health.get("issues", []) if i.get("code")})
except Exception: # noqa: BLE001
_LOGGER.debug("TaskMate feedback: health report failed", exc_info=True)
health_codes = []
settings = storage.get_settings() or {}
return {
"integration_version": str(integration.version or ""),
"ha_version": str(getattr(ha_const, "__version__", "")),
"installation_type": install_type,
"installation_type_raw": install_type_raw,
"install_method": _install_method(hass),
"lovelace_mode": _lovelace_mode(hass),
"language": str(getattr(hass.config, "language", "") or ""),
"counts": {
"children": sum(1 for c in children if not getattr(c, "is_adult", False)),
"adults": sum(1 for c in children if getattr(c, "is_adult", False)),
"chores": len(storage.get_chores()),
"rewards": len(storage.get_rewards()),
},
"card_design": str(settings.get("card_design") or "classic"),
"health_codes": health_codes,
"entry_id": _entry_id(hass, coordinator),
}
15 changes: 15 additions & 0 deletions custom_components/taskmate/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@
WS_REPORT_HEALTH: Final = "taskmate/reports/health"
WS_REPORT_QUALITY: Final = "taskmate/reports/quality"
WS_REPORT_MOODS: Final = "taskmate/reports/moods"
# Help & feedback page: the environment half of a GitHub issue.
WS_FEEDBACK_INFO: Final = "taskmate/feedback/info"
WS_SCHEDULED_LIST: Final = "taskmate/scheduled/list"
WS_SCHEDULED_ADD: Final = "taskmate/scheduled/add"
WS_SCHEDULED_REMOVE: Final = "taskmate/scheduled/remove"
Expand Down Expand Up @@ -269,6 +271,7 @@
WS_REPORT_HEALTH,
WS_REPORT_QUALITY,
WS_REPORT_MOODS,
WS_FEEDBACK_INFO,
WS_SETUP_CATALOGUE,
}

Expand Down Expand Up @@ -1150,6 +1153,17 @@ async def _ws_report_health(hass, connection, msg, coordinator):
connection.send_result(msg["id"], coordinator.health_report())


@websocket_api.websocket_command({vol.Required("type"): WS_FEEDBACK_INFO})
@websocket_api.async_response
@_admin_only
async def _ws_feedback_info(hass, connection, msg, coordinator):
# Imported here: feedback.py needs homeassistant.loader, which nothing
# else on this module's import path does.
from .feedback import async_feedback_info

connection.send_result(msg["id"], await async_feedback_info(hass, coordinator))


# ---------------------------------------------------------------------------
# Scheduled config changes (#675)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -3290,6 +3304,7 @@ async def _ws_config_import(hass, connection, msg, coordinator):
_ws_report_friction,
_ws_report_projection,
_ws_report_health,
_ws_feedback_info,
_ws_report_quality,
_ws_report_moods,
_ws_templates_export,
Expand Down
Loading
Loading