diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8099bd7..dc0135f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,9 +11,11 @@ import { StyledText, lightTheme, darkTheme } from "./utils/theme"; import Devices from "./pages/Devices/Devices"; import Settings from "./pages/Settings/Settings"; +import Groups from "./pages/Groups/Groups"; import Button from "./components/Button/Button"; import DeviceSettings from "./components/DeviceSettings/DeviceSettings"; +import { type Device, type Group } from "../../types/zigbee_types"; const Wrapper = styled.div` padding: 2em; @@ -30,7 +32,7 @@ const backend = import.meta.env.VITE_API_URL ?? ""; function App() { const [devices, setDevices] = useState(); - const [bridgeState, setBridgeState] = useState(); + const [groups, setGroups] = useState(); const [theme] = useDarkMode(); const themeMode = theme === "light" ? lightTheme : darkTheme; @@ -39,6 +41,10 @@ function App() { fetch(`${backend}/api/devices`) .then((res) => res.json()) .then((data) => setDevices(data.data)); + + fetch(`${backend}/api/groups`) + .then((res) => res.json()) + .then((data) => setGroups(data.data)); }, []); return ( @@ -62,21 +68,19 @@ function App() { - + {(params) => { if (!devices) return <>; const device = devices.find( - (device) => - Object.prototype.hasOwnProperty.call(device, "friendly_name") && - device.friendly_name === - decodeURIComponent(params.friendlyName), + (device: Device) => + device.ieee_address === decodeURIComponent(params.deviceId), ); if (!device) return ( - Device {params.friendlyName} does not exist on this + Device {params.deviceId} does not exist on this network. ); @@ -85,8 +89,12 @@ function App() { }} + + + + - + diff --git a/frontend/src/components/DeviceCard/DeviceCard.tsx b/frontend/src/components/DeviceCard/DeviceCard.tsx index 0b4bc66..9f98306 100644 --- a/frontend/src/components/DeviceCard/DeviceCard.tsx +++ b/frontend/src/components/DeviceCard/DeviceCard.tsx @@ -5,6 +5,8 @@ import { deviceDescription } from "../../utils/deviceUtilities"; import styled from "styled-components"; import { StyledText, StyledHeader } from "../../utils/theme"; +import { type Device } from "../../../../types/zigbee_types"; + const emojiLookup = { light: "💡", switch: "🔌", @@ -25,7 +27,7 @@ const Card = styled.div` } `; -function DeviceCard(props) { +function DeviceCard(props: { device: Device; onClick: Function }) { let deviceEmoji = "❓"; let deviceDefinition = props.device.definition; diff --git a/frontend/src/components/DeviceList/DeviceList.tsx b/frontend/src/components/DeviceList/DeviceList.tsx index 29be4fe..d60e732 100644 --- a/frontend/src/components/DeviceList/DeviceList.tsx +++ b/frontend/src/components/DeviceList/DeviceList.tsx @@ -3,13 +3,14 @@ import { Link } from "wouter"; import DeviceCard from "../DeviceCard/DeviceCard"; +import { type Device } from "../../../../types/zigbee_types"; -function DeviceList(props) { +function DeviceList(props: { devices: Device[]; onClick: Function }) { return (
- {props.devices.map((device) => ( + {props.devices.map((device: Device) => ( props.onClick} /> diff --git a/frontend/src/components/DeviceSettings/DeviceSettings.tsx b/frontend/src/components/DeviceSettings/DeviceSettings.tsx index fe90c4b..e4a4437 100644 --- a/frontend/src/components/DeviceSettings/DeviceSettings.tsx +++ b/frontend/src/components/DeviceSettings/DeviceSettings.tsx @@ -8,6 +8,8 @@ import { deviceSettingsGenerator } from "./generator"; import EditableText from "../EditableText/EditableText"; import { deviceDescription } from "../../utils/deviceUtilities"; +const backend = import.meta.env.VITE_API_URL ?? ""; + function DeviceSettings(props) { const [deviceSettingsState, setDeviceSettingsState] = useState(); const [deviceFriendlyNameState, setDeviceFriendlyNameState] = useState( @@ -26,9 +28,9 @@ function DeviceSettings(props) { properties[property.name] = ""; }); - getDeviceSettings(props.device.friendly_name, properties).then( - setDeviceSettingsState, - ); + fetch(`${backend}/api/devices/${props.device.ieee_address}/state`) + .then((res) => res.json()) + .then((data) => setDeviceSettingsState(data.data)); }, []); if (!deviceSettingsState) return ; diff --git a/frontend/src/components/DeviceSettings/generator.tsx b/frontend/src/components/DeviceSettings/generator.tsx index a4b62cc..b8e4c44 100644 --- a/frontend/src/components/DeviceSettings/generator.tsx +++ b/frontend/src/components/DeviceSettings/generator.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import { + mqttStateToBoolean, booleanToMqttState, hexToRGB, hslToRGB, @@ -10,6 +11,7 @@ import { } from "../../utils/deviceUtilities"; import { numericTransformer } from "../../utils/transformers"; import Toggle from "../Toggle/Toggle"; +import ColorPicker from "../ColorPicker/ColorPicker"; export const deviceSettingsGenerator = ( device, @@ -30,9 +32,7 @@ export const deviceSettingsGenerator = ( onChange={(event) => { const newMqttState = booleanToMqttState(event.target.checked); updateDeviceState( - deviceSettingsState, - setDeviceSettingsState, - device.friendly_name, + device.ieee_address, feature.name, newMqttState, ); diff --git a/frontend/src/components/GroupCard/GroupCard.tsx b/frontend/src/components/GroupCard/GroupCard.tsx new file mode 100644 index 0000000..0b00aa5 --- /dev/null +++ b/frontend/src/components/GroupCard/GroupCard.tsx @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: © 2024 Amber Cronin +// SPDX-License-Identifier: AGPL-3.0-or-later + +import styled from "styled-components"; +import { StyledText, StyledHeader } from "../../utils/theme"; + +import { type Group } from "../../../../types/zigbee_types"; + +const Card = styled.div` + margin: 2em 0em; + padding: 1em; + cursor: pointer; + border-radius: 2rem; + + &:hover { + background-color: ${({ theme }) => theme.hover}; + } +`; + +function GroupCard(props: { group: Group; onClick: Function }) { + return ( + + {props.group.friendly_name} + {`${props.group.members.length} devices`} + + ); +} + +export default GroupCard; diff --git a/frontend/src/components/GroupList/GroupList.tsx b/frontend/src/components/GroupList/GroupList.tsx new file mode 100644 index 0000000..a44c0d9 --- /dev/null +++ b/frontend/src/components/GroupList/GroupList.tsx @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: © 2024 Amber Cronin +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { Link } from "wouter"; +import GroupCard from "../GroupCard/GroupCard"; +import { type Group } from "../../../../types/zigbee_types"; + +function GroupList(props: { groups: Group[]; onClick?: Function }) { + return ( +
+ {props.groups.map((group: Group) => ( + + props.onClick} /> + + ))} +
+ ); +} + +export default GroupList; diff --git a/frontend/src/pages/Groups/Groups.tsx b/frontend/src/pages/Groups/Groups.tsx new file mode 100644 index 0000000..ad66919 --- /dev/null +++ b/frontend/src/pages/Groups/Groups.tsx @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: © 2024 Amber Cronin +// SPDX-License-Identifier: AGPL-3.0-or-later + +import LoadingSpinner from "../../components/LoadingSpinner/LoadingSpinner"; +import GroupList from "../../components/GroupList/GroupList"; + +function Groups(props) { + const selectedGroup = props.selectedGroup; + + let groupContent = undefined; + + if (!props.groups || !selectedGroup) groupContent = ; + + if (selectedGroup) groupContent = ; + + if (props.groups && !selectedGroup) { + groupContent = ; + } + + return <>{groupContent !== undefined ? groupContent : ""}; +} + +export default Groups; diff --git a/frontend/src/utils/deviceUtilities.ts b/frontend/src/utils/deviceUtilities.ts index 7824f3a..87fb475 100644 --- a/frontend/src/utils/deviceUtilities.ts +++ b/frontend/src/utils/deviceUtilities.ts @@ -1,30 +1,36 @@ // SPDX-FileCopyrightText: © 2021 Amber Cronin // SPDX-License-Identifier: AGPL-3.0-or-later -export const mqttStateToBoolean = (state) => { +const backend = import.meta.env.VITE_API_URL ?? ""; + +export const mqttStateToBoolean = (state: string): boolean => { if (state === "ON") return true; return false; }; -export const booleanToMqttState = (boolean) => { +export const booleanToMqttState = (boolean: boolean): string => { if (boolean) return "ON"; return "OFF"; }; export const updateDeviceState = ( - deviceSettingsState, - setDeviceSettingsState, - deviceFriendlyName, - property, - value, + deviceId: string, + property: string, + value: any, ) => { - const updateObject = {}; - updateObject[property] = value; - setDeviceSettings(deviceFriendlyName, updateObject); - - const clonedState = { ...deviceSettingsState }; - clonedState[property] = value; - setDeviceSettingsState(clonedState); + const request = new Request(`${backend}/api/devices/${deviceId}/state`, { + method: "POST", + mode: "cors", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ + setting: property, + value: value, + }), + }); + + fetch(request).then(); }; export const deviceDescription = (deviceDefinition) => {