From 9fe79b9366ff43297c7e0b0e69ae2cfd44ad5885 Mon Sep 17 00:00:00 2001 From: Michael Aigner Date: Fri, 15 Oct 2021 21:58:19 +0200 Subject: [PATCH] Add home assistant 1.1.0 (#30) * add Home Assistant 1.1.0 * move state component * add Search Entity Attributes root command * add heat_cool mode support * rename command * update states after action --- extensions/homeassistant/package-lock.json | 18 +-- extensions/homeassistant/package.json | 11 +- extensions/homeassistant/src/attributes.tsx | 5 + extensions/homeassistant/src/automations.tsx | 2 +- .../homeassistant/src/binarysensors.tsx | 2 +- extensions/homeassistant/src/climate.tsx | 2 +- .../src/components/attributes.tsx | 27 +++++ .../src/components/attributes_all.tsx | 104 ++++++++++++++++ .../{components.tsx => components/states.tsx} | 112 +++++++++++++----- extensions/homeassistant/src/covers.tsx | 2 +- extensions/homeassistant/src/dashboard.tsx | 2 +- extensions/homeassistant/src/index.tsx | 2 +- extensions/homeassistant/src/lights.tsx | 2 +- extensions/homeassistant/src/mediaplayers.tsx | 2 +- extensions/homeassistant/src/persons.tsx | 2 +- extensions/homeassistant/src/sensors.tsx | 2 +- 16 files changed, 246 insertions(+), 51 deletions(-) create mode 100644 extensions/homeassistant/src/attributes.tsx create mode 100644 extensions/homeassistant/src/components/attributes.tsx create mode 100644 extensions/homeassistant/src/components/attributes_all.tsx rename extensions/homeassistant/src/{components.tsx => components/states.tsx} (80%) diff --git a/extensions/homeassistant/package-lock.json b/extensions/homeassistant/package-lock.json index 09fc70b3eba..140b47910c6 100644 --- a/extensions/homeassistant/package-lock.json +++ b/extensions/homeassistant/package-lock.json @@ -1,14 +1,14 @@ { "name": "homeassistant", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "1.0.0", + "version": "1.1.0", "license": "MIT", "dependencies": { - "@raycast/api": "^1.24.5", + "@raycast/api": "^1.25.0", "node-fetch": "^2.6.1", "url-join": "^4.0.1" }, @@ -212,9 +212,9 @@ } }, "node_modules/@raycast/api": { - "version": "1.24.5", - "resolved": "https://registry.npmjs.org/@raycast/api/-/api-1.24.5.tgz", - "integrity": "sha512-dv7cjwH+fcG1inQi8qPMbzkrZLc1HPsiDKO5lYQKFmtFMCIsSivg5zmXm7tCHhDGEUSi4uZK+9H7Mo6Ov51wdA==", + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@raycast/api/-/api-1.25.0.tgz", + "integrity": "sha512-uRvJ1LEz0wlDSmw2p+mT63CpUabViyy26AU0M8PCegAv96wtzNfxeuxPU18QmObADOTjbLTv/HcZD8fSCUHjjg==", "dependencies": { "fast-json-patch": "3.1.0", "json-rpc-2.0": "0.2.19", @@ -2243,9 +2243,9 @@ } }, "@raycast/api": { - "version": "1.24.5", - "resolved": "https://registry.npmjs.org/@raycast/api/-/api-1.24.5.tgz", - "integrity": "sha512-dv7cjwH+fcG1inQi8qPMbzkrZLc1HPsiDKO5lYQKFmtFMCIsSivg5zmXm7tCHhDGEUSi4uZK+9H7Mo6Ov51wdA==", + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@raycast/api/-/api-1.25.0.tgz", + "integrity": "sha512-uRvJ1LEz0wlDSmw2p+mT63CpUabViyy26AU0M8PCegAv96wtzNfxeuxPU18QmObADOTjbLTv/HcZD8fSCUHjjg==", "requires": { "fast-json-patch": "3.1.0", "json-rpc-2.0": "0.2.19", diff --git a/extensions/homeassistant/package.json b/extensions/homeassistant/package.json index 428f82c586e..8099072b3ad 100644 --- a/extensions/homeassistant/package.json +++ b/extensions/homeassistant/package.json @@ -1,7 +1,7 @@ { "name": "homeassistant", "title": "Home Assistant", - "version": "1.0.0", + "version": "1.1.0", "author": "tonka3000", "license": "MIT", "description": "Home Assistant remote control. Control your house with Raycast 🚀", @@ -76,6 +76,13 @@ "subtitle" : "Home Assistant", "description": "Get/Set states of Home Assistant climate entities", "mode": "view" + }, + { + "name": "attributes", + "title": "All Entity Attributes", + "subtitle" : "Home Assistant", + "description": "Query Home Assistant entity attributes", + "mode": "view" } ], "preferences": [ @@ -97,7 +104,7 @@ } ], "dependencies": { - "@raycast/api": "^1.24.5", + "@raycast/api": "^1.25.0", "node-fetch": "^2.6.1", "url-join": "^4.0.1" }, diff --git a/extensions/homeassistant/src/attributes.tsx b/extensions/homeassistant/src/attributes.tsx new file mode 100644 index 00000000000..dff9348c72f --- /dev/null +++ b/extensions/homeassistant/src/attributes.tsx @@ -0,0 +1,5 @@ +import { StatesAttributesList } from "./components/attributes_all"; + +export default function main() { + return ; +} diff --git a/extensions/homeassistant/src/automations.tsx b/extensions/homeassistant/src/automations.tsx index 5bd0e6b0c87..1f862b92b1b 100644 --- a/extensions/homeassistant/src/automations.tsx +++ b/extensions/homeassistant/src/automations.tsx @@ -1,4 +1,4 @@ -import { StatesList } from "./components"; +import { StatesList } from "./components/states"; export default function main() { return ; diff --git a/extensions/homeassistant/src/binarysensors.tsx b/extensions/homeassistant/src/binarysensors.tsx index b3cad75fcc7..22faf342eb4 100644 --- a/extensions/homeassistant/src/binarysensors.tsx +++ b/extensions/homeassistant/src/binarysensors.tsx @@ -1,4 +1,4 @@ -import { StatesList } from "./components"; +import { StatesList } from "./components/states"; export default function main() { return ; diff --git a/extensions/homeassistant/src/climate.tsx b/extensions/homeassistant/src/climate.tsx index 8be50e669b6..031a6f24c6f 100644 --- a/extensions/homeassistant/src/climate.tsx +++ b/extensions/homeassistant/src/climate.tsx @@ -1,4 +1,4 @@ -import { StatesList } from "./components"; +import { StatesList } from "./components/states"; export default function main() { return ; diff --git a/extensions/homeassistant/src/components/attributes.tsx b/extensions/homeassistant/src/components/attributes.tsx new file mode 100644 index 00000000000..3a9ca918e11 --- /dev/null +++ b/extensions/homeassistant/src/components/attributes.tsx @@ -0,0 +1,27 @@ +import { ActionPanel, CopyToClipboardAction, List } from "@raycast/api"; +import { State } from "../haapi"; + +export function EntityAttributesList(props: { state: State }) { + const state = props.state; + const title = state.attributes.friendly_name + ? `${state.attributes.friendly_name} (${state.entity_id})` + : `${state.entity_id}`; + return ( + + + {Object.entries(state.attributes).map(([k, v]) => ( + + + + } + /> + ))} + + + ); +} diff --git a/extensions/homeassistant/src/components/attributes_all.tsx b/extensions/homeassistant/src/components/attributes_all.tsx new file mode 100644 index 00000000000..a956f7ff4eb --- /dev/null +++ b/extensions/homeassistant/src/components/attributes_all.tsx @@ -0,0 +1,104 @@ +import { ActionPanel, CopyToClipboardAction, List, showToast, ToastStyle } from "@raycast/api"; +import { useState, useEffect } from "react"; +import { createHomeAssistantClient } from "../common"; + +export const ha = createHomeAssistantClient(); + +class Attribute { + public name = ""; + public value: any; +} + +export function StatesAttributesList(props: { domain: string }) { + const [searchText, setSearchText] = useState(); + const { attributes, error, isLoading } = useSearch(searchText, props.domain); + + if (error) { + showToast(ToastStyle.Failure, "Cannot search Home Assistant states", error); + } + + if (!attributes) { + return ; + } + + return ( + + {attributes?.map((attr) => ( + + + + } + /> + ))} + + ); +} + +export function useSearch( + query: string | undefined, + domain: string +): { + attributes?: Attribute[]; + error?: string; + isLoading: boolean; +} { + const [attributes, setAttributes] = useState(); + const [error, setError] = useState(); + const [isLoading, setIsLoading] = useState(false); + + let cancel = false; + + 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()); + } + } finally { + if (!cancel) { + setIsLoading(false); + } + } + } + + fetchData(); + + return () => { + cancel = true; + }; + }, [query]); + + return { attributes, error, isLoading }; +} diff --git a/extensions/homeassistant/src/components.tsx b/extensions/homeassistant/src/components/states.tsx similarity index 80% rename from extensions/homeassistant/src/components.tsx rename to extensions/homeassistant/src/components/states.tsx index 31c858df557..e0be1c3bd12 100644 --- a/extensions/homeassistant/src/components.tsx +++ b/extensions/homeassistant/src/components/states.tsx @@ -1,24 +1,48 @@ import { ActionPanel, ActionPanelItem, - ActionPanelSubmenu, Color, CopyToClipboardAction, Icon, + ImageLike, + KeyboardShortcut, List, + OpenAction, popToRoot, + PushAction, showToast, ToastStyle, } from "@raycast/api"; -import { HomeAssistant, State } from "./haapi"; +import { State } from "../haapi"; import { useState, useEffect } from "react"; -import { createHomeAssistantClient } from "./common"; +import { createHomeAssistantClient } from "../common"; +import { EntityAttributesList } from "./attributes"; export const ha = createHomeAssistantClient(); +export function ShowAttributesAction(props: { state: State }) { + if (props.state.attributes) { + return ( + } + shortcut={{ modifiers: ["cmd", "shift"], key: "a" }} + icon={{ source: Icon.List, tintColor: Color.PrimaryText }} + /> + ); + } else { + return null; + } +} + export function StatesList(props: { domain: string }) { const [searchText, setSearchText] = useState(); - const { states, error, isLoading } = useSearch(searchText, props.domain); + const [updateTimestamp, setUpdateTimestamp] = useState(new Date()); + const { states, error, isLoading } = useSearch(searchText, props.domain, updateTimestamp); + + const refreshStates = () => { + setUpdateTimestamp(new Date()); + }; if (error) { showToast(ToastStyle.Failure, "Cannot search Home Assistant states", error); @@ -36,44 +60,66 @@ export function StatesList(props: { domain: string }) { title={state.attributes.friendly_name || state.entity_id} subtitle={state.entity_id} accessoryTitle={state.state} - actions={} + actions={} /> ))} ); } -export function StateActionPanel(props: { state: State }) { +export function StateActionPanel(props: { state: State; refreshStates: () => void }) { const state = props.state; const domain = props.state.entity_id.split(".")[0]; const entityID = props.state.entity_id; + const refreshStates = props.refreshStates; + + function StateActionItem(props: { + title: string; + onAction: () => Promise; + icon?: ImageLike | undefined; + shortcut?: KeyboardShortcut | undefined; + }) { + return ( + { + await props.onAction(); + refreshStates(); + }} + icon={props.icon} + shortcut={props.shortcut} + /> + ); + } + switch (domain) { case "cover": { return ( - await ha.toggleCover(props.state.entity_id)} icon={{ source: "toggle.png", tintColor: Color.PrimaryText }} /> - await ha.openCover(props.state.entity_id)} icon={{ source: Icon.ChevronUp, tintColor: Color.PrimaryText }} /> - await ha.closeCover(props.state.entity_id)} icon={{ source: Icon.ChevronDown, tintColor: Color.PrimaryText }} /> - await ha.stopCover(props.state.entity_id)} icon={{ source: Icon.XmarkCircle, tintColor: Color.PrimaryText }} /> + ); @@ -81,23 +127,24 @@ export function StateActionPanel(props: { state: State }) { case "light": { return ( - await ha.toggleLight(props.state.entity_id)} icon={{ source: "toggle.png", tintColor: Color.PrimaryText }} /> - await ha.turnOnLight(props.state.entity_id)} icon={{ source: "power.png", tintColor: Color.Green }} /> - await ha.turnOffLight(props.state.entity_id)} icon={{ source: "power.png", tintColor: Color.Red }} /> + ); @@ -105,57 +152,58 @@ export function StateActionPanel(props: { state: State }) { case "media_player": { return ( - await ha.playPauseMedia(entityID)} icon={{ source: "play-pause.jpg", tintColor: Color.PrimaryText }} /> - await ha.playMedia(entityID)} icon={{ source: "play.png", tintColor: Color.PrimaryText }} /> - await ha.pauseMedia(entityID)} icon={{ source: "pause.png", tintColor: Color.PrimaryText }} /> - await ha.stopMedia(entityID)} icon={{ source: Icon.XmarkCircle, tintColor: Color.PrimaryText }} /> - await ha.nextMedia(entityID)} icon={{ source: "next.png", tintColor: Color.PrimaryText }} /> - await ha.previousMedia(entityID)} icon={{ source: "previous.png", tintColor: Color.PrimaryText }} /> - await ha.volumeUpMedia(entityID)} icon={{ source: Icon.SpeakerArrowUp, tintColor: Color.PrimaryText }} /> - await ha.volumeDownMedia(entityID)} icon={{ source: Icon.SpeakerArrowDown, tintColor: Color.PrimaryText }} /> - await ha.muteMedia(entityID)} icon={{ source: Icon.SpeakerSlash, tintColor: Color.PrimaryText }} /> + @@ -163,7 +211,9 @@ export function StateActionPanel(props: { state: State }) { } case "climate": { const changeTempAllowed = - state.state === "heat" || state.state === "cool" || state.state == "auto" ? true : false; + state.state === "heat" || state.state === "cool" || state.state === "heat_cool" || state.state == "auto" + ? true + : false; const currentTempValue: number | undefined = state.attributes.temperature || undefined; const [currentTemp, setCurrentTemp] = useState(currentTempValue); const upperTemp = currentTemp ? currentTemp + 0.5 : undefined; @@ -186,7 +236,7 @@ export function StateActionPanel(props: { state: State }) { icon={{ source: "thermometer.png", tintColor: Color.PrimaryText }} > {temps.map((t) => ( - { @@ -204,7 +254,7 @@ export function StateActionPanel(props: { state: State }) { icon={{ source: Icon.Gear, tintColor: Color.PrimaryText }} > {state.attributes.hvac_modes?.map((o: string) => ( - { @@ -223,7 +273,7 @@ export function StateActionPanel(props: { state: State }) { icon={{ source: Icon.List, tintColor: Color.PrimaryText }} > {preset_modes?.map((o: string) => ( - { @@ -236,7 +286,7 @@ export function StateActionPanel(props: { state: State }) { )} {upperTemp && changeTempAllowed && ( - { @@ -247,7 +297,7 @@ export function StateActionPanel(props: { state: State }) { /> )} {lowerTemp && changeTempAllowed && ( - { @@ -257,6 +307,7 @@ export function StateActionPanel(props: { state: State }) { icon={{ source: "minus.png", tintColor: Color.PrimaryText }} /> )} + @@ -270,7 +321,8 @@ export function StateActionPanel(props: { state: State }) { export function useSearch( query: string | undefined, - domain: string + domain: string, + updateTimestamp: Date ): { states?: State[]; error?: string; @@ -313,7 +365,7 @@ export function useSearch( return () => { cancel = true; }; - }, [query]); + }, [query, updateTimestamp]); return { states, error, isLoading }; } diff --git a/extensions/homeassistant/src/covers.tsx b/extensions/homeassistant/src/covers.tsx index e7ccbedb2ad..554b21c1208 100644 --- a/extensions/homeassistant/src/covers.tsx +++ b/extensions/homeassistant/src/covers.tsx @@ -1,4 +1,4 @@ -import { StatesList } from "./components"; +import { StatesList } from "./components/states"; export default function main() { return ; diff --git a/extensions/homeassistant/src/dashboard.tsx b/extensions/homeassistant/src/dashboard.tsx index a3dbb0e0a35..7baade7e8d7 100644 --- a/extensions/homeassistant/src/dashboard.tsx +++ b/extensions/homeassistant/src/dashboard.tsx @@ -1,5 +1,5 @@ import { popToRoot, render, showHUD } from "@raycast/api"; -import { ha } from "./components"; +import { ha } from "./components/states"; import open from "open"; async function main() { diff --git a/extensions/homeassistant/src/index.tsx b/extensions/homeassistant/src/index.tsx index 7e3caa1d483..a8f9b60599f 100644 --- a/extensions/homeassistant/src/index.tsx +++ b/extensions/homeassistant/src/index.tsx @@ -1,4 +1,4 @@ -import { StatesList } from "./components"; +import { StatesList } from "./components/states"; export default function main() { return ; diff --git a/extensions/homeassistant/src/lights.tsx b/extensions/homeassistant/src/lights.tsx index e3d12bc1095..85718a8180b 100644 --- a/extensions/homeassistant/src/lights.tsx +++ b/extensions/homeassistant/src/lights.tsx @@ -1,4 +1,4 @@ -import { StatesList } from "./components"; +import { StatesList } from "./components/states"; export default function main() { return ; diff --git a/extensions/homeassistant/src/mediaplayers.tsx b/extensions/homeassistant/src/mediaplayers.tsx index d6c9fff1af4..b2cf7fb703e 100644 --- a/extensions/homeassistant/src/mediaplayers.tsx +++ b/extensions/homeassistant/src/mediaplayers.tsx @@ -1,4 +1,4 @@ -import { StatesList } from "./components"; +import { StatesList } from "./components/states"; export default function main() { return ; diff --git a/extensions/homeassistant/src/persons.tsx b/extensions/homeassistant/src/persons.tsx index 3ed1d1c922f..f9eb3adb07b 100644 --- a/extensions/homeassistant/src/persons.tsx +++ b/extensions/homeassistant/src/persons.tsx @@ -1,4 +1,4 @@ -import { StatesList } from "./components"; +import { StatesList } from "./components/states"; export default function main() { return ; diff --git a/extensions/homeassistant/src/sensors.tsx b/extensions/homeassistant/src/sensors.tsx index e8bf2d6bbca..28ed602477f 100644 --- a/extensions/homeassistant/src/sensors.tsx +++ b/extensions/homeassistant/src/sensors.tsx @@ -1,4 +1,4 @@ -import { StatesList } from "./components"; +import { StatesList } from "./components/states"; export default function main() { return ;