Skip to content

Commit f527a8e

Browse files
refactor: consolidate 5 redundant tools (merge after #806) (#813)
* refactor: consolidate 5 redundant tools (96 → 91) Merge tools that share internal helpers or are near-identical code: 1. Area + Floor tools (6 → 3): Add `type` parameter to ha_config_list_areas, ha_config_set_area, ha_config_remove_area. Remove ha_config_list_floors, ha_config_set_floor, ha_config_remove_floor. 2. Rename entity tools (2 → 1): Add `new_device_name` parameter to ha_rename_entity. Remove ha_rename_entity_and_device. 3. Operation status tools (2 → 1): Accept `str | list[str]` in ha_get_operation_status. Remove ha_get_bulk_status. Closes #774 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve mypy type error in ha_config_remove_area exception handler Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Gemini review feedback - Fix operation variable in ha_config_set_area to correctly reflect create vs update based on whether an ID is provided - Move json import to module level and narrow except clause to json.JSONDecodeError | TypeError Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review findings - Add try/except ToolError guard to ha_get_operation_status (was missing entirely, raw exceptions would escape without structured errors) - Document that timeout_seconds only applies to single-operation checks - Fix error dict passed as raw string in ha_config_list_areas error branch - Add warning log when entity registry lookup fails during device rename - Update homeassistant-addon/DOCS.md to remove stale floor tool references - Update README badge from 95+ to 91+ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve mypy dict type error in ha_get_operation_status Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add E2E tests for consolidated tool edge cases - Area/floor cross-type parameter handling (floor-only params on areas and vice versa, missing required IDs, create without name) - Rename response format verification (simple vs combined format, empty string device name) - Operation status list-mode dispatch (empty list, invalid IDs, single vs list code path differentiation) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: address review feedback on tool consolidation PR - Rename area/floor tools to _or_floor suffix for LLM discoverability (ha_config_list_areas_or_floors, ha_config_set_area_or_floor, ha_config_remove_area_or_floor) - Clarify floor_id parameter description - Add parse_json_param coercion for operation_id (JSON string lists) - Add warning field for failed entity registry lookup vs no device - Normalize empty string new_device_name to None (entity-only rename) - Extract extract_tool_error_message() helper replacing brittle json.loads(str(te)) pattern - Add assertions to operation status tests + JSON coercion test - Update empty device name test to match new normalization behavior - Merge upstream master (post-#806), resolve conflicts - Correct tool counts across all docs to 87 (actual all-inclusive count) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: kingpanther13 <kingpanther13@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5a01299 commit f527a8e

18 files changed

Lines changed: 889 additions & 560 deletions

File tree

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ ENABLE_WEBSOCKET=true
2323
DEBUG=false
2424
LOG_LEVEL=INFO
2525

26-
# Tool Search: replace the full tool catalog (~80 tools) with search-based
26+
# Tool Search: replace the full tool catalog (~92 tools) with search-based
2727
# discovery (~4 proxy tools). Reduces idle context from ~46K to ~5K tokens.
2828
# Recommended for models without deferred tool support or with limited context.
2929
# ENABLE_TOOL_SEARCH=false

.gemini/styleguide.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Project Context
44

5-
**ha-mcp** is a Model Context Protocol (MCP) server that enables AI assistants to control Home Assistant smart homes. It provides 80+ tools for entity control, automations, device management, and configuration via Home Assistant's REST and WebSocket APIs.
5+
**ha-mcp** is a Model Context Protocol (MCP) server that enables AI assistants to control Home Assistant smart homes. It provides 92+ tools for entity control, automations, device management, and configuration via Home Assistant's REST and WebSocket APIs.
66

77
**Key Technologies:**
88
- Python 3.13, FastMCP framework
@@ -11,7 +11,7 @@
1111
- Architecture: Tool registry with lazy loading, service layer pattern, WebSocket state verification
1212

1313
**Code Organization:**
14-
- `src/ha_mcp/tools/` - 80+ MCP tools (tool modules auto-discovered)
14+
- `src/ha_mcp/tools/` - 92+ MCP tools (tool modules auto-discovered)
1515
- `src/ha_mcp/client/` - REST and WebSocket clients
1616
- `tests/src/e2e/` - End-to-end tests with real Home Assistant instance
1717
- `tests/src/unit/` - Unit tests for utilities

.github/workflows/gemini-triage.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ jobs:
253253
Base your evaluation ONLY on the issue text provided below.
254254
255255
**Project**: Home Assistant MCP Server (ha-mcp)
256-
- MCP server with 95+ tools for AI assistants to control Home Assistant
256+
- MCP server with 92+ tools for AI assistants to control Home Assistant
257257
- Supports: Claude Desktop, Claude Code, Cursor, Gemini CLI, Continue, etc.
258258
- Installation: uvx, pip, docker, Home Assistant add-on
259259
- Users can run \`ha_report_issue\` tool to auto-collect diagnostic info
@@ -359,7 +359,7 @@ jobs:
359359
360360
**Project Context:**
361361
- ha-mcp is an MCP (Model Context Protocol) server that connects AI assistants to Home Assistant
362-
- It provides 95+ tools for controlling smart home devices, automations, and scenes
362+
- It provides 92+ tools for controlling smart home devices, automations, and scenes
363363
- Installation methods: uvx, pip, Docker, or Home Assistant add-on
364364
- Codebase structure: src/ha_mcp/ (tools, client, server code)
365365
- Common issues: connection timeouts, authentication errors, tool failures

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ Custom agent workflows are located in `.claude/agents/`:
6363

6464
## Project Overview
6565

66-
**Home Assistant MCP Server** - A production MCP server enabling AI assistants to control Home Assistant smart homes. Provides 80+ tools for entity control, automations, device management, and more.
66+
**Home Assistant MCP Server** - A production MCP server enabling AI assistants to control Home Assistant smart homes. Provides 92+ tools for entity control, automations, device management, and more.
6767

6868
- **Repo**: `homeassistant-ai/ha-mcp`
6969
- **Package**: `ha-mcp` on PyPI
@@ -421,7 +421,7 @@ Balance improvement against regression risk. Consider:
421421
### Setup
422422
```bash
423423
uv sync --group dev # Install with dev dependencies
424-
uv run ha-mcp # Run MCP server (80+ tools)
424+
uv run ha-mcp # Run MCP server (92+ tools)
425425
cp .env.example .env # Configure HA connection
426426
```
427427

@@ -484,7 +484,7 @@ src/ha_mcp/
484484
│ ├── rest_client.py # HTTP REST API client
485485
│ ├── websocket_client.py # Real-time state monitoring
486486
│ └── websocket_listener.py
487-
├── tools/ # 28 modules, 80+ tools
487+
├── tools/ # 28 modules, 92+ tools
488488
│ ├── registry.py # Lazy auto-discovery
489489
│ ├── smart_search.py # Fuzzy entity search
490490
│ ├── device_control.py # WebSocket-verified control

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<!-- mcp-name: io.github.homeassistant-ai/ha-mcp -->
99

1010
<p align="center">
11-
<img src="https://img.shields.io/badge/tools-94-blue" alt="95+ Tools">
11+
<img src="https://img.shields.io/badge/tools-92-blue" alt="95+ Tools">
1212
<a href="https://github.com/homeassistant-ai/ha-mcp/releases"><img src="https://img.shields.io/github/v/release/homeassistant-ai/ha-mcp" alt="Release"></a>
1313
<a href="https://github.com/homeassistant-ai/ha-mcp/actions/workflows/e2e-tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/homeassistant-ai/ha-mcp/e2e-tests.yml?branch=master&label=E2E%20Tests" alt="E2E Tests"></a>
1414
<a href="LICENSE.md"><img src="https://img.shields.io/github/license/homeassistant-ai/ha-mcp.svg" alt="License"></a>
@@ -131,7 +131,7 @@ Spend less time configuring, more time enjoying your smart home.
131131
<details>
132132
<!-- TOOLS_TABLE_START -->
133133

134-
<summary><b>Complete Tool List (94 tools)</b></summary>
134+
<summary><b>Complete Tool List (92 tools)</b></summary>
135135

136136
| Category | Tools |
137137
|----------|-------|
@@ -142,7 +142,7 @@ Spend less time configuring, more time enjoying your smart home.
142142
| **Calendar** | `ha_config_get_calendar_events`, `ha_config_remove_calendar_event`, `ha_config_set_calendar_event` |
143143
| **Camera** | `ha_get_camera_image` |
144144
| **Dashboards** | `ha_config_delete_dashboard_resource`, `ha_config_delete_dashboard`, `ha_config_get_dashboard`, `ha_config_list_dashboard_resources`, `ha_config_set_dashboard_resource`, `ha_config_set_dashboard`, `ha_dashboard_find_card` |
145-
| **Device Registry** | `ha_get_device`, `ha_remove_device`, `ha_rename_entity_and_device`, `ha_rename_entity`, `ha_update_device` |
145+
| **Device Registry** | `ha_get_device`, `ha_remove_device`, `ha_rename_entity`, `ha_update_device` |
146146
| **Entity Registry** | `ha_get_entity_exposure`, `ha_get_entity`, `ha_set_entity` |
147147
| **Files** | `ha_delete_file`, `ha_list_files`, `ha_read_file`, `ha_write_file` |
148148
| **Groups** | `ha_config_list_groups`, `ha_config_remove_group`, `ha_config_set_group` |
@@ -153,7 +153,7 @@ Spend less time configuring, more time enjoying your smart home.
153153
| **Labels & Categories** | `ha_config_get_category`, `ha_config_get_label`, `ha_config_remove_category`, `ha_config_remove_label`, `ha_config_set_category`, `ha_config_set_label` |
154154
| **Scripts** | `ha_config_get_script`, `ha_config_remove_script`, `ha_config_set_script` |
155155
| **Search & Discovery** | `ha_deep_search`, `ha_get_overview`, `ha_get_state`, `ha_get_states`, `ha_search_entities` |
156-
| **Service & Device Control** | `ha_bulk_control`, `ha_call_service`, `ha_get_bulk_status`, `ha_get_operation_status`, `ha_list_services` |
156+
| **Service & Device Control** | `ha_bulk_control`, `ha_call_service`, `ha_get_operation_status`, `ha_list_services` |
157157
| **System** | `ha_backup_create`, `ha_backup_restore`, `ha_check_config`, `ha_config_set_yaml`, `ha_get_system_health`, `ha_get_updates`, `ha_reload_core`, `ha_restart` |
158158
| **Todo Lists** | `ha_add_todo_item`, `ha_get_todo`, `ha_remove_todo_item`, `ha_update_todo_item` |
159159
| **Utilities** | `ha_eval_template`, `ha_install_mcp_tools`, `ha_report_issue` |

docs/FAQ.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ You can also use ha-mcp with other AI clients. See the [Setup Wizard](https://ho
1818

1919
| Feature | Built-in HA MCP | ha-mcp |
2020
|---------|-----------------|--------|
21-
| Tools | ~15 basic tools | 80+ comprehensive tools |
21+
| Tools | ~15 basic tools | 92+ comprehensive tools |
2222
| Focus | Device control | Full system administration |
2323
| Automations | Limited | Create, edit, debug, trace |
2424
| Dashboards | No | Full dashboard management |

homeassistant-addon/DOCS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ AI assistant integration for Home Assistant via Model Context Protocol (MCP).
44

55
## About
66

7-
This add-on enables AI assistants (Claude, ChatGPT, etc.) to control your Home Assistant installation through the Model Context Protocol (MCP). It provides 80+ tools for device control, automation management, entity search, calendars, todo lists, dashboards, backup/restore, history/statistics, camera snapshots, and system queries.
7+
This add-on enables AI assistants (Claude, ChatGPT, etc.) to control your Home Assistant installation through the Model Context Protocol (MCP). It provides 92+ tools for device control, automation management, entity search, calendars, todo lists, dashboards, backup/restore, history/statistics, camera snapshots, and system queries.
88

99
**Key Features:**
1010
- **Zero Configuration** - Automatically discovers Home Assistant connection
@@ -212,7 +212,7 @@ Custom secret path override. **Leave empty for auto-generation** (recommended).
212212

213213
**Default:** `false`
214214

215-
Replaces the full tool catalog (~80 tools, ~46K tokens) with search-based discovery (~4 proxy tools, ~5K tokens). When enabled, tools are found via `ha_search_tools` and executed through categorized proxies (read/write/delete).
215+
Replaces the full tool catalog (~92 tools, ~46K tokens) with search-based discovery (~4 proxy tools, ~5K tokens). When enabled, tools are found via `ha_search_tools` and executed through categorized proxies (read/write/delete).
216216

217217
**When to enable:**
218218
- Models **without native deferred tool support** — this includes OpenAI-compatible local models, and also **Claude Haiku** which does not use Claude's built-in deferred tool loading. Haiku users will see significant token savings with this enabled.

site/src/data/tools.json

Lines changed: 5 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,7 @@
792792
{
793793
"name": "ha_rename_entity",
794794
"title": "Rename Entity",
795-
"description": "Rename a Home Assistant entity by changing its entity_id.\n\nChanges the entity_id (e.g., light.old_name -> light.new_name).\nThe domain must remain the same - you cannot change a light to a switch.\n\nVOICE ASSISTANT EXPOSURE:\nBy default, this function preserves voice assistant exposure settings\n(Alexa, Google Assistant, Assist) when renaming. The exposure settings\nare stored separately from the entity registry and must be migrated\nmanually. Set preserve_voice_exposure=False to skip this migration.\n\nIMPORTANT LIMITATIONS:\n- References in automations/scripts/dashboards are NOT automatically updated\n- Entity history is preserved (HA 2022.4+)\n- Some entities cannot be renamed:\n - Entities without unique IDs\n - Entities disabled by integration\n\nEXAMPLES:\n- Rename light: ha_rename_entity(\"light.bedroom_1\", \"light.master_bedroom\")\n- Rename with friendly name: ha_rename_entity(\"sensor.temp\", \"sensor.living_room_temp\", name=\"Living Room Temperature\")\n- Rename without exposure migration: ha_rename_entity(\"light.old\", \"light.new\", preserve_voice_exposure=False)\n\nNOTE: This is different from renaming a device. Device and entity renaming are independent.\nRenaming a device does NOT rename its entities. See ha_update_device() for device renaming.\nFor renaming both entity and device together, use ha_rename_entity_and_device().",
795+
"description": "Rename a Home Assistant entity by changing its entity_id, optionally renaming its device too.\n\nChanges the entity_id (e.g., light.old_name -> light.new_name).\nThe domain must remain the same - you cannot change a light to a switch.\n\nDEVICE RENAME:\nProvide new_device_name to also rename the device associated with this entity.\nThe device is looked up automatically from the entity registry.\n\nVOICE ASSISTANT EXPOSURE:\nBy default, this function preserves voice assistant exposure settings\n(Alexa, Google Assistant, Assist) when renaming. The exposure settings\nare stored separately from the entity registry and must be migrated\nmanually. Set preserve_voice_exposure=False to skip this migration.\n\nIMPORTANT LIMITATIONS:\n- References in automations/scripts/dashboards are NOT automatically updated\n- Entity history is preserved (HA 2022.4+)\n- Some entities cannot be renamed:\n - Entities without unique IDs\n - Entities disabled by integration\n\nEXAMPLES:\n- Rename light: ha_rename_entity(\"light.bedroom_1\", \"light.master_bedroom\")\n- Rename with friendly name: ha_rename_entity(\"sensor.temp\", \"sensor.living_room_temp\", name=\"Living Room Temperature\")\n- Rename entity and device: ha_rename_entity(\"light.bedroom_1\", \"light.master_bedroom\", new_device_name=\"Master Bedroom Lamp\")\n- Rename without exposure migration: ha_rename_entity(\"light.old\", \"light.new\", preserve_voice_exposure=False)\n\nNOTE: Device and entity renaming are independent in HA. Renaming a device does\nNOT rename its entities. See ha_update_device() for device-only renaming.",
796796
"inputSchema": {
797797
"properties": {
798798
"entity_id": {
@@ -809,46 +809,12 @@
809809
"type": "Annotated[str | None, Field(description=\"Optional: New icon (e.g., 'mdi:lightbulb')\", default=None)]",
810810
"default": null
811811
},
812-
"preserve_voice_exposure": {
813-
"type": "Annotated[bool | str | None, Field(description='Migrate voice assistant exposure settings to the new entity_id. Defaults to True. Set to False to skip exposure migration.', default=None)]",
814-
"default": null
815-
}
816-
},
817-
"required": [
818-
"entity_id",
819-
"new_entity_id"
820-
]
821-
},
822-
"annotations": {
823-
"destructiveHint": true
824-
},
825-
"tags": [
826-
"Device Registry"
827-
],
828-
"source_file": "tools_registry.py"
829-
},
830-
{
831-
"name": "ha_rename_entity_and_device",
832-
"title": "Rename Entity and Device",
833-
"description": "Convenience tool to rename both an entity and its associated device in one operation.\n\nThis tool:\n1. Finds the device associated with the entity\n2. Renames the entity (changing entity_id)\n3. Renames the device (changing display name)\n4. Preserves voice assistant exposure settings (by default)\n\nWHEN TO USE:\n- When you want to rename a device and its primary entity together\n- For smart home devices where device and entity should have matching names\n\nIMPORTANT:\n- If the entity has multiple associated entities (e.g., a smart bulb with brightness, color, etc.),\n only the specified entity is renamed. Other entities retain their original IDs.\n- If the entity has no associated device, only the entity is renamed.\n\nEXAMPLES:\n- Rename lamp: ha_rename_entity_and_device(\"light.bedroom_1\", \"light.master_bedroom\", \"Master Bedroom Lamp\")\n- Rename only entity_id: ha_rename_entity_and_device(\"sensor.temp\", \"sensor.living_room_temp\")\n\nSee also:\n- ha_rename_entity() - Rename only the entity\n- ha_update_device() - Update only the device",
834-
"inputSchema": {
835-
"properties": {
836-
"entity_id": {
837-
"type": "Annotated[str, Field(description=\"Entity ID to rename (e.g., 'light.bedroom_lamp'). Used to find associated device.\")]"
838-
},
839-
"new_entity_id": {
840-
"type": "Annotated[str, Field(description=\"New entity ID (e.g., 'light.master_bedroom_lamp'). Domain must match.\")]"
841-
},
842812
"new_device_name": {
843-
"type": "Annotated[str | None, Field(description='New display name for the device. If not provided, device name is not changed.', default=None)]",
844-
"default": null
845-
},
846-
"new_entity_name": {
847-
"type": "Annotated[str | None, Field(description='New friendly name for the entity. If not provided, entity name is not changed.', default=None)]",
813+
"type": "Annotated[str | None, Field(description='Optional: New display name for the associated device. If provided, both entity and device are renamed in one operation.', default=None)]",
848814
"default": null
849815
},
850816
"preserve_voice_exposure": {
851-
"type": "Annotated[bool | str | None, Field(description='Migrate voice assistant exposure settings to the new entity_id. Defaults to True.', default=None)]",
817+
"type": "Annotated[bool | str | None, Field(description='Migrate voice assistant exposure settings to the new entity_id. Defaults to True. Set to False to skip exposure migration.', default=None)]",
852818
"default": null
853819
}
854820
},
@@ -2368,36 +2334,14 @@
23682334
],
23692335
"source_file": "tools_service.py"
23702336
},
2371-
{
2372-
"name": "ha_get_bulk_status",
2373-
"title": "Get Bulk Operation Status",
2374-
"description": "Check status of multiple device control operations.\n\nUse this tool to check the status of operations initiated by ha_bulk_control\nor control_device_smart. Each of these tools returns unique operation_ids\nthat can be tracked here.\n\n**IMPORTANT:** This tool is for tracking async device operations, NOT for\nchecking current entity states. To get current states of entities, use\nha_get_state instead.\n\n**Args:**\n operation_ids: List of operation IDs returned by ha_bulk_control or\n control_device_smart (e.g., [\"op_1234\", \"op_5678\"])\n\n**Returns:**\n Status summary with completion/pending/failed counts and detailed\n results for each operation.\n\n**Example:**\n # After calling control_device_smart\n result = control_device_smart(\"light.kitchen\", \"on\")\n op_id = result[\"operation_id\"] # e.g., \"op_1234\"\n\n # Check operation status\n status = ha_get_bulk_status([op_id])",
2375-
"inputSchema": {
2376-
"properties": {
2377-
"operation_ids": {
2378-
"type": "list[str]"
2379-
}
2380-
},
2381-
"required": [
2382-
"operation_ids"
2383-
]
2384-
},
2385-
"annotations": {
2386-
"readOnlyHint": true
2387-
},
2388-
"tags": [
2389-
"Service & Device Control"
2390-
],
2391-
"source_file": "tools_service.py"
2392-
},
23932337
{
23942338
"name": "ha_get_operation_status",
23952339
"title": "Get Operation Status",
2396-
"description": "Check status of device operation with real-time WebSocket verification.",
2340+
"description": "Check status of one or more device operations with real-time WebSocket verification.\n\nPass a single operation_id string to check one operation, or a list of IDs\nto check multiple operations at once (bulk status).\n\nThe timeout_seconds parameter applies to single-operation checks only.\nBulk checks poll each operation individually with a short internal timeout.\n\nUse this to track operations initiated by ha_bulk_control or ha_call_service.\nFor current entity states, use ha_get_state instead.",
23972341
"inputSchema": {
23982342
"properties": {
23992343
"operation_id": {
2400-
"type": "str"
2344+
"type": "Annotated[str | list[str], Field(description='Single operation ID or list of operation IDs to check. Use a single string for one operation, or a list for bulk status checks.')]"
24012345
},
24022346
"timeout_seconds": {
24032347
"type": "int",

site/src/pages/index.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const withBase = (path: string) => {
2525

2626
<!-- Badges -->
2727
<div class="flex flex-wrap justify-center gap-2 mb-8">
28-
<img src="https://img.shields.io/badge/tools-80+-blue" alt="80+ Tools" class="h-6" />
28+
<img src="https://img.shields.io/badge/tools-92+-blue" alt="92+ Tools" class="h-6" />
2929
<img src="https://img.shields.io/github/v/release/homeassistant-ai/ha-mcp" alt="Release" class="h-6" />
3030
<img src="https://img.shields.io/github/actions/workflow/status/homeassistant-ai/ha-mcp/e2e-tests.yml?branch=master&label=E2E%20Tests" alt="E2E Tests" class="h-6" />
3131
<img src="https://img.shields.io/github/license/homeassistant-ai/ha-mcp.svg" alt="License" class="h-6" />
@@ -167,7 +167,7 @@ const withBase = (path: string) => {
167167
</div>
168168
<div class="bg-slate-800/30 rounded-xl p-5 border border-slate-700/50">
169169
<h3 class="font-semibold text-white mb-2 flex items-center gap-2">
170-
<span>🛠️</span> 82+ Tools
170+
<span>🛠️</span> 92+ Tools
171171
</h3>
172172
<p class="text-slate-400 text-sm">Comprehensive toolset for complete HA control</p>
173173
</div>

0 commit comments

Comments
 (0)