commit 61e88054eb86eaa167ac8141d6bf35eaad6b375a Author: Alan Date: Sun Mar 15 15:36:27 2026 +1100 init composition of async path of actions is a little silly, will hopefully get around to a nicer way to do it. sunset is still todo, moving to build & deployment first. diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efa47ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +.direnv +src/*.js diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..1ef0dc8 --- /dev/null +++ b/flake.lock @@ -0,0 +1,23 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 0, + "narHash": "sha256-yf3iYLGbGVlIthlQIk5/4/EQDZNNEmuqKZkQssMljuw=", + "path": "/nix/store/arylzmnn080w2i8hi0x45pgkd3mmp53r-source", + "type": "path" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..73a50da --- /dev/null +++ b/flake.nix @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: Unlicense +{ + inputs = { + # nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + # Support a particular subset of the Nix systems + # systems.url = "github:nix-systems/default"; + }; + + outputs = + { nixpkgs, ... }: + let + eachSystem = + f: + nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed (system: f nixpkgs.legacyPackages.${system}); + in + { + devShells = eachSystem (pkgs: { + default = pkgs.mkShell { + packages = [ + pkgs.nodejs + + # Alternatively, you can use a specific major version of Node.js + + # pkgs.nodejs-22_x + + # Use corepack to install npm/pnpm/yarn as specified in package.json + pkgs.corepack + + # To install a specific alternative package manager directly, + # comment out one of these to use an alternative package manager. + + # pkgs.yarn + # pkgs.pnpm + # pkgs.bun + + # Required to enable the language server + pkgs.nodePackages.typescript + pkgs.nodePackages.typescript-language-server + + # Python is required on NixOS if the dependencies require node-gyp + + # pkgs.python3 + ]; + }; + }); + }; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..88ddb0e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,272 @@ +{ + "name": "lifx-scripts", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lifx-scripts", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "argparse-ts": "^0.9.0", + "eventemitter3": "^5.0.1", + "lifx-lan-client": "^2.1.2" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse-ts": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/argparse-ts/-/argparse-ts-0.9.0.tgz", + "integrity": "sha512-2WLkNkbGbh0OVpcLDOFa59WkCllwiHyrrCMtFIxhldHfLj8ng6ms3HQJGRTSYNtOULrsvIeyj0Btmj2d47xrXg==", + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/lifx-lan-client": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/lifx-lan-client/-/lifx-lan-client-2.1.2.tgz", + "integrity": "sha512-SNda50VkeccMxsW0B+TVhW/pzVWByyR4fJjYCVzTJDWv4uO1bCJOM9PhHBBHHurvcAXwTL6OmeC3d2X6JzXFcg==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "lodash": "^4.17.15" + }, + "engines": { + "node": ">=8.10" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6b733f5 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "lifx-scripts", + "version": "0.0.1", + "description": "Simple LIFX lights automation scripts", + "main": "src/main.ts", + "scripts": { + "prepare": "tsc -p .", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Alan", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "argparse-ts": "^0.9.0", + "lifx-lan-client": "^2.1.2" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..75cd4b7 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,136 @@ +import { EventEmitter } from 'eventemitter3' +import { LifxClient, LLight, LColor, LState } from './overrides' +import { ArgsParser } from "argparse-ts"; + +const client = new LifxClient(); +const emitter = ((client as unknown) as EventEmitter); + +const parser = new ArgsParser({ + name: "lifx-scripts", + version: '0.0.1', +}, [ + { + name: 'command', + description: 'Command to run', + type: 'string', + choices: ['list', 'sunrise', 'sunset'], + }, + { + name: '--status', + description: 'filter results by status', + type: 'string', + choices: ['', 'on', 'off'], + default: '', + nargs: '?', + }, + { + name: '--maxwait', + description: 'max time in ms to wait for all addresses to respond', + type: 'number', + default: 3000, + nargs: '?', + }, + { + name: '--addresses', + alias: '-a', + description: 'known ipv4 addresses to check', + type: 'string', + nargs: '*', + } +]); + +const args = parser.parse(process.argv.slice(2)); + +// this is probably dumb +// do we need to worry about mutexes in js? +var waiting = 0; + +const sunrise1 = (light: LLight) => { + const next = sunrise2(light); + light.color( + 0, // hue + 0, // saturation + 1, // brightness + 2500, // Kelvin + 0, // instantly + next, + ) +}; +const sunrise2 = (light: LLight) => { + const next = sunrise3(light); + return (error: any | null) => { + if (error !== null) { + next(error); + } else { + light.on(0, next) + } + }; +}; +const sunrise3 = (light: LLight) => { + const next = sunrise4(light); + return (error: any | null) => { + if (error !== null) { + console.error(error, light.id); + // presumably just drop... + } else { + light.color( + 0, // hue + 0, // saturation + 100, // brightness + 3300, // Kelvin + // 5 * 60 * 1000, // 5 minutes in ms + 3000, + next, + ) + } + } +} +const sunrise4 = (light: LLight) => { + waiting += 1; + return (error: any | null) => { + if (error !== null) { + console.error(error, light.id); + // presumably just drop... + } else { + console.log("Done!"); + } + waiting -= 1; + } +} + +emitter.on('discovery-stop', () => { + switch (args.positional.command) { + case 'list': + for (const [_, value] of Object.entries(client.lights(args.options.status))) { + console.log(`${(value as LLight).id}: ${(value as LLight).label}`); + } + break; + case 'sunrise': + for (const [_, value] of Object.entries(client.lights(args.options.status))) { + sunrise1(value as LLight); + } + break; + case 'sunset': + console.warn('todo'); + break; + } + + const waiter = () => { + if (waiting > 0) { + setTimeout(waiter, 100); + } else { + client.destroy(); + } + }; + waiter(); +}); + +client.init({ + lights: args.options.addresses, + stopAfterDiscovery: true, + discoveryInterval: 250, +}, () => { + setTimeout(() => { + client.stopDiscovery(); + }, (args.options.maxwait as number)) +}); diff --git a/src/overrides.ts b/src/overrides.ts new file mode 100644 index 0000000..0dee965 --- /dev/null +++ b/src/overrides.ts @@ -0,0 +1,51 @@ +/** + * Because Nothing can be perfect +**/ + +import { Client, Light } from 'lifx-lan-client'; +import { EventEmitter } from 'eventemitter3' + +class LifxClient extends Client { + lightAddresses!: Array; + + stopDiscovery() { + super.stopDiscovery(); + ((this as unknown) as EventEmitter).emit('discovery-stop'); + } + + startDiscovery(lights: any) { + super.startDiscovery(lights); + ((this as unknown) as EventEmitter).emit('discovery-start'); + } + + destroy(): void { + ((this as unknown) as EventEmitter).removeAllListeners(); + super.destroy(); + } +}; + +class LLight extends Light { + id!: string; + label!: string; + address!: string; +}; + +interface LColor { + hue: number, + saturation: number, + brightness: number, + kelvin: number, +} + +interface LState { + color: LColor, + power: number, + label: string, +} + +export { + LifxClient, + LLight, + LColor, + LState, +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c23a6c0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "skipLibCheck": true, + "module": "commonjs", + "target": "esnext", + "strict": true + } +}