Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions extensions/tuya-smart/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Tuya Smart Changelog

## [Enhancement] - {PR_MERGE_DATE}

- Added switches in root search

## [Fix] - {PR_MERGE_DATE}

Fixed an error that caused the extension to crash
Expand Down
3 changes: 3 additions & 0 deletions extensions/tuya-smart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"description": "Home automation with Tuya Smart",
"icon": "command-icon.png",
"author": "andresmorelos",
"contributors": [
"anwarulislam"
],
"categories": [
"Productivity",
"Other"
Expand Down
10 changes: 10 additions & 0 deletions extensions/tuya-smart/src/components/actionPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export function CommandActionPanel(props: {
command: FunctionItem;
newName?: string;
onAction: (props: { result: boolean; command: FunctionItem }) => void;
onTogglePinSwitch?: (deviceId: string, commandCode: string) => void;
isPinned?: boolean;
}): JSX.Element {
const deviceId = props.device.id;
const commandValue = props.command.value;
Expand All @@ -51,6 +53,14 @@ export function CommandActionPanel(props: {
onAction={props.onAction}
/>
)}
{props.onTogglePinSwitch && props.isPinned !== undefined && (
<Actions.SwitchPinAction
deviceId={deviceId}
commandCode={props.command.code}
isPinned={props.isPinned}
onTogglePin={props.onTogglePinSwitch}
/>
)}
<Action.Push
title="Rename"
icon={Icon.Pencil}
Expand Down
20 changes: 20 additions & 0 deletions extensions/tuya-smart/src/components/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,26 @@ export function DevicePinAction(props: { device: Device; onAction: (device: Devi
);
}

export function SwitchPinAction(props: {
deviceId: string;
commandCode: string;
isPinned: boolean;
onTogglePin: (deviceId: string, commandCode: string) => void;
}): JSX.Element {
const { isPinned, onTogglePin, deviceId, commandCode } = props;
return (
<Action
title={isPinned ? "Unpin Switch" : "Pin Switch"}
icon={Icon.Pin}
shortcut={{ modifiers: ["opt", "shift"], key: "p" }}
onAction={() => {
onTogglePin(deviceId, commandCode);
showToast(Toast.Style.Success, isPinned ? "Unpinned Switch" : "Pinned Switch");
}}
/>
);
}

export function BooleanCommand(props: {
deviceId: string;
command: FunctionItem;
Expand Down
6 changes: 6 additions & 0 deletions extensions/tuya-smart/src/components/filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export enum DeviceOnlineFilterType {
all = "all",
Online = "Online",
Offline = "Offline",
On = "On",
Off = "Off",
}

export function DeviceOnlineFilterDropdown(props: { onSelect: (value: DeviceOnlineFilterType) => void }): JSX.Element {
Expand All @@ -18,11 +20,15 @@ export function DeviceOnlineFilterDropdown(props: { onSelect: (value: DeviceOnli
<List.Dropdown.Item value={DeviceOnlineFilterType.all} title="All" />
<List.Dropdown.Item value={DeviceOnlineFilterType.Online} title="Online" />
<List.Dropdown.Item value={DeviceOnlineFilterType.Offline} title="Offline" />
<List.Dropdown.Item value={DeviceOnlineFilterType.On} title="On" />
<List.Dropdown.Item value={DeviceOnlineFilterType.Off} title="Off" />
</List.Dropdown>
);
}

export function placeholder(filter: DeviceOnlineFilterType): string {
if (filter === DeviceOnlineFilterType.On) return "Search On devices/switches by name";
if (filter === DeviceOnlineFilterType.Off) return "Search Off devices/switches by name";
return `Search ${
filter === DeviceOnlineFilterType.all
? "Online & Offline"
Expand Down
123 changes: 117 additions & 6 deletions extensions/tuya-smart/src/components/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useState } from "react";
import { timeConversion } from "../utils/functions";
import { Device, FunctionItem } from "../utils/interfaces";
import { CommandActionPanel, DeviceActionPanel } from "./actionPanels";
import { DeviceOnlineFilterType } from "./filter";

export interface DeviceListProps {
isLoading: boolean;
Expand All @@ -11,6 +12,9 @@ export interface DeviceListProps {
searchBarAccessory?: JSX.Element;
onSearchTextChange?: (q: string) => void;
onAction: (device: Device) => void;
filter: DeviceOnlineFilterType;
pinnedSwitches: string[];
onTogglePinSwitch: (deviceId: string, commandCode: string) => void;
}

export interface CommandListProps {
Expand All @@ -26,6 +30,29 @@ export function DeviceList(props: DeviceListProps): JSX.Element {
});

const notPinneddevices = devices.filter((device) => !device.pinned);

// Extract all switches
const allSwitches = devices.flatMap((device) =>
(device.status || [])
.filter((status) => status.code.toLowerCase().startsWith("switch") && typeof status.value === "boolean")
.map((status) => ({ device, status }))
);

// Filter switches based on On/Off filter
const filteredSwitches = allSwitches.filter(({ status }) => {
if (props.filter === DeviceOnlineFilterType.On) return status.value === true;
if (props.filter === DeviceOnlineFilterType.Off) return status.value === false;
return true;
});

// Separate pinned and unpinned switches
const pinnedSwitches = filteredSwitches.filter(({ device, status }) =>
props.pinnedSwitches.includes(`${device.id}:${status.code}`)
);
const unpinnedSwitches = filteredSwitches.filter(
({ device, status }) => !props.pinnedSwitches.includes(`${device.id}:${status.code}`)
);

return (
<List
searchBarPlaceholder={props.searchBarPlaceholder}
Expand All @@ -34,28 +61,110 @@ export function DeviceList(props: DeviceListProps): JSX.Element {
isLoading={props.isLoading}
isShowingDetail
>
<List.Section title="Pinned">
{pinnedDevices.map((device) => (
<DeviceListItem key={`formula-${device.name}`} device={device} onAction={props.onAction} />
))}
</List.Section>
{(pinnedSwitches.length > 0 || pinnedDevices.length > 0) && (
<List.Section title="Pinned">
{pinnedSwitches.map(({ device, status }) => (
<SwitchListItem
key={`${device.id}-${status.code}`}
device={device}
command={status}
onAction={props.onAction}
isPinned={true}
onTogglePin={props.onTogglePinSwitch}
/>
))}
{pinnedDevices.map((device) => (
<DeviceListItem key={`formula-${device.name}`} device={device} onAction={props.onAction} />
))}
</List.Section>
)}
<List.Section title="Devices">
{notPinneddevices.map((device) => (
<DeviceListItem key={`formula-${device.name}`} device={device} onAction={props.onAction} />
))}
</List.Section>
<List.Section title="Switches">
{unpinnedSwitches.map(({ device, status }) => (
<SwitchListItem
key={`${device.id}-${status.code}`}
device={device}
command={status}
onAction={props.onAction}
isPinned={false}
onTogglePin={props.onTogglePinSwitch}
/>
))}
</List.Section>
</List>
);
}

export function SwitchListItem(props: {
command: FunctionItem;
device: Device;
onAction: (device: Device) => void;
isPinned: boolean;
onTogglePin: (deviceId: string, commandCode: string) => void;
}): JSX.Element {
const [command, setCommand] = useState<FunctionItem>(props.command);
const device = props.device;

return (
<List.Item
title={command.name ?? command.code}
accessories={[{ text: device.name }]}
icon={{ source: Icon.Circle, tintColor: command.value ? Color.Green : Color.Red }}
detail={
<List.Item.Detail
metadata={
<List.Item.Detail.Metadata>
<List.Item.Detail.Metadata.Label title="Device Information" />
<List.Item.Detail.Metadata.Label title="Name" text={device.name} />
<List.Item.Detail.Metadata.Label title="Category" text={device.category} />
<List.Item.Detail.Metadata.Label title="Id" text={device.id} />
<List.Item.Detail.Metadata.Label title="Status" text={device.online ? "Online" : "Offline"} />
<List.Item.Detail.Metadata.Separator />
<List.Item.Detail.Metadata.Label title="Switch Information" />
<List.Item.Detail.Metadata.Label title="Code" text={command.code} />
<List.Item.Detail.Metadata.Label title="Value" text={command.value?.toString()} />
</List.Item.Detail.Metadata>
}
/>
}
actions={
<CommandActionPanel
command={command}
device={props.device}
onTogglePinSwitch={props.onTogglePin}
isPinned={props.isPinned}
onAction={({ command }) => {
setCommand(() => {
return { ...command };
});

const statusIndex = props.device.status.findIndex((status) => status.code === command.code);
if (statusIndex !== -1) {
props.device.status[statusIndex] = command;
}

props.onAction({
...props.device,
});
}}
/>
}
/>
);
}

export function DeviceListItem(props: { device: Device; onAction: (device: Device) => void }): JSX.Element {
const device = props.device;
const category = device.category;
const online = device.online;
const tintColor = online ? Color.Green : Color.Red;
const tooltip: string | undefined = online ? "Online" : "Offline";

const icon = { source: Icon.Circle, tintColor };
const icon = { source: Icon.Speaker, tintColor };

return (
<List.Item
Expand Down Expand Up @@ -115,6 +224,8 @@ export function CommandListItem(props: {
onAction: (device: Device) => void;
}): JSX.Element {
const [command, setCommand] = useState<FunctionItem>(props.command);
const [device] = useState<Device>(props.device); // Although device isn't used in render, keeping it consistent with valid logic if needed

return (
<List.Item
title={command.name ?? command.code}
Expand Down
20 changes: 16 additions & 4 deletions extensions/tuya-smart/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default function Command() {
const [isLoading, setIsLoading] = useState(true);
const [devices, setDevices] = useCachedState<Device[]>("devices", []);
const [categories, setCategories] = useCachedState<DeviceCategory[]>("categories", []);
const [pinnedSwitches, setPinnedSwitches] = useCachedState<string[]>("pinnedSwitches", []);

useEffect(() => {
const getDeviceCategories = async () => {
Expand Down Expand Up @@ -72,18 +73,29 @@ export default function Command() {
}, [categories]);

const finalDevices =
filter === DeviceOnlineFilterType.all
? devices
: filter === DeviceOnlineFilterType.Online
filter === DeviceOnlineFilterType.Online
? devices.filter((device) => device.online)
: devices.filter((device) => !device.online);
: filter === DeviceOnlineFilterType.Offline
? devices.filter((device) => !device.online)
: devices;

return (
<DeviceList
devices={finalDevices}
searchBarPlaceholder={placeholder(filter)}
searchBarAccessory={<DeviceOnlineFilterDropdown onSelect={setFilter} />}
isLoading={isLoading}
filter={filter}
pinnedSwitches={pinnedSwitches}
onTogglePinSwitch={(deviceId, commandCode) => {
const key = `${deviceId}:${commandCode}`;
setPinnedSwitches((prev) => {
if (prev.includes(key)) {
return prev.filter((k) => k !== key);
}
return [...prev, key];
});
}}
onAction={(device) => {
setDevices((prev) => {
const formatedDevices = prev.map((oldDevice) => {
Expand Down
Loading