diff --git a/README.md b/README.md index 3c39931..26d82e5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # Logitech Litra -This JavaScript driver allows you to control USB-connected [Logitech Litra Glow](https://www.logitech.com/en-gb/products/lighting/litra-glow.946-000002.html) and [Logitech Litra Beam](https://www.logitech.com/en-gb/products/lighting/litra-beam.946-000007.html) lights using a CLI and from your JavaScript code. +This driver allows you to control USB-connected Logitech Litra lights using a CLI or from JavaScript code. + +The following Logitech Litra devices are supported: + +* [Logitech Litra Glow](https://www.logitech.com/en-gb/products/lighting/litra-glow.946-000002.html) +* [Logitech Litra Beam](https://www.logitech.com/en-gb/products/lighting/litra-beam.946-000007.html) +* [Logitech Litra Beam LX](https://www.logitechg.com/en-gb/products/cameras-lighting/litra-beam-lx-led-light.946-000015.html?&utm_source=Google&utm_medium=Paid-Search&utm_campaign=Dialect_FY24_Q3_GBR_GA_G_DTX-LogiG-Creator_Google_na&gad_source=1&gclid=CjwKCAiAp5qsBhAPEiwAP0qeJs7jOdlBu8DCsEoOFt1_BK1HLABI0l2jglDweTnNDddt5neXm_vpyRoCic4QAvD_BwE) With this driver, you can: @@ -13,7 +19,7 @@ With this driver, you can: This library: -* only works with Litra devices connected via USB. Logitech Litra Beam devices connected via Bluetooth are not supported. +* only works with Litra devices connected via USB. Devices connected via Bluetooth are not supported. * is only tested on macOS Monterey (12.5) and Windows 11. It's powered by [`node-hid`](https://github.com/node-hid/node-hid), which is compatible with other macOS versions, Windows and Linux, so it would be expected to work there too, but your mileage may vary 🙏 ## Using as a command line tool @@ -135,7 +141,7 @@ You can set the brightness of your Litra device, measured in Lumen, using the `s To get the current brightness of your device, use the `getBrightnessInLumen` function. -The Litra Glow supports brightness between 20 and 250 Lumen. The Litra Beam supports brightness between 20 and 400 Lumen. +The Litra Glow supports brightness between 20 and 250 Lumen. The Litra Beam and Litra Beam LX support brightness between 20 and 400 Lumen. You can programatically check what brightness levels are supported by your device. Once you know what brightness levels are supported, you can set the brightness in Lumen. If you try to set a value that isn't allowed by your device, an error will be thrown: @@ -182,7 +188,7 @@ You can set the temperature of your Litra device, measured in Kelvin, using the The `getTemperatureInKelvin` function can be used to get the current temperature your device is set to. -Both the Litra Glow and Litra Beam support temperatures which are multiples of 100 between 2700 and 6500 Kelvin (i.e.. 2700, 2800, 2900, etc.). +All supported Litra devices support temperatures which are multiples of 100 between 2700 and 6500 Kelvin (i.e.. 2700, 2800, 2900, etc.). You can check programatically what temperature levels are supported by your device. Once you know what temperature levels are supported, you can set the temperature in Kelvin. If you try to set a value that isn't allowed by your device, an error will be thrown: diff --git a/dist/commonjs/driver.d.ts b/dist/commonjs/driver.d.ts index 7b0eb46..51a779e 100644 --- a/dist/commonjs/driver.d.ts +++ b/dist/commonjs/driver.d.ts @@ -1,7 +1,8 @@ /// export declare enum DeviceType { LitraGlow = "litra_glow", - LitraBeam = "litra_beam" + LitraBeam = "litra_beam", + LitraBeamLX = "litra_beam_lx" } export interface Device { hid: { diff --git a/dist/commonjs/driver.js b/dist/commonjs/driver.js index 9372067..e5f4196 100644 --- a/dist/commonjs/driver.js +++ b/dist/commonjs/driver.js @@ -10,30 +10,36 @@ var DeviceType; (function (DeviceType) { DeviceType["LitraGlow"] = "litra_glow"; DeviceType["LitraBeam"] = "litra_beam"; + DeviceType["LitraBeamLX"] = "litra_beam_lx"; })(DeviceType || (exports.DeviceType = DeviceType = {})); const VENDOR_ID = 0x046d; const PRODUCT_IDS = [ 0xc900, 0xc901, - 0xb901, // Litra Beam + 0xb901, + 0xc903, // Litra Beam LX ]; const USAGE_PAGE = 0xff43; const MINIMUM_BRIGHTNESS_IN_LUMEN_BY_DEVICE_TYPE = { [DeviceType.LitraGlow]: 20, [DeviceType.LitraBeam]: 30, + [DeviceType.LitraBeamLX]: 30, }; const MAXIMUM_BRIGHTNESS_IN_LUMEN_BY_DEVICE_TYPE = { [DeviceType.LitraGlow]: 250, [DeviceType.LitraBeam]: 400, + [DeviceType.LitraBeamLX]: 400, }; const MULTIPLES_OF_100_BETWEEN_2700_AND_6500 = (0, utils_1.multiplesWithinRange)(100, 2700, 6500); const ALLOWED_TEMPERATURES_IN_KELVIN_BY_DEVICE_TYPE = { [DeviceType.LitraGlow]: MULTIPLES_OF_100_BETWEEN_2700_AND_6500, [DeviceType.LitraBeam]: MULTIPLES_OF_100_BETWEEN_2700_AND_6500, + [DeviceType.LitraBeamLX]: MULTIPLES_OF_100_BETWEEN_2700_AND_6500, }; const NAME_BY_DEVICE_TYPE = { [DeviceType.LitraGlow]: 'Logitech Litra Glow', [DeviceType.LitraBeam]: 'Logitech Litra Beam', + [DeviceType.LitraBeamLX]: 'Logitech Litra Beam LX', }; const isLitraDevice = (device) => { return (device.vendorId === VENDOR_ID && @@ -79,22 +85,40 @@ const findDevices = () => { return matchingDevices.map(hidDeviceToDevice); }; exports.findDevices = findDevices; +const generateTurnOnBytes = (device) => { + if (device.type === DeviceType.LitraBeamLX) { + return (0, utils_1.padRight)([0x11, 0xff, 0x06, 0x1c, 0x01], 20, 0x00); + } + else { + return (0, utils_1.padRight)([0x11, 0xff, 0x04, 0x1c, 0x01], 20, 0x00); + } +}; /** * Turns your Logitech Litra device on * * @param {Device} device The device to turn on */ const turnOn = (device) => { - device.hid.write((0, utils_1.padRight)([0x11, 0xff, 0x04, 0x1c, 0x01], 20, 0x00)); + const bytes = generateTurnOnBytes(device); + device.hid.write(bytes); }; exports.turnOn = turnOn; +const generateTurnOffBytes = (device) => { + if (device.type === DeviceType.LitraBeamLX) { + return (0, utils_1.padRight)([0x11, 0xff, 0x06, 0x1c, 0x00], 20, 0x00); + } + else { + return (0, utils_1.padRight)([0x11, 0xff, 0x04, 0x1c, 0x00], 20, 0x00); + } +}; /** * Turns your Logitech Litra device off * * @param {Device} device The device to turn off */ const turnOff = (device) => { - device.hid.write((0, utils_1.padRight)([0x11, 0xff, 0x04, 0x1c, 0x00], 20, 0x00)); + const bytes = generateTurnOffBytes(device); + device.hid.write(bytes); }; exports.turnOff = turnOff; /** @@ -111,6 +135,14 @@ const toggle = (device) => { } }; exports.toggle = toggle; +const generateIsOnBytes = (device) => { + if (device.type === DeviceType.LitraBeamLX) { + return (0, utils_1.padRight)([0x11, 0xff, 0x06, 0x01], 20, 0x00); + } + else { + return (0, utils_1.padRight)([0x11, 0xff, 0x04, 0x01], 20, 0x00); + } +}; /** * Gets the current power state of your Logitech Litra device * @@ -118,11 +150,20 @@ exports.toggle = toggle; * @returns {boolean} Current power state where true = on and false = off */ const isOn = (device) => { - device.hid.write((0, utils_1.padRight)([0x11, 0xff, 0x04, 0x01], 20, 0x00)); + const bytes = generateIsOnBytes(device); + device.hid.write(bytes); const data = device.hid.readSync(); return data[4] === 1; }; exports.isOn = isOn; +const generateSetTemperatureInKelvinBytes = (device, temperatureInKelvin) => { + if (device.type === DeviceType.LitraBeamLX) { + return (0, utils_1.padRight)([0x11, 0xff, 0x06, 0x9c, ...(0, utils_1.integerToBytes)(temperatureInKelvin)], 20, 0x00); + } + else { + return (0, utils_1.padRight)([0x11, 0xff, 0x04, 0x9c, ...(0, utils_1.integerToBytes)(temperatureInKelvin)], 20, 0x00); + } +}; /** * Sets the temperature of your Logitech Litra device * @@ -143,9 +184,18 @@ const setTemperatureInKelvin = (device, temperatureInKelvin) => { if (!allowedTemperatures.includes(temperatureInKelvin)) { throw `Provided temperature must be a multiple of 100 between ${minimumTemperature} and ${maximumTemperature} for this device`; } - device.hid.write((0, utils_1.padRight)([0x11, 0xff, 0x04, 0x9c, ...(0, utils_1.integerToBytes)(temperatureInKelvin)], 20, 0x00)); + const bytes = generateSetTemperatureInKelvinBytes(device, temperatureInKelvin); + device.hid.write(bytes); }; exports.setTemperatureInKelvin = setTemperatureInKelvin; +const generateGetTemperatureInKelvinBytes = (device) => { + if (device.type === DeviceType.LitraBeamLX) { + return (0, utils_1.padRight)([0x11, 0xff, 0x06, 0x81], 20, 0x00); + } + else { + return (0, utils_1.padRight)([0x11, 0xff, 0x04, 0x81], 20, 0x00); + } +}; /** * Gets the temperature of your Logitech Litra device * @@ -153,7 +203,8 @@ exports.setTemperatureInKelvin = setTemperatureInKelvin; * @returns {number} The current temperature in Kelvin */ const getTemperatureInKelvin = (device) => { - device.hid.write((0, utils_1.padRight)([0x11, 0xff, 0x04, 0x81], 20, 0x00)); + const bytes = generateGetTemperatureInKelvinBytes(device); + device.hid.write(bytes); const data = device.hid.readSync(); // data[4] is the multiple of 256 // data[5] is the remainder of 256 @@ -161,6 +212,14 @@ const getTemperatureInKelvin = (device) => { return data[4] * 256 + data[5]; }; exports.getTemperatureInKelvin = getTemperatureInKelvin; +const generateSetBrightnessInLumenBytes = (device, brightnessInLumen) => { + if (device.type === DeviceType.LitraBeamLX) { + return (0, utils_1.padRight)([0x11, 0xff, 0x06, 0x4c, ...(0, utils_1.integerToBytes)(brightnessInLumen)], 20, 0x00); + } + else { + return (0, utils_1.padRight)([0x11, 0xff, 0x04, 0x4c, ...(0, utils_1.integerToBytes)(brightnessInLumen)], 20, 0x00); + } +}; /** * Sets the brightness of your Logitech Litra device, measured in Lumen * @@ -178,9 +237,18 @@ const setBrightnessInLumen = (device, brightnessInLumen) => { if (brightnessInLumen < minimumBrightness || brightnessInLumen > maximumBrightness) { throw `Provided brightness must be between ${minimumBrightness} and ${maximumBrightness} for this device`; } - device.hid.write((0, utils_1.padRight)([0x11, 0xff, 0x04, 0x4c, ...(0, utils_1.integerToBytes)(brightnessInLumen)], 20, 0x00)); + const bytes = generateSetBrightnessInLumenBytes(device, brightnessInLumen); + device.hid.write(bytes); }; exports.setBrightnessInLumen = setBrightnessInLumen; +const generateGetBrightnessInLumenBytes = (device) => { + if (device.type === DeviceType.LitraBeamLX) { + return (0, utils_1.padRight)([0x11, 0xff, 0x06, 0x31], 20, 0x00); + } + else { + return (0, utils_1.padRight)([0x11, 0xff, 0x04, 0x31], 20, 0x00); + } +}; /** * Gets the current brightness of your Logitech Litra device, measured in Lumen * @@ -188,7 +256,8 @@ exports.setBrightnessInLumen = setBrightnessInLumen; * @returns {number} The current brightness in Lumen */ const getBrightnessInLumen = (device) => { - device.hid.write((0, utils_1.padRight)([0x11, 0xff, 0x04, 0x31], 20, 0x00)); + const bytes = generateGetBrightnessInLumenBytes(device); + device.hid.write(bytes); const data = device.hid.readSync(); return data[5]; }; @@ -224,6 +293,8 @@ const getDeviceTypeByProductId = (productId) => { case PRODUCT_IDS[1]: case PRODUCT_IDS[2]: return DeviceType.LitraBeam; + case PRODUCT_IDS[3]: + return DeviceType.LitraBeamLX; default: throw 'Unknown device type'; } diff --git a/dist/esm/driver.d.ts b/dist/esm/driver.d.ts index 7b0eb46..51a779e 100644 --- a/dist/esm/driver.d.ts +++ b/dist/esm/driver.d.ts @@ -1,7 +1,8 @@ /// export declare enum DeviceType { LitraGlow = "litra_glow", - LitraBeam = "litra_beam" + LitraBeam = "litra_beam", + LitraBeamLX = "litra_beam_lx" } export interface Device { hid: { diff --git a/dist/esm/driver.js b/dist/esm/driver.js index d77f25c..5c41913 100644 --- a/dist/esm/driver.js +++ b/dist/esm/driver.js @@ -4,30 +4,36 @@ export var DeviceType; (function (DeviceType) { DeviceType["LitraGlow"] = "litra_glow"; DeviceType["LitraBeam"] = "litra_beam"; + DeviceType["LitraBeamLX"] = "litra_beam_lx"; })(DeviceType || (DeviceType = {})); const VENDOR_ID = 0x046d; const PRODUCT_IDS = [ 0xc900, 0xc901, - 0xb901, // Litra Beam + 0xb901, + 0xc903, // Litra Beam LX ]; const USAGE_PAGE = 0xff43; const MINIMUM_BRIGHTNESS_IN_LUMEN_BY_DEVICE_TYPE = { [DeviceType.LitraGlow]: 20, [DeviceType.LitraBeam]: 30, + [DeviceType.LitraBeamLX]: 30, }; const MAXIMUM_BRIGHTNESS_IN_LUMEN_BY_DEVICE_TYPE = { [DeviceType.LitraGlow]: 250, [DeviceType.LitraBeam]: 400, + [DeviceType.LitraBeamLX]: 400, }; const MULTIPLES_OF_100_BETWEEN_2700_AND_6500 = multiplesWithinRange(100, 2700, 6500); const ALLOWED_TEMPERATURES_IN_KELVIN_BY_DEVICE_TYPE = { [DeviceType.LitraGlow]: MULTIPLES_OF_100_BETWEEN_2700_AND_6500, [DeviceType.LitraBeam]: MULTIPLES_OF_100_BETWEEN_2700_AND_6500, + [DeviceType.LitraBeamLX]: MULTIPLES_OF_100_BETWEEN_2700_AND_6500, }; const NAME_BY_DEVICE_TYPE = { [DeviceType.LitraGlow]: 'Logitech Litra Glow', [DeviceType.LitraBeam]: 'Logitech Litra Beam', + [DeviceType.LitraBeamLX]: 'Logitech Litra Beam LX', }; const isLitraDevice = (device) => { return (device.vendorId === VENDOR_ID && @@ -71,13 +77,30 @@ export const findDevices = () => { const matchingDevices = HID.devices().filter(isLitraDevice); return matchingDevices.map(hidDeviceToDevice); }; +const generateTurnOnBytes = (device) => { + if (device.type === DeviceType.LitraBeamLX) { + return padRight([0x11, 0xff, 0x06, 0x1c, 0x01], 20, 0x00); + } + else { + return padRight([0x11, 0xff, 0x04, 0x1c, 0x01], 20, 0x00); + } +}; /** * Turns your Logitech Litra device on * * @param {Device} device The device to turn on */ export const turnOn = (device) => { - device.hid.write(padRight([0x11, 0xff, 0x04, 0x1c, 0x01], 20, 0x00)); + const bytes = generateTurnOnBytes(device); + device.hid.write(bytes); +}; +const generateTurnOffBytes = (device) => { + if (device.type === DeviceType.LitraBeamLX) { + return padRight([0x11, 0xff, 0x06, 0x1c, 0x00], 20, 0x00); + } + else { + return padRight([0x11, 0xff, 0x04, 0x1c, 0x00], 20, 0x00); + } }; /** * Turns your Logitech Litra device off @@ -85,7 +108,8 @@ export const turnOn = (device) => { * @param {Device} device The device to turn off */ export const turnOff = (device) => { - device.hid.write(padRight([0x11, 0xff, 0x04, 0x1c, 0x00], 20, 0x00)); + const bytes = generateTurnOffBytes(device); + device.hid.write(bytes); }; /** * Toggles your Logitech Litra device on or off @@ -100,6 +124,14 @@ export const toggle = (device) => { turnOn(device); } }; +const generateIsOnBytes = (device) => { + if (device.type === DeviceType.LitraBeamLX) { + return padRight([0x11, 0xff, 0x06, 0x01], 20, 0x00); + } + else { + return padRight([0x11, 0xff, 0x04, 0x01], 20, 0x00); + } +}; /** * Gets the current power state of your Logitech Litra device * @@ -107,10 +139,19 @@ export const toggle = (device) => { * @returns {boolean} Current power state where true = on and false = off */ export const isOn = (device) => { - device.hid.write(padRight([0x11, 0xff, 0x04, 0x01], 20, 0x00)); + const bytes = generateIsOnBytes(device); + device.hid.write(bytes); const data = device.hid.readSync(); return data[4] === 1; }; +const generateSetTemperatureInKelvinBytes = (device, temperatureInKelvin) => { + if (device.type === DeviceType.LitraBeamLX) { + return padRight([0x11, 0xff, 0x06, 0x9c, ...integerToBytes(temperatureInKelvin)], 20, 0x00); + } + else { + return padRight([0x11, 0xff, 0x04, 0x9c, ...integerToBytes(temperatureInKelvin)], 20, 0x00); + } +}; /** * Sets the temperature of your Logitech Litra device * @@ -131,7 +172,16 @@ export const setTemperatureInKelvin = (device, temperatureInKelvin) => { if (!allowedTemperatures.includes(temperatureInKelvin)) { throw `Provided temperature must be a multiple of 100 between ${minimumTemperature} and ${maximumTemperature} for this device`; } - device.hid.write(padRight([0x11, 0xff, 0x04, 0x9c, ...integerToBytes(temperatureInKelvin)], 20, 0x00)); + const bytes = generateSetTemperatureInKelvinBytes(device, temperatureInKelvin); + device.hid.write(bytes); +}; +const generateGetTemperatureInKelvinBytes = (device) => { + if (device.type === DeviceType.LitraBeamLX) { + return padRight([0x11, 0xff, 0x06, 0x81], 20, 0x00); + } + else { + return padRight([0x11, 0xff, 0x04, 0x81], 20, 0x00); + } }; /** * Gets the temperature of your Logitech Litra device @@ -140,13 +190,22 @@ export const setTemperatureInKelvin = (device, temperatureInKelvin) => { * @returns {number} The current temperature in Kelvin */ export const getTemperatureInKelvin = (device) => { - device.hid.write(padRight([0x11, 0xff, 0x04, 0x81], 20, 0x00)); + const bytes = generateGetTemperatureInKelvinBytes(device); + device.hid.write(bytes); const data = device.hid.readSync(); // data[4] is the multiple of 256 // data[5] is the remainder of 256 // together they come out to the temp in K return data[4] * 256 + data[5]; }; +const generateSetBrightnessInLumenBytes = (device, brightnessInLumen) => { + if (device.type === DeviceType.LitraBeamLX) { + return padRight([0x11, 0xff, 0x06, 0x4c, ...integerToBytes(brightnessInLumen)], 20, 0x00); + } + else { + return padRight([0x11, 0xff, 0x04, 0x4c, ...integerToBytes(brightnessInLumen)], 20, 0x00); + } +}; /** * Sets the brightness of your Logitech Litra device, measured in Lumen * @@ -164,7 +223,16 @@ export const setBrightnessInLumen = (device, brightnessInLumen) => { if (brightnessInLumen < minimumBrightness || brightnessInLumen > maximumBrightness) { throw `Provided brightness must be between ${minimumBrightness} and ${maximumBrightness} for this device`; } - device.hid.write(padRight([0x11, 0xff, 0x04, 0x4c, ...integerToBytes(brightnessInLumen)], 20, 0x00)); + const bytes = generateSetBrightnessInLumenBytes(device, brightnessInLumen); + device.hid.write(bytes); +}; +const generateGetBrightnessInLumenBytes = (device) => { + if (device.type === DeviceType.LitraBeamLX) { + return padRight([0x11, 0xff, 0x06, 0x31], 20, 0x00); + } + else { + return padRight([0x11, 0xff, 0x04, 0x31], 20, 0x00); + } }; /** * Gets the current brightness of your Logitech Litra device, measured in Lumen @@ -173,7 +241,8 @@ export const setBrightnessInLumen = (device, brightnessInLumen) => { * @returns {number} The current brightness in Lumen */ export const getBrightnessInLumen = (device) => { - device.hid.write(padRight([0x11, 0xff, 0x04, 0x31], 20, 0x00)); + const bytes = generateGetBrightnessInLumenBytes(device); + device.hid.write(bytes); const data = device.hid.readSync(); return data[5]; }; @@ -207,6 +276,8 @@ const getDeviceTypeByProductId = (productId) => { case PRODUCT_IDS[1]: case PRODUCT_IDS[2]: return DeviceType.LitraBeam; + case PRODUCT_IDS[3]: + return DeviceType.LitraBeamLX; default: throw 'Unknown device type'; } diff --git a/src/driver.ts b/src/driver.ts index 28f0102..f66535c 100644 --- a/src/driver.ts +++ b/src/driver.ts @@ -10,6 +10,7 @@ import { export enum DeviceType { LitraGlow = 'litra_glow', LitraBeam = 'litra_beam', + LitraBeamLX = 'litra_beam_lx', } const VENDOR_ID = 0x046d; @@ -17,17 +18,20 @@ const PRODUCT_IDS = [ 0xc900, // Litra Glow 0xc901, // Litra Beam 0xb901, // Litra Beam + 0xc903, // Litra Beam LX ]; const USAGE_PAGE = 0xff43; const MINIMUM_BRIGHTNESS_IN_LUMEN_BY_DEVICE_TYPE = { [DeviceType.LitraGlow]: 20, [DeviceType.LitraBeam]: 30, + [DeviceType.LitraBeamLX]: 30, }; const MAXIMUM_BRIGHTNESS_IN_LUMEN_BY_DEVICE_TYPE = { [DeviceType.LitraGlow]: 250, [DeviceType.LitraBeam]: 400, + [DeviceType.LitraBeamLX]: 400, }; const MULTIPLES_OF_100_BETWEEN_2700_AND_6500 = multiplesWithinRange(100, 2700, 6500); @@ -35,11 +39,13 @@ const MULTIPLES_OF_100_BETWEEN_2700_AND_6500 = multiplesWithinRange(100, 2700, 6 const ALLOWED_TEMPERATURES_IN_KELVIN_BY_DEVICE_TYPE = { [DeviceType.LitraGlow]: MULTIPLES_OF_100_BETWEEN_2700_AND_6500, [DeviceType.LitraBeam]: MULTIPLES_OF_100_BETWEEN_2700_AND_6500, + [DeviceType.LitraBeamLX]: MULTIPLES_OF_100_BETWEEN_2700_AND_6500, }; const NAME_BY_DEVICE_TYPE = { [DeviceType.LitraGlow]: 'Logitech Litra Glow', [DeviceType.LitraBeam]: 'Logitech Litra Beam', + [DeviceType.LitraBeamLX]: 'Logitech Litra Beam LX', }; // Conforms to the interface of `node-hid`'s `HID.HID`. Useful for mocking. @@ -100,13 +106,30 @@ export const findDevices = (): Device[] => { return matchingDevices.map(hidDeviceToDevice); }; +const generateTurnOnBytes = (device: Device): number[] => { + if (device.type === DeviceType.LitraBeamLX) { + return padRight([0x11, 0xff, 0x06, 0x1c, 0x01], 20, 0x00); + } else { + return padRight([0x11, 0xff, 0x04, 0x1c, 0x01], 20, 0x00); + } +}; + /** * Turns your Logitech Litra device on * * @param {Device} device The device to turn on */ export const turnOn = (device: Device): void => { - device.hid.write(padRight([0x11, 0xff, 0x04, 0x1c, 0x01], 20, 0x00)); + const bytes = generateTurnOnBytes(device); + device.hid.write(bytes); +}; + +const generateTurnOffBytes = (device: Device): number[] => { + if (device.type === DeviceType.LitraBeamLX) { + return padRight([0x11, 0xff, 0x06, 0x1c, 0x00], 20, 0x00); + } else { + return padRight([0x11, 0xff, 0x04, 0x1c, 0x00], 20, 0x00); + } }; /** @@ -115,7 +138,8 @@ export const turnOn = (device: Device): void => { * @param {Device} device The device to turn off */ export const turnOff = (device: Device): void => { - device.hid.write(padRight([0x11, 0xff, 0x04, 0x1c, 0x00], 20, 0x00)); + const bytes = generateTurnOffBytes(device); + device.hid.write(bytes); }; /** @@ -131,6 +155,14 @@ export const toggle = (device: Device): void => { } }; +const generateIsOnBytes = (device: Device): number[] => { + if (device.type === DeviceType.LitraBeamLX) { + return padRight([0x11, 0xff, 0x06, 0x01], 20, 0x00); + } else { + return padRight([0x11, 0xff, 0x04, 0x01], 20, 0x00); + } +}; + /** * Gets the current power state of your Logitech Litra device * @@ -138,13 +170,33 @@ export const toggle = (device: Device): void => { * @returns {boolean} Current power state where true = on and false = off */ export const isOn = (device: Device): boolean => { - device.hid.write(padRight([0x11, 0xff, 0x04, 0x01], 20, 0x00)); + const bytes = generateIsOnBytes(device); + device.hid.write(bytes); const data = device.hid.readSync(); return data[4] === 1; }; +const generateSetTemperatureInKelvinBytes = ( + device: Device, + temperatureInKelvin: number, +): number[] => { + if (device.type === DeviceType.LitraBeamLX) { + return padRight( + [0x11, 0xff, 0x06, 0x9c, ...integerToBytes(temperatureInKelvin)], + 20, + 0x00, + ); + } else { + return padRight( + [0x11, 0xff, 0x04, 0x9c, ...integerToBytes(temperatureInKelvin)], + 20, + 0x00, + ); + } +}; + /** * Sets the temperature of your Logitech Litra device * @@ -171,9 +223,17 @@ export const setTemperatureInKelvin = ( throw `Provided temperature must be a multiple of 100 between ${minimumTemperature} and ${maximumTemperature} for this device`; } - device.hid.write( - padRight([0x11, 0xff, 0x04, 0x9c, ...integerToBytes(temperatureInKelvin)], 20, 0x00), - ); + const bytes = generateSetTemperatureInKelvinBytes(device, temperatureInKelvin); + + device.hid.write(bytes); +}; + +const generateGetTemperatureInKelvinBytes = (device: Device): number[] => { + if (device.type === DeviceType.LitraBeamLX) { + return padRight([0x11, 0xff, 0x06, 0x81], 20, 0x00); + } else { + return padRight([0x11, 0xff, 0x04, 0x81], 20, 0x00); + } }; /** @@ -183,7 +243,8 @@ export const setTemperatureInKelvin = ( * @returns {number} The current temperature in Kelvin */ export const getTemperatureInKelvin = (device: Device): number => { - device.hid.write(padRight([0x11, 0xff, 0x04, 0x81], 20, 0x00)); + const bytes = generateGetTemperatureInKelvinBytes(device); + device.hid.write(bytes); const data = device.hid.readSync(); @@ -193,6 +254,25 @@ export const getTemperatureInKelvin = (device: Device): number => { return data[4] * 256 + data[5]; }; +const generateSetBrightnessInLumenBytes = ( + device: Device, + brightnessInLumen: number, +): number[] => { + if (device.type === DeviceType.LitraBeamLX) { + return padRight( + [0x11, 0xff, 0x06, 0x4c, ...integerToBytes(brightnessInLumen)], + 20, + 0x00, + ); + } else { + return padRight( + [0x11, 0xff, 0x04, 0x4c, ...integerToBytes(brightnessInLumen)], + 20, + 0x00, + ); + } +}; + /** * Sets the brightness of your Logitech Litra device, measured in Lumen * @@ -213,9 +293,17 @@ export const setBrightnessInLumen = (device: Device, brightnessInLumen: number): throw `Provided brightness must be between ${minimumBrightness} and ${maximumBrightness} for this device`; } - device.hid.write( - padRight([0x11, 0xff, 0x04, 0x4c, ...integerToBytes(brightnessInLumen)], 20, 0x00), - ); + const bytes = generateSetBrightnessInLumenBytes(device, brightnessInLumen); + + device.hid.write(bytes); +}; + +const generateGetBrightnessInLumenBytes = (device: Device): number[] => { + if (device.type === DeviceType.LitraBeamLX) { + return padRight([0x11, 0xff, 0x06, 0x31], 20, 0x00); + } else { + return padRight([0x11, 0xff, 0x04, 0x31], 20, 0x00); + } }; /** @@ -225,7 +313,8 @@ export const setBrightnessInLumen = (device: Device, brightnessInLumen: number): * @returns {number} The current brightness in Lumen */ export const getBrightnessInLumen = (device: Device): number => { - device.hid.write(padRight([0x11, 0xff, 0x04, 0x31], 20, 0x00)); + const bytes = generateGetBrightnessInLumenBytes(device); + device.hid.write(bytes); const data = device.hid.readSync(); @@ -271,6 +360,8 @@ const getDeviceTypeByProductId = (productId: number): DeviceType => { case PRODUCT_IDS[1]: case PRODUCT_IDS[2]: return DeviceType.LitraBeam; + case PRODUCT_IDS[3]: + return DeviceType.LitraBeamLX; default: throw 'Unknown device type'; } diff --git a/tests/driver.test.ts b/tests/driver.test.ts index bac4fa2..3ef989e 100644 --- a/tests/driver.test.ts +++ b/tests/driver.test.ts @@ -23,6 +23,7 @@ const FAKE_SERIAL_NUMBER = 'fake_serial_number'; let fakeDevice: Device; let fakeLitraGlow: Device; let fakeLitraBeam: Device; +let fakeLitraBeamLx: Device; beforeEach(() => { fakeDevice = { @@ -42,89 +43,219 @@ beforeEach(() => { hid: { write: jest.fn(), readSync: jest.fn() }, serialNumber: FAKE_SERIAL_NUMBER, }; + + fakeLitraBeamLx = { + type: DeviceType.LitraBeamLX, + hid: { write: jest.fn(), readSync: jest.fn() }, + serialNumber: FAKE_SERIAL_NUMBER, + }; }); describe('turnOn', () => { - it('sends the instruction to turn the device on', () => { - turnOn(fakeDevice); + it('sends the correct instruction to turn the device on for a Litra Glow', () => { + turnOn(fakeLitraGlow); + + expect(fakeLitraGlow.hid.write).toBeCalledWith([ + 17, 255, 4, 28, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); - expect(fakeDevice.hid.write).toBeCalledWith([ + it('sends the correct instruction to turn the device on for a Litra Beam', () => { + turnOn(fakeLitraBeam); + + expect(fakeLitraBeam.hid.write).toBeCalledWith([ 17, 255, 4, 28, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); }); + + it('sends the correct instruction to turn the device on for a Litra Beam LX', () => { + turnOn(fakeLitraBeamLx); + + expect(fakeLitraBeamLx.hid.write).toBeCalledWith([ + 17, 255, 6, 28, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); }); describe('turnOff', () => { - it('sends the instruction to turn the device off', () => { - turnOff(fakeDevice); + it('sends the correct instruction to turn the device off for a Litra Glow', () => { + turnOff(fakeLitraGlow); + + expect(fakeLitraGlow.hid.write).toBeCalledWith([ + 17, 255, 4, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); + + it('sends the correct instruction to turn the device off for a Litra Beam', () => { + turnOff(fakeLitraBeam); - expect(fakeDevice.hid.write).toBeCalledWith([ + expect(fakeLitraBeam.hid.write).toBeCalledWith([ 17, 255, 4, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); }); + + it('sends the correct instruction to turn the device off for a Litra Beam LX', () => { + turnOff(fakeLitraBeamLx); + + expect(fakeLitraBeamLx.hid.write).toBeCalledWith([ + 17, 255, 6, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); }); describe('toggle', () => { - it('sends the instruction to toggle the device on when it is off', () => { - fakeDevice.hid.readSync = jest + it('sends the right instruction to turn a Litra Glow on when it is off', () => { + fakeLitraGlow.hid.readSync = jest .fn() .mockReturnValue([17, 255, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); - toggle(fakeDevice); + toggle(fakeLitraGlow); - expect(fakeDevice.hid.write).toBeCalledWith([ + expect(fakeLitraGlow.hid.write).toBeCalledWith([ 17, 255, 4, 28, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); }); - it('sends the instruction to toggle the device off when it is on', () => { - fakeDevice.hid.readSync = jest + it('sends the right instruction to turn a Litra Beam on when it is off', () => { + fakeLitraBeam.hid.readSync = jest + .fn() + .mockReturnValue([17, 255, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + toggle(fakeLitraBeam); + + expect(fakeLitraBeam.hid.write).toBeCalledWith([ + 17, 255, 4, 28, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); + + it('sends the right instruction to turn a Litra Beam LX on when it is off', () => { + fakeLitraBeamLx.hid.readSync = jest + .fn() + .mockReturnValue([17, 255, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + toggle(fakeLitraBeamLx); + + expect(fakeLitraBeamLx.hid.write).toBeCalledWith([ + 17, 255, 6, 28, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); + + it('sends the right instruction to turn the Litra Glow off when it is on', () => { + fakeLitraGlow.hid.readSync = jest .fn() .mockReturnValue([17, 255, 4, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); - toggle(fakeDevice); + toggle(fakeLitraGlow); - expect(fakeDevice.hid.write).toBeCalledWith([ + expect(fakeLitraGlow.hid.write).toBeCalledWith([ 17, 255, 4, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); }); + + it('sends the right instruction to turn the Litra Beam off when it is on', () => { + fakeLitraBeam.hid.readSync = jest + .fn() + .mockReturnValue([17, 255, 4, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + toggle(fakeLitraBeam); + + expect(fakeLitraBeam.hid.write).toBeCalledWith([ + 17, 255, 4, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); + + it('sends the right instruction to turn the Litra Beam LX off when it is on', () => { + fakeLitraBeamLx.hid.readSync = jest + .fn() + .mockReturnValue([17, 255, 4, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + toggle(fakeLitraBeamLx); + + expect(fakeLitraBeamLx.hid.write).toBeCalledWith([ + 17, 255, 6, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); }); describe('isOn', () => { - it('sends the instruction to get the device power state when the device is off', () => { - fakeDevice.hid.readSync = jest + it('sends the right instruction to get the device power state for a Litra Glow', () => { + fakeLitraGlow.hid.readSync = jest .fn() .mockReturnValue([17, 255, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); - expect(isOn(fakeDevice)).toBe(false); + isOn(fakeLitraGlow); + + expect(fakeLitraGlow.hid.write).toBeCalledWith([ + 17, 255, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); + + it('sends the right instruction to get the device power state for a Litra Beam', () => { + fakeLitraBeam.hid.readSync = jest + .fn() + .mockReturnValue([17, 255, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + isOn(fakeLitraBeam); - expect(fakeDevice.hid.write).toBeCalledWith([ + expect(fakeLitraBeam.hid.write).toBeCalledWith([ 17, 255, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); }); - it('sends the instruction to get the device power state when the device is on', () => { + it('sends the right instruction to get the device power state for a Litra Beam LX', () => { + fakeLitraBeamLx.hid.readSync = jest + .fn() + .mockReturnValue([17, 255, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + isOn(fakeLitraBeamLx); + + expect(fakeLitraBeamLx.hid.write).toBeCalledWith([ + 17, 255, 6, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); + + it('returns true when the device is on', () => { fakeDevice.hid.readSync = jest .fn() .mockReturnValue([17, 255, 4, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); expect(isOn(fakeDevice)).toBe(true); + }); - expect(fakeDevice.hid.write).toBeCalledWith([ - 17, 255, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]); + it('returns false when the device is off', () => { + fakeDevice.hid.readSync = jest + .fn() + .mockReturnValue([17, 255, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + expect(isOn(fakeDevice)).toBe(false); }); }); describe('setTemperatureInKelvin', () => { - it('sends the instruction to set the device temperature', () => { - setTemperatureInKelvin(fakeDevice, 6300); + it('sends the right instruction to set the temperature for a Litra Glow', () => { + setTemperatureInKelvin(fakeLitraGlow, 6300); - expect(fakeDevice.hid.write).toBeCalledWith([ + expect(fakeLitraGlow.hid.write).toBeCalledWith([ 17, 255, 4, 156, 24, 156, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); }); + it('sends the right instruction to set the temperature for a Litra Beam', () => { + setTemperatureInKelvin(fakeLitraBeam, 6300); + + expect(fakeLitraBeam.hid.write).toBeCalledWith([ + 17, 255, 4, 156, 24, 156, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); + + it('sends the right instruction to set the temperature for a Litra Beam LX', () => { + setTemperatureInKelvin(fakeLitraBeamLx, 6300); + + expect(fakeLitraBeamLx.hid.write).toBeCalledWith([ + 17, 255, 6, 156, 24, 156, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); + it('throws an error if the temperature is below the minimum for the device', () => { expect(() => setTemperatureInKelvin(fakeLitraGlow, 2699)).toThrowError( 'Provided temperature must be a multiple of 100 between 2700 and 6500', @@ -163,30 +294,74 @@ describe('setTemperatureInKelvin', () => { }); describe('getTemperatureInKelvin', () => { - it('sends the instruction to get the device temperature', () => { - fakeDevice.hid.readSync = jest + it('sends the right instruction to get the temperature for a Litra Glow', () => { + fakeLitraGlow.hid.readSync = jest .fn() .mockReturnValue([ 17, 255, 4, 129, 19, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); - expect(getTemperatureInKelvin(fakeDevice)).toEqual(5000); + expect(getTemperatureInKelvin(fakeLitraGlow)).toEqual(5000); - expect(fakeDevice.hid.write).toBeCalledWith([ + expect(fakeLitraGlow.hid.write).toBeCalledWith([ 17, 255, 4, 129, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); }); + + it('sends the right instruction to get the temperature for a Litra Beam', () => { + fakeLitraBeam.hid.readSync = jest + .fn() + .mockReturnValue([ + 17, 255, 4, 129, 19, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + + expect(getTemperatureInKelvin(fakeLitraBeam)).toEqual(5000); + + expect(fakeLitraBeam.hid.write).toBeCalledWith([ + 17, 255, 4, 129, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); + + it('sends the right instruction to get the temperature for a Litra Beam LX', () => { + fakeLitraBeamLx.hid.readSync = jest + .fn() + .mockReturnValue([ + 17, 255, 4, 129, 19, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + + expect(getTemperatureInKelvin(fakeLitraBeamLx)).toEqual(5000); + + expect(fakeLitraBeamLx.hid.write).toBeCalledWith([ + 17, 255, 6, 129, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); }); describe('setBrightnessInLumen', () => { - it('sends the instruction to set the device brightness', () => { - setBrightnessInLumen(fakeDevice, 20); + it('sends the right instruction to set the brightness of a Litra Glow', () => { + setBrightnessInLumen(fakeLitraGlow, 20); - expect(fakeDevice.hid.write).toBeCalledWith([ + expect(fakeLitraGlow.hid.write).toBeCalledWith([ 17, 255, 4, 76, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); }); + it('sends the right instruction to set the brightness of a Litra Beam', () => { + setBrightnessInLumen(fakeLitraBeam, 30); + + expect(fakeLitraBeam.hid.write).toBeCalledWith([ + 17, 255, 4, 76, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); + + it('sends the right instruction to set the brightness of a Litra Beam LX', () => { + setBrightnessInLumen(fakeLitraBeamLx, 30); + + expect(fakeLitraBeamLx.hid.write).toBeCalledWith([ + 17, 255, 6, 76, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); + it('throws an error if the brightness is below the minimum for the device', () => { expect(() => setBrightnessInLumen(fakeLitraGlow, 19)).toThrowError( 'Provided brightness must be between 20 and 250', @@ -215,29 +390,59 @@ describe('setBrightnessInLumen', () => { }); describe('getBrightnessInLumen', () => { - it('sends the instruction to get the device brightness', () => { - fakeDevice.hid.readSync = jest + it('sends the right instruction to get the brightness of a Litra Glow', () => { + fakeLitraGlow.hid.readSync = jest + .fn() + .mockReturnValue([ + 17, 255, 4, 49, 0, 216, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + + expect(getBrightnessInLumen(fakeLitraGlow)).toEqual(216); + + expect(fakeLitraGlow.hid.write).toBeCalledWith([ + 17, 255, 4, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); + + it('sends the right instruction to get the brightness of a Litra Beam', () => { + fakeLitraBeam.hid.readSync = jest .fn() .mockReturnValue([ 17, 255, 4, 49, 0, 216, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); - expect(getBrightnessInLumen(fakeDevice)).toEqual(216); + expect(getBrightnessInLumen(fakeLitraBeam)).toEqual(216); - expect(fakeDevice.hid.write).toBeCalledWith([ + expect(fakeLitraBeam.hid.write).toBeCalledWith([ 17, 255, 4, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); }); + + it('sends the right instruction to get the brightness of a Litra Beam LX', () => { + fakeLitraBeamLx.hid.readSync = jest + .fn() + .mockReturnValue([ + 17, 255, 4, 49, 0, 216, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + + expect(getBrightnessInLumen(fakeLitraBeamLx)).toEqual(216); + + expect(fakeLitraBeamLx.hid.write).toBeCalledWith([ + 17, 255, 6, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); }); describe('setBrightnessPercentage', () => { - it('sends the instruction to set the device brightness based on a percentage', () => { - setBrightnessPercentage(fakeLitraGlow, 100); + it("sends the right instruction to set a Litra Glow's brightness based on a percentage", () => { + setBrightnessPercentage(fakeLitraBeam, 100); - expect(fakeLitraGlow.hid.write).toBeCalledWith([ - 17, 255, 4, 76, 0, 250, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + expect(fakeLitraBeam.hid.write).toBeCalledWith([ + 17, 255, 4, 76, 1, 144, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); + }); + it("sends the right instruction to set a Litra Beam's brightness based on a percentage", () => { setBrightnessPercentage(fakeLitraBeam, 100); expect(fakeLitraBeam.hid.write).toBeCalledWith([ @@ -245,13 +450,23 @@ describe('setBrightnessPercentage', () => { ]); }); - it('sends the instruction to set the device brightness to the minimum brightness when set to 0%', () => { + it("sends the right instruction to set a Litra Beam LX's brightness based on a percentage", () => { + setBrightnessPercentage(fakeLitraBeamLx, 100); + + expect(fakeLitraBeamLx.hid.write).toBeCalledWith([ + 17, 255, 6, 76, 1, 144, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); + + it("sends the right instruction to set a Litra Glow's brightness to the minimum brightness when set to 0%", () => { setBrightnessPercentage(fakeLitraGlow, 0); expect(fakeLitraGlow.hid.write).toBeCalledWith([ 17, 255, 4, 76, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); + }); + it("sends the right instruction to set a Litra Beam's brightness to the minimum brightness when set to 0%", () => { setBrightnessPercentage(fakeLitraBeam, 0); expect(fakeLitraBeam.hid.write).toBeCalledWith([ @@ -259,6 +474,14 @@ describe('setBrightnessPercentage', () => { ]); }); + it("sends the right instruction to set a Litra Beam LX's brightness to the minimum brightness when set to 0%", () => { + setBrightnessPercentage(fakeLitraBeamLx, 0); + + expect(fakeLitraBeamLx.hid.write).toBeCalledWith([ + 17, 255, 6, 76, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + }); + it('throws an error if the provided percentage is less than 0', () => { expect(() => setBrightnessPercentage(fakeDevice, -1)).toThrowError( 'Percentage must be between 0 and 100', @@ -280,6 +503,10 @@ describe('getMinimumBrightnessInLumenForDevice', () => { it('returns the correct minimum brightness for a Litra Beam', () => { expect(getMinimumBrightnessInLumenForDevice(fakeLitraBeam)).toEqual(30); }); + + it('returns the correct minimum brightness for a Litra Beam LX', () => { + expect(getMinimumBrightnessInLumenForDevice(fakeLitraBeamLx)).toEqual(30); + }); }); describe('getMaximumBrightnessInLumenForDevice', () => { @@ -290,6 +517,10 @@ describe('getMaximumBrightnessInLumenForDevice', () => { it('returns the correct maximum brightness for a Litra Beam', () => { expect(getMaximumBrightnessInLumenForDevice(fakeLitraBeam)).toEqual(400); }); + + it('returns the correct maximum brightness for a Litra Beam LX', () => { + expect(getMaximumBrightnessInLumenForDevice(fakeLitraBeamLx)).toEqual(400); + }); }); describe('getMinimumTemperatureInKelvinForDevice', () => { @@ -300,6 +531,10 @@ describe('getMinimumTemperatureInKelvinForDevice', () => { it('returns the correct minimum temperature for a Litra Beam', () => { expect(getMinimumTemperatureInKelvinForDevice(fakeLitraBeam)).toEqual(2700); }); + + it('returns the correct minimum temperature for a Litra Beam LX', () => { + expect(getMinimumTemperatureInKelvinForDevice(fakeLitraBeamLx)).toEqual(2700); + }); }); describe('getMaximumTemperatureInKelvinForDevice', () => { @@ -310,6 +545,10 @@ describe('getMaximumTemperatureInKelvinForDevice', () => { it('returns the correct maximum temperature for a Litra Beam', () => { expect(getMaximumTemperatureInKelvinForDevice(fakeLitraBeam)).toEqual(6500); }); + + it('returns the correct maximum temperature for a Litra Beam LX', () => { + expect(getMaximumTemperatureInKelvinForDevice(fakeLitraBeamLx)).toEqual(6500); + }); }); describe('getAllowedTemperaturesInKelvinForDevice', () => { @@ -328,6 +567,14 @@ describe('getAllowedTemperaturesInKelvinForDevice', () => { 5500, 5600, 5700, 5800, 5900, 6000, 6100, 6200, 6300, 6400, 6500, ]); }); + + it('returns the allowed temperatures for a Litra Beam LX', () => { + expect(getAllowedTemperaturesInKelvinForDevice(fakeLitraBeamLx)).toEqual([ + 2700, 2800, 2900, 3000, 3100, 3200, 3300, 3400, 3500, 3600, 3700, 3800, 3900, 4000, + 4100, 4200, 4300, 4400, 4500, 4600, 4700, 4800, 4900, 5000, 5100, 5200, 5300, 5400, + 5500, 5600, 5700, 5800, 5900, 6000, 6100, 6200, 6300, 6400, 6500, + ]); + }); }); describe('getNameForDevice', () => { @@ -338,4 +585,8 @@ describe('getNameForDevice', () => { it('returns the correct name for a Litra Beam', () => { expect(getNameForDevice(fakeLitraBeam)).toEqual('Logitech Litra Beam'); }); + + it('returns the correct name for a Litra Beam LX', () => { + expect(getNameForDevice(fakeLitraBeamLx)).toEqual('Logitech Litra Beam LX'); + }); });