Philips Hue Remote Control

/**
 * Questmate Custom Component - Philips Hue Remote Control (Questscript)
 *
 * Permissions:
 * @UseApp {HUE}
 *
 * Changelog:
 * v0.1 Initial release
 * v0.2 Converted Custom V2 component
 * v0.3 Added support for scenes and selecting multiple lights instead of just one
 *
 */

return defineCustomItem((Questmate) => {
  Questmate.registerView("ITEM_RUN_VIEW", async ({ useConfigData }) => {
    const [allowedScenes] = useConfigData("allowedScenes", []);
    const [allowedLights] = useConfigData("allowedLights", []);

    const availableLights = await lightsDataSource.retrieve();
    const availableScenes = await scenesDataSource.retrieve();

    return {
      components:
        availableLights.length === 0 && availableScenes.length === 0
          ? [
              {
                id: "loading-text",
                type: "text",
                content: " ",
              },
            ]
          : [
              ...availableLights
                .filter((light) => allowedLights.includes(light.id))
                .map((light) => {
                  return {
                    id: `light-${light.id}`,
                    type: "switch",
                    title: `💡 ${light.name}`,
                    value: light.data.on,
                    onSwitch: async (switchOn) => {
                      const lightResponse = await fetch(
                        `https://api.meethue.com/route/clip/v2/resource/light/${light.id}`,
                        {
                          method: "PUT",
                          headers: {
                            "Content-Type": "application/json",
                          },
                          body: JSON.stringify({ on: { on: switchOn } }),
                        }
                      );

                      const lightResponseData = await lightResponse.json();
                      console.log(
                        "lightResponseData",
                        JSON.stringify(lightResponseData, null, 2)
                      );
                      lightsDataSource.markResultsAsStale();
                    },
                  };
                }),
              ...availableScenes
                .filter((scene) => allowedScenes.includes(scene.id))
                .map((scene) => ({
                  id: `scene-${scene.id}`,
                  type: "button",
                  title: `🖼️ ${scene.name}`,
                  buttonLabel: "Activate",
                  onPress: async () => {
                    const sceneResponse = await fetch(
                      `https://api.meethue.com/route/clip/v2/resource/scene/${scene.id}`,
                      {
                        method: "PUT",
                        headers: {
                          "Content-Type": "application/json",
                        },
                        body: JSON.stringify({
                          recall: {
                            action: "active",
                          },
                        }),
                      }
                    );

                    const sceneResponseData = await sceneResponse.json();
                    console.log(
                      "sceneResponseData",
                      JSON.stringify(sceneResponseData, null, 2)
                    );
                  },
                })),
            ],
    };
  });

  Questmate.registerView("ITEM_CONFIG_VIEW", async ({ useConfigData }) => {
    const [allowedScenes, setAllowedScenes] = useConfigData(
      "allowedScenes",
      []
    );

    const [allowedLights, setAllowedLights] = useConfigData(
      "allowedLights",
      []
    );

    const availableScenes = await scenesDataSource.retrieve();
    const availableLights = await lightsDataSource.retrieve();

    return {
      components: [
        {
          id: "allowedLights",
          type: "text",
          content: "Allowed Lights",
        },
        ...availableLights.map((light) => ({
          id: `light-${light.id}`,
          type: "switch",
          title: light.name,
          value: allowedLights.includes(light.id),
          onSwitch: async (switchOn) => {
            if (switchOn) {
              setAllowedLights([...allowedLights, light.id]);
            } else {
              setAllowedLights(
                allowedLights.filter(
                  (allowedLight) => allowedLight !== light.id
                )
              );
            }
          },
        })),
        {
          id: "allowedScenes",
          type: "text",
          content: "Allowed Scenes",
        },
        ...availableScenes.map((scene) => ({
          id: `scene-${scene.id}`,
          type: "switch",
          title: scene.name,
          value: allowedScenes.includes(scene.id),
          onSwitch: async (switchOn) => {
            if (switchOn) {
              setAllowedScenes([...allowedScenes, scene.id]);
            } else {
              setAllowedScenes(
                allowedScenes.filter(
                  (allowedScene) => allowedScene !== scene.id
                )
              );
            }
          },
        })),
      ],
    };
  });

  const scenesDataSource = Questmate.registerDataSource({
    id: "scenes",
    initialData: [],
    refreshInterval: 120,
    fetcher: () => async () => {
      const sceneListResponse = await fetch(
        "https://api.meethue.com/route/clip/v2/resource/scene"
      );
      const { data: scenesData } = await sceneListResponse.json();
      const scenes = scenesData.map((scene) => ({
        name: scene.metadata.name,
        id: scene.id,
      }));

      return {
        data: scenes,
      };
    },
  });

  const lightsDataSource = Questmate.registerDataSource({
    id: "lights",
    initialData: [],
    refreshInterval: 120,
    aggregate: ({ results, staleResults }) => {
      const latestFreshResult = results[results.length - 1];
      if (latestFreshResult) {
        return latestFreshResult;
      } else {
        return staleResults[staleResults.length - 1];
      }
    },
    fetcher: () => async () => {
      const lightListResponse = await fetch(
        "https://api.meethue.com/route/clip/v2/resource/light"
      );
      const { data: lightsData } = await lightListResponse.json();
      const lights = lightsData.map((light) => ({
        name: light.metadata.name,
        id: light.id,
        data: {
          on: light.on.on,
        },
      }));
      return {
        data: lights,
      };
    },
  });
});