Initial Home Assistant commit
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
"""The AI Automation Suggester integration."""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, ServiceValidationError
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .api import async_register_http_views
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
CONF_PROVIDER,
|
||||
SERVICE_CLEAR_HISTORY,
|
||||
SERVICE_GENERATE_SUGGESTIONS,
|
||||
SERVICE_UPDATE_SUGGESTION,
|
||||
ATTR_PROVIDER_CONFIG,
|
||||
ATTR_CUSTOM_PROMPT,
|
||||
CONFIG_VERSION
|
||||
)
|
||||
from .coordinator import AIAutomationCoordinator
|
||||
from .store import async_get_suggestion_store
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Migrate old config entry if necessary."""
|
||||
_LOGGER.debug(f"async_migrate_entry {config_entry.version}")
|
||||
# Currently, no migration logic beyond ensuring version matches CONFIG_VERSION
|
||||
if config_entry.version < CONFIG_VERSION:
|
||||
_LOGGER.debug(f"Migrating config entry from version {config_entry.version} to {CONFIG_VERSION}")
|
||||
new_data = {**config_entry.data}
|
||||
new_data.pop('scan_frequency', None)
|
||||
new_data.pop('initial_lag_time', None)
|
||||
hass.config_entries.async_update_entry(config_entry, data=new_data, version=CONFIG_VERSION)
|
||||
_LOGGER.debug("Migration successful")
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
def _listish(value):
|
||||
"""Schema helper for service fields that can be a CSV string, list, or object."""
|
||||
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, (str, list, tuple, dict)):
|
||||
return value
|
||||
raise vol.Invalid("expected a list, comma-separated string, or object")
|
||||
|
||||
|
||||
GENERATE_SUGGESTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_PROVIDER_CONFIG): str,
|
||||
vol.Optional(ATTR_CUSTOM_PROMPT): str,
|
||||
vol.Optional("all_entities", default=False): bool,
|
||||
vol.Optional("domains", default=[]): _listish,
|
||||
vol.Optional("exclude_domains", default=[]): _listish,
|
||||
vol.Optional("exclude_entities", default=[]): _listish,
|
||||
vol.Optional("exclude_areas", default=[]): _listish,
|
||||
vol.Optional("entity_limit", default=200): vol.All(vol.Coerce(int), vol.Range(min=1, max=2000)),
|
||||
vol.Optional("automation_read_yaml", default=False): bool,
|
||||
vol.Optional("automation_limit", default=100): vol.All(vol.Coerce(int), vol.Range(min=0, max=1000)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
UPDATE_SUGGESTION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("suggestion_id"): str,
|
||||
vol.Required("status"): vol.In(["accepted", "declined", "dismissed", "new"]),
|
||||
}
|
||||
)
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the AI Automation Suggester component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
async_register_http_views(hass)
|
||||
|
||||
async def handle_generate_suggestions(call: ServiceCall) -> None:
|
||||
"""Handle the generate_suggestions service call."""
|
||||
try:
|
||||
coordinator = None
|
||||
provider_config = call.data.get(ATTR_PROVIDER_CONFIG)
|
||||
if provider_config:
|
||||
coordinator = hass.data[DOMAIN].get(provider_config)
|
||||
else:
|
||||
# Find first available coordinator if none specified
|
||||
for entry_id, coord in hass.data[DOMAIN].items():
|
||||
if isinstance(coord, AIAutomationCoordinator):
|
||||
coordinator = coord
|
||||
break
|
||||
|
||||
if coordinator is None:
|
||||
raise ServiceValidationError("No AI Automation Suggester provider configured")
|
||||
|
||||
await coordinator.async_generate_suggestions(
|
||||
custom_prompt=call.data.get(ATTR_CUSTOM_PROMPT),
|
||||
all_entities=call.data.get("all_entities", False),
|
||||
domains=call.data.get("domains", []),
|
||||
exclude_domains=call.data.get("exclude_domains", []),
|
||||
exclude_entities=call.data.get("exclude_entities", []),
|
||||
exclude_areas=call.data.get("exclude_areas", []),
|
||||
entity_limit=call.data.get("entity_limit", 200),
|
||||
automation_read_yaml=call.data.get("automation_read_yaml", False),
|
||||
automation_limit=call.data.get("automation_limit", 100),
|
||||
)
|
||||
|
||||
except KeyError:
|
||||
raise ServiceValidationError("Provider configuration not found")
|
||||
except Exception as err:
|
||||
raise ServiceValidationError(f"Failed to generate suggestions: {err}")
|
||||
|
||||
async def handle_clear_history(call: ServiceCall) -> None:
|
||||
"""Clear stored suggestion history."""
|
||||
|
||||
await async_get_suggestion_store(hass).async_clear()
|
||||
|
||||
async def handle_update_suggestion(call: ServiceCall) -> None:
|
||||
"""Update a stored suggestion status."""
|
||||
|
||||
suggestion = await async_get_suggestion_store(hass).async_update_status(
|
||||
call.data["suggestion_id"], call.data["status"]
|
||||
)
|
||||
if suggestion is None:
|
||||
raise ServiceValidationError("Suggestion not found")
|
||||
|
||||
# Register the service
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_GENERATE_SUGGESTIONS,
|
||||
handle_generate_suggestions,
|
||||
schema=GENERATE_SUGGESTIONS_SCHEMA,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_HISTORY,
|
||||
handle_clear_history,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_UPDATE_SUGGESTION,
|
||||
handle_update_suggestion,
|
||||
schema=UPDATE_SUGGESTION_SCHEMA,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up AI Automation Suggester from a config entry."""
|
||||
try:
|
||||
if CONF_PROVIDER not in entry.data:
|
||||
raise ConfigEntryNotReady("Provider not specified in config")
|
||||
|
||||
coordinator = AIAutomationCoordinator(hass, entry)
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
# Use the new async_forward_entry_setups method (plural) instead of the deprecated async_forward_entry_setup.
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Setup complete for %s with provider %s",
|
||||
entry.title,
|
||||
entry.data.get(CONF_PROVIDER)
|
||||
)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
|
||||
|
||||
@callback
|
||||
def handle_custom_event(event):
|
||||
_LOGGER.debug("Received custom event '%s', triggering suggestions with all_entities=True", event.event_type)
|
||||
hass.async_create_task(coordinator_request_all_suggestions())
|
||||
|
||||
async def coordinator_request_all_suggestions():
|
||||
await coordinator.async_generate_suggestions(all_entities=True)
|
||||
|
||||
entry.async_on_unload(hass.bus.async_listen("ai_automation_suggester_update", handle_custom_event))
|
||||
|
||||
return True
|
||||
|
||||
except Exception as err:
|
||||
_LOGGER.error("Failed to setup integration: %s", err)
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
try:
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
coordinator = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
await coordinator.async_shutdown()
|
||||
return unload_ok
|
||||
except Exception as err:
|
||||
_LOGGER.error("Error unloading entry: %s", err)
|
||||
return False
|
||||
|
||||
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Reload config entry."""
|
||||
await async_unload_entry(hass, entry)
|
||||
await async_setup_entry(hass, entry)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,53 @@
|
||||
"""HTTP API endpoints for stored automation suggestions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aiohttp import web
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .store import async_get_suggestion_store
|
||||
|
||||
|
||||
class AISuggestionsView(HomeAssistantView):
|
||||
"""Return stored suggestions for Lovelace cards and dashboards."""
|
||||
|
||||
url = "/api/ai_automation_suggester/suggestions"
|
||||
name = "api:ai_automation_suggester:suggestions"
|
||||
requires_auth = True
|
||||
|
||||
async def get(self, request: web.Request) -> web.Response:
|
||||
hass: HomeAssistant = request.app["hass"]
|
||||
store = async_get_suggestion_store(hass)
|
||||
return self.json(await store.async_list())
|
||||
|
||||
|
||||
class AISuggestionActionView(HomeAssistantView):
|
||||
"""Update a suggestion action from the existing custom card."""
|
||||
|
||||
url = "/api/ai_automation_suggester/{action}/{suggestion_id}"
|
||||
name = "api:ai_automation_suggester:suggestion_action"
|
||||
requires_auth = True
|
||||
|
||||
async def post(self, request: web.Request, action: str, suggestion_id: str) -> web.Response:
|
||||
status_map = {
|
||||
"accept": "accepted",
|
||||
"decline": "declined",
|
||||
"dismiss": "dismissed",
|
||||
}
|
||||
if action not in status_map:
|
||||
return self.json({"success": False, "error": "Unsupported action"}, status_code=400)
|
||||
|
||||
hass: HomeAssistant = request.app["hass"]
|
||||
store = async_get_suggestion_store(hass)
|
||||
suggestion = await store.async_update_status(suggestion_id, status_map[action])
|
||||
if suggestion is None:
|
||||
return self.json({"success": False, "error": "Suggestion not found"}, status_code=404)
|
||||
return self.json({"success": True, "suggestion": suggestion})
|
||||
|
||||
|
||||
def async_register_http_views(hass: HomeAssistant) -> None:
|
||||
"""Register HTTP API views."""
|
||||
|
||||
hass.http.register_view(AISuggestionsView())
|
||||
hass.http.register_view(AISuggestionActionView())
|
||||
@@ -0,0 +1,48 @@
|
||||
########################################################################################
|
||||
# AI Suggestions - New Entity Detection
|
||||
########################################################################################
|
||||
# This automation triggers the AI Automation Suggester whenever new entities are added to
|
||||
# your Home Assistant instance.
|
||||
#
|
||||
# CUSTOMIZATION OPTIONS:
|
||||
# 1. Trigger Events:
|
||||
# - Currently monitors both 'create' and 'update' events
|
||||
# - Remove the second trigger if you only want new entity detection
|
||||
#
|
||||
# 2. Throttling:
|
||||
# - Uses a cooldown of 1 hour to prevent excessive API calls
|
||||
# - Adjust the hours value in the condition template if needed
|
||||
########################################################################################
|
||||
|
||||
alias: "AI Suggestions - New Entity Detection"
|
||||
description: "Generates automation suggestions whenever new entities are registered in Home Assistant"
|
||||
|
||||
trigger:
|
||||
# Trigger when a new entity is created
|
||||
- platform: event
|
||||
event_type: entity_registry_updated
|
||||
event_data:
|
||||
action: create
|
||||
|
||||
# Optional: Trigger when an entity is updated
|
||||
- platform: event
|
||||
event_type: entity_registry_updated
|
||||
event_data:
|
||||
action: update
|
||||
|
||||
condition:
|
||||
# Simple throttling based on last trigger time
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{% set automation = states.automation.ai_suggestions_new_entity_detection %}
|
||||
{% if automation and automation.attributes.last_triggered %}
|
||||
{% set hours_since = ((now() - as_datetime(automation.attributes.last_triggered)).total_seconds() / 3600) | float %}
|
||||
{{ hours_since > 1.0 }}
|
||||
{% else %}
|
||||
true
|
||||
{% endif %}
|
||||
|
||||
action:
|
||||
- service: ai_automation_suggester.generate_suggestions
|
||||
target: {}
|
||||
data: {}
|
||||
@@ -0,0 +1,39 @@
|
||||
########################################################################################
|
||||
# AI Suggestions - Weekly System Review
|
||||
########################################################################################
|
||||
# This automation performs a comprehensive weekly review of your Home Assistant setup
|
||||
# to suggest new automation opportunities and improvements.
|
||||
#
|
||||
# CUSTOMIZATION OPTIONS:
|
||||
# 1. Schedule:
|
||||
# - Currently runs at 3 AM on Sundays
|
||||
# - Modify the 'at' field to change the time
|
||||
# - Change the weekday condition to run on different days
|
||||
########################################################################################
|
||||
|
||||
alias: "AI Suggestions - Weekly Review"
|
||||
description: "Performs a weekly scan of all entities to suggest new automation opportunities"
|
||||
|
||||
trigger:
|
||||
# Runs at 3 AM
|
||||
- platform: time
|
||||
at: "03:00:00"
|
||||
|
||||
condition:
|
||||
# Only runs on Sundays
|
||||
- condition: time
|
||||
weekday:
|
||||
- sun
|
||||
|
||||
action:
|
||||
# Generate suggestions using the configured provider(s), scanning all entities
|
||||
- service: ai_automation_suggester.generate_suggestions
|
||||
data:
|
||||
all_entities: true
|
||||
|
||||
# Create a notification
|
||||
- service: persistent_notification.create
|
||||
data:
|
||||
title: "Weekly Automation Review"
|
||||
message: "The AI Automation Suggester has completed its weekly review. Check the suggestions sensor for new automation ideas!"
|
||||
notification_id: "weekly_automation_review"
|
||||
@@ -0,0 +1,690 @@
|
||||
# custom_components/ai_automation_suggester/config_flow.py
|
||||
"""Config flow for AI Automation Suggester."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import TextSelector, TextSelectorConfig
|
||||
|
||||
from .const import *
|
||||
from .endpoint_utils import (
|
||||
bearer_auth_headers,
|
||||
ensure_http_url,
|
||||
ollama_api_candidates,
|
||||
ollama_base_url,
|
||||
openai_model_endpoint_candidates,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Lightweight provider validators (unchanged)
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
class ProviderValidator:
|
||||
"""Ping each provider with a dummy request to validate credentials."""
|
||||
|
||||
def __init__(self, hass, request_timeout: int | None = None):
|
||||
self.session = async_get_clientsession(hass)
|
||||
self.timeout = aiohttp.ClientTimeout(total=max(10, int(request_timeout or DEFAULT_REQUEST_TIMEOUT)))
|
||||
|
||||
async def validate_openai(self, api_key: str) -> Optional[str]:
|
||||
hdr = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
try:
|
||||
resp = await self.session.get("https://api.openai.com/v1/models", headers=hdr, timeout=self.timeout)
|
||||
return None if resp.status == 200 else await resp.text()
|
||||
except Exception as err: # noqa: BLE001
|
||||
return str(err)
|
||||
|
||||
async def validate_anthropic(self, api_key: str, model: str) -> Optional[str]:
|
||||
hdr = {
|
||||
"x-api-key": api_key,
|
||||
"anthropic-version": VERSION_ANTHROPIC,
|
||||
"content-type": "application/json",
|
||||
}
|
||||
try:
|
||||
resp = await self.session.get("https://api.anthropic.com/v1/models", headers=hdr, timeout=self.timeout)
|
||||
return None if resp.status == 200 else await resp.text()
|
||||
except Exception as err:
|
||||
return str(err)
|
||||
|
||||
async def validate_google(self, api_key: str, model: str) -> Optional[str]:
|
||||
try:
|
||||
resp = await self.session.get(
|
||||
f"https://generativelanguage.googleapis.com/v1beta/models/{model}?key={api_key}",
|
||||
timeout=self.timeout,
|
||||
)
|
||||
return None if resp.status == 200 else await resp.text()
|
||||
except Exception as err:
|
||||
return str(err)
|
||||
|
||||
async def validate_groq(self, api_key: str) -> Optional[str]:
|
||||
hdr = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
try:
|
||||
resp = await self.session.get("https://api.groq.com/openai/v1/models", headers=hdr, timeout=self.timeout)
|
||||
return None if resp.status == 200 else await resp.text()
|
||||
except Exception as err:
|
||||
return str(err)
|
||||
|
||||
async def validate_localai(self, ip: str, port: int, https: bool) -> Optional[str]:
|
||||
proto = "https" if https else "http"
|
||||
try:
|
||||
resp = await self.session.get(f"{proto}://{ip}:{port}/v1/models", timeout=self.timeout)
|
||||
return None if resp.status == 200 else await resp.text()
|
||||
except Exception as err:
|
||||
return str(err)
|
||||
|
||||
async def validate_ollama(
|
||||
self,
|
||||
ip: str | None,
|
||||
port: int | None,
|
||||
https: bool,
|
||||
base_url: str | None = None,
|
||||
api_key: str | None = None,
|
||||
) -> Optional[str]:
|
||||
base = ollama_base_url(base_url=base_url, ip_address=ip, port=port, https=https)
|
||||
if not base:
|
||||
return "Ollama host/port or base URL is required"
|
||||
last_error = None
|
||||
headers = bearer_auth_headers(api_key)
|
||||
try:
|
||||
for endpoint in ollama_api_candidates(base, "api/tags"):
|
||||
resp = await self.session.get(endpoint, headers=headers, timeout=self.timeout)
|
||||
if resp.status == 200:
|
||||
return None
|
||||
last_error = f"{endpoint}: {resp.status} {await resp.text()}"
|
||||
return last_error
|
||||
except Exception as err:
|
||||
return str(err)
|
||||
|
||||
async def validate_custom_openai(self, endpoint: str, api_key: str | None) -> Optional[str]:
|
||||
hdr = {"Content-Type": "application/json"}
|
||||
if api_key:
|
||||
hdr["Authorization"] = f"Bearer {api_key}"
|
||||
last_error = None
|
||||
try:
|
||||
for model_endpoint in openai_model_endpoint_candidates(endpoint):
|
||||
resp = await self.session.get(model_endpoint, headers=hdr, timeout=self.timeout)
|
||||
if resp.status == 200:
|
||||
return None
|
||||
last_error = f"{model_endpoint}: {resp.status} {await resp.text()}"
|
||||
return last_error or "No valid model endpoint could be built"
|
||||
except Exception as err:
|
||||
return str(err)
|
||||
|
||||
async def validate_perplexity(self, api_key: str, model: str) -> Optional[str]:
|
||||
hdr = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
# Perplexity 'sonar' models require max_tokens >= 16; a smaller value is
|
||||
# rejected with a 400 during validation (issue #171).
|
||||
payload = {"model": model, "messages": [{"role": "user", "content": "ping"}], "max_tokens": 16}
|
||||
try:
|
||||
resp = await self.session.post(ENDPOINT_PERPLEXITY, headers=hdr, json=payload, timeout=self.timeout)
|
||||
return None if resp.status == 200 else await resp.text()
|
||||
except Exception as err:
|
||||
return str(err)
|
||||
|
||||
async def validate_openrouter(self, api_key: str, model: str) -> Optional[str]:
|
||||
hdr = {"content-type": "application/json"}
|
||||
if api_key:
|
||||
hdr["Authorization"] = f"Bearer {api_key}"
|
||||
try:
|
||||
resp = await self.session.get(
|
||||
"https://openrouter.ai/api/v1/models", headers=hdr, timeout=self.timeout
|
||||
)
|
||||
return None if resp.status == 200 else await resp.text()
|
||||
except Exception as err:
|
||||
return str(err)
|
||||
|
||||
async def validate_generic_openai(self, endpoint: str, api_key: str) -> Optional[str]:
|
||||
hdr = {"Content-Type": "application/json"}
|
||||
if api_key:
|
||||
hdr["Authorization"] = f"Bearer {api_key}"
|
||||
try:
|
||||
resp = await self.session.get(ensure_http_url(endpoint), headers=hdr, timeout=self.timeout)
|
||||
return None if resp.status == 200 else await resp.text()
|
||||
except Exception as err:
|
||||
return str(err)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Config‑flow main class
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
class AIAutomationConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle integration setup via the UI."""
|
||||
|
||||
VERSION = CONFIG_VERSION
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.provider: str | None = None
|
||||
self.data: Dict[str, Any] = {}
|
||||
self.validator: ProviderValidator | None = None
|
||||
|
||||
# ───────── Initial provider choice ─────────
|
||||
async def async_step_user(self, user_input: Dict[str, Any] | None = None):
|
||||
errors: Dict[str, str] = {}
|
||||
if user_input:
|
||||
self.provider = user_input[CONF_PROVIDER]
|
||||
self.data.update(user_input)
|
||||
return await {
|
||||
"OpenAI": self.async_step_openai,
|
||||
"Anthropic": self.async_step_anthropic,
|
||||
"Google": self.async_step_google,
|
||||
"Groq": self.async_step_groq,
|
||||
"LocalAI": self.async_step_localai,
|
||||
"Ollama": self.async_step_ollama,
|
||||
"Custom OpenAI": self.async_step_custom_openai,
|
||||
"Mistral AI": self.async_step_mistral,
|
||||
"Perplexity AI": self.async_step_perplexity,
|
||||
"OpenRouter": self.async_step_openrouter,
|
||||
"OpenAI Azure": self.async_step_openai_azure,
|
||||
"Generic OpenAI": self.async_step_generic_openai,
|
||||
"LiteLLM": self.async_step_litellm,
|
||||
}[self.provider]()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PROVIDER): vol.In(
|
||||
[
|
||||
"Anthropic",
|
||||
"Custom OpenAI",
|
||||
"Generic OpenAI",
|
||||
"Google",
|
||||
"Groq",
|
||||
"LiteLLM",
|
||||
"LocalAI",
|
||||
"Mistral AI",
|
||||
"Ollama",
|
||||
"OpenAI Azure",
|
||||
"OpenAI",
|
||||
"OpenRouter",
|
||||
"Perplexity AI",
|
||||
]
|
||||
)
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
# ───────── helper to reduce repetition ─────────
|
||||
async def _provider_form(
|
||||
self,
|
||||
step_id: str,
|
||||
schema: vol.Schema,
|
||||
validate_fn,
|
||||
title: str,
|
||||
errors: Dict[str, str],
|
||||
placeholders: Dict[str, str],
|
||||
user_input: Dict[str, Any] | None,
|
||||
):
|
||||
if user_input:
|
||||
self.validator = ProviderValidator(self.hass, user_input.get(CONF_REQUEST_TIMEOUT))
|
||||
err = await validate_fn(user_input)
|
||||
if err is None:
|
||||
self.data.update({
|
||||
**user_input,
|
||||
CONF_MAX_INPUT_TOKENS: user_input.get(CONF_MAX_INPUT_TOKENS, DEFAULT_MAX_INPUT_TOKENS),
|
||||
CONF_MAX_OUTPUT_TOKENS: user_input.get(CONF_MAX_OUTPUT_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS),
|
||||
})
|
||||
return self.async_create_entry(title=title, data=self.data)
|
||||
errors["base"] = "api_error"
|
||||
placeholders["error_message"] = err
|
||||
|
||||
return self.async_show_form(step_id=step_id, data_schema=schema, errors=errors, description_placeholders=placeholders)
|
||||
|
||||
# ───────── provider‑specific steps (OpenAI shown; others similar) ─────────
|
||||
def _add_token_fields(self, base: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||
"""Append common tuning fields to the schema."""
|
||||
base[vol.Optional(CONF_MAX_INPUT_TOKENS, default=DEFAULT_MAX_INPUT_TOKENS)] = vol.All(
|
||||
vol.Coerce(int), vol.Range(min=100)
|
||||
)
|
||||
base[vol.Optional(CONF_MAX_OUTPUT_TOKENS, default=DEFAULT_MAX_OUTPUT_TOKENS)] = vol.All(
|
||||
vol.Coerce(int), vol.Range(min=100)
|
||||
)
|
||||
base[vol.Optional(CONF_CUSTOM_SYSTEM_PROMPT, default="")] = str
|
||||
base[vol.Optional(CONF_EXCLUDED_DOMAINS, default="")] = str
|
||||
base[vol.Optional(CONF_EXCLUDED_ENTITIES, default="")] = str
|
||||
base[vol.Optional(CONF_EXCLUDED_AREAS, default="")] = str
|
||||
base[vol.Optional(CONF_HISTORY_RETENTION, default=DEFAULT_HISTORY_RETENTION)] = vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1, max=250)
|
||||
)
|
||||
base[vol.Optional(CONF_REQUEST_TIMEOUT, default=DEFAULT_REQUEST_TIMEOUT)] = vol.All(
|
||||
vol.Coerce(int), vol.Range(min=10, max=1800)
|
||||
)
|
||||
return base
|
||||
|
||||
async def async_step_openai(self, user_input=None):
|
||||
schema = {
|
||||
vol.Required(CONF_OPENAI_API_KEY): TextSelector(TextSelectorConfig(type="password")),
|
||||
vol.Optional(CONF_OPENAI_MODEL, default=DEFAULT_MODELS["OpenAI"]): str,
|
||||
vol.Optional(CONF_OPENAI_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0)),
|
||||
vol.Optional(CONF_OPENAI_REASONING_EFFORT, default=DEFAULT_OPENAI_REASONING_EFFORT): vol.In(["minimal", "low", "medium", "high"]),
|
||||
}
|
||||
self._add_token_fields(schema)
|
||||
return await self._provider_form(
|
||||
"openai",
|
||||
vol.Schema(schema),
|
||||
lambda ui: self.validator.validate_openai(ui[CONF_OPENAI_API_KEY]),
|
||||
"AI Automation Suggester (OpenAI)",
|
||||
{},
|
||||
{},
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_anthropic(self, user_input=None):
|
||||
async def _v(ui):
|
||||
return await self.validator.validate_anthropic(
|
||||
ui[CONF_ANTHROPIC_API_KEY], ui.get(CONF_ANTHROPIC_MODEL, DEFAULT_MODELS["Anthropic"])
|
||||
)
|
||||
|
||||
schema = {
|
||||
vol.Required(CONF_ANTHROPIC_API_KEY): TextSelector(TextSelectorConfig(type="password")),
|
||||
vol.Optional(CONF_ANTHROPIC_MODEL, default=DEFAULT_MODELS["Anthropic"]): str,
|
||||
vol.Optional(CONF_ANTHROPIC_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0)),
|
||||
}
|
||||
self._add_token_fields(schema)
|
||||
return await self._provider_form(
|
||||
"anthropic",
|
||||
vol.Schema(schema),
|
||||
_v,
|
||||
"AI Automation Suggester (Anthropic)",
|
||||
{},
|
||||
{},
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_google(self, user_input=None):
|
||||
async def _v(ui):
|
||||
return await self.validator.validate_google(
|
||||
ui[CONF_GOOGLE_API_KEY], ui.get(CONF_GOOGLE_MODEL, DEFAULT_MODELS["Google"])
|
||||
)
|
||||
|
||||
schema = {
|
||||
vol.Required(CONF_GOOGLE_API_KEY): TextSelector(TextSelectorConfig(type="password")),
|
||||
vol.Optional(CONF_GOOGLE_MODEL, default=DEFAULT_MODELS["Google"]): str,
|
||||
vol.Optional(CONF_GOOGLE_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0)),
|
||||
}
|
||||
self._add_token_fields(schema)
|
||||
return await self._provider_form(
|
||||
"google",
|
||||
vol.Schema(schema),
|
||||
_v,
|
||||
"AI Automation Suggester (Google)",
|
||||
{},
|
||||
{},
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_groq(self, user_input=None):
|
||||
schema = {
|
||||
vol.Required(CONF_GROQ_API_KEY): TextSelector(TextSelectorConfig(type="password")),
|
||||
vol.Optional(CONF_GROQ_MODEL, default=DEFAULT_MODELS["Groq"]): str,
|
||||
vol.Optional(CONF_GROQ_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=0.0, max=2.0)
|
||||
),
|
||||
}
|
||||
self._add_token_fields(schema)
|
||||
return await self._provider_form(
|
||||
"groq",
|
||||
vol.Schema(schema),
|
||||
lambda ui: self.validator.validate_groq(ui[CONF_GROQ_API_KEY]),
|
||||
"AI Automation Suggester (Groq)",
|
||||
{},
|
||||
{},
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_localai(self, user_input=None):
|
||||
async def _v(ui):
|
||||
return await self.validator.validate_localai(ui[CONF_LOCALAI_IP_ADDRESS], ui[CONF_LOCALAI_PORT], ui[CONF_LOCALAI_HTTPS])
|
||||
|
||||
schema = {
|
||||
vol.Required(CONF_LOCALAI_IP_ADDRESS): str,
|
||||
vol.Required(CONF_LOCALAI_PORT, default=8080): int,
|
||||
vol.Required(CONF_LOCALAI_HTTPS, default=False): bool,
|
||||
vol.Optional(CONF_LOCALAI_MODEL, default=DEFAULT_MODELS["LocalAI"]): str,
|
||||
vol.Optional(CONF_LOCALAI_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=0.0, max=2.0)
|
||||
),
|
||||
}
|
||||
self._add_token_fields(schema)
|
||||
return await self._provider_form(
|
||||
"localai",
|
||||
vol.Schema(schema),
|
||||
_v,
|
||||
"AI Automation Suggester (LocalAI)",
|
||||
{},
|
||||
{},
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_ollama(self, user_input=None):
|
||||
async def _v(ui):
|
||||
return await self.validator.validate_ollama(
|
||||
ui.get(CONF_OLLAMA_IP_ADDRESS),
|
||||
ui.get(CONF_OLLAMA_PORT),
|
||||
ui.get(CONF_OLLAMA_HTTPS, False),
|
||||
ui.get(CONF_OLLAMA_BASE_URL),
|
||||
ui.get(CONF_OLLAMA_API_KEY),
|
||||
)
|
||||
|
||||
schema = {
|
||||
vol.Optional(CONF_OLLAMA_BASE_URL, default=""): str,
|
||||
vol.Optional(CONF_OLLAMA_API_KEY, default=""): TextSelector(TextSelectorConfig(type="password")),
|
||||
vol.Optional(CONF_OLLAMA_IP_ADDRESS, default="localhost"): str,
|
||||
vol.Optional(CONF_OLLAMA_PORT, default=11434): int,
|
||||
vol.Optional(CONF_OLLAMA_HTTPS, default=False): bool,
|
||||
vol.Optional(CONF_OLLAMA_MODEL, default=DEFAULT_MODELS["Ollama"]): str,
|
||||
vol.Optional(CONF_OLLAMA_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=0.0, max=2.0)
|
||||
),
|
||||
vol.Optional(CONF_OLLAMA_DISABLE_THINK, default=False): bool,
|
||||
|
||||
}
|
||||
self._add_token_fields(schema)
|
||||
return await self._provider_form(
|
||||
"ollama",
|
||||
vol.Schema(schema),
|
||||
_v,
|
||||
"AI Automation Suggester (Ollama)",
|
||||
{},
|
||||
{},
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_custom_openai(self, user_input=None):
|
||||
async def _v(ui):
|
||||
return await self.validator.validate_custom_openai(ui[CONF_CUSTOM_OPENAI_ENDPOINT], ui.get(CONF_CUSTOM_OPENAI_API_KEY))
|
||||
|
||||
schema = {
|
||||
vol.Required(CONF_CUSTOM_OPENAI_ENDPOINT): str,
|
||||
vol.Optional(CONF_CUSTOM_OPENAI_API_KEY): TextSelector(TextSelectorConfig(type="password")),
|
||||
vol.Optional(CONF_CUSTOM_OPENAI_MODEL, default=DEFAULT_MODELS["Custom OpenAI"]): str,
|
||||
vol.Optional(CONF_CUSTOM_OPENAI_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0)),
|
||||
}
|
||||
self._add_token_fields(schema)
|
||||
return await self._provider_form(
|
||||
"custom_openai",
|
||||
vol.Schema(schema),
|
||||
_v,
|
||||
"AI Automation Suggester (Custom OpenAI)",
|
||||
{},
|
||||
{},
|
||||
user_input,
|
||||
)
|
||||
|
||||
# Mistral: no live validation needed
|
||||
async def async_step_mistral(self, user_input=None):
|
||||
if user_input:
|
||||
self.data.update(user_input)
|
||||
return self.async_create_entry(title="AI Automation Suggester (Mistral AI)", data=self.data)
|
||||
|
||||
schema = {
|
||||
vol.Required(CONF_MISTRAL_API_KEY): TextSelector(TextSelectorConfig(type="password")),
|
||||
vol.Optional(CONF_MISTRAL_MODEL, default=DEFAULT_MODELS["Mistral AI"]): str,
|
||||
vol.Optional(CONF_MISTRAL_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=0.0, max=2.0)
|
||||
),
|
||||
}
|
||||
self._add_token_fields(schema)
|
||||
return self.async_show_form(step_id="mistral", data_schema=vol.Schema(schema))
|
||||
|
||||
async def async_step_perplexity(self, user_input=None):
|
||||
async def _v(ui):
|
||||
return await self.validator.validate_perplexity(
|
||||
ui[CONF_PERPLEXITY_API_KEY], ui.get(CONF_PERPLEXITY_MODEL, DEFAULT_MODELS["Perplexity AI"])
|
||||
)
|
||||
|
||||
schema = {
|
||||
vol.Required(CONF_PERPLEXITY_API_KEY): TextSelector(TextSelectorConfig(type="password")),
|
||||
vol.Optional(CONF_PERPLEXITY_MODEL, default=DEFAULT_MODELS["Perplexity AI"]): str,
|
||||
vol.Optional(CONF_PERPLEXITY_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=0.0, max=2.0)
|
||||
),
|
||||
}
|
||||
self._add_token_fields(schema)
|
||||
return await self._provider_form(
|
||||
"perplexity",
|
||||
vol.Schema(schema),
|
||||
_v,
|
||||
"AI Automation Suggester (Perplexity)",
|
||||
{},
|
||||
{},
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_openrouter(self, user_input=None):
|
||||
async def _v(ui):
|
||||
return await self.validator.validate_openrouter(
|
||||
ui[CONF_OPENROUTER_API_KEY],
|
||||
ui.get(CONF_OPENROUTER_MODEL, DEFAULT_MODELS["OpenRouter"]),
|
||||
)
|
||||
|
||||
schema = {
|
||||
vol.Required(CONF_OPENROUTER_API_KEY): TextSelector(TextSelectorConfig(type="password")),
|
||||
vol.Optional(
|
||||
CONF_OPENROUTER_MODEL, default=DEFAULT_MODELS["OpenRouter"]
|
||||
): str,
|
||||
vol.Optional(CONF_OPENROUTER_REASONING_MAX_TOKENS, default=0): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_OPENROUTER_TEMPERATURE, default=DEFAULT_TEMPERATURE
|
||||
): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0)),
|
||||
}
|
||||
self._add_token_fields(schema)
|
||||
return await self._provider_form(
|
||||
"openrouter",
|
||||
vol.Schema(schema),
|
||||
_v,
|
||||
"AI Automation Suggester (OpenRouter)",
|
||||
{},
|
||||
{},
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_openai_azure(self, user_input=None):
|
||||
async def _v(ui):
|
||||
if not ui.get(CONF_OPENAI_AZURE_API_KEY) or not ui.get(CONF_OPENAI_AZURE_DEPLOYMENT_ID) or not ui.get(CONF_OPENAI_AZURE_API_VERSION):
|
||||
return "All fields are required"
|
||||
return None
|
||||
|
||||
schema = {
|
||||
vol.Required(CONF_OPENAI_AZURE_API_KEY): TextSelector(TextSelectorConfig(type="password")),
|
||||
vol.Required(CONF_OPENAI_AZURE_DEPLOYMENT_ID, default=DEFAULT_MODELS["OpenAI Azure"]): str,
|
||||
vol.Required(CONF_OPENAI_AZURE_ENDPOINT, default="{your-resource-name}.openai.azure.com"): str,
|
||||
vol.Required(CONF_OPENAI_AZURE_API_VERSION, default="2025-01-01-preview"): str,
|
||||
vol.Optional(CONF_OPENAI_AZURE_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0)),
|
||||
}
|
||||
self._add_token_fields(schema)
|
||||
return await self._provider_form(
|
||||
"openai_azure",
|
||||
vol.Schema(schema),
|
||||
_v,
|
||||
"AI Automation Suggester (OpenAI Azure)",
|
||||
{},
|
||||
{},
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_generic_openai(self, user_input=None):
|
||||
"""Handle the Generic OpenAI API configuration."""
|
||||
async def _v(ui):
|
||||
if not ui.get(CONF_GENERIC_OPENAI_ENDPOINT):
|
||||
return "API URL is required"
|
||||
if ui.get(CONF_GENERIC_OPENAI_ENABLE_VALIDATION, False):
|
||||
if not ui.get(CONF_GENERIC_OPENAI_VALIDATION_ENDPOINT):
|
||||
return "Validation endpoint is required when validation is enabled"
|
||||
return await self.validator.validate_generic_openai(ui[CONF_GENERIC_OPENAI_VALIDATION_ENDPOINT], ui.get(CONF_GENERIC_OPENAI_API_KEY))
|
||||
|
||||
schema = {
|
||||
vol.Required(CONF_GENERIC_OPENAI_ENDPOINT): str,
|
||||
vol.Required(CONF_GENERIC_OPENAI_API_KEY): TextSelector(TextSelectorConfig(type="password")),
|
||||
vol.Required(CONF_GENERIC_OPENAI_MODEL, default=DEFAULT_MODELS["Generic OpenAI"]): str,
|
||||
vol.Optional(CONF_GENERIC_OPENAI_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0)),
|
||||
vol.Optional(CONF_GENERIC_OPENAI_VALIDATION_ENDPOINT, default=""): str,
|
||||
vol.Optional(CONF_GENERIC_OPENAI_ENABLE_VALIDATION, default=False): bool,
|
||||
}
|
||||
self._add_token_fields(schema)
|
||||
return await self._provider_form(
|
||||
"generic_openai",
|
||||
vol.Schema(schema),
|
||||
_v,
|
||||
"AI Automation Suggester (Generic OpenAI)",
|
||||
{},
|
||||
{},
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_litellm(self, user_input=None):
|
||||
"""Handle the LiteLLM configuration."""
|
||||
async def _v(ui):
|
||||
if not ui.get(CONF_LITELLM_MODEL):
|
||||
return "Model is required (e.g. openai/gpt-4o, anthropic/claude-sonnet-4-6)"
|
||||
|
||||
schema = {
|
||||
vol.Required(CONF_LITELLM_MODEL, default=DEFAULT_MODELS["LiteLLM"]): str,
|
||||
vol.Optional(CONF_LITELLM_API_KEY, default=""): TextSelector(TextSelectorConfig(type="password")),
|
||||
vol.Optional(CONF_LITELLM_API_BASE, default=""): str,
|
||||
vol.Optional(CONF_LITELLM_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0)),
|
||||
}
|
||||
self._add_token_fields(schema)
|
||||
return await self._provider_form(
|
||||
"litellm",
|
||||
vol.Schema(schema),
|
||||
_v,
|
||||
"AI Automation Suggester (LiteLLM)",
|
||||
{},
|
||||
{},
|
||||
user_input,
|
||||
)
|
||||
|
||||
# ───────── Options flow (edit after setup) ─────────
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
return AIAutomationOptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class AIAutomationOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Allow post‑setup tweaking of models, keys, token budgets."""
|
||||
|
||||
def __init__(self, config_entry):
|
||||
super().__init__()
|
||||
self._config_entry = config_entry
|
||||
|
||||
def _get_option(self, key, default=None):
|
||||
"""Get value from options, then data, then default."""
|
||||
if key in self._config_entry.options:
|
||||
return self._config_entry.options.get(key)
|
||||
if key in self._config_entry.data:
|
||||
return self._config_entry.data.get(key)
|
||||
return default
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
if user_input:
|
||||
new_data = {
|
||||
**self._config_entry.options,
|
||||
**user_input,
|
||||
CONF_MAX_INPUT_TOKENS: user_input.get(
|
||||
CONF_MAX_INPUT_TOKENS,
|
||||
self._get_option(CONF_MAX_INPUT_TOKENS, DEFAULT_MAX_INPUT_TOKENS)
|
||||
),
|
||||
CONF_MAX_OUTPUT_TOKENS: user_input.get(
|
||||
CONF_MAX_OUTPUT_TOKENS,
|
||||
self._get_option(CONF_MAX_OUTPUT_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS)
|
||||
),
|
||||
}
|
||||
return self.async_create_entry(title="", data=new_data)
|
||||
|
||||
provider = self._config_entry.data.get(CONF_PROVIDER)
|
||||
schema: Dict[Any, Any] = {
|
||||
vol.Optional(CONF_MAX_INPUT_TOKENS, default=self._get_option(CONF_MAX_INPUT_TOKENS, DEFAULT_MAX_INPUT_TOKENS)
|
||||
): vol.All(vol.Coerce(int), vol.Range(min=100)),
|
||||
vol.Optional(CONF_MAX_OUTPUT_TOKENS, default=self._get_option(CONF_MAX_OUTPUT_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS)
|
||||
): vol.All(vol.Coerce(int), vol.Range(min=100)),
|
||||
vol.Optional(CONF_CUSTOM_SYSTEM_PROMPT, default=self._get_option(CONF_CUSTOM_SYSTEM_PROMPT, "")): str,
|
||||
vol.Optional(CONF_EXCLUDED_DOMAINS, default=self._get_option(CONF_EXCLUDED_DOMAINS, "")): str,
|
||||
vol.Optional(CONF_EXCLUDED_ENTITIES, default=self._get_option(CONF_EXCLUDED_ENTITIES, "")): str,
|
||||
vol.Optional(CONF_EXCLUDED_AREAS, default=self._get_option(CONF_EXCLUDED_AREAS, "")): str,
|
||||
vol.Optional(CONF_HISTORY_RETENTION, default=self._get_option(CONF_HISTORY_RETENTION, DEFAULT_HISTORY_RETENTION)): vol.All(vol.Coerce(int), vol.Range(min=1, max=250)),
|
||||
vol.Optional(CONF_REQUEST_TIMEOUT, default=self._get_option(CONF_REQUEST_TIMEOUT, DEFAULT_REQUEST_TIMEOUT)): vol.All(vol.Coerce(int), vol.Range(min=10, max=1800)),
|
||||
}
|
||||
|
||||
# provider‑specific editable fields
|
||||
if provider == "OpenAI":
|
||||
schema[vol.Optional(CONF_OPENAI_API_KEY, default=self._get_option(CONF_OPENAI_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
|
||||
schema[vol.Optional(CONF_OPENAI_MODEL, default=self._get_option(CONF_OPENAI_MODEL, DEFAULT_MODELS["OpenAI"]))] = str
|
||||
schema[vol.Optional(CONF_OPENAI_TEMPERATURE, default=self._get_option(CONF_OPENAI_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
|
||||
schema[vol.Optional(CONF_OPENAI_REASONING_EFFORT, default=self._get_option(CONF_OPENAI_REASONING_EFFORT, DEFAULT_OPENAI_REASONING_EFFORT))] = vol.In(["minimal", "low", "medium", "high"])
|
||||
elif provider == "Anthropic":
|
||||
schema[vol.Optional(CONF_ANTHROPIC_API_KEY, default=self._get_option(CONF_ANTHROPIC_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
|
||||
schema[vol.Optional(CONF_ANTHROPIC_MODEL, default=self._get_option(CONF_ANTHROPIC_MODEL, DEFAULT_MODELS["Anthropic"]))] = str
|
||||
schema[vol.Optional(CONF_ANTHROPIC_TEMPERATURE, default=self._get_option(CONF_ANTHROPIC_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
|
||||
elif provider == "Google":
|
||||
schema[vol.Optional(CONF_GOOGLE_API_KEY, default=self._get_option(CONF_GOOGLE_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
|
||||
schema[vol.Optional(CONF_GOOGLE_MODEL, default=self._get_option(CONF_GOOGLE_MODEL, DEFAULT_MODELS["Google"]))] = str
|
||||
schema[vol.Optional(CONF_GOOGLE_TEMPERATURE, default=self._get_option(CONF_GOOGLE_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
|
||||
elif provider == "Groq":
|
||||
schema[vol.Optional(CONF_GROQ_API_KEY, default=self._get_option(CONF_GROQ_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
|
||||
schema[vol.Optional(CONF_GROQ_MODEL, default=self._get_option(CONF_GROQ_MODEL, DEFAULT_MODELS["Groq"]))] = str
|
||||
schema[vol.Optional(CONF_GROQ_TEMPERATURE, default=self._get_option(CONF_GROQ_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
|
||||
elif provider == "LocalAI":
|
||||
schema[vol.Optional(CONF_LOCALAI_HTTPS, default=self._get_option(CONF_LOCALAI_HTTPS, False))] = bool
|
||||
schema[vol.Optional(CONF_LOCALAI_MODEL, default=self._get_option(CONF_LOCALAI_MODEL, DEFAULT_MODELS["LocalAI"]))] = str
|
||||
schema[vol.Optional(CONF_LOCALAI_TEMPERATURE, default=self._get_option(CONF_LOCALAI_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
|
||||
schema[vol.Optional(CONF_LOCALAI_IP_ADDRESS, default=self._get_option(CONF_LOCALAI_IP_ADDRESS, "localhost"))] = str
|
||||
schema[vol.Optional(CONF_LOCALAI_PORT, default=self._get_option(CONF_LOCALAI_PORT, 8080))] = int
|
||||
elif provider == "Ollama":
|
||||
schema[vol.Optional(CONF_OLLAMA_BASE_URL, default=self._get_option(CONF_OLLAMA_BASE_URL, ""))] = str
|
||||
schema[vol.Optional(CONF_OLLAMA_API_KEY, default=self._get_option(CONF_OLLAMA_API_KEY, ""))] = TextSelector(TextSelectorConfig(type="password"))
|
||||
schema[vol.Optional(CONF_OLLAMA_IP_ADDRESS, default=self._get_option(CONF_OLLAMA_IP_ADDRESS, "localhost"))] = str
|
||||
schema[vol.Optional(CONF_OLLAMA_PORT, default=self._get_option(CONF_OLLAMA_PORT, 11434))] = int
|
||||
schema[vol.Optional(CONF_OLLAMA_HTTPS, default=self._get_option(CONF_OLLAMA_HTTPS, False))] = bool
|
||||
schema[vol.Optional(CONF_OLLAMA_MODEL, default=self._get_option(CONF_OLLAMA_MODEL, DEFAULT_MODELS["Ollama"]))] = str
|
||||
schema[vol.Optional(CONF_OLLAMA_TEMPERATURE, default=self._get_option(CONF_OLLAMA_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
|
||||
schema[vol.Optional(CONF_OLLAMA_DISABLE_THINK, default=self._get_option(CONF_OLLAMA_DISABLE_THINK, False))] = bool
|
||||
elif provider == "Custom OpenAI":
|
||||
schema[vol.Optional(CONF_CUSTOM_OPENAI_ENDPOINT, default=self._get_option(CONF_CUSTOM_OPENAI_ENDPOINT))] = str
|
||||
schema[vol.Optional(CONF_CUSTOM_OPENAI_API_KEY, default=self._get_option(CONF_CUSTOM_OPENAI_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
|
||||
schema[vol.Optional(CONF_CUSTOM_OPENAI_MODEL, default=self._get_option(CONF_CUSTOM_OPENAI_MODEL, DEFAULT_MODELS["Custom OpenAI"]))] = str
|
||||
schema[vol.Optional(CONF_CUSTOM_OPENAI_TEMPERATURE, default=self._get_option(CONF_CUSTOM_OPENAI_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
|
||||
elif provider == "Mistral AI":
|
||||
schema[vol.Optional(CONF_MISTRAL_API_KEY, default=self._get_option(CONF_MISTRAL_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
|
||||
schema[vol.Optional(CONF_MISTRAL_MODEL, default=self._get_option(CONF_MISTRAL_MODEL, DEFAULT_MODELS["Mistral AI"]))] = str
|
||||
schema[vol.Optional(CONF_MISTRAL_TEMPERATURE, default=self._get_option(CONF_MISTRAL_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
|
||||
elif provider == "Perplexity AI":
|
||||
schema[vol.Optional(CONF_PERPLEXITY_API_KEY, default=self._get_option(CONF_PERPLEXITY_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
|
||||
schema[vol.Optional(CONF_PERPLEXITY_MODEL, default=self._get_option(CONF_PERPLEXITY_MODEL, DEFAULT_MODELS["Perplexity AI"]))] = str
|
||||
schema[vol.Optional(CONF_PERPLEXITY_TEMPERATURE, default=self._get_option(CONF_PERPLEXITY_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
|
||||
elif provider == "OpenRouter":
|
||||
schema[vol.Optional(CONF_OPENROUTER_API_KEY, default=self._get_option(CONF_OPENROUTER_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
|
||||
schema[vol.Optional(CONF_OPENROUTER_MODEL, default=self._get_option(CONF_OPENROUTER_MODEL, DEFAULT_MODELS["OpenRouter"]))] = str
|
||||
schema[vol.Optional(CONF_OPENROUTER_REASONING_MAX_TOKENS, default=self._get_option(CONF_OPENROUTER_REASONING_MAX_TOKENS, 0))] = vol.All(vol.Coerce(int), vol.Range(min=0))
|
||||
schema[vol.Optional(CONF_OPENROUTER_TEMPERATURE, default=self._get_option(CONF_OPENROUTER_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
|
||||
elif provider == "OpenAI Azure":
|
||||
schema[vol.Optional(CONF_OPENAI_AZURE_API_KEY, default=self._get_option(CONF_OPENAI_AZURE_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
|
||||
schema[vol.Optional(CONF_OPENAI_AZURE_ENDPOINT, default=self._get_option(CONF_OPENAI_AZURE_ENDPOINT))] = str
|
||||
schema[vol.Optional(CONF_OPENAI_AZURE_DEPLOYMENT_ID, default=self._get_option(CONF_OPENAI_AZURE_DEPLOYMENT_ID, DEFAULT_MODELS["OpenAI Azure"]))] = str
|
||||
schema[vol.Optional(CONF_OPENAI_AZURE_API_VERSION, default=self._get_option(CONF_OPENAI_AZURE_API_VERSION, "2025-01-01-preview"))] = str
|
||||
schema[vol.Optional(CONF_OPENAI_AZURE_TEMPERATURE, default=self._get_option(CONF_OPENAI_AZURE_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
|
||||
elif provider == "Generic OpenAI":
|
||||
schema[vol.Optional(CONF_GENERIC_OPENAI_API_KEY, default=self._get_option(CONF_GENERIC_OPENAI_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
|
||||
schema[vol.Optional(CONF_GENERIC_OPENAI_ENDPOINT, default=self._get_option(CONF_GENERIC_OPENAI_ENDPOINT))] = str
|
||||
schema[vol.Optional(CONF_GENERIC_OPENAI_MODEL, default=self._get_option(CONF_GENERIC_OPENAI_MODEL, DEFAULT_MODELS["Generic OpenAI"]))] = str
|
||||
schema[vol.Optional(CONF_GENERIC_OPENAI_TEMPERATURE, default=self._get_option(CONF_GENERIC_OPENAI_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
|
||||
schema[vol.Optional(CONF_GENERIC_OPENAI_VALIDATION_ENDPOINT, default=self._get_option(CONF_GENERIC_OPENAI_VALIDATION_ENDPOINT, ""))] = str
|
||||
schema[vol.Optional(CONF_GENERIC_OPENAI_ENABLE_VALIDATION, default=self._get_option(CONF_GENERIC_OPENAI_ENABLE_VALIDATION, False))] = bool
|
||||
elif provider == "LiteLLM":
|
||||
schema[vol.Optional(CONF_LITELLM_API_KEY, default=self._get_option(CONF_LITELLM_API_KEY, ""))] = TextSelector(TextSelectorConfig(type="password"))
|
||||
schema[vol.Optional(CONF_LITELLM_MODEL, default=self._get_option(CONF_LITELLM_MODEL, DEFAULT_MODELS["LiteLLM"]))] = str
|
||||
schema[vol.Optional(CONF_LITELLM_API_BASE, default=self._get_option(CONF_LITELLM_API_BASE, ""))] = str
|
||||
schema[vol.Optional(CONF_LITELLM_TEMPERATURE, default=self._get_option(CONF_LITELLM_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
|
||||
|
||||
return self.async_show_form(step_id="init", data_schema=vol.Schema(schema))
|
||||
@@ -0,0 +1,193 @@
|
||||
"""Constants for the AI Automation Suggester integration."""
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Core
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
DOMAIN = "ai_automation_suggester"
|
||||
PLATFORMS = ["sensor"]
|
||||
CONFIG_VERSION = 3 # config-entry version (used by async_migrate_entry)
|
||||
INTEGRATION_NAME = "AI Automation Suggester"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Token budgeting
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Single legacy knob (kept for backward compatibility)
|
||||
CONF_MAX_TOKENS = "max_tokens"
|
||||
DEFAULT_MAX_TOKENS = 500 # legacy default – used for both budgets if new keys absent
|
||||
|
||||
# New, separate knobs (Issue #91)
|
||||
CONF_MAX_INPUT_TOKENS = "max_input_tokens" # how much of the prompt we keep
|
||||
CONF_MAX_OUTPUT_TOKENS = "max_output_tokens" # how long the AI response may be
|
||||
|
||||
DEFAULT_MAX_INPUT_TOKENS = DEFAULT_MAX_TOKENS
|
||||
DEFAULT_MAX_OUTPUT_TOKENS = DEFAULT_MAX_TOKENS
|
||||
|
||||
DEFAULT_TEMPERATURE = 0.7
|
||||
DEFAULT_REQUEST_TIMEOUT = 900
|
||||
DEFAULT_HISTORY_RETENTION = 25
|
||||
DEFAULT_OPENAI_REASONING_EFFORT = "low"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Provider‑selection key
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
CONF_PROVIDER = "provider"
|
||||
CONF_CUSTOM_SYSTEM_PROMPT = "custom_system_prompt"
|
||||
CONF_EXCLUDED_DOMAINS = "excluded_domains"
|
||||
CONF_EXCLUDED_ENTITIES = "excluded_entities"
|
||||
CONF_EXCLUDED_AREAS = "excluded_areas"
|
||||
CONF_HISTORY_RETENTION = "history_retention"
|
||||
CONF_REQUEST_TIMEOUT = "request_timeout"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Provider‑specific keys
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# OpenAI
|
||||
CONF_OPENAI_API_KEY = "openai_api_key"
|
||||
CONF_OPENAI_MODEL = "openai_model"
|
||||
CONF_OPENAI_TEMPERATURE = "openai_temperature"
|
||||
CONF_OPENAI_REASONING_EFFORT = "openai_reasoning_effort"
|
||||
|
||||
# OpenAI Azure
|
||||
CONF_OPENAI_AZURE_API_KEY = "openai_azure_api_key"
|
||||
CONF_OPENAI_AZURE_DEPLOYMENT_ID = "openai_azure_deployment_id"
|
||||
CONF_OPENAI_AZURE_API_VERSION = "openai_azure_api_version"
|
||||
CONF_OPENAI_AZURE_ENDPOINT = "openai_azure_endpoint"
|
||||
CONF_OPENAI_AZURE_TEMPERATURE = "openai_azure_temperature"
|
||||
|
||||
# Anthropic
|
||||
CONF_ANTHROPIC_API_KEY = "anthropic_api_key"
|
||||
CONF_ANTHROPIC_MODEL = "anthropic_model"
|
||||
CONF_ANTHROPIC_TEMPERATURE = "anthropic_temperature"
|
||||
VERSION_ANTHROPIC = "2023-06-01"
|
||||
|
||||
# Google
|
||||
CONF_GOOGLE_API_KEY = "google_api_key"
|
||||
CONF_GOOGLE_MODEL = "google_model"
|
||||
CONF_GOOGLE_TEMPERATURE = "google_temperature"
|
||||
|
||||
# Groq
|
||||
CONF_GROQ_API_KEY = "groq_api_key"
|
||||
CONF_GROQ_MODEL = "groq_model"
|
||||
CONF_GROQ_TEMPERATURE = "groq_temperature"
|
||||
|
||||
# LocalAI
|
||||
CONF_LOCALAI_IP_ADDRESS = "localai_ip"
|
||||
CONF_LOCALAI_PORT = "localai_port"
|
||||
CONF_LOCALAI_HTTPS = "localai_https"
|
||||
CONF_LOCALAI_MODEL = "localai_model"
|
||||
CONF_LOCALAI_TEMPERATURE = "localai_temperature"
|
||||
|
||||
# Ollama
|
||||
CONF_OLLAMA_IP_ADDRESS = "ollama_ip"
|
||||
CONF_OLLAMA_PORT = "ollama_port"
|
||||
CONF_OLLAMA_HTTPS = "ollama_https"
|
||||
CONF_OLLAMA_BASE_URL = "ollama_base_url"
|
||||
CONF_OLLAMA_API_KEY = "ollama_api_key"
|
||||
CONF_OLLAMA_MODEL = "ollama_model"
|
||||
CONF_OLLAMA_TEMPERATURE = "ollama_temperature"
|
||||
CONF_OLLAMA_DISABLE_THINK = "ollama_disable_think"
|
||||
|
||||
# Custom OpenAI
|
||||
CONF_CUSTOM_OPENAI_ENDPOINT = "custom_openai_endpoint"
|
||||
CONF_CUSTOM_OPENAI_API_KEY = "custom_openai_api_key"
|
||||
CONF_CUSTOM_OPENAI_MODEL = "custom_openai_model"
|
||||
CONF_CUSTOM_OPENAI_TEMPERATURE = "custom_openai_temperature"
|
||||
|
||||
# Mistral AI
|
||||
CONF_MISTRAL_API_KEY = "mistral_api_key"
|
||||
CONF_MISTRAL_MODEL = "mistral_model"
|
||||
CONF_MISTRAL_TEMPERATURE = "mistral_temperature"
|
||||
MISTRAL_MODELS = [
|
||||
"mistral-small-latest",
|
||||
"mistral-medium-latest",
|
||||
"mistral-large-latest",
|
||||
"ministral-8b-latest",
|
||||
"codestral-latest",
|
||||
]
|
||||
|
||||
# Perplexity AI
|
||||
CONF_PERPLEXITY_API_KEY = "perplexity_api_key"
|
||||
CONF_PERPLEXITY_MODEL = "perplexity_model"
|
||||
CONF_PERPLEXITY_TEMPERATURE = "perplexity_temperature"
|
||||
|
||||
# OpenRouter
|
||||
CONF_OPENROUTER_API_KEY = "openrouter_api_key"
|
||||
CONF_OPENROUTER_MODEL = "openrouter_model"
|
||||
CONF_OPENROUTER_REASONING_MAX_TOKENS = "openrouter_reasoning_max_tokens"
|
||||
CONF_OPENROUTER_TEMPERATURE = "openrouter_temperature"
|
||||
|
||||
# Generic OpenAI
|
||||
CONF_GENERIC_OPENAI_ENDPOINT = "generic_openai_api_endpoint"
|
||||
CONF_GENERIC_OPENAI_API_KEY = "generic_openai_api_key"
|
||||
CONF_GENERIC_OPENAI_MODEL = "generic_openai_model"
|
||||
CONF_GENERIC_OPENAI_TEMPERATURE = "generic_openai_temperature"
|
||||
CONF_GENERIC_OPENAI_VALIDATION_ENDPOINT = "generic_openai_validation_endpoint"
|
||||
CONF_GENERIC_OPENAI_ENABLE_VALIDATION = "generic_openai_enable_validation"
|
||||
|
||||
# LiteLLM
|
||||
CONF_LITELLM_API_KEY = "litellm_api_key"
|
||||
CONF_LITELLM_MODEL = "litellm_model"
|
||||
CONF_LITELLM_TEMPERATURE = "litellm_temperature"
|
||||
CONF_LITELLM_API_BASE = "litellm_api_base"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Model defaults per provider
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
DEFAULT_MODELS = {
|
||||
"OpenAI": "gpt-5.4-mini",
|
||||
"OpenAI Azure": "gpt-5.4-mini",
|
||||
"Anthropic": "claude-sonnet-4-6",
|
||||
"Google": "gemini-2.5-flash",
|
||||
"Groq": "llama-3.3-70b-versatile",
|
||||
"LocalAI": "llama3",
|
||||
"Ollama": "llama3.1",
|
||||
"Custom OpenAI": "gpt-4o-mini",
|
||||
"Mistral AI": "mistral-small-latest",
|
||||
"Perplexity AI": "sonar",
|
||||
"OpenRouter": "openai/gpt-5.4-mini",
|
||||
"Generic OpenAI": "gpt-4o-mini",
|
||||
"LiteLLM": "openai/gpt-4o-mini",
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Service & attribute names
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
ATTR_PROVIDER_CONFIG = "provider_config"
|
||||
ATTR_CUSTOM_PROMPT = "custom_prompt"
|
||||
SERVICE_GENERATE_SUGGESTIONS = "generate_suggestions"
|
||||
SERVICE_CLEAR_HISTORY = "clear_history"
|
||||
SERVICE_UPDATE_SUGGESTION = "update_suggestion"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Provider‑status sensor values
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PROVIDER_STATUS_CONNECTED = "connected"
|
||||
PROVIDER_STATUS_DISCONNECTED = "disconnected"
|
||||
PROVIDER_STATUS_ERROR = "error"
|
||||
PROVIDER_STATUS_INITIALIZING = "initializing"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# REST endpoints
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
ENDPOINT_OPENAI = "https://api.openai.com/v1/chat/completions"
|
||||
ENDPOINT_OPENAI_AZURE = "https://{endpoint}/openai/deployments/{deployment-id}/chat/completions?api-version={api_version}"
|
||||
ENDPOINT_ANTHROPIC = "https://api.anthropic.com/v1/messages"
|
||||
ENDPOINT_GOOGLE = "https://generativelanguage.googleapis.com/v1beta2/models/{model}:generateText?key={api_key}"
|
||||
ENDPOINT_GROQ = "https://api.groq.com/openai/v1/chat/completions"
|
||||
ENDPOINT_LOCALAI = "{protocol}://{ip_address}:{port}/v1/chat/completions"
|
||||
ENDPOINT_OLLAMA = "{protocol}://{ip_address}:{port}/api/chat"
|
||||
ENDPOINT_MISTRAL = "https://api.mistral.ai/v1/chat/completions"
|
||||
ENDPOINT_PERPLEXITY = "https://api.perplexity.ai/chat/completions"
|
||||
ENDPOINT_OPENROUTER = "https://openrouter.ai/api/v1/chat/completions"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Sensor Keys
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
SENSOR_KEY_SUGGESTIONS = "suggestions"
|
||||
SENSOR_KEY_STATUS = "status"
|
||||
SENSOR_KEY_INPUT_TOKENS = "input_tokens"
|
||||
SENSOR_KEY_OUTPUT_TOKENS = "output_tokens"
|
||||
SENSOR_KEY_MODEL = "model"
|
||||
SENSOR_KEY_LAST_ERROR = "last_error"
|
||||
SENSOR_KEY_HISTORY_COUNT = "history_count"
|
||||
@@ -0,0 +1,920 @@
|
||||
"""Coordinator for AI Automation Suggester."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import random
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import anyio
|
||||
import yaml
|
||||
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import *
|
||||
from .endpoint_utils import bearer_auth_headers, ollama_api_candidates, ollama_base_url, openai_chat_endpoint
|
||||
from .language_utils import suggestion_language_instruction
|
||||
from .model_catalog import (
|
||||
chat_token_parameter,
|
||||
compatibility_warnings,
|
||||
google_json_schema_response_format,
|
||||
json_schema_response_format,
|
||||
model_uses_responses_api,
|
||||
should_send_temperature,
|
||||
supports_json_schema,
|
||||
)
|
||||
from .store import async_get_suggestion_store
|
||||
from .suggestions import (
|
||||
STRUCTURED_OUTPUT_INSTRUCTIONS,
|
||||
format_suggestion_notification,
|
||||
parse_suggestion_response,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SYSTEM_PROMPT = """You are an AI assistant that generates Home Assistant automations
|
||||
based on entities, areas and devices, and suggests improvements to existing automations.
|
||||
|
||||
For each entity:
|
||||
1. Understand its function and context.
|
||||
2. Consider its current state and attributes.
|
||||
3. Suggest context-aware automations or tweaks, including real entity_ids.
|
||||
|
||||
If asked to focus on a theme (energy saving, presence lighting, etc.), integrate it.
|
||||
Also review existing automations and propose improvements.
|
||||
If you see a lot of text in a different language, focus on it for a translation for your output.
|
||||
"""
|
||||
|
||||
|
||||
class AIAutomationCoordinator(DataUpdateCoordinator):
|
||||
"""Build prompts, call the configured provider, and publish suggestions."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry) -> None:
|
||||
self.hass = hass
|
||||
self.entry = entry
|
||||
self.previous_entities: dict[str, dict] = {}
|
||||
self.last_update: datetime | None = None
|
||||
self.session = async_get_clientsession(hass)
|
||||
self._generation_lock = asyncio.Lock()
|
||||
self._last_error: str | None = None
|
||||
self._last_response_metadata: dict[str, Any] = {}
|
||||
|
||||
self.SYSTEM_PROMPT = SYSTEM_PROMPT
|
||||
self.scan_all = False
|
||||
self.selected_domains: list[str] = []
|
||||
self.excluded_domains: list[str] = self._opt_list(CONF_EXCLUDED_DOMAINS)
|
||||
self.excluded_entities: list[str] = self._opt_list(CONF_EXCLUDED_ENTITIES)
|
||||
self.excluded_areas: list[str] = self._opt_list(CONF_EXCLUDED_AREAS)
|
||||
self.entity_limit = 200
|
||||
self.automation_read_file = False
|
||||
self.automation_limit = 100
|
||||
|
||||
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=None)
|
||||
|
||||
self.data: dict = {
|
||||
"suggestions": "No suggestions yet",
|
||||
"suggestion": None,
|
||||
"suggestion_history": [],
|
||||
"suggestion_count": 0,
|
||||
"description": None,
|
||||
"yaml_block": None,
|
||||
"last_update": None,
|
||||
"entities_processed": [],
|
||||
"provider": self._opt(CONF_PROVIDER, "unknown"),
|
||||
"model": self._current_model(),
|
||||
"warnings": [],
|
||||
"last_error": None,
|
||||
"response_metadata": {},
|
||||
}
|
||||
|
||||
self.device_registry: dr.DeviceRegistry | None = None
|
||||
self.entity_registry: er.EntityRegistry | None = None
|
||||
self.area_registry: ar.AreaRegistry | None = None
|
||||
|
||||
def _opt(self, key: str, default=None):
|
||||
"""Return entry option, then setup data, then default."""
|
||||
|
||||
return self.entry.options.get(key, self.entry.data.get(key, default))
|
||||
|
||||
def _opt_list(self, key: str, default: list[str] | None = None) -> list[str]:
|
||||
"""Return a normalized list option."""
|
||||
|
||||
value = self._opt(key, default or [])
|
||||
return self._normalize_list(value)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_list(value: Any) -> list[str]:
|
||||
"""Normalize strings, dicts, tuples, and lists into a string list."""
|
||||
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, str):
|
||||
return [item.strip() for item in value.split(",") if item.strip()]
|
||||
if isinstance(value, dict):
|
||||
return [str(item).strip() for item in value if str(item).strip()]
|
||||
if isinstance(value, (list, tuple, set)):
|
||||
return [str(item).strip() for item in value if str(item).strip()]
|
||||
return [str(value).strip()] if str(value).strip() else []
|
||||
|
||||
def _budgets(self) -> tuple[int, int]:
|
||||
"""Return input and output token budgets with legacy fallback."""
|
||||
|
||||
out_budget = self._opt(
|
||||
CONF_MAX_OUTPUT_TOKENS, self._opt(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)
|
||||
)
|
||||
in_budget = self._opt(
|
||||
CONF_MAX_INPUT_TOKENS, self._opt(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)
|
||||
)
|
||||
return int(in_budget), int(out_budget)
|
||||
|
||||
def _timeout(self) -> aiohttp.ClientTimeout:
|
||||
seconds = int(self._opt(CONF_REQUEST_TIMEOUT, DEFAULT_REQUEST_TIMEOUT))
|
||||
return aiohttp.ClientTimeout(total=max(10, seconds))
|
||||
|
||||
def _current_model(self, provider: str | None = None) -> str:
|
||||
provider = provider or self._opt(CONF_PROVIDER, "OpenAI")
|
||||
model_key_map = {
|
||||
"OpenAI": CONF_OPENAI_MODEL,
|
||||
"Anthropic": CONF_ANTHROPIC_MODEL,
|
||||
"Google": CONF_GOOGLE_MODEL,
|
||||
"Groq": CONF_GROQ_MODEL,
|
||||
"LocalAI": CONF_LOCALAI_MODEL,
|
||||
"Ollama": CONF_OLLAMA_MODEL,
|
||||
"Custom OpenAI": CONF_CUSTOM_OPENAI_MODEL,
|
||||
"Mistral AI": CONF_MISTRAL_MODEL,
|
||||
"Perplexity AI": CONF_PERPLEXITY_MODEL,
|
||||
"OpenRouter": CONF_OPENROUTER_MODEL,
|
||||
"OpenAI Azure": CONF_OPENAI_AZURE_DEPLOYMENT_ID,
|
||||
"Generic OpenAI": CONF_GENERIC_OPENAI_MODEL,
|
||||
"LiteLLM": CONF_LITELLM_MODEL,
|
||||
}
|
||||
model_key = model_key_map.get(provider)
|
||||
return self._opt(model_key, DEFAULT_MODELS.get(provider, "unknown")) if model_key else "unknown"
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
self.device_registry = dr.async_get(self.hass)
|
||||
self.entity_registry = er.async_get(self.hass)
|
||||
self.area_registry = ar.async_get(self.hass)
|
||||
|
||||
async def async_shutdown(self):
|
||||
return
|
||||
|
||||
async def async_generate_suggestions(
|
||||
self,
|
||||
*,
|
||||
custom_prompt: str | None = None,
|
||||
all_entities: bool = False,
|
||||
domains: Any = None,
|
||||
exclude_domains: Any = None,
|
||||
exclude_entities: Any = None,
|
||||
exclude_areas: Any = None,
|
||||
entity_limit: int = 200,
|
||||
automation_read_yaml: bool = False,
|
||||
automation_limit: int = 100,
|
||||
) -> None:
|
||||
"""Run one suggestion generation with isolated request settings."""
|
||||
|
||||
async with self._generation_lock:
|
||||
saved = {
|
||||
"SYSTEM_PROMPT": self.SYSTEM_PROMPT,
|
||||
"scan_all": self.scan_all,
|
||||
"selected_domains": self.selected_domains,
|
||||
"excluded_domains": self.excluded_domains,
|
||||
"excluded_entities": self.excluded_entities,
|
||||
"excluded_areas": self.excluded_areas,
|
||||
"entity_limit": self.entity_limit,
|
||||
"automation_read_file": self.automation_read_file,
|
||||
"automation_limit": self.automation_limit,
|
||||
}
|
||||
try:
|
||||
persistent_prompt = str(self._opt(CONF_CUSTOM_SYSTEM_PROMPT, "") or "").strip()
|
||||
prompt_parts = [SYSTEM_PROMPT]
|
||||
if persistent_prompt:
|
||||
prompt_parts.append(f"Persistent user instructions:\n{persistent_prompt}")
|
||||
if custom_prompt:
|
||||
prompt_parts.append(f"Request-specific instructions:\n{custom_prompt}")
|
||||
self.SYSTEM_PROMPT = "\n\n".join(prompt_parts)
|
||||
self.scan_all = all_entities
|
||||
self.selected_domains = self._normalize_list(domains)
|
||||
self.excluded_domains = self._normalize_list(exclude_domains) or self._opt_list(CONF_EXCLUDED_DOMAINS)
|
||||
self.excluded_entities = self._normalize_list(exclude_entities) or self._opt_list(CONF_EXCLUDED_ENTITIES)
|
||||
self.excluded_areas = self._normalize_list(exclude_areas) or self._opt_list(CONF_EXCLUDED_AREAS)
|
||||
self.entity_limit = int(entity_limit)
|
||||
self.automation_read_file = bool(automation_read_yaml)
|
||||
self.automation_limit = int(automation_limit)
|
||||
await self.async_request_refresh()
|
||||
finally:
|
||||
self.SYSTEM_PROMPT = saved["SYSTEM_PROMPT"]
|
||||
self.scan_all = saved["scan_all"]
|
||||
self.selected_domains = saved["selected_domains"]
|
||||
self.excluded_domains = saved["excluded_domains"]
|
||||
self.excluded_entities = saved["excluded_entities"]
|
||||
self.excluded_areas = saved["excluded_areas"]
|
||||
self.entity_limit = saved["entity_limit"]
|
||||
self.automation_read_file = saved["automation_read_file"]
|
||||
self.automation_limit = saved["automation_limit"]
|
||||
|
||||
async def _async_update_data(self) -> dict:
|
||||
try:
|
||||
now = datetime.now()
|
||||
provider = self._opt(CONF_PROVIDER, "OpenAI")
|
||||
model = self._current_model(provider)
|
||||
warnings = compatibility_warnings(provider, model)
|
||||
self.last_update = now
|
||||
self._last_error = None
|
||||
self._last_response_metadata = {}
|
||||
|
||||
current = self._collect_entities()
|
||||
picked = current if self.scan_all else {k: v for k, v in current.items() if k not in self.previous_entities}
|
||||
if not picked:
|
||||
self.previous_entities = current
|
||||
history = await async_get_suggestion_store(self.hass).async_list()
|
||||
self.data.update(
|
||||
{
|
||||
"suggestion_history": history,
|
||||
"suggestion_count": len(history),
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"warnings": warnings,
|
||||
"last_update": now,
|
||||
}
|
||||
)
|
||||
return self.data
|
||||
|
||||
prompt = await self._build_prompt(picked)
|
||||
response = await self._dispatch(prompt)
|
||||
store = async_get_suggestion_store(self.hass)
|
||||
|
||||
if response:
|
||||
parsed = parse_suggestion_response(
|
||||
response,
|
||||
provider=provider,
|
||||
model=model,
|
||||
created_at=now,
|
||||
entities_processed=list(picked.keys()),
|
||||
inherited_warnings=warnings,
|
||||
response_metadata=self._last_response_metadata,
|
||||
)
|
||||
retention = int(self._opt(CONF_HISTORY_RETENTION, DEFAULT_HISTORY_RETENTION))
|
||||
history = await store.async_add_suggestions(parsed, retention=retention)
|
||||
latest = history[0] if history else parsed[0]
|
||||
|
||||
persistent_notification.async_create(
|
||||
self.hass,
|
||||
message=format_suggestion_notification(latest),
|
||||
title=f"AI Automation Suggestions ({provider})",
|
||||
notification_id=f"ai_automation_suggestions_{now.timestamp()}",
|
||||
)
|
||||
|
||||
self.data = {
|
||||
"suggestions": response,
|
||||
"suggestion": latest,
|
||||
"suggestion_history": history,
|
||||
"suggestion_count": len(history),
|
||||
"description": latest.get("description"),
|
||||
"yaml_block": latest.get("yamlCode"),
|
||||
"last_update": now,
|
||||
"entities_processed": list(picked.keys()),
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"warnings": latest.get("warnings", warnings),
|
||||
"last_error": None,
|
||||
"response_metadata": self._last_response_metadata,
|
||||
}
|
||||
else:
|
||||
history = await store.async_list()
|
||||
self.data.update(
|
||||
{
|
||||
"suggestions": "No suggestions available",
|
||||
"suggestion": None,
|
||||
"suggestion_history": history,
|
||||
"suggestion_count": len(history),
|
||||
"description": None,
|
||||
"yaml_block": None,
|
||||
"last_update": now,
|
||||
"entities_processed": [],
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"warnings": warnings,
|
||||
"last_error": self._last_error,
|
||||
"response_metadata": self._last_response_metadata,
|
||||
}
|
||||
)
|
||||
|
||||
self.previous_entities = current
|
||||
return self.data
|
||||
|
||||
except Exception as err: # noqa: BLE001
|
||||
self._last_error = str(err)
|
||||
_LOGGER.exception("Coordinator fatal error")
|
||||
self.data["last_error"] = self._last_error
|
||||
return self.data
|
||||
|
||||
def _collect_entities(self) -> dict[str, dict]:
|
||||
current: dict[str, dict] = {}
|
||||
selected_domains = set(self.selected_domains)
|
||||
for entity_id in self.hass.states.async_entity_ids():
|
||||
domain = entity_id.split(".", 1)[0]
|
||||
if selected_domains and domain not in selected_domains:
|
||||
continue
|
||||
if self._is_entity_excluded(entity_id):
|
||||
continue
|
||||
state = self.hass.states.get(entity_id)
|
||||
if state:
|
||||
current[entity_id] = {
|
||||
"state": state.state,
|
||||
"attributes": state.attributes,
|
||||
"last_changed": state.last_changed,
|
||||
"last_updated": state.last_updated,
|
||||
"friendly_name": state.attributes.get("friendly_name", entity_id),
|
||||
}
|
||||
return current
|
||||
|
||||
def _is_entity_excluded(self, entity_id: str) -> bool:
|
||||
domain = entity_id.split(".", 1)[0]
|
||||
if domain in set(self.excluded_domains):
|
||||
return True
|
||||
if entity_id in set(self.excluded_entities):
|
||||
return True
|
||||
if not self.excluded_areas or not self.entity_registry:
|
||||
return False
|
||||
|
||||
entity_entry = self.entity_registry.async_get(entity_id)
|
||||
device_entry = None
|
||||
if entity_entry and entity_entry.device_id and self.device_registry:
|
||||
device_entry = self.device_registry.async_get(entity_entry.device_id)
|
||||
area_id = entity_entry.area_id if entity_entry and entity_entry.area_id else None
|
||||
if not area_id and device_entry:
|
||||
area_id = device_entry.area_id
|
||||
area_names = {area_id.lower()} if area_id else set()
|
||||
if area_id and self.area_registry:
|
||||
area_entry = self.area_registry.async_get_area(area_id)
|
||||
if area_entry:
|
||||
area_names.add(area_entry.name.lower())
|
||||
excluded = {area.lower() for area in self.excluded_areas}
|
||||
return bool(area_names & excluded)
|
||||
|
||||
async def _build_prompt(self, entities: dict) -> str:
|
||||
max_attr = 500
|
||||
max_autom = self.automation_limit
|
||||
ent_sections: list[str] = []
|
||||
for entity_id, meta in random.sample(list(entities.items()), min(len(entities), self.entity_limit)):
|
||||
domain = entity_id.split(".", 1)[0]
|
||||
attr_str = str(meta["attributes"])
|
||||
if len(attr_str) > max_attr:
|
||||
attr_str = f"{attr_str[:max_attr]}...(truncated)"
|
||||
|
||||
entity_entry = self.entity_registry.async_get(entity_id) if self.entity_registry else None
|
||||
device_entry = (
|
||||
self.device_registry.async_get(entity_entry.device_id)
|
||||
if entity_entry and entity_entry.device_id and self.device_registry
|
||||
else None
|
||||
)
|
||||
area_id = entity_entry.area_id if entity_entry and entity_entry.area_id else None
|
||||
if not area_id and device_entry:
|
||||
area_id = device_entry.area_id
|
||||
area_name = "Unknown Area"
|
||||
if area_id and self.area_registry:
|
||||
area_entry = self.area_registry.async_get_area(area_id)
|
||||
if area_entry:
|
||||
area_name = area_entry.name
|
||||
|
||||
block = (
|
||||
f"Entity: {entity_id}\n"
|
||||
f"Friendly Name: {meta['friendly_name']}\n"
|
||||
f"Domain: {domain}\n"
|
||||
f"State: {meta['state']}\n"
|
||||
f"Attributes: {attr_str}\n"
|
||||
f"Area: {area_name}\n"
|
||||
)
|
||||
if device_entry:
|
||||
block += (
|
||||
"Device Info:\n"
|
||||
f" Manufacturer: {device_entry.manufacturer}\n"
|
||||
f" Model: {device_entry.model}\n"
|
||||
f" Device Name: {device_entry.name_by_user or device_entry.name}\n"
|
||||
f" Device ID: {device_entry.id}\n"
|
||||
)
|
||||
block += f"Last Changed: {meta['last_changed']}\nLast Updated: {meta['last_updated']}\n---\n"
|
||||
ent_sections.append(block)
|
||||
|
||||
autom_sections = self._read_automations_default(max_autom, max_attr)
|
||||
autom_codes: list[str] = []
|
||||
if self.automation_read_file:
|
||||
autom_codes = await self._read_automations_file_method(max_autom)
|
||||
language_instruction = suggestion_language_instruction(getattr(self.hass.config, "language", None))
|
||||
language_block = f"{language_instruction}\n\n" if language_instruction else ""
|
||||
|
||||
return (
|
||||
f"{self.SYSTEM_PROMPT}\n\n"
|
||||
f"{STRUCTURED_OUTPUT_INSTRUCTIONS}\n\n"
|
||||
f"{language_block}"
|
||||
f"Entities in your Home Assistant (sampled):\n{''.join(ent_sections)}\n"
|
||||
"Existing Automations Overview:\n"
|
||||
f"{''.join(autom_sections) if autom_sections else 'None found.'}\n\n"
|
||||
"Automations YAML Code (for analysis and improvement):\n"
|
||||
f"{''.join(autom_codes) if autom_codes else 'No automations YAML code included.'}\n\n"
|
||||
"Analyze the entities and existing automations. Propose useful new automations or improvements "
|
||||
"that reference only the entity_ids shown above."
|
||||
)
|
||||
|
||||
def _read_automations_default(self, max_autom: int, max_attr: int) -> list[str]:
|
||||
autom_sections: list[str] = []
|
||||
for automation_id in self.hass.states.async_entity_ids("automation")[:max_autom]:
|
||||
state = self.hass.states.get(automation_id)
|
||||
if state:
|
||||
attr = str(state.attributes)
|
||||
if len(attr) > max_attr:
|
||||
attr = f"{attr[:max_attr]}...(truncated)"
|
||||
autom_sections.append(
|
||||
f"Entity: {automation_id}\n"
|
||||
f"Friendly Name: {state.attributes.get('friendly_name', automation_id)}\n"
|
||||
f"State: {state.state}\n"
|
||||
f"Attributes: {attr}\n"
|
||||
"---\n"
|
||||
)
|
||||
return autom_sections
|
||||
|
||||
async def _read_automations_file_method(self, max_autom: int) -> list[str]:
|
||||
automations_file = Path(self.hass.config.path()) / "automations.yaml"
|
||||
autom_codes: list[str] = []
|
||||
try:
|
||||
async with await anyio.open_file(automations_file, "r", encoding="utf-8") as file:
|
||||
content = await file.read()
|
||||
automations = yaml.safe_load(content) or []
|
||||
if not isinstance(automations, list):
|
||||
_LOGGER.warning("automations.yaml did not parse as a list")
|
||||
return autom_codes
|
||||
for automation in automations[:max_autom]:
|
||||
if not isinstance(automation, dict):
|
||||
continue
|
||||
autom_codes.append(
|
||||
"Automation YAML:\n```yaml\n"
|
||||
f"{yaml.safe_dump([automation], sort_keys=False)}"
|
||||
"```\n---\n"
|
||||
)
|
||||
except FileNotFoundError:
|
||||
_LOGGER.warning("automations.yaml file was not found")
|
||||
except yaml.YAMLError as err:
|
||||
_LOGGER.warning("Error parsing automations.yaml: %s", err)
|
||||
return autom_codes
|
||||
|
||||
async def _dispatch(self, prompt: str) -> str | None:
|
||||
provider = self._opt(CONF_PROVIDER, "OpenAI")
|
||||
dispatch = {
|
||||
"OpenAI": self._openai,
|
||||
"Anthropic": self._anthropic,
|
||||
"Google": self._google,
|
||||
"Groq": self._groq,
|
||||
"LocalAI": self._localai,
|
||||
"Ollama": self._ollama,
|
||||
"Custom OpenAI": self._custom_openai,
|
||||
"Mistral AI": self._mistral,
|
||||
"Perplexity AI": self._perplexity,
|
||||
"OpenRouter": self._openrouter,
|
||||
"OpenAI Azure": self._openai_azure,
|
||||
"Generic OpenAI": self._generic_openai,
|
||||
"LiteLLM": self._litellm,
|
||||
}
|
||||
handler = dispatch.get(provider)
|
||||
if handler is None:
|
||||
self._last_error = f"Unknown provider '{provider}'"
|
||||
_LOGGER.error(self._last_error)
|
||||
return None
|
||||
try:
|
||||
return await handler(prompt)
|
||||
except Exception as err: # noqa: BLE001
|
||||
self._last_error = str(err)
|
||||
_LOGGER.exception("Dispatch error for %s", provider)
|
||||
return None
|
||||
|
||||
def _trim_prompt(self, prompt: str) -> str:
|
||||
in_budget, _ = self._budgets()
|
||||
if len(prompt) // 4 > in_budget:
|
||||
return prompt[: in_budget * 4]
|
||||
return prompt
|
||||
|
||||
async def _post_json(
|
||||
self,
|
||||
endpoint: str,
|
||||
*,
|
||||
headers: dict[str, str] | None = None,
|
||||
body: dict[str, Any],
|
||||
provider_label: str,
|
||||
) -> dict[str, Any] | None:
|
||||
async with self.session.post(
|
||||
endpoint,
|
||||
headers=headers,
|
||||
json=body,
|
||||
timeout=self._timeout(),
|
||||
) as response:
|
||||
response_text = await response.text()
|
||||
if response.status != 200:
|
||||
self._last_error = f"{provider_label} error {response.status}: {response_text}"
|
||||
_LOGGER.error(self._last_error)
|
||||
return None
|
||||
try:
|
||||
return await response.json(content_type=None)
|
||||
except Exception as err: # noqa: BLE001
|
||||
self._last_error = f"{provider_label} returned non-JSON response: {err}: {response_text[:500]}"
|
||||
_LOGGER.error(self._last_error)
|
||||
return None
|
||||
|
||||
def _openai_compatible_body(
|
||||
self,
|
||||
*,
|
||||
provider: str,
|
||||
model: str,
|
||||
prompt: str,
|
||||
temperature: float,
|
||||
extra: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
_, out_budget = self._budgets()
|
||||
body: dict[str, Any] = {
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
chat_token_parameter(provider, model): out_budget,
|
||||
}
|
||||
if should_send_temperature(provider, model):
|
||||
body["temperature"] = temperature
|
||||
if supports_json_schema(provider, model):
|
||||
body["response_format"] = json_schema_response_format()
|
||||
if extra:
|
||||
body.update(extra)
|
||||
return body
|
||||
|
||||
def _extract_chat_content(self, response: dict[str, Any], provider_label: str) -> str | None:
|
||||
choices = response.get("choices")
|
||||
if not isinstance(choices, list) or not choices:
|
||||
raise ValueError(f"{provider_label} response missing choices array: {response}")
|
||||
choice = choices[0]
|
||||
self._last_response_metadata = {
|
||||
"finish_reason": choice.get("finish_reason"),
|
||||
"native_finish_reason": choice.get("native_finish_reason"),
|
||||
"usage": response.get("usage"),
|
||||
}
|
||||
message = choice.get("message") or {}
|
||||
content = message.get("content")
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
if isinstance(content, list):
|
||||
return "".join(part.get("text", "") for part in content if isinstance(part, dict))
|
||||
raise ValueError(f"{provider_label} message missing content: {message}")
|
||||
|
||||
async def _openai(self, prompt: str) -> str | None:
|
||||
api_key = self._opt(CONF_OPENAI_API_KEY)
|
||||
if not api_key:
|
||||
raise ValueError("OpenAI API key not configured")
|
||||
model = self._current_model("OpenAI")
|
||||
prompt = self._trim_prompt(prompt)
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
if model_uses_responses_api("OpenAI", model):
|
||||
return await self._openai_responses(prompt, model, headers)
|
||||
body = self._openai_compatible_body(
|
||||
provider="OpenAI",
|
||||
model=model,
|
||||
prompt=prompt,
|
||||
temperature=float(self._opt(CONF_OPENAI_TEMPERATURE, DEFAULT_TEMPERATURE)),
|
||||
)
|
||||
response = await self._post_json(ENDPOINT_OPENAI, headers=headers, body=body, provider_label="OpenAI")
|
||||
return self._extract_chat_content(response, "OpenAI") if response else None
|
||||
|
||||
async def _openai_responses(self, prompt: str, model: str, headers: dict[str, str]) -> str | None:
|
||||
_, out_budget = self._budgets()
|
||||
body: dict[str, Any] = {
|
||||
"model": model,
|
||||
"input": [{"role": "user", "content": prompt}],
|
||||
"max_output_tokens": out_budget,
|
||||
"reasoning": {
|
||||
"effort": self._opt(CONF_OPENAI_REASONING_EFFORT, DEFAULT_OPENAI_REASONING_EFFORT)
|
||||
},
|
||||
"text": {"format": {"type": "json_schema", **json_schema_response_format()["json_schema"]}},
|
||||
}
|
||||
response = await self._post_json(
|
||||
"https://api.openai.com/v1/responses",
|
||||
headers=headers,
|
||||
body=body,
|
||||
provider_label="OpenAI Responses",
|
||||
)
|
||||
if not response:
|
||||
return None
|
||||
self._last_response_metadata = {
|
||||
"status": response.get("status"),
|
||||
"incomplete_details": response.get("incomplete_details"),
|
||||
"usage": response.get("usage"),
|
||||
}
|
||||
if isinstance(response.get("output_text"), str):
|
||||
return response["output_text"]
|
||||
output = response.get("output") or []
|
||||
text_parts: list[str] = []
|
||||
for item in output:
|
||||
for content in item.get("content", []) if isinstance(item, dict) else []:
|
||||
if isinstance(content, dict) and content.get("type") in {"output_text", "text"}:
|
||||
text_parts.append(str(content.get("text", "")))
|
||||
return "".join(text_parts) if text_parts else None
|
||||
|
||||
async def _openai_azure(self, prompt: str) -> str | None:
|
||||
endpoint_base = self._opt(CONF_OPENAI_AZURE_ENDPOINT)
|
||||
api_key = self._opt(CONF_OPENAI_AZURE_API_KEY)
|
||||
deployment_id = self._opt(CONF_OPENAI_AZURE_DEPLOYMENT_ID)
|
||||
api_version = self._opt(CONF_OPENAI_AZURE_API_VERSION, "2025-01-01-preview")
|
||||
if not endpoint_base or not deployment_id or not api_key:
|
||||
raise ValueError("Azure OpenAI endpoint, deployment, or API key not configured")
|
||||
endpoint_base = str(endpoint_base).rstrip("/")
|
||||
if not re.match(r"^https?://", endpoint_base):
|
||||
endpoint_base = f"https://{endpoint_base}"
|
||||
endpoint = f"{endpoint_base}/openai/deployments/{deployment_id}/chat/completions?api-version={api_version}"
|
||||
_, out_budget = self._budgets()
|
||||
model = self._current_model("OpenAI Azure")
|
||||
body: dict[str, Any] = {
|
||||
"messages": [{"role": "user", "content": self._trim_prompt(prompt)}],
|
||||
chat_token_parameter("OpenAI Azure", model): out_budget,
|
||||
}
|
||||
if should_send_temperature("OpenAI Azure", model):
|
||||
body["temperature"] = float(self._opt(CONF_OPENAI_AZURE_TEMPERATURE, DEFAULT_TEMPERATURE))
|
||||
response = await self._post_json(
|
||||
endpoint,
|
||||
headers={"api-key": api_key, "Content-Type": "application/json"},
|
||||
body=body,
|
||||
provider_label="Azure OpenAI",
|
||||
)
|
||||
return self._extract_chat_content(response, "Azure OpenAI") if response else None
|
||||
|
||||
async def _generic_openai(self, prompt: str) -> str | None:
|
||||
endpoint = str(self._opt(CONF_GENERIC_OPENAI_ENDPOINT) or "").rstrip("/")
|
||||
if not endpoint or not re.match(r"^https?://", endpoint):
|
||||
raise ValueError("Generic OpenAI endpoint must be a full http(s) URL")
|
||||
api_key = self._opt(CONF_GENERIC_OPENAI_API_KEY)
|
||||
model = self._current_model("Generic OpenAI")
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
body = self._openai_compatible_body(
|
||||
provider="Generic OpenAI",
|
||||
model=model,
|
||||
prompt=self._trim_prompt(prompt),
|
||||
temperature=float(self._opt(CONF_GENERIC_OPENAI_TEMPERATURE, DEFAULT_TEMPERATURE)),
|
||||
)
|
||||
response = await self._post_json(endpoint, headers=headers, body=body, provider_label="Generic OpenAI")
|
||||
return self._extract_chat_content(response, "Generic OpenAI") if response else None
|
||||
|
||||
async def _anthropic(self, prompt: str) -> str | None:
|
||||
api_key = self._opt(CONF_ANTHROPIC_API_KEY)
|
||||
if not api_key:
|
||||
raise ValueError("Anthropic API key not configured")
|
||||
_, out_budget = self._budgets()
|
||||
model = self._current_model("Anthropic")
|
||||
body: dict[str, Any] = {
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": [{"type": "text", "text": self._trim_prompt(prompt)}]}],
|
||||
"max_tokens": out_budget,
|
||||
"temperature": float(self._opt(CONF_ANTHROPIC_TEMPERATURE, DEFAULT_TEMPERATURE)),
|
||||
}
|
||||
response = await self._post_json(
|
||||
ENDPOINT_ANTHROPIC,
|
||||
headers={"x-api-key": api_key, "Content-Type": "application/json", "anthropic-version": VERSION_ANTHROPIC},
|
||||
body=body,
|
||||
provider_label="Anthropic",
|
||||
)
|
||||
if not response:
|
||||
return None
|
||||
self._last_response_metadata = {
|
||||
"stop_reason": response.get("stop_reason"),
|
||||
"usage": response.get("usage"),
|
||||
}
|
||||
content = response.get("content") or []
|
||||
text_parts = [part.get("text", "") for part in content if isinstance(part, dict) and part.get("type") == "text"]
|
||||
if text_parts:
|
||||
return "".join(text_parts)
|
||||
raise ValueError(f"Anthropic response missing text content: {response}")
|
||||
|
||||
async def _google(self, prompt: str) -> str | None:
|
||||
api_key = self._opt(CONF_GOOGLE_API_KEY)
|
||||
if not api_key:
|
||||
raise ValueError("Google API key not configured")
|
||||
_, out_budget = self._budgets()
|
||||
model = self._current_model("Google")
|
||||
generation_config: dict[str, Any] = {
|
||||
"temperature": float(self._opt(CONF_GOOGLE_TEMPERATURE, DEFAULT_TEMPERATURE)),
|
||||
"maxOutputTokens": out_budget,
|
||||
}
|
||||
if supports_json_schema("Google", model):
|
||||
generation_config["responseMimeType"] = "application/json"
|
||||
generation_config["responseSchema"] = google_json_schema_response_format()["json_schema"]["schema"]
|
||||
body = {"contents": [{"parts": [{"text": self._trim_prompt(prompt)}]}], "generationConfig": generation_config}
|
||||
endpoint = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"
|
||||
response = await self._post_json(endpoint, body=body, provider_label="Google")
|
||||
if not response:
|
||||
return None
|
||||
candidates = response.get("candidates") or []
|
||||
if not candidates:
|
||||
raise ValueError(f"Google response missing candidates: {response}")
|
||||
self._last_response_metadata = {
|
||||
"finish_reason": candidates[0].get("finishReason"),
|
||||
"usage": response.get("usageMetadata"),
|
||||
}
|
||||
parts = candidates[0].get("content", {}).get("parts", [])
|
||||
text_parts = [part.get("text", "") for part in parts if isinstance(part, dict)]
|
||||
return "".join(text_parts) if text_parts else None
|
||||
|
||||
async def _groq(self, prompt: str) -> str | None:
|
||||
api_key = self._opt(CONF_GROQ_API_KEY)
|
||||
if not api_key:
|
||||
raise ValueError("Groq API key not configured")
|
||||
model = self._current_model("Groq")
|
||||
body = self._openai_compatible_body(
|
||||
provider="Groq",
|
||||
model=model,
|
||||
prompt=self._trim_prompt(prompt),
|
||||
temperature=float(self._opt(CONF_GROQ_TEMPERATURE, DEFAULT_TEMPERATURE)),
|
||||
)
|
||||
response = await self._post_json(
|
||||
ENDPOINT_GROQ,
|
||||
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
||||
body=body,
|
||||
provider_label="Groq",
|
||||
)
|
||||
return self._extract_chat_content(response, "Groq") if response else None
|
||||
|
||||
async def _localai(self, prompt: str) -> str | None:
|
||||
ip = self._opt(CONF_LOCALAI_IP_ADDRESS)
|
||||
port = self._opt(CONF_LOCALAI_PORT)
|
||||
if not ip or not port:
|
||||
raise ValueError("LocalAI not fully configured")
|
||||
proto = "https" if self._opt(CONF_LOCALAI_HTTPS, False) else "http"
|
||||
endpoint = ENDPOINT_LOCALAI.format(protocol=proto, ip_address=ip, port=port)
|
||||
model = self._current_model("LocalAI")
|
||||
body = self._openai_compatible_body(
|
||||
provider="LocalAI",
|
||||
model=model,
|
||||
prompt=self._trim_prompt(prompt),
|
||||
temperature=float(self._opt(CONF_LOCALAI_TEMPERATURE, DEFAULT_TEMPERATURE)),
|
||||
)
|
||||
response = await self._post_json(endpoint, body=body, provider_label="LocalAI")
|
||||
return self._extract_chat_content(response, "LocalAI") if response else None
|
||||
|
||||
async def _ollama(self, prompt: str) -> str | None:
|
||||
ip = self._opt(CONF_OLLAMA_IP_ADDRESS)
|
||||
port = self._opt(CONF_OLLAMA_PORT)
|
||||
base = ollama_base_url(
|
||||
base_url=self._opt(CONF_OLLAMA_BASE_URL),
|
||||
ip_address=ip,
|
||||
port=port,
|
||||
https=self._opt(CONF_OLLAMA_HTTPS, False),
|
||||
)
|
||||
if not base:
|
||||
raise ValueError("Ollama host/port or base URL is not configured")
|
||||
messages = []
|
||||
if self._opt(CONF_OLLAMA_DISABLE_THINK, False):
|
||||
messages.append({"role": "system", "content": "/no_think"})
|
||||
messages.append({"role": "user", "content": self._trim_prompt(prompt)})
|
||||
_, out_budget = self._budgets()
|
||||
body = {
|
||||
"model": self._current_model("Ollama"),
|
||||
"messages": messages,
|
||||
"stream": False,
|
||||
"options": {
|
||||
"temperature": float(self._opt(CONF_OLLAMA_TEMPERATURE, DEFAULT_TEMPERATURE)),
|
||||
"num_predict": out_budget,
|
||||
},
|
||||
}
|
||||
response = None
|
||||
headers = bearer_auth_headers(self._opt(CONF_OLLAMA_API_KEY))
|
||||
for endpoint in ollama_api_candidates(base, "api/chat"):
|
||||
response = await self._post_json(endpoint, headers=headers, body=body, provider_label="Ollama")
|
||||
if response:
|
||||
break
|
||||
if not response:
|
||||
return None
|
||||
self._last_response_metadata = {"done_reason": response.get("done_reason"), "usage": response.get("eval_count")}
|
||||
return response.get("message", {}).get("content")
|
||||
|
||||
async def _custom_openai(self, prompt: str) -> str | None:
|
||||
endpoint = str(self._opt(CONF_CUSTOM_OPENAI_ENDPOINT) or "").rstrip("/")
|
||||
if not endpoint:
|
||||
raise ValueError("Custom OpenAI endpoint not configured")
|
||||
completions_endpoint = openai_chat_endpoint(endpoint)
|
||||
headers = {"Content-Type": "application/json"}
|
||||
api_key = self._opt(CONF_CUSTOM_OPENAI_API_KEY)
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
model = self._current_model("Custom OpenAI")
|
||||
body = self._openai_compatible_body(
|
||||
provider="Custom OpenAI",
|
||||
model=model,
|
||||
prompt=self._trim_prompt(prompt),
|
||||
temperature=float(self._opt(CONF_CUSTOM_OPENAI_TEMPERATURE, DEFAULT_TEMPERATURE)),
|
||||
)
|
||||
response = await self._post_json(completions_endpoint, headers=headers, body=body, provider_label="Custom OpenAI")
|
||||
return self._extract_chat_content(response, "Custom OpenAI") if response else None
|
||||
|
||||
async def _mistral(self, prompt: str) -> str | None:
|
||||
api_key = self._opt(CONF_MISTRAL_API_KEY)
|
||||
if not api_key:
|
||||
raise ValueError("Mistral API key not configured")
|
||||
model = self._current_model("Mistral AI")
|
||||
body = self._openai_compatible_body(
|
||||
provider="Mistral AI",
|
||||
model=model,
|
||||
prompt=self._trim_prompt(prompt),
|
||||
temperature=float(self._opt(CONF_MISTRAL_TEMPERATURE, DEFAULT_TEMPERATURE)),
|
||||
)
|
||||
response = await self._post_json(
|
||||
ENDPOINT_MISTRAL,
|
||||
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
||||
body=body,
|
||||
provider_label="Mistral",
|
||||
)
|
||||
return self._extract_chat_content(response, "Mistral") if response else None
|
||||
|
||||
async def _perplexity(self, prompt: str) -> str | None:
|
||||
api_key = self._opt(CONF_PERPLEXITY_API_KEY)
|
||||
if not api_key:
|
||||
raise ValueError("Perplexity API key not configured")
|
||||
model = self._current_model("Perplexity AI")
|
||||
body = self._openai_compatible_body(
|
||||
provider="Perplexity AI",
|
||||
model=model,
|
||||
prompt=self._trim_prompt(prompt),
|
||||
temperature=float(self._opt(CONF_PERPLEXITY_TEMPERATURE, DEFAULT_TEMPERATURE)),
|
||||
)
|
||||
response = await self._post_json(
|
||||
ENDPOINT_PERPLEXITY,
|
||||
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json", "Accept": "application/json"},
|
||||
body=body,
|
||||
provider_label="Perplexity",
|
||||
)
|
||||
return self._extract_chat_content(response, "Perplexity") if response else None
|
||||
|
||||
async def _openrouter(self, prompt: str) -> str | None:
|
||||
api_key = self._opt(CONF_OPENROUTER_API_KEY)
|
||||
if not api_key:
|
||||
raise ValueError("OpenRouter API key not configured")
|
||||
model = self._current_model("OpenRouter")
|
||||
extra: dict[str, Any] = {}
|
||||
reasoning_max_tokens = int(self._opt(CONF_OPENROUTER_REASONING_MAX_TOKENS, 0))
|
||||
if reasoning_max_tokens > 0:
|
||||
extra["reasoning"] = {"max_tokens": reasoning_max_tokens}
|
||||
body = self._openai_compatible_body(
|
||||
provider="OpenRouter",
|
||||
model=model,
|
||||
prompt=self._trim_prompt(prompt),
|
||||
temperature=float(self._opt(CONF_OPENROUTER_TEMPERATURE, DEFAULT_TEMPERATURE)),
|
||||
extra=extra,
|
||||
)
|
||||
response = await self._post_json(
|
||||
ENDPOINT_OPENROUTER,
|
||||
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
||||
body=body,
|
||||
provider_label="OpenRouter",
|
||||
)
|
||||
return self._extract_chat_content(response, "OpenRouter") if response else None
|
||||
|
||||
async def _litellm(self, prompt: str) -> str | None:
|
||||
import litellm
|
||||
|
||||
model = self._current_model("LiteLLM")
|
||||
_, out_budget = self._budgets()
|
||||
kwargs: dict[str, Any] = {
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": self._trim_prompt(prompt)}],
|
||||
"max_tokens": out_budget,
|
||||
"temperature": float(self._opt(CONF_LITELLM_TEMPERATURE, DEFAULT_TEMPERATURE)),
|
||||
}
|
||||
api_key = self._opt(CONF_LITELLM_API_KEY)
|
||||
if api_key:
|
||||
kwargs["api_key"] = api_key
|
||||
api_base = self._opt(CONF_LITELLM_API_BASE)
|
||||
if api_base:
|
||||
kwargs["api_base"] = api_base
|
||||
|
||||
timeout_seconds = int(self._opt(CONF_REQUEST_TIMEOUT, DEFAULT_REQUEST_TIMEOUT))
|
||||
kwargs["timeout"] = max(10, timeout_seconds)
|
||||
|
||||
response = await litellm.acompletion(**kwargs)
|
||||
self._last_response_metadata = {
|
||||
"finish_reason": response.choices[0].finish_reason,
|
||||
"usage": {
|
||||
"prompt_tokens": response.usage.prompt_tokens,
|
||||
"completion_tokens": response.usage.completion_tokens,
|
||||
"total_tokens": response.usage.total_tokens,
|
||||
},
|
||||
}
|
||||
content = response.choices[0].message.content
|
||||
if content is None:
|
||||
raise ValueError("LiteLLM response missing content")
|
||||
return content
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Endpoint normalization helpers for provider setup and requests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def ensure_http_url(value: str | None, *, default_scheme: str = "http") -> str:
|
||||
"""Return a stripped HTTP(S) URL, adding a scheme when one is omitted."""
|
||||
|
||||
endpoint = str(value or "").strip().rstrip("/")
|
||||
if not endpoint:
|
||||
return ""
|
||||
if re.match(r"^https?://", endpoint):
|
||||
return endpoint
|
||||
return f"{default_scheme}://{endpoint}"
|
||||
|
||||
|
||||
def openai_chat_endpoint(endpoint: str | None) -> str:
|
||||
"""Normalize an OpenAI-compatible endpoint to a chat completions URL."""
|
||||
|
||||
base = ensure_http_url(endpoint)
|
||||
if not base:
|
||||
return ""
|
||||
if base.endswith("/chat/completions"):
|
||||
return base
|
||||
if base.endswith("/api"):
|
||||
return f"{base}/chat/completions"
|
||||
if base.endswith("/v1"):
|
||||
return f"{base}/chat/completions"
|
||||
return f"{base}/v1/chat/completions"
|
||||
|
||||
|
||||
def openai_model_endpoint_candidates(endpoint: str | None) -> list[str]:
|
||||
"""Return likely model-listing endpoints for OpenAI-compatible servers."""
|
||||
|
||||
base = ensure_http_url(endpoint)
|
||||
if not base:
|
||||
return []
|
||||
if base.endswith("/chat/completions"):
|
||||
base = base[: -len("/chat/completions")]
|
||||
|
||||
candidates: list[str]
|
||||
if base.endswith("/api"):
|
||||
candidates = [f"{base}/models", f"{base}/v1/models"]
|
||||
elif base.endswith("/v1"):
|
||||
candidates = [f"{base}/models"]
|
||||
else:
|
||||
candidates = [f"{base}/v1/models", f"{base}/models", f"{base}/api/models"]
|
||||
return _dedupe(candidates)
|
||||
|
||||
|
||||
def ollama_base_url(
|
||||
*,
|
||||
base_url: str | None = None,
|
||||
ip_address: str | None = None,
|
||||
port: int | str | None = None,
|
||||
https: bool = False,
|
||||
) -> str:
|
||||
"""Build an Ollama-compatible base URL from either a full URL or host/port fields."""
|
||||
|
||||
if base_url:
|
||||
return ensure_http_url(base_url)
|
||||
if not ip_address:
|
||||
return ""
|
||||
host = str(ip_address).strip().rstrip("/")
|
||||
if re.match(r"^https?://", host):
|
||||
return host
|
||||
proto = "https" if https else "http"
|
||||
return f"{proto}://{host}:{port or 11434}"
|
||||
|
||||
|
||||
def ollama_api_candidates(base_url: str, api_path: str) -> list[str]:
|
||||
"""Return likely Ollama API paths for native Ollama and Open WebUI proxies."""
|
||||
|
||||
base = ensure_http_url(base_url)
|
||||
if not base:
|
||||
return []
|
||||
path = api_path.strip("/")
|
||||
if base.endswith(f"/{path}"):
|
||||
return [base]
|
||||
if base.endswith("/api") and path.startswith("api/"):
|
||||
return [f"{base}/{path.split('/', 1)[1]}"]
|
||||
candidates = [f"{base}/{path}"]
|
||||
if not base.endswith("/ollama"):
|
||||
candidates.append(f"{base}/ollama/{path}")
|
||||
return _dedupe(candidates)
|
||||
|
||||
|
||||
def bearer_auth_headers(api_key: str | None) -> dict[str, str] | None:
|
||||
"""Return bearer authorization headers for provider APIs that need a token."""
|
||||
|
||||
token = str(api_key or "").strip()
|
||||
if not token:
|
||||
return None
|
||||
if token.lower().startswith("bearer "):
|
||||
return {"Authorization": token}
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def _dedupe(values: list[str]) -> list[str]:
|
||||
seen: set[str] = set()
|
||||
deduped: list[str] = []
|
||||
for value in values:
|
||||
if value not in seen:
|
||||
seen.add(value)
|
||||
deduped.append(value)
|
||||
return deduped
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Language helpers for suggestion prompt localization."""
|
||||
|
||||
LANGUAGE_NAMES = {
|
||||
"ca": "Catalan",
|
||||
"cs": "Czech",
|
||||
"de": "German",
|
||||
"en": "English",
|
||||
"es": "Spanish",
|
||||
"it": "Italian",
|
||||
"nl": "Dutch",
|
||||
"pt": "Portuguese",
|
||||
"ru": "Russian",
|
||||
"tr": "Turkish",
|
||||
"zh": "Chinese",
|
||||
}
|
||||
|
||||
|
||||
def language_name(language_code: str | None) -> str | None:
|
||||
"""Return a friendly language name from a Home Assistant language code."""
|
||||
|
||||
normalized = str(language_code or "").strip().replace("_", "-").lower()
|
||||
if not normalized:
|
||||
return None
|
||||
base_code = normalized.split("-", 1)[0]
|
||||
return LANGUAGE_NAMES.get(base_code, normalized)
|
||||
|
||||
|
||||
def suggestion_language_instruction(language_code: str | None) -> str:
|
||||
"""Build an instruction that asks providers to localize suggestion text."""
|
||||
|
||||
name = language_name(language_code)
|
||||
if not name or name == "English":
|
||||
return ""
|
||||
return (
|
||||
f"Home Assistant configured language: {name}. "
|
||||
f"Write suggestion titles, descriptions, and warnings in {name}. "
|
||||
"Keep YAML, entity_ids, service names, and code identifiers unchanged."
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"domain": "ai_automation_suggester",
|
||||
"name": "AI Automation Suggester",
|
||||
"codeowners": ["@ITSpecialist111"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["http"],
|
||||
"documentation": "https://github.com/ITSpecialist111/ai_automation_suggester",
|
||||
"iot_class": "cloud_polling",
|
||||
"issue_tracker": "https://github.com/ITSpecialist111/ai_automation_suggester/issues",
|
||||
"requirements": [
|
||||
"anthropic>=0.8.0",
|
||||
"aiohttp>=3.8.0",
|
||||
"litellm>=1.67.0",
|
||||
"pyyaml>=6.0",
|
||||
"voluptuous>=0.13.1"
|
||||
],
|
||||
"version": "1.5.7"
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
"""Static model capability metadata for AI Automation Suggester.
|
||||
|
||||
The catalog is intentionally conservative. Providers still allow custom model
|
||||
names, but known metadata lets the integration choose safer request parameters
|
||||
and warn users about stale defaults, preview models, and deprecated IDs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
STATUS_STABLE = "stable"
|
||||
STATUS_PREVIEW = "preview"
|
||||
STATUS_DEPRECATED = "deprecated"
|
||||
STATUS_CUSTOM = "custom"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ModelCapabilities:
|
||||
"""Capabilities for a specific model or model family."""
|
||||
|
||||
model: str
|
||||
label: str = ""
|
||||
endpoint_family: str = "chat"
|
||||
context_window: int | None = None
|
||||
max_output_tokens: int | None = None
|
||||
token_parameter: str = "max_tokens"
|
||||
supports_structured_output: bool = False
|
||||
supports_json_schema: bool = False
|
||||
supports_reasoning: bool = False
|
||||
omit_temperature: bool = False
|
||||
status: str = STATUS_STABLE
|
||||
notes: tuple[str, ...] = field(default_factory=tuple)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProviderCatalog:
|
||||
"""Default model and known capabilities for a provider."""
|
||||
|
||||
provider: str
|
||||
default_model: str
|
||||
models: tuple[ModelCapabilities, ...]
|
||||
supports_model_listing: bool = False
|
||||
model_listing_url: str | None = None
|
||||
|
||||
|
||||
SUGGESTION_RESPONSE_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"suggestions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"yaml": {"type": "string"},
|
||||
"entities_used": {"type": "array", "items": {"type": "string"}},
|
||||
"automation_ids_used": {"type": "array", "items": {"type": "string"}},
|
||||
"confidence": {"type": "number"},
|
||||
"warnings": {"type": "array", "items": {"type": "string"}},
|
||||
},
|
||||
"required": ["title", "description", "yaml"],
|
||||
"additionalProperties": True,
|
||||
},
|
||||
}
|
||||
},
|
||||
"required": ["suggestions"],
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
|
||||
OPENAI_MODELS = (
|
||||
ModelCapabilities(
|
||||
"gpt-5.5",
|
||||
"GPT-5.5",
|
||||
endpoint_family="responses",
|
||||
token_parameter="max_output_tokens",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
supports_reasoning=True,
|
||||
omit_temperature=True,
|
||||
notes=("Use the OpenAI Responses API.",),
|
||||
),
|
||||
ModelCapabilities(
|
||||
"gpt-5.5-pro",
|
||||
"GPT-5.5 Pro",
|
||||
endpoint_family="responses",
|
||||
token_parameter="max_output_tokens",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
supports_reasoning=True,
|
||||
omit_temperature=True,
|
||||
notes=("Highest-intelligence OpenAI option; expect higher latency and cost.",),
|
||||
),
|
||||
ModelCapabilities(
|
||||
"gpt-5.4-mini",
|
||||
"GPT-5.4 Mini",
|
||||
endpoint_family="responses",
|
||||
token_parameter="max_output_tokens",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
supports_reasoning=True,
|
||||
omit_temperature=True,
|
||||
),
|
||||
ModelCapabilities(
|
||||
"gpt-5.4",
|
||||
"GPT-5.4",
|
||||
endpoint_family="responses",
|
||||
token_parameter="max_output_tokens",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
supports_reasoning=True,
|
||||
omit_temperature=True,
|
||||
),
|
||||
ModelCapabilities(
|
||||
"gpt-4o-mini",
|
||||
"GPT-4o Mini",
|
||||
token_parameter="max_completion_tokens",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
),
|
||||
ModelCapabilities(
|
||||
"gpt-4.1-mini",
|
||||
"GPT-4.1 Mini",
|
||||
token_parameter="max_completion_tokens",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
),
|
||||
ModelCapabilities(
|
||||
"o3",
|
||||
"o3",
|
||||
token_parameter="max_completion_tokens",
|
||||
supports_reasoning=True,
|
||||
omit_temperature=True,
|
||||
),
|
||||
ModelCapabilities(
|
||||
"o4-mini",
|
||||
"o4 Mini",
|
||||
token_parameter="max_completion_tokens",
|
||||
supports_reasoning=True,
|
||||
omit_temperature=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
PROVIDER_CATALOGS: dict[str, ProviderCatalog] = {
|
||||
"OpenAI": ProviderCatalog(
|
||||
"OpenAI",
|
||||
"gpt-5.4-mini",
|
||||
OPENAI_MODELS,
|
||||
True,
|
||||
"https://api.openai.com/v1/models",
|
||||
),
|
||||
"OpenAI Azure": ProviderCatalog(
|
||||
"OpenAI Azure",
|
||||
"gpt-5.4-mini",
|
||||
OPENAI_MODELS,
|
||||
),
|
||||
"Anthropic": ProviderCatalog(
|
||||
"Anthropic",
|
||||
"claude-sonnet-4-6",
|
||||
(
|
||||
ModelCapabilities(
|
||||
"claude-opus-4-7",
|
||||
"Claude Opus 4.7",
|
||||
token_parameter="max_tokens",
|
||||
supports_reasoning=True,
|
||||
notes=("Use adaptive thinking; manual budget_tokens is not accepted.",),
|
||||
),
|
||||
ModelCapabilities(
|
||||
"claude-sonnet-4-6",
|
||||
"Claude Sonnet 4.6",
|
||||
token_parameter="max_tokens",
|
||||
supports_reasoning=True,
|
||||
notes=("Adaptive thinking is recommended for reasoning-heavy requests.",),
|
||||
),
|
||||
ModelCapabilities("claude-haiku-4-5", "Claude Haiku 4.5"),
|
||||
ModelCapabilities(
|
||||
"claude-3-7-sonnet-latest",
|
||||
"Claude 3.7 Sonnet Latest",
|
||||
status=STATUS_DEPRECATED,
|
||||
notes=("Prefer Claude Sonnet 4.6 for new configurations.",),
|
||||
),
|
||||
),
|
||||
True,
|
||||
"https://api.anthropic.com/v1/models",
|
||||
),
|
||||
"Google": ProviderCatalog(
|
||||
"Google",
|
||||
"gemini-2.5-flash",
|
||||
(
|
||||
ModelCapabilities(
|
||||
"gemini-2.5-flash",
|
||||
"Gemini 2.5 Flash",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
),
|
||||
ModelCapabilities(
|
||||
"gemini-2.5-pro",
|
||||
"Gemini 2.5 Pro",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
),
|
||||
ModelCapabilities(
|
||||
"gemini-3-flash-preview",
|
||||
"Gemini 3 Flash Preview",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
status=STATUS_PREVIEW,
|
||||
),
|
||||
ModelCapabilities(
|
||||
"gemini-3.1-pro-preview",
|
||||
"Gemini 3.1 Pro Preview",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
status=STATUS_PREVIEW,
|
||||
),
|
||||
ModelCapabilities(
|
||||
"gemini-2.0-flash",
|
||||
"Gemini 2.0 Flash",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
status=STATUS_DEPRECATED,
|
||||
notes=("Gemini 2.0 Flash is deprecated in current Gemini docs.",),
|
||||
),
|
||||
),
|
||||
True,
|
||||
"https://generativelanguage.googleapis.com/v1beta/models",
|
||||
),
|
||||
"Groq": ProviderCatalog(
|
||||
"Groq",
|
||||
"llama-3.3-70b-versatile",
|
||||
(
|
||||
ModelCapabilities("llama-3.3-70b-versatile", "Llama 3.3 70B Versatile"),
|
||||
ModelCapabilities("llama-3.1-8b-instant", "Llama 3.1 8B Instant"),
|
||||
ModelCapabilities("openai/gpt-oss-120b", "GPT OSS 120B"),
|
||||
ModelCapabilities("openai/gpt-oss-20b", "GPT OSS 20B"),
|
||||
ModelCapabilities(
|
||||
"llama3-8b-8192",
|
||||
"Llama 3 8B 8192",
|
||||
status=STATUS_DEPRECATED,
|
||||
notes=("Use llama-3.1-8b-instant or llama-3.3-70b-versatile.",),
|
||||
),
|
||||
),
|
||||
True,
|
||||
"https://api.groq.com/openai/v1/models",
|
||||
),
|
||||
"LocalAI": ProviderCatalog("LocalAI", "llama3", (), True),
|
||||
"Ollama": ProviderCatalog("Ollama", "llama3.1", (), True),
|
||||
"Custom OpenAI": ProviderCatalog("Custom OpenAI", "gpt-4o-mini", OPENAI_MODELS, True),
|
||||
"Generic OpenAI": ProviderCatalog("Generic OpenAI", "gpt-4o-mini", OPENAI_MODELS, True),
|
||||
"Mistral AI": ProviderCatalog(
|
||||
"Mistral AI",
|
||||
"mistral-small-latest",
|
||||
(
|
||||
ModelCapabilities(
|
||||
"mistral-small-latest",
|
||||
"Mistral Small Latest",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
),
|
||||
ModelCapabilities(
|
||||
"mistral-medium-latest",
|
||||
"Mistral Medium Latest",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
),
|
||||
ModelCapabilities(
|
||||
"mistral-large-latest",
|
||||
"Mistral Large Latest",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
),
|
||||
ModelCapabilities(
|
||||
"mistral-medium",
|
||||
"Mistral Medium",
|
||||
status=STATUS_DEPRECATED,
|
||||
notes=("Prefer current -latest aliases or published dated IDs.",),
|
||||
),
|
||||
),
|
||||
True,
|
||||
"https://api.mistral.ai/v1/models",
|
||||
),
|
||||
"Perplexity AI": ProviderCatalog(
|
||||
"Perplexity AI",
|
||||
"sonar",
|
||||
(
|
||||
ModelCapabilities("sonar", "Sonar", supports_structured_output=True, supports_json_schema=True),
|
||||
ModelCapabilities(
|
||||
"sonar-pro",
|
||||
"Sonar Pro",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
),
|
||||
ModelCapabilities(
|
||||
"sonar-reasoning-pro",
|
||||
"Sonar Reasoning Pro",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
supports_reasoning=True,
|
||||
),
|
||||
ModelCapabilities(
|
||||
"sonar-deep-research",
|
||||
"Sonar Deep Research",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
supports_reasoning=True,
|
||||
notes=("Expect higher latency for research workflows.",),
|
||||
),
|
||||
),
|
||||
),
|
||||
"OpenRouter": ProviderCatalog(
|
||||
"OpenRouter",
|
||||
"openai/gpt-5.4-mini",
|
||||
(
|
||||
ModelCapabilities(
|
||||
"openai/gpt-5.5",
|
||||
"OpenAI GPT-5.5 via OpenRouter",
|
||||
token_parameter="max_tokens",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
supports_reasoning=True,
|
||||
omit_temperature=True,
|
||||
),
|
||||
ModelCapabilities(
|
||||
"openai/gpt-5.4-mini",
|
||||
"OpenAI GPT-5.4 Mini via OpenRouter",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
supports_reasoning=True,
|
||||
omit_temperature=True,
|
||||
),
|
||||
ModelCapabilities(
|
||||
"anthropic/claude-opus-4.7",
|
||||
"Claude Opus 4.7 via OpenRouter",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
supports_reasoning=True,
|
||||
),
|
||||
ModelCapabilities(
|
||||
"anthropic/claude-sonnet-4.6",
|
||||
"Claude Sonnet 4.6 via OpenRouter",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
supports_reasoning=True,
|
||||
),
|
||||
),
|
||||
True,
|
||||
"https://openrouter.ai/api/v1/models",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_provider_catalog(provider: str) -> ProviderCatalog | None:
|
||||
"""Return the static catalog for a provider."""
|
||||
|
||||
return PROVIDER_CATALOGS.get(provider)
|
||||
|
||||
|
||||
def get_default_model(provider: str) -> str:
|
||||
"""Return the recommended default model for a provider."""
|
||||
|
||||
catalog = get_provider_catalog(provider)
|
||||
return catalog.default_model if catalog else ""
|
||||
|
||||
|
||||
def get_model_capabilities(provider: str, model: str | None) -> ModelCapabilities:
|
||||
"""Return known capabilities for a selected model.
|
||||
|
||||
Unknown user-supplied model names remain valid and are treated as custom chat
|
||||
models with conservative OpenAI-compatible defaults.
|
||||
"""
|
||||
|
||||
selected = model or get_default_model(provider)
|
||||
catalog = get_provider_catalog(provider)
|
||||
if catalog:
|
||||
for item in catalog.models:
|
||||
if item.model == selected:
|
||||
return item
|
||||
for item in catalog.models:
|
||||
if selected.startswith(item.model.rstrip("*")):
|
||||
return item
|
||||
|
||||
if provider in {"OpenAI", "OpenAI Azure", "Custom OpenAI", "Generic OpenAI"}:
|
||||
if selected.startswith(("gpt-5", "o3", "o4")):
|
||||
return ModelCapabilities(
|
||||
selected,
|
||||
endpoint_family="responses" if provider == "OpenAI" else "chat",
|
||||
token_parameter="max_output_tokens" if provider == "OpenAI" else "max_completion_tokens",
|
||||
supports_structured_output=provider == "OpenAI",
|
||||
supports_json_schema=provider == "OpenAI",
|
||||
supports_reasoning=True,
|
||||
omit_temperature=True,
|
||||
)
|
||||
if selected.startswith(("gpt-4o", "gpt-4.1")):
|
||||
return ModelCapabilities(
|
||||
selected,
|
||||
token_parameter="max_completion_tokens",
|
||||
supports_structured_output=True,
|
||||
supports_json_schema=True,
|
||||
)
|
||||
|
||||
return ModelCapabilities(selected, status=STATUS_CUSTOM)
|
||||
|
||||
|
||||
def model_uses_responses_api(provider: str, model: str | None) -> bool:
|
||||
"""Return True when the model should use OpenAI's Responses API."""
|
||||
|
||||
return provider == "OpenAI" and get_model_capabilities(provider, model).endpoint_family == "responses"
|
||||
|
||||
|
||||
def chat_token_parameter(provider: str, model: str | None) -> str:
|
||||
"""Return the correct token limit parameter for chat-compatible APIs."""
|
||||
|
||||
capabilities = get_model_capabilities(provider, model)
|
||||
if capabilities.token_parameter == "max_output_tokens":
|
||||
return "max_completion_tokens"
|
||||
return capabilities.token_parameter
|
||||
|
||||
|
||||
def should_send_temperature(provider: str, model: str | None) -> bool:
|
||||
"""Return False for models known to reject custom temperature settings."""
|
||||
|
||||
return not get_model_capabilities(provider, model).omit_temperature
|
||||
|
||||
|
||||
def supports_json_schema(provider: str, model: str | None) -> bool:
|
||||
"""Return True if this provider/model should receive JSON schema requests."""
|
||||
|
||||
return get_model_capabilities(provider, model).supports_json_schema
|
||||
|
||||
|
||||
def json_schema_response_format() -> dict:
|
||||
"""Return an OpenAI-compatible JSON schema response_format block."""
|
||||
|
||||
return {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "automation_suggestions",
|
||||
"strict": False,
|
||||
"schema": SUGGESTION_RESPONSE_SCHEMA,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _strip_schema_keys(value, unsupported_keys: set[str]):
|
||||
"""Return a copy of a schema value without unsupported keys."""
|
||||
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: _strip_schema_keys(schema_value, unsupported_keys)
|
||||
for key, schema_value in value.items()
|
||||
if key not in unsupported_keys
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [_strip_schema_keys(item, unsupported_keys) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def google_json_schema_response_format() -> dict:
|
||||
"""Return a Google Gemini-compatible JSON schema response format."""
|
||||
|
||||
response_format = json_schema_response_format()
|
||||
return {
|
||||
"type": response_format["type"],
|
||||
"json_schema": {
|
||||
"name": response_format["json_schema"]["name"],
|
||||
"strict": response_format["json_schema"]["strict"],
|
||||
"schema": _strip_schema_keys(
|
||||
response_format["json_schema"]["schema"],
|
||||
{"additionalProperties"},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def compatibility_warnings(provider: str, model: str | None) -> list[str]:
|
||||
"""Return user-facing warnings for the selected provider/model."""
|
||||
|
||||
capabilities = get_model_capabilities(provider, model)
|
||||
warnings: list[str] = []
|
||||
if capabilities.status == STATUS_PREVIEW:
|
||||
warnings.append(f"{capabilities.model} is a preview model and may change without notice.")
|
||||
if capabilities.status == STATUS_DEPRECATED:
|
||||
warnings.append(f"{capabilities.model} is deprecated; choose a current model when possible.")
|
||||
warnings.extend(capabilities.notes)
|
||||
return warnings
|
||||
@@ -0,0 +1,459 @@
|
||||
# custom_components/ai_automation_suggester/sensor.py
|
||||
"""Sensor platform for AI Automation Suggester."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_UNKNOWN, EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
INTEGRATION_NAME,
|
||||
CONF_PROVIDER,
|
||||
PROVIDER_STATUS_CONNECTED,
|
||||
PROVIDER_STATUS_DISCONNECTED,
|
||||
PROVIDER_STATUS_ERROR,
|
||||
PROVIDER_STATUS_INITIALIZING,
|
||||
CONF_MAX_INPUT_TOKENS,
|
||||
DEFAULT_MAX_INPUT_TOKENS,
|
||||
CONF_MAX_OUTPUT_TOKENS,
|
||||
DEFAULT_MAX_OUTPUT_TOKENS,
|
||||
# Model configuration keys (used to display current model)
|
||||
CONF_OPENAI_MODEL,
|
||||
CONF_ANTHROPIC_MODEL,
|
||||
CONF_GOOGLE_MODEL,
|
||||
CONF_GROQ_MODEL,
|
||||
CONF_LOCALAI_MODEL,
|
||||
CONF_OLLAMA_MODEL,
|
||||
CONF_CUSTOM_OPENAI_MODEL,
|
||||
CONF_MISTRAL_MODEL,
|
||||
CONF_PERPLEXITY_MODEL,
|
||||
CONF_OPENROUTER_MODEL,
|
||||
CONF_OPENAI_AZURE_DEPLOYMENT_ID,
|
||||
CONF_GENERIC_OPENAI_MODEL,
|
||||
DEFAULT_MODELS,
|
||||
# Sensor Keys from const.py
|
||||
SENSOR_KEY_SUGGESTIONS,
|
||||
SENSOR_KEY_STATUS,
|
||||
SENSOR_KEY_INPUT_TOKENS,
|
||||
SENSOR_KEY_OUTPUT_TOKENS,
|
||||
SENSOR_KEY_MODEL,
|
||||
SENSOR_KEY_LAST_ERROR,
|
||||
SENSOR_KEY_HISTORY_COUNT,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PROVIDER_TO_MODEL_KEY_MAP: dict[str, str] = {
|
||||
"OpenAI": CONF_OPENAI_MODEL,
|
||||
"Anthropic": CONF_ANTHROPIC_MODEL,
|
||||
"Google": CONF_GOOGLE_MODEL,
|
||||
"Groq": CONF_GROQ_MODEL,
|
||||
"LocalAI": CONF_LOCALAI_MODEL,
|
||||
"Ollama": CONF_OLLAMA_MODEL,
|
||||
"Custom OpenAI": CONF_CUSTOM_OPENAI_MODEL,
|
||||
"Mistral AI": CONF_MISTRAL_MODEL,
|
||||
"Perplexity AI": CONF_PERPLEXITY_MODEL,
|
||||
"OpenRouter": CONF_OPENROUTER_MODEL,
|
||||
"OpenAI Azure": CONF_OPENAI_AZURE_DEPLOYMENT_ID,
|
||||
"Generic OpenAI": CONF_GENERIC_OPENAI_MODEL,
|
||||
}
|
||||
|
||||
SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KEY_SUGGESTIONS,
|
||||
name="AI Automation Suggestions",
|
||||
icon="mdi:robot-happy-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KEY_STATUS,
|
||||
name="AI Provider Status",
|
||||
icon="mdi:lan-check",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KEY_INPUT_TOKENS,
|
||||
name="Max Input Tokens",
|
||||
icon="mdi:format-letter-starts-with",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement="tokens",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KEY_OUTPUT_TOKENS,
|
||||
name="Max Output Tokens",
|
||||
icon="mdi:format-letter-ends-with",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement="tokens",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KEY_MODEL,
|
||||
name="AI Model In Use",
|
||||
icon="mdi:brain",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KEY_LAST_ERROR,
|
||||
name="Last Error Message",
|
||||
icon="mdi:alert-circle-outline",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KEY_HISTORY_COUNT,
|
||||
name="Suggestion History Count",
|
||||
icon="mdi:history",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AI Automation Suggester sensors from a config entry."""
|
||||
coordinator = cast(DataUpdateCoordinator, hass.data[DOMAIN][entry.entry_id])
|
||||
provider_name = entry.data.get(CONF_PROVIDER, "Unknown Provider")
|
||||
|
||||
entities: list[SensorEntity] = []
|
||||
for description in SENSOR_DESCRIPTIONS:
|
||||
formatted_name = f"{description.name} ({provider_name})"
|
||||
specific_description = SensorEntityDescription(
|
||||
key=description.key,
|
||||
name=formatted_name,
|
||||
icon=description.icon,
|
||||
entity_category=description.entity_category,
|
||||
native_unit_of_measurement=description.native_unit_of_measurement,
|
||||
state_class=description.state_class,
|
||||
device_class=description.device_class,
|
||||
)
|
||||
|
||||
if description.key == SENSOR_KEY_SUGGESTIONS:
|
||||
entities.append(AISuggestionsSensor(coordinator, entry, specific_description))
|
||||
elif description.key == SENSOR_KEY_STATUS:
|
||||
entities.append(AIProviderStatusSensor(coordinator, entry, specific_description))
|
||||
elif description.key == SENSOR_KEY_INPUT_TOKENS:
|
||||
entities.append(MaxInputTokensSensor(coordinator, entry, specific_description))
|
||||
elif description.key == SENSOR_KEY_OUTPUT_TOKENS:
|
||||
entities.append(MaxOutputTokensSensor(coordinator, entry, specific_description))
|
||||
elif description.key == SENSOR_KEY_MODEL:
|
||||
entities.append(AIModelSensor(coordinator, entry, specific_description))
|
||||
elif description.key == SENSOR_KEY_LAST_ERROR:
|
||||
entities.append(AILastErrorSensor(coordinator, entry, specific_description))
|
||||
elif description.key == SENSOR_KEY_HISTORY_COUNT:
|
||||
entities.append(AIHistoryCountSensor(coordinator, entry, specific_description))
|
||||
else:
|
||||
entities.append(AIBaseSensor(coordinator, entry, specific_description))
|
||||
|
||||
|
||||
# update_before_add must stay False: in HA 2025.x+, passing True calls
|
||||
# CoordinatorEntity.async_update() during platform setup, which triggers a
|
||||
# full LLM inference and exceeds the setup timeout (issue #166). Entities
|
||||
# populate from coordinator data and refresh on generate_suggestions.
|
||||
async_add_entities(entities, False)
|
||||
_LOGGER.debug("Sensor platform setup complete for provider: %s", provider_name)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Base sensor
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
class AIBaseSensor(CoordinatorEntity[DataUpdateCoordinator], SensorEntity):
|
||||
"""Base class for AI Automation Suggester sensors."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
entry: ConfigEntry,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{entry.entry_id}_{description.key}"
|
||||
self._entry = entry
|
||||
self._provider_name = entry.data.get(CONF_PROVIDER, "Unknown Provider")
|
||||
|
||||
# Common device info for all sensors of this config entry
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, entry.entry_id)},
|
||||
name=f"{INTEGRATION_NAME} ({self._provider_name})",
|
||||
manufacturer="Community",
|
||||
model=self._provider_name,
|
||||
sw_version=str(entry.version) if entry.version else "N/A",
|
||||
configuration_url=None, # Link Github?
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if coordinator is available and has data."""
|
||||
return super().available and self.coordinator.last_update_success
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
if self.coordinator.last_update_success:
|
||||
self._update_state_and_attributes()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
def _update_state_and_attributes(self) -> None:
|
||||
"""Update the sensor's state and attributes based on coordinator data.
|
||||
|
||||
This method should be overridden by subclasses.
|
||||
"""
|
||||
self._attr_native_value = STATE_UNKNOWN
|
||||
_LOGGER.debug(
|
||||
"Sensor %s._update_state_and_attributes not fully implemented for key %s",
|
||||
self.__class__.__name__,
|
||||
self.entity_description.key
|
||||
)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Suggestions sensor
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
class AISuggestionsSensor(AIBaseSensor):
|
||||
"""Shows the availability of new AI suggestions."""
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
entry: ConfigEntry,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
super().__init__(coordinator, entry, description)
|
||||
self._previous_suggestions_timestamp: float | None = None
|
||||
|
||||
# Initialize state with default values
|
||||
self._attr_native_value = "No Suggestions"
|
||||
self._attr_extra_state_attributes = {
|
||||
"suggestions": "No suggestions yet",
|
||||
"description": None,
|
||||
"yaml_block": None,
|
||||
"last_update": None,
|
||||
"entities_processed": [],
|
||||
"provider": self._entry.data.get(CONF_PROVIDER, "unknown"),
|
||||
"entities_processed_count": 0,
|
||||
"suggestion_count": 0,
|
||||
"warnings": [],
|
||||
}
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle added to Hass."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Update initial state from coordinator if data exists
|
||||
if self.coordinator.data:
|
||||
self._update_state_and_attributes()
|
||||
|
||||
|
||||
def _update_state_and_attributes(self) -> None:
|
||||
"""Update sensor state and attributes."""
|
||||
data = self.coordinator.data or {}
|
||||
suggestions = data.get("suggestions")
|
||||
last_update_timestamp = data.get("last_update")
|
||||
|
||||
if suggestions and suggestions not in ("No suggestions available", "No suggestions yet"):
|
||||
if last_update_timestamp and (self._previous_suggestions_timestamp is None or last_update_timestamp > self._previous_suggestions_timestamp):
|
||||
self._attr_native_value = "New Suggestions Available"
|
||||
self._previous_suggestions_timestamp = last_update_timestamp
|
||||
else:
|
||||
self._attr_native_value = "Suggestions Available"
|
||||
else:
|
||||
self._attr_native_value = "No Suggestions"
|
||||
|
||||
self._attr_extra_state_attributes = {
|
||||
"suggestions": suggestions,
|
||||
"description": data.get("description"),
|
||||
"yaml_block": data.get("yaml_block"),
|
||||
"last_update": data.get("last_update"),
|
||||
"entities_processed": data.get("entities_processed", []),
|
||||
"provider": self._entry.data.get(CONF_PROVIDER, "unknown"),
|
||||
"model": data.get("model"),
|
||||
"entities_processed_count": len(data.get("entities_processed", [])),
|
||||
"suggestion": data.get("suggestion"),
|
||||
"suggestion_count": data.get("suggestion_count", 0),
|
||||
"warnings": data.get("warnings", []),
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Provider‑status sensor
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
class AIProviderStatusSensor(AIBaseSensor):
|
||||
"""Indicates whether the configured provider is reachable."""
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
entry: ConfigEntry,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
super().__init__(coordinator, entry, description)
|
||||
self._update_state_and_attributes()
|
||||
|
||||
def _update_state_and_attributes(self) -> None:
|
||||
"""Update sensor state and attributes."""
|
||||
data = self.coordinator.data or {}
|
||||
if not self.coordinator.last_update_success:
|
||||
self._attr_native_value = PROVIDER_STATUS_ERROR
|
||||
elif not data:
|
||||
self._attr_native_value = PROVIDER_STATUS_INITIALIZING
|
||||
elif data.get("last_error"):
|
||||
self._attr_native_value = PROVIDER_STATUS_ERROR
|
||||
elif "suggestions" in data:
|
||||
self._attr_native_value = PROVIDER_STATUS_CONNECTED
|
||||
else:
|
||||
self._attr_native_value = PROVIDER_STATUS_DISCONNECTED
|
||||
|
||||
self._attr_extra_state_attributes = {
|
||||
"last_error_message": data.get("last_error", None),
|
||||
"last_attempted_update": data.get("last_update"),
|
||||
"provider": data.get("provider", self._provider_name),
|
||||
"model": data.get("model"),
|
||||
"warnings": data.get("warnings", []),
|
||||
"response_metadata": data.get("response_metadata", {}),
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Max Input Token Sensors
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
class MaxInputTokensSensor(AIBaseSensor):
|
||||
"""Shows the configured maximum input tokens."""
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
entry: ConfigEntry,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
super().__init__(coordinator, entry, description)
|
||||
self._update_state_and_attributes() # Initial update
|
||||
|
||||
def _update_state_and_attributes(self) -> None:
|
||||
"""Update sensor state from config entry options or data."""
|
||||
self._attr_native_value = self._entry.options.get(
|
||||
CONF_MAX_INPUT_TOKENS,
|
||||
self._entry.data.get(CONF_MAX_INPUT_TOKENS, DEFAULT_MAX_INPUT_TOKENS)
|
||||
)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Max Output Token Sensors
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
class MaxOutputTokensSensor(AIBaseSensor):
|
||||
"""Shows the configured maximum output tokens."""
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
entry: ConfigEntry,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
super().__init__(coordinator, entry, description)
|
||||
self._update_state_and_attributes() # Initial update
|
||||
|
||||
def _update_state_and_attributes(self) -> None:
|
||||
"""Update sensor state from config entry options or data."""
|
||||
self._attr_native_value = self._entry.options.get(
|
||||
CONF_MAX_OUTPUT_TOKENS,
|
||||
self._entry.data.get(CONF_MAX_OUTPUT_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS)
|
||||
)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Model Sensor
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
class AIModelSensor(AIBaseSensor):
|
||||
"""Shows the currently configured AI model."""
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
entry: ConfigEntry,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
super().__init__(coordinator, entry, description)
|
||||
self._update_state_and_attributes()
|
||||
|
||||
def _update_state_and_attributes(self) -> None:
|
||||
"""Update sensor state with the configured model."""
|
||||
provider = self._entry.data.get(CONF_PROVIDER)
|
||||
if not provider:
|
||||
self._attr_native_value = STATE_UNKNOWN
|
||||
return
|
||||
|
||||
model_key = PROVIDER_TO_MODEL_KEY_MAP.get(provider)
|
||||
if not model_key:
|
||||
self._attr_native_value = "Unknown Model Key"
|
||||
_LOGGER.warning("No model key found for provider: %s", provider)
|
||||
return
|
||||
|
||||
self._attr_native_value = self._entry.options.get(
|
||||
model_key,
|
||||
self._entry.data.get(model_key, DEFAULT_MODELS.get(provider, "unknown"))
|
||||
) if model_key else "unknown"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Last Error sensor
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
class AILastErrorSensor(AIBaseSensor):
|
||||
"""Shows the last error message from the AI provider."""
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
entry: ConfigEntry,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
super().__init__(coordinator, entry, description)
|
||||
self._update_state_and_attributes() # Initial update
|
||||
|
||||
def _update_state_and_attributes(self) -> None:
|
||||
"""Update sensor state with the last error message."""
|
||||
data = self.coordinator.data or {}
|
||||
last_error = data.get("last_error")
|
||||
self._attr_native_value = str(last_error) if last_error else "No Error"
|
||||
self._attr_extra_state_attributes = {
|
||||
"last_error_timestamp": data.get("last_update") if last_error else None,
|
||||
}
|
||||
|
||||
|
||||
class AIHistoryCountSensor(AIBaseSensor):
|
||||
"""Shows how many suggestions are retained in history."""
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
entry: ConfigEntry,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
super().__init__(coordinator, entry, description)
|
||||
self._update_state_and_attributes()
|
||||
|
||||
def _update_state_and_attributes(self) -> None:
|
||||
"""Update state from coordinator history data."""
|
||||
data = self.coordinator.data or {}
|
||||
self._attr_native_value = data.get("suggestion_count", 0)
|
||||
self._attr_extra_state_attributes = {
|
||||
"latest_suggestion_id": (data.get("suggestion") or {}).get("id"),
|
||||
"latest_suggestion_status": (data.get("suggestion") or {}).get("status"),
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
generate_suggestions:
|
||||
name: Generate Suggestions
|
||||
description: "Manually trigger AI automation suggestions."
|
||||
fields:
|
||||
provider_config:
|
||||
name: Provider Configuration
|
||||
description: Which provider configuration to use (if you have multiple configured)
|
||||
required: false
|
||||
selector:
|
||||
config_entry:
|
||||
integration: ai_automation_suggester
|
||||
custom_prompt:
|
||||
name: Custom Prompt
|
||||
description: Optional custom prompt to override or enhance the default system prompt.
|
||||
required: false
|
||||
example: "Focus on energy-saving automations"
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
all_entities:
|
||||
name: Consider All Entities
|
||||
description: "If true, consider all entities instead of just new entities."
|
||||
required: false
|
||||
default: false
|
||||
example: false
|
||||
selector:
|
||||
boolean: {}
|
||||
domains:
|
||||
name: Domains
|
||||
description: "List of domains to consider. If empty, consider all domains."
|
||||
required: false
|
||||
default: []
|
||||
selector:
|
||||
select:
|
||||
multiple: true
|
||||
custom_value: true
|
||||
options:
|
||||
- light
|
||||
- switch
|
||||
- sensor
|
||||
- binary_sensor
|
||||
- climate
|
||||
- cover
|
||||
- media_player
|
||||
- lock
|
||||
- alarm_control_panel
|
||||
exclude_domains:
|
||||
name: Exclude Domains
|
||||
description: "Domains to exclude from analysis, even when all entities are considered."
|
||||
required: false
|
||||
default: []
|
||||
selector:
|
||||
select:
|
||||
multiple: true
|
||||
custom_value: true
|
||||
options:
|
||||
- automation
|
||||
- update
|
||||
- button
|
||||
- event
|
||||
exclude_entities:
|
||||
name: Exclude Entities
|
||||
description: "Specific entity IDs to exclude from analysis."
|
||||
required: false
|
||||
default: []
|
||||
selector:
|
||||
entity:
|
||||
multiple: true
|
||||
exclude_areas:
|
||||
name: Exclude Areas
|
||||
description: "Area IDs or area names to exclude from analysis."
|
||||
required: false
|
||||
default: []
|
||||
selector:
|
||||
area:
|
||||
multiple: true
|
||||
entity_limit:
|
||||
name: Entity Limit
|
||||
description: "Maximum number of entities to consider (randomly selected from the chosen domains)."
|
||||
required: false
|
||||
default: 200
|
||||
example: 200
|
||||
selector:
|
||||
number:
|
||||
min: 50
|
||||
max: 1000
|
||||
mode: slider
|
||||
automation_read_yaml:
|
||||
name: Read 'automations.yaml' file.
|
||||
description: "Reads and appends the yaml code of the automations found in the automations.yaml file. This action will use a lot of input tokens, use it with care and with models with a large input window (e.g. Gemini)."
|
||||
required: false
|
||||
default: false
|
||||
example: false
|
||||
selector:
|
||||
boolean: {}
|
||||
automation_limit:
|
||||
name: Automation Limit
|
||||
description: "Maximum number of automations to analyze (default: 100)."
|
||||
required: false
|
||||
default: 100
|
||||
example: 100
|
||||
selector:
|
||||
number:
|
||||
min: 10
|
||||
max: 500
|
||||
mode: slider
|
||||
|
||||
clear_history:
|
||||
name: Clear Suggestion History
|
||||
description: "Clear all stored AI automation suggestions."
|
||||
|
||||
update_suggestion:
|
||||
name: Update Suggestion
|
||||
description: "Update the review status for a stored AI automation suggestion."
|
||||
fields:
|
||||
suggestion_id:
|
||||
name: Suggestion ID
|
||||
description: "The suggestion ID to update."
|
||||
required: true
|
||||
selector:
|
||||
text: {}
|
||||
status:
|
||||
name: Status
|
||||
description: "New review status for the suggestion."
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- new
|
||||
- accepted
|
||||
- declined
|
||||
- dismissed
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Persistent suggestion history storage."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.storage import Store
|
||||
|
||||
from .const import DEFAULT_HISTORY_RETENTION, DOMAIN
|
||||
|
||||
STORE_VERSION = 1
|
||||
STORE_KEY = f"{DOMAIN}.suggestions"
|
||||
STORE_DATA_KEY = "suggestions"
|
||||
HASS_STORE_KEY = "_suggestion_store"
|
||||
|
||||
|
||||
class SuggestionStore:
|
||||
"""Small wrapper around Home Assistant storage helper."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
self._store: Store[dict[str, Any]] = Store(hass, STORE_VERSION, STORE_KEY)
|
||||
self._data: dict[str, Any] | None = None
|
||||
|
||||
async def _async_load(self) -> dict[str, Any]:
|
||||
if self._data is None:
|
||||
self._data = await self._store.async_load() or {STORE_DATA_KEY: []}
|
||||
self._data.setdefault(STORE_DATA_KEY, [])
|
||||
return self._data
|
||||
|
||||
async def async_list(self) -> list[dict[str, Any]]:
|
||||
"""Return stored suggestions, newest first."""
|
||||
|
||||
data = await self._async_load()
|
||||
return list(data.get(STORE_DATA_KEY, []))
|
||||
|
||||
async def async_add_suggestions(
|
||||
self,
|
||||
suggestions: list[dict[str, Any]],
|
||||
retention: int = DEFAULT_HISTORY_RETENTION,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Persist suggestions and return the new stored list."""
|
||||
|
||||
data = await self._async_load()
|
||||
current = list(data.get(STORE_DATA_KEY, []))
|
||||
data[STORE_DATA_KEY] = suggestions + current
|
||||
if retention > 0:
|
||||
data[STORE_DATA_KEY] = data[STORE_DATA_KEY][:retention]
|
||||
await self._store.async_save(data)
|
||||
return list(data[STORE_DATA_KEY])
|
||||
|
||||
async def async_update_status(self, suggestion_id: str, status: str) -> dict[str, Any] | None:
|
||||
"""Update a suggestion status such as accepted, declined, or dismissed."""
|
||||
|
||||
data = await self._async_load()
|
||||
for suggestion in data.get(STORE_DATA_KEY, []):
|
||||
if suggestion.get("id") == suggestion_id:
|
||||
suggestion["status"] = status
|
||||
await self._store.async_save(data)
|
||||
return suggestion
|
||||
return None
|
||||
|
||||
async def async_clear(self) -> None:
|
||||
"""Clear all stored suggestions."""
|
||||
|
||||
data = await self._async_load()
|
||||
data[STORE_DATA_KEY] = []
|
||||
await self._store.async_save(data)
|
||||
|
||||
|
||||
def async_get_suggestion_store(hass: HomeAssistant) -> SuggestionStore:
|
||||
"""Return the singleton suggestion store for this Home Assistant instance."""
|
||||
|
||||
domain_data = hass.data.setdefault(DOMAIN, {})
|
||||
store = domain_data.get(HASS_STORE_KEY)
|
||||
if store is None:
|
||||
store = SuggestionStore(hass)
|
||||
domain_data[HASS_STORE_KEY] = store
|
||||
return store
|
||||
@@ -0,0 +1,308 @@
|
||||
{
|
||||
"title": "AI Automation Suggester",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Select AI Provider",
|
||||
"data": {
|
||||
"provider": "AI Provider"
|
||||
}
|
||||
},
|
||||
"openai": {
|
||||
"title": "Configure OpenAI",
|
||||
"data": {
|
||||
"openai_api_key": "OpenAI API Key",
|
||||
"openai_model": "OpenAI Model",
|
||||
"openai_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"anthropic": {
|
||||
"title": "Configure Anthropic",
|
||||
"data": {
|
||||
"anthropic_api_key": "Anthropic API Key",
|
||||
"anthropic_model": "Anthropic Model",
|
||||
"anthropic_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"google": {
|
||||
"title": "Configure Google",
|
||||
"data": {
|
||||
"google_api_key": "Google API Key",
|
||||
"google_model": "Google Model",
|
||||
"google_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"groq": {
|
||||
"title": "Configure Groq",
|
||||
"data": {
|
||||
"groq_api_key": "Groq API Key",
|
||||
"groq_model": "Groq Model",
|
||||
"groq_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"localai": {
|
||||
"title": "Configure LocalAI",
|
||||
"data": {
|
||||
"localai_ip": "LocalAI IP Address",
|
||||
"localai_port": "LocalAI Port",
|
||||
"localai_https": "Use HTTPS for LocalAI",
|
||||
"localai_model": "LocalAI Model",
|
||||
"localai_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Configure Ollama",
|
||||
"data": {
|
||||
"ollama_base_url": "Ollama/Open WebUI Base URL",
|
||||
"ollama_api_key": "Ollama/Open WebUI API Key (optional)",
|
||||
"ollama_ip": "Ollama IP Address",
|
||||
"ollama_port": "Ollama Port",
|
||||
"ollama_https": "Use HTTPS for Ollama",
|
||||
"ollama_model": "Ollama Model",
|
||||
"ollama_temperature": "Temperature",
|
||||
"ollama_disable_think": "Disable Think Mode (Ollama)",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"custom_openai": {
|
||||
"title": "Configure Custom OpenAI",
|
||||
"data": {
|
||||
"custom_openai_endpoint": "Custom OpenAI Endpoint",
|
||||
"custom_openai_api_key": "Custom OpenAI API Key (Optional)",
|
||||
"custom_openai_model": "Custom OpenAI Model",
|
||||
"custom_openai_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"mistral": {
|
||||
"title": "Configure Mistral AI",
|
||||
"data": {
|
||||
"mistral_api_key": "Mistral API Key",
|
||||
"mistral_model": "Mistral Model",
|
||||
"mistral_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"perplexity": {
|
||||
"title": "Configure Perplexity AI",
|
||||
"data": {
|
||||
"perplexity_api_key": "Perplexity API Key",
|
||||
"perplexity_model": "Perplexity Model",
|
||||
"perplexity_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"openrouter": {
|
||||
"title": "Configure OpenRouter",
|
||||
"data": {
|
||||
"openrouter_api_key": "OpenRouter API Key",
|
||||
"openrouter_model": "OpenRouter Model",
|
||||
"openrouter_reasoning_max_tokens": "OpenRouter Reasoning Max Tokens",
|
||||
"openrouter_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"openai_azure": {
|
||||
"title": "Configure OpenAI Azure",
|
||||
"data": {
|
||||
"openai_azure_api_key": "Azure OpenAI API Key",
|
||||
"openai_azure_deployment_id": "Azure Deployment ID",
|
||||
"openai_azure_endpoint": "Azure Endpoint",
|
||||
"openai_azure_api_version": "Azure API Version",
|
||||
"openai_azure_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"generic_openai": {
|
||||
"title": "Configure Generic OpenAI",
|
||||
"data": {
|
||||
"generic_openai_api_endpoint": "Generic OpenAI API Full URL (should end with 'completions')",
|
||||
"generic_openai_api_key": "Generic OpenAI API Key (Optional)",
|
||||
"generic_openai_model": "Generic OpenAI Model",
|
||||
"generic_openai_temperature": "Temperature",
|
||||
"generic_openai_validation_endpoint": "Validation URL (should end with 'models')",
|
||||
"generic_openai_enable_validation": "Enable Validation",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"litellm": {
|
||||
"title": "Configure LiteLLM",
|
||||
"data": {
|
||||
"litellm_model": "LiteLLM Model (e.g. openai/gpt-4o, anthropic/claude-sonnet-4-6, groq/llama-3.3-70b-versatile)",
|
||||
"litellm_api_key": "API Key (optional if set via environment variable)",
|
||||
"litellm_api_base": "API Base URL (optional, for LiteLLM proxy)",
|
||||
"litellm_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"api_error": "API validation failed: {error_message}",
|
||||
"cannot_connect": "Failed to connect. Please check your settings and network.",
|
||||
"unknown": "An unknown error occurred. Please check logs for details."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This AI provider is already configured. You can edit it from the integrations page."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "AI Automation Suggester Options",
|
||||
"data": {
|
||||
"openai_api_key": "OpenAI API Key",
|
||||
"openai_model": "OpenAI Model",
|
||||
"openai_temperature": "Temperature (OpenAI)",
|
||||
"anthropic_api_key": "Anthropic API Key",
|
||||
"anthropic_model": "Anthropic Model",
|
||||
"anthropic_temperature": "Temperature (Anthropic)",
|
||||
"google_api_key": "Google API Key",
|
||||
"google_model": "Google Model",
|
||||
"google_temperature": "Temperature (Google)",
|
||||
"groq_api_key": "Groq API Key",
|
||||
"groq_model": "Groq Model",
|
||||
"groq_temperature": "Temperature (Groq)",
|
||||
"localai_ip": "LocalAI IP Address",
|
||||
"localai_port": "LocalAI Port",
|
||||
"localai_https": "Use HTTPS for LocalAI",
|
||||
"localai_model": "LocalAI Model",
|
||||
"localai_temperature": "Temperature (LocalAI)",
|
||||
"ollama_base_url": "Ollama/Open WebUI Base URL",
|
||||
"ollama_api_key": "Ollama/Open WebUI API Key (optional)",
|
||||
"ollama_ip": "Ollama IP Address",
|
||||
"ollama_port": "Ollama Port",
|
||||
"ollama_https": "Use HTTPS for Ollama",
|
||||
"ollama_model": "Ollama Model",
|
||||
"ollama_temperature": "Temperature (Ollama)",
|
||||
"ollama_disable_think": "Disable Think Mode (Ollama)",
|
||||
"custom_openai_endpoint": "Custom OpenAI Endpoint",
|
||||
"custom_openai_api_key": "Custom OpenAI API Key",
|
||||
"custom_openai_model": "Custom OpenAI Model",
|
||||
"custom_openai_temperature": "Temperature (Custom OpenAI)",
|
||||
"mistral_api_key": "Mistral API Key",
|
||||
"mistral_model": "Mistral Model",
|
||||
"mistral_temperature": "Temperature (Mistral AI)",
|
||||
"perplexity_api_key": "Perplexity API Key",
|
||||
"perplexity_model": "Perplexity Model",
|
||||
"perplexity_temperature": "Temperature (Perplexity AI)",
|
||||
"openrouter_api_key": "OpenRouter API Key",
|
||||
"openrouter_model": "OpenRouter Model",
|
||||
"openrouter_reasoning_max_tokens": "OpenRouter Reasoning Max Tokens",
|
||||
"openrouter_temperature": "Temperature (OpenRouter)",
|
||||
"openai_azure_api_key": "Azure OpenAI API Key",
|
||||
"openai_azure_deployment_id": "Azure Deployment ID",
|
||||
"openai_azure_endpoint": "Azure Endpoint",
|
||||
"openai_azure_api_version": "Azure API Version",
|
||||
"openai_azure_temperature": "Temperature (OpenAI Azure)",
|
||||
"generic_openai_api_endpoint": "Generic OpenAI API Full URL (should end with 'completions')",
|
||||
"generic_openai_api_key": "Generic OpenAI API Key",
|
||||
"generic_openai_model": "Generic OpenAI Model",
|
||||
"generic_openai_temperature": "Temperature (Generic OpenAI)",
|
||||
"generic_openai_validation_endpoint": "Validation URL (Generic OpenAI, should end with 'models')",
|
||||
"generic_openai_enable_validation": "Enable Validation (Generic OpenAI)",
|
||||
"litellm_api_key": "LiteLLM API Key",
|
||||
"litellm_model": "LiteLLM Model",
|
||||
"litellm_api_base": "LiteLLM API Base URL",
|
||||
"litellm_temperature": "Temperature (LiteLLM)",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens",
|
||||
"custom_system_prompt": "Persistent Custom System Prompt",
|
||||
"excluded_domains": "Excluded Domains",
|
||||
"excluded_entities": "Excluded Entities",
|
||||
"excluded_areas": "Excluded Areas",
|
||||
"history_retention": "Suggestion History Retention",
|
||||
"request_timeout": "Provider Request Timeout",
|
||||
"openai_reasoning_effort": "OpenAI Reasoning Effort"
|
||||
},
|
||||
"description": "Adjust settings for your AI providers. Fields relevant to your configured provider will be used. Common settings like token limits are configured per provider."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_input": "One or more values are invalid."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"generate_suggestions": {
|
||||
"name": "Generate Suggestions",
|
||||
"description": "Manually trigger AI automation suggestions.",
|
||||
"fields": {
|
||||
"provider_config": {
|
||||
"name": "Provider Configuration",
|
||||
"description": "Which provider configuration to use (if you have multiple)"
|
||||
},
|
||||
"custom_prompt": {
|
||||
"name": "Custom Prompt",
|
||||
"description": "Optional custom prompt to override the default system prompt or guide the suggestions towards specific themes"
|
||||
},
|
||||
"all_entities": {
|
||||
"name": "Consider All Entities",
|
||||
"description": "If true, consider all entities instead of just new entities."
|
||||
},
|
||||
"domains": {
|
||||
"name": "Domains",
|
||||
"description": "List of domains to consider. If empty, consider all domains."
|
||||
},
|
||||
"entity_limit": {
|
||||
"name": "Entity Limit",
|
||||
"description": "Maximum number of entities to consider (randomly selected)."
|
||||
},
|
||||
"automation_read_yaml": {
|
||||
"name": "Read 'automations.yaml' file",
|
||||
"description": "Reads and appends the YAML code of the automations found in the 'automations.yaml' file. This action will use a lot of input tokens, use it with care and with models with a large context window (e.g. Gemini)."
|
||||
},
|
||||
"automation_limit": {
|
||||
"name": "Automation Limit",
|
||||
"description": "Maximum number of automations to analyze (default: 100)."
|
||||
},
|
||||
"exclude_domains": {
|
||||
"name": "Exclude Domains",
|
||||
"description": "Domains to exclude from analysis."
|
||||
},
|
||||
"exclude_entities": {
|
||||
"name": "Exclude Entities",
|
||||
"description": "Entity IDs to exclude from analysis."
|
||||
},
|
||||
"exclude_areas": {
|
||||
"name": "Exclude Areas",
|
||||
"description": "Areas to exclude from analysis."
|
||||
}
|
||||
}
|
||||
},
|
||||
"clear_history": {
|
||||
"name": "Clear Suggestion History",
|
||||
"description": "Clear all stored AI automation suggestions."
|
||||
},
|
||||
"update_suggestion": {
|
||||
"name": "Update Suggestion",
|
||||
"description": "Update the review status for a stored AI automation suggestion.",
|
||||
"fields": {
|
||||
"suggestion_id": {
|
||||
"name": "Suggestion ID",
|
||||
"description": "The suggestion ID to update."
|
||||
},
|
||||
"status": {
|
||||
"name": "Status",
|
||||
"description": "The new review status."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
"""Suggestion parsing and formatting helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import textwrap
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
import yaml
|
||||
|
||||
YAML_RE = re.compile(r"```(?:yaml|yml)\s*([\s\S]+?)\s*```", flags=re.IGNORECASE)
|
||||
JSON_RE = re.compile(r"```json\s*([\s\S]+?)\s*```", flags=re.IGNORECASE)
|
||||
STRING_FIELDS_AFTER_YAML = "entities_used|automation_ids_used|confidence|warnings"
|
||||
PARSE_REPAIR_WARNING = "The provider returned malformed JSON; suggestions were parsed best-effort."
|
||||
|
||||
|
||||
STRUCTURED_OUTPUT_INSTRUCTIONS = """
|
||||
Return a JSON object with this shape and no surrounding Markdown:
|
||||
{
|
||||
"suggestions": [
|
||||
{
|
||||
"title": "Short automation title",
|
||||
"description": "Why this automation is useful and how it works",
|
||||
"yaml": "Home Assistant automation YAML",
|
||||
"entities_used": ["domain.entity_id"],
|
||||
"automation_ids_used": [],
|
||||
"confidence": 0.0,
|
||||
"warnings": []
|
||||
}
|
||||
]
|
||||
}
|
||||
Only reference entity_ids present in the prompt. Keep suggestions review-only;
|
||||
do not claim that automations have been created or changed.
|
||||
"""
|
||||
|
||||
|
||||
def _as_list(value: Any) -> list:
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
return [value]
|
||||
|
||||
|
||||
def _try_json_loads(raw_response: str) -> dict | list | None:
|
||||
"""Try to decode model output as JSON, including fenced JSON."""
|
||||
|
||||
text = raw_response.strip()
|
||||
fenced = JSON_RE.search(text)
|
||||
if fenced:
|
||||
text = fenced.group(1).strip()
|
||||
|
||||
try:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
start = text.find("{")
|
||||
end = text.rfind("}")
|
||||
if start != -1 and end != -1 and end > start:
|
||||
try:
|
||||
return json.loads(text[start : end + 1])
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _decode_jsonish_string(value: str) -> str:
|
||||
"""Decode a JSON string fragment when possible."""
|
||||
|
||||
try:
|
||||
return str(json.loads(f'"{value}"'))
|
||||
except json.JSONDecodeError:
|
||||
return value.replace('\\"', '"').replace("\\n", "\n").strip()
|
||||
|
||||
|
||||
def _extract_string_field(segment: str, field: str) -> str | None:
|
||||
match = re.search(rf'"{re.escape(field)}"\s*:\s*"((?:\\.|[^"\\])*)"', segment)
|
||||
return _decode_jsonish_string(match.group(1)).strip() if match else None
|
||||
|
||||
|
||||
def _extract_array_field(segment: str, field: str) -> list:
|
||||
match = re.search(rf'"{re.escape(field)}"\s*:\s*(\[[\s\S]*?\])', segment)
|
||||
if not match:
|
||||
return []
|
||||
try:
|
||||
value = json.loads(match.group(1))
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
return value if isinstance(value, list) else []
|
||||
|
||||
|
||||
def _extract_number_field(segment: str, field: str) -> float | None:
|
||||
match = re.search(rf'"{re.escape(field)}"\s*:\s*(-?\d+(?:\.\d+)?)', segment)
|
||||
return float(match.group(1)) if match else None
|
||||
|
||||
|
||||
def _extract_yaml_field(segment: str) -> str | None:
|
||||
malformed = re.search(
|
||||
rf'"yaml"\s*:\s*""\s*\r?\n(?P<yaml>[\s\S]*?)\r?\n\s*""\s*,?\s*(?=\r?\n\s*"(?:{STRING_FIELDS_AFTER_YAML})"|\r?\n\s*\}})',
|
||||
segment,
|
||||
)
|
||||
if malformed:
|
||||
return textwrap.dedent(malformed.group("yaml")).strip() or None
|
||||
|
||||
valid = re.search(r'"yaml"\s*:\s*"((?:\\.|[^"\\])*)"', segment)
|
||||
if valid:
|
||||
return _decode_jsonish_string(valid.group(1)).strip() or None
|
||||
return None
|
||||
|
||||
|
||||
def _try_loose_structured_items(raw_response: str) -> list[dict[str, Any]]:
|
||||
"""Extract suggestions from malformed JSON-like provider responses."""
|
||||
|
||||
if '"suggestions"' not in raw_response and '"title"' not in raw_response:
|
||||
return []
|
||||
|
||||
title_matches = list(re.finditer(r'"title"\s*:', raw_response))
|
||||
items: list[dict[str, Any]] = []
|
||||
for index, match in enumerate(title_matches):
|
||||
start = raw_response.rfind("{", 0, match.start())
|
||||
if start == -1:
|
||||
start = match.start()
|
||||
end = title_matches[index + 1].start() if index + 1 < len(title_matches) else len(raw_response)
|
||||
segment = raw_response[start:end]
|
||||
|
||||
title = _extract_string_field(segment, "title")
|
||||
description = _extract_string_field(segment, "description")
|
||||
yaml_code = _extract_yaml_field(segment)
|
||||
if not any((title, description, yaml_code)):
|
||||
continue
|
||||
|
||||
item: dict[str, Any] = {
|
||||
"title": title,
|
||||
"description": description,
|
||||
"yaml": yaml_code,
|
||||
"entities_used": _extract_array_field(segment, "entities_used"),
|
||||
"automation_ids_used": _extract_array_field(segment, "automation_ids_used"),
|
||||
"warnings": _extract_array_field(segment, "warnings"),
|
||||
}
|
||||
confidence = _extract_number_field(segment, "confidence")
|
||||
if confidence is not None:
|
||||
item["confidence"] = confidence
|
||||
items.append(item)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def _validate_yaml(yaml_code: str | None) -> list[str]:
|
||||
warnings: list[str] = []
|
||||
if not yaml_code:
|
||||
warnings.append("No automation YAML was returned.")
|
||||
return warnings
|
||||
try:
|
||||
parsed = yaml.safe_load(yaml_code)
|
||||
except yaml.YAMLError as err:
|
||||
warnings.append(f"Returned YAML could not be parsed: {err}")
|
||||
return warnings
|
||||
if parsed is None:
|
||||
warnings.append("Returned YAML was empty after parsing.")
|
||||
elif not isinstance(parsed, (dict, list)):
|
||||
warnings.append("Returned YAML parsed to an unexpected scalar value.")
|
||||
return warnings
|
||||
|
||||
|
||||
def _normalise_suggestion(
|
||||
item: dict[str, Any],
|
||||
*,
|
||||
provider: str,
|
||||
model: str,
|
||||
created_at: datetime,
|
||||
entities_processed: list[str],
|
||||
inherited_warnings: list[str],
|
||||
response_metadata: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
title = str(item.get("title") or "AI automation suggestion").strip()
|
||||
description = str(item.get("description") or item.get("shortDescription") or "").strip()
|
||||
yaml_code = item.get("yaml") or item.get("yaml_block") or item.get("yamlCode")
|
||||
yaml_code = str(yaml_code).strip() if yaml_code else None
|
||||
warnings = [str(w) for w in inherited_warnings]
|
||||
warnings.extend(str(w) for w in _as_list(item.get("warnings")))
|
||||
warnings.extend(_validate_yaml(yaml_code))
|
||||
|
||||
finish_reason = response_metadata.get("finish_reason")
|
||||
if finish_reason in {"length", "max_tokens"}:
|
||||
warnings.append("The provider reported a length finish reason; the suggestion may be truncated.")
|
||||
if response_metadata.get("status") == "incomplete":
|
||||
warnings.append("The provider returned an incomplete response.")
|
||||
|
||||
suggestion_id = str(item.get("id") or uuid4())
|
||||
return {
|
||||
"id": suggestion_id,
|
||||
"title": title,
|
||||
"shortDescription": description[:180] if description else title,
|
||||
"detailedDescription": description,
|
||||
"description": description,
|
||||
"yamlCode": yaml_code,
|
||||
"yaml_block": yaml_code,
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"status": str(item.get("status") or "new"),
|
||||
"created_at": created_at.isoformat(),
|
||||
"entities_used": [str(e) for e in _as_list(item.get("entities_used"))],
|
||||
"automation_ids_used": [str(a) for a in _as_list(item.get("automation_ids_used"))],
|
||||
"entities_processed": entities_processed,
|
||||
"confidence": item.get("confidence"),
|
||||
"warnings": warnings,
|
||||
"response_metadata": response_metadata,
|
||||
}
|
||||
|
||||
|
||||
def parse_suggestion_response(
|
||||
raw_response: str,
|
||||
*,
|
||||
provider: str,
|
||||
model: str,
|
||||
created_at: datetime,
|
||||
entities_processed: list[str],
|
||||
inherited_warnings: list[str] | None = None,
|
||||
response_metadata: dict[str, Any] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Parse a provider response into stored suggestion dictionaries."""
|
||||
|
||||
inherited = inherited_warnings or []
|
||||
metadata = response_metadata or {}
|
||||
structured = _try_json_loads(raw_response)
|
||||
if isinstance(structured, dict):
|
||||
raw_items = structured.get("suggestions")
|
||||
if raw_items is None:
|
||||
raw_items = [structured]
|
||||
elif isinstance(raw_items, dict):
|
||||
raw_items = [raw_items]
|
||||
elif not isinstance(raw_items, list):
|
||||
raw_items = []
|
||||
suggestions = [
|
||||
_normalise_suggestion(
|
||||
item if isinstance(item, dict) else {"description": str(item)},
|
||||
provider=provider,
|
||||
model=model,
|
||||
created_at=created_at,
|
||||
entities_processed=entities_processed,
|
||||
inherited_warnings=inherited,
|
||||
response_metadata=metadata,
|
||||
)
|
||||
for item in raw_items
|
||||
]
|
||||
if suggestions:
|
||||
return suggestions
|
||||
elif isinstance(structured, list):
|
||||
suggestions = [
|
||||
_normalise_suggestion(
|
||||
item if isinstance(item, dict) else {"description": str(item)},
|
||||
provider=provider,
|
||||
model=model,
|
||||
created_at=created_at,
|
||||
entities_processed=entities_processed,
|
||||
inherited_warnings=inherited,
|
||||
response_metadata=metadata,
|
||||
)
|
||||
for item in structured
|
||||
]
|
||||
if suggestions:
|
||||
return suggestions
|
||||
|
||||
loose_items = _try_loose_structured_items(raw_response)
|
||||
if loose_items:
|
||||
loose_warnings = [*inherited, PARSE_REPAIR_WARNING]
|
||||
return [
|
||||
_normalise_suggestion(
|
||||
item,
|
||||
provider=provider,
|
||||
model=model,
|
||||
created_at=created_at,
|
||||
entities_processed=entities_processed,
|
||||
inherited_warnings=loose_warnings,
|
||||
response_metadata=metadata,
|
||||
)
|
||||
for item in loose_items
|
||||
]
|
||||
|
||||
yaml_match = YAML_RE.search(raw_response)
|
||||
yaml_code = yaml_match.group(1).strip() if yaml_match else None
|
||||
if yaml_match:
|
||||
description = YAML_RE.sub("", raw_response).strip()
|
||||
elif raw_response.lstrip().startswith(("{", "[")) or '"suggestions"' in raw_response:
|
||||
description = "The provider returned structured output that could not be parsed. Try regenerating with a lower entity limit or a newer model."
|
||||
else:
|
||||
description = raw_response.strip()
|
||||
return [
|
||||
_normalise_suggestion(
|
||||
{
|
||||
"title": "AI automation suggestion",
|
||||
"description": description,
|
||||
"yaml": yaml_code,
|
||||
},
|
||||
provider=provider,
|
||||
model=model,
|
||||
created_at=created_at,
|
||||
entities_processed=entities_processed,
|
||||
inherited_warnings=inherited,
|
||||
response_metadata=metadata,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def format_suggestion_notification(suggestion: dict[str, Any]) -> str:
|
||||
"""Render one suggestion for a Home Assistant notification."""
|
||||
|
||||
parts = [f"## {suggestion.get('title', 'AI automation suggestion')}"]
|
||||
description = suggestion.get("description")
|
||||
if description:
|
||||
parts.append(str(description))
|
||||
yaml_code = suggestion.get("yamlCode") or suggestion.get("yaml_block")
|
||||
if yaml_code:
|
||||
parts.append("```yaml\n" + str(yaml_code).strip() + "\n```")
|
||||
warnings = suggestion.get("warnings") or []
|
||||
if warnings:
|
||||
parts.append("Warnings:\n" + "\n".join(f"- {_format_notification_warning(warning)}" for warning in warnings))
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
def _format_notification_warning(warning: Any) -> str:
|
||||
"""Return a concise user-facing warning for notifications."""
|
||||
|
||||
text = str(warning)
|
||||
if text == PARSE_REPAIR_WARNING:
|
||||
return "Provider response needed formatting repair before display. Review the YAML before using it."
|
||||
if text == "The provider reported a length finish reason; the suggestion may be truncated.":
|
||||
return "The AI response may have been cut off. Review the YAML before using it."
|
||||
return text
|
||||
@@ -0,0 +1,252 @@
|
||||
{
|
||||
"title": "AI Automation Suggester",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Seleccionar Proveïdor d'IA",
|
||||
"data": {
|
||||
"provider": "Proveïdor d'IA"
|
||||
}
|
||||
},
|
||||
"openai": {
|
||||
"title": "Configurar OpenAI",
|
||||
"data": {
|
||||
"openai_api_key": "Clau API d'OpenAI",
|
||||
"openai_model": "Model d'OpenAI",
|
||||
"openai_temperature": "Temperatura",
|
||||
"max_input_tokens": "Màxim de tokens d'entrada",
|
||||
"max_output_tokens": "Màxim de tokens de sortida"
|
||||
}
|
||||
},
|
||||
"anthropic": {
|
||||
"title": "Configurar Anthropic",
|
||||
"data": {
|
||||
"anthropic_api_key": "Clau API d'Anthropic",
|
||||
"anthropic_model": "Model d'Anthropic",
|
||||
"anthropic_temperature": "Temperatura",
|
||||
"max_input_tokens": "Màxim de tokens d'entrada",
|
||||
"max_output_tokens": "Màxim de tokens de sortida"
|
||||
}
|
||||
},
|
||||
"google": {
|
||||
"title": "Configurar Google",
|
||||
"data": {
|
||||
"google_api_key": "Clau API de Google",
|
||||
"google_model": "Model de Google",
|
||||
"google_temperature": "Temperatura",
|
||||
"max_input_tokens": "Màxim de tokens d'entrada",
|
||||
"max_output_tokens": "Màxim de tokens de sortida"
|
||||
}
|
||||
},
|
||||
"groq": {
|
||||
"title": "Configurar Groq",
|
||||
"data": {
|
||||
"groq_api_key": "Clau API de Groq",
|
||||
"groq_model": "Model de Groq",
|
||||
"groq_temperature": "Temperatura",
|
||||
"max_input_tokens": "Màxim de tokens d'entrada",
|
||||
"max_output_tokens": "Màxim de tokens de sortida"
|
||||
}
|
||||
},
|
||||
"localai": {
|
||||
"title": "Configurar LocalAI",
|
||||
"data": {
|
||||
"localai_ip": "Adreça IP de LocalAI",
|
||||
"localai_port": "Port de LocalAI",
|
||||
"localai_https": "Utilitzar HTTPS per a LocalAI",
|
||||
"localai_model": "Model de LocalAI",
|
||||
"localai_temperature": "Temperatura",
|
||||
"max_input_tokens": "Màxim de tokens d'entrada",
|
||||
"max_output_tokens": "Màxim de tokens de sortida"
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Configurar Ollama",
|
||||
"data": {
|
||||
"ollama_ip": "Adreça IP d'Ollama",
|
||||
"ollama_port": "Port d'Ollama",
|
||||
"ollama_https": "Utilitzar HTTPS per a Ollama",
|
||||
"ollama_model": "Model d'Ollama",
|
||||
"ollama_temperature": "Temperatura",
|
||||
"ollama_disable_think": "Desactivar Mode de Pensament (Ollama)",
|
||||
"max_input_tokens": "Màxim de tokens d'entrada",
|
||||
"max_output_tokens": "Màxim de tokens de sortida"
|
||||
}
|
||||
},
|
||||
"custom_openai": {
|
||||
"title": "Configurar OpenAI Personalitzat",
|
||||
"data": {
|
||||
"custom_openai_endpoint": "URL de l'API d'OpenAI Personalitzada",
|
||||
"custom_openai_api_key": "Clau API d'OpenAI Personalitzada (Opcional)",
|
||||
"custom_openai_model": "Model d'OpenAI Personalitzat",
|
||||
"custom_openai_temperature": "Temperatura",
|
||||
"max_input_tokens": "Màxim de tokens d'entrada",
|
||||
"max_output_tokens": "Màxim de tokens de sortida"
|
||||
}
|
||||
},
|
||||
"mistral": {
|
||||
"title": "Configurar Mistral AI",
|
||||
"data": {
|
||||
"mistral_api_key": "Clau API de Mistral",
|
||||
"mistral_model": "Model de Mistral",
|
||||
"mistral_temperature": "Temperatura",
|
||||
"max_input_tokens": "Màxim de tokens d'entrada",
|
||||
"max_output_tokens": "Màxim de tokens de sortida"
|
||||
}
|
||||
},
|
||||
"perplexity": {
|
||||
"title": "Configurar Perplexity AI",
|
||||
"data": {
|
||||
"perplexity_api_key": "Clau API de Perplexity",
|
||||
"perplexity_model": "Model de Perplexity",
|
||||
"perplexity_temperature": "Temperatura",
|
||||
"max_input_tokens": "Màxim de tokens d'entrada",
|
||||
"max_output_tokens": "Màxim de tokens de sortida"
|
||||
}
|
||||
},
|
||||
"openrouter": {
|
||||
"title": "Configurar OpenRouter",
|
||||
"data": {
|
||||
"openrouter_api_key": "Clau API d'OpenRouter",
|
||||
"openrouter_model": "Model d'OpenRouter",
|
||||
"openrouter_reasoning_max_tokens": "Màxim de tokens de raonament d'OpenRouter",
|
||||
"openrouter_temperature": "Temperatura",
|
||||
"max_input_tokens": "Màxim de tokens d'entrada",
|
||||
"max_output_tokens": "Màxim de tokens de sortida"
|
||||
}
|
||||
},
|
||||
"openai_azure": {
|
||||
"title": "Configurar OpenAI Azure",
|
||||
"data": {
|
||||
"openai_azure_api_key": "Clau API d'Azure OpenAI",
|
||||
"openai_azure_deployment_id": "ID de Desplegament d'Azure",
|
||||
"openai_azure_endpoint": "URL de l'API d'Azure",
|
||||
"openai_azure_api_version": "Versió de l'API d'Azure",
|
||||
"openai_azure_temperature": "Temperatura",
|
||||
"max_input_tokens": "Màxim de tokens d'entrada",
|
||||
"max_output_tokens": "Màxim de tokens de sortida"
|
||||
}
|
||||
},
|
||||
"generic_openai": {
|
||||
"title": "Configurar OpenAI Genèric",
|
||||
"data": {
|
||||
"generic_openai_api_endpoint": "URL completa de l'API d'OpenAI genèrica (hauria d'acabar amb 'completions')",
|
||||
"generic_openai_api_key": "Clau API d'OpenAI Genèric (Opcional)",
|
||||
"generic_openai_model": "Model d'OpenAI Genèric",
|
||||
"generic_openai_temperature": "Temperatura",
|
||||
"generic_openai_validation_endpoint": "URL de validació (hauria d'acabar amb 'models')",
|
||||
"generic_openai_enable_validation": "Activar Validació",
|
||||
"max_input_tokens": "Màxim de tokens d'entrada",
|
||||
"max_output_tokens": "Màxim de tokens de sortida"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"api_error": "La validació de l'API ha fallat: {error_message}",
|
||||
"cannot_connect": "No s'ha pogut connectar. Revisa la teva configuració i la xarxa.",
|
||||
"unknown": "S'ha produït un error desconegut. Revisa els registres per a més detalls."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Aquest proveïdor d'IA ja està configurat. Pots editar-lo des de la pàgina d'integracions."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Opcions del Suggeridor d'Automatitzacions amb IA",
|
||||
"data": {
|
||||
"openai_api_key": "Clau API d'OpenAI",
|
||||
"openai_model": "Model d'OpenAI",
|
||||
"openai_temperature": "Temperatura (OpenAI)",
|
||||
"anthropic_api_key": "Clau API d'Anthropic",
|
||||
"anthropic_model": "Model d'Anthropic",
|
||||
"anthropic_temperature": "Temperatura (Anthropic)",
|
||||
"google_api_key": "Clau API de Google",
|
||||
"google_model": "Model de Google",
|
||||
"google_temperature": "Temperatura (Google)",
|
||||
"groq_api_key": "Clau API de Groq",
|
||||
"groq_model": "Model de Groq",
|
||||
"groq_temperature": "Temperatura (Groq)",
|
||||
"localai_ip": "Adreça IP de LocalAI",
|
||||
"localai_port": "Port de LocalAI",
|
||||
"localai_https": "Utilitzar HTTPS per a LocalAI",
|
||||
"localai_model": "Model de LocalAI",
|
||||
"localai_temperature": "Temperatura (LocalAI)",
|
||||
"ollama_ip": "Adreça IP d'Ollama",
|
||||
"ollama_port": "Port d'Ollama",
|
||||
"ollama_https": "Utilitzar HTTPS per a Ollama",
|
||||
"ollama_model": "Model d'Ollama",
|
||||
"ollama_temperature": "Temperatura (Ollama)",
|
||||
"ollama_disable_think": "Desactivar Mode de Pensament (Ollama)",
|
||||
"custom_openai_endpoint": "URL de l'API d'OpenAI Personalitzada",
|
||||
"custom_openai_api_key": "Clau API d'OpenAI Personalitzada",
|
||||
"custom_openai_model": "Model d'OpenAI Personalitzat",
|
||||
"custom_openai_temperature": "Temperatura (OpenAI Personalitzat)",
|
||||
"mistral_api_key": "Clau API de Mistral",
|
||||
"mistral_model": "Model de Mistral",
|
||||
"mistral_temperature": "Temperatura (Mistral AI)",
|
||||
"perplexity_api_key": "Clau API de Perplexity",
|
||||
"perplexity_model": "Model de Perplexity",
|
||||
"perplexity_temperature": "Temperatura (Perplexity AI)",
|
||||
"openrouter_api_key": "Clau API d'OpenRouter",
|
||||
"openrouter_model": "Model d'OpenRouter",
|
||||
"openrouter_reasoning_max_tokens": "Màxim de tokens de raonament d'OpenRouter",
|
||||
"openrouter_temperature": "Temperatura (OpenRouter)",
|
||||
"openai_azure_api_key": "Clau API d'Azure OpenAI",
|
||||
"openai_azure_deployment_id": "ID de Desplegament d'Azure",
|
||||
"openai_azure_endpoint": "URL de l'API d'Azure",
|
||||
"openai_azure_api_version": "Versió de l'API d'Azure",
|
||||
"openai_azure_temperature": "Temperatura (OpenAI Azure)",
|
||||
"generic_openai_api_endpoint": "URL completa de l'API d'OpenAI genèrica (hauria d'acabar amb 'completions')",
|
||||
"generic_openai_api_key": "Clau API d'OpenAI Genèric",
|
||||
"generic_openai_model": "Model d'OpenAI Genèric",
|
||||
"generic_openai_temperature": "Temperatura (OpenAI Genèric)",
|
||||
"generic_openai_validation_endpoint": "URL de validació (OpenAI Genèric, hauria d'acabar amb 'models')",
|
||||
"generic_openai_enable_validation": "Activar Validació (OpenAI Genèric)",
|
||||
"max_input_tokens": "Màxim de tokens d'entrada",
|
||||
"max_output_tokens": "Màxim de tokens de sortida"
|
||||
},
|
||||
"description": "Ajusta la configuració dels teus proveïdors d'IA. S'utilitzaran els camps rellevants per al proveïdor que hagis configurat. Els paràmetres comuns, com els límits de tokens, es configuren per a cada proveïdor."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_input": "Un o més valors són invàlids."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"generate_suggestions": {
|
||||
"name": "Generar Suggeriments",
|
||||
"description": "Activar manualment els suggeriments d'automatització amb IA.",
|
||||
"fields": {
|
||||
"provider_config": {
|
||||
"name": "Configuració del Proveïdor",
|
||||
"description": "Quina configuració de proveïdor utilitzar (si en tens diverses)?"
|
||||
},
|
||||
"custom_prompt": {
|
||||
"name": "Prompt Personalitzat",
|
||||
"description": "Prompt personalitzat opcional per anul·lar el prompt del sistema per defecte o guiar els suggeriments cap a temes específics."
|
||||
},
|
||||
"all_entities": {
|
||||
"name": "Considerar Totes les Entitats",
|
||||
"description": "Si és cert, considerar totes les entitats en lloc de només les noves."
|
||||
},
|
||||
"domains": {
|
||||
"name": "Dominis",
|
||||
"description": "Llista de dominis a considerar. Si està buit, es consideren tots els dominis."
|
||||
},
|
||||
"entity_limit": {
|
||||
"name": "Límit d'Entitats",
|
||||
"description": "Nombre màxim d'entitats a considerar (seleccionades aleatòriament)."
|
||||
},
|
||||
"automation_read_yaml": {
|
||||
"name": "Llegir el fitxer 'automations.yaml'",
|
||||
"description": "Llegeix i adjunta el codi YAML de les automatitzacions trobades al fitxer 'automations.yaml'. Aquesta acció consumirà molts tokens d'entrada, fes-la servir amb precaució i amb models que tinguin una finestra de context gran (p. ex., Gemini)."
|
||||
},
|
||||
"automation_limit": {
|
||||
"name": "Límit d'Automatitzacions",
|
||||
"description": "Nombre màxim d'automatitzacions a analitzar (per defecte: 100)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
{
|
||||
"title": "AI Návrhář automatizací",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Vyberte poskytovatele AI",
|
||||
"data": {
|
||||
"provider": "Poskytovatel AI"
|
||||
}
|
||||
},
|
||||
"openai": {
|
||||
"title": "Nakonfigurovat OpenAI",
|
||||
"data": {
|
||||
"openai_api_key": "OpenAI API klíč",
|
||||
"openai_model": "Model OpenAI",
|
||||
"openai_temperature": "Teplota",
|
||||
"max_input_tokens": "Maximální počet vstupních tokenů",
|
||||
"max_output_tokens": "Maximální počet výstupních tokenů"
|
||||
}
|
||||
},
|
||||
"anthropic": {
|
||||
"title": "Nakonfigurovat Anthropic",
|
||||
"data": {
|
||||
"anthropic_api_key": "Anthropic API klíč",
|
||||
"anthropic_model": "Model Anthropic",
|
||||
"anthropic_temperature": "Teplota",
|
||||
"max_input_tokens": "Maximální počet vstupních tokenů",
|
||||
"max_output_tokens": "Maximální počet výstupních tokenů"
|
||||
}
|
||||
},
|
||||
"google": {
|
||||
"title": "Nakonfigurovat Google",
|
||||
"data": {
|
||||
"google_api_key": "Google API klíč",
|
||||
"google_model": "Model Google",
|
||||
"google_temperature": "Teplota",
|
||||
"max_input_tokens": "Maximální počet vstupních tokenů",
|
||||
"max_output_tokens": "Maximální počet výstupních tokenů"
|
||||
}
|
||||
},
|
||||
"groq": {
|
||||
"title": "Nakonfigurovat Groq",
|
||||
"data": {
|
||||
"groq_api_key": "Groq API klíč",
|
||||
"groq_model": "Model Groq",
|
||||
"groq_temperature": "Teplota",
|
||||
"max_input_tokens": "Maximální počet vstupních tokenů",
|
||||
"max_output_tokens": "Maximální počet výstupních tokenů"
|
||||
}
|
||||
},
|
||||
"localai": {
|
||||
"title": "Nakonfigurovat LocalAI",
|
||||
"data": {
|
||||
"localai_ip": "IP adresa LocalAI",
|
||||
"localai_port": "Port LocalAI",
|
||||
"localai_https": "Použít HTTPS pro LocalAI",
|
||||
"localai_model": "Model LocalAI",
|
||||
"localai_temperature": "Teplota",
|
||||
"max_input_tokens": "Maximální počet vstupních tokenů",
|
||||
"max_output_tokens": "Maximální počet výstupních tokenů"
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Nakonfigurovat Ollama",
|
||||
"data": {
|
||||
"ollama_base_url": "Základní URL Ollama/Open WebUI",
|
||||
"ollama_api_key": "API klíč Ollama/Open WebUI (volitelné)",
|
||||
"ollama_ip": "IP adresa Ollama",
|
||||
"ollama_port": "Port Ollama",
|
||||
"ollama_https": "Použít HTTPS pro Ollama",
|
||||
"ollama_model": "Model Ollama",
|
||||
"ollama_temperature": "Teplota",
|
||||
"ollama_disable_think": "Zakázat režim přemýšlení (Ollama)",
|
||||
"max_input_tokens": "Maximální počet vstupních tokenů",
|
||||
"max_output_tokens": "Maximální počet výstupních tokenů"
|
||||
}
|
||||
},
|
||||
"custom_openai": {
|
||||
"title": "Nakonfigurovat vlastní OpenAI",
|
||||
"data": {
|
||||
"custom_openai_endpoint": "Koncový bod vlastního OpenAI",
|
||||
"custom_openai_api_key": "API klíč vlastního OpenAI (volitelné)",
|
||||
"custom_openai_model": "Model vlastního OpenAI",
|
||||
"custom_openai_temperature": "Teplota",
|
||||
"max_input_tokens": "Maximální počet vstupních tokenů",
|
||||
"max_output_tokens": "Maximální počet výstupních tokenů"
|
||||
}
|
||||
},
|
||||
"mistral": {
|
||||
"title": "Nakonfigurovat Mistral AI",
|
||||
"data": {
|
||||
"mistral_api_key": "Mistral API klíč",
|
||||
"mistral_model": "Model Mistral",
|
||||
"mistral_temperature": "Teplota",
|
||||
"max_input_tokens": "Maximální počet vstupních tokenů",
|
||||
"max_output_tokens": "Maximální počet výstupních tokenů"
|
||||
}
|
||||
},
|
||||
"perplexity": {
|
||||
"title": "Nakonfigurovat Perplexity AI",
|
||||
"data": {
|
||||
"perplexity_api_key": "Perplexity API klíč",
|
||||
"perplexity_model": "Model Perplexity",
|
||||
"perplexity_temperature": "Teplota",
|
||||
"max_input_tokens": "Maximální počet vstupních tokenů",
|
||||
"max_output_tokens": "Maximální počet výstupních tokenů"
|
||||
}
|
||||
},
|
||||
"openrouter": {
|
||||
"title": "Nakonfigurovat OpenRouter",
|
||||
"data": {
|
||||
"openrouter_api_key": "OpenRouter API klíč",
|
||||
"openrouter_model": "Model OpenRouter",
|
||||
"openrouter_reasoning_max_tokens": "Maximální počet tokenů pro uvažování OpenRouter",
|
||||
"openrouter_temperature": "Teplota",
|
||||
"max_input_tokens": "Maximální počet vstupních tokenů",
|
||||
"max_output_tokens": "Maximální počet výstupních tokenů"
|
||||
}
|
||||
},
|
||||
"openai_azure": {
|
||||
"title": "Nakonfigurovat Azure OpenAI",
|
||||
"data": {
|
||||
"openai_azure_api_key": "Azure OpenAI API klíč",
|
||||
"openai_azure_deployment_id": "ID nasazení Azure",
|
||||
"openai_azure_endpoint": "Koncový bod Azure",
|
||||
"openai_azure_api_version": "Verze Azure API",
|
||||
"openai_azure_temperature": "Teplota",
|
||||
"max_input_tokens": "Maximální počet vstupních tokenů",
|
||||
"max_output_tokens": "Maximální počet výstupních tokenů"
|
||||
}
|
||||
},
|
||||
"generic_openai": {
|
||||
"title": "Nakonfigurovat obecné OpenAI",
|
||||
"data": {
|
||||
"generic_openai_api_endpoint": "Úplná URL adresa API obecného OpenAI (musí končit na 'completions')",
|
||||
"generic_openai_api_key": "API klíč obecného OpenAI (volitelné)",
|
||||
"generic_openai_model": "Model obecného OpenAI",
|
||||
"generic_openai_temperature": "Teplota",
|
||||
"generic_openai_validation_endpoint": "Ověřovací URL (musí končit na 'models')",
|
||||
"generic_openai_enable_validation": "Povolit ověřování",
|
||||
"max_input_tokens": "Maximální počet vstupních tokenů",
|
||||
"max_output_tokens": "Maximální počet výstupních tokenů"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"api_error": "Ověření API se nezdařilo: {error_message}",
|
||||
"cannot_connect": "Nepodařilo se připojit. Zkontrolujte nastavení a síť.",
|
||||
"unknown": "Došlo k neznámé chybě. Podrobnosti najdete v logu."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Tento poskytovatel AI je již nakonfigurován. Můžete jej upravit na stránce integrací."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Nastavení AI Návrháře automatizací",
|
||||
"data": {
|
||||
"openai_api_key": "OpenAI API klíč",
|
||||
"openai_model": "Model OpenAI",
|
||||
"openai_temperature": "Teplota (OpenAI)",
|
||||
"anthropic_api_key": "Anthropic API klíč",
|
||||
"anthropic_model": "Model Anthropic",
|
||||
"anthropic_temperature": "Teplota (Anthropic)",
|
||||
"google_api_key": "Google API klíč",
|
||||
"google_model": "Model Google",
|
||||
"google_temperature": "Teplota (Google)",
|
||||
"groq_api_key": "Groq API klíč",
|
||||
"groq_model": "Model Groq",
|
||||
"groq_temperature": "Teplota (Groq)",
|
||||
"localai_ip": "IP adresa LocalAI",
|
||||
"localai_port": "Port LocalAI",
|
||||
"localai_https": "Používat HTTPS pro LocalAI",
|
||||
"localai_model": "Model LocalAI",
|
||||
"localai_temperature": "Teplota (LocalAI)",
|
||||
"ollama_base_url": "Základní URL Ollama/Open WebUI",
|
||||
"ollama_api_key": "API klíč Ollama/Open WebUI (volitelné)",
|
||||
"ollama_ip": "IP adresa Ollama",
|
||||
"ollama_port": "Port Ollama",
|
||||
"ollama_https": "Používat HTTPS pro Ollama",
|
||||
"ollama_model": "Model Ollama",
|
||||
"ollama_temperature": "Teplota (Ollama)",
|
||||
"ollama_disable_think": "Zakázat režim přemýšlení (Ollama)",
|
||||
"custom_openai_endpoint": "Koncový bod vlastního OpenAI",
|
||||
"custom_openai_api_key": "API klíč vlastního OpenAI",
|
||||
"custom_openai_model": "Model vlastního OpenAI",
|
||||
"custom_openai_temperature": "Teplota (vlastní OpenAI)",
|
||||
"mistral_api_key": "Mistral API klíč",
|
||||
"mistral_model": "Model Mistral",
|
||||
"mistral_temperature": "Teplota (Mistral AI)",
|
||||
"perplexity_api_key": "Perplexity API klíč",
|
||||
"perplexity_model": "Model Perplexity",
|
||||
"perplexity_temperature": "Teplota (Perplexity AI)",
|
||||
"openrouter_api_key": "OpenRouter API klíč",
|
||||
"openrouter_model": "Model OpenRouter",
|
||||
"openrouter_reasoning_max_tokens": "Maximální počet tokenů pro uvažování OpenRouter",
|
||||
"openrouter_temperature": "Teplota (OpenRouter)",
|
||||
"openai_azure_api_key": "Azure OpenAI API klíč",
|
||||
"openai_azure_deployment_id": "ID nasazení Azure",
|
||||
"openai_azure_endpoint": "Koncový bod Azure",
|
||||
"openai_azure_api_version": "Verze Azure API",
|
||||
"openai_azure_temperature": "Teplota (Azure OpenAI)",
|
||||
"generic_openai_api_endpoint": "Úplná URL adresa API obecného OpenAI (musí končit na 'completions')",
|
||||
"generic_openai_api_key": "API klíč obecného OpenAI",
|
||||
"generic_openai_model": "Model obecného OpenAI",
|
||||
"generic_openai_temperature": "Teplota (obecné OpenAI)",
|
||||
"generic_openai_validation_endpoint": "Ověřovací URL (obecné OpenAI, musí končit na 'models')",
|
||||
"generic_openai_enable_validation": "Povolit ověřování (obecné OpenAI)",
|
||||
"max_input_tokens": "Maximální počet vstupních tokenů",
|
||||
"max_output_tokens": "Maximální počet výstupních tokenů",
|
||||
"custom_system_prompt": "Trvalý vlastní systémový prompt",
|
||||
"excluded_domains": "Vyloučené domény",
|
||||
"excluded_entities": "Vyloučené entity",
|
||||
"excluded_areas": "Vyloučené oblasti",
|
||||
"history_retention": "Uchování historie návrhů",
|
||||
"request_timeout": "Časový limit požadavku na poskytovatele",
|
||||
"openai_reasoning_effort": "Úroveň uvažování OpenAI"
|
||||
},
|
||||
"description": "Upravte nastavení svých poskytovatelů AI. Použijí se pole relevantní pro nakonfigurovaného poskytovatele. Běžná nastavení, jako jsou limity tokenů, se konfigurují pro každého poskytovatele zvlášť."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_input": "Jedna nebo více hodnot je neplatná."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"generate_suggestions": {
|
||||
"name": "Generovat návrhy",
|
||||
"description": "Ručně spustit návrhy automatizací AI.",
|
||||
"fields": {
|
||||
"provider_config": {
|
||||
"name": "Konfigurace poskytovatele",
|
||||
"description": "Kterou konfiguraci poskytovatele použít (pokud jich máte více)?"
|
||||
},
|
||||
"custom_prompt": {
|
||||
"name": "Vlastní prompt",
|
||||
"description": "Volitelný vlastní prompt pro přepsání výchozího systémového promptu nebo směrování návrhů k určitým tématům."
|
||||
},
|
||||
"all_entities": {
|
||||
"name": "Zohlednit všechny entity",
|
||||
"description": "Pokud je zapnuto, zohlední všechny entity místo pouze nových entit."
|
||||
},
|
||||
"domains": {
|
||||
"name": "Domény",
|
||||
"description": "Seznam domén ke zohlednění. Pokud je prázdný, zohlední všechny domény."
|
||||
},
|
||||
"entity_limit": {
|
||||
"name": "Limit entit",
|
||||
"description": "Maximální počet entit ke zohlednění (náhodně vybrané)."
|
||||
},
|
||||
"automation_read_yaml": {
|
||||
"name": "Číst soubor 'automations.yaml'",
|
||||
"description": "Přečte a přidá YAML kód automatizací nalezených v souboru 'automations.yaml'. Tato akce použije mnoho vstupních tokenů, používejte ji opatrně a s modely s velkým kontextovým oknem (např. Gemini)."
|
||||
},
|
||||
"automation_limit": {
|
||||
"name": "Limit automatizací",
|
||||
"description": "Maximální počet automatizací k analýze (výchozí: 100)."
|
||||
},
|
||||
"exclude_domains": {
|
||||
"name": "Vyloučit domény",
|
||||
"description": "Domény, které se mají vyloučit z analýzy."
|
||||
},
|
||||
"exclude_entities": {
|
||||
"name": "Vyloučit entity",
|
||||
"description": "ID entit, které se mají vyloučit z analýzy."
|
||||
},
|
||||
"exclude_areas": {
|
||||
"name": "Vyloučit oblasti",
|
||||
"description": "Oblasti, které se mají vyloučit z analýzy."
|
||||
}
|
||||
}
|
||||
},
|
||||
"clear_history": {
|
||||
"name": "Vymazat historii návrhů",
|
||||
"description": "Vymazat všechny uložené návrhy automatizací AI."
|
||||
},
|
||||
"update_suggestion": {
|
||||
"name": "Aktualizovat návrh",
|
||||
"description": "Aktualizovat stav kontroly uloženého návrhu automatizace AI.",
|
||||
"fields": {
|
||||
"suggestion_id": {
|
||||
"name": "ID návrhu",
|
||||
"description": "ID návrhu, který se má aktualizovat."
|
||||
},
|
||||
"status": {
|
||||
"name": "Stav",
|
||||
"description": "Nový stav kontroly."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
{
|
||||
"title": "AI Automation Suggester",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "KI-Anbieter auswählen",
|
||||
"data": {
|
||||
"provider": "KI-Anbieter"
|
||||
}
|
||||
},
|
||||
"openai": {
|
||||
"title": "OpenAI konfigurieren",
|
||||
"data": {
|
||||
"openai_api_key": "OpenAI API-Schlüssel",
|
||||
"openai_model": "OpenAI-Modell",
|
||||
"openai_temperature": "Temperatur",
|
||||
"max_input_tokens": "Maximale Eingabe-Tokens",
|
||||
"max_output_tokens": "Maximale Ausgabe-Tokens"
|
||||
}
|
||||
},
|
||||
"anthropic": {
|
||||
"title": "Anthropic konfigurieren",
|
||||
"data": {
|
||||
"anthropic_api_key": "Anthropic API-Schlüssel",
|
||||
"anthropic_model": "Anthropic-Modell",
|
||||
"anthropic_temperature": "Temperatur",
|
||||
"max_input_tokens": "Maximale Eingabe-Tokens",
|
||||
"max_output_tokens": "Maximale Ausgabe-Tokens"
|
||||
}
|
||||
},
|
||||
"google": {
|
||||
"title": "Google konfigurieren",
|
||||
"data": {
|
||||
"google_api_key": "Google API-Schlüssel",
|
||||
"google_model": "Google-Modell",
|
||||
"google_temperature": "Temperatur",
|
||||
"max_input_tokens": "Maximale Eingabe-Tokens",
|
||||
"max_output_tokens": "Maximale Ausgabe-Tokens"
|
||||
}
|
||||
},
|
||||
"groq": {
|
||||
"title": "Groq konfigurieren",
|
||||
"data": {
|
||||
"groq_api_key": "Groq API-Schlüssel",
|
||||
"groq_model": "Groq-Modell",
|
||||
"groq_temperature": "Temperatur",
|
||||
"max_input_tokens": "Maximale Eingabe-Tokens",
|
||||
"max_output_tokens": "Maximale Ausgabe-Tokens"
|
||||
}
|
||||
},
|
||||
"localai": {
|
||||
"title": "LocalAI konfigurieren",
|
||||
"data": {
|
||||
"localai_ip": "LocalAI IP-Adresse",
|
||||
"localai_port": "LocalAI Port",
|
||||
"localai_https": "HTTPS für LocalAI verwenden",
|
||||
"localai_model": "LocalAI-Modell",
|
||||
"localai_temperature": "Temperatur",
|
||||
"max_input_tokens": "Maximale Eingabe-Tokens",
|
||||
"max_output_tokens": "Maximale Ausgabe-Tokens"
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Ollama konfigurieren",
|
||||
"data": {
|
||||
"ollama_ip": "Ollama IP-Adresse",
|
||||
"ollama_port": "Ollama Port",
|
||||
"ollama_https": "HTTPS für Ollama verwenden",
|
||||
"ollama_model": "Ollama-Modell",
|
||||
"ollama_temperature": "Temperatur",
|
||||
"ollama_disable_think": "Denkmodus deaktivieren (Ollama)",
|
||||
"max_input_tokens": "Maximale Eingabe-Tokens",
|
||||
"max_output_tokens": "Maximale Ausgabe-Tokens"
|
||||
}
|
||||
},
|
||||
"custom_openai": {
|
||||
"title": "Benutzerdefiniertes OpenAI konfigurieren",
|
||||
"data": {
|
||||
"custom_openai_endpoint": "Benutzerdefinierter OpenAI-Endpunkt",
|
||||
"custom_openai_api_key": "Benutzerdefinierter OpenAI API-Schlüssel (Optional)",
|
||||
"custom_openai_model": "Benutzerdefiniertes OpenAI-Modell",
|
||||
"custom_openai_temperature": "Temperatur",
|
||||
"max_input_tokens": "Maximale Eingabe-Tokens",
|
||||
"max_output_tokens": "Maximale Ausgabe-Tokens"
|
||||
}
|
||||
},
|
||||
"mistral": {
|
||||
"title": "Mistral AI konfigurieren",
|
||||
"data": {
|
||||
"mistral_api_key": "Mistral API-Schlüssel",
|
||||
"mistral_model": "Mistral-Modell",
|
||||
"mistral_temperature": "Temperatur",
|
||||
"max_input_tokens": "Maximale Eingabe-Tokens",
|
||||
"max_output_tokens": "Maximale Ausgabe-Tokens"
|
||||
}
|
||||
},
|
||||
"perplexity": {
|
||||
"title": "Perplexity AI konfigurieren",
|
||||
"data": {
|
||||
"perplexity_api_key": "Perplexity API-Schlüssel",
|
||||
"perplexity_model": "Perplexity-Modell",
|
||||
"perplexity_temperature": "Temperatur",
|
||||
"max_input_tokens": "Maximale Eingabe-Tokens",
|
||||
"max_output_tokens": "Maximale Ausgabe-Tokens"
|
||||
}
|
||||
},
|
||||
"openrouter": {
|
||||
"title": "OpenRouter konfigurieren",
|
||||
"data": {
|
||||
"openrouter_api_key": "OpenRouter API-Schlüssel",
|
||||
"openrouter_model": "OpenRouter-Modell",
|
||||
"openrouter_reasoning_max_tokens": "Maximale Tokens für Schlussfolgerungen (OpenRouter)",
|
||||
"openrouter_temperature": "Temperatur",
|
||||
"max_input_tokens": "Maximale Eingabe-Tokens",
|
||||
"max_output_tokens": "Maximale Ausgabe-Tokens"
|
||||
}
|
||||
},
|
||||
"openai_azure": {
|
||||
"title": "OpenAI Azure konfigurieren",
|
||||
"data": {
|
||||
"openai_azure_api_key": "Azure OpenAI API-Schlüssel",
|
||||
"openai_azure_deployment_id": "Azure Bereitstellungs-ID",
|
||||
"openai_azure_endpoint": "Azure-Endpunkt",
|
||||
"openai_azure_api_version": "Azure API-Version",
|
||||
"openai_azure_temperature": "Temperatur",
|
||||
"max_input_tokens": "Maximale Eingabe-Tokens",
|
||||
"max_output_tokens": "Maximale Ausgabe-Tokens"
|
||||
}
|
||||
},
|
||||
"generic_openai": {
|
||||
"title": "Generisches OpenAI konfigurieren",
|
||||
"data": {
|
||||
"generic_openai_api_endpoint": "Vollständige API-URL für generisches OpenAI (sollte auf 'completions' enden)",
|
||||
"generic_openai_api_key": "Generischer OpenAI API-Schlüssel (optional)",
|
||||
"generic_openai_model": "Generisches OpenAI-Modell",
|
||||
"generic_openai_temperature": "Temperatur",
|
||||
"generic_openai_validation_endpoint": "Validierungs-URL (sollte auf 'models' enden)",
|
||||
"generic_openai_enable_validation": "Validierung aktivieren",
|
||||
"max_input_tokens": "Maximale Eingabe-Tokens",
|
||||
"max_output_tokens": "Maximale Ausgabe-Tokens"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"api_error": "API-Validierung fehlgeschlagen: {error_message}",
|
||||
"cannot_connect": "Verbindung fehlgeschlagen. Bitte überprüfen Sie Ihre Einstellungen und Ihr Netzwerk.",
|
||||
"unknown": "Ein unbekannter Fehler ist aufgetreten. Bitte überprüfen Sie die Protokolle für Details."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Dieser KI-Anbieter ist bereits konfiguriert. Sie können ihn auf der Integrationsseite bearbeiten."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Optionen für KI-Automatisierungsvorschläge",
|
||||
"data": {
|
||||
"openai_api_key": "OpenAI API-Schlüssel",
|
||||
"openai_model": "OpenAI-Modell",
|
||||
"openai_temperature": "Temperatur (OpenAI)",
|
||||
"anthropic_api_key": "Anthropic API-Schlüssel",
|
||||
"anthropic_model": "Anthropic-Modell",
|
||||
"anthropic_temperature": "Temperatur (Anthropic)",
|
||||
"google_api_key": "Google API-Schlüssel",
|
||||
"google_model": "Google-Modell",
|
||||
"google_temperature": "Temperatur (Google)",
|
||||
"groq_api_key": "Groq API-Schlüssel",
|
||||
"groq_model": "Groq-Modell",
|
||||
"groq_temperature": "Temperatur (Groq)",
|
||||
"localai_ip": "LocalAI IP-Adresse",
|
||||
"localai_port": "LocalAI Port",
|
||||
"localai_https": "HTTPS für LocalAI verwenden",
|
||||
"localai_model": "LocalAI-Modell",
|
||||
"localai_temperature": "Temperatur (LocalAI)",
|
||||
"ollama_ip": "Ollama IP-Adresse",
|
||||
"ollama_port": "Ollama Port",
|
||||
"ollama_https": "HTTPS für Ollama verwenden",
|
||||
"ollama_model": "Ollama-Modell",
|
||||
"ollama_temperature": "Temperatur (Ollama)",
|
||||
"ollama_disable_think": "Denkmodus deaktivieren (Ollama)",
|
||||
"custom_openai_endpoint": "Benutzerdefinierter OpenAI-Endpunkt",
|
||||
"custom_openai_api_key": "Benutzerdefinierter OpenAI API-Schlüssel",
|
||||
"custom_openai_model": "Benutzerdefiniertes OpenAI-Modell",
|
||||
"custom_openai_temperature": "Temperatur (Benutzerdefiniertes OpenAI)",
|
||||
"mistral_api_key": "Mistral API-Schlüssel",
|
||||
"mistral_model": "Mistral-Modell",
|
||||
"mistral_temperature": "Temperatur (Mistral AI)",
|
||||
"perplexity_api_key": "Perplexity API-Schlüssel",
|
||||
"perplexity_model": "Perplexity-Modell",
|
||||
"perplexity_temperature": "Temperatur (Perplexity AI)",
|
||||
"openrouter_api_key": "OpenRouter API-Schlüssel",
|
||||
"openrouter_model": "OpenRouter-Modell",
|
||||
"openrouter_reasoning_max_tokens": "Maximale Tokens für Schlussfolgerungen (OpenRouter)",
|
||||
"openrouter_temperature": "Temperatur (OpenRouter)",
|
||||
"openai_azure_api_key": "Azure OpenAI API-Schlüssel",
|
||||
"openai_azure_deployment_id": "Azure Bereitstellungs-ID",
|
||||
"openai_azure_endpoint": "Azure-Endpunkt",
|
||||
"openai_azure_api_version": "Azure API-Version",
|
||||
"openai_azure_temperature": "Temperatur (OpenAI Azure)",
|
||||
"generic_openai_api_endpoint": "Vollständige API-URL für generisches OpenAI (sollte auf 'completions' enden)",
|
||||
"generic_openai_api_key": "Generischer OpenAI API-Schlüssel",
|
||||
"generic_openai_model": "Generisches OpenAI-Modell",
|
||||
"generic_openai_temperature": "Temperatur (Generisches OpenAI)",
|
||||
"generic_openai_validation_endpoint": "Validierungs-URL (Generisches OpenAI, sollte auf 'models' enden)",
|
||||
"generic_openai_enable_validation": "Validierung aktivieren (Generisches OpenAI)",
|
||||
"max_input_tokens": "Maximale Eingabe-Tokens",
|
||||
"max_output_tokens": "Maximale Ausgabe-Tokens"
|
||||
},
|
||||
"description": "Passen Sie die Einstellungen für Ihre KI-Anbieter an. Es werden nur die für den konfigurierten Anbieter relevanten Felder verwendet. Allgemeine Einstellungen wie Token-Limits werden pro Anbieter festgelegt."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_input": "Ein oder mehrere Werte sind ungültig."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"generate_suggestions": {
|
||||
"name": "Vorschläge generieren",
|
||||
"description": "Manuelles Auslösen von KI-Automatisierungsvorschlägen.",
|
||||
"fields": {
|
||||
"provider_config": {
|
||||
"name": "Anbieterkonfiguration",
|
||||
"description": "Welche Anbieterkonfiguration soll verwendet werden (falls mehrere vorhanden)?"
|
||||
},
|
||||
"custom_prompt": {
|
||||
"name": "Benutzerdefinierter Prompt",
|
||||
"description": "Optionaler benutzerdefinierter Prompt, um den Standard-System-Prompt zu überschreiben oder die Vorschläge auf bestimmte Themen auszurichten."
|
||||
},
|
||||
"all_entities": {
|
||||
"name": "Alle Entitäten berücksichtigen",
|
||||
"description": "Wenn wahr, werden alle Entitäten berücksichtigt, anstatt nur neue."
|
||||
},
|
||||
"domains": {
|
||||
"name": "Domänen",
|
||||
"description": "Liste der zu berücksichtigenden Domänen. Wenn leer, werden alle Domänen berücksichtigt."
|
||||
},
|
||||
"entity_limit": {
|
||||
"name": "Entitäten-Limit",
|
||||
"description": "Maximale Anzahl von Entitäten, die berücksichtigt werden (zufällig ausgewählt)."
|
||||
},
|
||||
"automation_read_yaml": {
|
||||
"name": "Datei 'automations.yaml' lesen",
|
||||
"description": "Liest den YAML-Code der in der 'automations.yaml'-Datei gefundenen Automatisierungen und fügt ihn an. Diese Aktion verbraucht viele Eingabe-Tokens. Verwenden Sie sie mit Vorsicht und mit Modellen, die ein großes Kontextfenster haben (z.B. Gemini)."
|
||||
},
|
||||
"automation_limit": {
|
||||
"name": "Limit für Automatisierungen",
|
||||
"description": "Maximale Anzahl der zu analysierenden Automatisierungen (Standard: 100)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
{
|
||||
"title": "AI Automation Suggester",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Select AI Provider",
|
||||
"data": {
|
||||
"provider": "AI Provider"
|
||||
}
|
||||
},
|
||||
"openai": {
|
||||
"title": "Configure OpenAI",
|
||||
"data": {
|
||||
"openai_api_key": "OpenAI API Key",
|
||||
"openai_model": "OpenAI Model",
|
||||
"openai_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"anthropic": {
|
||||
"title": "Configure Anthropic",
|
||||
"data": {
|
||||
"anthropic_api_key": "Anthropic API Key",
|
||||
"anthropic_model": "Anthropic Model",
|
||||
"anthropic_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"google": {
|
||||
"title": "Configure Google",
|
||||
"data": {
|
||||
"google_api_key": "Google API Key",
|
||||
"google_model": "Google Model",
|
||||
"google_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"groq": {
|
||||
"title": "Configure Groq",
|
||||
"data": {
|
||||
"groq_api_key": "Groq API Key",
|
||||
"groq_model": "Groq Model",
|
||||
"groq_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"localai": {
|
||||
"title": "Configure LocalAI",
|
||||
"data": {
|
||||
"localai_ip": "LocalAI IP Address",
|
||||
"localai_port": "LocalAI Port",
|
||||
"localai_https": "Use HTTPS for LocalAI",
|
||||
"localai_model": "LocalAI Model",
|
||||
"localai_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Configure Ollama",
|
||||
"data": {
|
||||
"ollama_base_url": "Ollama/Open WebUI Base URL",
|
||||
"ollama_api_key": "Ollama/Open WebUI API Key (optional)",
|
||||
"ollama_ip": "Ollama IP Address",
|
||||
"ollama_port": "Ollama Port",
|
||||
"ollama_https": "Use HTTPS for Ollama",
|
||||
"ollama_model": "Ollama Model",
|
||||
"ollama_temperature": "Temperature",
|
||||
"ollama_disable_think": "Disable Think Mode (Ollama)",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"custom_openai": {
|
||||
"title": "Configure Custom OpenAI",
|
||||
"data": {
|
||||
"custom_openai_endpoint": "Custom OpenAI Endpoint",
|
||||
"custom_openai_api_key": "Custom OpenAI API Key (Optional)",
|
||||
"custom_openai_model": "Custom OpenAI Model",
|
||||
"custom_openai_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"mistral": {
|
||||
"title": "Configure Mistral AI",
|
||||
"data": {
|
||||
"mistral_api_key": "Mistral API Key",
|
||||
"mistral_model": "Mistral Model",
|
||||
"mistral_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"perplexity": {
|
||||
"title": "Configure Perplexity AI",
|
||||
"data": {
|
||||
"perplexity_api_key": "Perplexity API Key",
|
||||
"perplexity_model": "Perplexity Model",
|
||||
"perplexity_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"openrouter": {
|
||||
"title": "Configure OpenRouter",
|
||||
"data": {
|
||||
"openrouter_api_key": "OpenRouter API Key",
|
||||
"openrouter_model": "OpenRouter Model",
|
||||
"openrouter_reasoning_max_tokens": "OpenRouter Reasoning Max Tokens",
|
||||
"openrouter_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"openai_azure": {
|
||||
"title": "Configure OpenAI Azure",
|
||||
"data": {
|
||||
"openai_azure_api_key": "Azure OpenAI API Key",
|
||||
"openai_azure_deployment_id": "Azure Deployment ID",
|
||||
"openai_azure_endpoint": "Azure Endpoint",
|
||||
"openai_azure_api_version": "Azure API Version",
|
||||
"openai_azure_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"generic_openai": {
|
||||
"title": "Configure Generic OpenAI",
|
||||
"data": {
|
||||
"generic_openai_api_endpoint": "Generic OpenAI API Full URL (should end with 'completions')",
|
||||
"generic_openai_api_key": "Generic OpenAI API Key (Optional)",
|
||||
"generic_openai_model": "Generic OpenAI Model",
|
||||
"generic_openai_temperature": "Temperature",
|
||||
"generic_openai_validation_endpoint": "Validation URL (should end with 'models')",
|
||||
"generic_openai_enable_validation": "Enable Validation",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
},
|
||||
"litellm": {
|
||||
"title": "Configure LiteLLM",
|
||||
"data": {
|
||||
"litellm_model": "LiteLLM Model (e.g. openai/gpt-4o, anthropic/claude-sonnet-4-6, groq/llama-3.3-70b-versatile)",
|
||||
"litellm_api_key": "API Key (optional if set via environment variable)",
|
||||
"litellm_api_base": "API Base URL (optional, for LiteLLM proxy)",
|
||||
"litellm_temperature": "Temperature",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"api_error": "API validation failed: {error_message}",
|
||||
"cannot_connect": "Failed to connect. Please check your settings and network.",
|
||||
"unknown": "An unknown error occurred. Please check logs for details."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This AI provider is already configured. You can edit it from the integrations page."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "AI Automation Suggester Options",
|
||||
"data": {
|
||||
"openai_api_key": "OpenAI API Key",
|
||||
"openai_model": "OpenAI Model",
|
||||
"openai_temperature": "Temperature (OpenAI)",
|
||||
"anthropic_api_key": "Anthropic API Key",
|
||||
"anthropic_model": "Anthropic Model",
|
||||
"anthropic_temperature": "Temperature (Anthropic)",
|
||||
"google_api_key": "Google API Key",
|
||||
"google_model": "Google Model",
|
||||
"google_temperature": "Temperature (Google)",
|
||||
"groq_api_key": "Groq API Key",
|
||||
"groq_model": "Groq Model",
|
||||
"groq_temperature": "Temperature (Groq)",
|
||||
"localai_ip": "LocalAI IP Address",
|
||||
"localai_port": "LocalAI Port",
|
||||
"localai_https": "Use HTTPS for LocalAI",
|
||||
"localai_model": "LocalAI Model",
|
||||
"localai_temperature": "Temperature (LocalAI)",
|
||||
"ollama_base_url": "Ollama/Open WebUI Base URL",
|
||||
"ollama_api_key": "Ollama/Open WebUI API Key (optional)",
|
||||
"ollama_ip": "Ollama IP Address",
|
||||
"ollama_port": "Ollama Port",
|
||||
"ollama_https": "Use HTTPS for Ollama",
|
||||
"ollama_model": "Ollama Model",
|
||||
"ollama_temperature": "Temperature (Ollama)",
|
||||
"ollama_disable_think": "Disable Think Mode (Ollama)",
|
||||
"custom_openai_endpoint": "Custom OpenAI Endpoint",
|
||||
"custom_openai_api_key": "Custom OpenAI API Key",
|
||||
"custom_openai_model": "Custom OpenAI Model",
|
||||
"custom_openai_temperature": "Temperature (Custom OpenAI)",
|
||||
"mistral_api_key": "Mistral API Key",
|
||||
"mistral_model": "Mistral Model",
|
||||
"mistral_temperature": "Temperature (Mistral AI)",
|
||||
"perplexity_api_key": "Perplexity API Key",
|
||||
"perplexity_model": "Perplexity Model",
|
||||
"perplexity_temperature": "Temperature (Perplexity AI)",
|
||||
"openrouter_api_key": "OpenRouter API Key",
|
||||
"openrouter_model": "OpenRouter Model",
|
||||
"openrouter_reasoning_max_tokens": "OpenRouter Reasoning Max Tokens",
|
||||
"openrouter_temperature": "Temperature (OpenRouter)",
|
||||
"openai_azure_api_key": "Azure OpenAI API Key",
|
||||
"openai_azure_deployment_id": "Azure Deployment ID",
|
||||
"openai_azure_endpoint": "Azure Endpoint",
|
||||
"openai_azure_api_version": "Azure API Version",
|
||||
"openai_azure_temperature": "Temperature (OpenAI Azure)",
|
||||
"generic_openai_api_endpoint": "Generic OpenAI API Full URL (should end with 'completions')",
|
||||
"generic_openai_api_key": "Generic OpenAI API Key",
|
||||
"generic_openai_model": "Generic OpenAI Model",
|
||||
"generic_openai_temperature": "Temperature (Generic OpenAI)",
|
||||
"generic_openai_validation_endpoint": "Validation URL (Generic OpenAI, should end with 'models')",
|
||||
"generic_openai_enable_validation": "Enable Validation (Generic OpenAI)",
|
||||
"litellm_api_key": "LiteLLM API Key",
|
||||
"litellm_model": "LiteLLM Model",
|
||||
"litellm_api_base": "LiteLLM API Base URL",
|
||||
"litellm_temperature": "Temperature (LiteLLM)",
|
||||
"max_input_tokens": "Max Input Tokens",
|
||||
"max_output_tokens": "Max Output Tokens",
|
||||
"custom_system_prompt": "Persistent Custom System Prompt",
|
||||
"excluded_domains": "Excluded Domains",
|
||||
"excluded_entities": "Excluded Entities",
|
||||
"excluded_areas": "Excluded Areas",
|
||||
"history_retention": "Suggestion History Retention",
|
||||
"request_timeout": "Provider Request Timeout",
|
||||
"openai_reasoning_effort": "OpenAI Reasoning Effort"
|
||||
},
|
||||
"description": "Adjust settings for your AI providers. Fields relevant to your configured provider will be used. Common settings like token limits are configured per provider."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_input": "One or more values are invalid."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"generate_suggestions": {
|
||||
"name": "Generate Suggestions",
|
||||
"description": "Manually trigger AI automation suggestions.",
|
||||
"fields": {
|
||||
"provider_config": {
|
||||
"name": "Provider Configuration",
|
||||
"description": "Which provider configuration to use (if you have multiple)"
|
||||
},
|
||||
"custom_prompt": {
|
||||
"name": "Custom Prompt",
|
||||
"description": "Optional custom prompt to override the default system prompt or guide the suggestions towards specific themes"
|
||||
},
|
||||
"all_entities": {
|
||||
"name": "Consider All Entities",
|
||||
"description": "If true, consider all entities instead of just new entities."
|
||||
},
|
||||
"domains": {
|
||||
"name": "Domains",
|
||||
"description": "List of domains to consider. If empty, consider all domains."
|
||||
},
|
||||
"entity_limit": {
|
||||
"name": "Entity Limit",
|
||||
"description": "Maximum number of entities to consider (randomly selected)."
|
||||
},
|
||||
"automation_read_yaml": {
|
||||
"name": "Read 'automations.yaml' file",
|
||||
"description": "Reads and appends the YAML code of the automations found in the 'automations.yaml' file. This action will use a lot of input tokens, use it with care and with models with a large context window (e.g. Gemini)."
|
||||
},
|
||||
"automation_limit": {
|
||||
"name": "Automation Limit",
|
||||
"description": "Maximum number of automations to analyze (default: 100)."
|
||||
},
|
||||
"exclude_domains": {
|
||||
"name": "Exclude Domains",
|
||||
"description": "Domains to exclude from analysis."
|
||||
},
|
||||
"exclude_entities": {
|
||||
"name": "Exclude Entities",
|
||||
"description": "Entity IDs to exclude from analysis."
|
||||
},
|
||||
"exclude_areas": {
|
||||
"name": "Exclude Areas",
|
||||
"description": "Areas to exclude from analysis."
|
||||
}
|
||||
}
|
||||
},
|
||||
"clear_history": {
|
||||
"name": "Clear Suggestion History",
|
||||
"description": "Clear all stored AI automation suggestions."
|
||||
},
|
||||
"update_suggestion": {
|
||||
"name": "Update Suggestion",
|
||||
"description": "Update the review status for a stored AI automation suggestion.",
|
||||
"fields": {
|
||||
"suggestion_id": {
|
||||
"name": "Suggestion ID",
|
||||
"description": "The suggestion ID to update."
|
||||
},
|
||||
"status": {
|
||||
"name": "Status",
|
||||
"description": "The new review status."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
{
|
||||
"title": "AI Automation Suggester",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Seleccionar Proveedor de IA",
|
||||
"data": {
|
||||
"provider": "Proveedor de IA"
|
||||
}
|
||||
},
|
||||
"openai": {
|
||||
"title": "Configurar OpenAI",
|
||||
"data": {
|
||||
"openai_api_key": "Clave API de OpenAI",
|
||||
"openai_model": "Modelo de OpenAI",
|
||||
"openai_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de tokens de entrada",
|
||||
"max_output_tokens": "Máximo de tokens de salida"
|
||||
}
|
||||
},
|
||||
"anthropic": {
|
||||
"title": "Configurar Anthropic",
|
||||
"data": {
|
||||
"anthropic_api_key": "Clave API de Anthropic",
|
||||
"anthropic_model": "Modelo de Anthropic",
|
||||
"anthropic_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de tokens de entrada",
|
||||
"max_output_tokens": "Máximo de tokens de salida"
|
||||
}
|
||||
},
|
||||
"google": {
|
||||
"title": "Configurar Google",
|
||||
"data": {
|
||||
"google_api_key": "Clave API de Google",
|
||||
"google_model": "Modelo de Google",
|
||||
"google_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de tokens de entrada",
|
||||
"max_output_tokens": "Máximo de tokens de salida"
|
||||
}
|
||||
},
|
||||
"groq": {
|
||||
"title": "Configurar Groq",
|
||||
"data": {
|
||||
"groq_api_key": "Clave API de Groq",
|
||||
"groq_model": "Modelo de Groq",
|
||||
"groq_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de tokens de entrada",
|
||||
"max_output_tokens": "Máximo de tokens de salida"
|
||||
}
|
||||
},
|
||||
"localai": {
|
||||
"title": "Configurar LocalAI",
|
||||
"data": {
|
||||
"localai_ip": "Dirección IP de LocalAI",
|
||||
"localai_port": "Puerto de LocalAI",
|
||||
"localai_https": "Usar HTTPS para LocalAI",
|
||||
"localai_model": "Modelo de LocalAI",
|
||||
"localai_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de tokens de entrada",
|
||||
"max_output_tokens": "Máximo de tokens de salida"
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Configurar Ollama",
|
||||
"data": {
|
||||
"ollama_ip": "Dirección IP de Ollama",
|
||||
"ollama_port": "Puerto de Ollama",
|
||||
"ollama_https": "Usar HTTPS para Ollama",
|
||||
"ollama_model": "Modelo de Ollama",
|
||||
"ollama_temperature": "Temperatura",
|
||||
"ollama_disable_think": "Desactivar Modo de Pensamiento (Ollama)",
|
||||
"max_input_tokens": "Máximo de tokens de entrada",
|
||||
"max_output_tokens": "Máximo de tokens de salida"
|
||||
}
|
||||
},
|
||||
"custom_openai": {
|
||||
"title": "Configurar OpenAI Personalizado",
|
||||
"data": {
|
||||
"custom_openai_endpoint": "URL de la API de OpenAI Personalizada",
|
||||
"custom_openai_api_key": "Clave API de OpenAI Personalizada (Opcional)",
|
||||
"custom_openai_model": "Modelo de OpenAI Personalizado",
|
||||
"custom_openai_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de tokens de entrada",
|
||||
"max_output_tokens": "Máximo de tokens de salida"
|
||||
}
|
||||
},
|
||||
"mistral": {
|
||||
"title": "Configurar Mistral AI",
|
||||
"data": {
|
||||
"mistral_api_key": "Clave API de Mistral",
|
||||
"mistral_model": "Modelo de Mistral",
|
||||
"mistral_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de tokens de entrada",
|
||||
"max_output_tokens": "Máximo de tokens de salida"
|
||||
}
|
||||
},
|
||||
"perplexity": {
|
||||
"title": "Configurar Perplexity AI",
|
||||
"data": {
|
||||
"perplexity_api_key": "Clave API de Perplexity",
|
||||
"perplexity_model": "Modelo de Perplexity",
|
||||
"perplexity_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de tokens de entrada",
|
||||
"max_output_tokens": "Máximo de tokens de salida"
|
||||
}
|
||||
},
|
||||
"openrouter": {
|
||||
"title": "Configurar OpenRouter",
|
||||
"data": {
|
||||
"openrouter_api_key": "Clave API de OpenRouter",
|
||||
"openrouter_model": "Modelo de OpenRouter",
|
||||
"openrouter_reasoning_max_tokens": "Máximo de tokens de razonamiento de OpenRouter",
|
||||
"openrouter_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de tokens de entrada",
|
||||
"max_output_tokens": "Máximo de tokens de salida"
|
||||
}
|
||||
},
|
||||
"openai_azure": {
|
||||
"title": "Configurar OpenAI Azure",
|
||||
"data": {
|
||||
"openai_azure_api_key": "Clave API de Azure OpenAI",
|
||||
"openai_azure_deployment_id": "ID de Despliegue de Azure",
|
||||
"openai_azure_endpoint": "URL de la API de Azure",
|
||||
"openai_azure_api_version": "Versión de la API de Azure",
|
||||
"openai_azure_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de tokens de entrada",
|
||||
"max_output_tokens": "Máximo de tokens de salida"
|
||||
}
|
||||
},
|
||||
"generic_openai": {
|
||||
"title": "Configurar OpenAI Genérico",
|
||||
"data": {
|
||||
"generic_openai_api_endpoint": "URL completa de la API de OpenAI genérica (debe terminar en 'completions')",
|
||||
"generic_openai_api_key": "Clave API de OpenAI Genérico (Opcional)",
|
||||
"generic_openai_model": "Modelo de OpenAI Genérico",
|
||||
"generic_openai_temperature": "Temperatura",
|
||||
"generic_openai_validation_endpoint": "URL de validación (debe terminar en 'models')",
|
||||
"generic_openai_enable_validation": "Habilitar Validación",
|
||||
"max_input_tokens": "Máximo de tokens de entrada",
|
||||
"max_output_tokens": "Máximo de tokens de salida"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"api_error": "Falló la validación de la API: {error_message}",
|
||||
"cannot_connect": "No se pudo conectar. Por favor, revisa tu configuración y red.",
|
||||
"unknown": "Ocurrió un error desconocido. Por favor, revisa los registros para más detalles."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Este proveedor de IA ya está configurado. Puedes editarlo desde la página de integraciones."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Opciones del Sugeridor de Automatizaciones con IA",
|
||||
"data": {
|
||||
"openai_api_key": "Clave API de OpenAI",
|
||||
"openai_model": "Modelo de OpenAI",
|
||||
"openai_temperature": "Temperatura (OpenAI)",
|
||||
"anthropic_api_key": "Clave API de Anthropic",
|
||||
"anthropic_model": "Modelo de Anthropic",
|
||||
"anthropic_temperature": "Temperatura (Anthropic)",
|
||||
"google_api_key": "Clave API de Google",
|
||||
"google_model": "Modelo de Google",
|
||||
"google_temperature": "Temperatura (Google)",
|
||||
"groq_api_key": "Clave API de Groq",
|
||||
"groq_model": "Modelo de Groq",
|
||||
"groq_temperature": "Temperatura (Groq)",
|
||||
"localai_ip": "Dirección IP de LocalAI",
|
||||
"localai_port": "Puerto de LocalAI",
|
||||
"localai_https": "Usar HTTPS para LocalAI",
|
||||
"localai_model": "Modelo de LocalAI",
|
||||
"localai_temperature": "Temperatura (LocalAI)",
|
||||
"ollama_ip": "Dirección IP de Ollama",
|
||||
"ollama_port": "Puerto de Ollama",
|
||||
"ollama_https": "Usar HTTPS para Ollama",
|
||||
"ollama_model": "Modelo de Ollama",
|
||||
"ollama_temperature": "Temperatura (Ollama)",
|
||||
"ollama_disable_think": "Desactivar Modo de Pensamiento (Ollama)",
|
||||
"custom_openai_endpoint": "URL de la API de OpenAI Personalizada",
|
||||
"custom_openai_api_key": "Clave API de OpenAI Personalizada",
|
||||
"custom_openai_model": "Modelo de OpenAI Personalizado",
|
||||
"custom_openai_temperature": "Temperatura (OpenAI Personalizado)",
|
||||
"mistral_api_key": "Clave API de Mistral",
|
||||
"mistral_model": "Modelo de Mistral",
|
||||
"mistral_temperature": "Temperatura (Mistral AI)",
|
||||
"perplexity_api_key": "Clave API de Perplexity",
|
||||
"perplexity_model": "Modelo de Perplexity",
|
||||
"perplexity_temperature": "Temperatura (Perplexity AI)",
|
||||
"openrouter_api_key": "Clave API de OpenRouter",
|
||||
"openrouter_model": "Modelo de OpenRouter",
|
||||
"openrouter_reasoning_max_tokens": "Máximo de tokens de razonamiento de OpenRouter",
|
||||
"openrouter_temperature": "Temperatura (OpenRouter)",
|
||||
"openai_azure_api_key": "Clave API de Azure OpenAI",
|
||||
"openai_azure_deployment_id": "ID de Despliegue de Azure",
|
||||
"openai_azure_endpoint": "URL de la API de Azure",
|
||||
"openai_azure_api_version": "Versión de la API de Azure",
|
||||
"openai_azure_temperature": "Temperatura (OpenAI Azure)",
|
||||
"generic_openai_api_endpoint": "URL completa de la API de OpenAI genérica (debe terminar en 'completions')",
|
||||
"generic_openai_api_key": "Clave API de OpenAI Genérico",
|
||||
"generic_openai_model": "Modelo de OpenAI Genérico",
|
||||
"generic_openai_temperature": "Temperatura (OpenAI Genérico)",
|
||||
"generic_openai_validation_endpoint": "URL de validación (OpenAI Genérico, debe terminar en 'models')",
|
||||
"generic_openai_enable_validation": "Habilitar Validación (OpenAI Genérico)",
|
||||
"max_input_tokens": "Máximo de tokens de entrada",
|
||||
"max_output_tokens": "Máximo de tokens de salida"
|
||||
},
|
||||
"description": "Ajusta la configuración de tus proveedores de IA. Se utilizarán los campos relevantes para el proveedor que hayas configurado. Los ajustes comunes, como los límites de tokens, se configuran para cada proveedor."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_input": "Uno o más valores son inválidos."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"generate_suggestions": {
|
||||
"name": "Generar Sugerencias",
|
||||
"description": "Activar manualmente las sugerencias de automatización con IA.",
|
||||
"fields": {
|
||||
"provider_config": {
|
||||
"name": "Configuración del Proveedor",
|
||||
"description": "¿Qué configuración de proveedor usar (si tienes varias)?"
|
||||
},
|
||||
"custom_prompt": {
|
||||
"name": "Prompt Personalizado",
|
||||
"description": "Prompt personalizado opcional para anular el prompt del sistema predeterminado o guiar las sugerencias hacia temas específicos."
|
||||
},
|
||||
"all_entities": {
|
||||
"name": "Considerar Todas las Entidades",
|
||||
"description": "Si es verdadero, considerar todas las entidades en lugar de solo las nuevas."
|
||||
},
|
||||
"domains": {
|
||||
"name": "Dominios",
|
||||
"description": "Lista de dominios a considerar. Si está vacío, se consideran todos los dominios."
|
||||
},
|
||||
"entity_limit": {
|
||||
"name": "Límite de Entidades",
|
||||
"description": "Número máximo de entidades a considerar (seleccionadas aleatoriamente)."
|
||||
},
|
||||
"automation_read_yaml": {
|
||||
"name": "Leer archivo 'automations.yaml'",
|
||||
"description": "Lee y adjunta el código YAML de las automatizaciones encontradas en el archivo 'automations.yaml'. Esta acción consumirá muchos tokens de entrada, úsala con precaución y con modelos que tengan una ventana de contexto grande (p. ej., Gemini)."
|
||||
},
|
||||
"automation_limit": {
|
||||
"name": "Límite de Automatizaciones",
|
||||
"description": "Número máximo de automatizaciones a analizar (por defecto: 100)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
{
|
||||
"title": "AI Automation Suggester",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Seleziona Provider AI",
|
||||
"data": {
|
||||
"provider": "Provider AI"
|
||||
}
|
||||
},
|
||||
"openai": {
|
||||
"title": "Configura OpenAI",
|
||||
"data": {
|
||||
"openai_api_key": "Chiave API OpenAI",
|
||||
"openai_model": "Modello OpenAI",
|
||||
"openai_temperature": "Temperatura",
|
||||
"max_input_tokens": "Numero massimo di token di input",
|
||||
"max_output_tokens": "Numero massimo di token di output"
|
||||
}
|
||||
},
|
||||
"anthropic": {
|
||||
"title": "Configura Anthropic",
|
||||
"data": {
|
||||
"anthropic_api_key": "Chiave API Anthropic",
|
||||
"anthropic_model": "Modello Anthropic",
|
||||
"anthropic_temperature": "Temperatura",
|
||||
"max_input_tokens": "Numero massimo di token di input",
|
||||
"max_output_tokens": "Numero massimo di token di output"
|
||||
}
|
||||
},
|
||||
"google": {
|
||||
"title": "Configura Google",
|
||||
"data": {
|
||||
"google_api_key": "Chiave API Google",
|
||||
"google_model": "Modello Google",
|
||||
"google_temperature": "Temperatura",
|
||||
"max_input_tokens": "Numero massimo di token di input",
|
||||
"max_output_tokens": "Numero massimo di token di output"
|
||||
}
|
||||
},
|
||||
"groq": {
|
||||
"title": "Configura Groq",
|
||||
"data": {
|
||||
"groq_api_key": "Chiave API Groq",
|
||||
"groq_model": "Modello Groq",
|
||||
"groq_temperature": "Temperatura",
|
||||
"max_input_tokens": "Numero massimo di token di input",
|
||||
"max_output_tokens": "Numero massimo di token di output"
|
||||
}
|
||||
},
|
||||
"localai": {
|
||||
"title": "Configura LocalAI",
|
||||
"data": {
|
||||
"localai_ip": "Indirizzo IP LocalAI",
|
||||
"localai_port": "Porta LocalAI",
|
||||
"localai_https": "Usa HTTPS per LocalAI",
|
||||
"localai_model": "Modello LocalAI",
|
||||
"localai_temperature": "Temperatura",
|
||||
"max_input_tokens": "Numero massimo di token di input",
|
||||
"max_output_tokens": "Numero massimo di token di output"
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Configura Ollama",
|
||||
"data": {
|
||||
"ollama_base_url": "URL base Ollama/Open WebUI",
|
||||
"ollama_api_key": "Chiave API Ollama/Open WebUI (opzionale)",
|
||||
"ollama_ip": "Indirizzo IP Ollama",
|
||||
"ollama_port": "Porta Ollama",
|
||||
"ollama_https": "Usa HTTPS per Ollama",
|
||||
"ollama_model": "Modello Ollama",
|
||||
"ollama_temperature": "Temperatura",
|
||||
"ollama_disable_think": "Disabilita Modalità Pensiero (Ollama)",
|
||||
"max_input_tokens": "Numero massimo di token di input",
|
||||
"max_output_tokens": "Numero massimo di token di output"
|
||||
}
|
||||
},
|
||||
"custom_openai": {
|
||||
"title": "Configura OpenAI Personalizzato",
|
||||
"data": {
|
||||
"custom_openai_endpoint": "Endpoint OpenAI Personalizzato",
|
||||
"custom_openai_api_key": "Chiave API OpenAI Personalizzata (Opzionale)",
|
||||
"custom_openai_model": "Modello OpenAI Personalizzato",
|
||||
"custom_openai_temperature": "Temperatura",
|
||||
"max_input_tokens": "Numero massimo di token di input",
|
||||
"max_output_tokens": "Numero massimo di token di output"
|
||||
}
|
||||
},
|
||||
"mistral": {
|
||||
"title": "Configura Mistral AI",
|
||||
"data": {
|
||||
"mistral_api_key": "Chiave API Mistral",
|
||||
"mistral_model": "Modello Mistral",
|
||||
"mistral_temperature": "Temperatura",
|
||||
"max_input_tokens": "Numero massimo di token di input",
|
||||
"max_output_tokens": "Numero massimo di token di output"
|
||||
}
|
||||
},
|
||||
"perplexity": {
|
||||
"title": "Configura Perplexity AI",
|
||||
"data": {
|
||||
"perplexity_api_key": "Chiave API Perplexity",
|
||||
"perplexity_model": "Modello Perplexity",
|
||||
"perplexity_temperature": "Temperatura",
|
||||
"max_input_tokens": "Numero massimo di token di input",
|
||||
"max_output_tokens": "Numero massimo di token di output"
|
||||
}
|
||||
},
|
||||
"openrouter": {
|
||||
"title": "Configura OpenRouter",
|
||||
"data": {
|
||||
"openrouter_api_key": "Chiave API OpenRouter",
|
||||
"openrouter_model": "Modello OpenRouter",
|
||||
"openrouter_reasoning_max_tokens": "Max Token di Ragionamento OpenRouter",
|
||||
"openrouter_temperature": "Temperatura",
|
||||
"max_input_tokens": "Numero massimo di token di input",
|
||||
"max_output_tokens": "Numero massimo di token di output"
|
||||
}
|
||||
},
|
||||
"openai_azure": {
|
||||
"title": "Configura OpenAI Azure",
|
||||
"data": {
|
||||
"openai_azure_api_key": "Chiave API Azure OpenAI",
|
||||
"openai_azure_deployment_id": "ID Distribuzione Azure",
|
||||
"openai_azure_endpoint": "Endpoint Azure",
|
||||
"openai_azure_api_version": "Versione API Azure",
|
||||
"openai_azure_temperature": "Temperatura",
|
||||
"max_input_tokens": "Numero massimo di token di input",
|
||||
"max_output_tokens": "Numero massimo di token di output"
|
||||
}
|
||||
},
|
||||
"generic_openai": {
|
||||
"title": "Configura OpenAI Generico",
|
||||
"data": {
|
||||
"generic_openai_api_endpoint": "URL API completa di OpenAI generico (deve terminare con 'completions')",
|
||||
"generic_openai_api_key": "Chiave API OpenAI Generico (Opzionale)",
|
||||
"generic_openai_model": "Modello OpenAI Generico",
|
||||
"generic_openai_temperature": "Temperatura",
|
||||
"generic_openai_validation_endpoint": "URL di validazione (deve terminare con 'models')",
|
||||
"generic_openai_enable_validation": "Abilita Validazione",
|
||||
"max_input_tokens": "Numero massimo di token di input",
|
||||
"max_output_tokens": "Numero massimo di token di output"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"api_error": "Validazione API fallita: {error_message}",
|
||||
"cannot_connect": "Connessione fallita. Controlla le impostazioni e la rete.",
|
||||
"unknown": "Si è verificato un errore sconosciuto. Controlla i log per i dettagli."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Questo provider AI è già configurato. Puoi modificarlo dalla pagina delle integrazioni."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Opzioni Suggeritore di Automazioni AI",
|
||||
"data": {
|
||||
"openai_api_key": "Chiave API OpenAI",
|
||||
"openai_model": "Modello OpenAI",
|
||||
"openai_temperature": "Temperatura (OpenAI)",
|
||||
"anthropic_api_key": "Chiave API Anthropic",
|
||||
"anthropic_model": "Modello Anthropic",
|
||||
"anthropic_temperature": "Temperatura (Anthropic)",
|
||||
"google_api_key": "Chiave API Google",
|
||||
"google_model": "Modello Google",
|
||||
"google_temperature": "Temperatura (Google)",
|
||||
"groq_api_key": "Chiave API Groq",
|
||||
"groq_model": "Modello Groq",
|
||||
"groq_temperature": "Temperatura (Groq)",
|
||||
"localai_ip": "Indirizzo IP LocalAI",
|
||||
"localai_port": "Porta LocalAI",
|
||||
"localai_https": "Usa HTTPS per LocalAI",
|
||||
"localai_model": "Modello LocalAI",
|
||||
"localai_temperature": "Temperatura (LocalAI)",
|
||||
"ollama_base_url": "URL base Ollama/Open WebUI",
|
||||
"ollama_api_key": "Chiave API Ollama/Open WebUI (opzionale)",
|
||||
"ollama_ip": "Indirizzo IP Ollama",
|
||||
"ollama_port": "Porta Ollama",
|
||||
"ollama_https": "Usa HTTPS per Ollama",
|
||||
"ollama_model": "Modello Ollama",
|
||||
"ollama_temperature": "Temperatura (Ollama)",
|
||||
"ollama_disable_think": "Disabilita Modalità Pensiero (Ollama)",
|
||||
"custom_openai_endpoint": "Endpoint OpenAI Personalizzato",
|
||||
"custom_openai_api_key": "Chiave API OpenAI Personalizzata",
|
||||
"custom_openai_model": "Modello OpenAI Personalizzato",
|
||||
"custom_openai_temperature": "Temperatura (OpenAI Personalizzato)",
|
||||
"mistral_api_key": "Chiave API Mistral",
|
||||
"mistral_model": "Modello Mistral",
|
||||
"mistral_temperature": "Temperatura (Mistral AI)",
|
||||
"perplexity_api_key": "Chiave API Perplexity",
|
||||
"perplexity_model": "Modello Perplexity",
|
||||
"perplexity_temperature": "Temperatura (Perplexity AI)",
|
||||
"openrouter_api_key": "Chiave API OpenRouter",
|
||||
"openrouter_model": "Modello OpenRouter",
|
||||
"openrouter_reasoning_max_tokens": "Max Token di Ragionamento OpenRouter",
|
||||
"openrouter_temperature": "Temperatura (OpenRouter)",
|
||||
"openai_azure_api_key": "Chiave API Azure OpenAI",
|
||||
"openai_azure_deployment_id": "ID Deployment Azure",
|
||||
"openai_azure_endpoint": "Endpoint Azure",
|
||||
"openai_azure_api_version": "Versione API Azure",
|
||||
"openai_azure_temperature": "Temperatura (OpenAI Azure)",
|
||||
"generic_openai_api_endpoint": "URL API completa di OpenAI generico (deve terminare con 'completions')",
|
||||
"generic_openai_api_key": "Chiave API OpenAI Generico",
|
||||
"generic_openai_model": "Modello OpenAI Generico",
|
||||
"generic_openai_temperature": "Temperatura (OpenAI Generico)",
|
||||
"generic_openai_validation_endpoint": "URL di validazione (OpenAI Generico, deve terminare con 'models')",
|
||||
"generic_openai_enable_validation": "Abilita Validazione (OpenAI Generico)",
|
||||
"max_input_tokens": "Numero massimo di token di input",
|
||||
"max_output_tokens": "Numero massimo di token di output",
|
||||
"custom_system_prompt": "Prompt di sistema personalizzato persistente",
|
||||
"excluded_domains": "Domini esclusi",
|
||||
"excluded_entities": "Entità escluse",
|
||||
"excluded_areas": "Aree escluse",
|
||||
"history_retention": "Conservazione cronologia suggerimenti",
|
||||
"request_timeout": "Timeout richiesta provider",
|
||||
"openai_reasoning_effort": "Intensità di ragionamento OpenAI"
|
||||
},
|
||||
"description": "Regola le impostazioni per i tuoi provider di IA. Verranno utilizzati i campi pertinenti al provider configurato. Le impostazioni comuni, come i limiti dei token, sono configurate per ciascun provider."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_input": "Uno o più valori non sono validi."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"generate_suggestions": {
|
||||
"name": "Genera Suggerimenti",
|
||||
"description": "Attiva manualmente i suggerimenti di automazione AI.",
|
||||
"fields": {
|
||||
"provider_config": {
|
||||
"name": "Configurazione Provider",
|
||||
"description": "Quale configurazione provider utilizzare (se ne hai più di una)?"
|
||||
},
|
||||
"custom_prompt": {
|
||||
"name": "Prompt Personalizzato",
|
||||
"description": "Prompt personalizzato opzionale per sovrascrivere il prompt di sistema predefinito o guidare i suggerimenti verso temi specifici."
|
||||
},
|
||||
"all_entities": {
|
||||
"name": "Considera Tutte le Entità",
|
||||
"description": "Se vero, considera tutte le entità invece di solo quelle nuove."
|
||||
},
|
||||
"domains": {
|
||||
"name": "Domini",
|
||||
"description": "Elenco dei domini da considerare. Se vuoto, considera tutti i domini."
|
||||
},
|
||||
"entity_limit": {
|
||||
"name": "Limite di Entità",
|
||||
"description": "Numero massimo di entità da considerare (scelte casualmente)."
|
||||
},
|
||||
"automation_read_yaml": {
|
||||
"name": "Leggi file 'automations.yaml'",
|
||||
"description": "Legge e aggiunge il codice YAML delle automazioni trovate nel file 'automations.yaml'. Questa azione utilizzerà molti token di input, usala con cautela e con modelli con un'ampia finestra di contesto (es. Gemini)."
|
||||
},
|
||||
"automation_limit": {
|
||||
"name": "Limite Automazioni",
|
||||
"description": "Numero massimo di automazioni da analizzare (predefinito: 100)."
|
||||
},
|
||||
"exclude_domains": {
|
||||
"name": "Escludi domini",
|
||||
"description": "Domini da escludere dall'analisi."
|
||||
},
|
||||
"exclude_entities": {
|
||||
"name": "Escludi entità",
|
||||
"description": "ID entità da escludere dall'analisi."
|
||||
},
|
||||
"exclude_areas": {
|
||||
"name": "Escludi aree",
|
||||
"description": "Aree da escludere dall'analisi."
|
||||
}
|
||||
}
|
||||
},
|
||||
"clear_history": {
|
||||
"name": "Cancella cronologia suggerimenti",
|
||||
"description": "Cancella tutti i suggerimenti di automazione AI memorizzati."
|
||||
},
|
||||
"update_suggestion": {
|
||||
"name": "Aggiorna suggerimento",
|
||||
"description": "Aggiorna lo stato di revisione di un suggerimento di automazione AI memorizzato.",
|
||||
"fields": {
|
||||
"suggestion_id": {
|
||||
"name": "ID suggerimento",
|
||||
"description": "L'ID del suggerimento da aggiornare."
|
||||
},
|
||||
"status": {
|
||||
"name": "Stato",
|
||||
"description": "Il nuovo stato di revisione."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
{
|
||||
"title": "AI Automation Suggester",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Selecteer AI Provider",
|
||||
"data": {
|
||||
"provider": "AI Provider"
|
||||
}
|
||||
},
|
||||
"openai": {
|
||||
"title": "Configureer OpenAI",
|
||||
"data": {
|
||||
"openai_api_key": "OpenAI API-sleutel",
|
||||
"openai_model": "OpenAI Model",
|
||||
"openai_temperature": "Temperatuur",
|
||||
"max_input_tokens": "Maximaal aantal invoertokens",
|
||||
"max_output_tokens": "Maximaal aantal uitvoertokens"
|
||||
}
|
||||
},
|
||||
"anthropic": {
|
||||
"title": "Configureer Anthropic",
|
||||
"data": {
|
||||
"anthropic_api_key": "Anthropic API-sleutel",
|
||||
"anthropic_model": "Anthropic Model",
|
||||
"anthropic_temperature": "Temperatuur",
|
||||
"max_input_tokens": "Maximaal aantal invoertokens",
|
||||
"max_output_tokens": "Maximaal aantal uitvoertokens"
|
||||
}
|
||||
},
|
||||
"google": {
|
||||
"title": "Configureer Google",
|
||||
"data": {
|
||||
"google_api_key": "Google API-sleutel",
|
||||
"google_model": "Google Model",
|
||||
"google_temperature": "Temperatuur",
|
||||
"max_input_tokens": "Maximaal aantal invoertokens",
|
||||
"max_output_tokens": "Maximaal aantal uitvoertokens"
|
||||
}
|
||||
},
|
||||
"groq": {
|
||||
"title": "Configureer Groq",
|
||||
"data": {
|
||||
"groq_api_key": "Groq API-sleutel",
|
||||
"groq_model": "Groq Model",
|
||||
"groq_temperature": "Temperatuur",
|
||||
"max_input_tokens": "Maximaal aantal invoertokens",
|
||||
"max_output_tokens": "Maximaal aantal uitvoertokens"
|
||||
}
|
||||
},
|
||||
"localai": {
|
||||
"title": "Configureer LocalAI",
|
||||
"data": {
|
||||
"localai_ip": "LocalAI IP-adres",
|
||||
"localai_port": "LocalAI Poort",
|
||||
"localai_https": "Gebruik HTTPS voor LocalAI",
|
||||
"localai_model": "LocalAI Model",
|
||||
"localai_temperature": "Temperatuur",
|
||||
"max_input_tokens": "Maximaal aantal invoertokens",
|
||||
"max_output_tokens": "Maximaal aantal uitvoertokens"
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Configureer Ollama",
|
||||
"data": {
|
||||
"ollama_ip": "Ollama IP-adres",
|
||||
"ollama_port": "Ollama Poort",
|
||||
"ollama_https": "Gebruik HTTPS voor Ollama",
|
||||
"ollama_model": "Ollama Model",
|
||||
"ollama_temperature": "Temperatuur",
|
||||
"ollama_disable_think": "Denkmodus uitschakelen (Ollama)",
|
||||
"max_input_tokens": "Maximaal aantal invoertokens",
|
||||
"max_output_tokens": "Maximaal aantal uitvoertokens"
|
||||
}
|
||||
},
|
||||
"custom_openai": {
|
||||
"title": "Configureer Aangepaste OpenAI",
|
||||
"data": {
|
||||
"custom_openai_endpoint": "Aangepast OpenAI Eindpunt",
|
||||
"custom_openai_api_key": "Aangepaste OpenAI API-sleutel (Optioneel)",
|
||||
"custom_openai_model": "Aangepast OpenAI Model",
|
||||
"custom_openai_temperature": "Temperatuur",
|
||||
"max_input_tokens": "Maximaal aantal invoertokens",
|
||||
"max_output_tokens": "Maximaal aantal uitvoertokens"
|
||||
}
|
||||
},
|
||||
"mistral": {
|
||||
"title": "Configureer Mistral AI",
|
||||
"data": {
|
||||
"mistral_api_key": "Mistral API-sleutel",
|
||||
"mistral_model": "Mistral Model",
|
||||
"mistral_temperature": "Temperatuur",
|
||||
"max_input_tokens": "Maximaal aantal invoertokens",
|
||||
"max_output_tokens": "Maximaal aantal uitvoertokens"
|
||||
}
|
||||
},
|
||||
"perplexity": {
|
||||
"title": "Configureer Perplexity AI",
|
||||
"data": {
|
||||
"perplexity_api_key": "Perplexity API-sleutel",
|
||||
"perplexity_model": "Perplexity Model",
|
||||
"perplexity_temperature": "Temperatuur",
|
||||
"max_input_tokens": "Maximaal aantal invoertokens",
|
||||
"max_output_tokens": "Maximaal aantal uitvoertokens"
|
||||
}
|
||||
},
|
||||
"openrouter": {
|
||||
"title": "Configureer OpenRouter",
|
||||
"data": {
|
||||
"openrouter_api_key": "OpenRouter API-sleutel",
|
||||
"openrouter_model": "OpenRouter Model",
|
||||
"openrouter_reasoning_max_tokens": "OpenRouter Redenering Max Tokens",
|
||||
"openrouter_temperature": "Temperatuur",
|
||||
"max_input_tokens": "Maximaal aantal invoertokens",
|
||||
"max_output_tokens": "Maximaal aantal uitvoertokens"
|
||||
}
|
||||
},
|
||||
"openai_azure": {
|
||||
"title": "Configureer OpenAI Azure",
|
||||
"data": {
|
||||
"openai_azure_api_key": "Azure OpenAI API-sleutel",
|
||||
"openai_azure_deployment_id": "Azure Implementatie-ID",
|
||||
"openai_azure_endpoint": "Azure Endpoint",
|
||||
"openai_azure_api_version": "Azure API-versie",
|
||||
"openai_azure_temperature": "Temperatuur",
|
||||
"max_input_tokens": "Maximaal aantal invoertokens",
|
||||
"max_output_tokens": "Maximaal aantal uitvoertokens"
|
||||
}
|
||||
},
|
||||
"generic_openai": {
|
||||
"title": "Configureer Generieke OpenAI",
|
||||
"data": {
|
||||
"generic_openai_api_endpoint": "Volledige API-URL voor generieke OpenAI (moet eindigen op 'completions')",
|
||||
"generic_openai_api_key": "Generieke OpenAI API-sleutel (Optioneel)",
|
||||
"generic_openai_model": "Generiek OpenAI Model",
|
||||
"generic_openai_temperature": "Temperatuur",
|
||||
"generic_openai_validation_endpoint": "Validatie-URL (moet eindigen op 'models')",
|
||||
"generic_openai_enable_validation": "Validatie Inschakelen",
|
||||
"max_input_tokens": "Maximaal aantal invoertokens",
|
||||
"max_output_tokens": "Maximaal aantal uitvoertokens"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"api_error": "API-validatie mislukt: {error_message}",
|
||||
"cannot_connect": "Verbinding mislukt. Controleer uw instellingen en netwerk.",
|
||||
"unknown": "Er is een onbekende fout opgetreden. Controleer de logs voor details."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Deze AI-provider is al geconfigureerd. U kunt deze bewerken via de integratiepagina."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "AI Automatisering Suggester Opties",
|
||||
"data": {
|
||||
"openai_api_key": "OpenAI API-sleutel",
|
||||
"openai_model": "OpenAI Model",
|
||||
"openai_temperature": "Temperatuur (OpenAI)",
|
||||
"anthropic_api_key": "Anthropic API-sleutel",
|
||||
"anthropic_model": "Anthropic Model",
|
||||
"anthropic_temperature": "Temperatuur (Anthropic)",
|
||||
"google_api_key": "Google API-sleutel",
|
||||
"google_model": "Google Model",
|
||||
"google_temperature": "Temperatuur (Google)",
|
||||
"groq_api_key": "Groq API-sleutel",
|
||||
"groq_model": "Groq Model",
|
||||
"groq_temperature": "Temperatuur (Groq)",
|
||||
"localai_ip": "LocalAI IP-adres",
|
||||
"localai_port": "LocalAI Poort",
|
||||
"localai_https": "Gebruik HTTPS voor LocalAI",
|
||||
"localai_model": "LocalAI Model",
|
||||
"localai_temperature": "Temperatuur (LocalAI)",
|
||||
"ollama_ip": "Ollama IP-adres",
|
||||
"ollama_port": "Ollama Poort",
|
||||
"ollama_https": "Gebruik HTTPS voor Ollama",
|
||||
"ollama_model": "Ollama Model",
|
||||
"ollama_temperature": "Temperatuur (Ollama)",
|
||||
"ollama_disable_think": "Denkmodus uitschakelen (Ollama)",
|
||||
"custom_openai_endpoint": "Aangepast OpenAI Eindpunt",
|
||||
"custom_openai_api_key": "Aangepaste OpenAI API-sleutel",
|
||||
"custom_openai_model": "Aangepast OpenAI Model",
|
||||
"custom_openai_temperature": "Temperatuur (Aangepaste OpenAI)",
|
||||
"mistral_api_key": "Mistral API-sleutel",
|
||||
"mistral_model": "Mistral Model",
|
||||
"mistral_temperature": "Temperatuur (Mistral AI)",
|
||||
"perplexity_api_key": "Perplexity API-sleutel",
|
||||
"perplexity_model": "Perplexity Model",
|
||||
"perplexity_temperature": "Temperatuur (Perplexity AI)",
|
||||
"openrouter_api_key": "OpenRouter API-sleutel",
|
||||
"openrouter_model": "OpenRouter Model",
|
||||
"openrouter_reasoning_max_tokens": "OpenRouter Redenering Max Tokens",
|
||||
"openrouter_temperature": "Temperatuur (OpenRouter)",
|
||||
"openai_azure_api_key": "Azure OpenAI API-sleutel",
|
||||
"openai_azure_deployment_id": "Azure Implementatie-ID",
|
||||
"openai_azure_endpoint": "Azure Eindpunt",
|
||||
"openai_azure_api_version": "Azure API Versie",
|
||||
"openai_azure_temperature": "Temperatuur (OpenAI Azure)",
|
||||
"generic_openai_api_endpoint": "Volledige API-URL voor generieke OpenAI (moet eindigen op 'completions')",
|
||||
"generic_openai_api_key": "Generieke OpenAI API-sleutel",
|
||||
"generic_openai_model": "Generiek OpenAI-model",
|
||||
"generic_openai_temperature": "Temperatuur (Generieke OpenAI)",
|
||||
"generic_openai_validation_endpoint": "Validatie-URL (Generieke OpenAI, moet eindigen op 'models')",
|
||||
"generic_openai_enable_validation": "Validatie Inschakelen (Generieke OpenAI)",
|
||||
"max_input_tokens": "Maximaal aantal invoertokens",
|
||||
"max_output_tokens": "Maximaal aantal uitvoertokens"
|
||||
},
|
||||
"description": "Pas de instellingen voor uw AI-providers aan. Alleen de velden die relevant zijn voor uw geconfigureerde provider worden gebruikt. Algemene instellingen zoals tokenlimieten worden per provider geconfigureerd."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_input": "Een of meer waarden zijn ongeldig."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"generate_suggestions": {
|
||||
"name": "Suggesties Genereren",
|
||||
"description": "Handmatig AI-automatiseringssuggesties activeren.",
|
||||
"fields": {
|
||||
"provider_config": {
|
||||
"name": "Aanbiederconfiguratie",
|
||||
"description": "Welke aanbiederconfiguratie moet worden gebruikt (indien er meerdere zijn)?"
|
||||
},
|
||||
"custom_prompt": {
|
||||
"name": "Aangepaste Prompt",
|
||||
"description": "Optionele aangepaste prompt om de standaardsysteemprompt te overschrijven of de suggesties naar specifieke thema's te leiden."
|
||||
},
|
||||
"all_entities": {
|
||||
"name": "Alle Entiteiten Beschouwen",
|
||||
"description": "Indien waar, worden alle entiteiten in overweging genomen in plaats van alleen nieuwe entiteiten."
|
||||
},
|
||||
"domains": {
|
||||
"name": "Domeinen",
|
||||
"description": "Lijst met te beschouwen domeinen. Als deze leeg is, worden alle domeinen beschouwd."
|
||||
},
|
||||
"entity_limit": {
|
||||
"name": "Entiteitenlimiet",
|
||||
"description": "Maximaal aantal te beschouwen entiteiten (willekeurig geselecteerd)."
|
||||
},
|
||||
"automation_read_yaml": {
|
||||
"name": "Lees 'automations.yaml' bestand",
|
||||
"description": "Leest en voegt de YAML-code van de automatiseringen uit het 'automations.yaml'-bestand toe. Deze actie verbruikt veel invoertokens, gebruik deze voorzichtig en met modellen met een groot contextvenster (bijv. Gemini)."
|
||||
},
|
||||
"automation_limit": {
|
||||
"name": "Automatiseringslimiet",
|
||||
"description": "Maximum aantal te analyseren automatiseringen (standaard: 100)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
{
|
||||
"title": "Sugestor de Automações por IA",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Selecionar Fornecedor de IA",
|
||||
"data": {
|
||||
"provider": "Fornecedor de IA"
|
||||
}
|
||||
},
|
||||
"openai": {
|
||||
"title": "Configurar OpenAI",
|
||||
"data": {
|
||||
"openai_api_key": "Chave API da OpenAI",
|
||||
"openai_model": "Modelo da OpenAI",
|
||||
"openai_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de Tokens de Entrada",
|
||||
"max_output_tokens": "Máximo de Tokens de Saída"
|
||||
}
|
||||
},
|
||||
"anthropic": {
|
||||
"title": "Configurar Anthropic",
|
||||
"data": {
|
||||
"anthropic_api_key": "Chave API da Anthropic",
|
||||
"anthropic_model": "Modelo da Anthropic",
|
||||
"anthropic_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de Tokens de Entrada",
|
||||
"max_output_tokens": "Máximo de Tokens de Saída"
|
||||
}
|
||||
},
|
||||
"google": {
|
||||
"title": "Configurar Google",
|
||||
"data": {
|
||||
"google_api_key": "Chave API da Google",
|
||||
"google_model": "Modelo da Google",
|
||||
"google_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de Tokens de Entrada",
|
||||
"max_output_tokens": "Máximo de Tokens de Saída"
|
||||
}
|
||||
},
|
||||
"groq": {
|
||||
"title": "Configurar Groq",
|
||||
"data": {
|
||||
"groq_api_key": "Chave API da Groq",
|
||||
"groq_model": "Modelo da Groq",
|
||||
"groq_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de Tokens de Entrada",
|
||||
"max_output_tokens": "Máximo de Tokens de Saída"
|
||||
}
|
||||
},
|
||||
"localai": {
|
||||
"title": "Configurar LocalAI",
|
||||
"data": {
|
||||
"localai_ip": "Endereço IP do LocalAI",
|
||||
"localai_port": "Porta do LocalAI",
|
||||
"localai_https": "Usar HTTPS para LocalAI",
|
||||
"localai_model": "Modelo do LocalAI",
|
||||
"localai_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de Tokens de Entrada",
|
||||
"max_output_tokens": "Máximo de Tokens de Saída"
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Configurar Ollama",
|
||||
"data": {
|
||||
"ollama_ip": "Endereço IP do Ollama",
|
||||
"ollama_port": "Porta do Ollama",
|
||||
"ollama_https": "Usar HTTPS para Ollama",
|
||||
"ollama_model": "Modelo do Ollama",
|
||||
"ollama_temperature": "Temperatura",
|
||||
"ollama_disable_think": "Desativar Modo 'Pensar' (Ollama)",
|
||||
"max_input_tokens": "Máximo de Tokens de Entrada",
|
||||
"max_output_tokens": "Máximo de Tokens de Saída"
|
||||
}
|
||||
},
|
||||
"custom_openai": {
|
||||
"title": "Configurar OpenAI Personalizado",
|
||||
"data": {
|
||||
"custom_openai_endpoint": "Endpoint da OpenAI Personalizado",
|
||||
"custom_openai_api_key": "Chave API da OpenAI Personalizado (Opcional)",
|
||||
"custom_openai_model": "Modelo da OpenAI Personalizado",
|
||||
"custom_openai_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de Tokens de Entrada",
|
||||
"max_output_tokens": "Máximo de Tokens de Saída"
|
||||
}
|
||||
},
|
||||
"mistral": {
|
||||
"title": "Configurar Mistral AI",
|
||||
"data": {
|
||||
"mistral_api_key": "Chave API da Mistral",
|
||||
"mistral_model": "Modelo da Mistral",
|
||||
"mistral_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de Tokens de Entrada",
|
||||
"max_output_tokens": "Máximo de Tokens de Saída"
|
||||
}
|
||||
},
|
||||
"perplexity": {
|
||||
"title": "Configurar Perplexity AI",
|
||||
"data": {
|
||||
"perplexity_api_key": "Chave API da Perplexity",
|
||||
"perplexity_model": "Modelo da Perplexity",
|
||||
"perplexity_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de Tokens de Entrada",
|
||||
"max_output_tokens": "Máximo de Tokens de Saída"
|
||||
}
|
||||
},
|
||||
"openrouter": {
|
||||
"title": "Configurar OpenRouter",
|
||||
"data": {
|
||||
"openrouter_api_key": "Chave API do OpenRouter",
|
||||
"openrouter_model": "Modelo do OpenRouter",
|
||||
"openrouter_reasoning_max_tokens": "Máximo de Tokens de Raciocínio do OpenRouter",
|
||||
"openrouter_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de Tokens de Entrada",
|
||||
"max_output_tokens": "Máximo de Tokens de Saída"
|
||||
}
|
||||
},
|
||||
"openai_azure": {
|
||||
"title": "Configurar OpenAI Azure",
|
||||
"data": {
|
||||
"openai_azure_api_key": "Chave API da OpenAI Azure",
|
||||
"openai_azure_deployment_id": "ID de Implementação (Deployment ID) Azure",
|
||||
"openai_azure_endpoint": "Endpoint Azure",
|
||||
"openai_azure_api_version": "Versão da API Azure",
|
||||
"openai_azure_temperature": "Temperatura",
|
||||
"max_input_tokens": "Máximo de Tokens de Entrada",
|
||||
"max_output_tokens": "Máximo de Tokens de Saída"
|
||||
}
|
||||
},
|
||||
"generic_openai": {
|
||||
"title": "Configurar OpenAI Genérico",
|
||||
"data": {
|
||||
"generic_openai_api_endpoint": "URL Completo da API OpenAI Genérica (deve terminar em 'completions')",
|
||||
"generic_openai_api_key": "Chave API da OpenAI Genérica (Opcional)",
|
||||
"generic_openai_model": "Modelo da OpenAI Genérica",
|
||||
"generic_openai_temperature": "Temperatura",
|
||||
"generic_openai_validation_endpoint": "URL de Validação (deve terminar em 'models')",
|
||||
"generic_openai_enable_validation": "Ativar Validação",
|
||||
"max_input_tokens": "Máximo de Tokens de Entrada",
|
||||
"max_output_tokens": "Máximo de Tokens de Saída"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"api_error": "Falha na validação da API: {error_message}",
|
||||
"cannot_connect": "Falha na ligação. Por favor, verifique as suas configurações e rede.",
|
||||
"unknown": "Ocorreu um erro desconhecido. Por favor, verifique os registos para mais detalhes (logs)."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Este fornecedor de IA já está configurado. Pode editá-lo na página de integrações."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Opções do Sugestor de Automações por IA",
|
||||
"data": {
|
||||
"openai_api_key": "Chave API da OpenAI",
|
||||
"openai_model": "Modelo da OpenAI",
|
||||
"openai_temperature": "Temperatura (OpenAI)",
|
||||
"anthropic_api_key": "Chave API da Anthropic",
|
||||
"anthropic_model": "Modelo da Anthropic",
|
||||
"anthropic_temperature": "Temperatura (Anthropic)",
|
||||
"google_api_key": "Chave API da Google",
|
||||
"google_model": "Modelo da Google",
|
||||
"google_temperature": "Temperatura (Google)",
|
||||
"groq_api_key": "Chave API da Groq",
|
||||
"groq_model": "Modelo da Groq",
|
||||
"groq_temperature": "Temperatura (Groq)",
|
||||
"localai_ip": "Endereço IP do LocalAI",
|
||||
"localai_port": "Porta do LocalAI",
|
||||
"localai_https": "Usar HTTPS para LocalAI",
|
||||
"localai_model": "Modelo do LocalAI",
|
||||
"localai_temperature": "Temperatura (LocalAI)",
|
||||
"ollama_ip": "Endereço IP do Ollama",
|
||||
"ollama_port": "Porta do Ollama",
|
||||
"ollama_https": "Usar HTTPS para Ollama",
|
||||
"ollama_model": "Modelo do Ollama",
|
||||
"ollama_temperature": "Temperatura (Ollama)",
|
||||
"ollama_disable_think": "Desativar Modo 'Pensar' (Ollama)",
|
||||
"custom_openai_endpoint": "Endpoint da OpenAI Personalizado",
|
||||
"custom_openai_api_key": "Chave API da OpenAI Personalizado",
|
||||
"custom_openai_model": "Modelo da OpenAI Personalizado",
|
||||
"custom_openai_temperature": "Temperatura (OpenAI Personalizado)",
|
||||
"mistral_api_key": "Chave API da Mistral",
|
||||
"mistral_model": "Modelo da Mistral",
|
||||
"mistral_temperature": "Temperatura (Mistral AI)",
|
||||
"perplexity_api_key": "Chave API da Perplexity",
|
||||
"perplexity_model": "Modelo da Perplexity",
|
||||
"perplexity_temperature": "Temperatura (Perplexity AI)",
|
||||
"openrouter_api_key": "Chave API do OpenRouter",
|
||||
"openrouter_model": "Modelo do OpenRouter",
|
||||
"openrouter_reasoning_max_tokens": "Máximo de Tokens de Raciocínio do OpenRouter",
|
||||
"openrouter_temperature": "Temperatura (OpenRouter)",
|
||||
"openai_azure_api_key": "Chave API da OpenAI Azure",
|
||||
"openai_azure_deployment_id": "ID de Implementação (Deployment ID) Azure",
|
||||
"openai_azure_endpoint": "Endpoint Azure",
|
||||
"openai_azure_api_version": "Versão da API Azure",
|
||||
"openai_azure_temperature": "Temperatura (OpenAI Azure)",
|
||||
"generic_openai_api_endpoint": "URL Completo da API OpenAI Genérica (deve terminar em 'completions')",
|
||||
"generic_openai_api_key": "Chave API da OpenAI Genérica",
|
||||
"generic_openai_model": "Modelo da OpenAI Genérica",
|
||||
"generic_openai_temperature": "Temperatura (OpenAI Genérica)",
|
||||
"generic_openai_validation_endpoint": "URL de Validação (OpenAI Genérica, deve terminar em 'models')",
|
||||
"generic_openai_enable_validation": "Ativar Validação (OpenAI Genérica)",
|
||||
"max_input_tokens": "Máximo de Tokens de Entrada",
|
||||
"max_output_tokens": "Máximo de Tokens de Saída"
|
||||
},
|
||||
"description": "Ajuste as configurações para os seus fornecedores de IA. Os campos relevantes para o seu fornecedor configurado serão utilizados. As configurações comuns, como limites de tokens, são definidas por fornecedor."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_input": "Um ou mais valores são inválidos."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"generate_suggestions": {
|
||||
"name": "Gerar Sugestões",
|
||||
"description": "Acionar manualmente as sugestões de automação por IA.",
|
||||
"fields": {
|
||||
"provider_config": {
|
||||
"name": "Configuração do Fornecedor",
|
||||
"description": "Qual configuração de fornecedor utilizar (caso tenha várias)"
|
||||
},
|
||||
"custom_prompt": {
|
||||
"name": "Prompt Personalizado",
|
||||
"description": "Prompt personalizado opcional para anular o prompt do sistema predefinido ou guiar as sugestões para temas específicos"
|
||||
},
|
||||
"all_entities": {
|
||||
"name": "Considerar Todas as Entidades",
|
||||
"description": "Se verdadeiro, considera todas as entidades em vez de apenas novas entidades."
|
||||
},
|
||||
"domains": {
|
||||
"name": "Domínios",
|
||||
"description": "Lista de domínios a considerar. Se vazio, considera todos os domínios."
|
||||
},
|
||||
"entity_limit": {
|
||||
"name": "Limite de Entidades",
|
||||
"description": "Número máximo de entidades a considerar (selecionadas aleatoriamente)."
|
||||
},
|
||||
"automation_read_yaml": {
|
||||
"name": "Ler ficheiro 'automations.yaml'",
|
||||
"description": "Lê e anexa o código YAML das automações encontradas no ficheiro 'automations.yaml'. Esta ação irá usar muitos tokens de entrada; use-a com cautela e com modelos com uma janela de contexto grande (ex: Gemini)."
|
||||
},
|
||||
"automation_limit": {
|
||||
"name": "Limite de Automações",
|
||||
"description": "Número máximo de automações a analisar (predefinição: 100)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
{
|
||||
"title": "AI Automation Suggester",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Выберите Провайдера ИИ",
|
||||
"data": {
|
||||
"provider": "Провайдер ИИ"
|
||||
}
|
||||
},
|
||||
"openai": {
|
||||
"title": "Настроить OpenAI",
|
||||
"data": {
|
||||
"openai_api_key": "Ключ API OpenAI",
|
||||
"openai_model": "Модель OpenAI",
|
||||
"openai_temperature": "Температура",
|
||||
"max_input_tokens": "Максимальное количество входных токенов",
|
||||
"max_output_tokens": "Максимальное количество выходных токенов"
|
||||
}
|
||||
},
|
||||
"anthropic": {
|
||||
"title": "Настроить Anthropic",
|
||||
"data": {
|
||||
"anthropic_api_key": "Ключ API Anthropic",
|
||||
"anthropic_model": "Модель Anthropic",
|
||||
"anthropic_temperature": "Температура",
|
||||
"max_input_tokens": "Максимальное количество входных токенов",
|
||||
"max_output_tokens": "Максимальное количество выходных токенов"
|
||||
}
|
||||
},
|
||||
"google": {
|
||||
"title": "Настроить Google",
|
||||
"data": {
|
||||
"google_api_key": "Ключ API Google",
|
||||
"google_model": "Модель Google",
|
||||
"google_temperature": "Температура",
|
||||
"max_input_tokens": "Максимальное количество входных токенов",
|
||||
"max_output_tokens": "Максимальное количество выходных токенов"
|
||||
}
|
||||
},
|
||||
"groq": {
|
||||
"title": "Настроить Groq",
|
||||
"data": {
|
||||
"groq_api_key": "Ключ API Groq",
|
||||
"groq_model": "Модель Groq",
|
||||
"groq_temperature": "Температура",
|
||||
"max_input_tokens": "Максимальное количество входных токенов",
|
||||
"max_output_tokens": "Максимальное количество выходных токенов"
|
||||
}
|
||||
},
|
||||
"localai": {
|
||||
"title": "Настроить LocalAI",
|
||||
"data": {
|
||||
"localai_ip": "IP-адрес LocalAI",
|
||||
"localai_port": "Порт LocalAI",
|
||||
"localai_https": "Использовать HTTPS для LocalAI",
|
||||
"localai_model": "Модель LocalAI",
|
||||
"localai_temperature": "Температура",
|
||||
"max_input_tokens": "Максимальное количество входных токенов",
|
||||
"max_output_tokens": "Максимальное количество выходных токенов"
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Настроить Ollama",
|
||||
"data": {
|
||||
"ollama_ip": "IP-адрес Ollama",
|
||||
"ollama_port": "Порт Ollama",
|
||||
"ollama_https": "Использовать HTTPS для Ollama",
|
||||
"ollama_model": "Модель Ollama",
|
||||
"ollama_temperature": "Температура",
|
||||
"ollama_disable_think": "Отключить Режим Размышления (Ollama)",
|
||||
"max_input_tokens": "Максимальное количество входных токенов",
|
||||
"max_output_tokens": "Максимальное количество выходных токенов"
|
||||
}
|
||||
},
|
||||
"custom_openai": {
|
||||
"title": "Настроить Пользовательский OpenAI",
|
||||
"data": {
|
||||
"custom_openai_endpoint": "Пользовательский Endpoint OpenAI",
|
||||
"custom_openai_api_key": "Пользовательский Ключ API OpenAI (Необязательно)",
|
||||
"custom_openai_model": "Пользовательская Модель OpenAI",
|
||||
"custom_openai_temperature": "Температура",
|
||||
"max_input_tokens": "Максимальное количество входных токенов",
|
||||
"max_output_tokens": "Максимальное количество выходных токенов"
|
||||
}
|
||||
},
|
||||
"mistral": {
|
||||
"title": "Настроить Mistral AI",
|
||||
"data": {
|
||||
"mistral_api_key": "Ключ API Mistral",
|
||||
"mistral_model": "Модель Mistral",
|
||||
"mistral_temperature": "Температура",
|
||||
"max_input_tokens": "Максимальное количество входных токенов",
|
||||
"max_output_tokens": "Максимальное количество выходных токенов"
|
||||
}
|
||||
},
|
||||
"perplexity": {
|
||||
"title": "Настроить Perplexity AI",
|
||||
"data": {
|
||||
"perplexity_api_key": "Ключ API Perplexity",
|
||||
"perplexity_model": "Модель Perplexity",
|
||||
"perplexity_temperature": "Температура",
|
||||
"max_input_tokens": "Максимальное количество входных токенов",
|
||||
"max_output_tokens": "Максимальное количество выходных токенов"
|
||||
}
|
||||
},
|
||||
"openrouter": {
|
||||
"title": "Настроить OpenRouter",
|
||||
"data": {
|
||||
"openrouter_api_key": "Ключ API OpenRouter",
|
||||
"openrouter_model": "Модель OpenRouter",
|
||||
"openrouter_reasoning_max_tokens": "Макс. Токенов для Размышлений OpenRouter",
|
||||
"openrouter_temperature": "Температура",
|
||||
"max_input_tokens": "Максимальное количество входных токенов",
|
||||
"max_output_tokens": "Максимальное количество выходных токенов"
|
||||
}
|
||||
},
|
||||
"openai_azure": {
|
||||
"title": "Настройка OpenAI Azure",
|
||||
"data": {
|
||||
"openai_azure_api_key": "Ключ API Azure OpenAI",
|
||||
"openai_azure_deployment_id": "ID развертывания Azure",
|
||||
"openai_azure_endpoint": "Конечная точка Azure",
|
||||
"openai_azure_api_version": "Версия API Azure",
|
||||
"openai_azure_temperature": "Температура",
|
||||
"max_input_tokens": "Максимальное количество входных токенов",
|
||||
"max_output_tokens": "Максимальное количество выходных токенов"
|
||||
}
|
||||
},
|
||||
"generic_openai": {
|
||||
"title": "Настройка универсального OpenAI",
|
||||
"data": {
|
||||
"generic_openai_api_endpoint": "Полный URL-адрес API универсального OpenAI (должен заканчиваться на 'completions')",
|
||||
"generic_openai_api_key": "Ключ API универсального OpenAI (опционально)",
|
||||
"generic_openai_model": "Модель универсального OpenAI",
|
||||
"generic_openai_temperature": "Температура",
|
||||
"generic_openai_validation_endpoint": "URL-адрес для проверки (должен заканчиваться на 'models')",
|
||||
"generic_openai_enable_validation": "Включить валидацию",
|
||||
"max_input_tokens": "Максимальное количество входных токенов",
|
||||
"max_output_tokens": "Максимальное количество выходных токенов"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"api_error": "Ошибка проверки API: {error_message}",
|
||||
"cannot_connect": "Не удалось подключиться. Проверьте настройки и сеть.",
|
||||
"unknown": "Произошла неизвестная ошибка. Проверьте логи для получения подробной информации."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Этот провайдер ИИ уже настроен. Вы можете отредактировать его на странице интеграций."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Настройки Советчика Автоматизаций с ИИ",
|
||||
"data": {
|
||||
"openai_api_key": "Ключ API OpenAI",
|
||||
"openai_model": "Модель OpenAI",
|
||||
"openai_temperature": "Температура (OpenAI)",
|
||||
"anthropic_api_key": "Ключ API Anthropic",
|
||||
"anthropic_model": "Модель Anthropic",
|
||||
"anthropic_temperature": "Температура (Anthropic)",
|
||||
"google_api_key": "Ключ API Google",
|
||||
"google_model": "Модель Google",
|
||||
"google_temperature": "Температура (Google)",
|
||||
"groq_api_key": "Ключ API Groq",
|
||||
"groq_model": "Модель Groq",
|
||||
"groq_temperature": "Температура (Groq)",
|
||||
"localai_ip": "IP-адрес LocalAI",
|
||||
"localai_port": "Порт LocalAI",
|
||||
"localai_https": "Использовать HTTPS для LocalAI",
|
||||
"localai_model": "Модель LocalAI",
|
||||
"localai_temperature": "Температура (LocalAI)",
|
||||
"ollama_ip": "IP-адрес Ollama",
|
||||
"ollama_port": "Порт Ollama",
|
||||
"ollama_https": "Использовать HTTPS для Ollama",
|
||||
"ollama_model": "Модель Ollama",
|
||||
"ollama_temperature": "Температура (Ollama)",
|
||||
"ollama_disable_think": "Отключить Режим Размышления (Ollama)",
|
||||
"custom_openai_endpoint": "Пользовательский Endpoint OpenAI",
|
||||
"custom_openai_api_key": "Пользовательский Ключ API OpenAI",
|
||||
"custom_openai_model": "Пользовательская Модель OpenAI",
|
||||
"custom_openai_temperature": "Температура (Пользовательский OpenAI)",
|
||||
"mistral_api_key": "Ключ API Mistral",
|
||||
"mistral_model": "Модель Mistral",
|
||||
"mistral_temperature": "Температура (Mistral AI)",
|
||||
"perplexity_api_key": "Ключ API Perplexity",
|
||||
"perplexity_model": "Модель Perplexity",
|
||||
"perplexity_temperature": "Температура (Perplexity AI)",
|
||||
"openrouter_api_key": "Ключ API OpenRouter",
|
||||
"openrouter_model": "Модель OpenRouter",
|
||||
"openrouter_reasoning_max_tokens": "Макс. Токенов для Размышлений OpenRouter",
|
||||
"openrouter_temperature": "Температура (OpenRouter)",
|
||||
"openai_azure_api_key": "Ключ API Azure OpenAI",
|
||||
"openai_azure_deployment_id": "ID Развертывания Azure",
|
||||
"openai_azure_endpoint": "Endpoint Azure",
|
||||
"openai_azure_api_version": "Версия API Azure",
|
||||
"openai_azure_temperature": "Температура (OpenAI Azure)",
|
||||
"generic_openai_api_endpoint": "Полный URL-адрес API для Общего OpenAI (должен заканчиваться на 'completions')",
|
||||
"generic_openai_api_key": "Ключ API Общего OpenAI",
|
||||
"generic_openai_model": "Модель Общего OpenAI",
|
||||
"generic_openai_temperature": "Температура (Общий OpenAI)",
|
||||
"generic_openai_validation_endpoint": "URL-адрес для проверки (Общий OpenAI, должен заканчиваться на 'models')",
|
||||
"generic_openai_enable_validation": "Включить валидацию (Общий OpenAI)",
|
||||
"max_input_tokens": "Максимальное количество входных токенов",
|
||||
"max_output_tokens": "Максимальное количество выходных токенов"
|
||||
},
|
||||
"description": "Настройте параметры для ваших провайдеров ИИ. Будут использоваться только те поля, которые относятся к настроенному вами провайдеру. Общие параметры, такие как лимиты токенов, настраиваются для каждого провайдера."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_input": "Одно или несколько значений недействительны."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"generate_suggestions": {
|
||||
"name": "Сгенерировать Предложения",
|
||||
"description": "Вручную запустить генерацию предложений по автоматизации с ИИ.",
|
||||
"fields": {
|
||||
"provider_config": {
|
||||
"name": "Конфигурация провайдера",
|
||||
"description": "Какую конфигурацию провайдера использовать (если у вас несколько)"
|
||||
},
|
||||
"custom_prompt": {
|
||||
"name": "Пользовательский запрос",
|
||||
"description": "Необязательный пользовательский запрос для переопределения системного запроса по умолчанию или направления предложений к определённым темам."
|
||||
},
|
||||
"all_entities": {
|
||||
"name": "Учитывать все сущности",
|
||||
"description": "Если включено, учитываются все сущности, а не только новые."
|
||||
},
|
||||
"domains": {
|
||||
"name": "Домены",
|
||||
"description": "Список доменов для рассмотрения. Если пусто, учитываются все домены."
|
||||
},
|
||||
"entity_limit": {
|
||||
"name": "Лимит сущностей",
|
||||
"description": "Максимальное количество сущностей для рассмотрения (выбирается случайным образом)."
|
||||
},
|
||||
"automation_read_yaml": {
|
||||
"name": "Прочитать файл 'automations.yaml'",
|
||||
"description": "Читает и добавляет YAML-код автоматизаций, найденных в файле 'automations.yaml'. Это действие использует много входных токенов, используйте его с осторожностью и с моделями с большим окном контекста (например, Gemini)."
|
||||
},
|
||||
"automation_limit": {
|
||||
"name": "Лимит автоматизаций",
|
||||
"description": "Максимальное количество автоматизаций для анализа (по умолчанию: 100)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
{
|
||||
"title": "AI Otomasyon Önericisi",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "AI Sağlayıcı Seçin",
|
||||
"data": {
|
||||
"provider": "AI Sağlayıcı"
|
||||
}
|
||||
},
|
||||
"openai": {
|
||||
"title": "OpenAI Yapılandırması",
|
||||
"data": {
|
||||
"openai_api_key": "OpenAI API Anahtarı",
|
||||
"openai_model": "OpenAI Modeli",
|
||||
"openai_temperature": "Sıcaklık",
|
||||
"max_input_tokens": "Maksimum Girdi Token",
|
||||
"max_output_tokens": "Maksimum Çıktı Token"
|
||||
}
|
||||
},
|
||||
"anthropic": {
|
||||
"title": "Anthropic Yapılandırması",
|
||||
"data": {
|
||||
"anthropic_api_key": "Anthropic API Anahtarı",
|
||||
"anthropic_model": "Anthropic Modeli",
|
||||
"anthropic_temperature": "Sıcaklık",
|
||||
"max_input_tokens": "Maksimum Girdi Token",
|
||||
"max_output_tokens": "Maksimum Çıktı Token"
|
||||
}
|
||||
},
|
||||
"google": {
|
||||
"title": "Google Yapılandırması",
|
||||
"data": {
|
||||
"google_api_key": "Google API Anahtarı",
|
||||
"google_model": "Google Modeli",
|
||||
"google_temperature": "Sıcaklık",
|
||||
"max_input_tokens": "Maksimum Girdi Token",
|
||||
"max_output_tokens": "Maksimum Çıktı Token"
|
||||
}
|
||||
},
|
||||
"groq": {
|
||||
"title": "Groq Yapılandırması",
|
||||
"data": {
|
||||
"groq_api_key": "Groq API Anahtarı",
|
||||
"groq_model": "Groq Modeli",
|
||||
"groq_temperature": "Sıcaklık",
|
||||
"max_input_tokens": "Maksimum Girdi Token",
|
||||
"max_output_tokens": "Maksimum Çıktı Token"
|
||||
}
|
||||
},
|
||||
"localai": {
|
||||
"title": "LocalAI Yapılandırması",
|
||||
"data": {
|
||||
"localai_ip": "LocalAI IP Adresi",
|
||||
"localai_port": "LocalAI Port",
|
||||
"localai_https": "LocalAI için HTTPS Kullan",
|
||||
"localai_model": "LocalAI Modeli",
|
||||
"localai_temperature": "Sıcaklık",
|
||||
"max_input_tokens": "Maksimum Girdi Token",
|
||||
"max_output_tokens": "Maksimum Çıktı Token"
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Ollama Yapılandırması",
|
||||
"data": {
|
||||
"ollama_ip": "Ollama IP Adresi",
|
||||
"ollama_port": "Ollama Port",
|
||||
"ollama_https": "Ollama için HTTPS Kullan",
|
||||
"ollama_model": "Ollama Modeli",
|
||||
"ollama_temperature": "Sıcaklık",
|
||||
"ollama_disable_think": "Düşünme Modunu Devre Dışı Bırak (Ollama)",
|
||||
"max_input_tokens": "Maksimum Girdi Token",
|
||||
"max_output_tokens": "Maksimum Çıktı Token"
|
||||
}
|
||||
},
|
||||
"custom_openai": {
|
||||
"title": "Özel OpenAI Yapılandırması",
|
||||
"data": {
|
||||
"custom_openai_endpoint": "Özel OpenAI Uç Noktası",
|
||||
"custom_openai_api_key": "Özel OpenAI API Anahtarı (İsteğe Bağlı)",
|
||||
"custom_openai_model": "Özel OpenAI Modeli",
|
||||
"custom_openai_temperature": "Sıcaklık",
|
||||
"max_input_tokens": "Maksimum Girdi Token",
|
||||
"max_output_tokens": "Maksimum Çıktı Token"
|
||||
}
|
||||
},
|
||||
"mistral": {
|
||||
"title": "Mistral AI Yapılandırması",
|
||||
"data": {
|
||||
"mistral_api_key": "Mistral API Anahtarı",
|
||||
"mistral_model": "Mistral Modeli",
|
||||
"mistral_temperature": "Sıcaklık",
|
||||
"max_input_tokens": "Maksimum Girdi Token",
|
||||
"max_output_tokens": "Maksimum Çıktı Token"
|
||||
}
|
||||
},
|
||||
"perplexity": {
|
||||
"title": "Perplexity AI Yapılandırması",
|
||||
"data": {
|
||||
"perplexity_api_key": "Perplexity API Anahtarı",
|
||||
"perplexity_model": "Perplexity Modeli",
|
||||
"perplexity_temperature": "Sıcaklık",
|
||||
"max_input_tokens": "Maksimum Girdi Token",
|
||||
"max_output_tokens": "Maksimum Çıktı Token"
|
||||
}
|
||||
},
|
||||
"openrouter": {
|
||||
"title": "OpenRouter Yapılandırması",
|
||||
"data": {
|
||||
"openrouter_api_key": "OpenRouter API Anahtarı",
|
||||
"openrouter_model": "OpenRouter Modeli",
|
||||
"openrouter_reasoning_max_tokens": "OpenRouter Akıl Yürütme Maks Token",
|
||||
"openrouter_temperature": "Sıcaklık",
|
||||
"max_input_tokens": "Maksimum Girdi Token",
|
||||
"max_output_tokens": "Maksimum Çıktı Token"
|
||||
}
|
||||
},
|
||||
"openai_azure": {
|
||||
"title": "OpenAI Azure Yapılandırması",
|
||||
"data": {
|
||||
"openai_azure_api_key": "Azure OpenAI API Anahtarı",
|
||||
"openai_azure_deployment_id": "Azure Dağıtım Kimliği",
|
||||
"openai_azure_endpoint": "Azure Uç Noktası",
|
||||
"openai_azure_api_version": "Azure API Sürümü",
|
||||
"openai_azure_temperature": "Sıcaklık",
|
||||
"max_input_tokens": "Maksimum Girdi Token",
|
||||
"max_output_tokens": "Maksimum Çıktı Token"
|
||||
}
|
||||
},
|
||||
"generic_openai": {
|
||||
"title": "Genel OpenAI Yapılandırması",
|
||||
"data": {
|
||||
"generic_openai_api_endpoint": "Genel OpenAI API Tam URL (sonunda 'completions' olmalı)",
|
||||
"generic_openai_api_key": "Genel OpenAI API Anahtarı (İsteğe Bağlı)",
|
||||
"generic_openai_model": "Genel OpenAI Modeli",
|
||||
"generic_openai_temperature": "Sıcaklık",
|
||||
"generic_openai_validation_endpoint": "Doğrulama URL'si (sonunda 'models' olmalı)",
|
||||
"generic_openai_enable_validation": "Doğrulamayı Etkinleştir",
|
||||
"max_input_tokens": "Maksimum Girdi Token",
|
||||
"max_output_tokens": "Maksimum Çıktı Token"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"api_error": "API doğrulaması başarısız oldu: {error_message}",
|
||||
"cannot_connect": "Bağlantı kurulamadı. Lütfen ayarlarınızı ve ağ bağlantınızı kontrol edin.",
|
||||
"unknown": "Bilinmeyen bir hata oluştu. Lütfen ayrıntılar için günlükleri kontrol edin."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Bu AI sağlayıcı zaten yapılandırılmış. Entegrasyonlar sayfasından düzenleyebilirsiniz."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "AI Otomasyon Önericisi Seçenekleri",
|
||||
"data": {
|
||||
"openai_api_key": "OpenAI API Anahtarı",
|
||||
"openai_model": "OpenAI Modeli",
|
||||
"openai_temperature": "Sıcaklık (OpenAI)",
|
||||
"anthropic_api_key": "Anthropic API Anahtarı",
|
||||
"anthropic_model": "Anthropic Modeli",
|
||||
"anthropic_temperature": "Sıcaklık (Anthropic)",
|
||||
"google_api_key": "Google API Anahtarı",
|
||||
"google_model": "Google Modeli",
|
||||
"google_temperature": "Sıcaklık (Google)",
|
||||
"groq_api_key": "Groq API Anahtarı",
|
||||
"groq_model": "Groq Modeli",
|
||||
"groq_temperature": "Sıcaklık (Groq)",
|
||||
"localai_ip": "LocalAI IP Adresi",
|
||||
"localai_port": "LocalAI Port",
|
||||
"localai_https": "LocalAI için HTTPS Kullan",
|
||||
"localai_model": "LocalAI Modeli",
|
||||
"localai_temperature": "Sıcaklık (LocalAI)",
|
||||
"ollama_ip": "Ollama IP Adresi",
|
||||
"ollama_port": "Ollama Port",
|
||||
"ollama_https": "Ollama için HTTPS Kullan",
|
||||
"ollama_model": "Ollama Modeli",
|
||||
"ollama_temperature": "Sıcaklık (Ollama)",
|
||||
"ollama_disable_think": "Düşünme Modunu Devre Dışı Bırak (Ollama)",
|
||||
"custom_openai_endpoint": "Özel OpenAI Uç Noktası",
|
||||
"custom_openai_api_key": "Özel OpenAI API Anahtarı",
|
||||
"custom_openai_model": "Özel OpenAI Modeli",
|
||||
"custom_openai_temperature": "Sıcaklık (Özel OpenAI)",
|
||||
"mistral_api_key": "Mistral API Anahtarı",
|
||||
"mistral_model": "Mistral Modeli",
|
||||
"mistral_temperature": "Sıcaklık (Mistral AI)",
|
||||
"perplexity_api_key": "Perplexity API Anahtarı",
|
||||
"perplexity_model": "Perplexity Modeli",
|
||||
"perplexity_temperature": "Sıcaklık (Perplexity AI)",
|
||||
"openrouter_api_key": "OpenRouter API Anahtarı",
|
||||
"openrouter_model": "OpenRouter Modeli",
|
||||
"openrouter_reasoning_max_tokens": "OpenRouter Akıl Yürütme Maks Token",
|
||||
"openrouter_temperature": "Sıcaklık (OpenRouter)",
|
||||
"openai_azure_api_key": "Azure OpenAI API Anahtarı",
|
||||
"openai_azure_deployment_id": "Azure Dağıtım Kimliği",
|
||||
"openai_azure_endpoint": "Azure Uç Noktası",
|
||||
"openai_azure_api_version": "Azure API Sürümü",
|
||||
"openai_azure_temperature": "Sıcaklık (OpenAI Azure)",
|
||||
"generic_openai_api_endpoint": "Genel OpenAI API Tam URL (sonunda 'completions' olmalı)",
|
||||
"generic_openai_api_key": "Genel OpenAI API Anahtarı",
|
||||
"generic_openai_model": "Genel OpenAI Modeli",
|
||||
"generic_openai_temperature": "Sıcaklık (Genel OpenAI)",
|
||||
"generic_openai_validation_endpoint": "Doğrulama URL'si (Genel OpenAI, sonunda 'models' olmalı)",
|
||||
"generic_openai_enable_validation": "Doğrulamayı Etkinleştir (Genel OpenAI)",
|
||||
"max_input_tokens": "Maksimum Girdi Token",
|
||||
"max_output_tokens": "Maksimum Çıktı Token"
|
||||
},
|
||||
"description": "AI sağlayıcılarınız için ayarları düzenleyin. Yapılandırılmış sağlayıcınızla ilgili alanlar kullanılacaktır. Token limitleri gibi ortak ayarlar her sağlayıcı için ayrı ayrı yapılandırılır."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_input": "Bir veya daha fazla değer geçersiz."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"generate_suggestions": {
|
||||
"name": "Öneri Oluştur",
|
||||
"description": "AI otomasyon önerilerini manuel olarak tetikleyin.",
|
||||
"fields": {
|
||||
"provider_config": {
|
||||
"name": "Sağlayıcı Yapılandırması",
|
||||
"description": "Hangi sağlayıcı yapılandırmasının kullanılacağı (birden fazla varsa)"
|
||||
},
|
||||
"custom_prompt": {
|
||||
"name": "Özel İstem",
|
||||
"description": "Varsayılan sistem istemini geçersiz kılmak veya önerileri belirli temalara yönlendirmek için isteğe bağlı özel istem"
|
||||
},
|
||||
"all_entities": {
|
||||
"name": "Tüm Varlıkları Değerlendir",
|
||||
"description": "Doğruysa, sadece yeni varlıklar yerine tüm varlıkları değerlendirir."
|
||||
},
|
||||
"domains": {
|
||||
"name": "Alan Adları",
|
||||
"description": "Değerlendirilecek alan adlarının listesi. Boşsa, tüm alan adları değerlendirilir."
|
||||
},
|
||||
"entity_limit": {
|
||||
"name": "Varlık Limiti",
|
||||
"description": "Değerlendirilecek maksimum varlık sayısı (rastgele seçilir)."
|
||||
},
|
||||
"automation_read_yaml": {
|
||||
"name": "'automations.yaml' dosyasını oku",
|
||||
"description": "'automations.yaml' dosyasında bulunan otomasyonların YAML kodunu okur ve ekler. Bu işlem çok fazla girdi token'ı kullanır, büyük bağlam penceresine sahip modellerle (örn. Gemini) dikkatli kullanın."
|
||||
},
|
||||
"automation_limit": {
|
||||
"name": "Otomasyon Limiti",
|
||||
"description": "Analiz edilecek maksimum otomasyon sayısı (varsayılan: 100)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
{
|
||||
"title": "AI Automation Suggester",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "选择 AI 提供商",
|
||||
"data": {
|
||||
"provider": "AI 提供商"
|
||||
}
|
||||
},
|
||||
"openai": {
|
||||
"title": "配置 OpenAI",
|
||||
"data": {
|
||||
"openai_api_key": "OpenAI API 密钥",
|
||||
"openai_model": "OpenAI 模型",
|
||||
"openai_temperature": "温度",
|
||||
"max_input_tokens": "最大输入令牌",
|
||||
"max_output_tokens": "最大输出令牌"
|
||||
}
|
||||
},
|
||||
"anthropic": {
|
||||
"title": "配置 Anthropic",
|
||||
"data": {
|
||||
"anthropic_api_key": "Anthropic API 密钥",
|
||||
"anthropic_model": "Anthropic 模型",
|
||||
"anthropic_temperature": "温度",
|
||||
"max_input_tokens": "最大输入令牌",
|
||||
"max_output_tokens": "最大输出令牌"
|
||||
}
|
||||
},
|
||||
"google": {
|
||||
"title": "配置 Google",
|
||||
"data": {
|
||||
"google_api_key": "Google API 密钥",
|
||||
"google_model": "Google 模型",
|
||||
"google_temperature": "温度",
|
||||
"max_input_tokens": "最大输入令牌",
|
||||
"max_output_tokens": "最大输出令牌"
|
||||
}
|
||||
},
|
||||
"groq": {
|
||||
"title": "配置 Groq",
|
||||
"data": {
|
||||
"groq_api_key": "Groq API 密钥",
|
||||
"groq_model": "Groq 模型",
|
||||
"groq_temperature": "温度",
|
||||
"max_input_tokens": "最大输入令牌",
|
||||
"max_output_tokens": "最大输出令牌"
|
||||
}
|
||||
},
|
||||
"localai": {
|
||||
"title": "配置 LocalAI",
|
||||
"data": {
|
||||
"localai_ip": "LocalAI IP 地址",
|
||||
"localai_port": "LocalAI 端口",
|
||||
"localai_https": "为 LocalAI 使用 HTTPS",
|
||||
"localai_model": "LocalAI 模型",
|
||||
"localai_temperature": "温度",
|
||||
"max_input_tokens": "最大输入令牌",
|
||||
"max_output_tokens": "最大输出令牌"
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"title": "配置 Ollama",
|
||||
"data": {
|
||||
"ollama_ip": "Ollama IP 地址",
|
||||
"ollama_port": "Ollama 端口",
|
||||
"ollama_https": "为 Ollama 使用 HTTPS",
|
||||
"ollama_model": "Ollama 模型",
|
||||
"ollama_temperature": "温度",
|
||||
"ollama_disable_think": "禁用思考模式 (Ollama)",
|
||||
"max_input_tokens": "最大输入令牌",
|
||||
"max_output_tokens": "最大输出令牌"
|
||||
}
|
||||
},
|
||||
"custom_openai": {
|
||||
"title": "配置自定义 OpenAI",
|
||||
"data": {
|
||||
"custom_openai_endpoint": "自定义 OpenAI 端点",
|
||||
"custom_openai_api_key": "自定义 OpenAI API 密钥 (可选)",
|
||||
"custom_openai_model": "自定义 OpenAI 模型",
|
||||
"custom_openai_temperature": "温度",
|
||||
"max_input_tokens": "最大输入令牌",
|
||||
"max_output_tokens": "最大输出令牌"
|
||||
}
|
||||
},
|
||||
"mistral": {
|
||||
"title": "配置 Mistral AI",
|
||||
"data": {
|
||||
"mistral_api_key": "Mistral API 密钥",
|
||||
"mistral_model": "Mistral 模型",
|
||||
"mistral_temperature": "温度",
|
||||
"max_input_tokens": "最大输入令牌",
|
||||
"max_output_tokens": "最大输出令牌"
|
||||
}
|
||||
},
|
||||
"perplexity": {
|
||||
"title": "配置 Perplexity AI",
|
||||
"data": {
|
||||
"perplexity_api_key": "Perplexity API 密钥",
|
||||
"perplexity_model": "Perplexity 模型",
|
||||
"perplexity_temperature": "温度",
|
||||
"max_input_tokens": "最大输入令牌",
|
||||
"max_output_tokens": "最大输出令牌"
|
||||
}
|
||||
},
|
||||
"openrouter": {
|
||||
"title": "配置 OpenRouter",
|
||||
"data": {
|
||||
"openrouter_api_key": "OpenRouter API 密钥",
|
||||
"openrouter_model": "OpenRouter 模型",
|
||||
"openrouter_reasoning_max_tokens": "OpenRouter 推理最大令牌数",
|
||||
"openrouter_temperature": "温度",
|
||||
"max_input_tokens": "最大输入令牌",
|
||||
"max_output_tokens": "最大输出令牌"
|
||||
}
|
||||
},
|
||||
"openai_azure": {
|
||||
"title": "配置 OpenAI Azure",
|
||||
"data": {
|
||||
"openai_azure_api_key": "Azure OpenAI API 密钥",
|
||||
"openai_azure_deployment_id": "Azure 部署 ID",
|
||||
"openai_azure_endpoint": "Azure 端点",
|
||||
"openai_azure_api_version": "Azure API 版本",
|
||||
"openai_azure_temperature": "温度",
|
||||
"max_input_tokens": "最大输入令牌",
|
||||
"max_output_tokens": "最大输出令牌"
|
||||
}
|
||||
},
|
||||
"generic_openai": {
|
||||
"title": "配置通用 OpenAI",
|
||||
"data": {
|
||||
"generic_openai_api_endpoint": "通用 OpenAI 完整 API URL (应以 'completions' 结尾)",
|
||||
"generic_openai_api_key": "通用 OpenAI API 密钥(可选)",
|
||||
"generic_openai_model": "通用 OpenAI 模型",
|
||||
"generic_openai_temperature": "温度",
|
||||
"generic_openai_validation_endpoint": "验证 URL (应以 'models' 结尾)",
|
||||
"generic_openai_enable_validation": "启用验证",
|
||||
"max_input_tokens": "最大输入令牌",
|
||||
"max_output_tokens": "最大输出令牌"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"api_error": "API 验证失败:{error_message}",
|
||||
"cannot_connect": "连接失败。请检查您的设置和网络。",
|
||||
"unknown": "发生未知错误。请检查日志以获取详细信息。"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "此 AI 提供商已配置。您可以从集成页面编辑它。"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "AI 自动化建议器选项",
|
||||
"data": {
|
||||
"openai_api_key": "OpenAI API 密钥",
|
||||
"openai_model": "OpenAI 模型",
|
||||
"openai_temperature": "温度 (OpenAI)",
|
||||
"anthropic_api_key": "Anthropic API 密钥",
|
||||
"anthropic_model": "Anthropic 模型",
|
||||
"anthropic_temperature": "温度 (Anthropic)",
|
||||
"google_api_key": "Google API 密钥",
|
||||
"google_model": "Google 模型",
|
||||
"google_temperature": "温度 (Google)",
|
||||
"groq_api_key": "Groq API 密钥",
|
||||
"groq_model": "Groq 模型",
|
||||
"groq_temperature": "温度 (Groq)",
|
||||
"localai_ip": "LocalAI IP 地址",
|
||||
"localai_port": "LocalAI 端口",
|
||||
"localai_https": "为 LocalAI 使用 HTTPS",
|
||||
"localai_model": "LocalAI 模型",
|
||||
"localai_temperature": "温度 (LocalAI)",
|
||||
"ollama_ip": "Ollama IP 地址",
|
||||
"ollama_port": "Ollama 端口",
|
||||
"ollama_https": "为 Ollama 使用 HTTPS",
|
||||
"ollama_model": "Ollama 模型",
|
||||
"ollama_temperature": "温度 (Ollama)",
|
||||
"ollama_disable_think": "禁用思考模式 (Ollama)",
|
||||
"custom_openai_endpoint": "自定义 OpenAI 端点",
|
||||
"custom_openai_api_key": "自定义 OpenAI API 密钥",
|
||||
"custom_openai_model": "自定义 OpenAI 模型",
|
||||
"custom_openai_temperature": "温度 (自定义 OpenAI)",
|
||||
"mistral_api_key": "Mistral API 密钥",
|
||||
"mistral_model": "Mistral 模型",
|
||||
"mistral_temperature": "温度 (Mistral AI)",
|
||||
"perplexity_api_key": "Perplexity API 密钥",
|
||||
"perplexity_model": "Perplexity 模型",
|
||||
"perplexity_temperature": "温度 (Perplexity AI)",
|
||||
"openrouter_api_key": "OpenRouter API 密钥",
|
||||
"openrouter_model": "OpenRouter 模型",
|
||||
"openrouter_reasoning_max_tokens": "OpenRouter 推理最大令牌数",
|
||||
"openrouter_temperature": "温度 (OpenRouter)",
|
||||
"openai_azure_api_key": "Azure OpenAI API 密钥",
|
||||
"openai_azure_deployment_id": "Azure 部署 ID",
|
||||
"openai_azure_endpoint": "Azure 端点",
|
||||
"openai_azure_api_version": "Azure API 版本",
|
||||
"openai_azure_temperature": "温度 (OpenAI Azure)",
|
||||
"generic_openai_api_endpoint": "通用 OpenAI API 完整 URL (应以 'completions' 结尾)",
|
||||
"generic_openai_api_key": "通用 OpenAI API 密钥",
|
||||
"generic_openai_model": "通用 OpenAI 模型",
|
||||
"generic_openai_temperature": "温度 (通用 OpenAI)",
|
||||
"generic_openai_validation_endpoint": "验证 URL (通用 OpenAI,应以 'models' 结尾)",
|
||||
"generic_openai_enable_validation": "启用验证 (通用 OpenAI)",
|
||||
"max_input_tokens": "最大输入令牌",
|
||||
"max_output_tokens": "最大输出令牌"
|
||||
},
|
||||
"description": "调整您的 AI 提供商设置。将使用与您配置的提供商相关的字段。令牌限制等通用设置是按提供商配置的。"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_input": "一个或多个值无效。"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"generate_suggestions": {
|
||||
"name": "生成建议",
|
||||
"description": "手动触发 AI 自动化建议。",
|
||||
"fields": {
|
||||
"provider_config": {
|
||||
"name": "提供商配置",
|
||||
"description": "选择要使用的提供商配置(如果有多个)"
|
||||
},
|
||||
"custom_prompt": {
|
||||
"name": "自定义提示",
|
||||
"description": "可选的自定义提示,用于覆盖默认系统提示或将建议引导至特定主题。"
|
||||
},
|
||||
"all_entities": {
|
||||
"name": "考虑所有实体",
|
||||
"description": "若为 true,则考虑所有实体而不仅仅是新实体。"
|
||||
},
|
||||
"domains": {
|
||||
"name": "领域",
|
||||
"description": "要考虑的领域列表。如果为空,则考虑所有领域。"
|
||||
},
|
||||
"entity_limit": {
|
||||
"name": "实体限制",
|
||||
"description": "要考虑的实体最大数量(随机选择)。"
|
||||
},
|
||||
"automation_read_yaml": {
|
||||
"name": "读取 'automations.yaml' 文件",
|
||||
"description": "读取并附加 'automations.yaml' 文件中找到的自动化的 YAML 代码。此操作将使用大量输入令牌,请谨慎使用,并配合具有较大上下文窗口的模型(例如 Gemini)。"
|
||||
},
|
||||
"automation_limit": {
|
||||
"name": "自动化数量上限",
|
||||
"description": "要分析的自动化最大数量(默认:100)。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
|
||||
class AiAutomationSuggesterCard extends LitElement {
|
||||
static properties = {
|
||||
hass: { attribute: false },
|
||||
suggestions: { state: true },
|
||||
loading: { state: true },
|
||||
error: { state: true },
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
padding: 16px;
|
||||
}
|
||||
.toolbar {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
border-top: 1px solid var(--divider-color, #ddd);
|
||||
padding: 12px 0;
|
||||
}
|
||||
li:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
pre {
|
||||
background: var(--code-editor-background-color, #f5f5f5);
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
padding: 12px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.meta {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color, #b00020);
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.suggestions = [];
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async fetchData() {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
try {
|
||||
this.suggestions = await this.hass.callApi("GET", "ai_automation_suggester/suggestions");
|
||||
} catch (err) {
|
||||
this.error = err.message || "Failed to fetch suggestions.";
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async copyYaml(yaml) {
|
||||
await navigator.clipboard.writeText(yaml || "");
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("hass-notification", {
|
||||
detail: { type: "info", message: "YAML copied to clipboard." },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async handleSuggestionAction(suggestionId, action) {
|
||||
try {
|
||||
const response = await this.hass.callApi(
|
||||
"POST",
|
||||
`ai_automation_suggester/${action}/${suggestionId}`,
|
||||
);
|
||||
if (response.success) {
|
||||
await this.fetchData();
|
||||
} else {
|
||||
this.error = response.error || `Failed to ${action} suggestion.`;
|
||||
}
|
||||
} catch (err) {
|
||||
this.error = err.message || `Failed to ${action} suggestion.`;
|
||||
}
|
||||
}
|
||||
|
||||
renderSuggestion(suggestion) {
|
||||
const yamlCode = suggestion.yamlCode || suggestion.yaml_block || "";
|
||||
return html`
|
||||
<li>
|
||||
<h3>${suggestion.title || "AI automation suggestion"}</h3>
|
||||
<p>${suggestion.shortDescription || suggestion.description || "No description returned."}</p>
|
||||
<p class="meta">
|
||||
${suggestion.provider || "Unknown provider"} - ${suggestion.model || "Unknown model"} -
|
||||
${suggestion.status || "new"}
|
||||
</p>
|
||||
${yamlCode ? html`<pre><code>${yamlCode}</code></pre>` : html`<p class="meta">No YAML was returned.</p>`}
|
||||
${(suggestion.warnings || []).length
|
||||
? html`<p class="meta">${suggestion.warnings.join(" ")}</p>`
|
||||
: ""}
|
||||
<div class="actions">
|
||||
<ha-button @click=${() => this.copyYaml(yamlCode)} .disabled=${!yamlCode}>Copy YAML</ha-button>
|
||||
<ha-button @click=${() => this.handleSuggestionAction(suggestion.id, "accept")}>Accept</ha-button>
|
||||
<ha-button @click=${() => this.handleSuggestionAction(suggestion.id, "decline")}>Decline</ha-button>
|
||||
<ha-button @click=${() => this.handleSuggestionAction(suggestion.id, "dismiss")}>Dismiss</ha-button>
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="toolbar">
|
||||
<h2>AI Automation Suggestions</h2>
|
||||
<ha-button @click=${this.fetchData}>Refresh</ha-button>
|
||||
</div>
|
||||
${this.loading
|
||||
? html`<p>Loading suggestions...</p>`
|
||||
: this.error
|
||||
? html`<p class="error">${this.error}</p>`
|
||||
: this.suggestions.length
|
||||
? html`<ul>${this.suggestions.map((suggestion) => this.renderSuggestion(suggestion))}</ul>`
|
||||
: html`<p>No stored suggestions yet.</p>`}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ai-automation-suggester-card", AiAutomationSuggesterCard);
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,261 @@
|
||||
"""
|
||||
Alexa Devices Alarm Control Panel using Guard Mode.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639
|
||||
"""
|
||||
|
||||
from asyncio import sleep
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from alexapy import hide_email, hide_serial
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity
|
||||
from homeassistant.const import CONF_EMAIL, STATE_UNAVAILABLE
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .alexa_entity import parse_guard_state_from_coordinator
|
||||
from .alexa_media import AlexaMedia
|
||||
from .const import (
|
||||
CONF_EXCLUDE_DEVICES,
|
||||
CONF_INCLUDE_DEVICES,
|
||||
CONF_QUEUE_DELAY,
|
||||
DATA_ALEXAMEDIA,
|
||||
DEFAULT_QUEUE_DELAY,
|
||||
DOMAIN as ALEXA_DOMAIN,
|
||||
)
|
||||
from .helpers import _catch_login_errors, add_devices, safe_get
|
||||
|
||||
try:
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanelState
|
||||
|
||||
STATE_ALARM_ARMED_AWAY = AlarmControlPanelState.ARMED_AWAY
|
||||
STATE_ALARM_DISARMED = AlarmControlPanelState.DISARMED
|
||||
except ImportError:
|
||||
from homeassistant.const import STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = [ALEXA_DOMAIN]
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, add_devices_callback, discovery_info=None
|
||||
) -> bool:
|
||||
"""Set up the Alexa alarm control panel platform."""
|
||||
devices: list[AlexaAlarmControlPanel] = []
|
||||
account = None
|
||||
if config:
|
||||
account = config.get(CONF_EMAIL)
|
||||
if account is None and discovery_info:
|
||||
account = safe_get(discovery_info, ["config", CONF_EMAIL])
|
||||
if account is None:
|
||||
raise ConfigEntryNotReady
|
||||
include_filter = config.get(CONF_INCLUDE_DEVICES, [])
|
||||
exclude_filter = config.get(CONF_EXCLUDE_DEVICES, [])
|
||||
account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account]
|
||||
guard_media_players = {}
|
||||
for key, device in account_dict["devices"]["media_player"].items():
|
||||
if key not in account_dict["entities"]["media_player"]:
|
||||
_LOGGER.debug(
|
||||
"%s: Media player %s not loaded yet; delaying load",
|
||||
hide_email(account),
|
||||
hide_serial(key),
|
||||
)
|
||||
raise ConfigEntryNotReady
|
||||
if "GUARD_EARCON" in device["capabilities"]:
|
||||
guard_media_players[key] = account_dict["entities"]["media_player"][key]
|
||||
if "alarm_control_panel" not in (account_dict["entities"]):
|
||||
(
|
||||
hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"][
|
||||
"alarm_control_panel"
|
||||
]
|
||||
) = {}
|
||||
alexa_client: Optional[AlexaAlarmControlPanel] = None
|
||||
guard_entities = safe_get(account_dict, ["devices", "guard"], [])
|
||||
if guard_entities:
|
||||
alexa_client = AlexaAlarmControlPanel(
|
||||
account_dict["login_obj"],
|
||||
account_dict["coordinator"],
|
||||
guard_entities[0],
|
||||
guard_media_players,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug("%s: No Alexa Guard entity found", hide_email(account))
|
||||
if not (alexa_client and alexa_client.unique_id):
|
||||
_LOGGER.debug(
|
||||
"%s: Skipping creation of uninitialized device: %s",
|
||||
hide_email(account),
|
||||
alexa_client,
|
||||
)
|
||||
elif alexa_client.unique_id not in (
|
||||
account_dict["entities"]["alarm_control_panel"]
|
||||
):
|
||||
devices.append(alexa_client)
|
||||
(
|
||||
hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"][
|
||||
"alarm_control_panel"
|
||||
][alexa_client.unique_id]
|
||||
) = alexa_client
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s: Skipping already added device: %s", hide_email(account), alexa_client
|
||||
)
|
||||
return await add_devices(
|
||||
hide_email(account),
|
||||
devices,
|
||||
add_devices_callback,
|
||||
include_filter,
|
||||
exclude_filter,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the Alexa alarm control panel platform by config_entry."""
|
||||
return await async_setup_platform(
|
||||
hass, config_entry.data, async_add_devices, discovery_info=None
|
||||
)
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
account = entry.data[CONF_EMAIL]
|
||||
_LOGGER.debug("Attempting to unload alarm control panel")
|
||||
account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account]
|
||||
for device in account_dict["entities"]["alarm_control_panel"].values():
|
||||
_LOGGER.debug("Removing %s", device)
|
||||
await device.async_remove()
|
||||
return True
|
||||
|
||||
|
||||
class AlexaAlarmControlPanel(AlarmControlPanelEntity, AlexaMedia, CoordinatorEntity):
|
||||
"""Implementation of Alexa Media Player alarm control panel."""
|
||||
|
||||
def __init__(self, login, coordinator, guard_entity, media_players=None) -> None:
|
||||
"""Initialize the Alexa device."""
|
||||
AlexaMedia.__init__(self, None, login)
|
||||
CoordinatorEntity.__init__(self, coordinator)
|
||||
_LOGGER.debug("%s: Initiating alarm control panel", hide_email(login.email))
|
||||
# AlexaAPI requires a AlexaClient object, need to clean this up
|
||||
|
||||
# Guard info
|
||||
self._appliance_id = guard_entity["appliance_id"]
|
||||
self._guard_entity_id = guard_entity["id"]
|
||||
self._friendly_name = "Alexa Guard " + self._appliance_id[-5:]
|
||||
self._media_players = {} or media_players
|
||||
self._attrs: dict[str, str] = {}
|
||||
_LOGGER.debug(
|
||||
"%s: Guard Discovered %s: %s %s",
|
||||
self.account,
|
||||
self._friendly_name,
|
||||
hide_serial(self._appliance_id),
|
||||
hide_serial(self._guard_entity_id),
|
||||
)
|
||||
|
||||
@_catch_login_errors
|
||||
async def _async_alarm_set(
|
||||
self,
|
||||
command: str = "",
|
||||
code=None, # pylint: disable=unused-argument
|
||||
) -> None:
|
||||
"""Send command."""
|
||||
try:
|
||||
if not self.enabled:
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
if command not in (STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED):
|
||||
_LOGGER.error("Invalid command: %s", command)
|
||||
return
|
||||
command_map = {STATE_ALARM_ARMED_AWAY: "AWAY", STATE_ALARM_DISARMED: "HOME"}
|
||||
available_media_players = list(
|
||||
filter(lambda x: x.state != STATE_UNAVAILABLE, self._media_players.values())
|
||||
)
|
||||
if available_media_players:
|
||||
_LOGGER.debug("Sending guard command to: %s", available_media_players[0])
|
||||
available_media_players[0].check_login_changes()
|
||||
# Extract appliance ID safely to prevent IndexError if format is unexpected
|
||||
appliance_parts = self._appliance_id.split("_")
|
||||
appliance_id = (
|
||||
appliance_parts[2] if len(appliance_parts) > 2 else self._appliance_id
|
||||
)
|
||||
await available_media_players[0].alexa_api.set_guard_state(
|
||||
appliance_id,
|
||||
command_map[command],
|
||||
queue_delay=self.hass.data[DATA_ALEXAMEDIA]["accounts"][self.email][
|
||||
"options"
|
||||
].get(CONF_QUEUE_DELAY, DEFAULT_QUEUE_DELAY),
|
||||
)
|
||||
await sleep(2) # delay
|
||||
else:
|
||||
_LOGGER.debug("Performing static guard command")
|
||||
await self.alexa_api.static_set_guard_state(
|
||||
self._login, self._guard_entity_id, command
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_alarm_disarm(
|
||||
self,
|
||||
code=None, # pylint:disable=unused-argument
|
||||
) -> None:
|
||||
"""Send disarm command."""
|
||||
await self._async_alarm_set(STATE_ALARM_DISARMED)
|
||||
|
||||
async def async_alarm_arm_away(
|
||||
self,
|
||||
code=None, # pylint:disable=unused-argument
|
||||
) -> None:
|
||||
"""Send arm away command."""
|
||||
await self._async_alarm_set(STATE_ALARM_ARMED_AWAY)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID."""
|
||||
return self._guard_entity_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._friendly_name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
_state = parse_guard_state_from_coordinator(
|
||||
self.coordinator, self._guard_entity_id
|
||||
)
|
||||
if _state == "ARMED_AWAY":
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
return STATE_ALARM_DISARMED
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Return the list of supported features."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
try:
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntityFeature,
|
||||
)
|
||||
except ImportError:
|
||||
return 0
|
||||
return AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
|
||||
@property
|
||||
def assumed_state(self) -> bool:
|
||||
"""Return assumed state.
|
||||
|
||||
Returns
|
||||
bool: Whether the state is assumed
|
||||
|
||||
"""
|
||||
last_refresh_success = (
|
||||
self.coordinator.data and self._guard_entity_id in self.coordinator.data
|
||||
)
|
||||
return not last_refresh_success
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return self._attrs
|
||||
@@ -0,0 +1,764 @@
|
||||
"""
|
||||
Alexa Devices Entities.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Optional, TypedDict
|
||||
|
||||
from alexapy import AlexaAPI, AlexaLogin
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .helpers import safe_get
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# How long we keep "requested state" protected from stale coordinator values
|
||||
# when capabilityStates do not include a usable timeOfSample.
|
||||
_REQUESTED_STATE_TTL = timedelta(seconds=15)
|
||||
|
||||
|
||||
def has_capability(
|
||||
appliance: dict[str, Any], interface_name: str, property_name: str
|
||||
) -> bool:
|
||||
"""Determine if an appliance from the Alexa network details offers a particular interface with enough support that is worth adding to Home Assistant.
|
||||
|
||||
Args:
|
||||
appliance(dict[str, Any]): An appliance from a call to AlexaAPI.get_network_details
|
||||
interface_name(str): One of the interfaces documented by the Alexa Smart Home Skills API
|
||||
property_name(str): The property that matches the interface name.
|
||||
|
||||
"""
|
||||
for cap in appliance["capabilities"]:
|
||||
props = cap.get("properties")
|
||||
if (
|
||||
cap["interfaceName"] == interface_name
|
||||
and props
|
||||
and (props["retrievable"] or props["proactivelyReported"])
|
||||
):
|
||||
for prop in props["supported"]:
|
||||
if prop["name"] == property_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_hue_v1(appliance: dict[str, Any]) -> bool:
|
||||
"""Determine if an appliance is managed via the Philips Hue v1 Hub.
|
||||
|
||||
This check catches old Philips Hue bulbs and hubs, but critically, it also catches things pretending to be older
|
||||
Philips Hue bulbs and hubs. This includes things exposed by HA to Alexa using the emulated_hue integration.
|
||||
"""
|
||||
return appliance.get("manufacturerName") == "Royal Philips Electronics"
|
||||
|
||||
|
||||
def is_skill(appliance: dict[str, Any]) -> bool:
|
||||
namespace = safe_get(appliance, ["driverIdentity", "namespace"], "")
|
||||
return namespace and namespace == "SKILL"
|
||||
|
||||
|
||||
def is_known_ha_bridge(appliance: dict[str, Any] | None) -> bool:
|
||||
"""Test whether a bridge appliance is a known HA bridge to avoid creating loops."""
|
||||
|
||||
if appliance is None:
|
||||
return False
|
||||
|
||||
if appliance.get("manufacturerName") in ("t0bst4r", "Matterbridge"):
|
||||
return True
|
||||
|
||||
# Identify Matter bridge hubs regardless of manufacturerName
|
||||
if "HUB" in appliance.get("applianceTypes", []):
|
||||
driver_ns = safe_get(appliance, ["driverIdentity", "namespace"], "")
|
||||
driver_id = safe_get(appliance, ["driverIdentity", "identifier"], "")
|
||||
if driver_ns == "AAA" and driver_id == "SonarCloudService":
|
||||
interfaces = {
|
||||
cap.get("interfaceName") for cap in appliance.get("capabilities", [])
|
||||
}
|
||||
if (
|
||||
"Alexa.Matter.NodeOperationalCredentials.FabricManagement" in interfaces
|
||||
or "Alexa.Commissionable" in interfaces
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_local(appliance: dict[str, Any]) -> bool:
|
||||
"""Test whether locally connected.
|
||||
|
||||
This is mainly present to prevent loops with the official Alexa integration.
|
||||
There is probably a better way to prevent that, but this works.
|
||||
"""
|
||||
|
||||
if appliance.get("connectedVia"):
|
||||
# connectedVia is a flag that determines which Echo devices holds the connection. Its blank for
|
||||
# skill derived devices and includes an Echo name for zigbee and local devices.
|
||||
return True
|
||||
|
||||
# This catches the Echo/AVS devices. connectedVia isn't reliable in this case.
|
||||
# Only the first appears to get that set.
|
||||
if "ALEXA_VOICE_ENABLED" in appliance.get("applianceTypes", []):
|
||||
return not is_skill(appliance)
|
||||
|
||||
# Ledvance/Sengled bulbs connected via bluetooth are hard to detect as locally connected
|
||||
# Amazon devices are not local but bypassing the local check allows for control by the integration
|
||||
# There is probably a better way, but this works for now.
|
||||
manufacturerNames = ["Ledvance", "Sengled", "Amazon"]
|
||||
if appliance.get("manufacturerName") in manufacturerNames:
|
||||
return not is_skill(appliance)
|
||||
|
||||
# Zigbee devices are guaranteed to be local and have a particular pattern of id
|
||||
zigbee_pattern = re.compile(
|
||||
"AAA_SonarCloudService_([0-9A-F][0-9A-F]:){7}[0-9A-F][0-9A-F]", flags=re.I
|
||||
)
|
||||
return zigbee_pattern.fullmatch(appliance.get("applianceId", "")) is not None
|
||||
|
||||
|
||||
def is_alexa_guard(appliance: dict[str, Any]) -> bool:
|
||||
"""Is the given appliance the guard alarm system of an echo."""
|
||||
return appliance["modelName"] == "REDROCK_GUARD_PANEL" and has_capability(
|
||||
appliance, "Alexa.SecurityPanelController", "armState"
|
||||
)
|
||||
|
||||
|
||||
def is_temperature_sensor(appliance: dict[str, Any]) -> bool:
|
||||
"""Is the given appliance the temperature sensor of an Echo."""
|
||||
return (
|
||||
is_local(appliance)
|
||||
and has_capability(appliance, "Alexa.TemperatureSensor", "temperature")
|
||||
and appliance["friendlyDescription"] != "Amazon Indoor Air Quality Monitor"
|
||||
)
|
||||
|
||||
|
||||
# Checks if air quality sensor
|
||||
def is_air_quality_sensor(appliance: dict[str, Any]) -> bool:
|
||||
"""Is the given appliance the Amazon Indoor Air Quality Monitor (AIAQM)."""
|
||||
return (
|
||||
appliance.get("friendlyDescription") == "Amazon Indoor Air Quality Monitor"
|
||||
and "AIR_QUALITY_MONITOR" in appliance.get("applianceTypes", [])
|
||||
and has_capability(appliance, "Alexa.RangeController", "rangeValue")
|
||||
)
|
||||
|
||||
|
||||
def is_light(appliance: dict[str, Any]) -> bool:
|
||||
"""Is the given appliance a light controlled locally by an Echo."""
|
||||
return (
|
||||
is_local(appliance)
|
||||
and (
|
||||
"LIGHT" in appliance.get("applianceTypes", [])
|
||||
or (
|
||||
"SMARTPLUG" in appliance.get("applianceTypes", [])
|
||||
and appliance.get("customerDefinedDeviceType") == "LIGHT"
|
||||
)
|
||||
)
|
||||
and has_capability(appliance, "Alexa.PowerController", "powerState")
|
||||
)
|
||||
|
||||
|
||||
def is_contact_sensor(appliance: dict[str, Any]) -> bool:
|
||||
"""Is the given appliance a contact sensor controlled locally by an Echo."""
|
||||
return (
|
||||
is_local(appliance)
|
||||
and "CONTACT_SENSOR" in appliance.get("applianceTypes", [])
|
||||
and has_capability(appliance, "Alexa.ContactSensor", "detectionState")
|
||||
)
|
||||
|
||||
|
||||
def is_switch(appliance: dict[str, Any]) -> bool:
|
||||
"""Is the given appliance a switch controlled locally by an Echo, which is not redeclared as a light."""
|
||||
return (
|
||||
is_local(appliance)
|
||||
and (
|
||||
"SMARTPLUG" in appliance.get("applianceTypes", [])
|
||||
or "SWITCH" in appliance.get("applianceTypes", [])
|
||||
)
|
||||
and appliance.get("customerDefinedDeviceType") != "LIGHT"
|
||||
and has_capability(appliance, "Alexa.PowerController", "powerState")
|
||||
)
|
||||
|
||||
|
||||
def get_friendliest_name(appliance: dict[str, Any]) -> str:
|
||||
"""Find the best friendly name. Alexa seems to store manual renames in aliases. Prefer that one."""
|
||||
aliases = appliance.get("aliases", [])
|
||||
for alias in aliases:
|
||||
friendly = alias.get("friendlyName")
|
||||
if friendly:
|
||||
return friendly
|
||||
return appliance["friendlyName"]
|
||||
|
||||
|
||||
def get_device_serial(appliance: dict[str, Any]) -> str | None:
|
||||
"""Find the device serial id if it is present."""
|
||||
alexa_device_id_list = appliance.get("alexaDeviceIdentifierList", [])
|
||||
for alexa_device_id in alexa_device_id_list:
|
||||
if isinstance(alexa_device_id, dict):
|
||||
return alexa_device_id.get("dmsDeviceSerialNumber")
|
||||
return None
|
||||
|
||||
|
||||
def get_device_bridge(
|
||||
appliance: dict[str, Any], appliances: dict[str, dict[str, Any]]
|
||||
) -> dict[str, Any] | None:
|
||||
"""Find the bridge device for an appliance connected through e.g. a Matter bridge."""
|
||||
|
||||
appliance_id = appliance.get("applianceId")
|
||||
if not isinstance(appliance_id, str) or "#" not in appliance_id:
|
||||
return None
|
||||
|
||||
# HA Matter Hub bridged endpoints are identified by applianceId prefixes
|
||||
# of the form AAA_SonarCloudService_<bridgeId>#<childId>.
|
||||
bridge_id, _sep, _child = appliance_id.partition("#")
|
||||
|
||||
if not bridge_id.startswith("AAA_SonarCloudService_"):
|
||||
return None
|
||||
|
||||
bridge = appliances.get(bridge_id)
|
||||
return bridge if isinstance(bridge, dict) else None
|
||||
|
||||
|
||||
AlexaEntityData = dict[str, list["AlexaCapabilityState"]]
|
||||
|
||||
|
||||
class AlexaEntity(TypedDict):
|
||||
"""Class for Alexaentity."""
|
||||
|
||||
id: str
|
||||
appliance_id: str
|
||||
name: str
|
||||
is_hue_v1: bool
|
||||
|
||||
|
||||
class AlexaLightEntity(AlexaEntity):
|
||||
"""Class for AlexaLightEntity."""
|
||||
|
||||
brightness: bool
|
||||
color: bool
|
||||
color_temperature: bool
|
||||
|
||||
|
||||
class AlexaTemperatureEntity(TypedDict, total=False):
|
||||
device_serial: str
|
||||
is_aiaqm: bool
|
||||
|
||||
|
||||
class AlexaAirQualityEntity(AlexaEntity):
|
||||
"""Class for AlexaAirQualityEntity."""
|
||||
|
||||
device_serial: str
|
||||
|
||||
|
||||
class AlexaAIAQMEntity(AlexaEntity):
|
||||
"""Entity-backed "device" representing an Amazon Indoor Air Quality Monitor."""
|
||||
|
||||
device_serial: str
|
||||
sensors: list[dict[str, str]]
|
||||
|
||||
|
||||
class AlexaBinaryEntity(AlexaEntity):
|
||||
"""Class for AlexaBinaryEntity."""
|
||||
|
||||
battery_level: bool
|
||||
|
||||
|
||||
class AlexaEntities(TypedDict):
|
||||
"""Class for Alexa Entities."""
|
||||
|
||||
light: list[AlexaLightEntity]
|
||||
guard: list[AlexaEntity]
|
||||
temperature: list[AlexaTemperatureEntity]
|
||||
air_quality: list[AlexaAirQualityEntity]
|
||||
aiaqm: list[AlexaAIAQMEntity]
|
||||
binary_sensor: list[AlexaBinaryEntity]
|
||||
smart_switch: list[AlexaEntity]
|
||||
|
||||
|
||||
class AlexaCapabilityState(TypedDict, total=False):
|
||||
"""Class for AlexaCapabilityState."""
|
||||
|
||||
name: str
|
||||
namespace: str
|
||||
value: int | float | str | dict[str, Any]
|
||||
instance: str
|
||||
timeOfSample: str
|
||||
uncertaintyInMilliseconds: int
|
||||
|
||||
|
||||
def parse_alexa_entities(
|
||||
network_details: list[dict[str, Any]] | None,
|
||||
debug: bool = False,
|
||||
) -> AlexaEntities:
|
||||
# pylint: disable=too-many-locals
|
||||
"""Turn the network details into a list of useful entities with the important details extracted."""
|
||||
temperature_sensors: list[AlexaTemperatureEntity] = []
|
||||
air_quality_sensors: list[AlexaAirQualityEntity] = []
|
||||
aiaqm_entities: list[AlexaAIAQMEntity] = []
|
||||
contact_sensors: list[AlexaBinaryEntity] = []
|
||||
switches: list[AlexaEntity] = []
|
||||
guards: list[AlexaEntity] = []
|
||||
lights: list[AlexaLightEntity] = []
|
||||
|
||||
function_name = "parse_alexa_entities()"
|
||||
|
||||
if not network_details:
|
||||
return {
|
||||
"light": lights,
|
||||
"guard": guards,
|
||||
"temperature": temperature_sensors,
|
||||
"air_quality": air_quality_sensors,
|
||||
"aiaqm": aiaqm_entities,
|
||||
"binary_sensor": contact_sensors,
|
||||
"smart_switch": switches,
|
||||
}
|
||||
|
||||
network_dict: dict[str, dict[str, Any]] = {}
|
||||
if debug:
|
||||
_LOGGER.debug("Processing network_details")
|
||||
|
||||
# Build an applianceId → appliance map first so bridged devices
|
||||
# can resolve their bridge regardless of list ordering.
|
||||
for appliance in network_details:
|
||||
appliance_id = appliance.get("applianceId")
|
||||
if appliance_id:
|
||||
network_dict[appliance_id] = appliance
|
||||
|
||||
for appliance in network_details:
|
||||
device_bridge = get_device_bridge(appliance, network_dict)
|
||||
|
||||
bridge_label = (
|
||||
device_bridge.get("friendlyName") or device_bridge.get("manufacturerName")
|
||||
if device_bridge
|
||||
else None
|
||||
)
|
||||
|
||||
appliance_id = str(appliance.get("applianceId", ""))
|
||||
|
||||
# Only log a bridge check when:
|
||||
# - we found a bridge, OR
|
||||
# - ADV debug is enabled AND the appliance looks like a bridge candidate
|
||||
if bridge_label is not None or (debug and "#" in appliance_id):
|
||||
_LOGGER.debug(
|
||||
"%s: Checking device bridge: %s",
|
||||
appliance.get("friendlyName"),
|
||||
bridge_label or "<none>",
|
||||
)
|
||||
|
||||
# ADV-only: only log resolution for cases where it might apply
|
||||
if debug and "#" in appliance_id:
|
||||
bridge_id = device_bridge.get("applianceId") if device_bridge else None
|
||||
_LOGGER.debug(
|
||||
"[%s] [ADV] Matter bridge resolution: appliance=%s → bridge=%s (connectedVia=%s, bridge=%s)",
|
||||
function_name,
|
||||
appliance_id,
|
||||
bridge_id,
|
||||
appliance.get("connectedVia"),
|
||||
bridge_label,
|
||||
)
|
||||
|
||||
if is_known_ha_bridge(device_bridge):
|
||||
if debug:
|
||||
_LOGGER.debug(
|
||||
'[%s] [ADV] Skipping bridged Matter device "%s" (%s) via known bridge: %s (%s)',
|
||||
function_name,
|
||||
appliance.get("friendlyName"),
|
||||
appliance.get("applianceId"),
|
||||
bridge_label,
|
||||
device_bridge.get("applianceId") if device_bridge else None,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
'Skipping bridged Matter device "%s" via known bridge "%s"',
|
||||
appliance.get("friendlyName"),
|
||||
bridge_label or "<unknown>",
|
||||
)
|
||||
continue
|
||||
|
||||
processed_appliance: AlexaEntity = {
|
||||
"id": appliance["entityId"],
|
||||
"appliance_id": appliance["applianceId"],
|
||||
"name": get_friendliest_name(appliance),
|
||||
"is_hue_v1": is_hue_v1(appliance),
|
||||
}
|
||||
|
||||
if is_alexa_guard(appliance):
|
||||
_LOGGER.debug("Added Alexa Guard: %s", processed_appliance["name"])
|
||||
guards.append(processed_appliance)
|
||||
|
||||
elif is_temperature_sensor(appliance):
|
||||
if debug:
|
||||
_LOGGER.debug(
|
||||
"Added temperature sensor: %s", processed_appliance["name"]
|
||||
)
|
||||
serial = get_device_serial(appliance)
|
||||
temp_entity: AlexaTemperatureEntity = {
|
||||
**processed_appliance,
|
||||
"device_serial": serial if serial else appliance["entityId"],
|
||||
}
|
||||
temperature_sensors.append(temp_entity)
|
||||
|
||||
elif is_air_quality_sensor(appliance):
|
||||
if debug:
|
||||
_LOGGER.debug("Added AIAQM sensor: %s", processed_appliance["name"])
|
||||
|
||||
serial = get_device_serial(appliance)
|
||||
device_serial = serial if serial else appliance["entityId"]
|
||||
|
||||
# Build a list of sub-sensors we can read via AlexaAPI.get_entity_state.
|
||||
# AIAQM metrics are exposed via Alexa.RangeController(rangeValue) with an
|
||||
# instance per metric. Some accounts/devices use numeric instances, so
|
||||
# we derive the sensor type from the friendlyName assetId/text.
|
||||
sensors: list[dict[str, str]] = []
|
||||
for cap in appliance.get("capabilities", []):
|
||||
if cap.get("interfaceName") != "Alexa.RangeController":
|
||||
continue
|
||||
|
||||
# Must support numeric rangeValue to be a sensor.
|
||||
supported = safe_get(cap, ["properties", "supported"], [])
|
||||
if not isinstance(supported, list) or not any(
|
||||
isinstance(p, dict) and p.get("name") == "rangeValue"
|
||||
for p in supported
|
||||
):
|
||||
continue
|
||||
|
||||
instance = cap.get("instance")
|
||||
if instance is None or instance == "":
|
||||
continue
|
||||
if not isinstance(instance, str):
|
||||
if isinstance(instance, (int, float)):
|
||||
instance = str(instance)
|
||||
else:
|
||||
continue
|
||||
|
||||
unit = safe_get(cap, ["configuration", "unitOfMeasure"], "") or ""
|
||||
|
||||
resources = (
|
||||
cap.get("resources", {})
|
||||
if isinstance(cap.get("resources"), dict)
|
||||
else {}
|
||||
)
|
||||
friendly = (
|
||||
resources.get("friendlyNames", [])
|
||||
if isinstance(resources.get("friendlyNames"), list)
|
||||
else []
|
||||
)
|
||||
|
||||
sensor_type: str | None = None
|
||||
for entry in friendly:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
value_obj = entry.get("value")
|
||||
asset_id = None
|
||||
if isinstance(value_obj, dict):
|
||||
asset_id = value_obj.get("assetId")
|
||||
else:
|
||||
asset_id = entry.get("assetId")
|
||||
|
||||
# Only treat Alexa.AirQuality assetIds as real AIAQM sensors.
|
||||
# Text-only friendlyNames (e.g. @type "text") must be ignored to avoid
|
||||
# creating extra sensors such as PM10.
|
||||
if isinstance(asset_id, str) and asset_id.startswith(
|
||||
"Alexa.AirQuality."
|
||||
):
|
||||
sensor_type = asset_id
|
||||
break
|
||||
|
||||
if not sensor_type:
|
||||
continue
|
||||
sensors.append(
|
||||
{
|
||||
"sensorType": str(sensor_type),
|
||||
"instance": instance,
|
||||
"unit": str(unit),
|
||||
}
|
||||
)
|
||||
|
||||
# Always register the AIAQM device (even if no sub-sensors are exposed).
|
||||
aiaqm_entity: AlexaAIAQMEntity = {
|
||||
**processed_appliance,
|
||||
"device_serial": device_serial,
|
||||
"sensors": sensors,
|
||||
}
|
||||
aiaqm_entities.append(aiaqm_entity)
|
||||
|
||||
# Backwards compatibility: also expose as air_quality for existing paths.
|
||||
aq_entity: AlexaAirQualityEntity = {
|
||||
**processed_appliance,
|
||||
"device_serial": device_serial,
|
||||
}
|
||||
air_quality_sensors.append(aq_entity)
|
||||
|
||||
# AIAQM also has temperature; ensure it gets created and grouped with AIAQM.
|
||||
temp_entity: AlexaTemperatureEntity = {
|
||||
**processed_appliance,
|
||||
"device_serial": device_serial,
|
||||
"is_aiaqm": True,
|
||||
}
|
||||
temperature_sensors.append(temp_entity)
|
||||
elif is_switch(appliance):
|
||||
if debug:
|
||||
_LOGGER.debug("Added switch: %s", processed_appliance["name"])
|
||||
switches.append(processed_appliance)
|
||||
|
||||
elif is_light(appliance):
|
||||
if debug:
|
||||
_LOGGER.debug("Added light %s", processed_appliance["name"])
|
||||
processed_appliance["brightness"] = has_capability(
|
||||
appliance, "Alexa.BrightnessController", "brightness"
|
||||
)
|
||||
processed_appliance["color"] = has_capability(
|
||||
appliance, "Alexa.ColorController", "color"
|
||||
)
|
||||
processed_appliance["color_temperature"] = has_capability(
|
||||
appliance,
|
||||
"Alexa.ColorTemperatureController",
|
||||
"colorTemperatureInKelvin",
|
||||
)
|
||||
light_entity: AlexaLightEntity = {
|
||||
**processed_appliance,
|
||||
"brightness": processed_appliance["brightness"],
|
||||
"color": processed_appliance["color"],
|
||||
"color_temperature": processed_appliance["color_temperature"],
|
||||
}
|
||||
lights.append(light_entity)
|
||||
|
||||
elif is_contact_sensor(appliance):
|
||||
if debug:
|
||||
_LOGGER.debug("Added contact sensor: %s", processed_appliance["name"])
|
||||
processed_appliance["battery_level"] = has_capability(
|
||||
appliance, "Alexa.BatteryLevelSensor", "batteryLevel"
|
||||
)
|
||||
binary_entity: AlexaBinaryEntity = {
|
||||
**processed_appliance,
|
||||
"battery_level": processed_appliance["battery_level"],
|
||||
}
|
||||
contact_sensors.append(binary_entity)
|
||||
|
||||
else:
|
||||
if debug:
|
||||
_LOGGER.debug("Unsupported entity: %s", processed_appliance["name"])
|
||||
|
||||
return {
|
||||
"light": lights,
|
||||
"guard": guards,
|
||||
"temperature": temperature_sensors,
|
||||
"air_quality": air_quality_sensors,
|
||||
"aiaqm": aiaqm_entities,
|
||||
"binary_sensor": contact_sensors,
|
||||
"smart_switch": switches,
|
||||
}
|
||||
|
||||
|
||||
async def get_entity_data(
|
||||
login_obj: AlexaLogin, entity_ids: list[str]
|
||||
) -> AlexaEntityData:
|
||||
"""Get and process the entity data into a more usable format."""
|
||||
|
||||
entities = {}
|
||||
if entity_ids:
|
||||
raw = await AlexaAPI.get_entity_state(login_obj, entity_ids=entity_ids)
|
||||
device_states = raw.get("deviceStates", []) if isinstance(raw, dict) else None
|
||||
if device_states:
|
||||
for device_state in device_states:
|
||||
entity_id = safe_get(device_state, ["entity", "entityId"])
|
||||
if entity_id:
|
||||
entities[entity_id] = []
|
||||
cap_states = device_state.get("capabilityStates", [])
|
||||
for cap_state in cap_states:
|
||||
entities[entity_id].append(json.loads(cap_state))
|
||||
return entities
|
||||
|
||||
|
||||
def parse_temperature_from_coordinator(
|
||||
coordinator: DataUpdateCoordinator,
|
||||
entity_id: str,
|
||||
debug: bool = False,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Get the temperature of an entity from the coordinator data."""
|
||||
temperature = parse_value_from_coordinator(
|
||||
coordinator,
|
||||
entity_id,
|
||||
"Alexa.TemperatureSensor",
|
||||
"temperature",
|
||||
debug=debug,
|
||||
)
|
||||
if debug:
|
||||
_LOGGER.debug("parse_temperature_from_coordinator: %s", temperature)
|
||||
return temperature
|
||||
|
||||
|
||||
def parse_air_quality_from_coordinator(
|
||||
coordinator: DataUpdateCoordinator,
|
||||
entity_id: str,
|
||||
instance_id: str,
|
||||
debug: bool = False,
|
||||
) -> int | float | str | None:
|
||||
"""Get the air quality of an entity from the coordinator data."""
|
||||
value = parse_value_from_coordinator(
|
||||
coordinator,
|
||||
entity_id,
|
||||
"Alexa.RangeController",
|
||||
"rangeValue",
|
||||
instance=instance_id,
|
||||
debug=debug,
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def parse_brightness_from_coordinator(
|
||||
coordinator: DataUpdateCoordinator, entity_id: str, since: datetime | None
|
||||
) -> int | None:
|
||||
"""Get the brightness in the range 0-100."""
|
||||
return parse_value_from_coordinator(
|
||||
coordinator,
|
||||
entity_id,
|
||||
"Alexa.BrightnessController",
|
||||
"brightness",
|
||||
since=since,
|
||||
)
|
||||
|
||||
|
||||
def parse_color_temp_from_coordinator(
|
||||
coordinator: DataUpdateCoordinator, entity_id: str, since: datetime | None
|
||||
) -> int | None:
|
||||
"""Get the color temperature in kelvin."""
|
||||
return parse_value_from_coordinator(
|
||||
coordinator,
|
||||
entity_id,
|
||||
"Alexa.ColorTemperatureController",
|
||||
"colorTemperatureInKelvin",
|
||||
since=since,
|
||||
)
|
||||
|
||||
|
||||
def parse_color_from_coordinator(
|
||||
coordinator: DataUpdateCoordinator, entity_id: str, since: datetime | None
|
||||
) -> tuple[float, float, float] | None:
|
||||
"""Get the color as a tuple of (hue, saturation, brightness)."""
|
||||
value = parse_value_from_coordinator(
|
||||
coordinator, entity_id, "Alexa.ColorController", "color", since
|
||||
)
|
||||
if value is not None:
|
||||
hue = value.get("hue", 0)
|
||||
saturation = value.get("saturation", 0)
|
||||
return hue, saturation, 1
|
||||
return None
|
||||
|
||||
|
||||
def parse_power_from_coordinator(
|
||||
coordinator: DataUpdateCoordinator, entity_id: str, since: datetime | None
|
||||
) -> str | None:
|
||||
"""Get the power state of the entity."""
|
||||
return parse_value_from_coordinator(
|
||||
coordinator,
|
||||
entity_id,
|
||||
"Alexa.PowerController",
|
||||
"powerState",
|
||||
since=since,
|
||||
)
|
||||
|
||||
|
||||
def parse_guard_state_from_coordinator(
|
||||
coordinator: DataUpdateCoordinator, entity_id: str
|
||||
) -> str | None:
|
||||
"""Get the guard state from the coordinator data."""
|
||||
return parse_value_from_coordinator(
|
||||
coordinator, entity_id, "Alexa.SecurityPanelController", "armState"
|
||||
)
|
||||
|
||||
|
||||
def parse_detection_state_from_coordinator(
|
||||
coordinator: DataUpdateCoordinator, entity_id: str
|
||||
) -> bool | None:
|
||||
"""Get the detection state from the coordinator data."""
|
||||
return parse_value_from_coordinator(
|
||||
coordinator, entity_id, "Alexa.ContactSensor", "detectionState"
|
||||
)
|
||||
|
||||
|
||||
def parse_value_from_coordinator(
|
||||
coordinator: DataUpdateCoordinator,
|
||||
entity_id: str,
|
||||
namespace: str,
|
||||
name: str,
|
||||
since: datetime | None = None,
|
||||
instance: str | None = None,
|
||||
*,
|
||||
debug: bool = False,
|
||||
) -> Any:
|
||||
"""Parse out values from coordinator for Alexa Entities."""
|
||||
if coordinator.data and entity_id in coordinator.data:
|
||||
found_match = False
|
||||
for cap_state in coordinator.data[entity_id]:
|
||||
cap_instance = cap_state.get("instance")
|
||||
instance_match = instance is None or (
|
||||
cap_instance is not None and str(cap_instance) == str(instance)
|
||||
)
|
||||
if (
|
||||
cap_state.get("namespace") == namespace
|
||||
and cap_state.get("name") == name
|
||||
and instance_match
|
||||
):
|
||||
found_match = True
|
||||
if is_cap_state_still_acceptable(cap_state, since):
|
||||
return cap_state.get("value")
|
||||
if debug:
|
||||
_LOGGER.debug(
|
||||
"Coordinator data for %s (%s/%s instance=%s) is too old; checking other matches.",
|
||||
entity_id,
|
||||
namespace,
|
||||
name,
|
||||
instance,
|
||||
)
|
||||
# Keep searching in case a newer matching cap_state exists later.
|
||||
continue
|
||||
if debug and found_match:
|
||||
_LOGGER.debug(
|
||||
"No acceptable coordinator data found for %s (%s/%s instance=%s).",
|
||||
entity_id,
|
||||
namespace,
|
||||
name,
|
||||
instance,
|
||||
)
|
||||
else:
|
||||
if debug:
|
||||
_LOGGER.debug(
|
||||
"Coordinator has no data yet for %s, %s, %s, %s",
|
||||
entity_id,
|
||||
namespace,
|
||||
name,
|
||||
instance,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def is_cap_state_still_acceptable(
|
||||
cap_state: dict[str, Any], since: datetime | None
|
||||
) -> bool:
|
||||
"""Determine if a particular capability state is still usable given its age."""
|
||||
if since is None:
|
||||
return True
|
||||
|
||||
# Don't protect requested state forever; after TTL fall back to coordinator
|
||||
# even if timeOfSample is missing/unparsable.
|
||||
if datetime.now(timezone.utc) - since > _REQUESTED_STATE_TTL:
|
||||
return True
|
||||
|
||||
formatted_time_of_sample = cap_state.get("timeOfSample")
|
||||
if not formatted_time_of_sample:
|
||||
# If we can't prove the sample is newer than the requested state,
|
||||
# do not allow it to override optimistic/requested values.
|
||||
return False
|
||||
|
||||
try:
|
||||
time_of_sample = datetime.fromisoformat(formatted_time_of_sample)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
return time_of_sample >= since
|
||||
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Alexa Devices Base Class.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from alexapy import AlexaAPI, hide_email
|
||||
|
||||
from .const import DATA_ALEXAMEDIA
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AlexaMedia:
|
||||
"""Implementation of Alexa Media Base object."""
|
||||
|
||||
def __init__(self, device, login) -> None:
|
||||
"""Initialize the Alexa device."""
|
||||
|
||||
# Class info
|
||||
self._login = login
|
||||
self.alexa_api = AlexaAPI(device, login)
|
||||
self.email = login.email
|
||||
self.account = hide_email(login.email)
|
||||
|
||||
def check_login_changes(self):
|
||||
"""Update Login object if it has changed."""
|
||||
# _LOGGER.debug("Checking if Login object has changed")
|
||||
try:
|
||||
login = self.hass.data[DATA_ALEXAMEDIA]["accounts"][self.email]["login_obj"]
|
||||
except (AttributeError, KeyError):
|
||||
return
|
||||
# _LOGGER.debug("Login object %s closed status: %s", login, login.session.closed)
|
||||
# _LOGGER.debug(
|
||||
# "Alexaapi %s closed status: %s",
|
||||
# self.alexa_api,
|
||||
# self.alexa_api._session.closed,
|
||||
# )
|
||||
if self.alexa_api.update_login(login):
|
||||
_LOGGER.debug("Login object has changed; updating")
|
||||
self._login = login
|
||||
self.email = login.email
|
||||
self.account = hide_email(login.email)
|
||||
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Alexa Devices Sensors.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from alexapy import hide_serial
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import (
|
||||
CONF_EMAIL,
|
||||
CONF_EXCLUDE_DEVICES,
|
||||
CONF_INCLUDE_DEVICES,
|
||||
DATA_ALEXAMEDIA,
|
||||
hide_email,
|
||||
)
|
||||
from .alexa_entity import parse_detection_state_from_coordinator
|
||||
from .const import CONF_EXTENDED_ENTITY_DISCOVERY
|
||||
from .helpers import add_devices, safe_get
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Set up the Alexa sensor platform."""
|
||||
devices: list[BinarySensorEntity] = []
|
||||
account = None
|
||||
if config:
|
||||
account = config.get(CONF_EMAIL)
|
||||
if account is None and discovery_info:
|
||||
account = safe_get(discovery_info, ["config", CONF_EMAIL])
|
||||
if account is None:
|
||||
raise ConfigEntryNotReady
|
||||
account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account]
|
||||
include_filter = config.get(CONF_INCLUDE_DEVICES, [])
|
||||
exclude_filter = config.get(CONF_EXCLUDE_DEVICES, [])
|
||||
coordinator = account_dict["coordinator"]
|
||||
binary_entities = safe_get(account_dict, ["devices", "binary_sensor"], [])
|
||||
if binary_entities and account_dict["options"].get(CONF_EXTENDED_ENTITY_DISCOVERY):
|
||||
for binary_entity in binary_entities:
|
||||
_LOGGER.debug(
|
||||
"Creating entity %s for a binary_sensor with name %s",
|
||||
hide_serial(binary_entity["id"]),
|
||||
binary_entity["name"],
|
||||
)
|
||||
contact_sensor = AlexaContact(coordinator, binary_entity)
|
||||
account_dict["entities"]["binary_sensor"].append(contact_sensor)
|
||||
devices.append(contact_sensor)
|
||||
|
||||
return await add_devices(
|
||||
hide_email(account),
|
||||
devices,
|
||||
add_devices_callback,
|
||||
include_filter,
|
||||
exclude_filter,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the Alexa sensor platform by config_entry."""
|
||||
return await async_setup_platform(
|
||||
hass, config_entry.data, async_add_devices, discovery_info=None
|
||||
)
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
account = entry.data[CONF_EMAIL]
|
||||
account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account]
|
||||
_LOGGER.debug("Attempting to unload binary sensors")
|
||||
for binary_sensor in account_dict["entities"]["binary_sensor"]:
|
||||
await binary_sensor.async_remove()
|
||||
return True
|
||||
|
||||
|
||||
class AlexaContact(CoordinatorEntity, BinarySensorEntity):
|
||||
"""A contact sensor controlled by an Echo."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.DOOR
|
||||
|
||||
def __init__(self, coordinator: CoordinatorEntity, details: dict):
|
||||
"""Initialize alexa contact sensor.
|
||||
|
||||
Args
|
||||
coordinator (CoordinatorEntity): Coordinator
|
||||
details (dict): Details dictionary
|
||||
|
||||
"""
|
||||
super().__init__(coordinator)
|
||||
self.alexa_entity_id = details["id"]
|
||||
self._name = details["name"]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique id."""
|
||||
return self.alexa_entity_id
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return whether on."""
|
||||
detection = parse_detection_state_from_coordinator(
|
||||
self.coordinator, self.alexa_entity_id
|
||||
)
|
||||
|
||||
return detection == "DETECTED" if detection is not None else None
|
||||
|
||||
@property
|
||||
def assumed_state(self) -> bool:
|
||||
"""Return assumed state."""
|
||||
last_refresh_success = (
|
||||
self.coordinator.data and self.alexa_entity_id in self.coordinator.data
|
||||
)
|
||||
return not last_refresh_success
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,419 @@
|
||||
"""
|
||||
Support to interface with Alexa Devices.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
PERCENTAGE,
|
||||
)
|
||||
|
||||
PROJECT_URL = "https://github.com/alandtse/alexa_media_player/"
|
||||
ISSUE_URL = f"{PROJECT_URL}issues"
|
||||
NOTIFY_URL = f"{PROJECT_URL}wiki/Configuration%3A-Notification-Component#use-the-notifyalexa_media-service"
|
||||
|
||||
DOMAIN = "alexa_media"
|
||||
DATA_ALEXAMEDIA = "alexa_media"
|
||||
|
||||
PLAY_SCAN_INTERVAL = 20
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
MIN_TIME_BETWEEN_SCANS = SCAN_INTERVAL
|
||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
||||
|
||||
ALEXA_COMPONENTS = [
|
||||
"media_player",
|
||||
]
|
||||
DEPENDENT_ALEXA_COMPONENTS = [
|
||||
"notify",
|
||||
"switch",
|
||||
"sensor",
|
||||
"alarm_control_panel",
|
||||
"light",
|
||||
"binary_sensor",
|
||||
]
|
||||
|
||||
HTTP_COOKIE_HEADER = "# HTTP Cookie File"
|
||||
CONF_ACCOUNTS = "accounts"
|
||||
CONF_DEBUG = "debug"
|
||||
CONF_HASS_URL = "hass_url"
|
||||
CONF_INCLUDE_DEVICES = "include_devices"
|
||||
CONF_EXCLUDE_DEVICES = "exclude_devices"
|
||||
CONF_QUEUE_DELAY = "queue_delay"
|
||||
CONF_PUBLIC_URL = "public_url"
|
||||
CONF_EXTENDED_ENTITY_DISCOVERY = "extended_entity_discovery"
|
||||
CONF_SECURITYCODE = "securitycode"
|
||||
CONF_OTPSECRET = "otp_secret"
|
||||
CONF_PROXY = "proxy"
|
||||
CONF_PROXY_WARNING = "proxy_warning"
|
||||
CONF_SCAN_INTERVAL = (
|
||||
"scan_interval" # local definition; HA's CONF_SCAN_INTERVAL is deprecated
|
||||
)
|
||||
CONF_TOTP_REGISTER = "registered"
|
||||
CONF_OAUTH = "oauth"
|
||||
DATA_LISTENER = "listener"
|
||||
|
||||
EXCEPTION_TEMPLATE = "An exception of type {0} occurred. Arguments:\n{1!r}"
|
||||
|
||||
DEFAULT_DEBUG = False
|
||||
DEFAULT_EXTENDED_ENTITY_DISCOVERY = False
|
||||
DEFAULT_HASS_URL = "http://homeassistant.local:8123"
|
||||
DEFAULT_PUBLIC_URL = ""
|
||||
DEFAULT_QUEUE_DELAY = 1.5
|
||||
DEFAULT_SCAN_INTERVAL = 60
|
||||
|
||||
EPOCH_MS_THRESHOLD = 10_000_000_000
|
||||
|
||||
# Service name constants used by services.py SERVICE_DEFS
|
||||
SERVICE_UPDATE_LAST_CALLED = "update_last_called"
|
||||
SERVICE_RESTORE_VOLUME = "restore_volume"
|
||||
SERVICE_GET_HISTORY_RECORDS = "get_history_records"
|
||||
SERVICE_FORCE_LOGOUT = "force_logout"
|
||||
SERVICE_ENABLE_NETWORK_DISCOVERY = "enable_network_discovery"
|
||||
|
||||
# Backoff durations for the last-called probe worker
|
||||
LAST_CALLED_429_BACKOFF_INITIAL_S = 30.0
|
||||
LAST_CALLED_429_BACKOFF_MAX_S = 15 * 60.0
|
||||
LAST_CALLED_CONN_BACKOFF_S = 10.0
|
||||
LAST_CALLED_LOGIN_BACKOFF_S = 30.0
|
||||
|
||||
# Tuning constants for the per-account last-called probe worker
|
||||
LAST_CALLED_DEBOUNCE_S = 3.5 # coalesce bursty pushes, but stay snappy
|
||||
LAST_CALLED_RETRY_DELAY_S = 4.0 # wider retry cadence for delayed routine history
|
||||
LAST_CALLED_RETRY_LIMIT = 2 # total attempts = 1 + retries (3 attempts)
|
||||
LAST_CALLED_STALE_FUDGE_MS = 5_000 # allow some clock/ordering jitter
|
||||
LAST_CALLED_SUCCESS_PACE_S = 4.0 # post-success pacing to avoid hammering
|
||||
LAST_CALLED_LOOKBACK_MS = 60_000
|
||||
LAST_CALLED_ITEMS = 10
|
||||
LAST_CALLED_COALESCE_WINDOW_MS = 2000
|
||||
|
||||
# Tuning constants for notification retries
|
||||
NOTIFICATION_COOLDOWN = 60
|
||||
NOTIFY_REFRESH_BACKOFF = 15.0
|
||||
NOTIFY_REFRESH_MAX_RETRIES = 3
|
||||
|
||||
# push-health magic numbers
|
||||
HTTP2_ERROR_THRESHOLD = 5
|
||||
LAST_PUSH_INACTIVITY_SECONDS = 600.0
|
||||
LAST_PING_MAX_AGE_SECONDS = 900.0
|
||||
|
||||
RECURRING_PATTERN = {
|
||||
None: "Never Repeat",
|
||||
"P1D": "Every day",
|
||||
"P1M": "Every month",
|
||||
"XXXX-WE": "Weekends",
|
||||
"XXXX-WD": "Weekdays",
|
||||
"XXXX-WXX-1": "Every Monday",
|
||||
"XXXX-WXX-2": "Every Tuesday",
|
||||
"XXXX-WXX-3": "Every Wednesday",
|
||||
"XXXX-WXX-4": "Every Thursday",
|
||||
"XXXX-WXX-5": "Every Friday",
|
||||
"XXXX-WXX-6": "Every Saturday",
|
||||
"XXXX-WXX-7": "Every Sunday",
|
||||
}
|
||||
|
||||
RECURRING_DAY = {
|
||||
"MO": 1,
|
||||
"TU": 2,
|
||||
"WE": 3,
|
||||
"TH": 4,
|
||||
"FR": 5,
|
||||
"SA": 6,
|
||||
"SU": 7,
|
||||
}
|
||||
RECURRING_PATTERN_ISO_SET = {
|
||||
None: {},
|
||||
"P1D": {1, 2, 3, 4, 5, 6, 7},
|
||||
"XXXX-WE": {6, 7},
|
||||
"XXXX-WD": {1, 2, 3, 4, 5},
|
||||
"XXXX-WXX-1": {1},
|
||||
"XXXX-WXX-2": {2},
|
||||
"XXXX-WXX-3": {3},
|
||||
"XXXX-WXX-4": {4},
|
||||
"XXXX-WXX-5": {5},
|
||||
"XXXX-WXX-6": {6},
|
||||
"XXXX-WXX-7": {7},
|
||||
}
|
||||
|
||||
ATTR_MESSAGE = "message"
|
||||
ATTR_EMAIL = "email"
|
||||
ATTR_ENTITY_ID = "entity_id"
|
||||
ATTR_NUM_ENTRIES = "entries"
|
||||
COMMON_BUCKET_COUNTS = (
|
||||
"accounts",
|
||||
"devices",
|
||||
"media_players",
|
||||
"players",
|
||||
"notifications",
|
||||
"entities",
|
||||
)
|
||||
COMMON_DIAGNOSTIC_BUCKETS = (
|
||||
"account",
|
||||
"accounts",
|
||||
"login",
|
||||
"logins",
|
||||
"session",
|
||||
"sessions",
|
||||
)
|
||||
COMMON_DIAGNOSTIC_NAMES = (
|
||||
"name",
|
||||
"deviceName",
|
||||
"accountName",
|
||||
"friendlyName",
|
||||
"title",
|
||||
)
|
||||
DEVICE_PLAYER_BUCKETS = ("devices", "media_players", "players")
|
||||
TO_REDACT: set[str] = {
|
||||
"email",
|
||||
"password",
|
||||
"access_token",
|
||||
"refresh_token",
|
||||
"token",
|
||||
"csrf",
|
||||
"cookie",
|
||||
"cookies",
|
||||
"session",
|
||||
"sessionid",
|
||||
"macDms",
|
||||
"mac_dms",
|
||||
"otp_secret",
|
||||
"authorization_code",
|
||||
"securitycode",
|
||||
"code_verifier",
|
||||
"adp_token",
|
||||
"device_private_key",
|
||||
"customerId",
|
||||
}
|
||||
STREAMING_ERROR_MESSAGE = (
|
||||
"Sorry, direct music streaming isn't supported. "
|
||||
"This limitation is set by Amazon, and not by Alexa-Media-Player, Music-Assistant, nor Home-Assistant."
|
||||
)
|
||||
PUBLIC_URL_ERROR_MESSAGE = (
|
||||
"To send TTS, please set the public URL in integration configuration."
|
||||
)
|
||||
STARTUP_MESSAGE = """
|
||||
{name} Version Info
|
||||
{DOMAIN}: v{version}
|
||||
alexapy API: v{alexapy_version}
|
||||
If you have any issues with this custom component, you need to open an issue here: {ISSUE_URL}
|
||||
"""
|
||||
|
||||
AUTH_CALLBACK_PATH = "/auth/alexamedia/callback"
|
||||
AUTH_CALLBACK_NAME = "auth:alexamedia:callback"
|
||||
AUTH_PROXY_PATH = "/auth/alexamedia/proxy"
|
||||
AUTH_PROXY_NAME = "auth:alexamedia:proxy"
|
||||
|
||||
ALEXA_UNIT_CONVERSION = {
|
||||
"Alexa.Unit.Percent": PERCENTAGE,
|
||||
"Alexa.Unit.PartsPerMillion": CONCENTRATION_PARTS_PER_MILLION,
|
||||
"Alexa.Unit.Density.MicroGramsPerCubicMeter": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
}
|
||||
|
||||
ALEXA_ICON_CONVERSION = {
|
||||
"Alexa.AirQuality.CarbonMonoxide": "mdi:molecule-co",
|
||||
"Alexa.AirQuality.Humidity": "mdi:water-percent",
|
||||
"Alexa.AirQuality.IndoorAirQuality": "mdi:numeric",
|
||||
"Alexa.AirQuality.ParticulateMatter": "mdi:blur",
|
||||
"Alexa.AirQuality.VolatileOrganicCompounds": "mdi:air-filter",
|
||||
}
|
||||
ALEXA_ICON_DEFAULT = "mdi:molecule"
|
||||
|
||||
# Device class mapping for air quality sensors
|
||||
# Maps Alexa sensor types to Home Assistant SensorDeviceClass
|
||||
ALEXA_AIR_QUALITY_DEVICE_CLASS = {
|
||||
"Alexa.AirQuality.ParticulateMatter": "pm25",
|
||||
"Alexa.AirQuality.CarbonMonoxide": "carbon_monoxide",
|
||||
"Alexa.AirQuality.IndoorAirQuality": "aqi",
|
||||
"Alexa.AirQuality.VolatileOrganicCompounds": "aqi",
|
||||
"Alexa.AirQuality.Humidity": "humidity",
|
||||
}
|
||||
|
||||
UPLOAD_PATH = "www/alexa_tts"
|
||||
|
||||
# Note: Some of these are likely wrong
|
||||
MODEL_IDS = {
|
||||
"A10A33FOX2NUBK": "Echo Spot (Gen1)",
|
||||
"A10L5JEZTKKCZ8": "Vobot Bunny",
|
||||
"A11QM4H9HGV71H": "Echo Show 5 (Gen3)",
|
||||
"A12GXV8XMS007S": "Fire TV (Gen1)",
|
||||
"A12IZU8NMHSY5U": "Generic Device",
|
||||
"A132LT22WVG6X5": "Samsung Soundbar Q700A",
|
||||
"A13B2WB920IZ7X": "Samsung HW-Q70T Soundbar",
|
||||
"A13W6HQIHKEN3Z": "Echo Auto",
|
||||
"A14ZH95E6SE9Z1": "Bose Home Speaker 300",
|
||||
"A15996VY63BQ2D": "Echo Show 8 (Gen2)",
|
||||
"A15ERDAKK5HQQG": "Sonos",
|
||||
"A15QWUTQ6FSMYX": "Echo Buds (Gen2)",
|
||||
"A16MZVIFVHX6P6": "Generic Echo",
|
||||
"A17LGWINFBUTZZ": "Anker Roav Viva",
|
||||
"A18BI6KPKDOEI4": "Ecobee4",
|
||||
"A18O6U1UQFJ0XK": "Echo Plus (Gen2)",
|
||||
"A18TCD9FP10WJ9": "Orbi Voice",
|
||||
"A18X8OBWBCSLD8": "Samsung Soundbar",
|
||||
"A195TXHV1M5D4A": "Echo Auto",
|
||||
"A1C66CX2XD756O": "Fire Tablet HD",
|
||||
"A1D54LQEG0OXJ2": "Denon Home 250",
|
||||
"A1EIANJ7PNB0Q7": "Echo Show 15 (Gen1)",
|
||||
"A1ENT81UXFMNNO": "Unknown",
|
||||
"A1ETW4IXK2PYBP": "Talk to Alexa",
|
||||
"A1F1F76XIW4DHQ": "Unknown TV",
|
||||
"A1F8D55J0FWDTN": "Fire TV (Toshiba)",
|
||||
"A1H0CMF1XM0ZP4": "Bose SoundTouch 30",
|
||||
"A1J16TEDOYCZTN": "Fire Tablet",
|
||||
"A1JJ0KFC4ZPNJ3": "Echo Input",
|
||||
"A1L4KDRIILU6N9": "Sony Speaker",
|
||||
"A1LOQ8ZHF4G510": "Samsung Soundbar Q990B",
|
||||
"A1M0A9L9HDBID3": "One-Link Safe and Sound",
|
||||
"A1MKGHX5VQBDWX": "Denon Home 150",
|
||||
"A1MUORL8FP149X": "Unknown",
|
||||
"A1N9SW0I0LUX5Y": "Ford/Lincoln Alexa App",
|
||||
"A1NL4BVLQ4L3N3": "Echo Show (Gen1)",
|
||||
"A1NQ0LXWBGVQS9": "2021 Samsung QLED TV",
|
||||
"A1P31Q3MOWSHOD": "Zolo Halo Speaker",
|
||||
"A1P7E7V3FCZKU6": "Fire TV (Gen3)",
|
||||
"A1Q69AKRWLJC0F": "TV",
|
||||
"A1Q7QCGNMXAKYW": "Generic Tablet",
|
||||
"A1QKZ9D0IJY332": "Samsung TV 2020-U",
|
||||
"A1RABVCI4QCIKC": "Echo Dot (Gen3)",
|
||||
"A1RTAM01W29CUP": "Windows App",
|
||||
"A1SCI5MODUBAT1": "Pioneer DMH-W466NEX",
|
||||
"A1TD5Z1R8IWBHA": "Tablet",
|
||||
"A1VGB7MHSIEYFK": "Fire TV Cube Gen3",
|
||||
"A1W2YILXTG9HA7": "Nextbase 522GW Dashcam",
|
||||
"A1W46V57KES4B5": "Cable TV box Brazil",
|
||||
"A1WZKXFLI43K86": "Fire TV Stick MAX",
|
||||
"A1XWJRHALS1REP": "Echo Show 5 (Gen2)",
|
||||
"A1Z88NGR2BK6A2": "Echo Show 8 (Gen1)",
|
||||
"A25EC4GIHFOCSG": "Unrecognized Media Player",
|
||||
"A25OJWHZA1MWNB": "2021 Samsung QLED TV",
|
||||
"A265XOI9586NML": "Fire TV Stick",
|
||||
"A27VEYGQBW3YR5": "Echo Link",
|
||||
"A2A3XFQ1AVYLHZ": "SONY WF-1000XM5",
|
||||
"A2BRQDVMSZD13S": "SURE Universal Remote",
|
||||
"A2C8J6UHV0KFCV": "Alexa Gear",
|
||||
"A2DS1Q2TPDJ48U": "Echo Dot Clock (Gen5)",
|
||||
"A2E0SNTXJVT7WK": "Fire TV (Gen2)",
|
||||
"A2E5N6DMWCW8MZ": "Brilliant Smart Switch",
|
||||
"A2EZ3TS0L1S2KV": "Sonos Beam",
|
||||
"A2GFL5ZMWNE0PX": "Fire TV (Gen3)",
|
||||
"A2H4LV5GIZ1JFT": "Echo Dot Clock (Gen4)",
|
||||
"A2HZENIFNYTXZD": "Facebook Portal",
|
||||
"A2I0SCCU3561Y8": "Samsung Soundbar Q800A",
|
||||
"A2IS7199CJBT71": "TV",
|
||||
"A2IVLV5VM2W81": "Alexa Mobile Voice iOS",
|
||||
"A2J0R2SD7G9LPA": "Lenovo SmartTab M10",
|
||||
"A2JKHJ0PX4J3L3": "Fire TV Cube (Gen2)",
|
||||
"A2LH725P8DQR2A": "Fabriq Riff",
|
||||
"A2LLN0UXRW4N50": "Echo Show 11 (Gen1)",
|
||||
"A2LWARUGJLBYEW": "Fire TV Stick (Gen2)",
|
||||
"A2M35JJZWCQOMZ": "Echo Plus (Gen1)",
|
||||
"A2M4YX06LWP8WI": "Fire Tablet",
|
||||
"A2N49KXGVA18AR": "Fire Tablet HD 10 Plus",
|
||||
"A2OSP3UA4VC85F": "Sonos",
|
||||
"A2R2GLZH1DFYQO": "Zolo Halo Speaker",
|
||||
"A2RU4B77X9R9NZ": "Echo Link Amp",
|
||||
"A2TF17PFR55MTB": "Alexa Mobile Voice Android",
|
||||
"A2TTLILJHVNI9X": "LG TV",
|
||||
"A2U21SRK4QGSE1": "Echo Dot (Gen4)",
|
||||
"A2UONLFQW0PADH": "Echo Show 8 (Gen3)",
|
||||
"A2V9UEGZ82H4KZ": "Fire Tablet HD 10",
|
||||
"A2VAXZ7UNGY4ZH": "Wyze Headphones",
|
||||
"A2WFDCBDEXOXR8": "Bose Soundbar 700",
|
||||
"A2WJ2CM9ARLMRH": "Rivian Electric Vehicle",
|
||||
"A2WN1FJ2HG09UN": "Ultimate Alexa App",
|
||||
"A2X8WT9JELC577": "Ecobee5",
|
||||
"A2XPGY5LRKB9BE": "Fitbit Versa 2",
|
||||
"A2Y04QPFCANLPQ": "Bose QuietComfort 35 II",
|
||||
"A303PJF6ISQ7IC": "Echo Auto",
|
||||
"A30YDR2MK8HMRV": "Echo (Gen3)",
|
||||
"A31DTMEEVDDOIV": "Fire TV Stick Lite",
|
||||
"A324YMIUSWQDGE": "Samsung 8K TV",
|
||||
"A32DDESGESSHZA": "Echo Dot (Gen3)",
|
||||
"A32DOYMUN6DTXA": "Echo Dot (Gen3)",
|
||||
"A339L426Y220I4": "Teufel Radio",
|
||||
"A347G2JC8I4HC7": "Roav Car Charger Pro",
|
||||
"A37CFAHI1O0CXT": "Logitech Blast",
|
||||
"A37M7RU8Z6ZFB": "Garmin Speak",
|
||||
"A37SHHQ3NUL7B5": "Bose Home Speaker 500",
|
||||
"A38949IHXHRQ5P": "Echo Tap",
|
||||
"A38BPK7OW001EX": "Raspberry Alexa",
|
||||
"A38EHHIB10L47V": "Fire Tablet HD 8",
|
||||
"A39BU42XNMN516": "Generic Device",
|
||||
"A3B50IC5QPZPWP": "Polk Command Bar",
|
||||
"A3B5K1G3EITBIF": "Facebook Portal",
|
||||
"A3BRT6REMPQWA8": "Bose Home Speaker 450",
|
||||
"A3BW5ZVFHRCQPO": "BMW Alexa Integration",
|
||||
"A3C9PE6TNYLTCH": "Speaker Group",
|
||||
"A3CY98NH016S5F": "Facebook Portal Mini",
|
||||
"A3D4YURNTARP5K": "Facebook Portal TV",
|
||||
"A3EH2E0YZ30OD6": "Echo Spot (Gen2)",
|
||||
"A3EVMLQTU6WL1W": "Fire TV Stick 4K Max (Gen1)",
|
||||
"A3F1S88NTZZXS9": "Dash Wand",
|
||||
"A3FX4UWTP28V1P": "Echo (Gen3)",
|
||||
"A3GFRGUNIGG1I5": "Samsung TV QN50Q60CAGXZD",
|
||||
"A3HF4YRA2L7XGC": "Fire TV Cube",
|
||||
"A3IYPH06PH1HRA": "Echo Frames",
|
||||
"A3K69RS3EIMXPI": "Hisense Smart TV",
|
||||
"A3KULB3NQN7Z1F": "Unknown TV",
|
||||
"A3L0T0VL9A921N": "Fire Tablet HD 8",
|
||||
"A3NPD82ABCPIDP": "Sonos Beam",
|
||||
"A3QPPX1R9W5RJV": "Fabriq Chorus",
|
||||
"A3QS1XP2U6UJX9": "SONY WF-1000XM4",
|
||||
"A3R9S4ZZECZ6YL": "Fire Tablet HD 10",
|
||||
"A3RBAYBE7VM004": "Echo Studio",
|
||||
"A3RCTOK2V0A4ZG": "LG TV",
|
||||
"A3RMGO6LYLH7YN": "Echo Dot (Gen4)",
|
||||
"A3S5BH2HU6VAYF": "Echo Dot (Gen2)",
|
||||
"A3SSG6GR8UU7SN": "Echo Sub",
|
||||
"A3SSWQ04XYPXBH": "Generic Tablet",
|
||||
"A3TCJ8RTT3NVI7": "Alexa Listens",
|
||||
"A3VRME03NAXFUB": "Echo Flex",
|
||||
"A4ZP7ZC4PI6TO": "Echo Show 5 (Gen1)",
|
||||
"A4ZXE0RM7LQ7A": "Echo Dot (Gen5)",
|
||||
"A52ARKF0HM2T4": "Facebook Portal+",
|
||||
"A6SIQKETF3L2E": "Unknown Device",
|
||||
"A7WXQPH584YP": "Echo (Gen2)",
|
||||
"A81PNL0A63P93": "Home Remote",
|
||||
"A8DM4FYR6D3HT": "TV",
|
||||
"AA1IN44SS3X6O": "Ecobee Thermostat Premium",
|
||||
"AB72C64C86AW2": "Echo (Gen1)",
|
||||
"ABJ2EHL7HQT4L": "Unknown Amplifier",
|
||||
"ADVBD696BHNV5": "Fire TV Stick (Gen1)",
|
||||
"AE7X7Z227NFNS": "HiMirror Mini",
|
||||
"AF473ZSOIRKFJ": "Onkyo VC-PX30",
|
||||
"AFF50AL5E3DIU": "Fire TV (Insignia)",
|
||||
"AFF5OAL5E3DIU": "Fire TV",
|
||||
"AGHZIK8D6X7QR": "Fire TV",
|
||||
"AHJYKVA63YCAQ": "Sonos",
|
||||
"AIPK7MM90V7TB": "Echo Show 10 (Gen3)",
|
||||
"AKKLQD9FZWWQS": "Jabra Elite",
|
||||
"AKNO1N0KSFN8L": "Echo Dot (Gen1)",
|
||||
"AKO51L5QAQKL2": "Alexa Jams",
|
||||
"AKPGW064GI9HE": "Fire TV Stick 4K (Gen3)",
|
||||
"ALCIV0P5M8TZ0": "Samsung Soundbar S800B",
|
||||
"ALT9P69K6LORD": "Echo Auto",
|
||||
"AMCZ48H33RCDF": "Samsung HW-Q910B 9.1.2 ch Soundbar",
|
||||
"AN630UQPG2CA4": "Fire TV (Toshiba)",
|
||||
"AO6HHP9UE6EOF": "Unknown Media Device",
|
||||
"AP1F6KUH00XPV": "Stereo/Subwoofer Pair",
|
||||
"AP4RS91ZQ0OOI": "Fire TV (Toshiba)",
|
||||
"APHEAY6LX7T13": "Samsung Smart Refrigerator",
|
||||
"AQCGW9PSYWRF": "TV",
|
||||
"AR6X0XNIME80V": "Unknown TV",
|
||||
"ASQZWP4GPYUT7": "Echo Pop",
|
||||
"ATNLRCEBX3W4P": "Generic Tablet",
|
||||
"AUPUQSVCVHXP0": "Ecobee Switch+",
|
||||
"AVD3HM0HOJAAL": "Sonos",
|
||||
"AVE5HX13UR5NO": "Logitech Zero Touch",
|
||||
"AVN2TMX8MU2YM": "Bose Home Speaker 500",
|
||||
"AVU7CPPF2ZRAS": "Fire Tablet HD 8",
|
||||
"AWZZ5CVHX2CD": "Echo Show (Gen2)",
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Optimized DataUpdateCoordinator for Alexa Media Player.
|
||||
|
||||
Optimizations:
|
||||
- Debouncer for request coalescing
|
||||
- Type-safe runtime data integration
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN, SCAN_INTERVAL
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .runtime_data import AlexaRuntimeData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Debounce cooldown in seconds - prevents API hammering during push bursts
|
||||
REQUEST_REFRESH_DEBOUNCE_COOLDOWN = 1.5
|
||||
|
||||
|
||||
class AlexaMediaCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Coordinator for Alexa Media Player.
|
||||
|
||||
Features:
|
||||
- Debounced refresh requests to avoid API hammering
|
||||
- Type-safe integration with runtime_data
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
runtime_data: AlexaRuntimeData | None,
|
||||
update_method: Callable,
|
||||
scan_interval: float | None = None,
|
||||
) -> None:
|
||||
"""Initialize the coordinator.
|
||||
|
||||
Args:
|
||||
hass: Home Assistant instance
|
||||
runtime_data: Runtime data for this config entry
|
||||
update_method: Async method to fetch data
|
||||
scan_interval: Polling interval in seconds (default: SCAN_INTERVAL)
|
||||
"""
|
||||
self.runtime_data = runtime_data
|
||||
self._scan_interval = scan_interval or SCAN_INTERVAL.total_seconds()
|
||||
|
||||
# Calculate update interval based on HTTP2 status
|
||||
http2_enabled = runtime_data.http2 is not None if runtime_data else False
|
||||
update_interval = timedelta(
|
||||
seconds=self._scan_interval * 10 if http2_enabled else self._scan_interval
|
||||
)
|
||||
|
||||
# Initialize debouncer for request coalescing
|
||||
# This prevents multiple rapid refresh requests from hammering the API
|
||||
debouncer = Debouncer(
|
||||
hass,
|
||||
_LOGGER,
|
||||
cooldown=REQUEST_REFRESH_DEBOUNCE_COOLDOWN,
|
||||
immediate=True,
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
config_entry=(
|
||||
runtime_data.config_entry
|
||||
if runtime_data and runtime_data.config_entry
|
||||
else None
|
||||
),
|
||||
update_method=update_method,
|
||||
update_interval=update_interval,
|
||||
request_refresh_debouncer=debouncer,
|
||||
)
|
||||
|
||||
def set_http2_status(self, enabled: bool) -> None:
|
||||
"""Update polling interval based on HTTP2 connection status.
|
||||
|
||||
When HTTP2 is enabled, we can poll less frequently since we get push updates.
|
||||
"""
|
||||
new_interval = timedelta(
|
||||
seconds=self._scan_interval * 10 if enabled else self._scan_interval
|
||||
)
|
||||
if self.update_interval != new_interval:
|
||||
self.update_interval = new_interval
|
||||
_LOGGER.debug(
|
||||
"Updated polling interval: %s (HTTP2: %s)",
|
||||
new_interval,
|
||||
enabled,
|
||||
)
|
||||
@@ -0,0 +1,445 @@
|
||||
"""Diagnostics support for Alexa Media Player."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import fields, is_dataclass
|
||||
from datetime import datetime
|
||||
from itertools import islice
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.redact import async_redact_data
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import (
|
||||
COMMON_BUCKET_COUNTS,
|
||||
COMMON_DIAGNOSTIC_BUCKETS,
|
||||
COMMON_DIAGNOSTIC_NAMES,
|
||||
DEVICE_PLAYER_BUCKETS,
|
||||
DOMAIN,
|
||||
TO_REDACT,
|
||||
)
|
||||
|
||||
|
||||
# --------------------
|
||||
# Local Functions
|
||||
# --------------------
|
||||
def _safe_dt(val: Any) -> str | None:
|
||||
"""Serialize datetimes safely for JSON diagnostics."""
|
||||
if isinstance(val, datetime):
|
||||
return val.isoformat()
|
||||
return None
|
||||
|
||||
|
||||
def _maybe_len(val: Any) -> int | None:
|
||||
"""Return the length of common container types or None if not applicable."""
|
||||
if isinstance(val, (list, tuple, dict, set)):
|
||||
return len(val)
|
||||
return None
|
||||
|
||||
|
||||
def _maybe_keys(val: Any, limit: int = 50) -> list[str] | None:
|
||||
"""Return a sanitized sample of mapping keys for diagnostics.
|
||||
|
||||
If ``val`` is a mapping, return up to ``limit`` obfuscated keys to provide
|
||||
structural insight without exposing sensitive data. Email-like keys are
|
||||
redacted when possible; otherwise keys are shortened to a non-identifying
|
||||
form. Returns ``None`` if ``val`` is not a mapping or keys cannot be read.
|
||||
"""
|
||||
|
||||
if isinstance(val, Mapping):
|
||||
try:
|
||||
# Sample up to `limit` keys to keep diagnostics small.
|
||||
def _safe_key(k: Any) -> str:
|
||||
s = str(k)
|
||||
# Emails/titles/tokens sometimes appear as keys in AMP structures.
|
||||
if re.match(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", s):
|
||||
try:
|
||||
from alexapy import ( # pylint: disable=import-outside-toplevel
|
||||
hide_email,
|
||||
)
|
||||
|
||||
return hide_email(s)
|
||||
except (ImportError, AttributeError, TypeError, ValueError):
|
||||
pass
|
||||
return _obfuscate_identifier(s)
|
||||
|
||||
return sorted(_safe_key(k) for k in islice(val.keys(), limit))
|
||||
except (TypeError, AttributeError):
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sample_names(val: Any, *, limit: int = 5) -> list[str] | None:
|
||||
"""Try to sample human-friendly names from a list/dict of device-like objects."""
|
||||
names: list[str] = []
|
||||
|
||||
def add_name(x: Any) -> None:
|
||||
if isinstance(x, Mapping):
|
||||
for key in COMMON_DIAGNOSTIC_NAMES:
|
||||
v = x.get(key)
|
||||
if isinstance(v, str) and v:
|
||||
names.append(v)
|
||||
return
|
||||
v = getattr(x, "name", None)
|
||||
if isinstance(v, str) and v:
|
||||
names.append(v)
|
||||
|
||||
if isinstance(val, Mapping):
|
||||
for v in islice(val.values(), limit * 2):
|
||||
add_name(v)
|
||||
if len(names) >= limit:
|
||||
break
|
||||
return names[:limit] if names else None
|
||||
|
||||
if isinstance(val, (list, tuple)):
|
||||
for v in val[: limit * 2]:
|
||||
add_name(v)
|
||||
if len(names) >= limit:
|
||||
break
|
||||
return names[:limit] if names else None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# --------------------
|
||||
# Coordinator discovery + summary
|
||||
# --------------------
|
||||
def _find_coordinators(obj: Any) -> list[DataUpdateCoordinator]:
|
||||
"""Recursively find DataUpdateCoordinator instances in an object tree."""
|
||||
found: list[DataUpdateCoordinator] = []
|
||||
visited: set[int] = set()
|
||||
|
||||
def walk(x: Any) -> None:
|
||||
obj_id = id(x)
|
||||
if obj_id in visited:
|
||||
return
|
||||
visited.add(obj_id)
|
||||
|
||||
if isinstance(x, DataUpdateCoordinator):
|
||||
found.append(x)
|
||||
return
|
||||
if is_dataclass(x):
|
||||
try:
|
||||
# Walk dataclass attributes directly; asdict() can lose/mangle objects.
|
||||
for f in fields(x):
|
||||
try:
|
||||
walk(getattr(x, f.name))
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
# Skip fields that can't be read safely
|
||||
pass
|
||||
except (TypeError, ValueError):
|
||||
# Fallback: vars() can work for some dataclass/slots variations
|
||||
try:
|
||||
for v in vars(x).values():
|
||||
walk(v)
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
# Ignore attributes that cannot be introspected via vars()
|
||||
pass
|
||||
return
|
||||
if isinstance(x, Mapping):
|
||||
for v in x.values():
|
||||
walk(v)
|
||||
return
|
||||
if isinstance(x, (list, tuple, set)):
|
||||
for v in x:
|
||||
walk(v)
|
||||
return
|
||||
# Ignore everything else.
|
||||
|
||||
walk(obj)
|
||||
return found
|
||||
|
||||
|
||||
def _summarize_coordinator_data(cdata: Any) -> dict:
|
||||
"""
|
||||
Allowlisted summary of coordinator.data.
|
||||
|
||||
Never dump raw coordinator data. Only return counts + small samples.
|
||||
Optimized for AMP: coordinator.data is often a mapping keyed by UUIDs.
|
||||
"""
|
||||
out: dict[str, Any] = {}
|
||||
|
||||
if isinstance(cdata, Mapping):
|
||||
out["data_key_count"] = len(cdata)
|
||||
|
||||
key_sample = list(islice(cdata.keys(), 10))
|
||||
|
||||
out["data_key_types_sample"] = [type(k).__name__ for k in key_sample]
|
||||
|
||||
sample_vals = [type(cdata.get(k)).__name__ for k in key_sample[:3]]
|
||||
if sample_vals:
|
||||
out["data_value_types_sample"] = sample_vals
|
||||
|
||||
# If coordinator.data sometimes contains named buckets (future-proof),
|
||||
# include just counts (but only if those keys actually exist).
|
||||
for key in COMMON_DIAGNOSTIC_BUCKETS:
|
||||
if key in cdata:
|
||||
out[f"{key}_count"] = _maybe_len(cdata.get(key))
|
||||
|
||||
# If AMP ever exposes last_called through coordinator.data, include only safe fields.
|
||||
last_called = cdata.get("last_called")
|
||||
if isinstance(last_called, Mapping):
|
||||
ts = last_called.get("timestamp")
|
||||
out["last_called"] = {
|
||||
"timestamp": _safe_dt(ts) or ts,
|
||||
"summary": last_called.get("summary"),
|
||||
}
|
||||
|
||||
# If there are device/player buckets, sample friendly names (no IDs).
|
||||
for key in DEVICE_PLAYER_BUCKETS:
|
||||
if key in cdata:
|
||||
sample = _sample_names(cdata.get(key))
|
||||
if sample:
|
||||
out[f"{key}_sample_names"] = sample
|
||||
break
|
||||
|
||||
return out
|
||||
|
||||
if isinstance(cdata, (list, tuple)):
|
||||
out["data_len"] = len(cdata)
|
||||
sample = _sample_names(cdata)
|
||||
if sample:
|
||||
out["sample_names"] = sample
|
||||
return out
|
||||
|
||||
if cdata is not None:
|
||||
out["data_type"] = type(cdata).__name__
|
||||
return out
|
||||
|
||||
|
||||
def _summarize_coordinator(coordinator: DataUpdateCoordinator) -> dict:
|
||||
"""Return a safe, compact view of a coordinator."""
|
||||
exc = getattr(coordinator, "last_exception", None)
|
||||
|
||||
data = {
|
||||
"name": getattr(coordinator, "name", None),
|
||||
"last_update_success": getattr(coordinator, "last_update_success", None),
|
||||
"has_exception": exc is not None,
|
||||
"last_exception_type": type(exc).__name__ if exc else None,
|
||||
"update_interval": (
|
||||
str(getattr(coordinator, "update_interval", None))
|
||||
if getattr(coordinator, "update_interval", None) is not None
|
||||
else None
|
||||
),
|
||||
"last_update": _safe_dt(getattr(coordinator, "last_update", None)),
|
||||
}
|
||||
|
||||
try:
|
||||
data["data_summary"] = _summarize_coordinator_data(
|
||||
getattr(coordinator, "data", None)
|
||||
)
|
||||
except (
|
||||
Exception
|
||||
) as exc: # noqa: BLE001 - intentionally broad; diagnostics must not crash
|
||||
data["data_summary_error"] = type(exc).__name__
|
||||
data["data_summary_error_present"] = True
|
||||
|
||||
return data
|
||||
|
||||
|
||||
# --------------------
|
||||
# AMP-specific (non-coordinator) runtime summaries
|
||||
# --------------------
|
||||
def _summarize_amp_entry_runtime(entry_runtime: Any) -> dict:
|
||||
"""
|
||||
Best-effort summary of hass.data[DOMAIN][entry_id] runtime.
|
||||
|
||||
AMP may not store anything here; keep robust.
|
||||
"""
|
||||
out: dict[str, Any] = {"present": entry_runtime is not None}
|
||||
|
||||
if isinstance(entry_runtime, Mapping):
|
||||
out["runtime_type"] = "mapping"
|
||||
out["runtime_keys"] = _maybe_keys(entry_runtime)
|
||||
# Common “bucket” counts if they happen to exist.
|
||||
for key in COMMON_BUCKET_COUNTS:
|
||||
if key in entry_runtime:
|
||||
out[f"{key}_count"] = _maybe_len(entry_runtime.get(key))
|
||||
# Small sample of names
|
||||
for key in DEVICE_PLAYER_BUCKETS:
|
||||
if key in entry_runtime:
|
||||
sample = _sample_names(entry_runtime.get(key))
|
||||
if sample:
|
||||
out[f"{key}_sample_names"] = sample
|
||||
break
|
||||
else:
|
||||
if entry_runtime is not None:
|
||||
out["runtime_type"] = type(entry_runtime).__name__
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _obfuscate_identifier(val: Any) -> str:
|
||||
"""Return a shortened, non-identifying representation of a value.
|
||||
|
||||
Non-string, empty, or very short values are fully masked. Longer strings
|
||||
are reduced to a minimal prefix and suffix to aid debugging without
|
||||
exposing the original identifier.
|
||||
"""
|
||||
if not isinstance(val, str) or not val or len(val) <= 4:
|
||||
return "****"
|
||||
return f"{val[:2]}...{val[-2:]}"
|
||||
|
||||
|
||||
def _obfuscate_title_with_email(title: str | None, email: str | None) -> str | None:
|
||||
"""Obfuscate email in config entry title using the same mechanism as AMP logs."""
|
||||
if not title or not email:
|
||||
return title
|
||||
|
||||
try:
|
||||
# Lazy import to keep diagnostics import cheap
|
||||
from alexapy import hide_email # pylint: disable=import-outside-toplevel
|
||||
|
||||
redacted = hide_email(email)
|
||||
except (ImportError, AttributeError, TypeError, ValueError):
|
||||
redacted = _obfuscate_identifier(email)
|
||||
|
||||
return title.replace(email, redacted)
|
||||
|
||||
|
||||
def _get_safe_config_entry_title(config_entry: ConfigEntry) -> str | None:
|
||||
"""Get obfuscated config entry title."""
|
||||
email = config_entry.data.get("email")
|
||||
return _obfuscate_title_with_email(config_entry.title, email)
|
||||
|
||||
|
||||
def _summarize_amp_domain(domain_data: Any, config_entry: ConfigEntry) -> dict:
|
||||
"""
|
||||
Best-effort summary of hass.data[DOMAIN] for AMP.
|
||||
|
||||
AMP historically stores account/login state in custom structures, not always
|
||||
keyed by entry_id, and often not using DataUpdateCoordinator.
|
||||
"""
|
||||
out: dict[str, Any] = {}
|
||||
out["domain_data_present"] = domain_data is not None
|
||||
out["domain_data_type"] = (
|
||||
type(domain_data).__name__ if domain_data is not None else None
|
||||
)
|
||||
|
||||
if not isinstance(domain_data, Mapping):
|
||||
return out
|
||||
|
||||
out["domain_keys"] = _maybe_keys(domain_data)
|
||||
|
||||
# Try a few common/likely buckets without dumping contents.
|
||||
# NOTE: We deliberately avoid copying values; only report counts/types/samples.
|
||||
for key in COMMON_DIAGNOSTIC_BUCKETS:
|
||||
if key in domain_data:
|
||||
val = domain_data.get(key)
|
||||
out[f"{key}_type"] = type(val).__name__
|
||||
out[f"{key}_len"] = _maybe_len(val)
|
||||
sample = _sample_names(val)
|
||||
if sample:
|
||||
out[f"{key}_sample_names"] = sample
|
||||
|
||||
# Try to locate the specific account blob by email/title if present.
|
||||
# The config entry title often contains "email - url". We'll only use it to
|
||||
# match keys; we won't add the email to diagnostics (redaction will remove it).
|
||||
raw_title = config_entry.title or ""
|
||||
email = config_entry.data.get("email")
|
||||
out["entry_title_hint"] = _obfuscate_title_with_email(raw_title, email)
|
||||
# Some integrations store per-entry runtime keyed by entry_id *or* by title/email.
|
||||
# Report whether those keys exist.
|
||||
out["has_entry_id_key"] = config_entry.entry_id in domain_data
|
||||
out["has_title_key"] = raw_title in domain_data if raw_title else False
|
||||
|
||||
return out
|
||||
|
||||
|
||||
# --------------------
|
||||
# Diagnostics entry points
|
||||
# --------------------
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict:
|
||||
"""Return diagnostics for a config entry."""
|
||||
domain_data = hass.data.get(DOMAIN)
|
||||
safe_title = _get_safe_config_entry_title(config_entry)
|
||||
|
||||
# AMP currently doesn't store runtime under entry_id.
|
||||
# This adds future-proofing for if and when it does.
|
||||
entry_runtime = None
|
||||
if isinstance(domain_data, Mapping):
|
||||
entry_runtime = domain_data.get(config_entry.entry_id)
|
||||
|
||||
# Coordinator discovery:
|
||||
# 1) Try under entry_runtime (best practice)
|
||||
# 2) If none found and domain_data is a mapping, try domain_data as a whole
|
||||
coordinators: list[DataUpdateCoordinator] = []
|
||||
searched: list[str] = []
|
||||
|
||||
if entry_runtime is not None:
|
||||
searched.append("hass.data[DOMAIN][entry_id]")
|
||||
coordinators = _find_coordinators(entry_runtime)
|
||||
|
||||
if not coordinators and isinstance(domain_data, Mapping):
|
||||
searched.append("hass.data[DOMAIN]")
|
||||
coordinators = _find_coordinators(domain_data)
|
||||
|
||||
coordinator_summaries = [_summarize_coordinator(c) for c in coordinators]
|
||||
|
||||
data: dict = {
|
||||
"entry": {
|
||||
"entry_id": config_entry.entry_id,
|
||||
"title": safe_title,
|
||||
"domain": config_entry.domain,
|
||||
"version": config_entry.version,
|
||||
"minor_version": config_entry.minor_version,
|
||||
},
|
||||
# Include config + options; sensitive values are redacted below.
|
||||
"data": dict(config_entry.data),
|
||||
"options": dict(config_entry.options),
|
||||
"account": {
|
||||
"searched_for_coordinators_in": searched,
|
||||
"coordinator_count": len(coordinator_summaries),
|
||||
"coordinators": coordinator_summaries,
|
||||
# AMP-specific summaries (useful when coordinator_count == 0)
|
||||
"amp_entry_runtime_summary": _summarize_amp_entry_runtime(entry_runtime),
|
||||
"amp_domain_summary": _summarize_amp_domain(domain_data, config_entry),
|
||||
},
|
||||
}
|
||||
|
||||
return async_redact_data(data, TO_REDACT)
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
_hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry
|
||||
) -> dict:
|
||||
"""Return diagnostics for a specific device."""
|
||||
safe_title = _get_safe_config_entry_title(config_entry)
|
||||
|
||||
try:
|
||||
# Lazy import to keep diagnostics import cheap
|
||||
from alexapy import hide_serial # pylint: disable=import-outside-toplevel
|
||||
|
||||
safe_serial = hide_serial(device.serial_number)
|
||||
except (ImportError, AttributeError, TypeError, ValueError):
|
||||
safe_serial = _obfuscate_identifier(device.serial_number)
|
||||
|
||||
data: dict = {
|
||||
"device": {
|
||||
"id": _obfuscate_identifier(device.id),
|
||||
"name": device.name,
|
||||
"name_by_user": device.name_by_user,
|
||||
"manufacturer": device.manufacturer,
|
||||
"model": device.model,
|
||||
"sw_version": device.sw_version,
|
||||
"serial_number": safe_serial,
|
||||
"identifiers": sorted(
|
||||
(domain, _obfuscate_identifier(value))
|
||||
for domain, value in device.identifiers
|
||||
),
|
||||
"via_device_id": _obfuscate_identifier(device.via_device_id),
|
||||
},
|
||||
"config_entry": {
|
||||
"entry_id": config_entry.entry_id,
|
||||
"title": safe_title,
|
||||
},
|
||||
}
|
||||
|
||||
return async_redact_data(data, TO_REDACT)
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Alexa Media Exceptions"""
|
||||
|
||||
|
||||
class EmptyDataException(Exception):
|
||||
"""Empty data exception"""
|
||||
|
||||
|
||||
class ForbiddenException(Exception):
|
||||
"""Forbidden exception"""
|
||||
|
||||
|
||||
class LoginForbiddenException(Exception):
|
||||
"""Login forbidden exception"""
|
||||
|
||||
|
||||
class LoginInvalidException(Exception):
|
||||
"""Invalid login exception"""
|
||||
|
||||
def __init__(self, attempts_remaining):
|
||||
self.attempts_remaining = attempts_remaining
|
||||
super().__init__(
|
||||
f"Invalid login credentials. {attempts_remaining} attempts remaining."
|
||||
)
|
||||
|
||||
|
||||
class TimeoutException(Exception):
|
||||
"""Timeout exception"""
|
||||
|
||||
def __init__(self, message=""):
|
||||
super().__init__(f"Timeout exception: {message}")
|
||||
|
||||
|
||||
class UnexpectedApiException(Exception):
|
||||
"""Unexpected API exception"""
|
||||
@@ -0,0 +1,585 @@
|
||||
"""
|
||||
Helper functions for Alexa Media Player.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import Any, Callable, Optional, TypeVar, overload
|
||||
|
||||
from alexapy import AlexapyLoginCloseRequested, AlexapyLoginError, hide_email
|
||||
from alexapy.alexalogin import AlexaLogin
|
||||
from dictor import dictor
|
||||
from homeassistant.const import CONF_EMAIL, CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConditionErrorMessage
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.instance_id import async_get as async_get_instance_id
|
||||
import wrapt
|
||||
|
||||
from .const import DATA_ALEXAMEDIA, EXCEPTION_TEMPLATE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ArgType = TypeVar("ArgType")
|
||||
|
||||
|
||||
def _norm_filter_token(value: Any) -> str | None:
|
||||
"""Normalize a single filter token for reliable matching."""
|
||||
if value is None:
|
||||
return None
|
||||
s = str(value).strip()
|
||||
if not s:
|
||||
return None
|
||||
return s.casefold()
|
||||
|
||||
|
||||
def _coerce_filter(value: Any) -> set[str]:
|
||||
"""Coerce include/exclude filter input into a normalized set[str].
|
||||
|
||||
Accepts:
|
||||
- None / empty -> empty set
|
||||
- comma-separated str -> split on commas
|
||||
- list/set/tuple -> per-item normalization
|
||||
- anything else -> single token (best effort)
|
||||
"""
|
||||
if not value:
|
||||
return set()
|
||||
|
||||
# Legacy/back-compat: allow comma-separated string
|
||||
if isinstance(value, str):
|
||||
out = set()
|
||||
for part in value.split(","):
|
||||
token = _norm_filter_token(part)
|
||||
if token:
|
||||
out.add(token)
|
||||
return out
|
||||
|
||||
if isinstance(value, (list, set, tuple)):
|
||||
out = set()
|
||||
for v in value:
|
||||
token = _norm_filter_token(v)
|
||||
if token:
|
||||
out.add(token)
|
||||
return out
|
||||
|
||||
token = _norm_filter_token(value)
|
||||
return {token} if token else set()
|
||||
|
||||
|
||||
async def add_devices(
|
||||
account: str,
|
||||
devices: list[Entity],
|
||||
add_devices_callback: Callable[[list[Entity], bool], None],
|
||||
include_filter: str | list[str] | set[str] | tuple[str, ...] | None = None,
|
||||
exclude_filter: str | list[str] | set[str] | tuple[str, ...] | None = None,
|
||||
) -> bool:
|
||||
"""Add devices using add_devices_callback."""
|
||||
include_filter_set = _coerce_filter(include_filter)
|
||||
exclude_filter_set = _coerce_filter(exclude_filter)
|
||||
if include_filter_set:
|
||||
_LOGGER.debug(
|
||||
"%s: include_filter_set: %s",
|
||||
account,
|
||||
include_filter_set,
|
||||
)
|
||||
if exclude_filter_set:
|
||||
_LOGGER.debug(
|
||||
"%s: exclude_filter_set: %s",
|
||||
account,
|
||||
exclude_filter_set,
|
||||
)
|
||||
|
||||
def _device_name(dev: Entity) -> str | None:
|
||||
"""Best-effort name before entity_id is assigned.
|
||||
|
||||
For AMP switches, reconstruct the legacy "<device> <suffix> switch"
|
||||
name only if those attributes were explicitly set.
|
||||
"""
|
||||
|
||||
# First prefer explicitly set name attributes (works for tests + most entities)
|
||||
name = (
|
||||
getattr(dev, "name", None)
|
||||
or getattr(dev, "_attr_name", None)
|
||||
or getattr(dev, "_name", None)
|
||||
or getattr(dev, "_device_name", None)
|
||||
or getattr(dev, "_friendly_name", None)
|
||||
)
|
||||
if name:
|
||||
return name
|
||||
|
||||
# Only attempt switch reconstruction if attributes were explicitly defined
|
||||
# (avoids MagicMock auto-attribute trap in tests)
|
||||
dev_dict = getattr(dev, "__dict__", {})
|
||||
|
||||
client = dev_dict.get("_client")
|
||||
suffix = dev_dict.get("_unique_id_suffix")
|
||||
|
||||
if client and suffix:
|
||||
client_dict = getattr(client, "__dict__", {})
|
||||
base = (
|
||||
client_dict.get("name")
|
||||
or client_dict.get("_attr_name")
|
||||
or client_dict.get("_name")
|
||||
or client_dict.get("_device_name")
|
||||
)
|
||||
if base:
|
||||
return f"{base} {suffix} switch"
|
||||
|
||||
return None
|
||||
|
||||
def _device_label(dev: Entity) -> str:
|
||||
"""Return a compact, stable identifier for logging."""
|
||||
name = _device_name(dev)
|
||||
entity_id = getattr(dev, "entity_id", None) # often not set yet
|
||||
dev_type = type(dev).__name__
|
||||
|
||||
if name and entity_id:
|
||||
return f"{name} ({dev_type}, {entity_id})"
|
||||
if name:
|
||||
return f"{name} ({dev_type})"
|
||||
return f"<unnamed> ({dev_type})"
|
||||
|
||||
def _devices_preview(devs: list[Entity]) -> str:
|
||||
max_items = 8
|
||||
labels = [_device_label(d) for d in devs[:max_items]]
|
||||
suffix = f" …(+{len(devs) - max_items} more)" if len(devs) > max_items else ""
|
||||
return ", ".join(labels) + suffix
|
||||
|
||||
def _filter_devices(
|
||||
devs: list[Entity],
|
||||
include_set: set[str],
|
||||
exclude_set: set[str],
|
||||
) -> list[Entity]:
|
||||
selected: list[Entity] = []
|
||||
|
||||
include_mode = bool(include_set)
|
||||
if include_mode and exclude_set:
|
||||
_LOGGER.debug(
|
||||
"%s: include_devices set; ignoring exclude_devices per documented precedence",
|
||||
account,
|
||||
)
|
||||
|
||||
for dev in devs:
|
||||
dev_name = _norm_filter_token(_device_name(dev))
|
||||
|
||||
# INCLUDE MODE: only include explicitly listed names
|
||||
if include_mode:
|
||||
if dev_name and dev_name in include_set:
|
||||
selected.append(dev)
|
||||
else:
|
||||
if not dev_name:
|
||||
_LOGGER.debug(
|
||||
"%s: Not including device (no name yet): %s",
|
||||
account,
|
||||
_device_label(dev),
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s: Not including device: %s (match key=%r)",
|
||||
account,
|
||||
_device_label(dev),
|
||||
dev_name,
|
||||
)
|
||||
continue
|
||||
|
||||
# EXCLUDE MODE: exclude listed names
|
||||
if exclude_set and dev_name and dev_name in exclude_set:
|
||||
_LOGGER.debug(
|
||||
"%s: Excluding device: %s (match key=%r)",
|
||||
account,
|
||||
_device_label(dev),
|
||||
dev_name,
|
||||
)
|
||||
continue
|
||||
|
||||
selected.append(dev)
|
||||
|
||||
return selected
|
||||
|
||||
devices = _filter_devices(devices, include_filter_set, exclude_filter_set)
|
||||
if not devices:
|
||||
return True
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s: Adding %d device(s): %s",
|
||||
account,
|
||||
len(devices),
|
||||
_devices_preview(devices),
|
||||
)
|
||||
|
||||
try:
|
||||
add_devices_callback(devices, False)
|
||||
except ConditionErrorMessage as exception_:
|
||||
message: str = exception_.message
|
||||
if message.startswith("Entity id already exists"):
|
||||
_LOGGER.debug("%s: Device already added: %s", account, message)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s: Unable to add %d device(s): %s",
|
||||
account,
|
||||
len(devices),
|
||||
message,
|
||||
)
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
_LOGGER.debug(
|
||||
"%s: Unable to add %d device(s): %s",
|
||||
account,
|
||||
len(devices),
|
||||
EXCEPTION_TEMPLATE.format(type(ex).__name__, ex.args),
|
||||
)
|
||||
else:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def retry_async(
|
||||
limit: int = 5, delay: float = 1, catch_exceptions: bool = True
|
||||
) -> Callable:
|
||||
"""Wrap function with retry logic.
|
||||
|
||||
The function will retry until true or the limit is reached. It will delay
|
||||
for the period of time specified exponentially increasing the delay.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
limit : int
|
||||
The max number of retries.
|
||||
delay : float
|
||||
The delay in seconds between retries.
|
||||
catch_exceptions : bool
|
||||
Whether exceptions should be caught and treated as failures or thrown.
|
||||
|
||||
Returns
|
||||
-------
|
||||
def
|
||||
Wrapped function.
|
||||
|
||||
"""
|
||||
|
||||
def wrap(func) -> Callable:
|
||||
@functools.wraps(func)
|
||||
async def wrapper(*args, **kwargs) -> Any:
|
||||
_LOGGER.debug(
|
||||
"%s.%s: Trying with limit %s delay %s catch_exceptions %s",
|
||||
func.__module__[func.__module__.find(".") + 1 :],
|
||||
func.__name__,
|
||||
limit,
|
||||
delay,
|
||||
catch_exceptions,
|
||||
)
|
||||
retries: int = 0
|
||||
result: bool = False
|
||||
next_try: int = 0
|
||||
while not result and retries < limit:
|
||||
if retries != 0:
|
||||
next_try = delay * 2**retries
|
||||
await asyncio.sleep(next_try)
|
||||
retries += 1
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
if not catch_exceptions:
|
||||
raise
|
||||
_LOGGER.debug(
|
||||
"%s.%s: failure caught due to exception: %s",
|
||||
func.__module__[func.__module__.find(".") + 1 :],
|
||||
func.__name__,
|
||||
EXCEPTION_TEMPLATE.format(type(ex).__name__, ex.args),
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"%s.%s: Try: %s/%s after waiting %s seconds result: %s",
|
||||
func.__module__[func.__module__.find(".") + 1 :],
|
||||
func.__name__,
|
||||
retries,
|
||||
limit,
|
||||
next_try,
|
||||
result,
|
||||
)
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
@wrapt.decorator
|
||||
async def _catch_login_errors(func, instance, args, kwargs) -> Any:
|
||||
"""Detect AlexapyLoginError and attempt relogin."""
|
||||
|
||||
result = None
|
||||
if instance is None and args:
|
||||
instance = args[0]
|
||||
if hasattr(instance, "check_login_changes"):
|
||||
# _LOGGER.debug(
|
||||
# "%s checking for login changes", instance,
|
||||
# )
|
||||
instance.check_login_changes()
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
except AlexapyLoginCloseRequested:
|
||||
_LOGGER.debug(
|
||||
"%s.%s: Ignoring attempt to access Alexa after HA shutdown",
|
||||
func.__module__[func.__module__.find(".") + 1 :],
|
||||
func.__name__,
|
||||
)
|
||||
return None
|
||||
except AlexapyLoginError as ex:
|
||||
login = None
|
||||
email = None
|
||||
all_args = list(args) + list(kwargs.values())
|
||||
# _LOGGER.debug("Func %s instance %s %s %s", func, instance, args, kwargs)
|
||||
if instance:
|
||||
if hasattr(instance, "_login"):
|
||||
login = instance._login # pylint: disable=protected-access
|
||||
hass = instance.hass
|
||||
else:
|
||||
for arg in all_args:
|
||||
_LOGGER.debug("Checking %s", arg)
|
||||
|
||||
if isinstance(arg, AlexaLogin):
|
||||
login = arg
|
||||
break
|
||||
if hasattr(arg, "_login"):
|
||||
login = instance._login
|
||||
hass = instance.hass
|
||||
break
|
||||
|
||||
if login:
|
||||
# Try to re-login
|
||||
email = login.email
|
||||
if await login.test_loggedin():
|
||||
_LOGGER.info(
|
||||
"%s.%s: Successful re-login after a login error for %s",
|
||||
func.__module__[func.__module__.find(".") + 1 :],
|
||||
func.__name__,
|
||||
hide_email(email),
|
||||
)
|
||||
return None
|
||||
_LOGGER.debug(
|
||||
"%s.%s: detected bad login for %s: %s",
|
||||
func.__module__[func.__module__.find(".") + 1 :],
|
||||
func.__name__,
|
||||
hide_email(email),
|
||||
EXCEPTION_TEMPLATE.format(type(ex).__name__, ex.args),
|
||||
)
|
||||
try:
|
||||
hass
|
||||
except NameError:
|
||||
hass = None
|
||||
report_relogin_required(hass, login, email)
|
||||
return None
|
||||
return result
|
||||
|
||||
|
||||
def report_relogin_required(hass, login, email) -> bool:
|
||||
"""Send message for relogin required."""
|
||||
if hass and login and email:
|
||||
if login.status:
|
||||
_LOGGER.debug(
|
||||
"Reporting need to relogin to %s with %s stats: %s",
|
||||
login.url,
|
||||
hide_email(email),
|
||||
login.stats,
|
||||
)
|
||||
hass.bus.async_fire(
|
||||
"alexa_media_relogin_required",
|
||||
event_data={
|
||||
"email": hide_email(email),
|
||||
"url": login.url,
|
||||
"stats": login.stats,
|
||||
},
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _existing_serials(hass, login_obj) -> list:
|
||||
"""Retrieve existing serial numbers for a given login object."""
|
||||
email: str = login_obj.email
|
||||
if (
|
||||
DATA_ALEXAMEDIA in hass.data
|
||||
and "accounts" in hass.data[DATA_ALEXAMEDIA]
|
||||
and email in hass.data[DATA_ALEXAMEDIA]["accounts"]
|
||||
):
|
||||
existing_serials = list(
|
||||
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"][
|
||||
"media_player"
|
||||
].keys()
|
||||
)
|
||||
device_data = (
|
||||
hass.data[DATA_ALEXAMEDIA]["accounts"][email]
|
||||
.get("devices", {})
|
||||
.get("media_player", {})
|
||||
)
|
||||
for serial in existing_serials[:]:
|
||||
device = device_data.get(serial, {})
|
||||
if "appDeviceList" in device and device["appDeviceList"]:
|
||||
apps = [
|
||||
x["serialNumber"]
|
||||
for x in device["appDeviceList"]
|
||||
if "serialNumber" in x
|
||||
]
|
||||
existing_serials.extend(apps)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"No accounts data found for %s. Skipping serials retrieval.", email
|
||||
)
|
||||
existing_serials = []
|
||||
return existing_serials
|
||||
|
||||
|
||||
async def calculate_uuid(hass, email: str, url: str) -> dict:
|
||||
"""Return uuid and index of email/url.
|
||||
|
||||
Args
|
||||
hass (bool): Hass entity
|
||||
url (str): url for account
|
||||
email (str): email for account
|
||||
|
||||
Returns
|
||||
dict: dictionary with uuid and index
|
||||
|
||||
"""
|
||||
result = {}
|
||||
return_index = 0
|
||||
if hass.config_entries.async_entries(DATA_ALEXAMEDIA):
|
||||
for index, entry in enumerate(
|
||||
hass.config_entries.async_entries(DATA_ALEXAMEDIA)
|
||||
):
|
||||
if entry.data.get(CONF_EMAIL) == email and entry.data.get(CONF_URL) == url:
|
||||
return_index = index
|
||||
break
|
||||
uuid = await async_get_instance_id(hass)
|
||||
result["uuid"] = hex(
|
||||
int(uuid, 16)
|
||||
# increment uuid for second accounts
|
||||
+ return_index
|
||||
# hash email/url in case HA uuid duplicated
|
||||
+ int(
|
||||
hashlib.sha256((email.lower() + url.lower()).encode()).hexdigest(),
|
||||
16, # nosec
|
||||
)
|
||||
)[-32:]
|
||||
result["index"] = return_index
|
||||
_LOGGER.debug("%s: Returning uuid %s", hide_email(email), result)
|
||||
return result
|
||||
|
||||
|
||||
def alarm_just_dismissed(
|
||||
alarm: dict[str, Any],
|
||||
previous_status: Optional[str],
|
||||
previous_version: Optional[str],
|
||||
) -> bool:
|
||||
"""Given the previous state of an alarm, determine if it has just been dismissed."""
|
||||
|
||||
if (
|
||||
previous_status not in ("SNOOZED", "ON")
|
||||
# The alarm had to be in a status that supported being dismissed
|
||||
or previous_version is None
|
||||
# The alarm was probably just created
|
||||
or not alarm
|
||||
# The alarm that was probably just deleted.
|
||||
or alarm.get("status") not in ("OFF", "ON")
|
||||
# A dismissed alarm is guaranteed to be turned off(one-off alarm) or left on(recurring alarm)
|
||||
or previous_version == alarm.get("version")
|
||||
# A dismissal always has a changed version.
|
||||
or int(alarm.get("version", "0")) > 1 + int(previous_version)
|
||||
):
|
||||
# This is an absurd thing to check, but it solves many, many edge cases.
|
||||
# Experimentally, when an alarm is dismissed, the version always increases by 1
|
||||
# When an alarm is edited either via app or voice, its version always increases by 2+
|
||||
return False
|
||||
|
||||
# It seems obvious that a check involving time should be necessary. It is not.
|
||||
# We know there was a change and that it wasn't an edit.
|
||||
# We also know the alarm's status rules out a snooze.
|
||||
# The only remaining possibility is that this alarm was just dismissed.
|
||||
return True
|
||||
|
||||
|
||||
def is_http2_enabled(hass: HomeAssistant | None, login_email: str) -> bool:
|
||||
"""Whether HTTP2 push is enabled for the current account session"""
|
||||
if hass:
|
||||
return bool(
|
||||
safe_get(
|
||||
hass.data,
|
||||
[DATA_ALEXAMEDIA, "accounts", login_email, "http2"],
|
||||
)
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
@overload
|
||||
def safe_get(
|
||||
data: Any,
|
||||
path_list: list[str | int] | None = None,
|
||||
checknone: bool = False,
|
||||
ignorecase: bool = False,
|
||||
pathsep: str = ".",
|
||||
search: Any = None,
|
||||
pretty: bool = False,
|
||||
rtype: str | None = None,
|
||||
) -> Any | None: ...
|
||||
|
||||
|
||||
@overload
|
||||
def safe_get(
|
||||
data: Any, path_list: list[str | int] | None, default: ArgType, *args, **kwargs
|
||||
) -> ArgType: ...
|
||||
|
||||
|
||||
def safe_get(
|
||||
data: Any, path_list: list[str | int] | None = None, *args, **kwargs
|
||||
) -> None | Any:
|
||||
"""Safely get nested value using path segments with optional type checking.
|
||||
|
||||
Args:
|
||||
data: Source data structure
|
||||
path_list: List of path segments (dots in segment names are auto-escaped)
|
||||
*args: Positional arguments passed to dictor (e.g., default value)
|
||||
**kwargs: Keyword arguments passed to dictor (checknone, ignorecase)
|
||||
|
||||
Returns:
|
||||
The value at the specified path, or None if:
|
||||
- The path doesn't exist and no default is provided
|
||||
or default if:
|
||||
- A default is provided and the path doesn't exist
|
||||
- A default is provided and the retrieved value's type doesn't match the default's type
|
||||
|
||||
Note:
|
||||
- Do not pass 'pathsep' in kwargs as the path is pre-built.
|
||||
- Type checking: When a default value is provided and a non-None value is retrieved,
|
||||
the result is validated against the default's type. If types don't match, default is returned.
|
||||
This prevents silent type errors from malformed data structures.
|
||||
|
||||
Examples:
|
||||
>>> safe_get({"a": {"b": "value"}}, ["a", "b"])
|
||||
'value'
|
||||
|
||||
>>> safe_get({"a": {"b": 123}}, ["a", "b"], "default")
|
||||
'default' # Type mismatch: int vs str
|
||||
|
||||
>>> safe_get({"a": {"b": "value"}}, ["a", "b"], "default")
|
||||
'value' # Type matches
|
||||
"""
|
||||
if not path_list:
|
||||
raise ValueError("path_list cannot be empty")
|
||||
|
||||
if "pathsep" in kwargs:
|
||||
kwargs.pop("pathsep") # Ignore pathsep since we build the path
|
||||
|
||||
escaped_segments = (str(seg).replace(".", "\\.") for seg in path_list)
|
||||
path = ".".join(escaped_segments)
|
||||
default = args[0] if args else (kwargs.get("default") if kwargs else None)
|
||||
result = dictor(data, path, *args, **kwargs)
|
||||
if default is not None and result is not None:
|
||||
if not isinstance(result, type(default)):
|
||||
result = default
|
||||
return result
|
||||
@@ -0,0 +1,530 @@
|
||||
"""
|
||||
Alexa Devices Lights.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from math import sqrt
|
||||
from typing import Optional
|
||||
|
||||
from alexapy import AlexaAPI, hide_serial
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_HS_COLOR,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util.color import (
|
||||
color_hs_to_RGB,
|
||||
color_hsb_to_RGB,
|
||||
color_name_to_rgb,
|
||||
color_RGB_to_hs,
|
||||
)
|
||||
|
||||
from . import (
|
||||
CONF_EMAIL,
|
||||
CONF_EXCLUDE_DEVICES,
|
||||
CONF_INCLUDE_DEVICES,
|
||||
DATA_ALEXAMEDIA,
|
||||
hide_email,
|
||||
)
|
||||
from .alexa_entity import (
|
||||
parse_brightness_from_coordinator,
|
||||
parse_color_from_coordinator,
|
||||
parse_color_temp_from_coordinator,
|
||||
parse_power_from_coordinator,
|
||||
)
|
||||
from .const import CONF_EXTENDED_ENTITY_DISCOVERY
|
||||
from .helpers import add_devices, safe_get
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
LOCAL_TIMEZONE = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Set up the Alexa sensor platform."""
|
||||
devices: list[LightEntity] = []
|
||||
account = None
|
||||
if config:
|
||||
account = config.get(CONF_EMAIL)
|
||||
if account is None and discovery_info:
|
||||
account = safe_get(discovery_info, ["config", CONF_EMAIL])
|
||||
if account is None:
|
||||
raise ConfigEntryNotReady
|
||||
account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account]
|
||||
include_filter = config.get(CONF_INCLUDE_DEVICES, [])
|
||||
exclude_filter = config.get(CONF_EXCLUDE_DEVICES, [])
|
||||
coordinator = account_dict["coordinator"]
|
||||
hue_emulated_enabled = "emulated_hue" in hass.config.as_dict().get(
|
||||
"components", set()
|
||||
)
|
||||
light_entities = safe_get(account_dict, ["devices", "light"], [])
|
||||
if light_entities and account_dict["options"].get(CONF_EXTENDED_ENTITY_DISCOVERY):
|
||||
for light_entity in light_entities:
|
||||
if not (light_entity["is_hue_v1"] and hue_emulated_enabled):
|
||||
_LOGGER.debug(
|
||||
"Creating entity %s for a light with name %s",
|
||||
hide_serial(light_entity["id"]),
|
||||
light_entity["name"],
|
||||
)
|
||||
light = AlexaLight(coordinator, account_dict["login_obj"], light_entity)
|
||||
account_dict["entities"]["light"].append(light)
|
||||
devices.append(light)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Light '%s' has not been added because it may originate from emulated_hue",
|
||||
light_entity["name"],
|
||||
)
|
||||
|
||||
return await add_devices(
|
||||
hide_email(account),
|
||||
devices,
|
||||
add_devices_callback,
|
||||
include_filter,
|
||||
exclude_filter,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the Alexa sensor platform by config_entry."""
|
||||
return await async_setup_platform(
|
||||
hass, config_entry.data, async_add_devices, discovery_info=None
|
||||
)
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
account = entry.data[CONF_EMAIL]
|
||||
account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account]
|
||||
_LOGGER.debug("Attempting to unload lights")
|
||||
for light in account_dict["entities"]["light"]:
|
||||
await light.async_remove()
|
||||
return True
|
||||
|
||||
|
||||
def color_modes(details) -> list:
|
||||
"""Return list of color modes."""
|
||||
if details["color"] and details["color_temperature"]:
|
||||
return [ColorMode.HS, ColorMode.COLOR_TEMP]
|
||||
if details["color"]:
|
||||
return [ColorMode.HS]
|
||||
if details["color_temperature"]:
|
||||
return [ColorMode.COLOR_TEMP]
|
||||
if details["brightness"]:
|
||||
return [ColorMode.BRIGHTNESS]
|
||||
return [ColorMode.ONOFF]
|
||||
|
||||
|
||||
class AlexaLight(CoordinatorEntity, LightEntity):
|
||||
"""A light controlled by an Echo."""
|
||||
|
||||
def __init__(self, coordinator, login, details):
|
||||
"""Initialize alexa light entity."""
|
||||
super().__init__(coordinator)
|
||||
self.alexa_entity_id = details["id"]
|
||||
self._name = details["name"]
|
||||
self._login = login
|
||||
self._attr_supported_color_modes = color_modes(details)
|
||||
self._attr_min_color_temp_kelvin = 2200
|
||||
self._attr_max_color_temp_kelvin = 6500
|
||||
|
||||
# Store the requested state from the last call to _set_state
|
||||
# This is so that no new network call is needed just to get values that are already known
|
||||
# This is useful because refreshing the full state can take a bit when many lights are in play.
|
||||
# Especially since Alexa actually polls the lights and that appears to be error-prone with some Zigbee lights.
|
||||
# That delay(1-5s in practice) causes the UI controls to jump all over the place after _set_state
|
||||
self._requested_state_at = None # When was state last set in UTC
|
||||
self._requested_power = None
|
||||
self._requested_ha_brightness = None
|
||||
self._requested_kelvin = None
|
||||
self._requested_hs = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique id."""
|
||||
return self.alexa_entity_id
|
||||
|
||||
@property
|
||||
def color_mode(self):
|
||||
"""Return color mode."""
|
||||
if (
|
||||
ColorMode.HS in self._attr_supported_color_modes
|
||||
and ColorMode.COLOR_TEMP in self._attr_supported_color_modes
|
||||
):
|
||||
hs_color = self.hs_color
|
||||
if hs_color is None or (hs_color[0] == 0 and hs_color[1] == 0):
|
||||
# (0,0) is white. When white, color temp is the better plan.
|
||||
return ColorMode.COLOR_TEMP
|
||||
return ColorMode.HS
|
||||
return self._attr_supported_color_modes[0]
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return whether on."""
|
||||
power = parse_power_from_coordinator(
|
||||
self.coordinator, self.alexa_entity_id, self._requested_state_at
|
||||
)
|
||||
if power is None:
|
||||
return self._requested_power if self._requested_power is not None else False
|
||||
return power == "ON"
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return brightness."""
|
||||
bright = parse_brightness_from_coordinator(
|
||||
self.coordinator, self.alexa_entity_id, self._requested_state_at
|
||||
)
|
||||
if bright is None:
|
||||
return self._requested_ha_brightness
|
||||
return alexa_brightness_to_ha(bright)
|
||||
|
||||
@property
|
||||
def color_temp_kelvin(self):
|
||||
"""Return color temperature."""
|
||||
kelvin = parse_color_temp_from_coordinator(
|
||||
self.coordinator, self.alexa_entity_id, self._requested_state_at
|
||||
)
|
||||
if kelvin is None:
|
||||
return self._requested_kelvin
|
||||
return kelvin_to_alexa(kelvin)[0]
|
||||
|
||||
@property
|
||||
def hs_color(self):
|
||||
"""Return hs color."""
|
||||
hsb = parse_color_from_coordinator(
|
||||
self.coordinator, self.alexa_entity_id, self._requested_state_at
|
||||
)
|
||||
if hsb is None:
|
||||
return self._requested_hs
|
||||
(
|
||||
adjusted_hs,
|
||||
color_name, # pylint:disable=unused-variable
|
||||
) = hsb_to_alexa_color(hsb)
|
||||
return adjusted_hs
|
||||
|
||||
@property
|
||||
def assumed_state(self) -> bool:
|
||||
"""Return whether state is assumed."""
|
||||
last_refresh_success = (
|
||||
self.coordinator.data and self.alexa_entity_id in self.coordinator.data
|
||||
)
|
||||
return not last_refresh_success
|
||||
|
||||
async def _set_state(self, power_on, brightness=None, kelvin=None, hs_color=None):
|
||||
# This is "rounding" on kelvin to the closest value Alexa is willing to acknowledge the existence of.
|
||||
# The alternative implementation would be to use effects instead.
|
||||
# That is far more non-standard, and would lock users out of things like the Flux integration.
|
||||
# The downsides to this approach is that the UI is giving the user a slider
|
||||
# When the user picks a slider value, the UI will "jump" to the closest possible value.
|
||||
# This trade-off doesn't feel as bad in practice as it sounds.
|
||||
adjusted_kelvin, color_temperature_name = kelvin_to_alexa(kelvin)
|
||||
if color_temperature_name is None:
|
||||
# This is "rounding" on HS color to closest value Alexa supports.
|
||||
# The alexa color list is short, but covers a pretty broad spectrum.
|
||||
# Like for kelvin above, this sounds bad but works ok in practice.
|
||||
adjusted_hs, color_name = hs_to_alexa_color(hs_color)
|
||||
else:
|
||||
# If a color temperature is being set, it is not possible to also adjust the color.
|
||||
adjusted_hs = None
|
||||
color_name = None
|
||||
|
||||
response = await AlexaAPI.set_light_state(
|
||||
self._login,
|
||||
self.alexa_entity_id,
|
||||
power_on,
|
||||
brightness=ha_brightness_to_alexa(brightness),
|
||||
color_temperature_name=color_temperature_name,
|
||||
color_name=color_name,
|
||||
)
|
||||
if not isinstance(response, dict):
|
||||
return await self.coordinator.async_request_refresh()
|
||||
control_responses = response.get("controlResponses", [])
|
||||
for response in control_responses:
|
||||
if not response.get("code") == "SUCCESS":
|
||||
# If something failed any state is possible, fallback to a full refresh
|
||||
return await self.coordinator.async_request_refresh()
|
||||
self._requested_power = power_on
|
||||
self._requested_ha_brightness = (
|
||||
brightness if brightness is not None else self.brightness
|
||||
)
|
||||
self._requested_kelvin = (
|
||||
adjusted_kelvin if adjusted_kelvin is not None else self.color_temp_kelvin
|
||||
)
|
||||
if adjusted_hs is not None:
|
||||
self._requested_hs = adjusted_hs
|
||||
elif adjusted_kelvin is not None:
|
||||
# If a kelvin value was set, it is critical that color is cleared out so that color mode is set properly
|
||||
self._requested_hs = None
|
||||
else:
|
||||
self._requested_hs = self.hs_color
|
||||
self._requested_state_at = datetime.datetime.now(
|
||||
datetime.timezone.utc
|
||||
) # must be set last so that previous getters work properly
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
# Confirm quickly, but debounce to avoid spamming during slider drags.
|
||||
account = self.hass.data[DATA_ALEXAMEDIA]["accounts"].get(self._login.email)
|
||||
if account:
|
||||
debouncer = account.get("confirm_refresh_debouncer")
|
||||
if debouncer:
|
||||
await debouncer.async_call()
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn on."""
|
||||
brightness = None
|
||||
kelvin = None
|
||||
hs_color = None
|
||||
if (
|
||||
ColorMode.ONOFF not in self._attr_supported_color_modes
|
||||
and ATTR_BRIGHTNESS in kwargs
|
||||
):
|
||||
brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
if (
|
||||
ColorMode.COLOR_TEMP in self._attr_supported_color_modes
|
||||
and ATTR_COLOR_TEMP_KELVIN in kwargs
|
||||
):
|
||||
kelvin = kwargs[ATTR_COLOR_TEMP_KELVIN]
|
||||
if ColorMode.HS in self._attr_supported_color_modes and ATTR_HS_COLOR in kwargs:
|
||||
hs_color = kwargs[ATTR_HS_COLOR]
|
||||
await self._set_state(True, brightness, kelvin, hs_color)
|
||||
|
||||
async def async_turn_off(self, **kwargs): # pylint:disable=unused-argument
|
||||
"""Turn off."""
|
||||
await self._set_state(False)
|
||||
|
||||
|
||||
def kelvin_to_alexa(kelvin: Optional[float]) -> tuple[Optional[float], Optional[str]]:
|
||||
"""Convert a given color temperature in kelvin to the closest available value that Alexa has support for."""
|
||||
if kelvin is None:
|
||||
return None, None
|
||||
if kelvin <= 2400:
|
||||
return 2200, "warm_white"
|
||||
if kelvin <= 3200:
|
||||
return 2700, "soft_white"
|
||||
if kelvin <= 4400:
|
||||
return 4000, "white"
|
||||
if kelvin <= 6000:
|
||||
return 5400, "daylight_white"
|
||||
return 6500, "cool_white"
|
||||
|
||||
|
||||
def ha_brightness_to_alexa(ha_brightness: Optional[float]) -> Optional[float]:
|
||||
"""Convert HA brightness to alexa brightness."""
|
||||
return (ha_brightness / 255 * 100) if ha_brightness is not None else None
|
||||
|
||||
|
||||
def alexa_brightness_to_ha(alexa: Optional[float]) -> Optional[float]:
|
||||
"""Convert Alexa brightness to HA brightness."""
|
||||
return (alexa / 100 * 255) if alexa is not None else None
|
||||
|
||||
|
||||
# This is a fairly complete list of all the colors that Alexa will respond to and their associated RGB value.
|
||||
ALEXA_COLORS = {
|
||||
"alice_blue": (240, 248, 255),
|
||||
"antique_white": (250, 235, 215),
|
||||
"aqua": (0, 255, 255),
|
||||
"aquamarine": (127, 255, 212),
|
||||
"azure": (240, 255, 255),
|
||||
"beige": (245, 245, 220),
|
||||
"bisque": (255, 228, 196),
|
||||
"black": (0, 0, 0),
|
||||
"blanched_almond": (255, 235, 205),
|
||||
"blue": (0, 0, 255),
|
||||
"blue_violet": (138, 43, 226),
|
||||
"brown": (165, 42, 42),
|
||||
"burlywood": (222, 184, 135),
|
||||
"cadet_blue": (95, 158, 160),
|
||||
"chartreuse": (127, 255, 0),
|
||||
"chocolate": (210, 105, 30),
|
||||
"coral": (255, 127, 80),
|
||||
"cornflower_blue": (100, 149, 237),
|
||||
"cornsilk": (255, 248, 220),
|
||||
"crimson": (220, 20, 60),
|
||||
"cyan": (0, 255, 255),
|
||||
"dark_blue": (0, 0, 139),
|
||||
"dark_cyan": (0, 139, 139),
|
||||
"dark_goldenrod": (184, 134, 11),
|
||||
"dark_green": (0, 100, 0),
|
||||
"dark_grey": (169, 169, 169),
|
||||
"dark_khaki": (189, 183, 107),
|
||||
"dark_magenta": (139, 0, 139),
|
||||
"dark_olive_green": (85, 107, 47),
|
||||
"dark_orange": (255, 140, 0),
|
||||
"dark_orchid": (153, 50, 204),
|
||||
"dark_red": (139, 0, 0),
|
||||
"dark_salmon": (233, 150, 122),
|
||||
"dark_sea_green": (143, 188, 143),
|
||||
"dark_slate_blue": (72, 61, 139),
|
||||
"dark_slate_grey": (47, 79, 79),
|
||||
"dark_turquoise": (0, 206, 209),
|
||||
"dark_violet": (148, 0, 211),
|
||||
"deep_pink": (255, 20, 147),
|
||||
"deep_sky_blue": (0, 191, 255),
|
||||
"dim_grey": (105, 105, 105),
|
||||
"dodger_blue": (30, 144, 255),
|
||||
"firebrick": (178, 34, 34),
|
||||
"floral_white": (255, 250, 240),
|
||||
"forest_green": (34, 139, 34),
|
||||
"fuchsia": (255, 0, 255),
|
||||
"gainsboro": (220, 220, 220),
|
||||
"ghost_white": (248, 248, 255),
|
||||
"gold": (255, 215, 0),
|
||||
"goldenrod": (218, 165, 32),
|
||||
"green": (0, 128, 0),
|
||||
"green_yellow": (173, 255, 47),
|
||||
"grey": (128, 128, 128),
|
||||
"honey_dew": (240, 255, 240),
|
||||
"hot_pink": (255, 105, 180),
|
||||
"indian_red": (205, 92, 92),
|
||||
"indigo": (75, 0, 130),
|
||||
"ivory": (255, 255, 240),
|
||||
"khaki": (240, 230, 140),
|
||||
"lavender": (230, 230, 250),
|
||||
"lavender_blush": (255, 240, 245),
|
||||
"lawn_green": (124, 252, 0),
|
||||
"lemon_chiffon": (255, 250, 205),
|
||||
"light_blue": (173, 216, 230),
|
||||
"light_coral": (240, 128, 128),
|
||||
"light_cyan": (224, 255, 255),
|
||||
"light_goldenrod_yellow": (250, 250, 210),
|
||||
"light_green": (144, 238, 144),
|
||||
"light_grey": (211, 211, 211),
|
||||
"light_pink": (255, 182, 193),
|
||||
"light_salmon": (255, 160, 122),
|
||||
"light_sea_green": (32, 178, 170),
|
||||
"light_sky_blue": (135, 206, 250),
|
||||
"light_slate_grey": (119, 136, 153),
|
||||
"light_steel_blue": (176, 196, 222),
|
||||
"light_yellow": (255, 255, 224),
|
||||
"lime": (0, 255, 0),
|
||||
"lime_green": (50, 205, 50),
|
||||
"linen": (250, 240, 230),
|
||||
"magenta": (255, 0, 255),
|
||||
"maroon": (128, 0, 0),
|
||||
"medium_aqua_marine": (102, 205, 170),
|
||||
"medium_blue": (0, 0, 205),
|
||||
"medium_orchid": (186, 85, 211),
|
||||
"medium_purple": (147, 112, 219),
|
||||
"medium_sea_green": (60, 179, 113),
|
||||
"medium_slate_blue": (123, 104, 238),
|
||||
"medium_spring_green": (0, 250, 154),
|
||||
"medium_turquoise": (72, 209, 204),
|
||||
"medium_violet_red": (199, 21, 133),
|
||||
"midnight_blue": (25, 25, 112),
|
||||
"mint_cream": (245, 255, 250),
|
||||
"misty_rose": (255, 228, 225),
|
||||
"moccasin": (255, 228, 181),
|
||||
"navajo_white": (255, 222, 173),
|
||||
"navy": (0, 0, 128),
|
||||
"old_lace": (253, 245, 230),
|
||||
"olive": (128, 128, 0),
|
||||
"olive_drab": (107, 142, 35),
|
||||
"orange": (255, 165, 0),
|
||||
"orange_red": (255, 69, 0),
|
||||
"orchid": (218, 112, 214),
|
||||
"pale_goldenrod": (238, 232, 170),
|
||||
"pale_green": (152, 251, 152),
|
||||
"pale_turquoise": (175, 238, 238),
|
||||
"pale_violet_red": (219, 112, 147),
|
||||
"papaya_whip": (255, 239, 213),
|
||||
"peach_puff": (255, 218, 185),
|
||||
"peru": (205, 133, 63),
|
||||
"pink": (255, 192, 203),
|
||||
"plum": (221, 160, 221),
|
||||
"powder_blue": (176, 224, 230),
|
||||
"purple": (128, 0, 128),
|
||||
"rebecca_purple": (102, 51, 153),
|
||||
"red": (255, 0, 0),
|
||||
"rosy_brown": (188, 143, 143),
|
||||
"royal_blue": (65, 105, 225),
|
||||
"saddle_brown": (139, 69, 19),
|
||||
"salmon": (250, 128, 114),
|
||||
"sandy_brown": (244, 164, 96),
|
||||
"sea_green": (46, 139, 87),
|
||||
"sea_shell": (255, 245, 238),
|
||||
"sienna": (160, 82, 45),
|
||||
"silver": (192, 192, 192),
|
||||
"sky_blue": (135, 206, 235),
|
||||
"slate_blue": (106, 90, 205),
|
||||
"slate_grey": (112, 128, 144),
|
||||
"snow": (255, 250, 250),
|
||||
"spring_green": (0, 255, 127),
|
||||
"steel_blue": (70, 130, 180),
|
||||
"tan": (210, 180, 140),
|
||||
"teal": (0, 128, 128),
|
||||
"thistle": (216, 191, 216),
|
||||
"tomato": (255, 99, 71),
|
||||
"turquoise": (64, 224, 208),
|
||||
"violet": (238, 130, 238),
|
||||
"wheat": (245, 222, 179),
|
||||
"white": (255, 255, 255),
|
||||
"white_smoke": (245, 245, 245),
|
||||
"yellow": (255, 255, 0),
|
||||
"yellow_green": (154, 205, 50),
|
||||
}
|
||||
|
||||
|
||||
def red_mean(color1: tuple[int, int, int], color2: tuple[int, int, int]) -> float:
|
||||
"""Get an approximate 'distance' between two colors using red mean.
|
||||
|
||||
Wikipedia says this method is "one of the better low-cost approximations".
|
||||
"""
|
||||
r_avg = (color2[0] + color1[0]) / 2
|
||||
r_delta = color2[0] - color1[0]
|
||||
g_delta = color2[1] - color1[1]
|
||||
b_delta = color2[2] - color1[2]
|
||||
r_term = (2 + r_avg / 256) * pow(r_delta, 2)
|
||||
g_term = 4 * pow(g_delta, 2)
|
||||
b_term = (2 + (255 - r_avg) / 256) * pow(b_delta, 2)
|
||||
return sqrt(r_term + g_term + b_term)
|
||||
|
||||
|
||||
def alexa_color_name_to_rgb(color_name: str) -> tuple[int, int, int]:
|
||||
"""Convert an alexa color name into RGB."""
|
||||
return color_name_to_rgb(color_name.replace("_", ""))
|
||||
|
||||
|
||||
def rgb_to_alexa_color(
|
||||
rgb: tuple[int, int, int],
|
||||
) -> tuple[Optional[tuple[float, float]], Optional[str]]:
|
||||
"""Convert a given RGB value into the closest Alexa color."""
|
||||
name, alexa_rgb = min(
|
||||
ALEXA_COLORS.items(),
|
||||
key=lambda alexa_color: red_mean(alexa_color[1], rgb),
|
||||
)
|
||||
red, green, blue = alexa_rgb
|
||||
return color_RGB_to_hs(red, green, blue), name
|
||||
|
||||
|
||||
def hs_to_alexa_color(
|
||||
hs_color: Optional[tuple[float, float]],
|
||||
) -> tuple[Optional[tuple[float, float]], Optional[str]]:
|
||||
"""Convert a given hue/saturation value into the closest Alexa color."""
|
||||
if hs_color is None:
|
||||
return None, None
|
||||
hue, saturation = hs_color
|
||||
return rgb_to_alexa_color(color_hs_to_RGB(hue, saturation))
|
||||
|
||||
|
||||
def hsb_to_alexa_color(
|
||||
hsb: Optional[tuple[float, float, float]],
|
||||
) -> tuple[Optional[tuple[float, float]], Optional[str]]:
|
||||
"""Convert a given hue/saturation/brightness value into the closest Alexa color."""
|
||||
if hsb is None:
|
||||
return None, None
|
||||
hue, saturation, brightness = hsb
|
||||
return rgb_to_alexa_color(color_hsb_to_RGB(hue, saturation, brightness))
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"domain": "alexa_media",
|
||||
"name": "Alexa Media Player",
|
||||
"codeowners": ["@alandtse", "@keatontaylor"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["persistent_notification", "http"],
|
||||
"documentation": "https://github.com/alandtse/alexa_media_player/wiki",
|
||||
"iot_class": "cloud_polling",
|
||||
"issue_tracker": "https://github.com/alandtse/alexa_media_player/issues",
|
||||
"loggers": ["alexapy", "authcaptureproxy"],
|
||||
"requirements": [
|
||||
"alexapy==1.29.22",
|
||||
"packaging>=20.3",
|
||||
"wrapt>=1.14.0",
|
||||
"dictor>=0.1.12,<0.2"
|
||||
],
|
||||
"version": "5.15.4"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,191 @@
|
||||
"""Performance metrics and caching for Alexa Media Player.
|
||||
|
||||
Provides boot time tracking and intelligent data caching.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BootMetrics:
|
||||
"""Track boot performance metrics."""
|
||||
|
||||
start_time: float = field(default_factory=time.monotonic)
|
||||
stages: dict[str, float] = field(default_factory=dict)
|
||||
|
||||
def record_stage(self, stage_name: str) -> None:
|
||||
"""Record a boot stage completion."""
|
||||
elapsed = time.monotonic() - self.start_time
|
||||
self.stages[stage_name] = elapsed
|
||||
_LOGGER.debug(
|
||||
"[BOOT METRICS] %s completed in %.3fs",
|
||||
stage_name,
|
||||
elapsed,
|
||||
)
|
||||
|
||||
def get_summary(self) -> dict[str, Any]:
|
||||
"""Get boot metrics summary."""
|
||||
total = time.monotonic() - self.start_time
|
||||
return {
|
||||
"total_time_seconds": round(total, 3),
|
||||
"stages": {k: round(v, 3) for k, v in self.stages.items()},
|
||||
}
|
||||
|
||||
|
||||
class DataCache:
|
||||
"""Simple TTL cache for API responses.
|
||||
|
||||
Reduces redundant API calls during startup and normal operation.
|
||||
"""
|
||||
|
||||
def __init__(self, ttl_seconds: float = 30.0, max_entries: int = 128) -> None:
|
||||
"""Initialize cache with TTL.
|
||||
|
||||
Args:
|
||||
ttl_seconds: Time-to-live for cached entries
|
||||
max_entries: Maximum number of entries before evicting oldest
|
||||
"""
|
||||
self._cache: dict[str, tuple[Any, float]] = {}
|
||||
self._ttl = ttl_seconds
|
||||
self._max_entries = max_entries
|
||||
self._hits = 0
|
||||
self._misses = 0
|
||||
|
||||
def get(self, key: str) -> Any | None:
|
||||
"""Get value from cache if not expired.
|
||||
|
||||
Args:
|
||||
key: Cache key
|
||||
|
||||
Returns:
|
||||
Cached value or None if expired/missing
|
||||
"""
|
||||
if key not in self._cache:
|
||||
self._misses += 1
|
||||
return None
|
||||
|
||||
value, timestamp = self._cache[key]
|
||||
if time.monotonic() - timestamp > self._ttl:
|
||||
# Expired
|
||||
del self._cache[key]
|
||||
self._misses += 1
|
||||
return None
|
||||
|
||||
self._hits += 1
|
||||
return value
|
||||
|
||||
def cache_set(self, key: str, value: Any) -> None:
|
||||
"""Store value in cache.
|
||||
|
||||
Note: Stores a direct reference (not a copy) for performance.
|
||||
Callers should treat cached values as read-only unless the caller created
|
||||
the cached object, is solely responsible for all mutations, and intentionally
|
||||
enriches it in-place (e.g., the device-dict refresh in async_update_data).
|
||||
|
||||
Args:
|
||||
key: Cache key
|
||||
value: Value to cache
|
||||
"""
|
||||
if len(self._cache) >= self._max_entries and key not in self._cache:
|
||||
oldest_key = min(self._cache, key=lambda k: self._cache[k][1])
|
||||
del self._cache[oldest_key]
|
||||
self._cache[key] = (value, time.monotonic())
|
||||
|
||||
def invalidate(self, key: str) -> None:
|
||||
"""Remove key from cache."""
|
||||
self._cache.pop(key, None)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all cached entries."""
|
||||
self._cache.clear()
|
||||
self._hits = 0
|
||||
self._misses = 0
|
||||
|
||||
def get_stats(self) -> dict[str, int]:
|
||||
"""Get cache statistics."""
|
||||
total = self._hits + self._misses
|
||||
hit_rate = (self._hits / total * 100) if total > 0 else 0
|
||||
return {
|
||||
"entries": len(self._cache),
|
||||
"hits": self._hits,
|
||||
"misses": self._misses,
|
||||
"hit_rate_percent": round(hit_rate, 1),
|
||||
}
|
||||
|
||||
|
||||
class AlexaMetrics:
|
||||
"""Central metrics collector for Alexa Media Player."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize metrics collector."""
|
||||
self.hass = hass
|
||||
self.boot_metrics: BootMetrics | None = None
|
||||
self.api_cache = DataCache(ttl_seconds=30.0)
|
||||
self._api_calls: dict[str, tuple[int, float]] = {} # count, total_time
|
||||
|
||||
def start_boot_tracking(self) -> None:
|
||||
"""Start tracking boot performance."""
|
||||
self.boot_metrics = BootMetrics()
|
||||
_LOGGER.debug("[BOOT METRICS] Started tracking")
|
||||
|
||||
def record_boot_stage(self, stage_name: str) -> None:
|
||||
"""Record a boot stage completion."""
|
||||
if self.boot_metrics:
|
||||
self.boot_metrics.record_stage(stage_name)
|
||||
|
||||
def record_api_call(self, endpoint: str, duration: float) -> None:
|
||||
"""Record API call metrics.
|
||||
|
||||
Args:
|
||||
endpoint: API endpoint name
|
||||
duration: Call duration in seconds
|
||||
"""
|
||||
if endpoint not in self._api_calls:
|
||||
self._api_calls[endpoint] = (0, 0.0)
|
||||
|
||||
count, total = self._api_calls[endpoint]
|
||||
self._api_calls[endpoint] = (count + 1, total + duration)
|
||||
|
||||
def get_api_stats(self) -> dict[str, Any]:
|
||||
"""Get API call statistics."""
|
||||
stats = {}
|
||||
for endpoint, (count, total) in self._api_calls.items():
|
||||
stats[endpoint] = {
|
||||
"calls": count,
|
||||
"total_time": round(total, 3),
|
||||
"avg_time": round(total / count, 3) if count > 0 else 0,
|
||||
}
|
||||
return stats
|
||||
|
||||
def get_full_report(self) -> dict[str, Any]:
|
||||
"""Get complete metrics report."""
|
||||
return {
|
||||
"boot": self.boot_metrics.get_summary() if self.boot_metrics else None,
|
||||
"cache": self.api_cache.get_stats(),
|
||||
"api_calls": self.get_api_stats(),
|
||||
}
|
||||
|
||||
|
||||
def get_metrics(hass: HomeAssistant) -> AlexaMetrics | None:
|
||||
"""Get metrics instance from hass data.
|
||||
|
||||
Args:
|
||||
hass: Home Assistant instance
|
||||
|
||||
Returns:
|
||||
AlexaMetrics instance or None if not initialized
|
||||
"""
|
||||
if DOMAIN in hass.data and "metrics" in hass.data[DOMAIN]:
|
||||
return hass.data[DOMAIN]["metrics"]
|
||||
return None
|
||||
@@ -0,0 +1,370 @@
|
||||
"""
|
||||
Alexa Devices notification service.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
||||
from alexapy.helpers import hide_email, hide_serial
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_DATA,
|
||||
ATTR_TARGET,
|
||||
ATTR_TITLE,
|
||||
ATTR_TITLE_DEFAULT,
|
||||
SERVICE_NOTIFY,
|
||||
BaseNotificationService,
|
||||
)
|
||||
from homeassistant.const import CONF_EMAIL
|
||||
from homeassistant.helpers.group import expand_entity_ids
|
||||
import voluptuous as vol
|
||||
|
||||
from .const import (
|
||||
CONF_QUEUE_DELAY,
|
||||
DATA_ALEXAMEDIA,
|
||||
DEFAULT_QUEUE_DELAY,
|
||||
DOMAIN,
|
||||
NOTIFY_URL,
|
||||
)
|
||||
from .helpers import retry_async
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@retry_async(limit=5, delay=2, catch_exceptions=True)
|
||||
async def async_get_service(hass, config, discovery_info=None):
|
||||
# pylint: disable=unused-argument
|
||||
"""Get the demo notification service."""
|
||||
result = False
|
||||
for account, account_dict in hass.data[DATA_ALEXAMEDIA]["accounts"].items():
|
||||
for key, _ in account_dict["devices"]["media_player"].items():
|
||||
if key not in account_dict["entities"]["media_player"]:
|
||||
_LOGGER.debug(
|
||||
"%s: Media player %s not loaded yet; delaying load",
|
||||
hide_email(account),
|
||||
hide_serial(key),
|
||||
)
|
||||
return False
|
||||
result = hass.data[DATA_ALEXAMEDIA]["notify_service"] = AlexaNotificationService(
|
||||
hass
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
_LOGGER.debug("Attempting to unload notify")
|
||||
target_account = entry.data[CONF_EMAIL]
|
||||
other_accounts = False
|
||||
for account, account_dict in hass.data[DATA_ALEXAMEDIA]["accounts"].items():
|
||||
if account == target_account:
|
||||
if "entities" not in account_dict:
|
||||
continue
|
||||
for device in account_dict["entities"]["media_player"].values():
|
||||
if device.entity_id:
|
||||
entity_id = device.entity_id.split(".")
|
||||
hass.services.async_remove(
|
||||
SERVICE_NOTIFY, f"{DOMAIN}_{entity_id[1]}"
|
||||
)
|
||||
else:
|
||||
other_accounts = True
|
||||
if not other_accounts:
|
||||
hass.services.async_remove(SERVICE_NOTIFY, f"{DOMAIN}")
|
||||
if hass.data[DATA_ALEXAMEDIA].get("notify_service"):
|
||||
hass.data[DATA_ALEXAMEDIA].pop("notify_service")
|
||||
return True
|
||||
|
||||
|
||||
class AlexaNotificationService(BaseNotificationService):
|
||||
"""Implement Alexa Media Player notification service."""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize the service."""
|
||||
self.hass = hass
|
||||
self.last_called = True
|
||||
|
||||
def convert(self, names, type_="entities", filter_matches=False):
|
||||
"""Return a list of converted Alexa devices based on names.
|
||||
|
||||
Names may be matched either by serialNumber, accountName, or
|
||||
Homeassistant entity_id and can return any of the above plus entities
|
||||
|
||||
Parameters
|
||||
----------
|
||||
names : list(string)
|
||||
A list of names to convert
|
||||
type_ : string
|
||||
The type to return entities, entity_ids, serialnumbers, names
|
||||
filter_matches : bool
|
||||
Whether non-matching items are removed from the returned list.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list(string)
|
||||
List of home assistant entity_ids
|
||||
|
||||
"""
|
||||
devices = []
|
||||
if isinstance(names, str):
|
||||
names = [names]
|
||||
for item in names:
|
||||
matched = False
|
||||
for alexa in self.devices:
|
||||
# _LOGGER.debug(
|
||||
# "Testing item: %s against (%s, %s, %s, %s)",
|
||||
# item,
|
||||
# alexa,
|
||||
# alexa.name,
|
||||
# hide_serial(alexa.unique_id),
|
||||
# alexa.entity_id,
|
||||
# )
|
||||
if item in (
|
||||
alexa,
|
||||
alexa.name,
|
||||
alexa.unique_id,
|
||||
alexa.entity_id,
|
||||
alexa.device_serial_number,
|
||||
):
|
||||
if type_ == "entities":
|
||||
converted = alexa
|
||||
elif type_ == "serialnumbers":
|
||||
converted = alexa.device_serial_number
|
||||
elif type_ == "names":
|
||||
converted = alexa.name
|
||||
elif type_ == "entity_ids":
|
||||
converted = alexa.entity_id
|
||||
devices.append(converted)
|
||||
matched = True
|
||||
# _LOGGER.debug("Converting: %s to (%s): %s", item, type_, converted)
|
||||
if not filter_matches and not matched:
|
||||
devices.append(item)
|
||||
return devices
|
||||
|
||||
@property
|
||||
def targets(self):
|
||||
"""Return a dictionary of Alexa devices."""
|
||||
devices = {}
|
||||
for email, account_dict in self.hass.data[DATA_ALEXAMEDIA]["accounts"].items():
|
||||
if "entities" not in account_dict:
|
||||
continue
|
||||
last_called_entity = None
|
||||
for _, entity in account_dict["entities"]["media_player"].items():
|
||||
if entity is None or entity.entity_id is None:
|
||||
continue
|
||||
entity_name = (entity.entity_id).split(".")[1]
|
||||
devices[entity_name] = entity.unique_id
|
||||
if self.last_called and entity.extra_state_attributes.get(
|
||||
"last_called"
|
||||
):
|
||||
attrs = entity.extra_state_attributes
|
||||
try:
|
||||
ts = int(attrs.get("last_called_timestamp") or 0)
|
||||
except (TypeError, ValueError):
|
||||
ts = 0
|
||||
if last_called_entity is None:
|
||||
last_called_entity = entity
|
||||
else:
|
||||
best_attrs = last_called_entity.extra_state_attributes
|
||||
try:
|
||||
best_ts = int(best_attrs.get("last_called_timestamp") or 0)
|
||||
except (TypeError, ValueError):
|
||||
best_ts = 0
|
||||
if ts > best_ts:
|
||||
last_called_entity = entity
|
||||
if last_called_entity is not None:
|
||||
entity_name = (last_called_entity.entity_id).split(".")[1]
|
||||
entity_name_last_called = (
|
||||
f"last_called{'_'+ email if entity_name[-1:].isdigit() else ''}"
|
||||
)
|
||||
devices[entity_name_last_called] = last_called_entity.unique_id
|
||||
return devices
|
||||
|
||||
@property
|
||||
def devices(self):
|
||||
"""Return a list of Alexa devices."""
|
||||
devices = []
|
||||
if (
|
||||
"accounts" not in self.hass.data[DATA_ALEXAMEDIA]
|
||||
or not self.hass.data[DATA_ALEXAMEDIA]["accounts"].items()
|
||||
):
|
||||
return devices
|
||||
for _, account_dict in self.hass.data[DATA_ALEXAMEDIA]["accounts"].items():
|
||||
devices = devices + list(account_dict["entities"]["media_player"].values())
|
||||
return devices
|
||||
|
||||
async def async_send_message(self, message="", **kwargs):
|
||||
# pylint: disable=too-many-branches
|
||||
"""Send a message to an Alexa device."""
|
||||
_LOGGER.debug("Message: %s, kwargs: %s", message, kwargs)
|
||||
_LOGGER.debug("Target type: %s", type(kwargs.get(ATTR_TARGET)))
|
||||
kwargs["message"] = message
|
||||
targets = kwargs.get(ATTR_TARGET)
|
||||
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||
data = kwargs.get(ATTR_DATA, {})
|
||||
data = data if data is not None else {}
|
||||
if isinstance(targets, str):
|
||||
try:
|
||||
targets = json.loads(targets)
|
||||
except json.JSONDecodeError:
|
||||
_LOGGER.error("Target must be a valid json")
|
||||
return
|
||||
processed_targets = []
|
||||
for target in targets:
|
||||
_LOGGER.debug("Processing: %s", target)
|
||||
if not isinstance(target, str):
|
||||
processed_targets.append(target)
|
||||
_LOGGER.debug("Processed non-string target: %s", processed_targets)
|
||||
continue
|
||||
try:
|
||||
parsed = json.loads(target)
|
||||
if isinstance(parsed, list):
|
||||
processed_targets.extend(parsed)
|
||||
else:
|
||||
processed_targets.append(parsed)
|
||||
_LOGGER.debug("Processed Target by json: %s", processed_targets)
|
||||
except json.JSONDecodeError:
|
||||
if "," in target:
|
||||
processed_targets += [
|
||||
item.strip() for item in target.split(",") if item.strip()
|
||||
]
|
||||
else:
|
||||
processed_targets.append(target.strip())
|
||||
_LOGGER.debug("Processed Target by string: %s", processed_targets)
|
||||
# Expand Home Assistant group targets into member entity IDs before
|
||||
# passing to convert(). The convert() method resolves Alexa-specific
|
||||
# identifiers (entity_id, name, serial), but it does not expand HA groups.
|
||||
#
|
||||
# Supported group forms:
|
||||
# - media_player.* helper groups with an entity_id attribute
|
||||
# - old-style YAML group.* entities, via expand_entity_ids()
|
||||
#
|
||||
# Expansion happens here, while targets are still plain strings. Do not run
|
||||
# expand_entity_ids() after convert(), because convert() returns Alexa
|
||||
# objects, not entity ID strings.
|
||||
expanded_targets = []
|
||||
|
||||
for target in processed_targets:
|
||||
if not isinstance(target, str):
|
||||
expanded_targets.append(target)
|
||||
continue
|
||||
|
||||
# UI media_player group helper
|
||||
if (
|
||||
target.startswith("media_player.")
|
||||
and (state := self.hass.states.get(target)) is not None
|
||||
and "entity_id" in state.attributes
|
||||
):
|
||||
members = state.attributes["entity_id"]
|
||||
if isinstance(members, (list, tuple)):
|
||||
expanded_targets.extend(members)
|
||||
else:
|
||||
expanded_targets.append(target)
|
||||
continue
|
||||
|
||||
# Old-style YAML group.*, expand before convert()
|
||||
if target.startswith("group."):
|
||||
try:
|
||||
expanded_targets.extend(expand_entity_ids(self.hass, [target]))
|
||||
except ValueError:
|
||||
_LOGGER.debug("Invalid Home Assistant group target: %s", target)
|
||||
expanded_targets.append(target)
|
||||
continue
|
||||
|
||||
expanded_targets.append(target)
|
||||
|
||||
entities = self.convert(expanded_targets, type_="entities")
|
||||
tasks = []
|
||||
for account, account_dict in self.hass.data[DATA_ALEXAMEDIA][
|
||||
"accounts"
|
||||
].items():
|
||||
data_type = data.get("type", "tts")
|
||||
for alexa in account_dict["entities"]["media_player"].values():
|
||||
if data_type == "tts":
|
||||
targets = self.convert(
|
||||
entities, type_="entities", filter_matches=True
|
||||
)
|
||||
# _LOGGER.debug("TTS entities: %s", targets)
|
||||
if alexa in targets and alexa.available:
|
||||
_LOGGER.debug("TTS by %s : %s", alexa, message)
|
||||
tasks.append(
|
||||
alexa.async_send_tts(
|
||||
message,
|
||||
queue_delay=self.hass.data[DATA_ALEXAMEDIA]["accounts"][
|
||||
account
|
||||
]["options"].get(CONF_QUEUE_DELAY, DEFAULT_QUEUE_DELAY),
|
||||
)
|
||||
)
|
||||
elif data_type == "announce":
|
||||
targets = self.convert(
|
||||
entities, type_="serialnumbers", filter_matches=True
|
||||
)
|
||||
# _LOGGER.debug(
|
||||
# "Announce targets: %s entities: %s",
|
||||
# list(map(hide_serial, targets)),
|
||||
# entities,
|
||||
# )
|
||||
if alexa.device_serial_number in targets and alexa.available:
|
||||
_LOGGER.debug(
|
||||
("%s: Announce by %s to targets: %s: %s"),
|
||||
hide_email(account),
|
||||
alexa,
|
||||
list(map(hide_serial, targets)),
|
||||
message,
|
||||
)
|
||||
tasks.append(
|
||||
alexa.async_send_announcement(
|
||||
message,
|
||||
targets=targets,
|
||||
title=title,
|
||||
method=(data["method"] if "method" in data else "all"),
|
||||
queue_delay=self.hass.data[DATA_ALEXAMEDIA]["accounts"][
|
||||
account
|
||||
]["options"].get(CONF_QUEUE_DELAY, DEFAULT_QUEUE_DELAY),
|
||||
)
|
||||
)
|
||||
break
|
||||
elif data_type == "push":
|
||||
targets = self.convert(
|
||||
entities, type_="entities", filter_matches=True
|
||||
)
|
||||
if alexa in targets and alexa.available:
|
||||
_LOGGER.debug("Push by %s: %s %s", alexa, title, message)
|
||||
tasks.append(
|
||||
alexa.async_send_mobilepush(
|
||||
message,
|
||||
title=title,
|
||||
queue_delay=self.hass.data[DATA_ALEXAMEDIA]["accounts"][
|
||||
account
|
||||
]["options"].get(CONF_QUEUE_DELAY, DEFAULT_QUEUE_DELAY),
|
||||
)
|
||||
)
|
||||
elif data_type == "dropin_notification":
|
||||
targets = self.convert(
|
||||
entities, type_="entities", filter_matches=True
|
||||
)
|
||||
if alexa in targets and alexa.available:
|
||||
_LOGGER.debug(
|
||||
"Notification dropin by %s: %s %s", alexa, title, message
|
||||
)
|
||||
tasks.append(
|
||||
alexa.async_send_dropin_notification(
|
||||
message,
|
||||
title=title,
|
||||
queue_delay=self.hass.data[DATA_ALEXAMEDIA]["accounts"][
|
||||
account
|
||||
]["options"].get(CONF_QUEUE_DELAY, DEFAULT_QUEUE_DELAY),
|
||||
)
|
||||
)
|
||||
else:
|
||||
errormessage = (
|
||||
f"{account}: Data value `type={data_type}` is not implemented. "
|
||||
f"See {NOTIFY_URL}"
|
||||
)
|
||||
_LOGGER.debug(errormessage)
|
||||
raise vol.Invalid(errormessage)
|
||||
await asyncio.gather(*tasks)
|
||||
@@ -0,0 +1,184 @@
|
||||
"""Runtime data for Alexa Media Player integration.
|
||||
|
||||
This module implements the Platinum architecture using entry.runtime_data
|
||||
instead of the legacy hass.data[DOMAIN] pattern.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import (
|
||||
DEFAULT_EXTENDED_ENTITY_DISCOVERY,
|
||||
DEFAULT_PUBLIC_URL,
|
||||
DEFAULT_QUEUE_DELAY,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from alexapy import AlexaLogin, HTTP2EchoClient
|
||||
|
||||
from .coordinator import AlexaMediaCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlexaRuntimeData:
|
||||
"""Runtime data for Alexa Media Player.
|
||||
|
||||
This replaces the legacy dict-based storage in hass.data[DATA_ALEXAMEDIA]["accounts"][email].
|
||||
All fields are type-safe and properly initialized.
|
||||
"""
|
||||
|
||||
# Core components (optional to support partial initialisation)
|
||||
login_obj: AlexaLogin | None = None
|
||||
config_entry: ConfigEntry | None = None
|
||||
coordinator: AlexaMediaCoordinator | None = None
|
||||
|
||||
# HTTP2 Push connection
|
||||
http2: HTTP2EchoClient | None = None
|
||||
http2_error: int = 0
|
||||
http2_lastattempt: float = 0.0
|
||||
http2_commands: dict[str, float] = field(default_factory=dict)
|
||||
http2_activity: dict[str, Any] = field(
|
||||
default_factory=lambda: {"serials": {}, "refreshed": {}}
|
||||
)
|
||||
|
||||
# Device storage
|
||||
devices: dict[str, Any] = field(
|
||||
default_factory=lambda: {
|
||||
"media_player": {},
|
||||
"switch": {},
|
||||
"guard": [],
|
||||
"light": [],
|
||||
"binary_sensor": [],
|
||||
"temperature": [],
|
||||
"smart_switch": [],
|
||||
}
|
||||
)
|
||||
entities: dict[str, Any] = field(
|
||||
default_factory=lambda: {
|
||||
"media_player": {},
|
||||
"switch": {},
|
||||
"sensor": {},
|
||||
"light": [],
|
||||
"binary_sensor": [],
|
||||
"alarm_control_panel": {},
|
||||
"smart_switch": [],
|
||||
}
|
||||
)
|
||||
excluded: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# State tracking
|
||||
new_devices: bool = True
|
||||
auth_info: dict[str, Any] | None = None
|
||||
should_get_network: bool = True
|
||||
second_account_index: int = 0
|
||||
|
||||
# Notifications
|
||||
notifications: dict[str, Any] = field(default_factory=dict)
|
||||
notifications_pending: set[str] = field(default_factory=set)
|
||||
notifications_refresh_task: asyncio.Task | None = None
|
||||
notifications_retry_count: int = 0
|
||||
last_notif_poll: float = 0.0
|
||||
|
||||
# Last called tracking
|
||||
last_called: dict[str, Any] | None = None
|
||||
last_called_customer_history_ts: int = 0
|
||||
last_called_probe_task: asyncio.Task | None = None
|
||||
last_called_probe_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
||||
last_called_probe_last_run: float = 0.0
|
||||
last_push_activity: float = 0.0
|
||||
|
||||
# Options (mirrored from config_entry)
|
||||
options: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# Listeners for cleanup
|
||||
listeners: list[Callable] = field(default_factory=list)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Initialize computed fields after dataclass creation."""
|
||||
# Initialize options from config_entry if available
|
||||
if self.config_entry:
|
||||
from .const import (
|
||||
CONF_DEBUG,
|
||||
CONF_EXCLUDE_DEVICES,
|
||||
CONF_EXTENDED_ENTITY_DISCOVERY,
|
||||
CONF_INCLUDE_DEVICES,
|
||||
CONF_PUBLIC_URL,
|
||||
CONF_QUEUE_DELAY,
|
||||
CONF_SCAN_INTERVAL,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
self.options = {
|
||||
CONF_INCLUDE_DEVICES: self.config_entry.data.get(
|
||||
CONF_INCLUDE_DEVICES, ""
|
||||
),
|
||||
CONF_EXCLUDE_DEVICES: self.config_entry.data.get(
|
||||
CONF_EXCLUDE_DEVICES, ""
|
||||
),
|
||||
CONF_QUEUE_DELAY: self.config_entry.data.get(
|
||||
CONF_QUEUE_DELAY, DEFAULT_QUEUE_DELAY
|
||||
),
|
||||
CONF_SCAN_INTERVAL: self.config_entry.data.get(
|
||||
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
|
||||
),
|
||||
CONF_PUBLIC_URL: self.config_entry.data.get(
|
||||
CONF_PUBLIC_URL, DEFAULT_PUBLIC_URL
|
||||
),
|
||||
CONF_EXTENDED_ENTITY_DISCOVERY: self.config_entry.data.get(
|
||||
CONF_EXTENDED_ENTITY_DISCOVERY, DEFAULT_EXTENDED_ENTITY_DISCOVERY
|
||||
),
|
||||
CONF_DEBUG: self.config_entry.data.get(CONF_DEBUG, False),
|
||||
}
|
||||
|
||||
@property
|
||||
def email(self) -> str:
|
||||
"""Return account email."""
|
||||
return self.login_obj.email if self.login_obj else ""
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
"""Return account URL."""
|
||||
return self.login_obj.url if self.login_obj else ""
|
||||
|
||||
def get_device(self, device_type: str, serial: str) -> Any | None:
|
||||
"""Get a device by type and serial."""
|
||||
devices = self.devices.get(device_type, {})
|
||||
if isinstance(devices, dict):
|
||||
return devices.get(serial)
|
||||
if isinstance(devices, list):
|
||||
for device in devices:
|
||||
if isinstance(device, dict) and device.get("serialNumber") == serial:
|
||||
return device
|
||||
if (
|
||||
device
|
||||
and hasattr(device, "serialNumber")
|
||||
and device.serialNumber == serial
|
||||
):
|
||||
return device
|
||||
return None
|
||||
|
||||
def get_entity(self, entity_type: str, key: str) -> Any | None:
|
||||
"""Get an entity by type and key."""
|
||||
entities = self.entities.get(entity_type, {})
|
||||
if isinstance(entities, dict):
|
||||
return entities.get(key)
|
||||
if isinstance(entities, list):
|
||||
for entity in entities:
|
||||
if hasattr(entity, "unique_id") and entity.unique_id == key:
|
||||
return entity
|
||||
if hasattr(entity, "serial") and entity.serial == key:
|
||||
return entity
|
||||
return None
|
||||
|
||||
def add_listener(self, unsub: Callable) -> None:
|
||||
"""Add a listener for cleanup."""
|
||||
self.listeners.append(unsub)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,421 @@
|
||||
"""
|
||||
Alexa Services.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, Callable
|
||||
|
||||
from alexapy import AlexaAPI, AlexapyLoginError, hide_email
|
||||
from alexapy.errors import AlexapyConnectionError
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
import voluptuous as vol
|
||||
|
||||
from .const import (
|
||||
ATTR_EMAIL,
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_NUM_ENTRIES,
|
||||
DATA_ALEXAMEDIA,
|
||||
DOMAIN,
|
||||
SERVICE_ENABLE_NETWORK_DISCOVERY,
|
||||
SERVICE_FORCE_LOGOUT,
|
||||
SERVICE_GET_HISTORY_RECORDS,
|
||||
SERVICE_RESTORE_VOLUME,
|
||||
SERVICE_UPDATE_LAST_CALLED,
|
||||
)
|
||||
from .helpers import _catch_login_errors, report_relogin_required, safe_get
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
FORCE_LOGOUT_SCHEMA = vol.Schema(
|
||||
{vol.Optional(ATTR_EMAIL, default=[]): vol.All(cv.ensure_list, [cv.string])}
|
||||
)
|
||||
LAST_CALL_UPDATE_SCHEMA = vol.Schema(
|
||||
{vol.Optional(ATTR_EMAIL, default=[]): vol.All(cv.ensure_list, [cv.string])}
|
||||
)
|
||||
RESTORE_VOLUME_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_id})
|
||||
|
||||
GET_HISTORY_RECORDS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Optional(ATTR_NUM_ENTRIES, default=5): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
ENABLE_NETWORK_DISCOVERY_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_EMAIL, default=[]): vol.All(
|
||||
cv.ensure_list,
|
||||
[cv.string],
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AlexaServiceDef:
|
||||
"""Definition for an Alexa Media custom service."""
|
||||
|
||||
name: str # service name as exposed in HA: alexa_media.<name>
|
||||
schema: vol.Schema # voluptuous schema
|
||||
handler: str # method name on AlexaMediaServices
|
||||
|
||||
|
||||
SERVICE_DEFS: tuple[AlexaServiceDef, ...] = (
|
||||
AlexaServiceDef(
|
||||
name=SERVICE_FORCE_LOGOUT,
|
||||
schema=FORCE_LOGOUT_SCHEMA,
|
||||
handler="force_logout",
|
||||
),
|
||||
AlexaServiceDef(
|
||||
name=SERVICE_UPDATE_LAST_CALLED,
|
||||
schema=LAST_CALL_UPDATE_SCHEMA,
|
||||
handler="last_call_handler",
|
||||
),
|
||||
AlexaServiceDef(
|
||||
name=SERVICE_RESTORE_VOLUME,
|
||||
schema=RESTORE_VOLUME_SCHEMA,
|
||||
handler="restore_volume",
|
||||
),
|
||||
AlexaServiceDef(
|
||||
name=SERVICE_GET_HISTORY_RECORDS,
|
||||
schema=GET_HISTORY_RECORDS_SCHEMA,
|
||||
handler="get_history_records",
|
||||
),
|
||||
AlexaServiceDef(
|
||||
name=SERVICE_ENABLE_NETWORK_DISCOVERY,
|
||||
schema=ENABLE_NETWORK_DISCOVERY_SCHEMA,
|
||||
handler="enable_network_discovery",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class AlexaMediaServices:
|
||||
def __init__(self, hass: HomeAssistant, functions: dict[str, Callable[..., Any]]):
|
||||
self.hass = hass
|
||||
self._functions = functions
|
||||
|
||||
async def register(self) -> None:
|
||||
"""Register Alexa Media custom services."""
|
||||
for service_def in SERVICE_DEFS:
|
||||
handler = getattr(self, service_def.handler)
|
||||
self.hass.services.async_register(
|
||||
DOMAIN,
|
||||
service_def.name,
|
||||
handler,
|
||||
schema=service_def.schema,
|
||||
)
|
||||
|
||||
async def unregister(self) -> None:
|
||||
"""Unregister Alexa Media custom services."""
|
||||
for service_def in SERVICE_DEFS:
|
||||
self.hass.services.async_remove(DOMAIN, service_def.name)
|
||||
|
||||
async def force_logout(self, call: ServiceCall) -> bool:
|
||||
"""Handle force logout service request.
|
||||
|
||||
Arguments
|
||||
call.ATTR_EMAIL {List[str] | None}: List of case-sensitive Alexa emails.
|
||||
If None, all accounts are logged out.
|
||||
|
||||
Returns
|
||||
bool -- True if at least one account was marked for relogin.
|
||||
"""
|
||||
requested_emails = call.data.get(ATTR_EMAIL)
|
||||
_LOGGER.debug("Service force_logout called for: %s", requested_emails)
|
||||
|
||||
accounts = self.hass.data[DATA_ALEXAMEDIA]["accounts"]
|
||||
success = False
|
||||
|
||||
for email, account_dict in accounts.items():
|
||||
if requested_emails and email not in requested_emails:
|
||||
continue
|
||||
|
||||
login_obj = account_dict["login_obj"]
|
||||
|
||||
# This is the effective “force logout” for this account: mark it as
|
||||
# requiring reauthentication and notify the user/UI.
|
||||
report_relogin_required(self.hass, login_obj, email)
|
||||
success = True
|
||||
_LOGGER.debug(
|
||||
"Marked Alexa Media account %s for relogin via force_logout service",
|
||||
hide_email(email),
|
||||
)
|
||||
|
||||
if requested_emails and not success:
|
||||
_LOGGER.warning(
|
||||
"force_logout called for %s but no matching Alexa Media accounts were found",
|
||||
requested_emails,
|
||||
)
|
||||
|
||||
return success
|
||||
|
||||
@_catch_login_errors
|
||||
async def last_call_handler(self, call: ServiceCall) -> None:
|
||||
"""Handle last call service request.
|
||||
|
||||
Arguments
|
||||
call.ATTR_EMAIL: {List[str: None]}: List of case-sensitive Alexa emails.
|
||||
If None, all accounts are updated.
|
||||
"""
|
||||
requested_emails = call.data.get(ATTR_EMAIL)
|
||||
update_last_called = self._functions.get("update_last_called")
|
||||
|
||||
if not callable(update_last_called):
|
||||
_LOGGER.error(
|
||||
"update_last_called function not registered; cannot update last_called"
|
||||
)
|
||||
return
|
||||
|
||||
_LOGGER.debug("Service update_last_called called for: %s", requested_emails)
|
||||
|
||||
for email, account_dict in self.hass.data[DATA_ALEXAMEDIA]["accounts"].items():
|
||||
if requested_emails and email not in requested_emails:
|
||||
continue
|
||||
|
||||
login_obj = account_dict["login_obj"]
|
||||
|
||||
async def _run_update_last_called(email: str, login_obj) -> None:
|
||||
try:
|
||||
await update_last_called(login_obj)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except AlexapyLoginError:
|
||||
report_relogin_required(self.hass, login_obj, email)
|
||||
except AlexapyConnectionError:
|
||||
_LOGGER.error(
|
||||
"Unable to connect to Alexa for %s;"
|
||||
" check your network connection and try again",
|
||||
hide_email(email),
|
||||
)
|
||||
except Exception: # pragma: no cover
|
||||
_LOGGER.exception(
|
||||
"Unexpected error updating last_called for %s",
|
||||
hide_email(email),
|
||||
)
|
||||
finally:
|
||||
# Clean up task reference when done
|
||||
if email in self.hass.data[DATA_ALEXAMEDIA]["accounts"]:
|
||||
self.hass.data[DATA_ALEXAMEDIA]["accounts"][email].pop(
|
||||
"service_update_last_called_task", None
|
||||
)
|
||||
|
||||
# Cancel any existing task for this account before creating a new one
|
||||
existing_task = account_dict.get("service_update_last_called_task")
|
||||
if existing_task and not existing_task.done():
|
||||
existing_task.cancel()
|
||||
|
||||
# Store task handle for proper cleanup on unload
|
||||
task = self.hass.async_create_task(
|
||||
_run_update_last_called(email, login_obj),
|
||||
name=f"alexa_media.update_last_called.{hide_email(email)}",
|
||||
)
|
||||
account_dict["service_update_last_called_task"] = task
|
||||
|
||||
async def restore_volume(self, call: ServiceCall) -> bool:
|
||||
"""Handle restore volume service request.
|
||||
|
||||
Arguments:
|
||||
call.ATTR_ENTITY_ID {str: None} -- Alexa Media Player entity.
|
||||
|
||||
"""
|
||||
entity_id = call.data.get(ATTR_ENTITY_ID)
|
||||
_LOGGER.debug("Service restore_volume called for: %s", entity_id)
|
||||
|
||||
# Retrieve the entity registry and entity entry
|
||||
entity_registry = er.async_get(self.hass)
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
|
||||
if not entity_entry:
|
||||
_LOGGER.error("Entity %s not found in registry", entity_id)
|
||||
return False
|
||||
|
||||
# Retrieve the state and attributes
|
||||
state = self.hass.states.get(entity_id)
|
||||
if not state:
|
||||
_LOGGER.warning("Entity %s has no state; cannot restore volume", entity_id)
|
||||
return False
|
||||
|
||||
previous_volume = state.attributes.get("previous_volume")
|
||||
current_volume = state.attributes.get("volume_level")
|
||||
|
||||
if previous_volume is None:
|
||||
_LOGGER.warning(
|
||||
"Previous volume not found for %s; attempting to use current volume level: %s",
|
||||
entity_id,
|
||||
current_volume,
|
||||
)
|
||||
previous_volume = current_volume
|
||||
|
||||
if previous_volume is None:
|
||||
_LOGGER.warning(
|
||||
"No valid volume levels found for entity %s; cannot restore volume",
|
||||
entity_id,
|
||||
)
|
||||
return False
|
||||
|
||||
# Call the volume_set service with the retrieved volume
|
||||
await self.hass.services.async_call(
|
||||
domain="media_player",
|
||||
service="volume_set",
|
||||
service_data={
|
||||
"volume_level": previous_volume,
|
||||
},
|
||||
target={"entity_id": entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
_LOGGER.debug("Volume restored to %s for entity %s", previous_volume, entity_id)
|
||||
return True
|
||||
|
||||
async def get_history_records(self, call: ServiceCall) -> bool:
|
||||
"""Handle request to get history records and store them on the entity."""
|
||||
entity_id = call.data.get(ATTR_ENTITY_ID)
|
||||
number_of_entries = call.data.get(ATTR_NUM_ENTRIES)
|
||||
|
||||
# Validate number_of_entries
|
||||
try:
|
||||
number_of_entries_int = int(number_of_entries)
|
||||
except (TypeError, ValueError):
|
||||
_LOGGER.exception(
|
||||
"Service get_history_records for %s has invalid entries value: %s",
|
||||
entity_id,
|
||||
number_of_entries,
|
||||
)
|
||||
return False
|
||||
|
||||
if number_of_entries_int <= 0:
|
||||
_LOGGER.error(
|
||||
"Service get_history_records for %s with %s entries is invalid; must be > 0",
|
||||
entity_id,
|
||||
number_of_entries_int,
|
||||
)
|
||||
return False
|
||||
|
||||
_LOGGER.debug(
|
||||
"Service get_history_records for: %s with %s entries",
|
||||
entity_id,
|
||||
number_of_entries_int,
|
||||
)
|
||||
|
||||
# Validate the target entity
|
||||
entity_registry = er.async_get(self.hass)
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
if not entity_entry or entity_entry.platform != DOMAIN:
|
||||
_LOGGER.error("Entity %s not found or not part of %s", entity_id, DOMAIN)
|
||||
return False
|
||||
target_serial_number = entity_entry.unique_id
|
||||
|
||||
history_data_total: list[dict[str, Any]] = []
|
||||
|
||||
async def _collect_history_for_account(login_obj) -> None:
|
||||
"""Collect history entries for a single account matching the target device."""
|
||||
# Get the history records. Input: time_from, time_to (both None here).
|
||||
history_data = await AlexaAPI.get_customer_history_records(
|
||||
login_obj, None, None
|
||||
)
|
||||
if not history_data:
|
||||
return
|
||||
|
||||
for item in history_data:
|
||||
summary = safe_get(item, ["description", "summary"], "")
|
||||
device_serial_number = item.get("deviceSerialNumber")
|
||||
timestamp = item.get("creationTimestamp")
|
||||
|
||||
if (
|
||||
not summary
|
||||
or summary == ","
|
||||
or device_serial_number != target_serial_number
|
||||
or timestamp is None
|
||||
):
|
||||
continue
|
||||
|
||||
entry = {
|
||||
"timestamp": timestamp,
|
||||
"summary": summary,
|
||||
"response": item.get("alexaResponse", ""),
|
||||
}
|
||||
history_data_total.append(entry)
|
||||
|
||||
# Iterate accounts and collect history
|
||||
for email, account_dict in self.hass.data[DATA_ALEXAMEDIA]["accounts"].items():
|
||||
login_obj = account_dict["login_obj"]
|
||||
try:
|
||||
await _collect_history_for_account(login_obj)
|
||||
except AlexapyConnectionError:
|
||||
_LOGGER.exception(
|
||||
"Error retrieving history for %s",
|
||||
hide_email(email),
|
||||
)
|
||||
except AlexapyLoginError:
|
||||
_LOGGER.exception(
|
||||
"Login error retrieving history for %s",
|
||||
hide_email(email),
|
||||
)
|
||||
report_relogin_required(self.hass, login_obj, email)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
# Let HA cancellation propagate
|
||||
raise
|
||||
except Exception:
|
||||
# Fallback for truly unexpected errors
|
||||
_LOGGER.exception(
|
||||
"Unexpected error retrieving history for %s",
|
||||
hide_email(email),
|
||||
)
|
||||
|
||||
# Sort and limit entries
|
||||
history_data_total.sort(key=lambda x: x["timestamp"], reverse=True)
|
||||
history_data_total = history_data_total[:number_of_entries_int]
|
||||
|
||||
# Update the entity's attributes
|
||||
state = self.hass.states.get(entity_id)
|
||||
if state is not None:
|
||||
new_attributes = dict(state.attributes)
|
||||
new_attributes["history_records"] = history_data_total
|
||||
self.hass.states.async_set(entity_id, state.state, new_attributes)
|
||||
return True
|
||||
|
||||
_LOGGER.error("Entity %s state not found", entity_id)
|
||||
return False
|
||||
|
||||
async def enable_network_discovery(self, call: ServiceCall) -> None:
|
||||
"""Re-enable network discovery for one or more Alexa accounts."""
|
||||
data = call.data or {}
|
||||
target_emails: list[str] = data.get(ATTR_EMAIL, [])
|
||||
|
||||
accounts = self.hass.data[DATA_ALEXAMEDIA]["accounts"]
|
||||
any_matched = False
|
||||
|
||||
for email, account_dict in accounts.items():
|
||||
if target_emails and email not in target_emails:
|
||||
continue
|
||||
|
||||
any_matched = True
|
||||
|
||||
if "should_get_network" not in account_dict:
|
||||
_LOGGER.debug(
|
||||
"Account %s has no 'should_get_network' flag; skipping",
|
||||
hide_email(email),
|
||||
)
|
||||
continue
|
||||
|
||||
account_dict["should_get_network"] = True
|
||||
_LOGGER.debug(
|
||||
"Re-enabled network discovery for Alexa Media account %s",
|
||||
hide_email(email),
|
||||
)
|
||||
|
||||
if target_emails and not any_matched:
|
||||
_LOGGER.warning(
|
||||
"enable_network_discovery called for %s but no matching Alexa Media accounts were found",
|
||||
target_emails,
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
force_logout:
|
||||
# Description of the service
|
||||
description: Force logout of Alexa Login account and deletion of .pickle. Intended for debugging use.
|
||||
# Different fields that your service accepts
|
||||
fields:
|
||||
# Key of the field
|
||||
email:
|
||||
# Description of the field
|
||||
description: List of Alexa accounts to log out. If empty, will log out from all known accounts.
|
||||
# Example value that can be passed for this field
|
||||
example: "my_email@alexa.com"
|
||||
|
||||
restore_volume:
|
||||
description: Restores an Alexa Media Player volume level to the previous volume level.
|
||||
fields:
|
||||
entity_id:
|
||||
name: Entity
|
||||
description: Alexa Media Player device to restore volume on.
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
domain: media_player
|
||||
integration: alexa_media
|
||||
|
||||
get_history_records:
|
||||
description: Returns the last entries of all the customer history.
|
||||
fields:
|
||||
entity_id:
|
||||
name: Entity
|
||||
description: Alexa Media Player device to get history from.
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
domain: media_player
|
||||
integration: alexa_media
|
||||
entries:
|
||||
name: Entries
|
||||
description: Number of records to return.
|
||||
required: false
|
||||
default: 5
|
||||
example: 5
|
||||
|
||||
update_last_called:
|
||||
# Description of the service
|
||||
description: Forces update of last_called echo device for each Alexa account.
|
||||
# Different fields that your service accepts
|
||||
fields:
|
||||
# Key of the field
|
||||
email:
|
||||
# Description of the field
|
||||
description: List of Alexa accounts to update. If empty, will update all known accounts.
|
||||
# Example value that can be passed for this field
|
||||
example: "my_email@alexa.com"
|
||||
|
||||
enable_network_discovery:
|
||||
name: Enable network discovery
|
||||
description: >
|
||||
Re-enable Alexa network discovery so the next polling cycle will
|
||||
rediscover Alexa devices for the selected accounts.
|
||||
fields:
|
||||
email:
|
||||
name: Account email(s)
|
||||
description: >
|
||||
Optional Alexa account email or list of emails. If omitted,
|
||||
all Alexa Media accounts will be refreshed.
|
||||
required: false
|
||||
example: [email protected]
|
||||
selector:
|
||||
text:
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "The Forgot Password page was detected. This normally is the result of too many failed logins. Amazon may require action before a relogin can be attempted.",
|
||||
"login_failed": "Alexa Media Player failed to login.",
|
||||
"reauth_successful": "Alexa Media Player successfully reauthenticated. Please ignore the \"Aborted\" message from HA."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Error connecting; check network and retry",
|
||||
"identifier_exists": "Email for Alexa URL already registered",
|
||||
"invalid_credentials": "Invalid credentials",
|
||||
"invalid_auth": "Login was not successful. Please double-check your email, password, and Authenticator key.",
|
||||
"oauth_error": "Could not complete OAuth login. Please try again.",
|
||||
"invalid_url": "URL is invalid: {message}",
|
||||
"2fa_key_invalid": "{otp_secret} is invalid",
|
||||
"unable_to_connect_hass_url": "Unable to connect to Home Assistant Local URL. Please check the URL under Settings > System > Network > Home Assistant URL > Local network",
|
||||
"unknown_error": "Unknown error: {message}"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "Advanced debug",
|
||||
"email": "Email Address",
|
||||
"exclude_devices": "or Exclude these devices from all (comma separated)",
|
||||
"extended_entity_discovery": "Include additional sensors, switches and lights",
|
||||
"hass_url": "Local network URL to access Home Assistant",
|
||||
"include_devices": "Only include these devices (comma separated)",
|
||||
"otp_secret": "52-character Authenticator App Key for Amazon 2SV",
|
||||
"password": "Password",
|
||||
"public_url": "Public URL shared with external hosted services",
|
||||
"queue_delay": "Delay to queue multiple commands together (seconds)",
|
||||
"scan_interval": "Scheduled polling interval (seconds)",
|
||||
"securitycode": "One-time password (OTP)",
|
||||
"should_get_network": "Discover Alexa network",
|
||||
"url": "Amazon region domain (e.g., amazon.co.uk)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
|
||||
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
},
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "Ignore and Continue - I understand that no support for login issues are provided for bypassing this warning."
|
||||
},
|
||||
"description": "The HA server cannot connect to the URL provided: {hass_url}.\n> {error}\n\nTo fix this, please confirm your browser can reach {hass_url}. This field is from Settings > System > Network > Home Assistant URL.\n\nIf you are **certain** your browser can reach this URL, you can bypass this warning.",
|
||||
"title": "Alexa Media Player - Unable to Connect to HA URL"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "Yes, OTP code was verified"
|
||||
},
|
||||
"description": "**{email} - alexa.{url}** \nHave you verified the OTP code in Amazon 2SV? \n >OTP Code: {message}",
|
||||
"title": "Alexa Media Player - OTP Confirmation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Alexa Media Player - Reconfiguration",
|
||||
"description": "* Required entry",
|
||||
"data": {
|
||||
"debug": "Advanced debug",
|
||||
"exclude_devices": "or Exclude these devices from all (comma separated)",
|
||||
"extended_entity_discovery": "Include additional sensors, switches and lights",
|
||||
"include_devices": "Only include these devices (comma separated)",
|
||||
"otp_secret": "52-character Authenticator App Key for Amazon 2SV",
|
||||
"public_url": "Public URL shared with external hosted services",
|
||||
"queue_delay": "Delay to queue multiple commands together (seconds)",
|
||||
"scan_interval": "Scheduled polling interval (seconds)",
|
||||
"should_get_network": "Discover Alexa network"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
|
||||
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"force_logout": {
|
||||
"name": "Force Logout",
|
||||
"description": "Force account to logout. Used mainly for debugging.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"name": "Email address",
|
||||
"description": "Accounts to clear. Empty will clear all."
|
||||
}
|
||||
}
|
||||
},
|
||||
"restore_volume": {
|
||||
"name": "Restore Previous Volume",
|
||||
"description": "Restore previous volume level on Alexa media player device",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Select media player:",
|
||||
"description": "Entity to restore the previous volume level on"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_history_records": {
|
||||
"name": "Get History Records",
|
||||
"description": "Parses the history records for the specified device",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Select media player:",
|
||||
"description": "Entity to get the history for"
|
||||
},
|
||||
"entries": {
|
||||
"name": "Number of entries",
|
||||
"description": "Number of entries to get"
|
||||
}
|
||||
}
|
||||
},
|
||||
"update_last_called": {
|
||||
"name": "Update Last Called Sensor",
|
||||
"description": "Forces update of last_called echo device for each Alexa account.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"name": "Email address",
|
||||
"description": "List of Alexa accounts to update. If empty, will update all known accounts."
|
||||
}
|
||||
}
|
||||
},
|
||||
"enable_network_discovery": {
|
||||
"name": "Enable Network Discovery",
|
||||
"description": "Re-enables Alexa network discovery so the next polling cycle will rediscover Alexa devices for the selected accounts.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"name": "Email address",
|
||||
"description": "Optional Alexa account email or list of emails. If empty, all known accounts will be refreshed."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Do not disturb"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Shuffle"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Repeat"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"temperature": {
|
||||
"name": "Temperature"
|
||||
},
|
||||
"air_quality": {
|
||||
"name": "Air quality"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "Carbon monoxide"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Humidity"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "Indoor air quality"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "Particulate matter"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Volatile organic compounds"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Next alarm"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "Next timer"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Next reminder"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"title": "YAML configuration is deprecated",
|
||||
"description": "YAML configuration of Alexa Media Player is deprecated.\nPlease remove `alexa_media` from your configuration, restart Home Assistant and use the UI to configure it instead.\nSettings > Devices & services > Integrations > ADD INTEGRATION"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
"""
|
||||
Alexa Devices Switches.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from alexapy import AlexaAPI
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, NoEntitySpecifiedError
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import (
|
||||
CONF_EMAIL,
|
||||
CONF_EXCLUDE_DEVICES,
|
||||
CONF_INCLUDE_DEVICES,
|
||||
DATA_ALEXAMEDIA,
|
||||
DOMAIN as ALEXA_DOMAIN,
|
||||
hide_email,
|
||||
hide_serial,
|
||||
)
|
||||
from .alexa_entity import parse_power_from_coordinator
|
||||
from .alexa_media import AlexaMedia
|
||||
from .const import CONF_EXTENDED_ENTITY_DISCOVERY
|
||||
from .helpers import _catch_login_errors, add_devices, safe_get
|
||||
|
||||
try:
|
||||
from homeassistant.components.switch import SwitchEntity as SwitchDevice
|
||||
except ImportError:
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Set up the Alexa switch platform."""
|
||||
devices: list[DNDSwitch] = []
|
||||
SWITCH_TYPES = [ # pylint: disable=invalid-name
|
||||
("dnd", DNDSwitch),
|
||||
("shuffle", ShuffleSwitch),
|
||||
("repeat", RepeatSwitch),
|
||||
]
|
||||
account = None
|
||||
if config:
|
||||
account = config.get(CONF_EMAIL)
|
||||
if account is None and discovery_info:
|
||||
account = safe_get(discovery_info, ["config", CONF_EMAIL])
|
||||
if account is None:
|
||||
raise ConfigEntryNotReady
|
||||
include_filter = config.get(CONF_INCLUDE_DEVICES, [])
|
||||
exclude_filter = config.get(CONF_EXCLUDE_DEVICES, [])
|
||||
account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account]
|
||||
_LOGGER.debug("%s: Loading switches", hide_email(account))
|
||||
if "switch" not in account_dict["entities"]:
|
||||
hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"]["switch"] = {}
|
||||
for key, _ in account_dict["devices"]["media_player"].items():
|
||||
if key not in account_dict["entities"]["media_player"]:
|
||||
_LOGGER.debug(
|
||||
"%s: Media player %s not loaded yet; delaying load",
|
||||
hide_email(account),
|
||||
hide_serial(key),
|
||||
)
|
||||
raise ConfigEntryNotReady
|
||||
if key not in (
|
||||
hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"]["switch"]
|
||||
):
|
||||
hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"]["switch"][
|
||||
key
|
||||
] = {}
|
||||
for switch_key, class_ in SWITCH_TYPES:
|
||||
if (
|
||||
switch_key == "dnd"
|
||||
and not safe_get(account_dict, ["devices", "switch", key, "dnd"])
|
||||
) or (
|
||||
switch_key in ["shuffle", "repeat"]
|
||||
and "MUSIC_SKILL"
|
||||
not in account_dict["devices"]["media_player"]
|
||||
.get(key, {})
|
||||
.get("capabilities", {})
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s: Skipping %s for %s",
|
||||
hide_email(account),
|
||||
switch_key,
|
||||
hide_serial(key),
|
||||
)
|
||||
continue
|
||||
alexa_client = class_(
|
||||
account_dict["entities"]["media_player"][key]
|
||||
) # type: AlexaMediaSwitch
|
||||
_LOGGER.debug(
|
||||
"%s: Found %s %s switch with status: %s",
|
||||
hide_email(account),
|
||||
hide_serial(key),
|
||||
switch_key,
|
||||
alexa_client.is_on,
|
||||
)
|
||||
devices.append(alexa_client)
|
||||
(
|
||||
hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"][
|
||||
"switch"
|
||||
][key][switch_key]
|
||||
) = alexa_client
|
||||
else:
|
||||
for alexa_client in hass.data[DATA_ALEXAMEDIA]["accounts"][account][
|
||||
"entities"
|
||||
]["switch"][key].values():
|
||||
_LOGGER.debug(
|
||||
"%s: Skipping already added device: %s",
|
||||
hide_email(account),
|
||||
alexa_client,
|
||||
)
|
||||
# Add Amazon Smart Plug devices
|
||||
switch_entities = safe_get(account_dict, ["devices", "smart_switch"], [])
|
||||
hue_emulated_enabled = "emulated_hue" in hass.config.as_dict().get(
|
||||
"components", set()
|
||||
)
|
||||
if switch_entities and account_dict["options"].get(CONF_EXTENDED_ENTITY_DISCOVERY):
|
||||
for switch_entity in switch_entities:
|
||||
if not (switch_entity["is_hue_v1"] and hue_emulated_enabled):
|
||||
_LOGGER.debug(
|
||||
"Creating entity %s for a switch with name %s",
|
||||
hide_serial(switch_entity["id"]),
|
||||
switch_entity["name"],
|
||||
)
|
||||
coordinator = account_dict["coordinator"]
|
||||
switch = SmartSwitch(
|
||||
coordinator, account_dict["login_obj"], switch_entity
|
||||
)
|
||||
account_dict["entities"]["smart_switch"].append(switch)
|
||||
devices.append(switch)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Switch '%s' has not been added because it may originate from emulated_hue",
|
||||
switch_entity["name"],
|
||||
)
|
||||
return await add_devices(
|
||||
hide_email(account),
|
||||
devices,
|
||||
add_devices_callback,
|
||||
include_filter,
|
||||
exclude_filter,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the Alexa switch platform by config_entry."""
|
||||
return await async_setup_platform(
|
||||
hass, config_entry.data, async_add_devices, discovery_info=None
|
||||
)
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
account = entry.data[CONF_EMAIL]
|
||||
_LOGGER.debug("Attempting to unload switch")
|
||||
account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account]
|
||||
for key, switches in account_dict["entities"]["switch"].items():
|
||||
for device in switches[key].values():
|
||||
_LOGGER.debug("Removing %s", device)
|
||||
await device.async_remove()
|
||||
return True
|
||||
|
||||
|
||||
class AlexaMediaSwitch(SwitchDevice, AlexaMedia):
|
||||
"""Representation of a Alexa Media switch."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client,
|
||||
switch_property: str,
|
||||
switch_function: str,
|
||||
unique_id_suffix: str = "switch",
|
||||
):
|
||||
"""Initialize the Alexa Switch device."""
|
||||
# Class info
|
||||
self._client = client
|
||||
self._unique_id_suffix = unique_id_suffix
|
||||
self._switch_property = switch_property
|
||||
self._switch_function = switch_function
|
||||
super().__init__(client, client._login)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Store register state change callback."""
|
||||
try:
|
||||
if not self.enabled:
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
# Register event handler on bus
|
||||
self._listener = async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{ALEXA_DOMAIN}_{hide_email(self.email)}"[0:32],
|
||||
self._handle_event,
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Prepare to remove entity."""
|
||||
# Register event handler on bus
|
||||
self._listener()
|
||||
|
||||
def _handle_event(self, event):
|
||||
"""Handle events.
|
||||
|
||||
This will update PUSH_MEDIA_QUEUE_CHANGE events to see if the switch
|
||||
should be updated.
|
||||
"""
|
||||
try:
|
||||
if not self.enabled:
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
if "queue_state" in event:
|
||||
queue_state = event["queue_state"]
|
||||
if queue_state["dopplerId"]["deviceSerialNumber"] == self._client.unique_id:
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@_catch_login_errors
|
||||
async def _set_switch(self, state, **kwargs):
|
||||
# pylint: disable=unused-argument
|
||||
try:
|
||||
if not self.enabled:
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
success = await getattr(self.alexa_api, self._switch_function)(state)
|
||||
# if function returns success, make immediate state change
|
||||
if success:
|
||||
setattr(self._client, self._switch_property, state)
|
||||
_LOGGER.debug(
|
||||
"Setting %s to %s",
|
||||
self.name,
|
||||
getattr(self._client, self._switch_property),
|
||||
)
|
||||
self.schedule_update_ha_state()
|
||||
elif self.should_poll:
|
||||
# if we need to poll, refresh media_client
|
||||
_LOGGER.debug(
|
||||
"Requesting update of %s due to %s switch to %s",
|
||||
self._client,
|
||||
self._unique_id_suffix,
|
||||
state,
|
||||
)
|
||||
await self._client.async_update()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if on."""
|
||||
return self.available and getattr(self._client, self._switch_property)
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn on switch."""
|
||||
await self._set_switch(True, **kwargs)
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn off switch."""
|
||||
await self._set_switch(False, **kwargs)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return the availability of the switch."""
|
||||
return (
|
||||
self._client.available
|
||||
and getattr(self._client, self._switch_property) is not None
|
||||
)
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Return whether the state is an assumed_state."""
|
||||
return self._client.assumed_state
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID."""
|
||||
return self._client.unique_id + "_" + self._unique_id_suffix
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device_class of the switch."""
|
||||
return "switch"
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
"""Return whether the switch should be hidden from the UI."""
|
||||
return not self.available
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return True
|
||||
|
||||
@_catch_login_errors
|
||||
async def async_update(self):
|
||||
"""Update state."""
|
||||
try:
|
||||
if not self.enabled:
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
self.schedule_update_ha_state()
|
||||
except NoEntitySpecifiedError:
|
||||
pass # we ignore this due to a harmless startup race condition
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device_info for device registry."""
|
||||
return {
|
||||
"identifiers": {(ALEXA_DOMAIN, self._client.unique_id)},
|
||||
"via_device": (ALEXA_DOMAIN, self._client.unique_id),
|
||||
}
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of the switch."""
|
||||
return self._icon()
|
||||
|
||||
def _icon(self, on=None, off=None): # pylint: disable=invalid-name
|
||||
return on if self.is_on else off
|
||||
|
||||
|
||||
class DNDSwitch(AlexaMediaSwitch):
|
||||
"""Representation of a Alexa Media Do Not Disturb switch."""
|
||||
|
||||
_attr_translation_key = "do_not_disturb"
|
||||
|
||||
def __init__(self, client):
|
||||
"""Initialize the Alexa Switch."""
|
||||
# Class info
|
||||
super().__init__(
|
||||
client,
|
||||
"dnd_state",
|
||||
"set_dnd_state",
|
||||
"do not disturb", # Keep original suffix for backward compatibility
|
||||
)
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of the switch."""
|
||||
return super()._icon("mdi:minus-circle", "mdi:minus-circle-off")
|
||||
|
||||
@property
|
||||
def entity_category(self):
|
||||
"""Return the entity category of the switch."""
|
||||
return EntityCategory.CONFIG
|
||||
|
||||
def _handle_event(self, event):
|
||||
"""Handle events."""
|
||||
try:
|
||||
if not self.enabled:
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
if "dnd_update" in event:
|
||||
result = list(
|
||||
filter(
|
||||
lambda x: x["deviceSerialNumber"]
|
||||
== self._client.device_serial_number,
|
||||
event["dnd_update"],
|
||||
)
|
||||
)
|
||||
if result:
|
||||
state = result[0]["enabled"] is True
|
||||
if state != self.is_on:
|
||||
_LOGGER.debug("Detected %s changed to %s", self, state)
|
||||
setattr(self._client, self._switch_property, state)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
class ShuffleSwitch(AlexaMediaSwitch):
|
||||
"""Representation of a Alexa Media Shuffle switch."""
|
||||
|
||||
_attr_translation_key = "shuffle"
|
||||
|
||||
def __init__(self, client):
|
||||
"""Initialize the Alexa Switch."""
|
||||
# Class info
|
||||
super().__init__(client, "shuffle", "shuffle", "shuffle")
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of the switch."""
|
||||
return super()._icon("mdi:shuffle", "mdi:shuffle-disabled")
|
||||
|
||||
@property
|
||||
def entity_category(self):
|
||||
"""Return the entity category of the switch."""
|
||||
return EntityCategory.CONFIG
|
||||
|
||||
|
||||
class RepeatSwitch(AlexaMediaSwitch):
|
||||
"""Representation of a Alexa Media Repeat switch."""
|
||||
|
||||
_attr_translation_key = "repeat"
|
||||
|
||||
def __init__(self, client):
|
||||
"""Initialize the Alexa Switch."""
|
||||
# Class info
|
||||
super().__init__(client, "repeat_state", "repeat", "repeat")
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of the switch."""
|
||||
return super()._icon("mdi:repeat", "mdi:repeat-off")
|
||||
|
||||
@property
|
||||
def entity_category(self):
|
||||
"""Return the entity category of the switch."""
|
||||
return EntityCategory.CONFIG
|
||||
|
||||
|
||||
class SmartSwitch(CoordinatorEntity, SwitchDevice):
|
||||
def __init__(self, coordinator, login, details):
|
||||
"""Initialize alexa light entity."""
|
||||
super().__init__(coordinator)
|
||||
self.alexa_entity_id = details["id"]
|
||||
self._name = details["name"]
|
||||
self._login = login
|
||||
|
||||
# Store the requested state from the last call to _set_state
|
||||
# This is so that no new network call is needed just to get values that are already known
|
||||
# This is useful because refreshing the full state can take a bit when many switches are in play.
|
||||
# Especially since Alexa actually polls the switches and that appears to be error-prone with some Zigbee lights.
|
||||
# That delay(1-5s in practice) causes the UI controls to jump all over the place after _set_state
|
||||
self._requested_state_at = None # When was state last set in UTC
|
||||
self._requested_power = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique id."""
|
||||
return self.alexa_entity_id
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return whether on."""
|
||||
power = parse_power_from_coordinator(
|
||||
self.coordinator, self.alexa_entity_id, self._requested_state_at
|
||||
)
|
||||
if power is None:
|
||||
return self._requested_power if self._requested_power is not None else False
|
||||
return power == "ON"
|
||||
|
||||
@property
|
||||
def assumed_state(self) -> bool:
|
||||
"""Return whether state is assumed."""
|
||||
last_refresh_success = (
|
||||
self.coordinator.data and self.alexa_entity_id in self.coordinator.data
|
||||
)
|
||||
return not last_refresh_success
|
||||
|
||||
async def _set_state(self, power_on: bool) -> None:
|
||||
response = await AlexaAPI.set_light_state(
|
||||
self._login,
|
||||
self.alexa_entity_id,
|
||||
power_on,
|
||||
)
|
||||
|
||||
if not isinstance(response, dict):
|
||||
# If something failed any state is possible, fallback to a full refresh
|
||||
await self.coordinator.async_request_refresh()
|
||||
return
|
||||
|
||||
control_responses = response.get("controlResponses", [])
|
||||
for ctrl_resp in control_responses:
|
||||
if ctrl_resp.get("code") != "SUCCESS":
|
||||
# If something failed any state is possible, fallback to a full refresh
|
||||
await self.coordinator.async_request_refresh()
|
||||
return
|
||||
|
||||
self._requested_power = power_on
|
||||
self._requested_state_at = datetime.datetime.now(
|
||||
datetime.timezone.utc
|
||||
) # must be set last so that previous getters work properly
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
# Confirm quickly, but debounce to avoid spamming across multiple entities.
|
||||
account = self.hass.data[DATA_ALEXAMEDIA]["accounts"].get(self._login.email)
|
||||
if account:
|
||||
debouncer = account.get("confirm_refresh_debouncer")
|
||||
if debouncer:
|
||||
await debouncer.async_call()
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn on."""
|
||||
await self._set_state(True)
|
||||
|
||||
async def async_turn_off(self, **kwargs): # pylint:disable=unused-argument
|
||||
"""Turn off."""
|
||||
await self._set_state(False)
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "تم الكشف عن صفحة نسيت كلمة المرور. عادةً ما يكون هذا نتيجة لمحاولات تسجيل دخول فاشلة كثيرة. قد تتطلب أمازون اتخاذ إجراء قبل محاولة تسجيل الدخول مرة أخرى.",
|
||||
"login_failed": "فشل تسجيل الدخول إلى Alexa Media Player.",
|
||||
"reauth_successful": "تمت إعادة التحقق من Alexa Media Player بنجاح. يرجى تجاهل رسالة \"تم الإلغاء\" من Home Assistant."
|
||||
},
|
||||
"error": {
|
||||
"2fa_key_invalid": "{otp_secret} غير صالح",
|
||||
"connection_error": "خطأ في الاتصال؛ تحقق من الشبكة وأعد المحاولة",
|
||||
"identifier_exists": "البريد الإلكتروني لرابط Alexa مسجل مسبقًا",
|
||||
"invalid_auth": "لم تنجح عملية تسجيل الدخول. يرجى التحقق من بريدك الإلكتروني وكلمة المرور ومفتاح المصادقة.",
|
||||
"invalid_credentials": "بيانات اعتماد غير صالحة",
|
||||
"invalid_url": "رابط غير صالح: {message}",
|
||||
"oauth_error": "تعذر إكمال تسجيل الدخول عبر OAuth. يرجى المحاولة مرة أخرى.",
|
||||
"unable_to_connect_hass_url": "غير قادر على الاتصال بالرابط المحلي لـ Home Assistant. يرجى التحقق من العنوان ضمن:\nالإعدادات > النظام > الشبكة > رابط Home Assistant > الشبكة المحلية.",
|
||||
"unknown_error": "خطأ غير معروف: {message}"
|
||||
},
|
||||
"step": {
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "تجاهل ومتابعة - أتفهم أنه لا يوجد دعم لمشاكل تسجيل الدخول لتجاوز هذا التحذير."
|
||||
},
|
||||
"description": "لا يمكن لخادم Home Assistant الاتصال بالرابط المقدم: {hass_url}. \n > {error} \n \n لإصلاح هذه المشكلة، يرجى التأكد من أن متصفحك يمكنه الوصول إلى {hass_url}. هذا الحقل موجود في الإعدادات > النظام > الشبكة > رابط Home Assistant. \n \n إذا كنت **متأكدًا** من أن متصفحك يمكنه الوصول إلى هذا الرابط، فيمكنك تجاوز هذا التحذير.",
|
||||
"title": "Alexa Media Player - غير قادر على الاتصال برابط Home Assistant"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "نعم، تم التحقق من رمز OTP"
|
||||
},
|
||||
"description": "** {email} - alexa. {url} ** \n هل قمت بالتحقق من رمز OTP في Amazon 2SV؟ \n >رمز OTP: {message}",
|
||||
"title": "Alexa Media Player - تأكيد OTP"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "تصحيح الأخطاء المتقدم",
|
||||
"email": "البريد الإلكتروني",
|
||||
"exclude_devices": "أو استبعاد هذه الأجهزة من الكل (مفصولة بفواصل)",
|
||||
"extended_entity_discovery": "أضف أجهزة استشعار ومفاتيح وأضواء إضافية",
|
||||
"hass_url": "رابط الشبكة المحلية للوصول إلى Home Assistant",
|
||||
"include_devices": "تضمين هذه الأجهزة فقط (مفصولة بفواصل)",
|
||||
"otp_secret": "مفتاح تطبيق المصادقة المؤلف من 52 حرفًا للتحقق الثنائي من أمازون",
|
||||
"password": "كلمة المرور",
|
||||
"public_url": "رابط عام مشترك مع خدمات مستضافة خارجية",
|
||||
"queue_delay": "تأخير وضع أوامر متعددة في قائمة الانتظار معًا (بالثواني)",
|
||||
"scan_interval": "الفاصل الزمني للإستطلاع المجدول (بالثواني)",
|
||||
"securitycode": "كلمة مرور لمرة واحدة (OTP)",
|
||||
"should_get_network": "اكتشف شبكة اليكسا",
|
||||
"url": "نطاق منطقة Amazon (على سبيل المثال، amazon.co.uk)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
|
||||
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Air quality"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "Carbon monoxide"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Humidity"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "Indoor air quality"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "Particulate matter"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Volatile organic compounds"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Next alarm"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Next reminder"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "Next timer"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperature"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Do not disturb"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Repeat"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Shuffle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"description": "تم إيقاف استخدام ملف YAML لتكوين مشغل وسائط Alexa.\n\nيرجى إزالة `alexa_media` من ملف التكوين، وإعادة تشغيل Home Assistant، واستخدام واجهة المستخدم لتكوينه.\n\nالإعدادات > الأجهزة والخدمات > التكاملات > إضافة تكامل",
|
||||
"title": "إعدادات YAML غير معتمد"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"debug": "تصحيح الأخطاء المتقدم",
|
||||
"exclude_devices": "أو استبعاد هذه الأجهزة من الكل (مفصولة بفواصل)",
|
||||
"extended_entity_discovery": "أضف أجهزة استشعار ومفاتيح وأضواء إضافية",
|
||||
"include_devices": "تضمين هذه الأجهزة فقط (مفصولة بفواصل)",
|
||||
"otp_secret": "52-character Authenticator App Key for Amazon 2SV",
|
||||
"public_url": "رابط عام مشترك مع خدمات مستضافة خارجية",
|
||||
"queue_delay": "تأخير وضع أوامر متعددة في قائمة الانتظار معًا (بالثواني)",
|
||||
"scan_interval": "تكرار الاستطلاع المجدول (بالثواني)",
|
||||
"should_get_network": "اكتشف شبكة اليكسا"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
|
||||
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
},
|
||||
"description": "* إدخالات مطلوبة",
|
||||
"title": "Alexa Media Player - إعادة التكوين"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"enable_network_discovery": {
|
||||
"description": "يعيد تفعيل اكتشاف شبكة Alexa بحيث تعيد دورة الاستطلاع التالية اكتشاف أجهزة Alexa للحسابات المحددة.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "بريد إلكتروني اختياري لحساب أليكسا أو قائمة عناوين البريد الإلكتروني. في حال عدم وجودها، سيتم تحديث جميع الحسابات المعروفة.",
|
||||
"name": "عنوان البريد الإلكتروني"
|
||||
}
|
||||
},
|
||||
"name": "تفعيل اكتشاف الشبكة"
|
||||
},
|
||||
"force_logout": {
|
||||
"description": "إجبار الحساب على تسجيل الخروج. يُستخدم بشكل أساسي لأغراض التصحيح.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "الحسابات المراد مسحها. إذا كانت فارغة سيتم مسح الكل.",
|
||||
"name": "البريد الإلكتروني"
|
||||
}
|
||||
},
|
||||
"name": "فرض تسجيل الخروج"
|
||||
},
|
||||
"get_history_records": {
|
||||
"description": "يقوم بتحليل سجلات التاريخ للجهاز المحدد",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "الكيان الذي سيتم الحصول منه على السجل",
|
||||
"name": "حدد مشغل الوسائط:"
|
||||
},
|
||||
"entries": {
|
||||
"description": "عدد الإدخالات المطلوب الحصول عليها",
|
||||
"name": "عدد الإدخالات"
|
||||
}
|
||||
},
|
||||
"name": "الحصول على سجلات التاريخ"
|
||||
},
|
||||
"restore_volume": {
|
||||
"description": "استعادة مستوى الصوت السابق على جهاز Alexa media player",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "العنصر لاستعادة مستوى الصوت السابق عليه",
|
||||
"name": "اختر مشغل الوسائط:"
|
||||
}
|
||||
},
|
||||
"name": "استعادة مستوى الصوت السابق"
|
||||
},
|
||||
"update_last_called": {
|
||||
"description": "فرض التحديث لـ \"آخر إتصال\" من جهاز echo لجميع حسابات Alexa.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "قائمة حسابات Alexa للتحديث. إذا كانت فارغة، سيتم تحديث جميع الحسابات المعروفة.",
|
||||
"name": "البريد الإلكتروني"
|
||||
}
|
||||
},
|
||||
"name": "تحديث مستشعر \"آخر إتصال\""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "The Forgot Password page was detected. This normally is the result of too many failed logins. Amazon may require action before a relogin can be attempted.",
|
||||
"login_failed": "Alexa Media Player failed to login.",
|
||||
"reauth_successful": "Alexa Media Player successfully reauthenticated. Please ignore the \"Aborted\" message from HA."
|
||||
},
|
||||
"error": {
|
||||
"2fa_key_invalid": "{otp_secret} is invalid",
|
||||
"connection_error": "Error connecting; check network and retry",
|
||||
"identifier_exists": "Email for Alexa URL already registered",
|
||||
"invalid_auth": "Login was not successful. Please double-check your email, password, and Authenticator key.",
|
||||
"invalid_credentials": "Invalid credentials",
|
||||
"invalid_url": "URL is invalid: {message}",
|
||||
"oauth_error": "Could not complete OAuth login. Please try again.",
|
||||
"unable_to_connect_hass_url": "Unable to connect to Home Assistant Local URL. Please check the URL under Settings > System > Network > Home Assistant URL > Local network",
|
||||
"unknown_error": "Unknown error: {message}"
|
||||
},
|
||||
"step": {
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "Ignore and Continue - I understand that no support for login issues are provided for bypassing this warning."
|
||||
},
|
||||
"description": "The HA server cannot connect to the URL provided: {hass_url}.\n> {error}\n\nTo fix this, please confirm your browser can reach {hass_url}. This field is from Settings > System > Network > Home Assistant URL.\n\nIf you are **certain** your browser can reach this URL, you can bypass this warning.",
|
||||
"title": "Alexa Media Player - Unable to Connect to HA URL"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "Yes, OTP code was verified"
|
||||
},
|
||||
"description": "**{email} - alexa.{url}** \nHave you verified the OTP code in Amazon 2SV? \n >OTP Code: {message}",
|
||||
"title": "Alexa Media Player - OTP Confirmation"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "Advanced debug",
|
||||
"email": "Email Address",
|
||||
"exclude_devices": "or Exclude these devices from all (comma separated)",
|
||||
"extended_entity_discovery": "Include additional sensors, switches and lights",
|
||||
"hass_url": "Local network URL to access Home Assistant",
|
||||
"include_devices": "Only include these devices (comma separated)",
|
||||
"otp_secret": "52-character Authenticator App Key for Amazon 2SV",
|
||||
"password": "Password",
|
||||
"public_url": "Public URL shared with external hosted services",
|
||||
"queue_delay": "Delay to queue multiple commands together (seconds)",
|
||||
"scan_interval": "Scheduled polling interval (seconds)",
|
||||
"securitycode": "One-time password (OTP)",
|
||||
"should_get_network": "Discover Alexa network",
|
||||
"url": "Amazon region domain (e.g., amazon.co.uk)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
|
||||
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Air quality"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "Carbon monoxide"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Humidity"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "Indoor air quality"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "Particulate matter"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Volatile organic compounds"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Next alarm"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Next reminder"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "Next timer"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperature"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Do not disturb"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Repeat"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Shuffle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"description": "YAML configuration of Alexa Media Player is deprecated.\nPlease remove `alexa_media` from your configuration, restart Home Assistant and use the UI to configure it instead.\nSettings > Devices & services > Integrations > ADD INTEGRATION",
|
||||
"title": "YAML configuration is deprecated"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"debug": "Advanced debug",
|
||||
"exclude_devices": "or Exclude these devices from all (comma separated)",
|
||||
"extended_entity_discovery": "Include additional sensors, switches and lights",
|
||||
"include_devices": "Only include these devices (comma separated)",
|
||||
"otp_secret": "52-character Authenticator App Key for Amazon 2SV",
|
||||
"public_url": "Public URL shared with external hosted services",
|
||||
"queue_delay": "Delay to queue multiple commands together (seconds)",
|
||||
"scan_interval": "Scheduled polling interval (seconds)",
|
||||
"should_get_network": "Discover Alexa network"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
|
||||
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
},
|
||||
"description": "* Required entry",
|
||||
"title": "Alexa Media Player - Reconfiguration"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"enable_network_discovery": {
|
||||
"description": "Re-enables Alexa network discovery so the next polling cycle will rediscover Alexa devices for the selected accounts.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Optional Alexa account email or list of emails. If empty, all known accounts will be refreshed.",
|
||||
"name": "Email address"
|
||||
}
|
||||
},
|
||||
"name": "Enable Network Discovery"
|
||||
},
|
||||
"force_logout": {
|
||||
"description": "Force account to logout. Used mainly for debugging.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Accounts to clear. Empty will clear all.",
|
||||
"name": "Email address"
|
||||
}
|
||||
},
|
||||
"name": "Force Logout"
|
||||
},
|
||||
"get_history_records": {
|
||||
"description": "Parses the history records for the specified device",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entity to get the history for",
|
||||
"name": "Select media player:"
|
||||
},
|
||||
"entries": {
|
||||
"description": "Number of entries to get",
|
||||
"name": "Number of entries"
|
||||
}
|
||||
},
|
||||
"name": "Get History Records"
|
||||
},
|
||||
"restore_volume": {
|
||||
"description": "Restore previous volume level on Alexa media player device",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entity to restore the previous volume level on",
|
||||
"name": "Select media player:"
|
||||
}
|
||||
},
|
||||
"name": "Restore Previous Volume"
|
||||
},
|
||||
"update_last_called": {
|
||||
"description": "Forces update of last_called echo device for each Alexa account.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "List of Alexa accounts to update. If empty, will update all known accounts.",
|
||||
"name": "Email address"
|
||||
}
|
||||
},
|
||||
"name": "Update Last Called Sensor"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "The Forgot Password page was detected. This normally is the result of too many failed logins. Amazon may require action before a relogin can be attempted.",
|
||||
"login_failed": "Alexa Media Player failed to login.",
|
||||
"reauth_successful": "Alexa Media Player successfully reauthenticated. Please ignore the \"Aborted\" message from HA."
|
||||
},
|
||||
"error": {
|
||||
"2fa_key_invalid": "{otp_secret} is invalid",
|
||||
"connection_error": "Error connecting; check network and retry",
|
||||
"identifier_exists": "Email for Alexa URL already registered",
|
||||
"invalid_auth": "Login was not successful. Please double-check your email, password, and Authenticator key.",
|
||||
"invalid_credentials": "Invalid credentials",
|
||||
"invalid_url": "URL is invalid: {message}",
|
||||
"oauth_error": "Could not complete OAuth login. Please try again.",
|
||||
"unable_to_connect_hass_url": "Unable to connect to Home Assistant Local URL. Please check the URL under Settings > System > Network > Home Assistant URL > Local network",
|
||||
"unknown_error": "Unknown error: {message}"
|
||||
},
|
||||
"step": {
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "Ignore and Continue - I understand that no support for login issues are provided for bypassing this warning."
|
||||
},
|
||||
"description": "The HA server cannot connect to the URL provided: {hass_url}.\n> {error}\n\nTo fix this, please confirm your browser can reach {hass_url}. This field is from Settings > System > Network > Home Assistant URL.\n\nIf you are **certain** your browser can reach this URL, you can bypass this warning.",
|
||||
"title": "Alexa Media Player - Unable to Connect to HA URL"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "Yes, OTP code was verified"
|
||||
},
|
||||
"description": "**{email} - alexa.{url}** \nHave you verified the OTP code in Amazon 2SV? \n >OTP Code: {message}",
|
||||
"title": "Alexa Media Player - OTP Confirmation"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "Advanced debug",
|
||||
"email": "Email Address",
|
||||
"exclude_devices": "or Exclude these devices from all (comma separated)",
|
||||
"extended_entity_discovery": "Include additional sensors, switches and lights",
|
||||
"hass_url": "Local network URL to access Home Assistant",
|
||||
"include_devices": "Only include these devices (comma separated)",
|
||||
"otp_secret": "52-character Authenticator App Key for Amazon 2SV",
|
||||
"password": "Password",
|
||||
"public_url": "Public URL shared with external hosted services",
|
||||
"queue_delay": "Delay to queue multiple commands together (seconds)",
|
||||
"scan_interval": "Scheduled polling interval (seconds)",
|
||||
"securitycode": "One-time password (OTP)",
|
||||
"should_get_network": "Discover Alexa network",
|
||||
"url": "Amazon region domain (e.g., amazon.co.uk)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
|
||||
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Air quality"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "Carbon monoxide"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Humidity"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "Indoor air quality"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "Particulate matter"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Volatile organic compounds"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Next alarm"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Next reminder"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "Next timer"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperature"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Do not disturb"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Repeat"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Shuffle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"description": "YAML configuration of Alexa Media Player is deprecated.\nPlease remove `alexa_media` from your configuration, restart Home Assistant and use the UI to configure it instead.\nSettings > Devices & services > Integrations > ADD INTEGRATION",
|
||||
"title": "YAML configuration is deprecated"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"debug": "Advanced debug",
|
||||
"exclude_devices": "or Exclude these devices from all (comma separated)",
|
||||
"extended_entity_discovery": "Include additional sensors, switches and lights",
|
||||
"include_devices": "Only include these devices (comma separated)",
|
||||
"otp_secret": "52-character Authenticator App Key for Amazon 2SV",
|
||||
"public_url": "Public URL shared with external hosted services",
|
||||
"queue_delay": "Delay to queue multiple commands together (seconds)",
|
||||
"scan_interval": "Scheduled polling interval (seconds)",
|
||||
"should_get_network": "Discover Alexa network"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
|
||||
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
},
|
||||
"description": "* Required entry",
|
||||
"title": "Alexa Media Player - Reconfiguration"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"enable_network_discovery": {
|
||||
"description": "Re-enables Alexa network discovery so the next polling cycle will rediscover Alexa devices for the selected accounts.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Optional Alexa account email or list of emails. If empty, all known accounts will be refreshed.",
|
||||
"name": "Email address"
|
||||
}
|
||||
},
|
||||
"name": "Enable Network Discovery"
|
||||
},
|
||||
"force_logout": {
|
||||
"description": "Force account to logout. Used mainly for debugging.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Accounts to clear. Empty will clear all.",
|
||||
"name": "Email address"
|
||||
}
|
||||
},
|
||||
"name": "Force Logout"
|
||||
},
|
||||
"get_history_records": {
|
||||
"description": "Parses the history records for the specified device",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entity to get the history for",
|
||||
"name": "Select media player:"
|
||||
},
|
||||
"entries": {
|
||||
"description": "Number of entries to get",
|
||||
"name": "Number of entries"
|
||||
}
|
||||
},
|
||||
"name": "Get History Records"
|
||||
},
|
||||
"restore_volume": {
|
||||
"description": "Restore previous volume level on Alexa media player device",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entity to restore the previous volume level on",
|
||||
"name": "Select media player:"
|
||||
}
|
||||
},
|
||||
"name": "Restore Previous Volume"
|
||||
},
|
||||
"update_last_called": {
|
||||
"description": "Forces update of last_called echo device for each Alexa account.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "List of Alexa accounts to update. If empty, will update all known accounts.",
|
||||
"name": "Email address"
|
||||
}
|
||||
},
|
||||
"name": "Update Last Called Sensor"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "Die Seite 'Passwort vergessen' wurde erkannt. Dies ist normalerweise das Ergebnis zu vieler fehlgeschlagener Anmeldeversuche. Amazon könnte eine Aktion verlangen, bevor ein erneuter Login versucht werden kann.",
|
||||
"login_failed": "Alexa Media Player konnte nicht angemeldet werden.",
|
||||
"reauth_successful": "Alexa Media Player wurde erfolgreich neu authentifiziert. Bitte ignorieren Sie die Meldung „Abgebrochen“ von Home Assistant."
|
||||
},
|
||||
"error": {
|
||||
"2fa_key_invalid": "{otp_secret} ist ungültig",
|
||||
"connection_error": "Verbindungsfehler; Netzwerk prüfen und erneut versuchen",
|
||||
"identifier_exists": "Diese E-Mail-Adresse ist bereits registriert",
|
||||
"invalid_auth": "Die Anmeldung ist fehlgeschlagen. Bitte überprüfen Sie Ihre E-Mail-Adresse, Ihr Passwort und Ihren Authentifizierungsschlüssel.",
|
||||
"invalid_credentials": "Ungültige Zugangsdaten",
|
||||
"invalid_url": "URL ist ungültig: {message}",
|
||||
"oauth_error": "Die OAuth-Anmeldung konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut.",
|
||||
"unable_to_connect_hass_url": "Es konnte keine Verbindung zur lokalen Home Assistant-URL hergestellt werden. Bitte überprüfen Sie die URL unter Einstellungen > System > Netzwerk > Home Assistant-URL > Lokales Netzwerk.",
|
||||
"unknown_error": "Unbekannter Fehler: {message}"
|
||||
},
|
||||
"step": {
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "Ignorieren und Fortfahren - Ich verstehe, dass keine Unterstützung für Anmeldeprobleme beim Umgehen dieser Warnung angeboten wird."
|
||||
},
|
||||
"description": "Der HA-Server kann keine Verbindung zur bereitgestellten URL herstellen: {hass_url}.\n> {error}\n\nUm dies zu beheben, bestätigen Sie bitte, dass Ihr **HA-Server** {hass_url} erreichen kann. Dieses Feld stammt aus der externen URL unter Konfiguration -> Allgemein, aber Sie können auch Ihre interne URL ausprobieren.\n\nWenn Sie **sicher** sind, dass Ihr Client diese URL erreichen kann, können Sie diese Warnung ignorieren und fortsetzen.",
|
||||
"title": "Alexa Media Player - Keine Verbindung zur Home Assistant-URL möglich"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "Ja, der OTP-Code wurde verifiziert."
|
||||
},
|
||||
"description": "**{email} - alexa.{url}** \nHaben Sie erfolgreich einen OTP-Code aus dem integrierten 2FA-App-Schlüssel mit Amazon bestätigt?\n >OTP-Code {message}",
|
||||
"title": "Alexa Media Player - OTP-Bestätigung"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "Erweitertes Debugging",
|
||||
"email": "E-Mail-Adresse",
|
||||
"exclude_devices": "oder Diese Geräte von allen ausschließen (durch Komma getrennt)",
|
||||
"extended_entity_discovery": "Fügen Sie zusätzliche Sensoren, Schalter und Leuchten hinzu.",
|
||||
"hass_url": "Lokale Netzwerk-URL für den Zugriff auf Home Assistant",
|
||||
"include_devices": "Eingebundene Geräte (Komma getrennt)",
|
||||
"otp_secret": "52-stelliger Authenticator-App Schlüssel für Amazon 2SV",
|
||||
"password": "Passwort",
|
||||
"public_url": "Öffentliche URL, die mit extern gehosteten Diensten geteilt wird",
|
||||
"queue_delay": "Verzögerung beim Zusammenführen mehrerer Befehle in die Warteschlange (Sekunden)",
|
||||
"scan_interval": "Geplantes Abfrageintervall (Sekunden)",
|
||||
"securitycode": "Einmalpasswort (OTP)",
|
||||
"should_get_network": "Entdecken Sie das Alexa-Netzwerk",
|
||||
"url": "Amazon Region (z.B. amazon.de)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Ermöglicht eine sehr ausführliche Protokollierung auf Trace-Ebene für die erweiterte Fehlerbehebung. \n Aufgrund des erhöhten Protokollvolumens wird dies für den Normalbetrieb nicht empfohlen. \n Stellen Sie sicher, dass die Protokollierungsstufe auf DEBUG eingestellt ist, um die vollständige Ausgabe zu erhalten.",
|
||||
"otp_secret": "Beispiel: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Luftqualität"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "Kohlenmonoxid"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Luftfeuchtigkeit"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "Innenraumluftqualität"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "Feinstaub"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Flüchtige organische Verbindungen"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Nächster Alarm"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Nächste Erinnerung"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "Nächster Timer"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperatur"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Bitte nicht stören"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Wiederholen"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Shuffle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"description": "Die YAML-Konfiguration des Alexa Media Players ist veraltet.\nBitte entfernen Sie `alexa_media` aus Ihrer Konfiguration, starten Sie Home Assistant neu und verwenden Sie stattdessen die Benutzeroberfläche zur Konfiguration.\nEinstellungen > Geräte & Dienste > Integrationen > INTEGRATION HINZUFÜGEN",
|
||||
"title": "Die YAML-Konfiguration ist veraltet"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"debug": "Erweitertes Debugging",
|
||||
"exclude_devices": "oder Diese Geräte von allen ausschließen (durch Komma getrennt)",
|
||||
"extended_entity_discovery": "Fügen Sie zusätzliche Sensoren, Schalter und Leuchten hinzu.",
|
||||
"include_devices": "Nur diese Geräte angeben (durch Komma getrennt)",
|
||||
"otp_secret": "52-stelliger Authenticator-App Schlüssel für Amazon 2SV",
|
||||
"public_url": "Öffentliche URL, die mit externen gehosteten Diensten geteilt wird",
|
||||
"queue_delay": "Verzögerung beim Zusammenführen mehrerer Befehle in die Warteschlange (Sekunden)",
|
||||
"scan_interval": "Geplante Abfragehäufigkeit (Sekunden)",
|
||||
"should_get_network": "Entdecken Sie das Alexa-Netzwerk"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Ermöglicht eine sehr ausführliche Protokollierung auf Trace-Ebene für die erweiterte Fehlerbehebung. \n Aufgrund des erhöhten Protokollvolumens wird dies für den Normalbetrieb nicht empfohlen. \n Stellen Sie sicher, dass die Protokollierungsstufe auf DEBUG eingestellt ist, um die vollständige Ausgabe zu erhalten.",
|
||||
"otp_secret": "Beispiel: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
},
|
||||
"description": "* Erforderliche Angaben",
|
||||
"title": "Alexa Media Player - Rekonfiguration"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"enable_network_discovery": {
|
||||
"description": "Aktiviert die Alexa-Netzwerkerkennung erneut, sodass beim nächsten Abfragezyklus die Alexa-Geräte für die ausgewählten Konten erneut erkannt werden.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Optionale E-Mail-Adresse oder Liste von E-Mail-Adressen Ihres Alexa-Kontos. Falls leer, werden alle bekannten Konten aktualisiert.",
|
||||
"name": "E-Mail-Adresse"
|
||||
}
|
||||
},
|
||||
"name": "Aktivieren Sie die Netzwerkerkennung"
|
||||
},
|
||||
"force_logout": {
|
||||
"description": "Logout erzwingen. Primär für Debugging genutzt.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Zu löschende Accounts. Falls leer, werden alle gelöscht.",
|
||||
"name": "E-Mail-Adresse"
|
||||
}
|
||||
},
|
||||
"name": "Logout erzwingen"
|
||||
},
|
||||
"get_history_records": {
|
||||
"description": "Analysiert die Verlaufsdatensätze für das angegebene Gerät",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entität, für die der Verlauf abgerufen werden soll",
|
||||
"name": "Mediaplayer auswählen:"
|
||||
},
|
||||
"entries": {
|
||||
"description": "Anzahl der abzurufenden Einträge",
|
||||
"name": "Anzahl der Einträge"
|
||||
}
|
||||
},
|
||||
"name": "Verlaufsdatensätze abrufen"
|
||||
},
|
||||
"restore_volume": {
|
||||
"description": "Vorherige Lautstärke auf dem Alexa-Mediaplayer-Gerät wiederherstellen",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entität zum Wiederherstellen der vorherigen Lautstärke auf",
|
||||
"name": "Mediaplayer auswählen:"
|
||||
}
|
||||
},
|
||||
"name": "Vorherige Lautstärke wiederherstellen"
|
||||
},
|
||||
"update_last_called": {
|
||||
"description": "Erzwinge Updates der zuletzt aufgerufenen Echo Geräte für jeden Alexa Account.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Liste der zu aktualisierenden Alexa-Konten. Wenn leer, werden alle bekannten Konten aktualisiert.",
|
||||
"name": "E-Mail-Adresse"
|
||||
}
|
||||
},
|
||||
"name": "Aktualisiere den zuletzt aufgerufenen Sensor"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "The Forgot Password page was detected. This normally is the result of too many failed logins. Amazon may require action before a relogin can be attempted.",
|
||||
"login_failed": "Alexa Media Player failed to login.",
|
||||
"reauth_successful": "Alexa Media Player successfully reauthenticated. Please ignore the \"Aborted\" message from HA."
|
||||
},
|
||||
"error": {
|
||||
"2fa_key_invalid": "{otp_secret} is invalid",
|
||||
"connection_error": "Error connecting; check network and retry",
|
||||
"identifier_exists": "Email for Alexa URL already registered",
|
||||
"invalid_auth": "Login was not successful. Please double-check your email, password, and Authenticator key.",
|
||||
"invalid_credentials": "Invalid credentials",
|
||||
"invalid_url": "URL is invalid: {message}",
|
||||
"oauth_error": "Could not complete OAuth login. Please try again.",
|
||||
"unable_to_connect_hass_url": "Unable to connect to Home Assistant Local URL. Please check the URL under Settings > System > Network > Home Assistant URL > Local network",
|
||||
"unknown_error": "Unknown error: {message}"
|
||||
},
|
||||
"step": {
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "Ignore and Continue - I understand that no support for login issues are provided for bypassing this warning."
|
||||
},
|
||||
"description": "The HA server cannot connect to the URL provided: {hass_url}.\n> {error}\n\nTo fix this, please confirm your browser can reach {hass_url}. This field is from Settings > System > Network > Home Assistant URL.\n\nIf you are **certain** your browser can reach this URL, you can bypass this warning.",
|
||||
"title": "Alexa Media Player - Unable to Connect to HA URL"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "Yes, OTP code was verified"
|
||||
},
|
||||
"description": "**{email} - alexa.{url}** \nHave you verified the OTP code in Amazon 2SV? \n >OTP Code: {message}",
|
||||
"title": "Alexa Media Player - OTP Confirmation"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "Advanced debug",
|
||||
"email": "Email Address",
|
||||
"exclude_devices": "or Exclude these devices from all (comma separated)",
|
||||
"extended_entity_discovery": "Include additional sensors, switches and lights",
|
||||
"hass_url": "Local network URL to access Home Assistant",
|
||||
"include_devices": "Only include these devices (comma separated)",
|
||||
"otp_secret": "52-character Authenticator App Key for Amazon 2SV",
|
||||
"password": "Password",
|
||||
"public_url": "Public URL shared with external hosted services",
|
||||
"queue_delay": "Delay to queue multiple commands together (seconds)",
|
||||
"scan_interval": "Scheduled polling interval (seconds)",
|
||||
"securitycode": "One-time password (OTP)",
|
||||
"should_get_network": "Discover Alexa network",
|
||||
"url": "Amazon region domain (e.g., amazon.co.uk)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
|
||||
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Air quality"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "Carbon monoxide"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Humidity"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "Indoor air quality"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "Particulate matter"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Volatile organic compounds"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Next alarm"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Next reminder"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "Next timer"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperature"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Do not disturb"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Repeat"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Shuffle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"description": "YAML configuration of Alexa Media Player is deprecated.\nPlease remove `alexa_media` from your configuration, restart Home Assistant and use the UI to configure it instead.\nSettings > Devices & services > Integrations > ADD INTEGRATION",
|
||||
"title": "YAML configuration is deprecated"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"debug": "Advanced debug",
|
||||
"exclude_devices": "or Exclude these devices from all (comma separated)",
|
||||
"extended_entity_discovery": "Include additional sensors, switches and lights",
|
||||
"include_devices": "Only include these devices (comma separated)",
|
||||
"otp_secret": "52-character Authenticator App Key for Amazon 2SV",
|
||||
"public_url": "Public URL shared with external hosted services",
|
||||
"queue_delay": "Delay to queue multiple commands together (seconds)",
|
||||
"scan_interval": "Scheduled polling interval (seconds)",
|
||||
"should_get_network": "Discover Alexa network"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
|
||||
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
},
|
||||
"description": "* Required entry",
|
||||
"title": "Alexa Media Player - Reconfiguration"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"enable_network_discovery": {
|
||||
"description": "Re-enables Alexa network discovery so the next polling cycle will rediscover Alexa devices for the selected accounts.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Optional Alexa account email or list of emails. If empty, all known accounts will be refreshed.",
|
||||
"name": "Email address"
|
||||
}
|
||||
},
|
||||
"name": "Enable Network Discovery"
|
||||
},
|
||||
"force_logout": {
|
||||
"description": "Force account to logout. Used mainly for debugging.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Accounts to clear. Empty will clear all.",
|
||||
"name": "Email address"
|
||||
}
|
||||
},
|
||||
"name": "Force Logout"
|
||||
},
|
||||
"get_history_records": {
|
||||
"description": "Parses the history records for the specified device",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entity to get the history for",
|
||||
"name": "Select media player:"
|
||||
},
|
||||
"entries": {
|
||||
"description": "Number of entries to get",
|
||||
"name": "Number of entries"
|
||||
}
|
||||
},
|
||||
"name": "Get History Records"
|
||||
},
|
||||
"restore_volume": {
|
||||
"description": "Restore previous volume level on Alexa media player device",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entity to restore the previous volume level on",
|
||||
"name": "Select media player:"
|
||||
}
|
||||
},
|
||||
"name": "Restore Previous Volume"
|
||||
},
|
||||
"update_last_called": {
|
||||
"description": "Forces update of last_called echo device for each Alexa account.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "List of Alexa accounts to update. If empty, will update all known accounts.",
|
||||
"name": "Email address"
|
||||
}
|
||||
},
|
||||
"name": "Update Last Called Sensor"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "Se detectó la página de Olvidé mi contraseña. Normalmente, esto es el resultado de demasiados intentos fallidos de inicio de sesión. Amazon puede requerir acción antes de que se pueda intentar iniciar sesión nuevamente.",
|
||||
"login_failed": "Alexa Media Player no pudo iniciar sesión.",
|
||||
"reauth_successful": "Alexa Media Player se volvió a autenticar con éxito. Ignore el mensaje \"Cancelado\" de HA."
|
||||
},
|
||||
"error": {
|
||||
"2fa_key_invalid": "{otp_secret} no es válido",
|
||||
"connection_error": "Error al conectar, verifique la red y vuelva a intentarlo",
|
||||
"identifier_exists": "Correo electrónico para la URL de Alexa ya registrado",
|
||||
"invalid_auth": "No se pudo iniciar sesión correctamente. Por favor, revise su correo electrónico, contraseña y clave de autenticación.",
|
||||
"invalid_credentials": "Credenciales no válidas",
|
||||
"invalid_url": "La URL no es válida: {message}",
|
||||
"oauth_error": "No se pudo completar el inicio de sesión de OAuth. Inténtalo de nuevo.",
|
||||
"unable_to_connect_hass_url": "No se puede conectar a la URL local de Home Assistant. Verifique la URL en Ajustes > Sistema > Red > URL de Home Assistant > Red local.",
|
||||
"unknown_error": "Error desconocido: {message}"
|
||||
},
|
||||
"step": {
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "Ignorar y continuar: entiendo que no se proporciona soporte para problemas de inicio de sesión para eludir esta advertencia."
|
||||
},
|
||||
"description": "El servidor HA no puede conectarse a la URL proporcionada: {hass_url}.\n> {error}\n\nPara solucionar esto, confirme que su navegador pueda acceder a {hass_url}. Este campo se encuentra en Ajustes > Sistema > Red > URL de Home Assistant.\n\nSi está **seguro** de que su navegador puede acceder a esta URL, puede omitir esta advertencia.",
|
||||
"title": "Alexa Media Player: no se puede conectar a la URL de alta disponibilidad"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "Sí, el código OTP fue verificado"
|
||||
},
|
||||
"description": "**{email} - alexa.{url}** \n¿Has verificado el código OTP en Amazon 2SV?\n>Código OTP: {message}",
|
||||
"title": "Alexa Media Player - OTP Confirmación"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "Depuración avanzada",
|
||||
"email": "Dirección de correo electrónico",
|
||||
"exclude_devices": "o Excluir estos dispositivos de todos (separados por comas)",
|
||||
"extended_entity_discovery": "Incluye sensores, interruptores y luces adicionales.",
|
||||
"hass_url": "URL de red local para acceder a Home Assistant",
|
||||
"include_devices": "Incluya solo estos dispositivos (separados por comas)",
|
||||
"otp_secret": "Clave de aplicación de autenticación de 52 caracteres para la verificación en dos pasos de Amazon",
|
||||
"password": "Contraseña",
|
||||
"public_url": "URL pública compartida con servicios alojados externos",
|
||||
"queue_delay": "Retraso para poner en cola varios comandos juntos (segundos)",
|
||||
"scan_interval": "Intervalo de sondeo programado (segundos)",
|
||||
"securitycode": "Contraseña de un solo uso (OTP)",
|
||||
"should_get_network": "Descubra la red Alexa",
|
||||
"url": "Región del dominio de Amazon (por ejemplo, amazon.es)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Habilita un registro de nivel de seguimiento muy detallado para la resolución de problemas avanzada. \n No se recomienda para el funcionamiento normal debido al aumento del volumen de registro. \n Asegúrese de que los niveles del registrador estén configurados en DEBUG para obtener una salida completa.",
|
||||
"otp_secret": "Ejemplo: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Calidad del aire"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "Monóxido de carbono"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Humedad"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "Calidad del aire interior"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "materia particulada"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Compuestos orgánicos volátiles"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Próxima alarma"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Próximo recordatorio"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "Próximo temporizador"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperatura"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "No molestar"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Repetir"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Barajar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"description": "La configuración YAML de Alexa Media Player está obsoleta.\nElimina `alexa_media` de tu configuración, reinicia Home Assistant y usa la interfaz de usuario para configurarlo.\nAjustes > Dispositivos y servicios > Integraciones > AÑADIR INTEGRACIÓN",
|
||||
"title": "La configuración de YAML está obsoleta"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"debug": "Depuración avanzada",
|
||||
"exclude_devices": "o Excluir estos dispositivos de todos (separados por comas)",
|
||||
"extended_entity_discovery": "Incluye sensores, interruptores y luces adicionales.",
|
||||
"include_devices": "Incluya solo estos dispositivos (separados por comas)",
|
||||
"otp_secret": "Clave de aplicación de autenticación de 52 caracteres para la verificación en dos pasos de Amazon",
|
||||
"public_url": "URL pública compartida con servicios alojados externos",
|
||||
"queue_delay": "Retraso para poner en cola varios comandos juntos (segundos)",
|
||||
"scan_interval": "Frecuencia de sondeo programada (segundos)",
|
||||
"should_get_network": "Descubra la red Alexa"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Habilita un registro de nivel de seguimiento muy detallado para la resolución de problemas avanzada. \n No se recomienda para el funcionamiento normal debido al aumento del volumen de registro. \n Asegúrese de que los niveles del registrador estén configurados en DEBUG para obtener una salida completa.",
|
||||
"otp_secret": "Ejemplo: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
},
|
||||
"description": "* Entradas obligatorias",
|
||||
"title": "Alexa Media Player - Reconfiguración"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"enable_network_discovery": {
|
||||
"description": "Vuelve a habilitar el descubrimiento de red de Alexa para que el próximo ciclo de sondeo redescubra los dispositivos Alexa para las cuentas seleccionadas.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Correo electrónico o lista de correos electrónicos de la cuenta de Alexa (opcional). Si está vacío, se actualizarán todas las cuentas conocidas.",
|
||||
"name": "Dirección de correo electrónico"
|
||||
}
|
||||
},
|
||||
"name": "Habilitar el descubrimiento de red"
|
||||
},
|
||||
"force_logout": {
|
||||
"description": "Obligar el cierre de sesión de la cuenta. Usar principalmente para depuración.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Cuentas a borrar. Si se deja vacío se borraran todas.",
|
||||
"name": "Dirección de correo electrónico"
|
||||
}
|
||||
},
|
||||
"name": "Obligar cierre de sesión"
|
||||
},
|
||||
"get_history_records": {
|
||||
"description": "Analiza los registros del historial del dispositivo especificado",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entidad para obtener el historial",
|
||||
"name": "Seleccionar reproductor multimedia:"
|
||||
},
|
||||
"entries": {
|
||||
"description": "Número de entradas a obtener",
|
||||
"name": "Número de entradas"
|
||||
}
|
||||
},
|
||||
"name": "Obtener registros históricos"
|
||||
},
|
||||
"restore_volume": {
|
||||
"description": "Restaurar el nivel de volumen anterior en el reproductor multimedia Alexa",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entidad para restaurar el nivel de volumen anterior",
|
||||
"name": "Seleccionar reproductor multimedia:"
|
||||
}
|
||||
},
|
||||
"name": "Restaurar volumen anterior"
|
||||
},
|
||||
"update_last_called": {
|
||||
"description": "Obligar la actualización del último dispositivo Echo llamado para cada cuenta Alexa.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Cuentas de Alexa para actualizar. Si se deja vacío, se actualizaran todas las cuentas.",
|
||||
"name": "Dirección de correo electrónico"
|
||||
}
|
||||
},
|
||||
"name": "Actualizar el último sensor utilizado"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "La page de réinitialisation du mot de passe a été détectée. Cela résulte généralement de trop nombreuses tentatives de connexion échouées. Amazon peut exiger une action avant qu'une nouvelle connexion ne puisse être tentée.",
|
||||
"login_failed": "Alexa Media Player n'a pas réussi à se connecter.",
|
||||
"reauth_successful": "Alexa Media Player s'est ré-authentifié avec succès. Veuillez ignorer le message \"Abandonné\" de Home Assistant."
|
||||
},
|
||||
"error": {
|
||||
"2fa_key_invalid": "{otp_secret} n'est pas valide",
|
||||
"connection_error": "Erreur de connexion ; vérifiez le réseau et réessayez",
|
||||
"identifier_exists": "L'adresse e-mail pour cette URL Alexa est déjà enregistrée",
|
||||
"invalid_auth": "La connexion a échoué. Veuillez vérifier votre adresse e-mail, votre mot de passe et votre clé d'authentification.",
|
||||
"invalid_credentials": "Identifiants invalides",
|
||||
"invalid_url": "L'URL n'est pas valide: {message}",
|
||||
"oauth_error": "Impossible de terminer la connexion OAuth. Veuillez réessayer.",
|
||||
"unable_to_connect_hass_url": "Impossible de se connecter à l'URL locale de Home Assistant. Veuillez vérifier l'URL sous Paramètres > Système > Réseau > URL de Home Assistant > Réseau local",
|
||||
"unknown_error": "Erreur inconnue : {message}"
|
||||
},
|
||||
"step": {
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "Ignorer et continuer - Je comprends qu'aucune assistance pour les problèmes de connexion ne sera fournie si je contourne cet avertissement."
|
||||
},
|
||||
"description": "Le serveur Home Assistant ne peut pas se connecter à l'URL fournie : {hass_url}.\n> {error}\n\nPour résoudre ce problème, veuillez confirmer que votre navigateur peut atteindre {hass_url}. Ce champ provient de Paramètres > Système > Réseau > URL de Home Assistant.\n\nSi vous êtes **certain** que votre navigateur peut accéder à cette URL, vous pouvez ignorer cet avertissement.",
|
||||
"title": "Alexa Media Player - Impossible de se connecter à l'URL de HA"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "Oui, le code OTP a été vérifié"
|
||||
},
|
||||
"description": "**{email} - alexa.{url}**\nAvez-vous vérifié le code OTP dans la validation en deux étapes Amazon ?\n> Code OTP : {message}",
|
||||
"title": "Alexa Media Player - Confirmation OTP"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "Débogage avancé",
|
||||
"email": "Adresse e-mail",
|
||||
"exclude_devices": "ou Exclure ces appareils (séparés par des virgules)",
|
||||
"extended_entity_discovery": "Inclure les capteurs, interrupteurs et lumières additionnels",
|
||||
"hass_url": "URL du réseau local pour accéder à Home Assistant",
|
||||
"include_devices": "Inclure uniquement ces appareils (séparés par des virgules)",
|
||||
"otp_secret": "Clé d'authentification à 52 caractères pour Amazon 2SV",
|
||||
"password": "Mot de passe",
|
||||
"public_url": "URL publique partagée avec les services externes hébergés",
|
||||
"queue_delay": "Délai pour regrouper plusieurs commandes (secondes)",
|
||||
"scan_interval": "Intervalle d'interrogation programmé (secondes)",
|
||||
"securitycode": "Mot de passe à usage unique (OTP)",
|
||||
"should_get_network": "Découvrir le réseau Alexa",
|
||||
"url": "Domaine de la région Amazon (ex : amazon.fr)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Active une journalisation très détaillée, au niveau de la trace, pour un dépannage avancé. \n Non recommandé en fonctionnement normal en raison de l'augmentation du volume des journaux. \n Assurez-vous que le niveau de journalisation est défini sur DEBUG pour obtenir une sortie complète.",
|
||||
"otp_secret": "Exemple : 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "qualité de l'air"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "Monoxyde de carbone"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Humidité"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "qualité de l'air intérieur"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "Matières particulaires"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Composés organiques volatils"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Prochaine alarme"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Prochain rappel"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "La prochaine fois"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Température"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Ne pas déranger"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Répéter"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Mélanger"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"description": "La configuration YAML d'Alexa Media Player est obsolète.\nVeuillez supprimer `alexa_media` de votre configuration, redémarrer Home Assistant et utiliser l'interface utilisateur pour la configurer à la place.\nParamètres > Appareils et services > Intégrations > AJOUTER UNE INTÉGRATION",
|
||||
"title": "La configuration YAML est obsolète"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"debug": "Débogage avancé",
|
||||
"exclude_devices": "ou Exclure ces appareils (séparés par des virgules)",
|
||||
"extended_entity_discovery": "Inclure les capteurs, interrupteurs et lumières additionnels",
|
||||
"include_devices": "Inclure uniquement ces appareils (séparés par des virgules)",
|
||||
"otp_secret": "Clé d'authentification à 52 caractères pour Amazon 2SV",
|
||||
"public_url": "URL publique partagée avec les services externes hébergés",
|
||||
"queue_delay": "Délai pour regrouper plusieurs commandes (secondes)",
|
||||
"scan_interval": "Fréquence d'interrogation programmée (secondes)",
|
||||
"should_get_network": "Découvrir le réseau Alexa"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Active une journalisation très détaillée, au niveau de la trace, pour un dépannage avancé. \n Non recommandé en fonctionnement normal en raison de l'augmentation du volume des journaux. \n Assurez-vous que le niveau de journalisation est défini sur DEBUG pour obtenir une sortie complète.",
|
||||
"otp_secret": "Exemple : 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
},
|
||||
"description": "* Champs obligatoires",
|
||||
"title": "Alexa Media Player - Reconfiguration"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"enable_network_discovery": {
|
||||
"description": "Réactive la découverte du réseau Alexa afin que le prochain cycle d'interrogation redécouvre les appareils Alexa pour les comptes sélectionnés.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Adresse e-mail du compte Alexa ou liste d'adresses (facultatif). Si vide, tous les comptes connus seront actualisés.",
|
||||
"name": "Adresse email"
|
||||
}
|
||||
},
|
||||
"name": "Activer la découverte du réseau"
|
||||
},
|
||||
"force_logout": {
|
||||
"description": "Force la déconnexion du compte. Utilisé principalement pour le débogage.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Comptes à effacer. Laisser vide effacera tous les comptes.",
|
||||
"name": "Adresse e-mail"
|
||||
}
|
||||
},
|
||||
"name": "Forcer la déconnexion"
|
||||
},
|
||||
"get_history_records": {
|
||||
"description": "Analyse les enregistrements d'historique pour l'appareil spécifié.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entité pour laquelle obtenir l'historique",
|
||||
"name": "Sélectionner le lecteur multimédia:"
|
||||
},
|
||||
"entries": {
|
||||
"description": "Nombre d'entrées à récupérer",
|
||||
"name": "Nombre d'entrées"
|
||||
}
|
||||
},
|
||||
"name": "Obtenir les enregistrements d'historique"
|
||||
},
|
||||
"restore_volume": {
|
||||
"description": "Restaure le niveau de volume précédent sur l'appareil Alexa Media Player.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entité sur laquelle restaurer le niveau de volume précédent",
|
||||
"name": "Sélectionner le lecteur multimédia:"
|
||||
}
|
||||
},
|
||||
"name": "Restaurer le volume précédent"
|
||||
},
|
||||
"update_last_called": {
|
||||
"description": "Force la mise à jour du dernier appareil Echo appelé pour chaque compte Alexa.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Liste des comptes Alexa à mettre à jour. Si vide, tous les comptes connus seront mis à jour.",
|
||||
"name": "Adresse e-mail"
|
||||
}
|
||||
},
|
||||
"name": "Mettre à jour le capteur du dernier appel"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "La pagina Password Dimenticata è stata rilevata. Questo normalmente è il risultato di troppi tentativi di accesso falliti. Amazon potrebbe richiedere un'azione prima di poter tentare nuovamente il login.",
|
||||
"login_failed": "Alexa Media Player ha fallito il login.",
|
||||
"reauth_successful": "Alexa Media Player è stato riautenticato con successo. Ignorare il messaggio \"Abortito\" da HA"
|
||||
},
|
||||
"error": {
|
||||
"2fa_key_invalid": "{otp_secret} non è valido",
|
||||
"connection_error": "Errore durante la connessione; controlla la rete e riprova",
|
||||
"identifier_exists": "L'email per l'URL di Alexa è già stata registrata",
|
||||
"invalid_auth": "Accesso non riuscito. Controlla nuovamente la tua email, la password e la chiave di autenticazione.",
|
||||
"invalid_credentials": "Credenziali non valide",
|
||||
"invalid_url": "URL non valido: {message}",
|
||||
"oauth_error": "Impossibile completare l'accesso OAuth. Riprova.",
|
||||
"unable_to_connect_hass_url": "Impossibile connettersi all'URL locale di Home Assistant. Controllare l'URL in Impostazioni > Sistema > Rete > URL di Home Assistant > Rete locale",
|
||||
"unknown_error": "Errore sconosciuto: {message}"
|
||||
},
|
||||
"step": {
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "Ignora e continua - capisco che non verrà fornito alcun supporto per i problemi di accesso derivanti dall'aggirare questo avviso."
|
||||
},
|
||||
"description": "Il server HA non riesce a connettersi all'URL fornito: {hass_url}.\n> {error}\n\nPer risolvere questo problema, verifica che il tuo browser possa raggiungere {hass_url}. Questo campo si trova in Impostazioni > Sistema > Rete > URL Home Assistant.\n\nSe sei **certo** che il tuo browser possa raggiungere questo URL, puoi ignorare questo avviso.",
|
||||
"title": "Alexa Media Player - Impossibile connettersi all'URL HA"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "Sì, il codice OTP è stato verificato"
|
||||
},
|
||||
"description": "**{email} - alexa.{url}**\nHai verificato il codice OTP in Amazon 2SV?\n>Codice OTP {message}",
|
||||
"title": "Alexa Media Player - Conferma OTP"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "Debug avanzato",
|
||||
"email": "Indirizzo email",
|
||||
"exclude_devices": "o Escludi questi dispositivi da tutti (separati da virgole)",
|
||||
"extended_entity_discovery": "Includere sensori, interruttori e luci aggiuntivi",
|
||||
"hass_url": "URL della rete locale per accedere a Home Assistant",
|
||||
"include_devices": "Includi solo questi dispositivi (separati da virgole)",
|
||||
"otp_secret": "Chiave da 52 caratteri dell'app Authenticator per il 2SV di Amazon",
|
||||
"password": "Password",
|
||||
"public_url": "URL pubblico condiviso con servizi ospitati esterni",
|
||||
"queue_delay": "Ritardo per mettere in coda più comandi contemporaneamente (secondi)",
|
||||
"scan_interval": "Frequenza di sondaggio pianificata (secondi)",
|
||||
"securitycode": "Password monouso (OTP)",
|
||||
"should_get_network": "Scopri la rete Alexa",
|
||||
"url": "Regione del dominio Amazon (ad es., amazon.it)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Abilita la registrazione molto dettagliata a livello di traccia per la risoluzione avanzata dei problemi. \n Non consigliato per il normale funzionamento a causa dell'aumento del volume di registro. \n Assicurarsi che i livelli del logger siano impostati su DEBUG per un output completo.",
|
||||
"otp_secret": "Esempio: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Qualità dell'aria"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "monossido di carbonio"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Umidità"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "Qualità dell'aria interna"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "particolato"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Composti organici volatili"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Prossimo allarme"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Prossimo promemoria"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "Prossimo timer"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperatura"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Non disturbare"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Ripetere"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Mescolare"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"description": "La configurazione YAML di Alexa Media Player è obsoleta.\nRimuovi `alexa_media` dalla configurazione, riavvia Home Assistant e utilizza l'interfaccia utente per configurarla.\nImpostazioni > Dispositivi e servizi > Integrazioni > AGGIUNGI INTEGRAZIONE",
|
||||
"title": "La configurazione YAML è deprecata"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"debug": "Debug avanzato",
|
||||
"exclude_devices": "o Escludi questi dispositivi da tutti (separati da virgole)",
|
||||
"extended_entity_discovery": "Includere sensori, interruttori e luci aggiuntivi",
|
||||
"include_devices": "Includi solo questi dispositivi (separati da virgole)",
|
||||
"otp_secret": "Chiave da 52 caratteri dell'app Authenticator per il 2SV di Amazon",
|
||||
"public_url": "URL pubblico condiviso con servizi ospitati esterni",
|
||||
"queue_delay": "Ritardo per mettere in coda più comandi contemporaneamente (secondi)",
|
||||
"scan_interval": "Frequenza di sondaggio pianificata (secondi)",
|
||||
"should_get_network": "Scopri la rete Alexa"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Abilita la registrazione molto dettagliata a livello di traccia per la risoluzione avanzata dei problemi. \n Non consigliato per il normale funzionamento a causa dell'aumento del volume di registro. \n Assicurarsi che i livelli del logger siano impostati su DEBUG per un output completo.",
|
||||
"otp_secret": "Esempio: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
},
|
||||
"description": "* Voci obbligatorie",
|
||||
"title": "Alexa Media Player - Riconfigurazione"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"enable_network_discovery": {
|
||||
"description": "Riattiva la rilevazione della rete Alexa in modo che il successivo ciclo di sondaggio rilevi i dispositivi Alexa per gli account selezionati.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Indirizzo email dell'account Alexa facoltativo o elenco d'indirizzi email. Se vuoto, tutti gli account noti verranno aggiornati.",
|
||||
"name": "Indirizzo email"
|
||||
}
|
||||
},
|
||||
"name": "Abilita rilevamento rete"
|
||||
},
|
||||
"force_logout": {
|
||||
"description": "Forza logout dell'account. Usato principalmente per il debugging.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Account da eliminare. Se vuoto, verranno cancellati tutti.",
|
||||
"name": "Indirizzo email"
|
||||
}
|
||||
},
|
||||
"name": "Forza Logout"
|
||||
},
|
||||
"get_history_records": {
|
||||
"description": "Analizza i record cronologici per il dispositivo specificato",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entità per cui ottenere la cronologia",
|
||||
"name": "Seleziona lettore multimediale:"
|
||||
},
|
||||
"entries": {
|
||||
"description": "Numero di voci da ottenere",
|
||||
"name": "Numero di voci"
|
||||
}
|
||||
},
|
||||
"name": "Ottieni i record della cronologia"
|
||||
},
|
||||
"restore_volume": {
|
||||
"description": "Ripristina il livello del volume precedente sul dispositivo lettore multimediale Alexa",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entità per ripristinare il livello del volume precedente",
|
||||
"name": "Seleziona lettore multimediale:"
|
||||
}
|
||||
},
|
||||
"name": "Ripristina il volume precedente"
|
||||
},
|
||||
"update_last_called": {
|
||||
"description": "Forza l'aggiornamento del dispositivo echo last_called per ogni account Alexa.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Lista di account Alexa da aggiornare. Se vuoto, verranno aggiornati tutti.",
|
||||
"name": "Indirizzo email"
|
||||
}
|
||||
},
|
||||
"name": "Aggiorna sensore last_called"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "「パスワードを忘れた場合」ページが検出されました。これは通常、ログインに何度も失敗した結果です。Amazonでは、再ログインを試みる前に対応を求める場合があります。",
|
||||
"login_failed": "Alexa Media Playerがログインに失敗しました。",
|
||||
"reauth_successful": "Alexa Media Playerは正常に再認証されました。Home Assistant からの \"Aborted\" メッセージは無視してください。"
|
||||
},
|
||||
"error": {
|
||||
"2fa_key_invalid": "{otp_secret} は無効な認証アプリキーです",
|
||||
"connection_error": "接続エラー:ネットワークを確認して再試行してください",
|
||||
"identifier_exists": "Alexa URLに対するメールアドレスはすでに登録されています",
|
||||
"invalid_auth": "ログインに失敗しました。メールアドレス、パスワード、認証キーを再度ご確認ください。",
|
||||
"invalid_credentials": "無効な資格情報",
|
||||
"invalid_url": "URL が無効です:{message}",
|
||||
"oauth_error": "OAuthログインを完了できませんでした。もう一度お試しください。",
|
||||
"unable_to_connect_hass_url": "Home Assistant URL に接続できません。[設定] -> [全般] の [外部 URL] を確認してください。",
|
||||
"unknown_error": "不明なエラー:{message}"
|
||||
},
|
||||
"step": {
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "無視して続行 - この警告を回避することで、ログイン問題に関するサポートはされないことを承知しています。"
|
||||
},
|
||||
"description": "Home Assistant サーバーへ、指定された URL {hass_url}で接続できません。 \n> {error}\n\nこの問題を解決するには、あなたのHome Assistant サーバーに{hass_url}でアクセスできることを確認してください。このフィールドは、[設定] -> [全般] の [外部 URL] からのものですが、内部 URL を試すこともできます。クライアントがこの URL にアクセスできることが 確実であれば、この警告をバイパスできます。",
|
||||
"title": "Alexa Media Player - Home Assistant URLに接続できません"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "はい、OTP コードを確認しました。"
|
||||
},
|
||||
"description": "** {email} - alexa. {url} **\nAmazon 2段階認証で OTPコードを確認しましたか? \n>OTP コード: {message}",
|
||||
"title": "Alexa Media Player - OTP の確認"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "高度なデバッグ",
|
||||
"email": "メールアドレス",
|
||||
"exclude_devices": "除外するデバイス(カンマ区切り)",
|
||||
"extended_entity_discovery": "Echo経由で接続されたデバイスを含める",
|
||||
"hass_url": "Home AssistantにアクセスするためのURL",
|
||||
"include_devices": "含まれるデバイス(カンマ区切り)",
|
||||
"otp_secret": "Amazon 2段階認証用認証アプリキー(52桁)",
|
||||
"password": "パスワード",
|
||||
"public_url": "外部ホスティング・サービスと共有される公開URL",
|
||||
"queue_delay": "コマンドをキューにまとめて待機させる秒数",
|
||||
"scan_interval": "スキャン間隔秒数",
|
||||
"securitycode": "[%key_id:55616596%]",
|
||||
"should_get_network": "Alexaネットワークを探索",
|
||||
"url": "Amazon 地域ドメイン (例: amazon.co.jp)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
|
||||
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Air quality"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "Carbon monoxide"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Humidity"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "Indoor air quality"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "Particulate matter"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Volatile organic compounds"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Next alarm"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Next reminder"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "Next timer"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperature"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Do not disturb"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Repeat"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Shuffle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"description": "Alexa Media PlayerのYAML設定は非推奨であり、バージョン4.14.0で削除される予定です。 この設定の自動インポートは行われません。 設定から削除し、Home Assistantを再起動して、代わりにUIを使用して設定してください。 [設定] -> [デバイスとサービス] -> [統合] -> [統合を追加]",
|
||||
"title": "YAML設定は非推奨です"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"debug": "高度なデバッグ",
|
||||
"exclude_devices": "除外するデバイス(カンマ区切り)",
|
||||
"extended_entity_discovery": "Alexaデバイスに接続された追加のセンサー、スイッチ、ライトを含める",
|
||||
"include_devices": "含まれるデバイス(カンマ区切り)",
|
||||
"otp_secret": "52-character Authenticator App Key for Amazon 2SV",
|
||||
"public_url": "Home Assistant にアクセスするための公開URL (末尾の '/' を含む)",
|
||||
"queue_delay": "コマンドをキューにまとめて待機させる秒数",
|
||||
"scan_interval": "スキャン間隔秒数",
|
||||
"should_get_network": "Alexaネットワークを探索"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
|
||||
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
},
|
||||
"description": "* 必須エントリ\n注: **拡張エンティティ検出** を使用するには、**Alexa ネットワークの探索** を有効にする必要があります。",
|
||||
"title": "Alexa Media Player - 再設定"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"enable_network_discovery": {
|
||||
"description": "Alexa ネットワーク検出を再度有効にすると、次のポーリングサイクルで、選択したアカウントの Alexa デバイスが再検出されます。",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "オプションのalexaアカウントのメールアドレスまたはメールアドレスのリスト。空の場合、すべての既知のアカウントが更新されます。",
|
||||
"name": "Emailアドレス"
|
||||
}
|
||||
},
|
||||
"name": "ネットワーク探索を有効にする"
|
||||
},
|
||||
"force_logout": {
|
||||
"description": "アカウントを強制的にログアウトさせます (主にデバッグに使用します)",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "削除するアカウント 空にするとすべて削除されます",
|
||||
"name": "メールアドレス"
|
||||
}
|
||||
},
|
||||
"name": "強制ログアウト"
|
||||
},
|
||||
"get_history_records": {
|
||||
"description": "指定したデバイスの履歴レコードを解析します",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "履歴を取得するエンティティ",
|
||||
"name": "メディアプレーヤーを選択:"
|
||||
},
|
||||
"entries": {
|
||||
"description": "取得するエントリー数",
|
||||
"name": "エントリー数"
|
||||
}
|
||||
},
|
||||
"name": "履歴レコードを取得する"
|
||||
},
|
||||
"restore_volume": {
|
||||
"description": "Alexaメディアプレーヤーデバイスで以前の音量レベルを復元する",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "以前の音量レベルを復元するエンティティ",
|
||||
"name": "メディアプレーヤーを選択:"
|
||||
}
|
||||
},
|
||||
"name": "以前のボリュームを復元"
|
||||
},
|
||||
"update_last_called": {
|
||||
"description": "各Alexaアカウントの最後に呼び出されたEchoデバイスを強制的に更新します。",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "更新する Alexa アカウントの一覧。空の場合、既知のすべてのアカウントが更新されます。",
|
||||
"name": "メールアドレス"
|
||||
}
|
||||
},
|
||||
"name": "最後に呼び出されたセンサーを更新"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "Glemt passord-siden ble oppdaget. Dette er vanligvis et resultat av for mange mislykkede påloggingsforsøk. Amazon kan kreve at du gjør noe før du kan prøve å logge inn igjen.",
|
||||
"login_failed": "Alexa Media Player kunne ikke logge inn.",
|
||||
"reauth_successful": "Alexa Media Player er autentisert på nytt. Vennligst ignorer meldingen «Avbrutt» fra HA."
|
||||
},
|
||||
"error": {
|
||||
"2fa_key_invalid": "{otp_secret} er ugyldig",
|
||||
"connection_error": "Feil ved tilkobling; sjekk nettverket og prøv på nytt",
|
||||
"identifier_exists": "E-post for Alexa URL allerede registrert",
|
||||
"invalid_auth": "Innloggingen mislyktes. Dobbeltsjekk e-post, passord og autentiseringsnøkkel.",
|
||||
"invalid_credentials": "ugyldige legitimasjon",
|
||||
"invalid_url": "URL er ugyldig: {message}",
|
||||
"oauth_error": "Kunne ikke fullføre OAuth-pålogging. Prøv på nytt.",
|
||||
"unable_to_connect_hass_url": "Kan ikke koble til den lokale URL-adressen for Home Assistant. Sjekk URL-adressen under Innstillinger > System > Nettverk > URL for Home Assistant > Lokalt nettverk",
|
||||
"unknown_error": "Ukjent feil: {message}"
|
||||
},
|
||||
"step": {
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "Ignorer og fortsett – jeg forstår at det ikke gis støtte for innloggingsproblemer når denne advarselen omgås."
|
||||
},
|
||||
"description": "HA-serveren kan ikke koble til den oppgitte URL-en: {hass_url}.\n> {error}\n\nFor å fikse dette, må du bekrefte at nettleseren din kan nå {hass_url}. Dette feltet er fra Innstillinger > System > Nettverk > URL-adresse for Home Assistant.\n\nHvis du er **sikker** på at nettleseren din kan nå denne URL-en, kan du omgå denne advarselen.",
|
||||
"title": "Alexa Media Player – Kan ikke koble til HA URL"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "Ja, engangskoden ble bekreftet"
|
||||
},
|
||||
"description": "**{email} - alexa.{url}** \nHar du bekreftet engangskoden i Amazon 2SV?\n >OTP Kode {message}",
|
||||
"title": "Alexa Media Player - OTP bekreftelse"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "Avansert feilsøking",
|
||||
"email": "Epostadresse",
|
||||
"exclude_devices": "eller Ekskluder disse enhetene fra alle (kommaseparert)",
|
||||
"extended_entity_discovery": "Inkluder ekstra sensorer, brytere og lys",
|
||||
"hass_url": "URL-adresse for lokalt nettverk for å få tilgang til Home Assistant",
|
||||
"include_devices": "Inkluder bare disse enhetene (kommaseparert)",
|
||||
"otp_secret": "52-tegns nøkkel fra autentiseringsappen for Amazon 2SV",
|
||||
"password": "Passord",
|
||||
"public_url": "Offentlig URL delt med eksterne vertsbaserte tjenester",
|
||||
"queue_delay": "Forsinkelse for å sette flere kommandoer sammen i kø (sekunder)",
|
||||
"scan_interval": "Planlagt avstemningsintervall (sekunder)",
|
||||
"securitycode": "Engangspassord (OTP)",
|
||||
"should_get_network": "Oppdag Alexa-nettverket",
|
||||
"url": "Amazon-regiondomenet (f.eks. Amazon.co.uk)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Muliggjør svært detaljert logging på spornivå for avansert feilsøking. \n Anbefales ikke for normal drift på grunn av økt loggvolum. \n Sørg for at loggnivåene er satt til DEBUG for full utdata.",
|
||||
"otp_secret": "Eksempel: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Luftkvalitet"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "Karbonmonoksid"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Fuktighet"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "Innendørs luftkvalitet"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "Partikkelformet materiale"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Flyktige organiske forbindelser"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Neste alarm"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Neste påminnelse"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "Neste timer"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperatur"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Ikke forstyrr"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Gjenta"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Bland"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"description": "YAML-konfigurasjonen av Alexa Media Player er utdatert.\nFjern `alexa_media` fra konfigurasjonen din, start Home Assistant på nytt og bruk brukergrensesnittet til å konfigurere den i stedet.\nInnstillinger > Enheter og tjenester > Integrasjoner > LEGG TIL INTEGRASJON",
|
||||
"title": "YAML-konfigurasjonen er utdatert"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"debug": "Avansert feilsøking",
|
||||
"exclude_devices": "eller Ekskluder disse enhetene fra alle (kommaseparert)",
|
||||
"extended_entity_discovery": "Inkluder ekstra sensorer, brytere og lys",
|
||||
"include_devices": "Inkluder bare disse enhetene (kommaseparert)",
|
||||
"otp_secret": "52-tegns nøkkel fra autentiseringsappen for Amazon 2SV",
|
||||
"public_url": "Offentlig URL delt med eksterne vertsbaserte tjenester",
|
||||
"queue_delay": "Forsinkelse for å sette flere kommandoer sammen i kø (sekunder)",
|
||||
"scan_interval": "Planlagt avstemningsfrekvens (sekunder)",
|
||||
"should_get_network": "Oppdag Alexa-nettverket"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Muliggjør svært detaljert logging på spornivå for avansert feilsøking. \n Anbefales ikke for normal drift på grunn av økt loggvolum. \n Sørg for at loggnivåene er satt til DEBUG for full utdata.",
|
||||
"otp_secret": "Eksempel: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
},
|
||||
"description": "* Obligatoriske oppføringer",
|
||||
"title": "Alexa Media Player – Rekonfigurasjon"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"enable_network_discovery": {
|
||||
"description": "Aktiverer Alexa-nettverksoppdagelse på nytt, slik at neste avstemningssyklus vil oppdage Alexa-enheter på nytt for de valgte kontoene.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Valgfri e-postadresse for Alexa-kontoen eller liste over e-postadresser. Hvis tom, vil alle kjente kontoer bli oppdatert.",
|
||||
"name": "E-postadresse"
|
||||
}
|
||||
},
|
||||
"name": "Aktiver nettverksoppdagelse"
|
||||
},
|
||||
"force_logout": {
|
||||
"description": "Tving kontoen til å logge ut. Brukes hovedsakelig til feilsøking.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Kontoer som skal tømmes. Tøm vil tømme alle.",
|
||||
"name": "E-postadresse"
|
||||
}
|
||||
},
|
||||
"name": "Tving utlogging"
|
||||
},
|
||||
"get_history_records": {
|
||||
"description": "Analyserer historikkpostene for den angitte enheten",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entitet å hente historien for",
|
||||
"name": "Velg mediespiller:"
|
||||
},
|
||||
"entries": {
|
||||
"description": "Antall oppføringer å få",
|
||||
"name": "Antall oppføringer"
|
||||
}
|
||||
},
|
||||
"name": "Få historikk"
|
||||
},
|
||||
"restore_volume": {
|
||||
"description": "Gjenopprett forrige volumnivå på Alexa mediespillerenhet",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entitet for å gjenopprette forrige volumnivå på",
|
||||
"name": "Velg mediespiller:"
|
||||
}
|
||||
},
|
||||
"name": "Gjenopprett forrige volum"
|
||||
},
|
||||
"update_last_called": {
|
||||
"description": "Tvinger frem oppdatering av sist oppringte echo-enhet for hver Alexa-konto.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Liste over Alexa-kontoer som skal oppdateres. Hvis tom, oppdateres alle kjente kontoer.",
|
||||
"name": "E-postadresse"
|
||||
}
|
||||
},
|
||||
"name": "Oppdater sist oppringte sensor"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "De pagina 'Wachtwoord vergeten' is gedetecteerd. Dit is meestal het gevolg van te veel mislukte inlogpogingen. Amazon kan actie vereisen voordat opnieuw kan worden ingelogd.",
|
||||
"login_failed": "Het inloggen van Alexa Mediaspeler is mislukt.",
|
||||
"reauth_successful": "Alexa Mediaspeler is met succes opnieuw geverifieerd. Negeer a.u.b. het bericht \"Afgebroken\" van HA."
|
||||
},
|
||||
"error": {
|
||||
"2fa_key_invalid": "{otp_secret} is ongeldig",
|
||||
"connection_error": "Fout bij verbinding; controleer netwerk en probeer opnieuw",
|
||||
"identifier_exists": "E-mailadres voor Alexa-URL is al geregistreerd",
|
||||
"invalid_auth": "Inloggen is mislukt. Controleer uw e-mailadres, wachtwoord en Authenticator-sleutel nogmaals.",
|
||||
"invalid_credentials": "Ongeldige inloggegevens",
|
||||
"invalid_url": "De URL is ongeldig: {message}",
|
||||
"oauth_error": "OAuth-aanmelding kon niet worden voltooid. Probeer het opnieuw.",
|
||||
"unable_to_connect_hass_url": "Kan geen verbinding maken met de lokale Home Assistant-URL. Controleer de URL onder Instellingen > Systeem > Netwerk > Home Assistant-URL > Lokaal netwerk.",
|
||||
"unknown_error": "Onbekende fout: {message}"
|
||||
},
|
||||
"step": {
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "Negeren en doorgaan - Ik begrijp dat er geen ondersteuning wordt geboden bij inlogproblemen wanneer ik deze waarschuwing omzeil."
|
||||
},
|
||||
"description": "De HA server kan geen verbinding maken met de opgegeven URL: {hass_url}.\n> {error}\n\nOm dit op te lossen, controleer of uw browser {hass_url} kan bereiken. Dit veld vindt u in Instellingen > Systeem > Netwerk > Home Assistant-URL.\n\nAls u er **zeker van bent** dat uw browser deze URL kan bereiken, kunt u deze waarschuwing negeren.",
|
||||
"title": "Alexa Mediaspeler - Kan geen verbinding maken met HA URL"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "Ja, de OTP-code is geverifieerd."
|
||||
},
|
||||
"description": "**{email} - alexa.{url}**\nHeb je met succes een OTP van de ingebouwde 2FA App Key met Amazon bevestigd? \n >OTP-code {message}",
|
||||
"title": "Alexa Mediaspeler - OTP Bevestiging"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "Geavanceerde foutopsporing",
|
||||
"email": "E-mailadres",
|
||||
"exclude_devices": "of Sluit deze apparaten uit van alles (gescheiden door komma's)",
|
||||
"extended_entity_discovery": "Voeg extra sensoren, schakelaars en lampen toe.",
|
||||
"hass_url": "Lokale netwerk-URL om toegang te krijgen tot Home Assistant",
|
||||
"include_devices": "Vermeld alleen deze apparaten (gescheiden door komma's)",
|
||||
"otp_secret": "52-karakter Authenticator-appsleutel voor Amazon 2SV",
|
||||
"password": "Wachtwoord",
|
||||
"public_url": "Openbare URL gedeeld met externe hostingdiensten",
|
||||
"queue_delay": "Vertraging om meerdere opdrachten tegelijk in de wachtrij te plaatsen (seconden)",
|
||||
"scan_interval": "Gepland pollinginterval (seconden)",
|
||||
"securitycode": "Eenmalig wachtwoord (OTP)",
|
||||
"should_get_network": "Ontdek het Alexa-netwerk",
|
||||
"url": "Domeinnaam van Amazon regio (bijv: amazon.nl)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Schakelt zeer gedetailleerde logboekregistratie op traceniveau in voor geavanceerde probleemoplossing. \n Niet aanbevolen voor normaal gebruik vanwege het toegenomen logvolume. \n Zorg ervoor dat de logniveaus zijn ingesteld op DEBUG voor volledige uitvoer.",
|
||||
"otp_secret": "Voorbeeld: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Luchtkwaliteit"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "Koolmonoxide"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Vochtigheid"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "Binnenluchtkwaliteit"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "Fijnstof"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Vluchtige organische verbindingen"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Volgende alarm"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Volgende herinnering"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "Volgende keer"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperatuur"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Niet storen"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Herhalen"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Schudden"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"description": "De YAML-configuratie van Alexa Media Player is verouderd.\n\nVerwijder `alexa_media` uit uw configuratie, herstart Home Assistant en gebruik in plaats daarvan de gebruikersinterface om het te configureren.\n\nInstellingen > Apparaten en services > Integraties > INTEGRATIE TOEVOEGEN",
|
||||
"title": "YAML-configuratie is verouderd"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"debug": "Geavanceerde foutopsporing",
|
||||
"exclude_devices": "of Sluit deze apparaten uit van alles (gescheiden door komma's)",
|
||||
"extended_entity_discovery": "Voeg extra sensoren, schakelaars en lampen toe",
|
||||
"include_devices": "Vermeld alleen deze apparaten (gescheiden door komma's)",
|
||||
"otp_secret": "52-karakter Authenticator-appsleutel voor Amazon 2SV",
|
||||
"public_url": "Openbare URL gedeeld met externe hostingdiensten",
|
||||
"queue_delay": "Vertraging om meerdere opdrachten tegelijk in de wachtrij te plaatsen (seconden)",
|
||||
"scan_interval": "Geplande pollingfrequentie (seconden)",
|
||||
"should_get_network": "Ontdek het Alexa-netwerk"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Schakelt zeer gedetailleerde logboekregistratie op traceniveau in voor geavanceerde probleemoplossing. \n Niet aanbevolen voor normaal gebruik vanwege het toegenomen logvolume. \n Zorg ervoor dat de logniveaus zijn ingesteld op DEBUG voor volledige uitvoer.",
|
||||
"otp_secret": "Voorbeeld: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
},
|
||||
"description": "* Vereiste invoer",
|
||||
"title": "Alexa Mediaspeler - Herconfiguratie"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"enable_network_discovery": {
|
||||
"description": "Hiermee wordt de Alexa-netwerkdetectie opnieuw ingeschakeld, zodat de volgende pollingcyclus Alexa-apparaten voor de geselecteerde accounts opnieuw kan detecteren.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Optioneel Alexa-account-e-mailadres of lijst met e-mailadressen. Indien leeg, worden alle bekende accounts vernieuwd.",
|
||||
"name": "E-mailadres"
|
||||
}
|
||||
},
|
||||
"name": "Schakel netwerkdetectie in"
|
||||
},
|
||||
"force_logout": {
|
||||
"description": "Forceer account om uit te loggen. Voornamelijk gebruikt voor foutopsporing.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Te vereffenen accounts. Leegmaken zal alles wissen.",
|
||||
"name": "E-mailadres"
|
||||
}
|
||||
},
|
||||
"name": "Uitloggen forceren"
|
||||
},
|
||||
"get_history_records": {
|
||||
"description": "Analyseert de geschiedenisrecords voor het opgegeven apparaat",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entiteit om de geschiedenis op te halen",
|
||||
"name": "Selecteer mediaspeler:"
|
||||
},
|
||||
"entries": {
|
||||
"description": "Aantal inzendingen om te krijgen",
|
||||
"name": "Aantal inzendingen"
|
||||
}
|
||||
},
|
||||
"name": "Geschiedenisrecords ophalen"
|
||||
},
|
||||
"restore_volume": {
|
||||
"description": "Herstel het vorige volumeniveau op het Alexa-mediaspelerapparaat",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entiteit om het vorige volumeniveau te herstellen op",
|
||||
"name": "Selecteer mediaspeler:"
|
||||
}
|
||||
},
|
||||
"name": "Vorig volume herstellen"
|
||||
},
|
||||
"update_last_called": {
|
||||
"description": "Forceert update van last_called echo apparaat voor elk Alexa-account.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Lijst met Alexa accounts om bij te werken. Als het veld leeg is, worden alle bekende accounts bijgewerkt.",
|
||||
"name": "E-mailadres"
|
||||
}
|
||||
},
|
||||
"name": "Laatst opgeroepen sensor bijwerken"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "Wykryto stronę „Zapomniałem hasła”. Zwykle jest to spowodowane zbyt wieloma nieudanymi próbami logowania. Amazon może wymagać podjęcia działań, zanim będzie można ponownie spróbować zalogować się.",
|
||||
"login_failed": "Alexa Media Player nie może się zalogować.",
|
||||
"reauth_successful": "Odtwarzacz multimedialny Alexa pomyślnie przeszedł ponowne uwierzytelnienie. Proszę zignorować komunikat „Przerwano” od HA."
|
||||
},
|
||||
"error": {
|
||||
"2fa_key_invalid": "{otp_secret} jest nieprawidłowy",
|
||||
"connection_error": "Błąd podczas łączenia; sprawdź sieć i spróbuj ponownie",
|
||||
"identifier_exists": "Adres e-mail dla Alexy już jest zarejestrowany",
|
||||
"invalid_auth": "Logowanie nie powiodło się. Sprawdź ponownie swój adres e-mail, hasło i klucz uwierzytelniający.",
|
||||
"invalid_credentials": "Nieprawidłowe dane logowania",
|
||||
"invalid_url": "URL jest nieprawidłowy: {message}",
|
||||
"oauth_error": "Nie udało się dokończyć logowania OAuth. Spróbuj ponownie.",
|
||||
"unable_to_connect_hass_url": "Nie można połączyć się z lokalnym adresem URL Home Assistant. Sprawdź adres URL w Ustawieniach > System > Sieć > Adres URL Home Assistant > Sieć lokalna.",
|
||||
"unknown_error": "Nieznany błąd: {message}"
|
||||
},
|
||||
"step": {
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "Ignoruj i kontynuuj – rozumiem, że nie jest zapewniane wsparcie dla problemów z logowaniem wynikających z obejścia tego ostrzeżenia."
|
||||
},
|
||||
"description": "Serwer HA nie może połączyć się z podanym adresem URL: {hass_url}.\n> {error}\n\nAby rozwiązać ten problem, upewnij się, że Twoja przeglądarka może uzyskać dostęp do adresu {hass_url}. To pole znajduje się w Ustawieniach > System > Sieć > Adres URL Asystenta Domowego.\n\nJeśli masz **pewność**, że Twoja przeglądarka może uzyskać dostęp do tego adresu URL, możesz pominąć to ostrzeżenie.",
|
||||
"title": "Alexa Media Player – nie można połączyć się z adresem URL HA"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "Tak, kod OTP został zweryfikowany"
|
||||
},
|
||||
"description": "**{email} - alexa.{url}** \nCzy zweryfikowałeś kod OTP w Amazon 2SV?\n>Kod OTP: {message}",
|
||||
"title": "Alexa Media Player - Potwierdzanie hasła jednorazowego"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "Zaawansowane debugowanie",
|
||||
"email": "Adres e-mail",
|
||||
"exclude_devices": "lub Wyklucz te urządzenia ze wszystkich (rozdzielone przecinkami)",
|
||||
"extended_entity_discovery": "Dodaj dodatkowe czujniki, przełączniki i światła",
|
||||
"hass_url": "Lokalny adres URL sieciowy umożliwiający dostęp do Home Assistant",
|
||||
"include_devices": "Uwzględnij tylko te urządzenia (rozdzielone przecinkami)",
|
||||
"otp_secret": "52-znakowy klucz aplikacji uwierzytelniającej dla Amazon 2SV",
|
||||
"password": "Hasło",
|
||||
"public_url": "Publiczny adres URL udostępniany zewnętrznym usługom hostowanym",
|
||||
"queue_delay": "Opóźnienie w kolejkowaniu wielu poleceń (sekundy)",
|
||||
"scan_interval": "Zaplanowany interwał sondowania (sekundy)",
|
||||
"securitycode": "Jednorazowe hasło (OTP)",
|
||||
"should_get_network": "Odkryj sieć Alexa",
|
||||
"url": "Region/domena Amazon (np. amazon.co.uk)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Włącza bardzo szczegółowe rejestrowanie na poziomie śledzenia w celu zaawansowanego rozwiązywania problemów. \n Niezalecane do normalnego użytkowania ze względu na zwiększoną objętość dziennika. \n Upewnij się, że poziomy rejestratora są ustawione na DEBUG, aby uzyskać pełny wynik.",
|
||||
"otp_secret": "Przykład: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Jakość powietrza"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "Tlenek węgla"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Wilgotność"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "Jakość powietrza w pomieszczeniach"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "Cząstki stałe"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Lotne związki organiczne"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Następny alarm"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Następne przypomnienie"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "Następnym razem"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperatura"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Nie przeszkadzać"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Powtarzać"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Odtwarzanie losowe"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"description": "Konfiguracja YAML odtwarzacza multimedialnego Alexa jest przestarzała.\nUsuń „alexa_media” z konfiguracji, uruchom ponownie Asystenta Domowego i skonfiguruj go za pomocą interfejsu użytkownika.\nUstawienia > Urządzenia i usługi > Integracje > DODAJ INTEGRACJĘ",
|
||||
"title": "Konfiguracja YAML jest przestarzała"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"debug": "Zaawansowane debugowanie",
|
||||
"exclude_devices": "lub Wyklucz te urządzenia ze wszystkich (rozdzielone przecinkami)",
|
||||
"extended_entity_discovery": "Dodaj dodatkowe czujniki, przełączniki i światła",
|
||||
"include_devices": "Uwzględnij tylko te urządzenia (rozdzielone przecinkami)",
|
||||
"otp_secret": "52-znakowy klucz aplikacji uwierzytelniającej dla Amazon 2SV",
|
||||
"public_url": "Publiczny adres URL udostępniany zewnętrznym usługom hostowanym",
|
||||
"queue_delay": "Opóźnienie w kolejkowaniu wielu poleceń (sekundy)",
|
||||
"scan_interval": "Interwał skanowania (sekundy)",
|
||||
"should_get_network": "Odkryj sieć Alexa"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Włącza bardzo szczegółowe rejestrowanie na poziomie śledzenia w celu zaawansowanego rozwiązywania problemów. \n Niezalecane do normalnego użytkowania ze względu na zwiększoną objętość dziennika. \n Upewnij się, że poziomy rejestratora są ustawione na DEBUG, aby uzyskać pełny wynik.",
|
||||
"otp_secret": "Przykład: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
},
|
||||
"description": "* Wymagane wpisy",
|
||||
"title": "Odtwarzacz multimedialny Alexa – rekonfiguracja"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"enable_network_discovery": {
|
||||
"description": "Ponownie włącza wykrywanie sieci Alexa, dzięki czemu kolejny cykl sondowania ponownie wykryje urządzenia Alexa dla wybranych kont.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Opcjonalny adres e-mail konta Alexa lub lista adresów e-mail. Jeśli jest pusty, wszystkie znane konta zostaną odświeżone.",
|
||||
"name": "Adres e-mail"
|
||||
}
|
||||
},
|
||||
"name": "Włącz wykrywanie sieci"
|
||||
},
|
||||
"force_logout": {
|
||||
"description": "Wymuś wylogowanie z konta. Używane głównie do debugowania.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Konta do wyczyszczenia. Opcja „Opróżnij” wyczyści wszystkie konta.",
|
||||
"name": "Adres e-mail"
|
||||
}
|
||||
},
|
||||
"name": "Wymuś wylogowanie"
|
||||
},
|
||||
"get_history_records": {
|
||||
"description": "Analizuje zapisy historii dla określonego urządzenia",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Podmiot, dla którego ma zostać pobrana historia",
|
||||
"name": "Wybierz odtwarzacz multimedialny:"
|
||||
},
|
||||
"entries": {
|
||||
"description": "Liczba wpisów do uzyskania",
|
||||
"name": "Liczba wpisów"
|
||||
}
|
||||
},
|
||||
"name": "Pobierz zapisy historyczne"
|
||||
},
|
||||
"restore_volume": {
|
||||
"description": "Przywróć poprzedni poziom głośności na urządzeniu z odtwarzaczem multimedialnym Alexa",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Podmiot przywracający poprzedni poziom głośności",
|
||||
"name": "Wybierz odtwarzacz multimedialny:"
|
||||
}
|
||||
},
|
||||
"name": "Przywróć poprzednią głośność"
|
||||
},
|
||||
"update_last_called": {
|
||||
"description": "Wymusza aktualizację ostatnio używanego urządzenia echo dla każdego konta Alexa.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Lista kont Alexa do aktualizacji. Jeśli pusta, zaktualizuje wszystkie znane konta.",
|
||||
"name": "Adres e-mail"
|
||||
}
|
||||
},
|
||||
"name": "Aktualizuj ostatnio wywołany czujnik"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "A página de Esqueceu a Senha foi detectada. Isso normalmente é resultado de muitas tentativas de login falhadas. A Amazon pode exigir ação antes que seja possível tentar fazer login novamente.",
|
||||
"login_failed": "Alexa Media Player falhou no login.",
|
||||
"reauth_successful": "Alexa Media Player reautenticado com sucesso. Por favor, ignore a mensagem \"Abortado\" do Home Assistant."
|
||||
},
|
||||
"error": {
|
||||
"2fa_key_invalid": "{otp_secret} é inválido",
|
||||
"connection_error": "Erro de conexão; verifique a sua conexão e tente novamente",
|
||||
"identifier_exists": "Email para URL Alexa já registrado",
|
||||
"invalid_auth": "O ‘login’ não foi bem-sucedido. Verifique novamente seu endereço eletrônico, senha e chave de autenticação.",
|
||||
"invalid_credentials": "Credenciais inválidas",
|
||||
"invalid_url": "O URL é inválido: {message}",
|
||||
"oauth_error": "Não foi possível concluir o ‘login’ OAuth. Tente novamente.",
|
||||
"unable_to_connect_hass_url": "Não foi possível conectar ao URL local do Home Assistant. Verifique o URL em Configurações > Sistema > Rede > URL do Home Assistant > Rede local.",
|
||||
"unknown_error": "Erro desconhecido: {message}"
|
||||
},
|
||||
"step": {
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "Ignorar e continuar - Entendo que nenhum suporte para problemas de login é fornecido para ignorar este aviso."
|
||||
},
|
||||
"description": "O servidor HA não consegue se conectar ao URL fornecido: {hass_url}.\n> {error}\n\nPara corrigir isso, confirme se o seu navegador consegue acessar {hass_url}. Este campo está em Configurações > Sistema > Rede > URL do Home Assistant.\n\nSe você tiver **certeza** de que seu navegador consegue acessar este URL, pode ignorar este aviso.",
|
||||
"title": "Alexa Media Player - Não foi possível se conectar a URL do Home Assistant"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "Sim, o código OTP foi verificado."
|
||||
},
|
||||
"description": "**{email} - alexa.{url}**\nVocê verificou o código OTP na verificação em duas etapas da Amazon?\n >Código OTP: {message}",
|
||||
"title": "Alexa Media Player - Confirmação OTP"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "Depuração avançada",
|
||||
"email": "Endereço eletrônico",
|
||||
"exclude_devices": "ou Excluir esses dispositivos de todos (separados por vírgula)",
|
||||
"extended_entity_discovery": "Inclua sensores, interruptores e luzes adicionais.",
|
||||
"hass_url": "URL da rede local para acessar o Home Assistant",
|
||||
"include_devices": "Inclua apenas estes dispositivos (separados por vírgula)",
|
||||
"otp_secret": "Chave de 52 caracteres do App Autenticador para 2SV da Amazon",
|
||||
"password": "Senha",
|
||||
"public_url": "URL pública compartilhada com serviços hospedados externamente",
|
||||
"queue_delay": "Tempo de espera para enfileirar vários comandos (em segundos)",
|
||||
"scan_interval": "Intervalo de sondagem programado (segundos)",
|
||||
"securitycode": "Senha de uso único (OTP)",
|
||||
"should_get_network": "Descubra a rede Alexa",
|
||||
"url": "Domínio regional da Amazon (ex: amazon.co.uk)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Habilita o registro detalhado em nível de rastreamento para solução de problemas avançada. \n Não recomendado para operação normal devido ao aumento do volume de logs. \n Certifique-se de que os níveis de registro estejam definidos como DEBUG para obter a saída completa.",
|
||||
"otp_secret": "Exemplo: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Qualidade do ar"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "Monóxido de carbono"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Umidade"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "Qualidade do ar interior"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "Material particulado"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Compostos orgânicos voláteis"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Próximo alarme"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Próximo lembrete"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "Próximo cronômetro"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperatura"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Não incomodar"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Repita"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Embaralhar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"description": "A configuração YAML do Alexa Media Player está obsoleta.\nRemova `alexa_media` da sua configuração, reinicie o Home Assistant e use a ‘interface’ do usuário para configurá-lo.\n\nConfigurações > Dispositivos e serviços > Integrações > ADICIONAR INTEGRAÇÃO",
|
||||
"title": "A configuração YAML está obsoleta!"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"debug": "Depuração avançada",
|
||||
"exclude_devices": "ou Excluir esses dispositivos de todos (separados por vírgula)",
|
||||
"extended_entity_discovery": "Inclua sensores, interruptores e luzes adicionais.",
|
||||
"include_devices": "Inclua apenas estes dispositivos (separados por vírgula)",
|
||||
"otp_secret": "Chave de 52 caracteres do App Autenticador para 2SV da Amazon",
|
||||
"public_url": "URL pública compartilhada com serviços hospedados externamente",
|
||||
"queue_delay": "Tempo de espera para enfileirar vários comandos (em segundos)",
|
||||
"scan_interval": "Frequência de sondagem programada (segundos)",
|
||||
"should_get_network": "Descubra a rede Alexa"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Habilita o registro detalhado em nível de rastreamento para solução de problemas avançada. \n Não recomendado para operação normal devido ao aumento do volume de logs. \n Certifique-se de que os níveis de registro estejam definidos como DEBUG para obter a saída completa.",
|
||||
"otp_secret": "Exemplo: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
},
|
||||
"description": "* Entradas obrigatórias",
|
||||
"title": "Alexa Media Player - Reconfiguração"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"enable_network_discovery": {
|
||||
"description": "Reativa a descoberta de rede da Alexa para que o próximo ciclo de pesquisa redescubra os dispositivos Alexa das contas selecionadas.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Opcional: Endereço eletrônico da conta Alexa ou lista de endereços eletrônicos. Se estiver vazio, todas as contas conhecidas serão atualizadas.",
|
||||
"name": "Endereço eletrônico"
|
||||
}
|
||||
},
|
||||
"name": "Habilitar descoberta de rede"
|
||||
},
|
||||
"force_logout": {
|
||||
"description": "Forçar o logout da conta. Usado principalmente para depuração.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Contas para limpar. Deixar vazio limpará tudo.",
|
||||
"name": "Endereço eletrônico"
|
||||
}
|
||||
},
|
||||
"name": "Forçar o logout"
|
||||
},
|
||||
"get_history_records": {
|
||||
"description": "Analisa os registros de histórico do dispositivo especificado:",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entidade para obter o histórico de:",
|
||||
"name": "Selecione o media player:"
|
||||
},
|
||||
"entries": {
|
||||
"description": "Número de entradas para obter:",
|
||||
"name": "Número de entradas"
|
||||
}
|
||||
},
|
||||
"name": "Obter registros históricos"
|
||||
},
|
||||
"restore_volume": {
|
||||
"description": "Restaurar o nível de volume anterior no dispositivo reprodutor de mídia Alexa.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entidade para restaurar o nível de volume anterior",
|
||||
"name": "Selecione o media player:"
|
||||
}
|
||||
},
|
||||
"name": "Restaurar volume anterior"
|
||||
},
|
||||
"update_last_called": {
|
||||
"description": "Força a atualização do último dispositivo eco chamado para cada conta Alexa.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Lista de contas Alexa para atualizar. Se deixar vazio, atualizará todas as contas conhecidas.",
|
||||
"name": "Endereço eletrônico"
|
||||
}
|
||||
},
|
||||
"name": "Atualizar último sensor chamado"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "A página de Esqueci a Palavra-passe foi detetada. Isto normalmente é o resultado de demasiadas tentativas de login falhadas. A Amazon pode exigir uma ação antes de ser possível tentar iniciar sessão novamente.",
|
||||
"login_failed": "Alexa Media Player não conseguiu fazer o login.",
|
||||
"reauth_successful": "Alexa Media Player reautenticado com sucesso. Por favor, ignore a mensagem \"Aborted\" do HA."
|
||||
},
|
||||
"error": {
|
||||
"2fa_key_invalid": "{otp_secret} é inválido",
|
||||
"connection_error": "Erro ao conectar; verifique a rede e tente novamente",
|
||||
"identifier_exists": "E-mail para URL Alexa já registado",
|
||||
"invalid_auth": "O ‘login’ não foi bem-sucedido. Verifique novamente o seu endereço eletrónico, senha e chave de autenticação.",
|
||||
"invalid_credentials": "Credenciais inválidas",
|
||||
"invalid_url": "O URL é inválido: {message}",
|
||||
"oauth_error": "Não foi possível concluir o login OAuth. Tente novamente.",
|
||||
"unable_to_connect_hass_url": "Não foi possível conectar ao URL local do Home Assistant. Verifique o URL em Configurações > Sistema > Rede > URL do Home Assistant > Rede local.",
|
||||
"unknown_error": "Erro desconhecido: {message}"
|
||||
},
|
||||
"step": {
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "Ignore e Continue - Entendo que não há suporte para problemas de login para ignorar este aviso."
|
||||
},
|
||||
"description": "O servidor HA não consegue se conectar ao URL fornecido: {hass_url}.\n > {error} \n\nPara corrigir isso, confirme se o seu navegador consegue acessar o endereço. {hass_url}. Este campo é de Configurações > Sistema > Rede > URL do Home Assistant.\n\nSe você tiver **certeza** de que o seu navegador consegue acessar este URL, pode ignorar este aviso.",
|
||||
"title": "Alexa Media Player - Não é possível conectar ao URL de alta disponibilidade"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "Sim, o código OTP foi verificado."
|
||||
},
|
||||
"description": "** {email} - alexa. {url} **\nVocê verificou o código OTP na verificação de duas vias da Amazon?\n>Código OTP: {message}",
|
||||
"title": "Alexa Media Player - Confirmação OTP"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "Depuração avançada",
|
||||
"email": "Endereço de e-mail",
|
||||
"exclude_devices": "ou Excluir esses dispositivos de todos (separados por vírgula)",
|
||||
"extended_entity_discovery": "Inclua sensores, interruptores e luzes adicionais.",
|
||||
"hass_url": "URL da rede local para acessar o Home Assistant",
|
||||
"include_devices": "Inclua apenas estes dispositivos (separados por vírgula)",
|
||||
"otp_secret": "Chave de 52 caracteres da App Autenticadora para Amazon 2SV",
|
||||
"password": "Senha",
|
||||
"public_url": "URL pública compartilhada com serviços hospedados externos",
|
||||
"queue_delay": "Tempo de espera para enfileirar vários comandos (em segundos)",
|
||||
"scan_interval": "Intervalo de sondagem programado (segundos)",
|
||||
"securitycode": "Palavra-passe de uso único (OTP)",
|
||||
"should_get_network": "Descubra a rede Alexa",
|
||||
"url": "Região do domínio Amazon (ex. amazon.com.br)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Habilita o registro detalhado em nível de rastreamento para solução de problemas avançada. \n Não recomendado para operação normal devido ao aumento do volume de logs. \n Certifique-se de que os níveis de registro estejam definidos como DEBUG para obter a saída completa.",
|
||||
"otp_secret": "Exemplo: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Qualidade do ar"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "Monóxido de carbono"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Umidade"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "Qualidade do ar interior"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "Material particulado"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Compostos orgânicos voláteis"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Próximo alarme"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Próximo lembrete"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "Próximo cronômetro"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperatura"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Não incomodar"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Repita"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Embaralhar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"description": "A configuração YAML do Alexa Media Player está obsoleta.\nRemova alexa_media da sua configuração, reinicie o Home Assistant e utilize a “interface” do utilizador para o configurar.\nConfigurações > Dispositivos e serviços > Integrações > ADICIONAR INTEGRAÇÃO",
|
||||
"title": "A configuração YAML está obsoleta"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"debug": "Depuração avançada",
|
||||
"exclude_devices": "ou Excluir esses dispositivos de todos (separados por vírgula)",
|
||||
"extended_entity_discovery": "Inclua sensores, interruptores e luzes adicionais.",
|
||||
"include_devices": "Inclua apenas estes dispositivos (separados por vírgula)",
|
||||
"otp_secret": "Chave de 52 caracteres da App Autenticadora para Amazon 2SV",
|
||||
"public_url": "URL pública compartilhada com serviços hospedados externamente",
|
||||
"queue_delay": "Tempo de espera para enfileirar vários comandos (em segundos)",
|
||||
"scan_interval": "Frequência de sondagem programada (segundos)",
|
||||
"should_get_network": "Descubra a rede Alexa"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Habilita o registro detalhado em nível de rastreamento para solução de problemas avançada. \n Não recomendado para operação normal devido ao aumento do volume de logs. \n Certifique-se de que os níveis de registro estejam definidos como DEBUG para obter a saída completa.",
|
||||
"otp_secret": "Exemplo: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
},
|
||||
"description": "* Entradas obrigatórias",
|
||||
"title": "Alexa Media Player - Reconfiguração"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"enable_network_discovery": {
|
||||
"description": "Reativa a descoberta de rede da Alexa para que o próximo ciclo de pesquisa redescubra os dispositivos Alexa das contas selecionadas.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Opcional: Endereço eletrónico da conta Alexa ou lista de endereços eletrónicos. Se estiver vazio, todas as contas conhecidas serão atualizadas.",
|
||||
"name": "Endereço de email"
|
||||
}
|
||||
},
|
||||
"name": "Habilitar descoberta de rede"
|
||||
},
|
||||
"force_logout": {
|
||||
"description": "Forçar o logout da conta. Usado principalmente para depuração.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Contas a limpar. Vazio vai limpar tudo.",
|
||||
"name": "Endereço de email"
|
||||
}
|
||||
},
|
||||
"name": "Forçar logout"
|
||||
},
|
||||
"get_history_records": {
|
||||
"description": "Analisa os registos de histórico do dispositivo especificado",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entidade para obter o histórico de",
|
||||
"name": "Selecione o media player:"
|
||||
},
|
||||
"entries": {
|
||||
"description": "Número de entradas para obter",
|
||||
"name": "Número de entradas"
|
||||
}
|
||||
},
|
||||
"name": "Obter registos históricos"
|
||||
},
|
||||
"restore_volume": {
|
||||
"description": "Restaurar o nível de volume anterior no dispositivo reprodutor de média Alexa",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entidade para restaurar o nível de volume anterior em",
|
||||
"name": "Selecione o media player:"
|
||||
}
|
||||
},
|
||||
"name": "Restaurar volume anterior"
|
||||
},
|
||||
"update_last_called": {
|
||||
"description": "Força a atualização do dispositivo de echo last_called para cada conta Alexa.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Lista de contas Alexa para atualizar. Se estiver vazio, atualizará todas as contas conhecidas.",
|
||||
"name": "Endereço de email"
|
||||
}
|
||||
},
|
||||
"name": "Atualizar último sensor chamado"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "Обнаружена страница «Забыли пароль». Обычно это происходит из-за слишком большого количества неудачных попыток входа. Amazon может потребовать действий, прежде чем можно будет повторно войти в систему.",
|
||||
"login_failed": "Алекса Медиа Проигрыватель логин не удался.",
|
||||
"reauth_successful": "Алекса Медиа Проигрыватель успешно прошел повторную аутентификацию. Пожалуйста, игнорируйте сообщение «Прервано» от HA."
|
||||
},
|
||||
"error": {
|
||||
"2fa_key_invalid": "{otp_secret} недействителен",
|
||||
"connection_error": "Ошибка подключения; проверьте сеть и повторите попытку",
|
||||
"identifier_exists": "Электронная почта для Alexa уже зарегистрирована",
|
||||
"invalid_auth": "Не удалось войти. Пожалуйста, проверьте адрес электронной почты, пароль и ключ аутентификации.",
|
||||
"invalid_credentials": "Неверные учетные данные",
|
||||
"invalid_url": "Недопустимый URL-адрес: {message}",
|
||||
"oauth_error": "Не удалось завершить вход через OAuth. Попробуйте ещё раз.",
|
||||
"unable_to_connect_hass_url": "Не удаётся подключиться к локальному URL-адресу Home Assistant. Проверьте URL-адрес в разделе «Настройки» > «Система» > «Сеть» > «URL-адрес Home Assistant» > «Локальная сеть».",
|
||||
"unknown_error": "Неизвестная ошибка: {message}"
|
||||
},
|
||||
"step": {
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "Игнорировать и продолжить. Я понимаю, что для обхода этого предупреждения не предоставляется никакой поддержки при проблемах со входом в систему."
|
||||
},
|
||||
"description": "Home Assistant сервер не может подключиться по указанному адресу: {hass_url}.\n> {error}\n\nДля решения этой проблемы, пожалуйста, убедитесь, что ваш браузер имеет доступ к указанному ресурсу. {hass_url}. Это поле находится в разделе Настройки > Система > Сеть > URL-адрес Home Assistant.\n\nЕсли вы **уверены**, что ваш клиент может получить доступ к этому URL-адресу, вы можете обойти это предупреждение.",
|
||||
"title": "Алекса Медиа Проигрыватель - не может подключиться к Home Assistant адресу"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "Да, код OTP был подтвержден."
|
||||
},
|
||||
"description": "**{email} - Алекса.{url}** \nВы подтвердили OTP-код в Amazon 2SV?\n >OTP-код {message}",
|
||||
"title": "Алекса Медиа Проигрыватель — подтверждение OTP"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "Расширенные возможности отладки",
|
||||
"email": "Адрес электронной почты",
|
||||
"exclude_devices": "или Исключить эти устройства из всех (разделенных запятыми)",
|
||||
"extended_entity_discovery": "Включите дополнительные датчики, выключатели и осветительные приборы.",
|
||||
"hass_url": "URL-адрес локальной сети для доступа к Home Assistant",
|
||||
"include_devices": "Укажите только эти устройства (разделенные запятыми).",
|
||||
"otp_secret": "52-символьный ключ приложения аутентификатора для Amazon 2SV",
|
||||
"password": "Пароль",
|
||||
"public_url": "Публичный URL-адрес, предоставленный внешним размещенным службам",
|
||||
"queue_delay": "Задержка для объединения нескольких команд в очередь (в секундах)",
|
||||
"scan_interval": "Запланированный интервал опроса (в секундах)",
|
||||
"securitycode": "Одноразовый пароль (OTP)",
|
||||
"should_get_network": "Откройте для себя сеть Alexa",
|
||||
"url": "Домен региона Amazon (например, amazon.co.uk)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Включает очень подробное логирование на уровне трассировки для расширенного поиска и устранения неисправностей. \n Не рекомендуется для обычной работы из-за увеличенного объема логов. \n Убедитесь, что уровни логирования установлены на DEBUG для получения полного вывода.",
|
||||
"otp_secret": "Пример: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Качество воздуха"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "Оксид углерода"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Влажность"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "Качество воздуха в помещении"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "Твердые частицы"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Летучие органические соединения"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Следующий будильник"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Следующее напоминание"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "В следующий раз"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Температура"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Просьба не беспокоить"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Повторить"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Перетасовка"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"description": "Конфигурация Alexa Media Player в формате YAML устарела.\nУдалите `alexa_media` из конфигурации, перезапустите Home Assistant и используйте пользовательский интерфейс для настройки.\nНастройки > Устройства и сервисы > Интеграции > ДОБАВИТЬ ИНТЕГРАЦИЮ",
|
||||
"title": "Конфигурация YAML устарела"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"debug": "Расширенные возможности отладки",
|
||||
"exclude_devices": "или Исключить эти устройства из всех (разделенных запятыми)",
|
||||
"extended_entity_discovery": "Включите дополнительные датчики, выключатели и осветительные приборы.",
|
||||
"include_devices": "Укажите только эти устройства (разделенные запятыми).",
|
||||
"otp_secret": "52-символьный ключ приложения аутентификатора для Amazon 2SV",
|
||||
"public_url": "Публичный URL-адрес предоставляется внешним хостинг-сервисам.",
|
||||
"queue_delay": "Задержка для объединения нескольких команд в очередь (в секундах)",
|
||||
"scan_interval": "Запланированная частота опроса (в секундах)",
|
||||
"should_get_network": "Откройте для себя сеть Alexa"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Включает очень подробное логирование на уровне трассировки для расширенного поиска и устранения неисправностей. \n Не рекомендуется для обычной работы из-за увеличенного объема логов. \n Убедитесь, что уровни логирования установлены на DEBUG для получения полного вывода.",
|
||||
"otp_secret": "Пример: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
},
|
||||
"description": "* Обязательные поля",
|
||||
"title": "Alexa Media Player - Перенастройка"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"enable_network_discovery": {
|
||||
"description": "Повторно включает обнаружение сети Alexa, чтобы в следующем цикле опроса были повторно обнаружены устройства Alexa для выбранных учетных записей.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Необязательный адрес электронной почты для учётной записи Alexa или список адресов электронной почты. Если не указано, все известные учётные записи будут обновлены.",
|
||||
"name": "Почтовые адреса"
|
||||
}
|
||||
},
|
||||
"name": "Включить сетевое обнаружение"
|
||||
},
|
||||
"force_logout": {
|
||||
"description": "Принудительный выход из аккаунта. В основном используется для отладки.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Аккаунты для очистки. Если пустое, то будут очищены все.",
|
||||
"name": "Почтовые адреса"
|
||||
}
|
||||
},
|
||||
"name": "Принудительный выход"
|
||||
},
|
||||
"get_history_records": {
|
||||
"description": "Анализирует записи истории для указанного устройства.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Сущность, для которой нужно получить историю",
|
||||
"name": "Выберите медиа плеер:"
|
||||
},
|
||||
"entries": {
|
||||
"description": "Количество записей, которые нужно получить",
|
||||
"name": "Количество записей"
|
||||
}
|
||||
},
|
||||
"name": "Получить исторические записи"
|
||||
},
|
||||
"restore_volume": {
|
||||
"description": "Восстановить предыдущий уровень громкости на медиа плеере Alexa",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Сущность для восстановления предыдущего уровня громкости",
|
||||
"name": "Выберите медиа плеер:"
|
||||
}
|
||||
},
|
||||
"name": "Восстановить предыдущий том"
|
||||
},
|
||||
"update_last_called": {
|
||||
"description": "Принудительное обновление последнего вызванного устройства для каждого аккаунта Алекса.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Список аккаунтов Алекса для обновления. Если пустое, будут обновлены все аккаунты.",
|
||||
"name": "Почтовые адреса"
|
||||
}
|
||||
},
|
||||
"name": "Обновление последнего вызванного сенсора"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "The Forgot Password page was detected. This normally is the result of too many failed logins. Amazon may require action before a relogin can be attempted.",
|
||||
"login_failed": "Alexa Media Player failed to login.",
|
||||
"reauth_successful": "Alexa Media Player successfully reauthenticated. Please ignore the \"Aborted\" message from HA."
|
||||
},
|
||||
"error": {
|
||||
"2fa_key_invalid": "{otp_secret} is invalid",
|
||||
"connection_error": "Error connecting; check network and retry",
|
||||
"identifier_exists": "Email for Alexa URL already registered",
|
||||
"invalid_auth": "Login was not successful. Please double-check your email, password, and Authenticator key.",
|
||||
"invalid_credentials": "Invalid credentials",
|
||||
"invalid_url": "URL is invalid: {message}",
|
||||
"oauth_error": "Could not complete OAuth login. Please try again.",
|
||||
"unable_to_connect_hass_url": "Unable to connect to Home Assistant Local URL. Please check the URL under Settings > System > Network > Home Assistant URL > Local network",
|
||||
"unknown_error": "Unknown error: {message}"
|
||||
},
|
||||
"step": {
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "Ignore and Continue - I understand that no support for login issues are provided for bypassing this warning."
|
||||
},
|
||||
"description": "The HA server cannot connect to the URL provided: {hass_url}.\n> {error}\n\nTo fix this, please confirm your browser can reach {hass_url}. This field is from Settings > System > Network > Home Assistant URL.\n\nIf you are **certain** your browser can reach this URL, you can bypass this warning.",
|
||||
"title": "Alexa Media Player - Unable to Connect to HA URL"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "Yes, OTP code was verified"
|
||||
},
|
||||
"description": "**{email} - alexa.{url}** \nHave you verified the OTP code in Amazon 2SV? \n >OTP Code: {message}",
|
||||
"title": "Alexa Media Player - OTP Confirmation"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "Advanced debug",
|
||||
"email": "Email Address",
|
||||
"exclude_devices": "or Exclude these devices from all (comma separated)",
|
||||
"extended_entity_discovery": "Include additional sensors, switches and lights",
|
||||
"hass_url": "Local network URL to access Home Assistant",
|
||||
"include_devices": "Only include these devices (comma separated)",
|
||||
"otp_secret": "52-character Authenticator App Key for Amazon 2SV",
|
||||
"password": "Password",
|
||||
"public_url": "Public URL shared with external hosted services",
|
||||
"queue_delay": "Delay to queue multiple commands together (seconds)",
|
||||
"scan_interval": "Scheduled polling interval (seconds)",
|
||||
"securitycode": "One-time password (OTP)",
|
||||
"should_get_network": "Discover Alexa network",
|
||||
"url": "Amazon region domain (e.g., amazon.co.uk)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
|
||||
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Air quality"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "Carbon monoxide"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Humidity"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "Indoor air quality"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "Particulate matter"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Volatile organic compounds"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Next alarm"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Next reminder"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "Next timer"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperature"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Do not disturb"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Repeat"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Shuffle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"description": "YAML configuration of Alexa Media Player is deprecated.\nPlease remove `alexa_media` from your configuration, restart Home Assistant and use the UI to configure it instead.\nSettings > Devices & services > Integrations > ADD INTEGRATION",
|
||||
"title": "YAML configuration is deprecated"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"debug": "Advanced debug",
|
||||
"exclude_devices": "or Exclude these devices from all (comma separated)",
|
||||
"extended_entity_discovery": "Include additional sensors, switches and lights",
|
||||
"include_devices": "Only include these devices (comma separated)",
|
||||
"otp_secret": "52-character Authenticator App Key for Amazon 2SV",
|
||||
"public_url": "Public URL shared with external hosted services",
|
||||
"queue_delay": "Delay to queue multiple commands together (seconds)",
|
||||
"scan_interval": "Scheduled polling interval (seconds)",
|
||||
"should_get_network": "Discover Alexa network"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
|
||||
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
},
|
||||
"description": "* Required entry",
|
||||
"title": "Alexa Media Player - Reconfiguration"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"enable_network_discovery": {
|
||||
"description": "Re-enables Alexa network discovery so the next polling cycle will rediscover Alexa devices for the selected accounts.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Optional Alexa account email or list of emails. If empty, all known accounts will be refreshed.",
|
||||
"name": "Email address"
|
||||
}
|
||||
},
|
||||
"name": "Enable Network Discovery"
|
||||
},
|
||||
"force_logout": {
|
||||
"description": "Force account to logout. Used mainly for debugging.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "Accounts to clear. Empty will clear all.",
|
||||
"name": "Email address"
|
||||
}
|
||||
},
|
||||
"name": "Force Logout"
|
||||
},
|
||||
"get_history_records": {
|
||||
"description": "Parses the history records for the specified device",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entity to get the history for",
|
||||
"name": "Select media player:"
|
||||
},
|
||||
"entries": {
|
||||
"description": "Number of entries to get",
|
||||
"name": "Number of entries"
|
||||
}
|
||||
},
|
||||
"name": "Get History Records"
|
||||
},
|
||||
"restore_volume": {
|
||||
"description": "Restore previous volume level on Alexa media player device",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Entity to restore the previous volume level on",
|
||||
"name": "Select media player:"
|
||||
}
|
||||
},
|
||||
"name": "Restore Previous Volume"
|
||||
},
|
||||
"update_last_called": {
|
||||
"description": "Forces update of last_called echo device for each Alexa account.",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "List of Alexa accounts to update. If empty, will update all known accounts.",
|
||||
"name": "Email address"
|
||||
}
|
||||
},
|
||||
"name": "Update Last Called Sensor"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"forgot_password": "检测到“忘记密码”页面。这通常是由于多次登录失败导致的。在重新登录之前,亚马逊可能需要采取一些措施。",
|
||||
"login_failed": "Alexa 媒体播放器登录失败。",
|
||||
"reauth_successful": "Alexa 媒体播放器已成功重新验证。请忽略来自 HA 的“Aborted”消息。"
|
||||
},
|
||||
"error": {
|
||||
"2fa_key_invalid": "{otp_secret} 无效",
|
||||
"connection_error": "连接错误;检查网络并重试",
|
||||
"identifier_exists": "Alexa URL的电子邮件已注册",
|
||||
"invalid_auth": "登录失败。请仔细检查您的邮箱、密码和验证码。",
|
||||
"invalid_credentials": "无效的凭证",
|
||||
"invalid_url": "URL 无效: {message}",
|
||||
"oauth_error": "OAuth登录失败,请重试。",
|
||||
"unable_to_connect_hass_url": "无法连接到 Home Assistant 本地 URL。请检查“设置”>“系统”>“网络”>“Home Assistant URL”>“本地网络”中的 URL。",
|
||||
"unknown_error": "未知错误: {message}"
|
||||
},
|
||||
"step": {
|
||||
"proxy_warning": {
|
||||
"data": {
|
||||
"proxy_warning": "忽略并继续 - 我了解不提供对登录问题的支持来绕过此警告。"
|
||||
},
|
||||
"description": "HA 服务器无法连接到提供的 URL:{hass_url}。\n> {error}\n\n要解决此问题,请确认您的浏览器可以访问 {hass_url}。此字段位于“设置”>“系统”>“网络”>“Home Assistant URL”中。\n\n如果您**确定**您的浏览器可以访问此 URL,则可以绕过此警告。",
|
||||
"title": "Alexa 媒体播放器 - 无法连接到 HA URL"
|
||||
},
|
||||
"totp_register": {
|
||||
"data": {
|
||||
"registered": "是的,OTP验证码已验证"
|
||||
},
|
||||
"description": "**{email} - alexa.{url}** \n您是否已在亚马逊 2SV 中验证过 OTP 代码?\n >OTP Code {message}",
|
||||
"title": "Alexa 媒体播放器 - OTP 确认"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"debug": "高级调试",
|
||||
"email": "电子邮件地址",
|
||||
"exclude_devices": "或者将这些设备从所有列表中排除(以逗号分隔)",
|
||||
"extended_entity_discovery": "增加额外的传感器、开关和灯",
|
||||
"hass_url": "用于访问 Home Assistant 的本地网络 URL",
|
||||
"include_devices": "仅包含以下设备(以逗号分隔)",
|
||||
"otp_secret": "亚马逊双重验证的 52 字符身份验证器应用密钥",
|
||||
"password": "密码",
|
||||
"public_url": "与外部托管服务共享的公共 URL",
|
||||
"queue_delay": "将多个命令排队的延迟时间(秒)",
|
||||
"scan_interval": "计划轮询间隔(秒)",
|
||||
"securitycode": "一次性密码(OTP)",
|
||||
"should_get_network": "发现 Alexa 网络",
|
||||
"url": "亚马逊区域域名(例如 amazon.co.uk)"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
|
||||
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Air quality"
|
||||
},
|
||||
"air_quality_carbon_monoxide": {
|
||||
"name": "Carbon monoxide"
|
||||
},
|
||||
"air_quality_humidity": {
|
||||
"name": "Humidity"
|
||||
},
|
||||
"air_quality_indoor_air_quality": {
|
||||
"name": "Indoor air quality"
|
||||
},
|
||||
"air_quality_particulate_matter": {
|
||||
"name": "Particulate matter"
|
||||
},
|
||||
"air_quality_volatile_organic_compounds": {
|
||||
"name": "Volatile organic compounds"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Next alarm"
|
||||
},
|
||||
"next_reminder": {
|
||||
"name": "Next reminder"
|
||||
},
|
||||
"next_timer": {
|
||||
"name": "Next timer"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperature"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Do not disturb"
|
||||
},
|
||||
"repeat": {
|
||||
"name": "Repeat"
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "Shuffle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_configuration": {
|
||||
"description": "Alexa Media Player 的 YAML 配置已弃用。\n请从配置中移除 `alexa_media`,重启 Home Assistant,然后改用用户界面进行配置。\n设置 > 设备和服务 > 集成 > 添加集成",
|
||||
"title": "YAML配置已弃用"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"debug": "高级调试",
|
||||
"exclude_devices": "或者将这些设备从所有列表中排除(以逗号分隔)",
|
||||
"extended_entity_discovery": "增加额外的传感器、开关和灯",
|
||||
"include_devices": "仅包含以下设备(以逗号分隔)",
|
||||
"otp_secret": "52-character Authenticator App Key for Amazon 2SV",
|
||||
"public_url": "与外部托管服务共享的公共 URL",
|
||||
"queue_delay": "将多个命令排队的延迟时间(秒)",
|
||||
"scan_interval": "计划轮询频率(秒)",
|
||||
"should_get_network": "发现 Alexa 网络"
|
||||
},
|
||||
"data_description": {
|
||||
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
|
||||
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
|
||||
},
|
||||
"description": "* 必填项",
|
||||
"title": "Alexa 媒体播放器 - 重新配置"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"enable_network_discovery": {
|
||||
"description": "重新启用 Alexa 网络发现功能,以便在下一个轮询周期中重新发现所选帐户的 Alexa 设备。",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "可选的 Alexa 帐户电子邮件地址或电子邮件地址列表。如果为空,则会刷新所有已知帐户。",
|
||||
"name": "电子邮件"
|
||||
}
|
||||
},
|
||||
"name": "启用网络发现"
|
||||
},
|
||||
"force_logout": {
|
||||
"description": "强制帐户注销。主要用于调试。",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "要清除的帐户。清空将清除所有帐户。",
|
||||
"name": "电子邮件地址"
|
||||
}
|
||||
},
|
||||
"name": "强制注销"
|
||||
},
|
||||
"get_history_records": {
|
||||
"description": "解析指定设备的历史记录",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "要获取历史记录的实体",
|
||||
"name": "选择媒体播放器:"
|
||||
},
|
||||
"entries": {
|
||||
"description": "需要获取的条目数量",
|
||||
"name": "条目数量"
|
||||
}
|
||||
},
|
||||
"name": "获取历史记录"
|
||||
},
|
||||
"restore_volume": {
|
||||
"description": "恢复 Alexa 媒体播放器设备上的先前音量级别",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "实体恢复先前的音量水平",
|
||||
"name": "选择媒体播放器:"
|
||||
}
|
||||
},
|
||||
"name": "恢复先前的音量"
|
||||
},
|
||||
"update_last_called": {
|
||||
"description": "强制更新每个 Alexa 帐户的 last_called 回声设备。",
|
||||
"fields": {
|
||||
"email": {
|
||||
"description": "要更新的 Alexa 帐户列表。如果为空,将更新所有已知帐户。",
|
||||
"name": "电子邮件地址"
|
||||
}
|
||||
},
|
||||
"name": "更新上次呼叫传感器"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"""C.A.F.E. - Visual automation editor for Home Assistant."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .panel import async_register_panel, async_unregister_panel
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the C.A.F.E. component."""
|
||||
# This will be called when the integration is loaded
|
||||
# But actual setup happens in async_setup_entry
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry) -> bool:
|
||||
"""Set up C.A.F.E. from a config entry."""
|
||||
|
||||
# Register the panel (frontend)
|
||||
await async_register_panel(hass)
|
||||
|
||||
_LOGGER.info("C.A.F.E. integration set up successfully")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
async_unregister_panel(hass)
|
||||
return True
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user