Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions companion/lib/Service/HttpApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,9 @@ export class ServiceHttpApi {
this.#apiRouter.route('/variable/:label/:name/value').get(this.#moduleVariableGetValue)

// surfaces
this.#apiRouter.get('/surfaces', this.#surfacesList)
this.#apiRouter.post('/surfaces/rescan', this.#surfacesRescan)
this.#apiRouter.post('/surfaces/:id/brightness', this.#surfaceSetBrightness)

// connections
this.#apiRouter.get('/connections', this.#connectionsList)
Expand Down Expand Up @@ -349,6 +351,61 @@ export class ServiceHttpApi {
)
}

/**
* List all surfaces
*/
#surfacesList = (_req: Express.Request, res: Express.Response): void => {
this.logger.debug('Got HTTP GET /api/surfaces')

const surfaces = this.#serviceApi.getSurfacesList().flatMap((group) => {
// All surfaces in a group share the same page, so resolve it once per group
const page = this.#serviceApi.getSurfacePage(group.id)
return group.surfaces.map((surface) => ({
id: surface.id,
type: surface.type,
integrationType: surface.integrationType,
name: surface.name,
displayName: surface.displayName,
isConnected: surface.isConnected,
size: surface.size,
brightness: surface.brightness,
page,
}))
})

res.json(surfaces)
}

/**
* Set the brightness of a surface
*/
#surfaceSetBrightness = (req: Express.Request, res: Express.Response): void => {
const surfaceId = req.params.id

const rawValue = req.query.brightness
const brightness = Number(rawValue)

this.logger.debug(`Got HTTP surface set brightness "${surfaceId}" to ${JSON.stringify(rawValue)}`)

if (
rawValue === undefined ||
rawValue === '' ||
!Number.isFinite(brightness) ||
brightness < 0 ||
brightness > 100
) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
res.status(400).send('Invalid brightness')
return
}

if (!this.#serviceApi.surfaceSetBrightness(surfaceId, brightness)) {
res.status(404).send('Not found')
return
}

res.send('ok')
}

/**
* Perform surfaces rescan
*/
Expand Down
20 changes: 20 additions & 0 deletions companion/lib/Service/ServiceApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ClientConnectionConfig } from '@companion-app/shared/Model/Connect
import type { CustomVariablesModel } from '@companion-app/shared/Model/CustomVariableModel.js'
import type { InstanceStatusEntry } from '@companion-app/shared/Model/InstanceStatus.js'
import type { ButtonStyleProperties } from '@companion-app/shared/Model/StyleModel.js'
import type { ClientDevicesListItem } from '@companion-app/shared/Model/Surfaces.js'
import type { ModuleVariableDefinitions, VariableValue } from '@companion-app/shared/Model/Variables.js'
import type { ControlCommonEvents } from '../Controls/ControlDependencies.js'
import type { IControlStore } from '../Controls/IControlStore.js'
Expand Down Expand Up @@ -226,6 +227,25 @@ export class ServiceApi extends EventEmitter<ServiceApiEvents> {
this.#surfaceController.devicePageDown(surfaceId)
}

surfaceSetBrightness(surfaceId: string, brightness: number): boolean {
return this.#surfaceController.setDeviceBrightness(surfaceId, brightness, true)
}

getSurfacesList(): ClientDevicesListItem[] {
return this.#surfaceController.getDevicesList()
}

/**
* Get the current page (id, number and name) a surface is showing
*/
getSurfacePage(surfaceId: string): { id: string; number: number | null; name: string } | null {
const pageId = this.#surfaceController.devicePageGet(surfaceId, true)
if (!pageId) return null
const number = this.getPageNumberForId(pageId)
const name = number !== null ? (this.#pageStore.getPageInfo(number)?.name ?? '') : ''
return { id: pageId, number, name }
}

getCachedRenderOrGeneratePlaceholder(location: ControlLocation): ImageResult {
return this.#graphicsController.getCachedRenderOrGeneratePlaceholder(location)
}
Expand Down
9 changes: 5 additions & 4 deletions companion/lib/Surface/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1927,12 +1927,13 @@ export class SurfaceController extends EventEmitter<SurfaceControllerEvents> {
* @param surfaceId
* @param brightness 0-100
* @param looseIdMatching
* @returns whether a matching surface was found
*/
setDeviceBrightness(surfaceId: string, brightness: number, looseIdMatching = false): void {
setDeviceBrightness(surfaceId: string, brightness: number, looseIdMatching = false): boolean {
const device = this.#getSurfaceHandlerForId(surfaceId, looseIdMatching)
if (device) {
device.setBrightness(brightness)
}
if (!device) return false
device.setBrightness(brightness)
return true
}

/**
Expand Down
100 changes: 100 additions & 0 deletions companion/test/Service/HttpApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,106 @@ describe('HttpApi', () => {
expect(res.text).toBe('fail')
})
})

describe('list', () => {
test('ok', async () => {
const { app, serviceApi } = createService()
serviceApi.getSurfacesList.mockReturnValue([
{
id: 'group',
surfaces: [
{
id: 'emulator',
type: 'Emulator',
integrationType: 'emulator',
name: '',
displayName: 'Emulator (emulator)',
isConnected: true,
size: { rows: 4, columns: 8 },
brightness: 50,
},
],
},
] as any)
serviceApi.getSurfacePage.mockReturnValue({ id: 'abcd1234', number: 1, name: 'Main' })

const res = await supertest(app).get('/api/surfaces').send()
expect(res.status).toBe(200)
expect(res.body).toEqual([
{
id: 'emulator',
type: 'Emulator',
integrationType: 'emulator',
name: '',
displayName: 'Emulator (emulator)',
isConnected: true,
size: { rows: 4, columns: 8 },
brightness: 50,
page: { id: 'abcd1234', number: 1, name: 'Main' },
},
])

expect(serviceApi.getSurfacePage).toHaveBeenCalledWith('group')

expect(serviceApi.getSurfacesList).toHaveBeenCalledTimes(1)
})
})

describe('set brightness', () => {
test('ok from query', async () => {
const { app, serviceApi } = createService()
serviceApi.surfaceSetBrightness.mockReturnValue(true)

const res = await supertest(app).post('/api/surfaces/emulator/brightness?brightness=30').send()
expect(res.status).toBe(200)
expect(res.text).toBe('ok')

expect(serviceApi.surfaceSetBrightness).toHaveBeenCalledTimes(1)
expect(serviceApi.surfaceSetBrightness).toHaveBeenCalledWith('emulator', 30)
})

test('unknown surface', async () => {
const { app, serviceApi } = createService()
serviceApi.surfaceSetBrightness.mockReturnValue(false)

const res = await supertest(app).post('/api/surfaces/does-not-exist/brightness?brightness=30').send()
expect(res.status).toBe(404)
expect(res.text).toBe('Not found')

expect(serviceApi.surfaceSetBrightness).toHaveBeenCalledTimes(1)
expect(serviceApi.surfaceSetBrightness).toHaveBeenCalledWith('does-not-exist', 30)
})

test('no value', async () => {
const { app, serviceApi } = createService()

const res = await supertest(app).post('/api/surfaces/emulator/brightness').send()
expect(res.status).toBe(400)
expect(res.text).toBe('Invalid brightness')

expect(serviceApi.surfaceSetBrightness).toHaveBeenCalledTimes(0)
})

test('out of range', async () => {
const { app, serviceApi } = createService()

const res = await supertest(app).post('/api/surfaces/emulator/brightness?brightness=150').send()
expect(res.status).toBe(400)
expect(res.text).toBe('Invalid brightness')

expect(serviceApi.surfaceSetBrightness).toHaveBeenCalledTimes(0)
})

test('not a number', async () => {
const { app, serviceApi } = createService()

const res = await supertest(app).post('/api/surfaces/emulator/brightness?brightness=abc').send()
expect(res.status).toBe(400)
expect(res.text).toBe('Invalid brightness')

expect(serviceApi.surfaceSetBrightness).toHaveBeenCalledTimes(0)
})
})
})

describe('custom-variable', () => {
Expand Down
35 changes: 35 additions & 0 deletions docs/user-guide/5_remote-control/http-remote-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,35 @@ This API tries to follow REST principles, and the convention that a `POST` reque
Method: POST
Path: `/api/surfaces/rescan`

### Surfaces

- List all surfaces
Method: GET
Path: `/api/surfaces`
Response: JSON array of surface objects, each with `id`, `type`, `integrationType`, `name`, `displayName`, `isConnected`, `size` (`{rows, columns}` or `null`), `brightness` (0-100, or `null` if unknown), and `page` — an object `{id, number, name}` describing the current page (or `null` if unknown).

```json
[
{
"id": "emulator:emulator",
"type": "Emulator",
"integrationType": "emulator",
"name": "",
"displayName": "Emulator (emulator:emulator)",
"isConnected": true,
"size": { "rows": 4, "columns": 8 },
"brightness": 100,
"page": { "id": "abcd1234", "number": 1, "name": "Main" }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
]
```

- Set the brightness of a surface
Method: POST
Path: `/api/surfaces/<surface id>/brightness?brightness=<0-100>`
Error (400): `Invalid brightness` if the value is missing or outside 0-100
Error (404): `Not found` if no connected surface matches the given id
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### Connection Management

- List all connections with their current status
Expand Down Expand Up @@ -154,6 +183,12 @@ Content-Type `application/json`
Body: `{"name":"Douglas", "answer":42}` - Body needs to be a valid JSON.
The object will be stored in the variable value and will not be converted to a string. You can also use the data types boolean, number, array or null. JSON does not support sending undefined as a value, but we interpret an empty body as undefined, properties of an object can of course be undefined.

List all surfaces:
GET `/api/surfaces`

Set the brightness of the emulator surface to 30%:
POST `/api/surfaces/emulator/brightness?brightness=30`

List all connections:
GET `/api/connections`

Expand Down
Loading