fix: Resolve production issues — history dir creation and sensor attributes > 16KB

History:
- Add defensive os.makedirs in _sync_test_directory_write to ensure
  directory exists before write test (fixes ERRNO 2 on fresh install)

Sensor attributes (fixes Recorder 16KB limit violation):
- Reduce conversation_history preview: 3 entries × 256 chars (was 5 × 4096)
- Reduce last response/question truncation to 2048 chars (was 4096)
- Reduce system_prompt in attributes to 512 chars (was 4096)
- Full data remains accessible via ha_text_ai.get_history service
This commit is contained in:
SMKRV
2026-03-12 12:43:03 +03:00
parent c45828953d
commit 4e3b4cfdb7
2 changed files with 19 additions and 14 deletions
+1
View File
@@ -123,6 +123,7 @@ class HistoryManager:
@staticmethod @staticmethod
def _sync_test_directory_write(test_file_path: str) -> None: def _sync_test_directory_write(test_file_path: str) -> None:
try: try:
os.makedirs(os.path.dirname(test_file_path), mode=0o755, exist_ok=True)
with open(test_file_path, "w") as f: with open(test_file_path, "w") as f:
f.write("Permission test") f.write("Permission test")
os.remove(test_file_path) os.remove(test_file_path)
+18 -14
View File
@@ -46,7 +46,6 @@ from .const import (
ATTR_SYSTEM_PROMPT, ATTR_SYSTEM_PROMPT,
ATTR_RESPONSE, ATTR_RESPONSE,
ATTR_QUESTION, ATTR_QUESTION,
ATTR_CONVERSATION_HISTORY,
METRIC_TOTAL_TOKENS, METRIC_TOTAL_TOKENS,
METRIC_PROMPT_TOKENS, METRIC_PROMPT_TOKENS,
METRIC_COMPLETION_TOKENS, METRIC_COMPLETION_TOKENS,
@@ -67,7 +66,6 @@ from .const import (
ENTITY_ICON_PROCESSING, ENTITY_ICON_PROCESSING,
DEFAULT_NAME_PREFIX, DEFAULT_NAME_PREFIX,
CONF_MAX_HISTORY_SIZE, CONF_MAX_HISTORY_SIZE,
MAX_ATTRIBUTE_SIZE,
VERSION, VERSION,
) )
@@ -76,6 +74,11 @@ from .utils import safe_log_data
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# HA Recorder limit is 16384 bytes for state_attributes.
# Budget per field to stay well within the limit.
_ATTR_TEXT_LIMIT = 2048
_ATTR_PROMPT_LIMIT = 512
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@@ -243,7 +246,7 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
ATTR_TOTAL_ERRORS: metrics.get("total_errors", 0), ATTR_TOTAL_ERRORS: metrics.get("total_errors", 0),
"instance_name": self._instance_name, "instance_name": self._instance_name,
"normalized_name": self._normalized_name, "normalized_name": self._normalized_name,
ATTR_SYSTEM_PROMPT: (data.get("system_prompt", "")[:MAX_ATTRIBUTE_SIZE] ATTR_SYSTEM_PROMPT: (data.get("system_prompt", "")[:_ATTR_PROMPT_LIMIT]
if data.get("system_prompt") else None), if data.get("system_prompt") else None),
ATTR_IS_PROCESSING: data.get("is_processing", False), ATTR_IS_PROCESSING: data.get("is_processing", False),
ATTR_IS_RATE_LIMITED: data.get("is_rate_limited", False), ATTR_IS_RATE_LIMITED: data.get("is_rate_limited", False),
@@ -253,18 +256,19 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
ATTR_HISTORY_SIZE: data.get("history_size", 0), ATTR_HISTORY_SIZE: data.get("history_size", 0),
} }
# History limit # Conversation history preview (compact: last 3, truncated to 256 chars).
# Full history is available via ha_text_ai.get_history service.
conversation_history = data.get("conversation_history", []) conversation_history = data.get("conversation_history", [])
if conversation_history: if conversation_history:
limited_history = [] preview = conversation_history[-3:]
for entry in conversation_history: attributes["conversation_history"] = [
limited_entry = { {
"timestamp": entry["timestamp"], "timestamp": entry["timestamp"],
"question": entry["question"][:MAX_ATTRIBUTE_SIZE], "question": entry["question"][:256],
"response": entry["response"][:MAX_ATTRIBUTE_SIZE] "response": entry["response"][:256],
} }
limited_history.append(limited_entry) for entry in preview
attributes[ATTR_CONVERSATION_HISTORY] = limited_history ]
# Metrics # Metrics
if isinstance(metrics, dict): if isinstance(metrics, dict):
@@ -283,11 +287,11 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
last_response = data.get("last_response", {}) last_response = data.get("last_response", {})
if isinstance(last_response, dict): if isinstance(last_response, dict):
attributes.update({ attributes.update({
ATTR_RESPONSE: last_response.get("response", "")[:MAX_ATTRIBUTE_SIZE], ATTR_RESPONSE: last_response.get("response", "")[:_ATTR_TEXT_LIMIT],
ATTR_QUESTION: last_response.get("question", "")[:MAX_ATTRIBUTE_SIZE], ATTR_QUESTION: last_response.get("question", "")[:_ATTR_TEXT_LIMIT],
"last_model": last_response.get("model", ""), "last_model": last_response.get("model", ""),
"last_timestamp": last_response.get("timestamp", ""), "last_timestamp": last_response.get("timestamp", ""),
"last_error": (last_response.get("error", "")[:MAX_ATTRIBUTE_SIZE] "last_error": (last_response.get("error", "")[:_ATTR_TEXT_LIMIT]
if last_response.get("error") else None), if last_response.get("error") else None),
}) })