Add event and button entities, select entity for model switching, and update changelog to version 0.1.56
This commit is contained in:
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
All notable changes to the OpenClaw Home Assistant Integration will be documented in this file.
|
All notable changes to the OpenClaw Home Assistant Integration will be documented in this file.
|
||||||
|
|
||||||
|
## [0.1.56] - 2026-02-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Event entities** (`event.openclaw_message_received`, `event.openclaw_tool_invoked`) — native HA EventEntity entities that fire on each assistant reply and tool invocation result. Selectable in the automation UI without YAML.
|
||||||
|
- **Button entities** — dashboard-friendly buttons for common actions:
|
||||||
|
- **Clear History** — clears in-memory conversation history
|
||||||
|
- **Sync History** — triggers a backend coordinator refresh
|
||||||
|
- **Run Diagnostics** — fires a connectivity check against the gateway
|
||||||
|
- **Select entity** (`select.openclaw_active_model`) — exposes the list of available models from the gateway's `/v1/models` endpoint, allowing model switching from the HA dashboard. Selection is persisted in config entry options.
|
||||||
|
- Coordinator now caches the full model list (not just the first model) and exposes it via `coordinator.available_models`.
|
||||||
|
|
||||||
## [0.1.55] - 2026-02-23
|
## [0.1.55] - 2026-02-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ _CARD_PATH = Path(__file__).parent / "www" / _CARD_FILENAME
|
|||||||
# URL at which the card JS is served (registered via register_static_path)
|
# URL at which the card JS is served (registered via register_static_path)
|
||||||
_CARD_STATIC_URL = f"/openclaw/{_CARD_FILENAME}"
|
_CARD_STATIC_URL = f"/openclaw/{_CARD_FILENAME}"
|
||||||
# Versioned URL used for Lovelace resource registration to avoid stale browser cache
|
# Versioned URL used for Lovelace resource registration to avoid stale browser cache
|
||||||
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.55"
|
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.56"
|
||||||
|
|
||||||
OpenClawConfigEntry = ConfigEntry
|
OpenClawConfigEntry = ConfigEntry
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
"""Button entities for the OpenClaw integration.
|
||||||
|
|
||||||
|
Provides dashboard-friendly buttons for common actions:
|
||||||
|
- Clear History — clears in-memory conversation history
|
||||||
|
- Sync History — triggers backend history re-sync
|
||||||
|
- Run Diagnostics — fires a connectivity check against the gateway
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .api import OpenClawApiClient
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import OpenClawCoordinator
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
BUTTON_DESCRIPTIONS: tuple[ButtonEntityDescription, ...] = (
|
||||||
|
ButtonEntityDescription(
|
||||||
|
key="clear_history",
|
||||||
|
translation_key="clear_history",
|
||||||
|
name="OpenClaw Clear History",
|
||||||
|
icon="mdi:delete-sweep",
|
||||||
|
),
|
||||||
|
ButtonEntityDescription(
|
||||||
|
key="sync_history",
|
||||||
|
translation_key="sync_history",
|
||||||
|
name="OpenClaw Sync History",
|
||||||
|
icon="mdi:sync",
|
||||||
|
),
|
||||||
|
ButtonEntityDescription(
|
||||||
|
key="run_diagnostics",
|
||||||
|
translation_key="run_diagnostics",
|
||||||
|
name="OpenClaw Run Diagnostics",
|
||||||
|
icon="mdi:stethoscope",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up OpenClaw button entities from a config entry."""
|
||||||
|
entry_data: dict = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
coordinator: OpenClawCoordinator = entry_data["coordinator"]
|
||||||
|
client: OpenClawApiClient = entry_data["client"]
|
||||||
|
|
||||||
|
entities = [
|
||||||
|
OpenClawButton(coordinator, client, description, entry, hass)
|
||||||
|
for description in BUTTON_DESCRIPTIONS
|
||||||
|
]
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class OpenClawButton(CoordinatorEntity[OpenClawCoordinator], ButtonEntity):
|
||||||
|
"""Button entity for OpenClaw actions."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: OpenClawCoordinator,
|
||||||
|
client: OpenClawApiClient,
|
||||||
|
description: ButtonEntityDescription,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the button."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = description
|
||||||
|
self._client = client
|
||||||
|
self._entry = entry
|
||||||
|
self._hass = hass
|
||||||
|
self._attr_unique_id = f"{entry.entry_id}_{description.key}"
|
||||||
|
self._attr_device_info = {
|
||||||
|
"identifiers": {(DOMAIN, entry.entry_id)},
|
||||||
|
"name": "OpenClaw Assistant",
|
||||||
|
"manufacturer": "OpenClaw",
|
||||||
|
"model": "OpenClaw Gateway",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def async_press(self) -> None:
|
||||||
|
"""Handle button press."""
|
||||||
|
key = self.entity_description.key
|
||||||
|
|
||||||
|
if key == "clear_history":
|
||||||
|
store_key = f"{DOMAIN}_chat_history"
|
||||||
|
store = self._hass.data.get(store_key)
|
||||||
|
if isinstance(store, dict):
|
||||||
|
store.clear()
|
||||||
|
_LOGGER.info("OpenClaw chat history cleared via button")
|
||||||
|
|
||||||
|
elif key == "sync_history":
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
_LOGGER.info("OpenClaw history sync triggered via button")
|
||||||
|
|
||||||
|
elif key == "run_diagnostics":
|
||||||
|
try:
|
||||||
|
alive = await self._client.async_check_alive()
|
||||||
|
if alive:
|
||||||
|
_LOGGER.info("OpenClaw diagnostics: gateway is reachable")
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("OpenClaw diagnostics: gateway did not respond")
|
||||||
|
except Exception as err: # noqa: BLE001
|
||||||
|
_LOGGER.error("OpenClaw diagnostics failed: %s", err)
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
@@ -106,7 +106,7 @@ DATA_LAST_TOOL_ERROR = "last_tool_error"
|
|||||||
DATA_LAST_TOOL_RESULT_PREVIEW = "last_tool_result_preview"
|
DATA_LAST_TOOL_RESULT_PREVIEW = "last_tool_result_preview"
|
||||||
|
|
||||||
# Platforms
|
# Platforms
|
||||||
PLATFORMS = ["sensor", "binary_sensor", "conversation"]
|
PLATFORMS = ["sensor", "binary_sensor", "conversation", "event", "button", "select"]
|
||||||
|
|
||||||
# Events
|
# Events
|
||||||
EVENT_MESSAGE_RECEIVED = f"{DOMAIN}_message_received"
|
EVENT_MESSAGE_RECEIVED = f"{DOMAIN}_message_received"
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ class OpenClawCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
self.client = client
|
self.client = client
|
||||||
self._last_activity: datetime | None = None
|
self._last_activity: datetime | None = None
|
||||||
self._model_cache: dict[str, Any] = {}
|
self._model_cache: dict[str, Any] = {}
|
||||||
|
self._available_models: list[str] = []
|
||||||
self._consecutive_failures = 0
|
self._consecutive_failures = 0
|
||||||
self._last_tool_state: dict[str, Any] = {
|
self._last_tool_state: dict[str, Any] = {
|
||||||
DATA_LAST_TOOL_NAME: None,
|
DATA_LAST_TOOL_NAME: None,
|
||||||
@@ -147,6 +148,9 @@ class OpenClawCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
DATA_PROVIDER: current.get("owned_by"),
|
DATA_PROVIDER: current.get("owned_by"),
|
||||||
DATA_CONTEXT_WINDOW: current.get("context_window"),
|
DATA_CONTEXT_WINDOW: current.get("context_window"),
|
||||||
}
|
}
|
||||||
|
self._available_models = [
|
||||||
|
m.get("id") for m in models if m.get("id")
|
||||||
|
]
|
||||||
except OpenClawAuthError as err:
|
except OpenClawAuthError as err:
|
||||||
_LOGGER.warning("Gateway auth failed during poll: %s", err)
|
_LOGGER.warning("Gateway auth failed during poll: %s", err)
|
||||||
await self._try_refresh_token()
|
await self._try_refresh_token()
|
||||||
@@ -199,6 +203,11 @@ class OpenClawCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
"""
|
"""
|
||||||
self._last_activity = datetime.now(timezone.utc)
|
self._last_activity = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available_models(self) -> list[str]:
|
||||||
|
"""Return the list of model IDs from the last successful poll."""
|
||||||
|
return list(self._available_models)
|
||||||
|
|
||||||
def record_tool_invocation(
|
def record_tool_invocation(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
"""Event entities for the OpenClaw integration.
|
||||||
|
|
||||||
|
Provides native HA EventEntity entities for:
|
||||||
|
- openclaw_message_received — fires on each assistant reply
|
||||||
|
- openclaw_tool_invoked — fires on each tool invocation result
|
||||||
|
|
||||||
|
These complement the raw HA bus events with proper entity-registry entries
|
||||||
|
that are selectable in the automation UI (no YAML needed).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.event import EventEntity, EventEntityDescription
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
DOMAIN,
|
||||||
|
EVENT_MESSAGE_RECEIVED,
|
||||||
|
EVENT_TOOL_INVOKED,
|
||||||
|
)
|
||||||
|
|
||||||
|
EVENT_DESCRIPTIONS: tuple[EventEntityDescription, ...] = (
|
||||||
|
EventEntityDescription(
|
||||||
|
key="message_received",
|
||||||
|
translation_key="message_received",
|
||||||
|
name="OpenClaw Message Received",
|
||||||
|
icon="mdi:message-text",
|
||||||
|
event_types=["message_received"],
|
||||||
|
),
|
||||||
|
EventEntityDescription(
|
||||||
|
key="tool_invoked",
|
||||||
|
translation_key="tool_invoked",
|
||||||
|
name="OpenClaw Tool Invoked",
|
||||||
|
icon="mdi:tools",
|
||||||
|
event_types=["tool_invoked_ok", "tool_invoked_error"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up OpenClaw event entities from a config entry."""
|
||||||
|
entities = [
|
||||||
|
OpenClawEventEntity(entry, description)
|
||||||
|
for description in EVENT_DESCRIPTIONS
|
||||||
|
]
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
# Wire HA bus events → entity triggers
|
||||||
|
for entity in entities:
|
||||||
|
entity.async_start_listening(hass)
|
||||||
|
|
||||||
|
|
||||||
|
class OpenClawEventEntity(EventEntity):
|
||||||
|
"""Event entity that mirrors HA bus events into the entity registry."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
description: EventEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the event entity."""
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{entry.entry_id}_{description.key}"
|
||||||
|
self._attr_device_info = {
|
||||||
|
"identifiers": {(DOMAIN, entry.entry_id)},
|
||||||
|
"name": "OpenClaw Assistant",
|
||||||
|
"manufacturer": "OpenClaw",
|
||||||
|
"model": "OpenClaw Gateway",
|
||||||
|
}
|
||||||
|
self._entry_id = entry.entry_id
|
||||||
|
self._unsub: callback | None = None
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_start_listening(self, hass: HomeAssistant) -> None:
|
||||||
|
"""Subscribe to the matching HA bus event."""
|
||||||
|
key = self.entity_description.key
|
||||||
|
|
||||||
|
if key == "message_received":
|
||||||
|
bus_event = EVENT_MESSAGE_RECEIVED
|
||||||
|
elif key == "tool_invoked":
|
||||||
|
bus_event = EVENT_TOOL_INVOKED
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_event(event) -> None:
|
||||||
|
data: dict[str, Any] = dict(event.data or {})
|
||||||
|
if key == "message_received":
|
||||||
|
self._trigger_event("message_received", data)
|
||||||
|
elif key == "tool_invoked":
|
||||||
|
ok = data.get("ok", False)
|
||||||
|
event_type = "tool_invoked_ok" if ok else "tool_invoked_error"
|
||||||
|
self._trigger_event(event_type, data)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
self._unsub = hass.bus.async_listen(bus_event, _handle_event)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Unsubscribe when entity is removed."""
|
||||||
|
if self._unsub:
|
||||||
|
self._unsub()
|
||||||
|
self._unsub = None
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 62 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 62 KiB |
@@ -8,7 +8,7 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"issue_tracker": "https://github.com/techartdev/OpenClawHomeAssistant/issues",
|
"issue_tracker": "https://github.com/techartdev/OpenClawHomeAssistant/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"version": "0.1.55",
|
"version": "0.1.56",
|
||||||
"dependencies": ["conversation"],
|
"dependencies": ["conversation"],
|
||||||
"after_dependencies": ["hassio", "lovelace"]
|
"after_dependencies": ["hassio", "lovelace"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
"""Select entity for the OpenClaw integration.
|
||||||
|
|
||||||
|
Provides a Select entity that exposes the list of available models
|
||||||
|
from the gateway's /v1/models endpoint, allowing the user to switch
|
||||||
|
the active model from the HA dashboard.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DATA_MODEL, DOMAIN
|
||||||
|
from .coordinator import OpenClawCoordinator
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SELECT_DESCRIPTIONS: tuple[SelectEntityDescription, ...] = (
|
||||||
|
SelectEntityDescription(
|
||||||
|
key="active_model",
|
||||||
|
translation_key="active_model",
|
||||||
|
name="OpenClaw Active Model",
|
||||||
|
icon="mdi:brain",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up OpenClaw select entities from a config entry."""
|
||||||
|
entry_data: dict = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
coordinator: OpenClawCoordinator = entry_data["coordinator"]
|
||||||
|
|
||||||
|
entities = [
|
||||||
|
OpenClawModelSelect(coordinator, description, entry)
|
||||||
|
for description in SELECT_DESCRIPTIONS
|
||||||
|
]
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class OpenClawModelSelect(CoordinatorEntity[OpenClawCoordinator], SelectEntity):
|
||||||
|
"""Select entity for switching the active OpenClaw model."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: OpenClawCoordinator,
|
||||||
|
description: SelectEntityDescription,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the select entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = description
|
||||||
|
self._entry = entry
|
||||||
|
self._attr_unique_id = f"{entry.entry_id}_{description.key}"
|
||||||
|
self._attr_device_info = {
|
||||||
|
"identifiers": {(DOMAIN, entry.entry_id)},
|
||||||
|
"name": "OpenClaw Assistant",
|
||||||
|
"manufacturer": "OpenClaw",
|
||||||
|
"model": "OpenClaw Gateway",
|
||||||
|
}
|
||||||
|
# Initialise from coordinator cache
|
||||||
|
models = coordinator.available_models
|
||||||
|
self._attr_options = models if models else ["unknown"]
|
||||||
|
current = (coordinator.data or {}).get(DATA_MODEL)
|
||||||
|
self._attr_current_option = current if current in self._attr_options else (
|
||||||
|
self._attr_options[0] if self._attr_options else None
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
"""Update options and current selection when coordinator refreshes."""
|
||||||
|
models = self.coordinator.available_models
|
||||||
|
if models:
|
||||||
|
self._attr_options = models
|
||||||
|
current = (self.coordinator.data or {}).get(DATA_MODEL)
|
||||||
|
if current and current in self._attr_options:
|
||||||
|
self._attr_current_option = current
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
"""Handle the user selecting a new model.
|
||||||
|
|
||||||
|
Persists the selection in the config entry's options so that the
|
||||||
|
conversation agent and card can read it via ``get_settings``.
|
||||||
|
Then triggers a coordinator refresh.
|
||||||
|
"""
|
||||||
|
_LOGGER.info("OpenClaw active model changed to: %s", option)
|
||||||
|
self._attr_current_option = option
|
||||||
|
|
||||||
|
# Store in config entry options so other components can read it
|
||||||
|
new_options = dict(self._entry.options)
|
||||||
|
new_options["active_model"] = option
|
||||||
|
self.hass.config_entries.async_update_entry(
|
||||||
|
self._entry, options=new_options
|
||||||
|
)
|
||||||
|
|
||||||
|
self.async_write_ha_state()
|
||||||
@@ -82,6 +82,30 @@
|
|||||||
"connected": {
|
"connected": {
|
||||||
"name": "Connected"
|
"name": "Connected"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"event": {
|
||||||
|
"message_received": {
|
||||||
|
"name": "Message Received"
|
||||||
|
},
|
||||||
|
"tool_invoked": {
|
||||||
|
"name": "Tool Invoked"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"clear_history": {
|
||||||
|
"name": "Clear History"
|
||||||
|
},
|
||||||
|
"sync_history": {
|
||||||
|
"name": "Sync History"
|
||||||
|
},
|
||||||
|
"run_diagnostics": {
|
||||||
|
"name": "Run Diagnostics"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"select": {
|
||||||
|
"active_model": {
|
||||||
|
"name": "Active Model"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
|
|||||||
@@ -84,6 +84,30 @@
|
|||||||
"connected": {
|
"connected": {
|
||||||
"name": "Connected"
|
"name": "Connected"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"event": {
|
||||||
|
"message_received": {
|
||||||
|
"name": "Message Received"
|
||||||
|
},
|
||||||
|
"tool_invoked": {
|
||||||
|
"name": "Tool Invoked"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"clear_history": {
|
||||||
|
"name": "Clear History"
|
||||||
|
},
|
||||||
|
"sync_history": {
|
||||||
|
"name": "Sync History"
|
||||||
|
},
|
||||||
|
"run_diagnostics": {
|
||||||
|
"name": "Run Diagnostics"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"select": {
|
||||||
|
"active_model": {
|
||||||
|
"name": "Active Model"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
|
|||||||
Reference in New Issue
Block a user