Skip to content

Commit

Permalink
feat: add support for working with multiple Logitech Litra devices wi…
Browse files Browse the repository at this point in the history
…th `findDevices()` and exposed `serialNumber`s

Adds multiple device support
  • Loading branch information
timrogers authored Feb 2, 2023
2 parents f242cec + a4a1169 commit 29ffba0
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 79 deletions.
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,22 @@ if (device) {
}
```

If you're a *huge* fan of Litra devices and you have multiple plugged in at the same time, it'll return whatever one it happens to find first.
If you're a *huge* fan of Litra devices and you have multiple plugged in at the same time, use `findDevices` instead:

```js
const devices = findDevices();

if (devices.length > 0) {
console.log(`Found ${devices.length} devices connected`);
for (let i = 0; i < devices.length; ++i) {
console.log(`Device ${i + 1}: ${devices[i].type}`);
}

// Do something
} else {
// Blow up
}
```

#### Turning your Litra device on or off

Expand Down
43 changes: 33 additions & 10 deletions src/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,25 @@ export interface Device {
write: (values: number[] | Buffer) => number;
};
type: DeviceType;
serialNumber: string;
}

const isLitraDevice = (device: HID.Device): boolean => {
return (
device.vendorId === VENDOR_ID &&
PRODUCT_IDS.includes(device.productId) &&
device.usagePage === USAGE_PAGE
);
};

const hidDeviceToDevice = (hidDevice: HID.Device): Device => {
return {
type: getDeviceTypeByProductId(hidDevice.productId),
hid: new HID.HID(hidDevice.path as string),
serialNumber: hidDevice.serialNumber as string,
};
};

/**
* Finds your Logitech Litra device and returns it. Returns `null` if a
* supported device cannot be found connected to your computer.
Expand All @@ -51,23 +68,29 @@ export interface Device {
* or `null` if a matching device cannot be found connected to your computer.
*/
export const findDevice = (): Device | null => {
const matchingDevice = HID.devices().find(
(device) =>
device.vendorId === VENDOR_ID &&
PRODUCT_IDS.includes(device.productId) &&
device.usagePage === USAGE_PAGE,
);
const matchingDevice = HID.devices().find(isLitraDevice);

if (matchingDevice) {
return {
type: getDeviceTypeByProductId(matchingDevice.productId),
hid: new HID.HID(matchingDevice.path as string),
};
return hidDeviceToDevice(matchingDevice);
} else {
return null;
}
};

/**
* Finds one or more Logitech Litra devices and returns them.
* Returns an empty `Array` if no supported devices could be found
* connected to your computer.
*
* @returns {Device[], null} An Array representing your Logitech Litra devices,
* passed into other functions like `turnOn` and `setTemperatureInKelvin`. The
* Array will be empty if no matching devices could be found connected to your computer.
*/
export const findDevices = (): Device[] => {
const matchingDevices = HID.devices().filter(isLitraDevice);
return matchingDevices.map(hidDeviceToDevice);
};

/**
* Turns your Logitech Litra device on.
*
Expand Down
103 changes: 35 additions & 68 deletions tests/driver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,37 @@ import {
setTemperaturePercentage,
turnOff,
turnOn,
Device,
} from '../src/driver';

const FAKE_SERIAL_NUMBER = 'fake_serial_number';

let fakeDevice: Device;
let fakeLitraGlow: Device;
let fakeLitraBeam: Device;

beforeEach(() => {
fakeDevice = {
type: DeviceType.LitraGlow,
hid: { write: jest.fn() },
serialNumber: FAKE_SERIAL_NUMBER,
};

fakeLitraGlow = {
type: DeviceType.LitraGlow,
hid: { write: jest.fn() },
serialNumber: FAKE_SERIAL_NUMBER,
};

fakeLitraBeam = {
type: DeviceType.LitraBeam,
hid: { write: jest.fn() },
serialNumber: FAKE_SERIAL_NUMBER,
};
});

describe('turnOn', () => {
it('sends the instruction to turn the device on', () => {
const fakeDevice = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

turnOn(fakeDevice);

expect(fakeDevice.hid.write).toBeCalledWith([
Expand All @@ -26,8 +51,6 @@ describe('turnOn', () => {

describe('turnOff', () => {
it('sends the instruction to turn the device off', () => {
const fakeDevice = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

turnOff(fakeDevice);

expect(fakeDevice.hid.write).toBeCalledWith([
Expand All @@ -38,8 +61,6 @@ describe('turnOff', () => {

describe('setTemperatureInKelvin', () => {
it('sends the instruction to set the device temperature', () => {
const fakeDevice = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

setTemperatureInKelvin(fakeDevice, 6300);

expect(fakeDevice.hid.write).toBeCalledWith([
Expand All @@ -48,36 +69,26 @@ describe('setTemperatureInKelvin', () => {
});

it('throws an error if the temperature is below the minimum for the device', () => {
const fakeLitraGlow = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

expect(() => setTemperatureInKelvin(fakeLitraGlow, 2699)).toThrowError(
'Provided temperature must be between 2700 and 6500',
);

const fakeLitraBeam = { type: DeviceType.LitraBeam, hid: { write: jest.fn() } };

expect(() => setTemperatureInKelvin(fakeLitraBeam, 2699)).toThrowError(
'Provided temperature must be between 2700 and 6500',
);
});

it('throws an error if the temperature is above the maximum for the device', () => {
const fakeLitraGlow = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

expect(() => setTemperatureInKelvin(fakeLitraGlow, 6501)).toThrowError(
'Provided temperature must be between 2700 and 6500',
);

const fakeLitraBeam = { type: DeviceType.LitraBeam, hid: { write: jest.fn() } };

expect(() => setTemperatureInKelvin(fakeLitraBeam, 6501)).toThrowError(
'Provided temperature must be between 2700 and 6500',
);
});

it('throws an error if the temperature is not an integer', () => {
const fakeDevice = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

expect(() => setTemperatureInKelvin(fakeDevice, 1337.9)).toThrowError(
'Provided temperature must be an integer',
);
Expand All @@ -86,16 +97,12 @@ describe('setTemperatureInKelvin', () => {

describe('setTemperaturePercentage', () => {
it('sends the instruction to set the device temperature based on a percentage, ', () => {
const fakeLitraGlow = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

setTemperaturePercentage(fakeLitraGlow, 100);

expect(fakeLitraGlow.hid.write).toBeCalledWith([
17, 255, 4, 156, 25, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
]);

const fakeLitraBeam = { type: DeviceType.LitraBeam, hid: { write: jest.fn() } };

setTemperaturePercentage(fakeLitraBeam, 100);

expect(fakeLitraBeam.hid.write).toBeCalledWith([
Expand All @@ -104,16 +111,12 @@ describe('setTemperaturePercentage', () => {
});

it('sends the instruction to set the device temperature to the minimum temperature when set to 0%', () => {
const fakeLitraGlow = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

setTemperaturePercentage(fakeLitraGlow, 0);

expect(fakeLitraGlow.hid.write).toBeCalledWith([
17, 255, 4, 156, 10, 140, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
]);

const fakeLitraBeam = { type: DeviceType.LitraBeam, hid: { write: jest.fn() } };

setTemperaturePercentage(fakeLitraBeam, 0);

expect(fakeLitraBeam.hid.write).toBeCalledWith([
Expand All @@ -122,16 +125,12 @@ describe('setTemperaturePercentage', () => {
});

it('throws an error if the provided percentage is less than 0', () => {
const fakeDevice = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

expect(() => setTemperaturePercentage(fakeDevice, -1)).toThrowError(
'Percentage must be between 0 and 100',
);
});

it('throws an error if the provided percentage is more than 100', () => {
const fakeDevice = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

expect(() => setTemperaturePercentage(fakeDevice, 101)).toThrowError(
'Percentage must be between 0 and 100',
);
Expand All @@ -140,8 +139,6 @@ describe('setTemperaturePercentage', () => {

describe('setBrightnessInLumen', () => {
it('sends the instruction to set the device temperature', () => {
const fakeDevice = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

setBrightnessInLumen(fakeDevice, 20);

expect(fakeDevice.hid.write).toBeCalledWith([
Expand All @@ -150,36 +147,26 @@ describe('setBrightnessInLumen', () => {
});

it('throws an error if the brightness is below the minimum for the device', () => {
const fakeLitraGlow = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

expect(() => setBrightnessInLumen(fakeLitraGlow, 19)).toThrowError(
'Provided brightness must be between 20 and 250',
);

const fakeLitraBeam = { type: DeviceType.LitraBeam, hid: { write: jest.fn() } };

expect(() => setBrightnessInLumen(fakeLitraBeam, 19)).toThrowError(
'Provided brightness must be between 30 and 400',
);
});

it('throws an error if the brightness is above the maximum for the device', () => {
const fakeLitraGlow = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

expect(() => setBrightnessInLumen(fakeLitraGlow, 251)).toThrowError(
'Provided brightness must be between 20 and 250',
);

const fakeLitraBeam = { type: DeviceType.LitraBeam, hid: { write: jest.fn() } };

expect(() => setBrightnessInLumen(fakeLitraBeam, 401)).toThrowError(
'Provided brightness must be between 30 and 400',
);
});

it('throws an error if the brightness is not an integer', () => {
const fakeDevice = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

expect(() => setBrightnessInLumen(fakeDevice, 1337.9)).toThrowError(
'Provided brightness must be an integer',
);
Expand All @@ -188,16 +175,12 @@ describe('setBrightnessInLumen', () => {

describe('setBrightnessPercentage', () => {
it('sends the instruction to set the device brightness based on a percentage', () => {
const fakeLitraGlow = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

setBrightnessPercentage(fakeLitraGlow, 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,
]);

const fakeLitraBeam = { type: DeviceType.LitraBeam, hid: { write: jest.fn() } };

setBrightnessPercentage(fakeLitraBeam, 100);

expect(fakeLitraBeam.hid.write).toBeCalledWith([
Expand All @@ -206,16 +189,12 @@ describe('setBrightnessPercentage', () => {
});

it('sends the instruction to set the device brightness to the minimum brightness when set to 0%', () => {
const fakeLitraGlow = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

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,
]);

const fakeLitraBeam = { type: DeviceType.LitraBeam, hid: { write: jest.fn() } };

setBrightnessPercentage(fakeLitraBeam, 0);

expect(fakeLitraBeam.hid.write).toBeCalledWith([
Expand All @@ -224,16 +203,12 @@ describe('setBrightnessPercentage', () => {
});

it('throws an error if the provided percentage is less than 0', () => {
const fakeDevice = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

expect(() => setBrightnessPercentage(fakeDevice, -1)).toThrowError(
'Percentage must be between 0 and 100',
);
});

it('throws an error if the provided percentage is more than 100', () => {
const fakeDevice = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };

expect(() => setBrightnessPercentage(fakeDevice, 101)).toThrowError(
'Percentage must be between 0 and 100',
);
Expand All @@ -242,48 +217,40 @@ describe('setBrightnessPercentage', () => {

describe('getMinimumBrightnessInLumenForDevice', () => {
it('returns the correct minimum brightness for a Litra Glow', () => {
const fakeDevice = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };
expect(getMinimumBrightnessInLumenForDevice(fakeDevice)).toEqual(20);
expect(getMinimumBrightnessInLumenForDevice(fakeLitraGlow)).toEqual(20);
});

it('returns the correct minimum brightness for a Litra Beam', () => {
const fakeDevice = { type: DeviceType.LitraBeam, hid: { write: jest.fn() } };
expect(getMinimumBrightnessInLumenForDevice(fakeDevice)).toEqual(30);
expect(getMinimumBrightnessInLumenForDevice(fakeLitraBeam)).toEqual(30);
});
});

describe('getMaximumBrightnessInLumenForDevice', () => {
it('returns the correct maximum brightness for a Litra Glow', () => {
const fakeDevice = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };
expect(getMaximumBrightnessInLumenForDevice(fakeDevice)).toEqual(250);
expect(getMaximumBrightnessInLumenForDevice(fakeLitraGlow)).toEqual(250);
});

it('returns the correct maximum brightness for a Litra Beam', () => {
const fakeDevice = { type: DeviceType.LitraBeam, hid: { write: jest.fn() } };
expect(getMaximumBrightnessInLumenForDevice(fakeDevice)).toEqual(400);
expect(getMaximumBrightnessInLumenForDevice(fakeLitraBeam)).toEqual(400);
});
});

describe('getMinimumTemperatureInKelvinForDevice', () => {
it('returns the correct minimum temperature for a Litra Glow', () => {
const fakeDevice = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };
expect(getMinimumTemperatureInKelvinForDevice(fakeDevice)).toEqual(2700);
expect(getMinimumTemperatureInKelvinForDevice(fakeLitraGlow)).toEqual(2700);
});

it('returns the correct minimum temperature for a Litra Beam', () => {
const fakeDevice = { type: DeviceType.LitraBeam, hid: { write: jest.fn() } };
expect(getMinimumTemperatureInKelvinForDevice(fakeDevice)).toEqual(2700);
expect(getMinimumTemperatureInKelvinForDevice(fakeLitraBeam)).toEqual(2700);
});
});

describe('getMaximumTemperatureInKelvinForDevice', () => {
it('returns the correct maximum temperature for a Litra Glow', () => {
const fakeDevice = { type: DeviceType.LitraGlow, hid: { write: jest.fn() } };
expect(getMaximumTemperatureInKelvinForDevice(fakeDevice)).toEqual(6500);
expect(getMaximumTemperatureInKelvinForDevice(fakeLitraGlow)).toEqual(6500);
});

it('returns the correct maximum temperature for a Litra Beam', () => {
const fakeDevice = { type: DeviceType.LitraBeam, hid: { write: jest.fn() } };
expect(getMaximumTemperatureInKelvinForDevice(fakeDevice)).toEqual(6500);
expect(getMaximumTemperatureInKelvinForDevice(fakeLitraBeam)).toEqual(6500);
});
});

0 comments on commit 29ffba0

Please sign in to comment.