mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-27 17:44:01 +08:00
Release v2.0.0
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
"""Data coordinator for HA text AI."""
|
"""DataUpdateCoordinator for HA Text AI."""
|
||||||
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
import time
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@@ -9,10 +8,19 @@ from homeassistant.exceptions import HomeAssistantError
|
|||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
DOMAIN,
|
||||||
|
STATE_READY,
|
||||||
|
STATE_PROCESSING,
|
||||||
|
STATE_ERROR,
|
||||||
|
STATE_RATE_LIMITED,
|
||||||
|
STATE_MAINTENANCE,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
class HATextAICoordinator(DataUpdateCoordinator):
|
class HATextAICoordinator(DataUpdateCoordinator):
|
||||||
"""Class to manage fetching data from API."""
|
"""Class to manage fetching data from the API."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -28,6 +36,7 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize coordinator."""
|
"""Initialize coordinator."""
|
||||||
update_interval_td = timedelta(seconds=update_interval)
|
update_interval_td = timedelta(seconds=update_interval)
|
||||||
|
self.instance_name = instance_name
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
@@ -36,37 +45,38 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
update_interval=update_interval_td,
|
update_interval=update_interval_td,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.instance_name = instance_name
|
self.hass = hass
|
||||||
self.client = client
|
self.client = client
|
||||||
self.model = model
|
self.model = model
|
||||||
self.max_tokens = max_tokens
|
|
||||||
self.temperature = temperature
|
self.temperature = temperature
|
||||||
self._max_history_size = max_history_size
|
self.max_tokens = max_tokens
|
||||||
self._is_anthropic = is_anthropic
|
self.max_history_size = max_history_size
|
||||||
|
self.is_anthropic = is_anthropic
|
||||||
|
|
||||||
|
# Регистрируем instance в системе
|
||||||
|
self.hass.data.setdefault(DOMAIN, {})
|
||||||
|
self.hass.data[DOMAIN][instance_name] = self
|
||||||
|
|
||||||
self._system_prompt = None
|
self._system_prompt = None
|
||||||
|
self._conversation_history = []
|
||||||
|
self._performance_metrics = {
|
||||||
|
"total_tokens": 0,
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 0,
|
||||||
|
"successful_requests": 0,
|
||||||
|
"failed_requests": 0,
|
||||||
|
"total_errors": 0,
|
||||||
|
"average_latency": 0,
|
||||||
|
"max_latency": 0,
|
||||||
|
"min_latency": float('inf'),
|
||||||
|
}
|
||||||
|
|
||||||
# Статусы
|
|
||||||
self._is_processing = False
|
self._is_processing = False
|
||||||
self._is_rate_limited = False
|
self._is_rate_limited = False
|
||||||
self._is_maintenance = False
|
self._is_maintenance = False
|
||||||
|
self.endpoint_status = "ready"
|
||||||
|
|
||||||
# История и метрики
|
# Инициализация last_response
|
||||||
self._history: List[Dict[str, Any]] = []
|
|
||||||
self._request_count = 0
|
|
||||||
self._error_count = 0
|
|
||||||
self._last_error = None
|
|
||||||
self._performance_metrics = {
|
|
||||||
"total_requests": 0,
|
|
||||||
"total_errors": 0,
|
|
||||||
"avg_response_time": 0.0,
|
|
||||||
"last_request_time": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Версия API и последний ответ
|
|
||||||
self.api_version = "v1"
|
|
||||||
self.last_response = None
|
|
||||||
self.endpoint_status = "initialized"
|
|
||||||
|
|
||||||
self.last_response = {
|
self.last_response = {
|
||||||
"timestamp": dt_util.utcnow().isoformat(),
|
"timestamp": dt_util.utcnow().isoformat(),
|
||||||
"question": "",
|
"question": "",
|
||||||
@@ -74,24 +84,39 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
"model": model,
|
"model": model,
|
||||||
"instance": instance_name,
|
"instance": instance_name,
|
||||||
"error": None
|
"error": None
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
self._start_time = dt_util.utcnow()
|
||||||
def is_processing(self) -> bool:
|
_LOGGER.info(f"Initialized HA Text AI coordinator with instance: {instance_name}")
|
||||||
"""Return True if currently processing a request."""
|
|
||||||
return self._is_processing
|
|
||||||
|
|
||||||
@property
|
async def _async_update_data(self) -> Dict[str, Any]:
|
||||||
def is_rate_limited(self) -> bool:
|
"""Update data via library."""
|
||||||
"""Return True if currently rate limited."""
|
return {
|
||||||
return self._is_rate_limited
|
"state": self._get_current_state(),
|
||||||
|
"metrics": self._performance_metrics,
|
||||||
|
"last_response": self.last_response,
|
||||||
|
"is_processing": self._is_processing,
|
||||||
|
"is_rate_limited": self._is_rate_limited,
|
||||||
|
"is_maintenance": self._is_maintenance,
|
||||||
|
"endpoint_status": self.endpoint_status,
|
||||||
|
"uptime": (dt_util.utcnow() - self._start_time).total_seconds(),
|
||||||
|
"system_prompt": self._system_prompt,
|
||||||
|
"history_size": len(self._conversation_history),
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
def _get_current_state(self) -> str:
|
||||||
def is_maintenance(self) -> bool:
|
"""Get current state based on internal flags."""
|
||||||
"""Return True if in maintenance mode."""
|
if self._is_processing:
|
||||||
return self._is_maintenance
|
return STATE_PROCESSING
|
||||||
|
elif self._is_rate_limited:
|
||||||
|
return STATE_RATE_LIMITED
|
||||||
|
elif self._is_maintenance:
|
||||||
|
return STATE_MAINTENANCE
|
||||||
|
elif self.last_response.get("error"):
|
||||||
|
return STATE_ERROR
|
||||||
|
return STATE_READY
|
||||||
|
|
||||||
async def async_ask_question(
|
async def async_process_question(
|
||||||
self,
|
self,
|
||||||
question: str,
|
question: str,
|
||||||
model: Optional[str] = None,
|
model: Optional[str] = None,
|
||||||
@@ -100,12 +125,22 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
system_prompt: Optional[str] = None,
|
system_prompt: Optional[str] = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Process a question with optional parameters."""
|
"""Process a question with optional parameters."""
|
||||||
|
if not question:
|
||||||
|
raise ValueError("Question cannot be empty")
|
||||||
|
|
||||||
|
_LOGGER.debug(f"Processing question for instance {self.instance_name}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
self._is_processing = True
|
||||||
|
await self.async_update_ha_state()
|
||||||
|
|
||||||
temp_model = model or self.model
|
temp_model = model or self.model
|
||||||
temp_temperature = temperature or self.temperature
|
temp_temperature = temperature or self.temperature
|
||||||
temp_max_tokens = max_tokens or self.max_tokens
|
temp_max_tokens = max_tokens or self.max_tokens
|
||||||
temp_system_prompt = system_prompt or self._system_prompt
|
temp_system_prompt = system_prompt or self._system_prompt
|
||||||
|
|
||||||
|
start_time = dt_util.utcnow()
|
||||||
|
|
||||||
messages = []
|
messages = []
|
||||||
if temp_system_prompt:
|
if temp_system_prompt:
|
||||||
messages.append({"role": "system", "content": temp_system_prompt})
|
messages.append({"role": "system", "content": temp_system_prompt})
|
||||||
@@ -118,162 +153,127 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
"messages": messages,
|
"messages": messages,
|
||||||
}
|
}
|
||||||
|
|
||||||
return await self.async_process_message(question, **kwargs)
|
response = await self.async_process_message(question, **kwargs)
|
||||||
|
|
||||||
|
# Обновляем метрики
|
||||||
|
end_time = dt_util.utcnow()
|
||||||
|
latency = (end_time - start_time).total_seconds()
|
||||||
|
self._update_metrics(latency, response)
|
||||||
|
|
||||||
|
# Обновляем историю
|
||||||
|
self._update_history(question, response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_LOGGER.error("Error processing question: %s", err)
|
self._handle_error(err)
|
||||||
raise HomeAssistantError(f"Failed to process question: {err}")
|
raise HomeAssistantError(f"Failed to process question: {err}")
|
||||||
|
|
||||||
async def _async_update_data(self) -> Dict[str, Any]:
|
finally:
|
||||||
"""Update data from API."""
|
self._is_processing = False
|
||||||
|
await self.async_update_ha_state()
|
||||||
|
|
||||||
|
async def async_process_message(self, question: str, **kwargs) -> dict:
|
||||||
|
"""Process message using the AI client."""
|
||||||
try:
|
try:
|
||||||
await self.async_refresh_metrics()
|
if self.is_anthropic:
|
||||||
return {
|
response = await self._process_anthropic_message(question, **kwargs)
|
||||||
"status": self.endpoint_status,
|
|
||||||
"metrics": self._performance_metrics,
|
|
||||||
"last_update": dt_util.utcnow().isoformat(),
|
|
||||||
"last_response": self.last_response,
|
|
||||||
"is_processing": self._is_processing,
|
|
||||||
"is_rate_limited": self._is_rate_limited,
|
|
||||||
"is_maintenance": self._is_maintenance,
|
|
||||||
"instance": self.instance_name,
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
self._last_error = str(e)
|
|
||||||
self._error_count += 1
|
|
||||||
_LOGGER.error("Error updating data: %s", str(e))
|
|
||||||
raise HomeAssistantError(f"Error updating data: {str(e)}")
|
|
||||||
|
|
||||||
async def async_refresh_metrics(self) -> None:
|
|
||||||
"""Refresh performance metrics."""
|
|
||||||
self._performance_metrics.update({
|
|
||||||
"total_requests": self._request_count,
|
|
||||||
"total_errors": self._error_count,
|
|
||||||
"last_request_time": dt_util.utcnow().isoformat(),
|
|
||||||
"instance": self.instance_name,
|
|
||||||
})
|
|
||||||
|
|
||||||
async def async_process_message(self, message: str, **kwargs) -> dict:
|
|
||||||
"""Process message and update last_response."""
|
|
||||||
try:
|
|
||||||
self._is_processing = True
|
|
||||||
self.async_update_listeners()
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
messages = kwargs.pop("messages", [{"role": "user", "content": message}])
|
|
||||||
model = kwargs.pop("model", self.model)
|
|
||||||
temperature = kwargs.pop("temperature", self.temperature)
|
|
||||||
max_tokens = kwargs.pop("max_tokens", self.max_tokens)
|
|
||||||
|
|
||||||
if self._is_anthropic:
|
|
||||||
response = await self.client.messages.create(
|
|
||||||
model=model,
|
|
||||||
max_tokens=max_tokens,
|
|
||||||
temperature=temperature,
|
|
||||||
messages=messages,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
response_text = response.content[0].text
|
|
||||||
else:
|
else:
|
||||||
response = await self.client.chat.completions.create(
|
response = await self._process_openai_message(question, **kwargs)
|
||||||
model=model,
|
|
||||||
temperature=temperature,
|
|
||||||
max_tokens=max_tokens,
|
|
||||||
messages=messages,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
response_text = response.choices[0].message.content
|
|
||||||
|
|
||||||
elapsed_time = time.time() - start_time
|
|
||||||
self._request_count += 1
|
|
||||||
|
|
||||||
if self._performance_metrics["avg_response_time"] == 0:
|
|
||||||
self._performance_metrics["avg_response_time"] = elapsed_time
|
|
||||||
else:
|
|
||||||
self._performance_metrics["avg_response_time"] = (
|
|
||||||
(self._performance_metrics["avg_response_time"] * (self._request_count - 1) + elapsed_time)
|
|
||||||
/ self._request_count
|
|
||||||
)
|
|
||||||
|
|
||||||
self.last_response = {
|
self.last_response = {
|
||||||
"timestamp": dt_util.utcnow().isoformat(),
|
"timestamp": dt_util.utcnow().isoformat(),
|
||||||
"question": message,
|
"question": question,
|
||||||
"response": response_text,
|
"response": response["content"],
|
||||||
"model": model,
|
"model": kwargs.get("model", self.model),
|
||||||
"instance": self.instance_name,
|
"instance": self.instance_name,
|
||||||
"error": None
|
"error": None
|
||||||
}
|
}
|
||||||
|
|
||||||
self._history.append(self.last_response)
|
return response
|
||||||
if len(self._history) > self._max_history_size:
|
|
||||||
self._history.pop(0)
|
|
||||||
|
|
||||||
self.endpoint_status = "ready"
|
|
||||||
self._is_processing = False
|
|
||||||
self.async_update_listeners()
|
|
||||||
|
|
||||||
return self.last_response
|
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
self._error_count += 1
|
self._handle_error(err)
|
||||||
self._last_error = str(err)
|
|
||||||
self._performance_metrics["total_errors"] += 1
|
|
||||||
|
|
||||||
self.last_response = {
|
|
||||||
"timestamp": dt_util.utcnow().isoformat(),
|
|
||||||
"question": message,
|
|
||||||
"response": None,
|
|
||||||
"instance": self.instance_name,
|
|
||||||
"error": str(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.endpoint_status = "error"
|
|
||||||
self._is_processing = False
|
|
||||||
self.async_update_listeners()
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def async_set_system_prompt(self, prompt: str) -> None:
|
async def _process_anthropic_message(self, question: str, **kwargs) -> dict:
|
||||||
"""Set the system prompt."""
|
"""Process message using Anthropic API."""
|
||||||
self._system_prompt = prompt
|
response = await self.client.messages.create(
|
||||||
_LOGGER.debug("[%s] System prompt updated: %s", self.instance_name, prompt)
|
model=kwargs["model"],
|
||||||
|
max_tokens=kwargs["max_tokens"],
|
||||||
async def async_clear_history(self) -> None:
|
messages=kwargs["messages"],
|
||||||
"""Clear conversation history."""
|
temperature=kwargs["temperature"],
|
||||||
self._history.clear()
|
)
|
||||||
_LOGGER.debug("[%s] Conversation history cleared", self.instance_name)
|
return {
|
||||||
self.async_update_listeners()
|
"content": response.content[0].text,
|
||||||
|
"tokens": {
|
||||||
async def async_get_history(
|
"prompt": response.usage.input_tokens,
|
||||||
self,
|
"completion": response.usage.output_tokens,
|
||||||
limit: Optional[int] = None,
|
"total": response.usage.input_tokens + response.usage.output_tokens
|
||||||
filter_model: Optional[str] = None,
|
}
|
||||||
instance: Optional[str] = None
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""Get conversation history with optional filtering."""
|
|
||||||
history = self._history.copy()
|
|
||||||
|
|
||||||
if instance and instance != self.instance_name:
|
|
||||||
return []
|
|
||||||
|
|
||||||
if filter_model:
|
|
||||||
history = [h for h in history if h.get("model") == filter_model]
|
|
||||||
|
|
||||||
if limit and limit > 0:
|
|
||||||
history = history[-limit:]
|
|
||||||
|
|
||||||
return history
|
|
||||||
|
|
||||||
async def async_reset_metrics(self) -> None:
|
|
||||||
"""Reset all metrics."""
|
|
||||||
self._request_count = 0
|
|
||||||
self._error_count = 0
|
|
||||||
self._performance_metrics = {
|
|
||||||
"total_requests": 0,
|
|
||||||
"total_errors": 0,
|
|
||||||
"avg_response_time": 0.0,
|
|
||||||
"last_request_time": None,
|
|
||||||
"instance": self.instance_name,
|
|
||||||
}
|
}
|
||||||
_LOGGER.debug("[%s] Metrics reset", self.instance_name)
|
|
||||||
self.async_update_listeners()
|
async def _process_openai_message(self, question: str, **kwargs) -> dict:
|
||||||
|
"""Process message using OpenAI API."""
|
||||||
|
response = await self.client.chat.completions.create(
|
||||||
|
model=kwargs["model"],
|
||||||
|
messages=kwargs["messages"],
|
||||||
|
temperature=kwargs["temperature"],
|
||||||
|
max_tokens=kwargs["max_tokens"],
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"content": response.choices[0].message.content,
|
||||||
|
"tokens": {
|
||||||
|
"prompt": response.usage.prompt_tokens,
|
||||||
|
"completion": response.usage.completion_tokens,
|
||||||
|
"total": response.usage.total_tokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _update_metrics(self, latency: float, response: dict) -> None:
|
||||||
|
"""Update performance metrics."""
|
||||||
|
metrics = self._performance_metrics
|
||||||
|
tokens = response.get("tokens", {})
|
||||||
|
|
||||||
|
metrics["total_tokens"] += tokens.get("total", 0)
|
||||||
|
metrics["prompt_tokens"] += tokens.get("prompt", 0)
|
||||||
|
metrics["completion_tokens"] += tokens.get("completion", 0)
|
||||||
|
metrics["successful_requests"] += 1
|
||||||
|
|
||||||
|
# Обновляем латентность
|
||||||
|
metrics["average_latency"] = (
|
||||||
|
(metrics["average_latency"] * (metrics["successful_requests"] - 1) + latency)
|
||||||
|
/ metrics["successful_requests"]
|
||||||
|
)
|
||||||
|
metrics["max_latency"] = max(metrics["max_latency"], latency)
|
||||||
|
metrics["min_latency"] = min(metrics["min_latency"], latency)
|
||||||
|
|
||||||
|
def _update_history(self, question: str, response: dict) -> None:
|
||||||
|
"""Update conversation history."""
|
||||||
|
self._conversation_history.append({
|
||||||
|
"timestamp": dt_util.utcnow().isoformat(),
|
||||||
|
"question": question,
|
||||||
|
"response": response["content"]
|
||||||
|
})
|
||||||
|
|
||||||
|
# Ограничиваем размер истории
|
||||||
|
while len(self._conversation_history) > self.max_history_size:
|
||||||
|
self._conversation_history.pop(0)
|
||||||
|
|
||||||
|
def _handle_error(self, error: Exception) -> None:
|
||||||
|
"""Handle error and update metrics."""
|
||||||
|
self._performance_metrics["total_errors"] += 1
|
||||||
|
self._performance_metrics["failed_requests"] += 1
|
||||||
|
|
||||||
|
self.last_response = {
|
||||||
|
"timestamp": dt_util.utcnow().isoformat(),
|
||||||
|
"question": "",
|
||||||
|
"response": "",
|
||||||
|
"model": self.model,
|
||||||
|
"instance": self.instance_name,
|
||||||
|
"error": str(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async def async_update_ha_state(self) -> None:
|
||||||
|
"""Update Home Assistant state."""
|
||||||
|
await self.async_request_refresh()
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the HA Text AI sensor."""
|
"""Set up the HA Text AI sensor."""
|
||||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
instance_name = coordinator.instance_name
|
||||||
|
_LOGGER.info(f"Setting up HA Text AI sensor with instance: {instance_name}")
|
||||||
async_add_entities([HATextAISensor(coordinator, entry)], True)
|
async_add_entities([HATextAISensor(coordinator, entry)], True)
|
||||||
|
|
||||||
class HATextAISensor(CoordinatorEntity, SensorEntity):
|
class HATextAISensor(CoordinatorEntity, SensorEntity):
|
||||||
@@ -86,6 +88,9 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
self._attr_unique_id = f"{config_entry.entry_id}_{slugify(self._attr_name)}"
|
self._attr_unique_id = f"{config_entry.entry_id}_{slugify(self._attr_name)}"
|
||||||
self.entity_id = f"sensor.ha_text_ai_{slugify(self._attr_name)}"
|
self.entity_id = f"sensor.ha_text_ai_{slugify(self._attr_name)}"
|
||||||
|
|
||||||
|
# Сохраняем instance_name из координатора
|
||||||
|
self._instance_name = coordinator.instance_name
|
||||||
|
|
||||||
self._current_state = STATE_INITIALIZING
|
self._current_state = STATE_INITIALIZING
|
||||||
self._error_count = 0
|
self._error_count = 0
|
||||||
self._last_error = None
|
self._last_error = None
|
||||||
@@ -103,13 +108,15 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
sw_version="1.0.0",
|
sw_version="1.0.0",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_LOGGER.info(f"Initialized sensor with instance: {self._instance_name}")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
"""Return the native value of the sensor."""
|
"""Return the native value of the sensor."""
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return STATE_DISCONNECTED
|
return STATE_DISCONNECTED
|
||||||
|
|
||||||
status = self.coordinator.data.get("status", STATE_READY)
|
status = self.coordinator.data.get("state", STATE_READY)
|
||||||
self._current_state = status
|
self._current_state = status
|
||||||
return status
|
return status
|
||||||
|
|
||||||
@@ -131,6 +138,7 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
ATTR_API_STATUS: self._current_state,
|
ATTR_API_STATUS: self._current_state,
|
||||||
ATTR_TOTAL_ERRORS: self._error_count,
|
ATTR_TOTAL_ERRORS: self._error_count,
|
||||||
ATTR_LAST_ERROR: self._last_error,
|
ATTR_LAST_ERROR: self._last_error,
|
||||||
|
"instance_name": self._instance_name, # Добавляем instance_name в атрибуты
|
||||||
}
|
}
|
||||||
|
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
|
|||||||
Reference in New Issue
Block a user