feat: add HomeAssistant components (#5803)

* Add Homeassistant Component

https://www.home-assistant.io/

* Add files via upload

* Add files via upload

* refactor: rename Home Assistant control and state listing components

Introduced two new components for Home Assistant integration:
1. HomeAssistantControl: A tool for controlling Home Assistant devices, allowing actions like turn_on, turn_off, and toggle with specified entity IDs.
2. ListHomeAssistantStates: A tool to retrieve the current states of Home Assistant entities, with optional filtering by domain.

Both components include necessary input fields and documentation links for user guidance.

* refactor: format Home Assistant SVG component and index file for consistency

* fix: spread props in Home Assistant SVG component for better customization

* refactor: update Home Assistant icon and improve SVG structure for better readability

---------

Co-authored-by: Nadir J <31660040+NadirJ@users.noreply.github.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
This commit is contained in:
YAMON.IO 2025-04-02 04:47:53 +09:00 committed by GitHub
commit e2d0ec865a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 331 additions and 0 deletions

View file

@ -0,0 +1,7 @@
from .home_assistant_control import HomeAssistantControl
from .list_home_assistant_states import ListHomeAssistantStates
__all__ = [
"HomeAssistantControl",
"ListHomeAssistantStates",
]

View file

@ -0,0 +1,152 @@
import json
from typing import Any
import requests
from langchain.tools import StructuredTool
from pydantic import BaseModel, Field
from langflow.base.langchain_utilities.model import LCToolComponent
from langflow.field_typing import Tool
from langflow.inputs import SecretStrInput, StrInput
from langflow.schema import Data
class HomeAssistantControl(LCToolComponent):
"""This tool is used to control Home Assistant devices.
A very simple tool to control Home Assistant devices.
- The agent only needs to provide action (turn_on, turn_off, toggle) + entity_id (e.g., switch.xxx, light.xxx).
- The domain (e.g., 'switch', 'light') is automatically extracted from entity_id.
"""
display_name: str = "Home Assistant Control"
description: str = (
"A very simple tool to control Home Assistant devices. "
"Only action (turn_on, turn_off, toggle) and entity_id need to be provided."
)
documentation: str = "https://developers.home-assistant.io/docs/api/rest/"
icon: str = "HomeAssistant"
# --- Input fields for LangFlow UI (token, URL) ---
inputs = [
SecretStrInput(
name="ha_token",
display_name="Home Assistant Token",
info="Home Assistant Long-Lived Access Token",
required=True,
),
StrInput(
name="base_url",
display_name="Home Assistant URL",
info="e.g., http://192.168.0.10:8123",
required=True,
),
StrInput(
name="default_action",
display_name="Default Action (Optional)",
info="One of turn_on, turn_off, toggle",
required=False,
),
StrInput(
name="default_entity_id",
display_name="Default Entity ID (Optional)",
info="Default entity ID to control (e.g., switch.unknown_switch_3)",
required=False,
),
]
# --- Parameters exposed to the agent (Pydantic schema) ---
class ToolSchema(BaseModel):
"""Parameters to be passed by the agent: action, entity_id only."""
action: str = Field(..., description="Home Assistant service name. (One of turn_on, turn_off, toggle)")
entity_id: str = Field(
...,
description="Entity ID to control (e.g., switch.xxx, light.xxx, cover.xxx, etc.)."
"Do not infer; use the list_homeassistant_states tool to retrieve it.",
)
def run_model(self) -> Data:
"""Used when the 'Run' button is clicked in LangFlow.
- Uses default_action and default_entity_id entered in the UI.
"""
action = self.default_action or "turn_off"
entity_id = self.default_entity_id or "switch.unknown_switch_3"
result = self._control_device(
ha_token=self.ha_token,
base_url=self.base_url,
action=action,
entity_id=entity_id,
)
return self._make_data_response(result)
def build_tool(self) -> Tool:
"""Returns a tool to be used by the agent (LLM).
- The agent can only pass action and entity_id as arguments.
"""
return StructuredTool.from_function(
name="home_assistant_control",
description=(
"A tool to control Home Assistant devices easily. "
"Parameters: action ('turn_on'/'turn_off'/'toggle'), entity_id ('switch.xxx', etc.)."
"Entity ID must be obtained using the list_homeassistant_states tool and not guessed."
),
func=self._control_device_for_tool, # Wrapper function below
args_schema=self.ToolSchema,
)
def _control_device_for_tool(self, action: str, entity_id: str) -> dict[str, Any] | str:
"""Function called by the agent.
-> Internally calls _control_device.
"""
return self._control_device(
ha_token=self.ha_token,
base_url=self.base_url,
action=action,
entity_id=entity_id,
)
def _control_device(
self,
ha_token: str,
base_url: str,
action: str,
entity_id: str,
) -> dict[str, Any] | str:
"""Actual logic to call the Home Assistant service.
The domain is extracted from the beginning of the entity_id.
Example: entity_id="switch.unknown_switch_3" -> domain="switch".
"""
try:
domain = entity_id.split(".")[0] # switch, light, cover, etc.
url = f"{base_url}/api/services/{domain}/{action}"
headers = {
"Authorization": f"Bearer {ha_token}",
"Content-Type": "application/json",
}
payload = {"entity_id": entity_id}
response = requests.post(url, headers=headers, json=payload, timeout=10)
response.raise_for_status()
return response.json() # HA response JSON on success
except requests.exceptions.RequestException as e:
return f"Error: Failed to call service. {e}"
except Exception as e: # noqa: BLE001
return f"An unexpected error occurred: {e}"
def _make_data_response(self, result: dict[str, Any] | str) -> Data:
"""Returns a response in the LangFlow Data format."""
if isinstance(result, str):
# Handle error messages
return Data(text=result)
# Convert dict to JSON string
formatted_json = json.dumps(result, indent=2, ensure_ascii=False)
return Data(data=result, text=formatted_json)

View file

@ -0,0 +1,137 @@
import json
from typing import Any
import requests
from langchain.tools import StructuredTool
from pydantic import BaseModel, Field
from langflow.base.langchain_utilities.model import LCToolComponent
from langflow.field_typing import Tool
from langflow.inputs import SecretStrInput, StrInput
from langflow.schema import Data
class ListHomeAssistantStates(LCToolComponent):
display_name: str = "List HomeAssistant States"
description: str = (
"Retrieve states from Home Assistant. "
"The agent only needs to specify 'filter_domain' (optional). "
"Token and base_url are not exposed to the agent."
)
documentation: str = "https://developers.home-assistant.io/docs/api/rest/"
icon = "HomeAssistant"
# 1) Define fields to be received in LangFlow UI
inputs = [
SecretStrInput(
name="ha_token",
display_name="Home Assistant Token",
info="Home Assistant Long-Lived Access Token",
required=True,
),
StrInput(
name="base_url",
display_name="Home Assistant URL",
info="e.g., http://192.168.0.10:8123",
required=True,
),
StrInput(
name="filter_domain",
display_name="Default Filter Domain (Optional)",
info="light, switch, sensor, etc. (Leave empty to fetch all)",
required=False,
),
]
# 2) Pydantic schema containing only parameters exposed to the agent
class ToolSchema(BaseModel):
"""Parameters to be passed by the agent: filter_domain only."""
filter_domain: str = Field("", description="Filter domain (e.g., 'light'). If empty, returns all.")
def run_model(self) -> Data:
"""Execute the LangFlow component.
Uses self.ha_token, self.base_url, self.filter_domain as entered in the UI.
Triggered when 'Run' is clicked directly without an agent.
"""
filter_domain = self.filter_domain or "" # Use "" for fetching all states
result = self._list_states(
ha_token=self.ha_token,
base_url=self.base_url,
filter_domain=filter_domain,
)
return self._make_data_response(result)
def build_tool(self) -> Tool:
"""Build a tool object to be used by the agent.
The agent can only pass 'filter_domain' as a parameter.
'ha_token' and 'base_url' are not exposed (stored as self attributes).
"""
return StructuredTool.from_function(
name="list_homeassistant_states",
description=(
"Retrieve states from Home Assistant. "
"You can provide filter_domain='light', 'switch', etc. to narrow results."
),
func=self._list_states_for_tool, # Wrapper function below
args_schema=self.ToolSchema, # Requires only filter_domain
)
def _list_states_for_tool(self, filter_domain: str = "") -> list[Any] | str:
"""Execute the tool when called by the agent.
'ha_token' and 'base_url' are stored in self (not exposed).
"""
return self._list_states(
ha_token=self.ha_token,
base_url=self.base_url,
filter_domain=filter_domain,
)
def _list_states(
self,
ha_token: str,
base_url: str,
filter_domain: str = "",
) -> list[Any] | str:
"""Call the Home Assistant /api/states endpoint."""
try:
headers = {
"Authorization": f"Bearer {ha_token}",
"Content-Type": "application/json",
}
url = f"{base_url}/api/states"
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
all_states = response.json()
if filter_domain:
return [st for st in all_states if st.get("entity_id", "").startswith(f"{filter_domain}.")]
except requests.exceptions.RequestException as e:
return f"Error: Failed to fetch states. {e}"
except (ValueError, TypeError) as e:
return f"Error processing response: {e}"
return all_states
def _make_data_response(self, result: list[Any] | str | dict) -> Data:
"""Format the response into a Data object."""
try:
if isinstance(result, list):
# Wrap list data into a dictionary and convert to text
wrapped_result = {"result": result}
return Data(data=wrapped_result, text=json.dumps(wrapped_result, indent=2, ensure_ascii=False))
if isinstance(result, dict):
# Return dictionary as-is
return Data(data=result, text=json.dumps(result, indent=2, ensure_ascii=False))
if isinstance(result, str):
# Return error messages or strings
return Data(data={}, text=result)
# Handle unexpected data types
return Data(data={}, text="Error: Unexpected response format.")
except (TypeError, ValueError) as e:
# Handle specific exceptions during formatting
return Data(data={}, text=f"Error: Failed to process response. Details: {e!s}")

View file

@ -0,0 +1,20 @@
const SvgHomeAssistant = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="400"
height="400"
viewBox="0 0 400 400"
fill="none"
{...props}
>
<path
d="M320 301.762C320 310.012 313.25 316.762 305 316.762H95C86.75 316.762 80 310.012 80 301.762V211.762C80 203.512 84.77 191.993 90.61 186.153L189.39 87.3725C195.22 81.5425 204.77 81.5425 210.6 87.3725L309.39 186.162C315.22 191.992 320 203.522 320 211.772V301.772V301.762Z"
fill="#F2F4F9"
/>
<path
d="M309.39 186.153L210.61 87.3725C204.78 81.5425 195.23 81.5425 189.4 87.3725L90.61 186.153C84.78 191.983 80 203.512 80 211.762V301.762C80 310.012 86.75 316.762 95 316.762H187.27L146.64 276.132C144.55 276.852 142.32 277.262 140 277.262C128.7 277.262 119.5 268.062 119.5 256.762C119.5 245.462 128.7 236.262 140 236.262C151.3 236.262 160.5 245.462 160.5 256.762C160.5 259.092 160.09 261.322 159.37 263.412L191 295.042V179.162C184.2 175.822 179.5 168.842 179.5 160.772C179.5 149.472 188.7 140.272 200 140.272C211.3 140.272 220.5 149.472 220.5 160.772C220.5 168.842 215.8 175.822 209 179.162V260.432L240.46 228.972C239.84 227.012 239.5 224.932 239.5 222.772C239.5 211.472 248.7 202.272 260 202.272C271.3 202.272 280.5 211.472 280.5 222.772C280.5 234.072 271.3 243.272 260 243.272C257.5 243.272 255.12 242.802 252.91 241.982L209 285.892V316.772H305C313.25 316.772 320 310.022 320 301.772V211.772C320 203.522 315.23 192.002 309.39 186.162V186.153Z"
fill="#18BCF2"
/>
</svg>
);
export default SvgHomeAssistant;

View file

@ -0,0 +1,4 @@
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M320 301.762C320 310.012 313.25 316.762 305 316.762H95C86.75 316.762 80 310.012 80 301.762V211.762C80 203.512 84.77 191.993 90.61 186.153L189.39 87.3725C195.22 81.5425 204.77 81.5425 210.6 87.3725L309.39 186.162C315.22 191.992 320 203.522 320 211.772V301.772V301.762Z" fill="#F2F4F9"/>
<path d="M309.39 186.153L210.61 87.3725C204.78 81.5425 195.23 81.5425 189.4 87.3725L90.61 186.153C84.78 191.983 80 203.512 80 211.762V301.762C80 310.012 86.75 316.762 95 316.762H187.27L146.64 276.132C144.55 276.852 142.32 277.262 140 277.262C128.7 277.262 119.5 268.062 119.5 256.762C119.5 245.462 128.7 236.262 140 236.262C151.3 236.262 160.5 245.462 160.5 256.762C160.5 259.092 160.09 261.322 159.37 263.412L191 295.042V179.162C184.2 175.822 179.5 168.842 179.5 160.772C179.5 149.472 188.7 140.272 200 140.272C211.3 140.272 220.5 149.472 220.5 160.772C220.5 168.842 215.8 175.822 209 179.162V260.432L240.46 228.972C239.84 227.012 239.5 224.932 239.5 222.772C239.5 211.472 248.7 202.272 260 202.272C271.3 202.272 280.5 211.472 280.5 222.772C280.5 234.072 271.3 243.272 260 243.272C257.5 243.272 255.12 242.802 252.91 241.982L209 285.892V316.772H305C313.25 316.772 320 310.022 320 301.772V211.772C320 203.522 315.23 192.002 309.39 186.162V186.153Z" fill="#18BCF2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,9 @@
import React, { forwardRef } from "react";
import SvgHomeAssistant from "./HomeAssistant";
export const HomeAssistantIcon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <SvgHomeAssistant ref={ref} {...props} />;
});

View file

@ -7,6 +7,7 @@ import { ExaIcon } from "@/icons/Exa";
import { GleanIcon } from "@/icons/Glean";
import { GoogleDriveIcon } from "@/icons/GoogleDrive";
import { GridHorizontalIcon } from "@/icons/GridHorizontal";
import { HomeAssistantIcon } from "@/icons/HomeAssistant";
import { JSIcon } from "@/icons/JSicon";
import { LangwatchIcon } from "@/icons/Langwatch";
import { MilvusIcon } from "@/icons/Milvus";
@ -799,6 +800,7 @@ export const nodeIconsLucide: iconsType = {
Arize: ArizeIcon,
Apify: ApifyIcon,
ApifyWhite: ApifyWhiteIcon,
HomeAssistant: HomeAssistantIcon,
//Node Icons
model_specs: FileSliders,