Skip to content
Merged
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
9 changes: 9 additions & 0 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface AppSettings {
nightLightSceneAssignments?: Record<string, string>;
lastView?: "list" | "settings" | "scenes";
confirmOnOffPressActions?: Record<string, boolean>;
deviceControlDrafts?: Record<string, Record<string, unknown>>;
windowBounds?: { width: number; height: number; x: number; y: number };
}

Expand Down Expand Up @@ -50,6 +51,14 @@ const schema = {
default: {},
additionalProperties: { type: "boolean" },
},
deviceControlDrafts: {
type: "object",
default: {},
additionalProperties: {
type: "object",
additionalProperties: true,
},
},
windowBounds: {
type: "object",
properties: {
Expand Down
13 changes: 10 additions & 3 deletions src/renderer/src/components/device-controls/CurtainControls.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import React, { useState } from "react";
import React from "react";
import { Box, Button, Slider, Typography } from "@mui/material";
import { useTranslation } from "../../useTranslation";
import { DeviceControlProps } from "./DeviceControls.types";
import { clamp, SectionLabel } from "./utils";
import { usePersistedDeviceControlState } from "../../hooks/usePersistedDeviceControlState";
import {
COMMAND_SET_POSITION,
COMMAND_TURN_OFF,
COMMAND_TURN_ON,
} from "../../constants/commandConstants";

export const CurtainControls: React.FC<DeviceControlProps> = ({
device,
sendCommand,
controlsDisabled,
dense,
}) => {
const { t } = useTranslation();
const [position, setPosition] = useState(50);
const [draft, setDraft] = usePersistedDeviceControlState(device.deviceId, "curtain", {
position: 50,
});
const { position } = draft;
const gridColumns = dense ? 2 : 3;
const maxControlWidth = dense ? 360 : 520;
const buttonGridSx = {
Expand Down Expand Up @@ -68,7 +73,9 @@ export const CurtainControls: React.FC<DeviceControlProps> = ({
min={0}
max={100}
step={1}
onChange={(_, value) => setPosition(value as number)}
onChange={(_, value) =>
setDraft((current) => ({ ...current, position: value as number }))
}
onChangeCommitted={(_, value) =>
sendCommand(
COMMAND_SET_POSITION,
Expand Down
18 changes: 11 additions & 7 deletions src/renderer/src/components/device-controls/DynamicControls.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useMemo } from "react";
import {
Box,
Button,
Expand All @@ -16,6 +16,7 @@ import {
COMMAND_TYPE_COMMAND,
DEFAULT_PARAMETER,
} from "../../constants/commandConstants";
import { usePersistedDeviceControlState } from "../../hooks/usePersistedDeviceControlState";

export const DynamicControls: React.FC<DeviceControlProps> = ({
device,
Expand All @@ -24,10 +25,7 @@ export const DynamicControls: React.FC<DeviceControlProps> = ({
dense,
}) => {
const { definition, isHiddenByDefinition } = useDeviceType(device);
const [dynamicParams, setDynamicParams] = useState<Record<string, any>>({});
const maxControlWidth = dense ? 360 : 520;

useEffect(() => {
const defaultDynamicParams = useMemo(() => {
const defaults: Record<string, any> = {};
(definition?.commands || []).forEach((cmd) => {
const param = cmd.parameter;
Expand All @@ -39,8 +37,14 @@ export const DynamicControls: React.FC<DeviceControlProps> = ({
if (param.type === "text" && param.defaultValue !== undefined)
defaults[cmd.command] = param.defaultValue;
});
setDynamicParams(defaults);
}, [definition, device.deviceId]);
return defaults;
}, [definition]);
const [dynamicParams, setDynamicParams] = usePersistedDeviceControlState(
device.deviceId,
"dynamicControls",
defaultDynamicParams
);
const maxControlWidth = dense ? 360 : 520;

const resolveParameterValue = (cmd: DeviceCommandDefinition) => {
const spec = cmd.parameter;
Expand Down
13 changes: 10 additions & 3 deletions src/renderer/src/components/device-controls/FanControls.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import React, { useState } from "react";
import React from "react";
import { Box, Button, Slider, Typography } from "@mui/material";
import { useTranslation } from "../../useTranslation";
import { DeviceControlProps } from "./DeviceControls.types";
import { clamp, SectionLabel } from "./utils";
import { usePersistedDeviceControlState } from "../../hooks/usePersistedDeviceControlState";
import {
COMMAND_TURN_OFF,
COMMAND_TURN_ON,
} from "../../constants/commandConstants";

export const FanControls: React.FC<DeviceControlProps> = ({
device,
sendCommand,
controlsDisabled,
dense,
}) => {
const { t } = useTranslation();
const [fanSpeed, setFanSpeed] = useState(50);
const [draft, setDraft] = usePersistedDeviceControlState(device.deviceId, "fan", {
fanSpeed: 50,
});
const { fanSpeed } = draft;

const gridColumns = dense ? 2 : 3;
const maxControlWidth = dense ? 360 : 520;
Expand Down Expand Up @@ -93,7 +98,9 @@ export const FanControls: React.FC<DeviceControlProps> = ({
min={1}
max={100}
step={1}
onChange={(_, value) => setFanSpeed(value as number)}
onChange={(_, value) =>
setDraft((current) => ({ ...current, fanSpeed: value as number }))
}
onChangeCommitted={(_, value) =>
sendCommand("setWindSpeed", clamp(value as number, 1, 100))
}
Expand Down
28 changes: 23 additions & 5 deletions src/renderer/src/components/device-controls/HumidifierControls.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useState } from "react";
import React from "react";
import { Box, Button, Slider, Stack, Typography } from "@mui/material";
import { useTranslation } from "../../useTranslation";
import { DeviceControlProps } from "./DeviceControls.types";
import { clamp, SectionLabel } from "./utils";
import { useDeviceType } from "../../hooks/useDeviceType";
import { usePersistedDeviceControlState } from "../../hooks/usePersistedDeviceControlState";
import {
COMMAND_SET_MODE,
COMMAND_TURN_OFF,
Expand All @@ -20,8 +21,15 @@ export const HumidifierControls: React.FC<DeviceControlProps> = ({
const { t } = useTranslation();
const { isEvaporativeHumidifier } = useDeviceType(device);

const [targetHumidify, setTargetHumidify] = useState(60);
const [humidifierLevel, setHumidifierLevel] = useState(50);
const [draft, setDraft] = usePersistedDeviceControlState(
device.deviceId,
"humidifier",
{
targetHumidify: 60,
humidifierLevel: 50,
}
);
const { targetHumidify, humidifierLevel } = draft;

type EvaporativeModeKey =
| "level1"
Expand Down Expand Up @@ -182,7 +190,12 @@ export const HumidifierControls: React.FC<DeviceControlProps> = ({
min={0}
max={100}
step={1}
onChange={(_, value) => setHumidifierLevel(value as number)}
onChange={(_, value) =>
setDraft((current) => ({
...current,
humidifierLevel: value as number,
}))
}
onChangeCommitted={(_, value) =>
sendCommand(COMMAND_SET_MODE, clamp(value as number, 0, 100))
}
Expand Down Expand Up @@ -229,7 +242,12 @@ export const HumidifierControls: React.FC<DeviceControlProps> = ({
min={0}
max={100}
step={1}
onChange={(_, value) => setTargetHumidify(value as number)}
onChange={(_, value) =>
setDraft((current) => ({
...current,
targetHumidify: value as number,
}))
}
valueLabelDisplay="auto"
aria-label="Target humidity"
disabled={controlsDisabled}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useState } from "react";
import React from "react";
import { Box, Button, MenuItem, Slider, Stack, TextField, Typography } from "@mui/material";
import { useTranslation } from "../../useTranslation";
import { DeviceControlProps } from "./DeviceControls.types";
import { SectionLabel, clamp } from "./utils";
import { useDeviceType } from "../../hooks/useDeviceType";
import { usePersistedDeviceControlState } from "../../hooks/usePersistedDeviceControlState";
import { CustomCommandControls } from "./CustomCommandControls";
import {
COMMAND_TURN_OFF,
Expand All @@ -23,11 +24,18 @@ export const InfraredRemoteControls: React.FC<DeviceControlProps> = (props) => {
isRemoteLight,
} = useDeviceType(device);

const [acTemperature, setAcTemperature] = useState(26);
const [acMode, setAcMode] = useState("2"); // 2: cool (per docs)
const [acFanSpeed, setAcFanSpeed] = useState("3"); // 3: medium
const [acPower, setAcPower] = useState<"on" | "off">("on");
const [tvChannel, setTvChannel] = useState("");
const [draft, setDraft] = usePersistedDeviceControlState(
device.deviceId,
"infraredRemote",
{
acTemperature: 26,
acMode: "2", // 2: cool (per docs)
acFanSpeed: "3", // 3: medium
acPower: "on" as "on" | "off",
tvChannel: "",
}
);
const { acTemperature, acMode, acFanSpeed, acPower, tvChannel } = draft;

const gridColumns = dense ? 2 : 3;
const maxControlWidth = dense ? 360 : 520;
Expand Down Expand Up @@ -87,7 +95,9 @@ export const InfraredRemoteControls: React.FC<DeviceControlProps> = (props) => {
min={16}
max={32}
step={1}
onChange={(_, v) => setAcTemperature(v as number)}
onChange={(_, v) =>
setDraft((current) => ({ ...current, acTemperature: v as number }))
}
valueLabelDisplay="auto"
aria-label="AC temperature"
disabled={controlsDisabled}
Expand All @@ -99,7 +109,9 @@ export const InfraredRemoteControls: React.FC<DeviceControlProps> = (props) => {
size="small"
label={t("Mode")}
value={acMode}
onChange={(e) => setAcMode(e.target.value)}
onChange={(e) =>
setDraft((current) => ({ ...current, acMode: e.target.value }))
}
fullWidth
>
<MenuItem value="1">{t("Auto")}</MenuItem>
Expand All @@ -113,7 +125,9 @@ export const InfraredRemoteControls: React.FC<DeviceControlProps> = (props) => {
size="small"
label={t("Fan speed")}
value={acFanSpeed}
onChange={(e) => setAcFanSpeed(e.target.value)}
onChange={(e) =>
setDraft((current) => ({ ...current, acFanSpeed: e.target.value }))
}
fullWidth
>
<MenuItem value="1">{t("Auto")}</MenuItem>
Expand All @@ -126,7 +140,12 @@ export const InfraredRemoteControls: React.FC<DeviceControlProps> = (props) => {
size="small"
label={t("Power")}
value={acPower}
onChange={(e) => setAcPower(e.target.value as "on" | "off")}
onChange={(e) =>
setDraft((current) => ({
...current,
acPower: e.target.value as "on" | "off",
}))
}
fullWidth
>
<MenuItem value="on">{t("On")}</MenuItem>
Expand Down Expand Up @@ -199,7 +218,9 @@ export const InfraredRemoteControls: React.FC<DeviceControlProps> = (props) => {
label="Channel number"
size="small"
value={tvChannel}
onChange={(e) => setTvChannel(e.target.value)}
onChange={(e) =>
setDraft((current) => ({ ...current, tvChannel: e.target.value }))
}
placeholder="e.g. 15"
/>
<Button
Expand Down
22 changes: 16 additions & 6 deletions src/renderer/src/components/device-controls/LightControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useTranslation } from "../../useTranslation";
import { DeviceControlProps } from "./DeviceControls.types";
import { clamp, hexToRgbParameter, SectionLabel } from "./utils";
import { useDeviceType } from "../../hooks/useDeviceType";
import { usePersistedDeviceControlState } from "../../hooks/usePersistedDeviceControlState";
import {
COMMAND_SET_BRIGHTNESS,
COMMAND_SET_COLOR,
Expand Down Expand Up @@ -54,9 +55,12 @@ export const LightControls: React.FC<DeviceControlProps> = ({

const isTokenValid = useSelector(selectIsTokenValidated);

const [brightness, setBrightness] = useState(80);
const [colorTemp, setColorTemp] = useState(4000);
const [color, setColor] = useState("#ffffff");
const [draft, setDraft] = usePersistedDeviceControlState(device.deviceId, "light", {
brightness: 80,
colorTemp: 4000,
color: "#ffffff",
});
const { brightness, colorTemp, color } = draft;

// Night light scene logic
const scenes = useSelector(selectScenes);
Expand Down Expand Up @@ -239,7 +243,9 @@ export const LightControls: React.FC<DeviceControlProps> = ({
value={brightness}
min={1}
max={100}
onChange={(_, value) => setBrightness(value as number)}
onChange={(_, value) =>
setDraft((current) => ({ ...current, brightness: value as number }))
}
onChangeCommitted={(_, value) =>
sendCommand(COMMAND_SET_BRIGHTNESS, Math.round(value as number))
}
Expand All @@ -260,7 +266,9 @@ export const LightControls: React.FC<DeviceControlProps> = ({
min={2700}
max={6500}
step={100}
onChange={(_, value) => setColorTemp(value as number)}
onChange={(_, value) =>
setDraft((current) => ({ ...current, colorTemp: value as number }))
}
onChangeCommitted={(_, value) =>
sendCommand(
COMMAND_SET_COLOR_TEMPERATURE,
Expand All @@ -284,7 +292,9 @@ export const LightControls: React.FC<DeviceControlProps> = ({
label="Color (#RRGGBB)"
size="small"
value={color}
onChange={(e) => setColor(e.target.value)}
onChange={(e) =>
setDraft((current) => ({ ...current, color: e.target.value }))
}
sx={{ minWidth: 180, maxWidth: compactInputWidth }}
/>
<Button
Expand Down
16 changes: 13 additions & 3 deletions src/renderer/src/components/device-controls/VacuumControls.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import React, { useState } from "react";
import React from "react";
import { Box, Button, Slider, Typography } from "@mui/material";
import { useTranslation } from "../../useTranslation";
import { DeviceControlProps } from "./DeviceControls.types";
import { clamp, SectionLabel } from "./utils";
import { usePersistedDeviceControlState } from "../../hooks/usePersistedDeviceControlState";

export const VacuumControls: React.FC<DeviceControlProps> = ({
device,
sendCommand,
controlsDisabled,
dense,
}) => {
const { t } = useTranslation();
const [vacuumPowerLevel, setVacuumPowerLevel] = useState(1);
const [draft, setDraft] = usePersistedDeviceControlState(device.deviceId, "vacuum", {
vacuumPowerLevel: 1,
});
const { vacuumPowerLevel } = draft;

const gridColumns = dense ? 2 : 3;
const maxControlWidth = dense ? 360 : 520;
Expand Down Expand Up @@ -67,7 +72,12 @@ export const VacuumControls: React.FC<DeviceControlProps> = ({
min={0}
max={3}
step={1}
onChange={(_, value) => setVacuumPowerLevel(value as number)}
onChange={(_, value) =>
setDraft((current) => ({
...current,
vacuumPowerLevel: value as number,
}))
}
onChangeCommitted={(_, value) =>
sendCommand("PowLevel", clamp(value as number, 0, 3))
}
Expand Down
Loading
Loading