-
Notifications
You must be signed in to change notification settings - Fork 128
feat: add support for virtual devices #318
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
bflorian
wants to merge
1
commit into
SmartThingsCommunity:master
from
bflorian:virtual-devices-phase1
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| "@smartthings/cli": minor | ||
| "@smartthings/cli-lib": patch | ||
| "@smartthings/cli-testlib": patch | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These changes don't modify testlib, so it doesn't need a release in the changeset. |
||
| --- | ||
|
|
||
| Added commands to create virtual devices and generate events on their behalf | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
230 changes: 230 additions & 0 deletions
230
packages/cli/src/__tests__/commands/devices/virtual.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,230 @@ | ||
| import inquirer from 'inquirer' | ||
| import { | ||
| APICommand, | ||
| APIOrganizationCommand, | ||
| FileInputProcessor, | ||
| outputListing, | ||
| selectFromList, | ||
| } from '@smartthings/cli-lib' | ||
| import VirtualDevicesCommand, { | ||
| chooseDeviceName, | ||
| chooseDeviceProfileDefinition, | ||
| chooseDevicePrototype, | ||
| chooseRoom, | ||
| } from '../../../commands/devices/virtual' | ||
| import { chooseDeviceProfile } from '../../../commands/deviceprofiles' | ||
| import { Device, DeviceIntegrationType, DeviceProfile, DeviceProfileStatus, DevicesEndpoint } from '@smartthings/core-sdk' | ||
|
|
||
|
|
||
| jest.mock('../../../commands/deviceprofiles') | ||
|
|
||
| describe('chooseDeviceName function', () => { | ||
| const command = { } as unknown as APICommand<typeof APICommand.flags> | ||
|
|
||
| it('choose with from prompt', async () => { | ||
| const promptSpy = jest.spyOn(inquirer, 'prompt') | ||
| promptSpy.mockResolvedValue({ deviceName: 'Device Name' }) | ||
|
|
||
| const value = await chooseDeviceName(command) | ||
| expect(promptSpy).toHaveBeenCalledTimes(1) | ||
| expect(promptSpy).toHaveBeenCalledWith({ | ||
| type: 'input', name: 'deviceName', | ||
| message: 'Device Name:', | ||
| }) | ||
| expect(value).toBeDefined() | ||
| expect(value).toBe('Device Name') | ||
| }) | ||
|
|
||
| it('choose with default', async () => { | ||
| const promptSpy = jest.spyOn(inquirer, 'prompt') | ||
| promptSpy.mockResolvedValue({ deviceName: 'Another Device Name' }) | ||
|
|
||
| const value = await chooseDeviceName(command, 'Device Name') | ||
| expect(promptSpy).toHaveBeenCalledTimes(0) | ||
| expect(value).toBeDefined() | ||
| expect(value).toBe('Device Name') | ||
| }) | ||
| }) | ||
|
|
||
| describe('chooseRoom function', () => { | ||
| const selectFromListMock = jest.mocked(selectFromList) | ||
| const listRoomsMock = jest.fn() | ||
| const client = { rooms: { list: listRoomsMock } } | ||
| const command = { client } as unknown as APICommand<typeof APICommand.flags> | ||
|
|
||
| it('choose room from prompt', async () => { | ||
| selectFromListMock.mockResolvedValueOnce('room-id') | ||
|
|
||
| const value = await chooseRoom(command, 'location-id', undefined, true) | ||
|
|
||
| expect(selectFromListMock).toHaveBeenCalledTimes(1) | ||
| expect(selectFromListMock).toHaveBeenCalledWith(command, | ||
| expect.objectContaining({ primaryKeyName: 'roomId', sortKeyName: 'name' }), | ||
| expect.objectContaining({ autoChoose: true })) | ||
| expect(value).toBeDefined() | ||
| expect(value).toBe('room-id') | ||
| }) | ||
|
|
||
| it('choose room with default', async () => { | ||
| selectFromListMock.mockResolvedValueOnce('room-id') | ||
|
|
||
| const value = await chooseRoom(command, 'location-id', 'room-id', true) | ||
|
|
||
| expect(selectFromListMock).toHaveBeenCalledTimes(1) | ||
| expect(selectFromListMock).toHaveBeenCalledWith(command, | ||
| expect.objectContaining({ primaryKeyName: 'roomId', sortKeyName: 'name' }), | ||
| expect.objectContaining({ autoChoose: true, preselectedId: 'room-id' })) | ||
| expect(value).toBeDefined() | ||
| expect(value).toBe('room-id') | ||
| }) | ||
| }) | ||
|
|
||
| describe('chooseDeviceProfileDefinition function', () => { | ||
| const chooseDeviceProfileMock = jest.mocked(chooseDeviceProfile) | ||
| const command = { } as unknown as APIOrganizationCommand<typeof APIOrganizationCommand.flags> | ||
|
|
||
| it('choose profile ID from prompt', async() => { | ||
| chooseDeviceProfileMock.mockResolvedValueOnce('device-profile-id') | ||
|
|
||
| const value = await chooseDeviceProfileDefinition(command) | ||
|
|
||
| expect(chooseDeviceProfileMock).toHaveBeenCalledTimes(1) | ||
| expect(chooseDeviceProfileMock).toHaveBeenCalledWith(command, | ||
| undefined, | ||
| expect.objectContaining({ allowIndex: true})) | ||
| expect(value).toBeDefined() | ||
| expect(value).toEqual({deviceProfileId: 'device-profile-id', deviceProfile: undefined}) | ||
| }) | ||
|
|
||
| it('choose profile ID from default', async() => { | ||
| const value = await chooseDeviceProfileDefinition(command, 'device-profile-id') | ||
|
|
||
| expect(chooseDeviceProfileMock).toHaveBeenCalledTimes(0) | ||
| expect(value).toBeDefined() | ||
| expect(value).toEqual({deviceProfileId: 'device-profile-id', deviceProfile: undefined}) | ||
| }) | ||
|
|
||
| it('choose definition from file argument', async() => { | ||
| const deviceProfile: DeviceProfile = { | ||
| id: 'device-profile-id', | ||
| name: 'name', | ||
| components: [], | ||
| status: DeviceProfileStatus.PUBLISHED, | ||
| } | ||
|
|
||
| const fileSpy = jest.spyOn(FileInputProcessor.prototype, 'read').mockResolvedValueOnce(deviceProfile) | ||
|
|
||
| const value = await chooseDeviceProfileDefinition(command, undefined, 'device-profile-file') | ||
|
|
||
| expect(chooseDeviceProfileMock).toHaveBeenCalledTimes(0) | ||
| expect(fileSpy).toHaveBeenCalledTimes(1) | ||
| expect(value).toBeDefined() | ||
| expect(value).toEqual({deviceProfileId: undefined, deviceProfile}) | ||
| }) | ||
| }) | ||
|
|
||
| describe('chooseDevicePrototype function', () => { | ||
| const selectFromListMock = jest.mocked(selectFromList) | ||
| const command = {} as unknown as APICommand<typeof APICommand.flags> | ||
|
|
||
| it('choose from default list prompt', async () => { | ||
| selectFromListMock.mockResolvedValueOnce('VIRTUAL_SWITCH') | ||
|
|
||
| const value = await chooseDevicePrototype(command) | ||
|
|
||
| expect(selectFromListMock).toHaveBeenCalledTimes(1) | ||
| expect(selectFromListMock).toHaveBeenCalledWith(command, | ||
| expect.objectContaining({primaryKeyName: 'id', sortKeyName: 'name'}), | ||
| expect.not.objectContaining({preselectedId: 'VIRTUAL_SWITCH'})) | ||
| expect(value).toBeDefined() | ||
| expect(value).toBe('VIRTUAL_SWITCH') | ||
| }) | ||
|
|
||
| it('choose with command line value', async () => { | ||
| selectFromListMock.mockResolvedValueOnce('VIRTUAL_SWITCH') | ||
|
|
||
| const value = await chooseDevicePrototype(command, 'VIRTUAL_SWITCH') | ||
|
|
||
| expect(selectFromListMock).toHaveBeenCalledTimes(1) | ||
| expect(selectFromListMock).toHaveBeenCalledWith(command, | ||
| expect.objectContaining({primaryKeyName: 'id', sortKeyName: 'name'}), | ||
| expect.objectContaining({preselectedId: 'VIRTUAL_SWITCH'})) | ||
| expect(value).toBeDefined() | ||
| expect(value).toBe('VIRTUAL_SWITCH') | ||
| }) | ||
|
|
||
| it('choose from extended list prompt', async () => { | ||
| selectFromListMock.mockResolvedValueOnce('more') | ||
| selectFromListMock.mockResolvedValueOnce('VIRTUAL_CONTACT_SENSOR') | ||
|
|
||
| const value = await chooseDevicePrototype(command) | ||
|
|
||
| expect(selectFromListMock).toHaveBeenCalledTimes(2) | ||
| expect(selectFromListMock).toHaveBeenNthCalledWith(1, command, | ||
| expect.objectContaining({primaryKeyName: 'id', sortKeyName: 'name'}), | ||
| expect.toBeObject()) | ||
| expect(selectFromListMock).toHaveBeenNthCalledWith(2, command, | ||
| expect.objectContaining({primaryKeyName: 'id', sortKeyName: 'name'}), | ||
| expect.toBeObject()) | ||
| expect(value).toBeDefined() | ||
| expect(value).toBe('VIRTUAL_CONTACT_SENSOR') | ||
| }) | ||
| }) | ||
|
|
||
| describe('VirtualDevicesCommand', () => { | ||
| const outputListingMock = jest.mocked(outputListing) | ||
| const devices = [{ deviceId: 'device-id' }] as Device[] | ||
| const listSpy = jest.spyOn(DevicesEndpoint.prototype, 'list').mockResolvedValue(devices) | ||
|
|
||
| it('virtual devices in all locations', async() => { | ||
| await expect(VirtualDevicesCommand.run([])).resolves.not.toThrow() | ||
|
|
||
| expect(outputListingMock).toHaveBeenCalledTimes(1) | ||
| }) | ||
|
|
||
| it('use simple fields by default', async() => { | ||
| await expect(VirtualDevicesCommand.run([])).resolves.not.toThrow() | ||
|
|
||
| expect(outputListingMock).toHaveBeenCalledTimes(1) | ||
| expect(outputListingMock.mock.calls[0][1].listTableFieldDefinitions) | ||
| .toEqual(['label', 'deviceId']) | ||
| }) | ||
|
|
||
| it('include location and room with verbose flag', async() => { | ||
| await expect(VirtualDevicesCommand.run(['--verbose'])).resolves.not.toThrow() | ||
|
|
||
| expect(outputListingMock).toHaveBeenCalledTimes(1) | ||
| expect(outputListingMock.mock.calls[0][1].listTableFieldDefinitions) | ||
| .toEqual(['label', 'deviceId', 'location', 'room']) | ||
| }) | ||
|
|
||
| it('virtual devices uses location id in list', async() => { | ||
| outputListingMock.mockImplementationOnce(async (_command, _config, _idOrIndex, listFunction) => { | ||
| await listFunction() | ||
| }) | ||
|
|
||
| await expect(VirtualDevicesCommand.run(['--location-id=location-id'])).resolves.not.toThrow() | ||
|
|
||
| expect(outputListingMock).toHaveBeenCalledTimes(1) | ||
| expect(listSpy).toHaveBeenCalledTimes(1) | ||
| expect(listSpy).toHaveBeenCalledWith(expect.objectContaining({ | ||
| type: DeviceIntegrationType.VIRTUAL, | ||
| locationId: ['location-id'], | ||
| })) | ||
| }) | ||
|
|
||
| it('virtual devices uses installed app id in list', async() => { | ||
| outputListingMock.mockImplementationOnce(async (_command, _config, _idOrIndex, listFunction) => { | ||
| await listFunction() | ||
| }) | ||
|
|
||
| await expect(VirtualDevicesCommand.run(['--installed-app-id=installed-app-id'])).resolves.not.toThrow() | ||
|
|
||
| expect(outputListingMock).toHaveBeenCalledTimes(1) | ||
| expect(listSpy).toHaveBeenCalledTimes(1) | ||
| expect(listSpy).toHaveBeenCalledWith(expect.objectContaining({ | ||
| type: DeviceIntegrationType.VIRTUAL, | ||
| installedAppId: 'installed-app-id', | ||
| })) | ||
| }) | ||
| }) |
109 changes: 109 additions & 0 deletions
109
packages/cli/src/__tests__/commands/devices/virtual/create-standard.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import { inputAndOutputItem } from '@smartthings/cli-lib' | ||
| import { | ||
| VirtualDeviceStandardCreateRequest, | ||
| VirtualDevicesEndpoint, | ||
| } from '@smartthings/core-sdk' | ||
| import VirtualDeviceCreateStandardCommand from '../../../../commands/devices/virtual/create-standard' | ||
| import { chooseDeviceName, chooseDevicePrototype, chooseRoom } from '../../../../commands/devices/virtual' | ||
| import { chooseLocation } from '../../../../commands/locations' | ||
|
|
||
|
|
||
| jest.mock('../../../../commands/locations') | ||
| jest.mock('../../../../commands/devices/virtual') | ||
|
|
||
| describe('VirtualDeviceStandardCreateCommand', () => { | ||
| const mockInputAndOutputItem = jest.mocked(inputAndOutputItem) | ||
| const createSpy = jest.spyOn(VirtualDevicesEndpoint.prototype, 'createStandard').mockImplementation() | ||
|
|
||
| it('calls correct endpoint', async () => { | ||
| const createRequest: VirtualDeviceStandardCreateRequest = { | ||
| name: 'Device Name', | ||
| owner: { | ||
| ownerType: 'LOCATION', | ||
| ownerId: 'location-id', | ||
| }, | ||
| prototype: 'VIRTUAL_SWITCH', | ||
| } | ||
|
|
||
| mockInputAndOutputItem.mockImplementationOnce(async (_command, _config, actionFunction) => { | ||
| await actionFunction(undefined, createRequest) | ||
| }) | ||
|
|
||
| await expect(VirtualDeviceCreateStandardCommand.run([])).resolves.not.toThrow() | ||
|
|
||
| expect(createSpy).toBeCalledWith(createRequest) | ||
| }) | ||
|
|
||
| it('overwrites name, location, and room from command line', async () => { | ||
| const createRequest: VirtualDeviceStandardCreateRequest = { | ||
| name: 'Device Name', | ||
| owner: { | ||
| ownerType: 'LOCATION', | ||
| ownerId: 'location-id', | ||
| }, | ||
| prototype: 'VIRTUAL_SWITCH', | ||
| } | ||
|
|
||
| const expectedCreateRequest: VirtualDeviceStandardCreateRequest = { | ||
| name: 'NewDeviceName', | ||
| owner: { | ||
| ownerType: 'LOCATION', | ||
| ownerId: 'new-location-id', | ||
| }, | ||
| roomId: 'new-room-id', | ||
| prototype: 'VIRTUAL_SWITCH', | ||
| } | ||
|
|
||
| mockInputAndOutputItem.mockImplementationOnce(async (_command, _config, actionFunction) => { | ||
| await actionFunction(undefined, createRequest) | ||
| }) | ||
|
|
||
| await expect(VirtualDeviceCreateStandardCommand.run([ | ||
| '--name=NewDeviceName', | ||
| '--location-id=new-location-id', | ||
| '--room-id=new-room-id', | ||
| ])).resolves.not.toThrow() | ||
|
|
||
| expect(createSpy).toBeCalledWith(expectedCreateRequest) | ||
| }) | ||
|
|
||
| it('command line flag input', async () => { | ||
| const mockChooseDeviceName = jest.mocked(chooseDeviceName) | ||
| const mockChooseRoom = jest.mocked(chooseRoom) | ||
| const mockChooseLocation = jest.mocked(chooseLocation) | ||
| const mockChooseDevicePrototype = jest.mocked(chooseDevicePrototype) | ||
|
|
||
| const expectedCreateRequest: VirtualDeviceStandardCreateRequest = { | ||
| name: 'DeviceName', | ||
| owner: { | ||
| ownerType: 'LOCATION', | ||
| ownerId: 'location-id', | ||
| }, | ||
| roomId: 'room-id', | ||
| prototype: 'VIRTUAL_SWITCH', | ||
| } | ||
|
|
||
| mockChooseDeviceName.mockResolvedValueOnce('DeviceName') | ||
| mockChooseLocation.mockResolvedValueOnce('location-id') | ||
| mockChooseRoom.mockResolvedValueOnce('room-id') | ||
| mockChooseDevicePrototype.mockResolvedValueOnce('VIRTUAL_SWITCH') | ||
|
|
||
| mockInputAndOutputItem.mockImplementationOnce(async (_command, _config, actionFunction, inputProcessor) => { | ||
| const data = await inputProcessor.read() | ||
| await actionFunction(undefined, data) | ||
| }) | ||
|
|
||
| await expect(VirtualDeviceCreateStandardCommand.run([ | ||
| '--name=DeviceName', | ||
| '--location-id=location-id', | ||
| '--room-id=room-id', | ||
| '--prototype=VIRTUAL_SWITCH', | ||
| ])).resolves.not.toThrow() | ||
|
|
||
| expect(mockChooseDeviceName).toBeCalledWith(expect.any(VirtualDeviceCreateStandardCommand), 'DeviceName') | ||
| expect(mockChooseLocation).toBeCalledWith(expect.any(VirtualDeviceCreateStandardCommand), 'location-id', true) | ||
| expect(mockChooseRoom).toBeCalledWith(expect.any(VirtualDeviceCreateStandardCommand), 'location-id', 'room-id', true) | ||
| expect(mockChooseDevicePrototype).toBeCalledWith(expect.any(VirtualDeviceCreateStandardCommand), 'VIRTUAL_SWITCH') | ||
| expect(createSpy).toBeCalledWith(expectedCreateRequest) | ||
| }) | ||
| }) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we want to stick with patch for all commits so no versions get bumped while we're still considering the CLI beta. @john-u is that correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it will matter while in prerelease, it should not touch the major.minor.patch version number until we promote out of beta.