Explorar el Código

api based server side

Timothy Pomeroy hace 2 años
padre
commit
848c34b957
Se han modificado 10 ficheros con 255 adiciones y 202 borrados
  1. 4 1
      .eslintrc.json
  2. 0 2
      app/admin/page.js
  3. 67 0
      app/api/automation/[id]/route.js
  4. 54 0
      app/api/power/[id]/route.js
  5. 69 80
      app/page.js
  6. 19 117
      components/Automation.js
  7. 1 1
      components/Management.js
  8. 0 1
      next.config.js
  9. 3 0
      package.json
  10. 38 0
      yarn.lock

+ 4 - 1
.eslintrc.json

@@ -1,3 +1,6 @@
 {
-  "extends": "next/core-web-vitals"
+  "extends": "next/core-web-vitals",
+  "rules": {
+    "react-hooks/exhaustive-deps": "off"
+  }
 }

+ 0 - 2
app/admin/page.js

@@ -266,6 +266,4 @@ export default function Home() {
       )) || <Loader size="lg" />}
     </Layout>
   );
-
-  return;
 }

+ 67 - 0
app/api/automation/[id]/route.js

@@ -0,0 +1,67 @@
+import { pick } from "lodash";
+import { NextResponse } from "next/server";
+import { WLEDClient } from "wled-client";
+
+import automation from "data/automation.json";
+
+const action = async ({ id, client, on }) => {
+  let wled;
+  let errors = [];
+  try {
+    console.log("connect to", client);
+    wled = new WLEDClient(client); // setup a connection to the client
+    await wled.init(); // init the connection
+  } catch (err) {
+    console.log("connect error", err.message);
+    errors.push({ error: err.message, type: "connect", client: client });
+  }
+  try {
+    console.log("set preset to", id);
+    await wled.setPreset(id); // set the preset
+    // await wled.setPreset(id); // set it again
+  } catch (err) {
+    console.log("preset error", err.message);
+    errors.push({ error: err.message, type: "preset", client: client, id: id });
+  }
+  try {
+    console.log("set power to", on);
+    if (on) await wled.turnOn(); // turn off the lights
+    else await wled.turnOff(); // turn off the lights
+  } catch (err) {
+    console.log("power error", err.message);
+    errors.push({ error: err.message, type: "power", client: client, on });
+  }
+  try {
+    await wled.refreshState();
+  } catch (err) {
+    console.log("state refresh", err.message);
+    errors.push({ error: err.message, type: "refreshState", client: client });
+  }
+  return {
+    ...pick(wled?.state || {}, ["on", "brightness", "presetId", "udpSync"]),
+    errors
+  };
+};
+
+export async function GET(req, { params }) {
+  let promises = [];
+  let id = params?.id || null;
+  try {
+    if (!id) throw new Error("No id specified");
+    let clients = (automation?.[id] && Object.keys(automation?.[id])) || [];
+    if (!clients) throw new Error("No clients for id", id);
+    for (let client of clients) {
+      promises.push(action({ id, client, ...automation?.[id]?.[client] }));
+    }
+  } catch (err) {
+    return NextResponse.json({ error: err?.message }, { status: 500 });
+  }
+  return Promise.allSettled(promises)
+    .then((results) => {
+      console.log("results", results);
+      return NextResponse.json(results?.map((o) => o?.value));
+    })
+    .catch((err) => {
+      return NextResponse.json({ error: err?.message }, { status: 500 });
+    });
+}

+ 54 - 0
app/api/power/[id]/route.js

@@ -0,0 +1,54 @@
+import { isEmpty, pick } from "lodash";
+import { NextResponse } from "next/server";
+import { WLEDClient } from "wled-client";
+
+import config from "data/config.json";
+
+const action = async ({ client, on }) => {
+  let wled;
+  let errors = [];
+  try {
+    console.log("connect to", client);
+    wled = new WLEDClient(client); // setup a connection to the client
+    await wled.init(); // init the connection
+  } catch (err) {
+    errors.push({ error: err.message, type: "connect", client });
+  }
+  try {
+    console.log("set power to", on);
+    if (on) await wled.turnOn(); // turn off the lights
+    else await wled.turnOff(); // turn off the lights
+  } catch (err) {
+    errors.push({ error: err.message, type: "power", client, on });
+  }
+  try {
+    await wled.refreshState();
+  } catch (err) {
+    console.log("state refresh", err.message);
+    errors.push({ error: err.message, type: "refreshState", client });
+  }
+  return { ...pick(wled?.state || {}, ["on"]), errors };
+};
+
+export async function GET(req, { params }) {
+  let id = params?.id;
+  let promises = [];
+  try {
+    if (!id) throw new Error("Invalid power state");
+    let clients = (config && Object.keys(config)) || [];
+    if (isEmpty(clients)) throw new Error("No clients found");
+    for (let client of clients) {
+      promises.push(action({ client, on: id === "on" || false }));
+    }
+  } catch (err) {
+    return NextResponse.json({ error: err?.message }, { status: 500 });
+  }
+  return Promise.allSettled(promises)
+    .then((results) => {
+      console.log("results", results);
+      return NextResponse.json(results?.map((o) => o?.value));
+    })
+    .catch((err) => {
+      return NextResponse.json({ error: err?.message }, { status: 500 });
+    });
+}

+ 69 - 80
app/page.js

@@ -1,8 +1,7 @@
 "use client";
 
 import { useEffect, useState } from "react";
-import { Col, Loader, Row } from "tiny-ui";
-import { WLEDClient } from "wled-client";
+import { Col, Row } from "tiny-ui";
 
 import { Automation, Layout, Notification } from "components";
 
@@ -13,106 +12,96 @@ import presets from "data/presets.json";
 export default function Home() {
   const [names, setNames] = useState(null);
   const [clients, setClients] = useState(null);
-  const [connections, setConnections] = useState(null);
+  const [busy, setBusy] = useState(false);
 
   useEffect(() => {
-    let t = [];
     let c = [];
     let n = [];
     for (let client of Object.keys(config)) {
       c.push(client);
       n.push(config[client]);
-      t.push(new WLEDClient(client));
     }
     setClients(c);
-    setConnections(t);
     setNames(n);
   }, []);
 
-  const handleSync = async (payload) => {
-    if (payload?.clients && payload?.value) {
-      for (let client of payload?.clients) {
-        let indx = clients.findIndex((o) => o === client);
-        if (connections[indx]) {
-          let wled = connections[indx];
-          switch (payload?.value) {
-            case "enable":
-              await wled.enableUDPSync({ send: true, receive: false });
-              break;
-            case "disable":
-              await wled.disableUDPSync();
-              break;
-          }
-        }
-      }
-      Notification({
-        title: `Sync ${payload?.value}`,
-        description: `Sync ${
-          payload?.value
-        } complete for ${payload?.clients?.join(", ")}`
+  const handleAutomation = (id) => {
+    setBusy(true);
+    fetch(`/api/automation/${id}`)
+      .then((resp) => resp && resp.json())
+      .then((result) => {
+        setBusy(false);
+        Notification({
+          title: `Automation set to ${id}`,
+          description: (
+            <>
+              {result?.map((o, i) => (
+                <div key={i} style={{ margin: "0 0 1rem 0" }}>
+                  <strong>
+                    {names[i]} ({clients[i]})
+                  </strong>
+                  {o &&
+                    Object.keys(o)?.map((v, k) => (
+                      <div key={k}>
+                        <strong>{v}: </strong> {JSON.stringify(o[v])}
+                      </div>
+                    ))}
+                </div>
+              ))}
+            </>
+          )
+        });
+      })
+      .catch((err) => {
+        setBusy(false);
       });
-    }
-  };
-
-  const handlePower = async (payload) => {
-    if (payload?.clients && payload?.value) {
-      for (let client of payload?.clients) {
-        let indx = clients.findIndex((o) => o === client);
-        if (connections[indx]) {
-          let wled = connections[indx];
-          switch (payload?.value) {
-            case "on":
-              await wled.turnOn();
-              break;
-            case "off":
-              await wled.turnOff();
-              break;
-          }
-        }
-      }
-      Notification({
-        title: `Power ${payload?.value}`,
-        description: `Power ${
-          payload?.value
-        } complete for ${payload?.clients?.join(", ")}`
-      });
-    }
   };
 
-  const handlePreset = async (payload) => {
-    if (payload?.clients && payload?.value) {
-      for (let client of payload?.clients) {
-        let indx = clients.findIndex((o) => o === client);
-        if (connections[indx]) {
-          let wled = connections[indx];
-          await wled.setPreset(payload?.value);
-        }
-      }
-      Notification({
-        title: `Preset set`,
-        description: `Preset ${payload?.value} set on ${payload?.clients?.join(
-          ", "
-        )}`
+  const handlePower = (value) => {
+    setBusy(true);
+    fetch(`/api/power/${value}`)
+      .then((resp) => resp && resp.json())
+      .then((result) => {
+        setBusy(false);
+        Notification({
+          title: `Power set to ${value}`,
+          description: (
+            <>
+              {result?.map((o, i) => (
+                <div key={i} style={{ margin: "0 0 1rem 0" }}>
+                  <strong>
+                    {names[i]} ({clients[i]})
+                  </strong>
+                  {o &&
+                    Object.keys(o)?.map((v, k) => (
+                      <div key={k}>
+                        <strong>{v}: </strong> {JSON.stringify(o[v])}
+                      </div>
+                    ))}
+                </div>
+              ))}
+            </>
+          )
+        });
+      })
+      .catch((err) => {
+        setBusy(false);
       });
-    }
   };
 
   return (
     <Layout page="home">
       <Row style={{ backgroundColor: "#fff" }}>
         <Col style={{ margin: "2rem" }}>
-          {(connections && clients && (
-            <Automation
-              clients={clients}
-              connections={connections}
-              names={names}
-              presets={presets}
-              automation={automation}
-              onPreset={handlePreset}
-              onSync={handleSync}
-              onPower={handlePower}
-            />
-          )) || <Loader size="lg" />}
+          <Automation
+            automation={automation}
+            clients={clients}
+            names={names}
+            presets={presets}
+            onAutomation={handleAutomation}
+            onPower={handlePower}
+            busy={busy}
+          />
         </Col>
       </Row>
     </Layout>

+ 19 - 117
components/Automation.js

@@ -1,46 +1,24 @@
 import { useEffect, useState } from "react";
-import { Button, Collapse, Drawer, Icon, Steps, Typography } from "tiny-ui";
-import WLED from "./WLED.js";
+import { Button, Icon, Steps, Typography } from "tiny-ui";
 
 import { useLocalStorage } from "lib/state";
-import { isBlank } from "lib/utils";
 
 const { Step } = Steps;
-const { Panel } = Collapse;
-const { Heading, Paragraph, Text } = Typography;
-
-const Client = ({ name, client, connection, ...rest }) => {
-  const normalize = (v) => JSON.stringify(v, null, 2);
-  return (
-    <Panel header={name} {...rest}>
-      <p>IP address: {client}</p>
-      <div style={{ overflowY: "scroll", height: 200 }}>
-        {Object.keys(connection?.state)?.map((o, i) => (
-          <div key={i}>
-            <strong>{o}:</strong> {normalize(connection?.state?.[o])}
-          </div>
-        ))}
-      </div>
-    </Panel>
-  );
-};
-const WLEDClient = WLED(Client);
+const { Paragraph } = Typography;
 
 const Automation = ({
-  automation,
-  presets,
   clients,
   names,
-  onPreset,
-  onSync,
+  presets,
+  automation,
+  onAutomation,
   onPower,
-  connections,
+  busy,
   ...props
 }) => {
   const [current, setCurrent] = useLocalStorage("preset", -1, 3600);
   const [total, setTotal] = useState();
   const [help, setHelp] = useState(false);
-  const [info, setInfo] = useState(false);
 
   const getTitle = (indx) => presets?.[indx]?.name || indx;
   const getName = (v) => {
@@ -57,45 +35,15 @@ const Automation = ({
     );
   };
 
-  const handleChange = (indx) => {
-    let keys = Object.keys(automation?.[indx]);
-    let preset = { clients: keys || [], value: indx };
-    let power = { on: [], off: [] };
-    let sync = { enable: [], disable: [] };
-    // loop the keys and determine what to do
-    for (let key of keys) {
-      if (automation?.[indx]?.[key]?.on === true) power.on.push(key);
-      else power.off.push(key);
-      if (automation?.[indx]?.[key]?.sync === true) sync.enable.push(key);
-      else sync.disable.push(key);
-    }
-    if (!isBlank(sync?.enable))
-      onSync({ clients: sync?.enable, value: "enable" }); // enable sync
-    if (!isBlank(sync?.disable))
-      onSync({ clients: sync?.disable, value: "disable" }); // disable sync
-    setTimeout(() => {
-      onPreset(preset); // apply the preset
-    }, 100);
-    setTimeout(() => {
-      if (!isBlank(power?.on)) onPower({ clients: power?.on, value: "on" }); // power on
-      if (!isBlank(power?.off)) onPower({ clients: power?.off, value: "off" }); // power off
-    }, 200);
-  };
-
-  const handlePower = (value) => {
-    let keys = Object.keys(automation?.[1]);
-    onPower({ clients: keys, value: value });
-  };
-
   useEffect(() => {
     setTotal((automation && Object.keys(automation)?.length) || 0);
-  }, []);
+  }, [automation]);
 
   useEffect(() => {
     if (current > -1 && current < total) {
-      handleChange(current + 1);
+      onAutomation(current + 1);
     }
-  }, [current]);
+  }, [current, total]);
 
   let steps = Object.keys(automation).map((o, i) => {
     return (
@@ -111,7 +59,7 @@ const Automation = ({
                   round
                   icon={<Icon name="loader-circle" />}
                   onClick={() => {
-                    handleChange(o);
+                    onAutomation(o);
                   }}
                 />
               </div>
@@ -140,7 +88,7 @@ const Automation = ({
             title="Reapply"
             icon={<Icon name="loader-circle" />}
             onClick={() => {
-              handleChange(current + 1);
+              onAutomation(current + 1);
             }}
           />
           <Button
@@ -157,7 +105,7 @@ const Automation = ({
           <Button
             title="Power on"
             onClick={() => {
-              handlePower("on");
+              onPower("on");
             }}
           >
             On
@@ -165,7 +113,7 @@ const Automation = ({
           <Button
             title="Power off"
             onClick={() => {
-              handlePower("off");
+              onPower("off");
             }}
           >
             Off
@@ -180,15 +128,12 @@ const Automation = ({
             }}
           />
         </Button.Group>
-        <Button.Group size="md" round>
-          <Button
-            title="Information"
-            icon={<Icon name="info" />}
-            onClick={() => {
-              setInfo(true);
-            }}
-          />
-        </Button.Group>
+        {(busy && (
+          <Button.Group size="md" round>
+            <Button title="Busy" icon={<Icon spin name="loader-3quarter" />} />
+          </Button.Group>
+        )) ||
+          null}
       </div>
       <Steps
         current={current}
@@ -197,49 +142,6 @@ const Automation = ({
       >
         {steps}
       </Steps>
-      <Drawer
-        header="Client connections"
-        placement="right"
-        size={480}
-        onClose={() => setInfo(false)}
-        visible={info}
-      >
-        <Collapse accordion bordered={false}>
-          {connections?.map((o, i) => (
-            <WLEDClient
-              key={i}
-              itemKey={i}
-              connection={o}
-              client={clients[i]}
-              name={names[i]}
-            />
-          ))}
-        </Collapse>
-      </Drawer>
-      <Drawer
-        header="Help"
-        placement="right"
-        size={480}
-        onClose={() => setHelp(false)}
-        visible={help}
-      >
-        <>
-          <Paragraph>
-            Select one of the presets from the left to start applying preset
-            automation.
-          </Paragraph>
-          <Paragraph>
-            You can use the navigation buttons above to advance forward{" "}
-            <Icon name="arrow-right" size={16} /> and backward{" "}
-            <Icon name="arrow-left" size={16} /> through the preset list.
-          </Paragraph>
-          <Paragraph>
-            If a preset fails to apply to all of the intended WLED instances
-            then use the Reapply button <Icon name="loader-circle" size={16} />{" "}
-            within the navigation menu bar or within the target step.
-          </Paragraph>
-        </>
-      </Drawer>
     </>
   );
 };

+ 1 - 1
components/Management.js

@@ -147,7 +147,7 @@ const CopyModal = ({ data, client, clients, onConfirm, ...rest }) => {
           ?.filter((o) => o !== client)
           ?.map((o, i) => ({ key: o, label: o }))
       );
-  }, [clients]);
+  }, [clients, client]);
 
   const handleConfirm = (_) => {
     onConfirm(payload);

+ 0 - 1
next.config.js

@@ -4,7 +4,6 @@ const NODE_ENV = process.env.NODE_ENV || "development";
 
 /** @type {import('next').NextConfig} */
 const nextConfig = {
-  // output: "standalone",
   webpack: (config, options) => {
     config.resolve.alias.data = path.resolve(__dirname, `data/${NODE_ENV}`);
     return config;

+ 3 - 0
package.json

@@ -14,6 +14,8 @@
     "lint": "next lint"
   },
   "dependencies": {
+    "bufferutil": "^4.0.7",
+    "encoding": "^0.1.13",
     "eslint": "8.49.0",
     "eslint-config-next": "13.4.19",
     "lodash": "^4.17.21",
@@ -22,6 +24,7 @@
     "react-dom": "18.2.0",
     "styled-components": "^6.0.8",
     "tiny-ui": "^0.0.95",
+    "utf-8-validate": "^6.0.3",
     "wled-client": "^0.22.1"
   },
   "devDependencies": {

+ 38 - 0
yarn.lock

@@ -1557,6 +1557,13 @@ browserslist@^4.21.10, browserslist@^4.21.9:
     node-releases "^2.0.13"
     update-browserslist-db "^1.0.11"
 
+bufferutil@^4.0.7:
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.7.tgz#60c0d19ba2c992dd8273d3f73772ffc894c153ad"
+  integrity sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==
+  dependencies:
+    node-gyp-build "^4.3.0"
+
 busboy@1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
@@ -1794,6 +1801,13 @@ emoji-regex@^9.2.2:
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
   integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
 
+encoding@^0.1.13:
+  version "0.1.13"
+  resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
+  integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==
+  dependencies:
+    iconv-lite "^0.6.2"
+
 enhanced-resolve@^5.12.0:
   version "5.15.0"
   resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35"
@@ -2395,6 +2409,13 @@ has@^1.0.3:
   dependencies:
     function-bind "^1.1.1"
 
+iconv-lite@^0.6.2:
+  version "0.6.3"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
+  integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3.0.0"
+
 ignore@^5.2.0:
   version "5.2.4"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
@@ -2855,6 +2876,11 @@ node-fetch@^2.6.1:
   dependencies:
     whatwg-url "^5.0.0"
 
+node-gyp-build@^4.3.0:
+  version "4.6.1"
+  resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.1.tgz#24b6d075e5e391b8d5539d98c7fc5c210cac8a3e"
+  integrity sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==
+
 node-releases@^2.0.13:
   version "2.0.13"
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
@@ -3241,6 +3267,11 @@ safe-regex-test@^1.0.0:
     get-intrinsic "^1.1.3"
     is-regex "^1.1.4"
 
+"safer-buffer@>= 2.1.2 < 3.0.0":
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+  integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
 sass@^1.67.0:
   version "1.67.0"
   resolved "https://registry.yarnpkg.com/sass/-/sass-1.67.0.tgz#fed84d74b9cd708db603b1380d6dc1f71bb24f6f"
@@ -3602,6 +3633,13 @@ uri-js@^4.2.2:
   dependencies:
     punycode "^2.1.0"
 
+utf-8-validate@^6.0.3:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-6.0.3.tgz#7d8c936d854e86b24d1d655f138ee27d2636d777"
+  integrity sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==
+  dependencies:
+    node-gyp-build "^4.3.0"
+
 watchpack@2.4.0:
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"