Skip to content

Commit 383c03e

Browse files
committed
feat: added support for virtual devices
1 parent b4416a0 commit 383c03e

File tree

4 files changed

+235
-0
lines changed

4 files changed

+235
-0
lines changed

src/endpoint/virtualdevices.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { Endpoint} from '../endpoint'
2+
import { EndpointClient, EndpointClientConfig, HttpClientParams } from '../endpoint-client'
3+
import { CommandMapping, Device, DeviceEvent } from './devices'
4+
import { DeviceProfileCreateRequest } from './deviceprofiles'
5+
6+
7+
export interface VirtualDeviceOwner {
8+
ownerType: VirtualDeviceOwnerTypeEnum
9+
ownerId: string
10+
}
11+
12+
export type VirtualDeviceOwnerTypeEnum = 'USER' | 'LOCATION'
13+
14+
export type ExecutionTarget = 'CLOUD' | 'LOCAL'
15+
16+
export interface VirtualDeviceCreateRequest {
17+
name: string
18+
owner: VirtualDeviceOwner
19+
deviceProfileId?: string
20+
deviceProfile?: DeviceProfileCreateRequest
21+
roomId?: string
22+
commandMappings?: CommandMapping
23+
executionTarget?: ExecutionTarget
24+
hubId?: string
25+
driverId?: string
26+
}
27+
28+
export interface VirtualDeviceStandardCreateRequest {
29+
name: string
30+
owner: VirtualDeviceOwner
31+
prototype: string
32+
roomId?: string
33+
executionTarget?: ExecutionTarget
34+
hubId?: string
35+
driverId?: string
36+
}
37+
38+
export interface VirtualDeviceListOptions {
39+
locationId?: string
40+
}
41+
42+
export interface VirtualDeviceEventsResponse {
43+
stateChanges: boolean[]
44+
}
45+
46+
export class VirtualDevicesEndpoint extends Endpoint {
47+
constructor(config: EndpointClientConfig) {
48+
super(new EndpointClient('virtualdevices', config))
49+
}
50+
51+
/**
52+
* Returns list of virtual devices.
53+
* @param options map of filter options. Currently only 'locationId' is supported.
54+
*/
55+
public list(options: VirtualDeviceListOptions = {}): Promise<Device[]> {
56+
const params: HttpClientParams = {}
57+
if ('locationId' in options && options.locationId) {
58+
params.locationId = options.locationId
59+
} else if (this.client.config.locationId) {
60+
params.locationId = this.client.config.locationId
61+
}
62+
return this.client.getPagedItems<Device>(undefined, params)
63+
}
64+
65+
/**
66+
* Create a virtual device from a device profile. An existing device profile can be designated by ID, or the
67+
* definition of a device profile can be provided inline.
68+
*/
69+
public create(definition: VirtualDeviceCreateRequest): Promise<Device> {
70+
return this.client.post<Device>('', definition)
71+
}
72+
73+
/**
74+
* Creates a virtual device from a standard prototype.
75+
*/
76+
public createStandard(definition: VirtualDeviceStandardCreateRequest): Promise<Device> {
77+
return this.client.post<Device>('prototypes', definition)
78+
}
79+
80+
/**
81+
* Creates events for the specified device
82+
* @param id UUID of the device
83+
* @param deviceEvents list of events
84+
*/
85+
public createEvents(id: string, deviceEvents: DeviceEvent[]): Promise<VirtualDeviceEventsResponse> {
86+
return this.client.post(`${id}/events`, { deviceEvents })
87+
}
88+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ export * from './endpoint/schedules'
2929
export * from './endpoint/schema'
3030
export * from './endpoint/services'
3131
export * from './endpoint/subscriptions'
32+
export * from './endpoint/virtualdevices'

src/st-client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { SubscriptionsEndpoint } from './endpoint/subscriptions'
2323
import { SchedulesEndpoint } from './endpoint/schedules'
2424
import { SchemaEndpoint } from './endpoint/schema'
2525
import { ServicesEndpoint } from './endpoint/services'
26+
import { VirtualDevicesEndpoint } from './endpoint/virtualdevices'
2627
import { SmartThingsURLProvider, defaultSmartThingsURLProvider, HttpClientHeaders } from './endpoint-client'
2728

2829

@@ -48,6 +49,7 @@ export class SmartThingsClient extends RESTClient {
4849
public readonly schedules: SchedulesEndpoint
4950
public readonly schema: SchemaEndpoint
5051
public readonly services: ServicesEndpoint
52+
public readonly virtualDevices: VirtualDevicesEndpoint
5153

5254
constructor(authenticator: Authenticator, config?: RESTClientConfig) {
5355
super(authenticator, config)
@@ -73,6 +75,7 @@ export class SmartThingsClient extends RESTClient {
7375
this.schedules = new SchedulesEndpoint(this.config)
7476
this.schema = new SchemaEndpoint(this.config)
7577
this.services = new ServicesEndpoint(this.config)
78+
this.virtualDevices = new VirtualDevicesEndpoint(this.config)
7679
}
7780

7881
public setLocation(id: string): SmartThingsClient {

test/unit/virtualdevices.test.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { Device, DeviceEvent } from '../../src/endpoint/devices'
2+
import {
3+
VirtualDeviceCreateRequest,
4+
VirtualDeviceStandardCreateRequest,
5+
VirtualDevicesEndpoint,
6+
} from '../../src/endpoint/virtualdevices'
7+
import { BearerTokenAuthenticator } from '../../src/authenticator'
8+
import { EndpointClient } from '../../src/endpoint-client'
9+
10+
11+
const authenticator = new BearerTokenAuthenticator('00000000-0000-0000-0000-000000000000')
12+
13+
describe('VirtualDevicesEndpoint', () => {
14+
afterEach(() => {
15+
jest.clearAllMocks()
16+
})
17+
18+
const postSpy = jest.spyOn(EndpointClient.prototype, 'post').mockImplementation()
19+
const getPagedItemsSpy = jest.spyOn(EndpointClient.prototype, 'getPagedItems').mockImplementation()
20+
21+
const virtualDevicesEndpoint = new VirtualDevicesEndpoint({ authenticator })
22+
23+
const deviceList = [{ listed: 'device' }] as unknown as Device[]
24+
25+
describe('list', () => {
26+
getPagedItemsSpy.mockResolvedValue(deviceList)
27+
28+
it('works without options', async () => {
29+
expect(await virtualDevicesEndpoint.list()).toBe(deviceList)
30+
31+
expect(getPagedItemsSpy).toHaveBeenCalledTimes(1)
32+
expect(getPagedItemsSpy).toHaveBeenCalledWith(undefined, {})
33+
})
34+
35+
it('includes configured locationId', async () => {
36+
const devices = new VirtualDevicesEndpoint({ authenticator, locationId: 'configured-location-id' })
37+
expect(await devices.list()).toBe(deviceList)
38+
39+
expect(getPagedItemsSpy).toHaveBeenCalledTimes(1)
40+
expect(getPagedItemsSpy).toHaveBeenCalledWith(undefined, { locationId: 'configured-location-id' })
41+
})
42+
43+
it('include wanted locationId', async () => {
44+
expect(await virtualDevicesEndpoint.list({ locationId: 'wanted-locationId' })).toBe(deviceList)
45+
46+
expect(getPagedItemsSpy).toHaveBeenCalledTimes(1)
47+
expect(getPagedItemsSpy).toHaveBeenCalledWith(undefined, { locationId: 'wanted-locationId' })
48+
})
49+
})
50+
51+
describe('create', () => {
52+
it('creates from device profile ID', async () => {
53+
const device = { new: 'device' }
54+
postSpy.mockResolvedValueOnce(device)
55+
56+
const deviceCreate: VirtualDeviceCreateRequest = {
57+
owner: {
58+
ownerId: 'owner-id',
59+
ownerType: 'LOCATION',
60+
},
61+
name: 'Living room light',
62+
roomId: 'room-id',
63+
deviceProfileId: 'profile-id',
64+
}
65+
66+
const expectedData = deviceCreate
67+
68+
expect(await virtualDevicesEndpoint.create(deviceCreate)).toBe(device)
69+
70+
expect(postSpy).toHaveBeenCalledTimes(1)
71+
expect(postSpy).toHaveBeenCalledWith('', expectedData)
72+
})
73+
74+
it('creates from device profile definition', async () => {
75+
const device = { new: 'device' }
76+
postSpy.mockResolvedValueOnce(device)
77+
78+
const deviceCreate: VirtualDeviceCreateRequest = {
79+
owner: {
80+
ownerId: 'owner-id',
81+
ownerType: 'LOCATION',
82+
},
83+
name: 'Living room light',
84+
roomId: 'room-id',
85+
deviceProfile: {
86+
'components': [
87+
{
88+
'id': 'main',
89+
'capabilities': [
90+
{
91+
'id': 'switch',
92+
'version': 1,
93+
},
94+
],
95+
'categories': [],
96+
},
97+
],
98+
},
99+
}
100+
101+
const expectedData = deviceCreate
102+
103+
expect(await virtualDevicesEndpoint.create(deviceCreate)).toBe(device)
104+
105+
expect(postSpy).toHaveBeenCalledTimes(1)
106+
expect(postSpy).toHaveBeenCalledWith('', expectedData)
107+
})
108+
})
109+
110+
describe('createStandard', () => {
111+
it('creates from prototype', async () => {
112+
const device = {new: 'device'}
113+
postSpy.mockResolvedValueOnce(device)
114+
115+
const deviceCreate: VirtualDeviceStandardCreateRequest = {
116+
owner: {
117+
ownerId: 'owner-id',
118+
ownerType: 'LOCATION',
119+
},
120+
name: 'Living room light',
121+
roomId: 'room-id',
122+
prototype: 'SWITCH',
123+
}
124+
125+
const expectedData = deviceCreate
126+
127+
expect(await virtualDevicesEndpoint.createStandard(deviceCreate)).toBe(device)
128+
129+
expect(postSpy).toHaveBeenCalledTimes(1)
130+
expect(postSpy).toHaveBeenCalledWith('prototypes', expectedData)
131+
})
132+
})
133+
134+
test('createEvents', async () => {
135+
const events: DeviceEvent[] = [{ component: 'main' } as DeviceEvent]
136+
postSpy.mockResolvedValueOnce([true])
137+
138+
expect(await virtualDevicesEndpoint.createEvents('device-id', events)).toStrictEqual([true])
139+
140+
expect(postSpy).toHaveBeenCalledTimes(1)
141+
expect(postSpy).toHaveBeenCalledWith('device-id/events', { deviceEvents: events })
142+
})
143+
})

0 commit comments

Comments
 (0)