Skip to content

Commit

Permalink
make entity states real-time (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
tonka3000 authored Oct 18, 2021
1 parent 59ee078 commit bf84409
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 181 deletions.
15 changes: 13 additions & 2 deletions extensions/homeassistant/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 18 additions & 17 deletions extensions/homeassistant/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "homeassistant",
"title": "Home Assistant",
"version": "1.1.0",
"version": "1.2.0",
"author": "tonka3000",
"license": "MIT",
"description": "Home Assistant remote control. Control your house with Raycast 🚀",
Expand All @@ -10,77 +10,77 @@
{
"name": "index",
"title": "All Entities",
"subtitle" : "Home Assistant",
"subtitle": "Home Assistant",
"description": "Get/Set states of Home Assistant entities",
"mode": "view"
},
{
"name": "covers",
"title": "Covers",
"subtitle" : "Home Assistant",
"subtitle": "Home Assistant",
"description": "Get/Set states of Home Assistant covers",
"mode": "view"
},
{
"name": "lights",
"title": "Lights",
"subtitle" : "Home Assistant",
"subtitle": "Home Assistant",
"description": "Get/Set states of Home Assistant lights",
"mode": "view"
},
{
"name": "persons",
"title": "Persons",
"subtitle" : "Home Assistant",
"subtitle": "Home Assistant",
"description": "Get/Set states of Home Assistant persons",
"mode": "view"
},
{
"name": "sensors",
"title": "Sensors",
"subtitle" : "Home Assistant",
"subtitle": "Home Assistant",
"description": "Get/Set states of Home Assistant sensors",
"mode": "view"
},
{
"name": "binarysensors",
"title": "Binary Sensors",
"subtitle" : "Home Assistant",
"subtitle": "Home Assistant",
"description": "Get/Set states of Home Assistant binary sensors",
"mode": "view"
},
{
"name": "mediaplayers",
"title": "Mediaplayers",
"subtitle" : "Home Assistant",
"subtitle": "Home Assistant",
"description": "Get/Set states of Home Assistant mediaplayers",
"mode": "view"
},
{
"name": "automations",
"title": "Automations",
"subtitle" : "Home Assistant",
"subtitle": "Home Assistant",
"description": "Home Assistant automations",
"mode": "view"
},
{
"name": "dashboard",
"title": "Dashboard",
"subtitle" : "Home Assistant",
"subtitle": "Home Assistant",
"description": "Open Home Assistant dashboard",
"mode": "view"
"mode": "no-view"
},
{
"name": "climate",
"title": "Climate",
"subtitle" : "Home Assistant",
"subtitle": "Home Assistant",
"description": "Get/Set states of Home Assistant climate entities",
"mode": "view"
},
{
"name": "attributes",
"title": "All Entity Attributes",
"subtitle" : "Home Assistant",
"title": "All Entities with Attributes",
"subtitle": "Home Assistant",
"description": "Query Home Assistant entity attributes",
"mode": "view"
}
Expand All @@ -99,12 +99,13 @@
"type": "password",
"required": true,
"title": "API Token",
"description": "Your Home Assistant API token",
"placeholder": "Enter your Home Assistant API token"
"description": "Your Home Assistant long-lived access token",
"placeholder": "Enter your Home Assistant long-lived access token"
}
],
"dependencies": {
"@raycast/api": "^1.25.0",
"home-assistant-js-websocket": "^5.11.1",
"node-fetch": "^2.6.1",
"url-join": "^4.0.1"
},
Expand All @@ -123,4 +124,4 @@
"dev": "ray develop",
"build": "ray build -e dist"
}
}
}
19 changes: 18 additions & 1 deletion extensions/homeassistant/src/common.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
import {
preferences,
} from "@raycast/api";
import { Connection, createConnection, createLongLivedTokenAuth } from "home-assistant-js-websocket";
import { HomeAssistant } from "./haapi";

export function createHomeAssistantClient() {
const instance = preferences.instance?.value as string;
const token = preferences.token?.value as string;
const ha = new HomeAssistant(instance, token);
return ha;
}
}

var con: Connection;

export async function getHAWSConnection() {
if (con) {
console.log("return existing ws con");
return con;
} else {
console.log("create new home assistant ws con");
const instance = preferences.instance?.value as string;
const token = preferences.token?.value as string;
const auth = createLongLivedTokenAuth(instance, token);
con = await createConnection({ auth });
return con;
}
}
126 changes: 57 additions & 69 deletions extensions/homeassistant/src/components/attributes_all.tsx
Original file line number Diff line number Diff line change
@@ -1,104 +1,92 @@
import { ActionPanel, CopyToClipboardAction, List, showToast, ToastStyle } from "@raycast/api";
import { ActionPanel, CopyToClipboardAction, List, ListItem, showToast, ToastStyle } from "@raycast/api";
import { useState, useEffect } from "react";
import { createHomeAssistantClient } from "../common";
import { State } from "../haapi";
import { useHAStates } from "../hooks";

export const ha = createHomeAssistantClient();

class Attribute {
public name = "";
public value: any;
}

export function StatesAttributesList(props: { domain: string }) {
const [searchText, setSearchText] = useState<string>();
const { attributes, error, isLoading } = useSearch(searchText, props.domain);
const { states: allStates, error, isLoading } = useHAStates();
const { states } = useSearch(searchText, allStates);

if (error) {
showToast(ToastStyle.Failure, "Cannot search Home Assistant states", error);
}

if (!attributes) {
if (!states) {
return <List isLoading={true} searchBarPlaceholder="Loading" />;
}

const stateTitle = (state: State): string => {
const attrs = state.attributes;
return attrs.friendly_name ? `${attrs.friendly_name} (${state.entity_id})` : state.entity_id;
};

return (
<List
searchBarPlaceholder="Filter by entity ID or attribute name"
isLoading={isLoading}
onSearchTextChange={setSearchText}
>
{attributes?.map((attr) => (
<List.Item
key={attr.name}
title={attr.name}
accessoryTitle={`${attr.value}`.substring(0, 50)}
actions={
<ActionPanel>
<CopyToClipboardAction content={`${attr.value}`} />
</ActionPanel>
}
/>
{states.map((state: State) => (
<List.Section key={state.entity_id} title={stateTitle(state)}>
<List.Item key={`${state.entity_id}_state`} title="state" accessoryTitle={`${state.state}`} />
{Object.entries(state.attributes).map(([k, v]) => (
<List.Item
key={state.entity_id + k}
title={k}
accessoryTitle={`${v}`}
actions={
<ActionPanel>
<CopyToClipboardAction title="Copy Value" content={`${v}`} />
<CopyToClipboardAction title="Copy Name" content={`${k}`} />
<CopyToClipboardAction title="Copy Entity ID" content={`${state.entity_id}`} />
</ActionPanel>
}
/>
))}
</List.Section>
))}
</List>
);
}

export function useSearch(
function useSearch(
query: string | undefined,
domain: string
allStates?: State[]
): {
attributes?: Attribute[];
error?: string;
isLoading: boolean;
states?: State[];
} {
const [attributes, setAttributes] = useState<Attribute[]>();
const [error, setError] = useState<string>();
const [isLoading, setIsLoading] = useState<boolean>(false);

let cancel = false;
const [states, setStates] = useState<State[]>();
const lquery = query ? query.toLocaleLowerCase().trim() : query;

useEffect(() => {
async function fetchData() {
if (query === null || cancel) {
return;
}

setIsLoading(true);
setError(undefined);

try {
const haStates = await ha.getStates({ domain: domain, query: "" });
let attributesData: Attribute[] = [];
haStates.forEach((e) =>
Object.entries(e.attributes).forEach(([k, v]) =>
attributesData.push({ name: `${e.entity_id}.${k}`, value: v })
)
);
if (query) {
const lquery = query.toLocaleLowerCase().trim();
attributesData = attributesData.filter((v) => v.name.toLocaleLowerCase().includes(lquery));
}
attributesData = attributesData.slice(0, 100);
if (!cancel) {
setAttributes(attributesData);
}
} catch (e: any) {
if (!cancel) {
setError(e.toString());
if (allStates) {
let filteredStates: State[] = [];
allStates.forEach((s) => {
let attrs: Record<string, any> = {};
for (const [k, v] of Object.entries(s.attributes)) {
if (lquery) {
const friendlyName: string = (s.attributes.friendly_name || "").toLocaleLowerCase();
const eid = `${s.entity_id}.${k}`.toLocaleLowerCase();
if (eid.includes(lquery) || friendlyName.includes(lquery)) {
attrs[k] = v;
}
} else {
attrs[k] = v;
}
}
} finally {
if (!cancel) {
setIsLoading(false);
if (Object.keys(attrs).length > 0) {
const ns: State = { entity_id: s.entity_id, state: s.state, attributes: attrs };
filteredStates.push(ns);
}
}
});
setStates(filteredStates.slice(0, 100));
} else {
setStates([]);
}

fetchData();

return () => {
cancel = true;
};
}, [query]);

return { attributes, error, isLoading };
}, [query, allStates]);
return { states };
}
Loading

0 comments on commit bf84409

Please sign in to comment.