From 1972163089a36dd9f5bf51bfc09af316a783fa5f Mon Sep 17 00:00:00 2001 From: atrovato <1839717+atrovato@users.noreply.github.com> Date: Mon, 13 Mar 2023 21:39:23 +0100 Subject: [PATCH 1/2] GoogleHome curtain/shutter devices --- .../deviceTypes/googleActions.curtain.type.js | 13 ++ .../deviceTypes/googleActions.shutter.type.js | 13 ++ .../google-actions/lib/deviceTypes/index.js | 4 +- .../traits/googleActions.openClose.trait.js | 62 +++++++ .../google-actions/lib/traits/index.js | 3 +- ...tions.openCloseTrait.trait.curtain.test.js | 161 ++++++++++++++++++ ...tions.openCloseTrait.trait.shutter.test.js | 160 +++++++++++++++++ 7 files changed, 414 insertions(+), 2 deletions(-) create mode 100644 server/services/google-actions/lib/deviceTypes/googleActions.curtain.type.js create mode 100644 server/services/google-actions/lib/deviceTypes/googleActions.shutter.type.js create mode 100644 server/services/google-actions/lib/traits/googleActions.openClose.trait.js create mode 100644 server/test/services/google-actions/lib/smarthome/devices_and_traits/googleActions.openCloseTrait.trait.curtain.test.js create mode 100644 server/test/services/google-actions/lib/smarthome/devices_and_traits/googleActions.openCloseTrait.trait.shutter.test.js diff --git a/server/services/google-actions/lib/deviceTypes/googleActions.curtain.type.js b/server/services/google-actions/lib/deviceTypes/googleActions.curtain.type.js new file mode 100644 index 0000000000..7a28948e4a --- /dev/null +++ b/server/services/google-actions/lib/deviceTypes/googleActions.curtain.type.js @@ -0,0 +1,13 @@ +const { DEVICE_FEATURE_CATEGORIES } = require('../../../../utils/constants'); + +/** + * @see https://developers.google.com/assistant/smarthome/guides/curtain + */ +const curtainType = { + key: 'action.devices.types.CURTAIN', + category: DEVICE_FEATURE_CATEGORIES.CURTAIN, +}; + +module.exports = { + curtainType, +}; diff --git a/server/services/google-actions/lib/deviceTypes/googleActions.shutter.type.js b/server/services/google-actions/lib/deviceTypes/googleActions.shutter.type.js new file mode 100644 index 0000000000..f276c4148f --- /dev/null +++ b/server/services/google-actions/lib/deviceTypes/googleActions.shutter.type.js @@ -0,0 +1,13 @@ +const { DEVICE_FEATURE_CATEGORIES } = require('../../../../utils/constants'); + +/** + * @see https://developers.google.com/assistant/smarthome/guides/shutter + */ +const shutterType = { + key: 'action.devices.types.SHUTTER', + category: DEVICE_FEATURE_CATEGORIES.SHUTTER, +}; + +module.exports = { + shutterType, +}; diff --git a/server/services/google-actions/lib/deviceTypes/index.js b/server/services/google-actions/lib/deviceTypes/index.js index 7b3c35dc62..4b94edd4f4 100644 --- a/server/services/google-actions/lib/deviceTypes/index.js +++ b/server/services/google-actions/lib/deviceTypes/index.js @@ -1,4 +1,6 @@ +const { curtainType } = require('./googleActions.curtain.type'); const { lightType } = require('./googleActions.light.type'); +const { shutterType } = require('./googleActions.shutter.type'); const { switchType } = require('./googleActions.switch.type'); -module.exports = [lightType, switchType]; +module.exports = [curtainType, lightType, shutterType, switchType]; diff --git a/server/services/google-actions/lib/traits/googleActions.openClose.trait.js b/server/services/google-actions/lib/traits/googleActions.openClose.trait.js new file mode 100644 index 0000000000..0fd8b9831d --- /dev/null +++ b/server/services/google-actions/lib/traits/googleActions.openClose.trait.js @@ -0,0 +1,62 @@ +const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../../../utils/constants'); + +/** + * @see https://developers.google.com/assistant/smarthome/traits/openclose + */ +const openCloseTrait = { + key: 'action.devices.traits.OpenClose', + features: [ + { + category: DEVICE_FEATURE_CATEGORIES.CURTAIN, + type: DEVICE_FEATURE_TYPES.CURTAIN.POSITION, + }, + { + category: DEVICE_FEATURE_CATEGORIES.SHUTTER, + type: DEVICE_FEATURE_TYPES.SHUTTER.POSITION, + }, + ], + generateAttributes: (device) => { + const { has_feedback: hasFeedback = false } = device.features.find( + (feature) => + (feature.category === DEVICE_FEATURE_CATEGORIES.CURTAIN && + feature.type === DEVICE_FEATURE_TYPES.CURTAIN.POSITION) || + (feature.category === DEVICE_FEATURE_CATEGORIES.SHUTTER && + feature.type === DEVICE_FEATURE_TYPES.SHUTTER.POSITION), + ); + + return { + commandOnlyOpenClose: !hasFeedback, + }; + }, + states: [ + { + key: 'openPercent', + readValue: (feature) => { + return feature.last_value; + }, + }, + ], + commands: { + 'action.devices.commands.OpenClose': { + openPercent: { + writeValue: (paramValue) => { + return paramValue; + }, + features: [ + { + category: DEVICE_FEATURE_CATEGORIES.CURTAIN, + type: DEVICE_FEATURE_TYPES.CURTAIN.POSITION, + }, + { + category: DEVICE_FEATURE_CATEGORIES.SHUTTER, + type: DEVICE_FEATURE_TYPES.SHUTTER.POSITION, + }, + ], + }, + }, + }, +}; + +module.exports = { + openCloseTrait, +}; diff --git a/server/services/google-actions/lib/traits/index.js b/server/services/google-actions/lib/traits/index.js index 680ec482a7..aea55e0a1b 100644 --- a/server/services/google-actions/lib/traits/index.js +++ b/server/services/google-actions/lib/traits/index.js @@ -1,8 +1,9 @@ const { colorSettingTrait } = require('./googleActions.colorSetting.trait'); const { brightnessTrait } = require('./googleActions.brightness.trait'); const { onOffTrait } = require('./googleActions.on_off.trait'); +const { openCloseTrait } = require('./googleActions.openClose.trait'); -const TRAITS = [brightnessTrait, colorSettingTrait, onOffTrait]; +const TRAITS = [brightnessTrait, colorSettingTrait, onOffTrait, openCloseTrait]; const TRAIT_BY_COMMAND = {}; TRAITS.forEach((trait) => { diff --git a/server/test/services/google-actions/lib/smarthome/devices_and_traits/googleActions.openCloseTrait.trait.curtain.test.js b/server/test/services/google-actions/lib/smarthome/devices_and_traits/googleActions.openCloseTrait.trait.curtain.test.js new file mode 100644 index 0000000000..5d8d462a7b --- /dev/null +++ b/server/test/services/google-actions/lib/smarthome/devices_and_traits/googleActions.openCloseTrait.trait.curtain.test.js @@ -0,0 +1,161 @@ +const sinon = require('sinon'); +const { expect } = require('chai'); + +const { assert, fake } = sinon; +const GoogleActionsHandler = require('../../../../../../services/google-actions/lib'); +const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES, EVENTS } = require('../../../../../../utils/constants'); + +const device = { + name: 'Device 1', + selector: 'device-1', + external_id: 'device-1-external-id', + features: [ + { + category: DEVICE_FEATURE_CATEGORIES.CURTAIN, + type: DEVICE_FEATURE_TYPES.CURTAIN.POSITION, + last_value: 73, + has_feedback: true, + }, + ], + model: 'device-model', + room: { + name: 'living-room', + }, +}; + +const gladys = { + event: { + emit: fake.resolves(null), + }, + stateManager: { + get: fake.returns(device), + state: { + device: { + device_1: { + get: fake.returns(device), + }, + }, + }, + }, +}; +const serviceId = 'd1e45425-fe25-4968-ac0f-bc695d5202d9'; + +const headers = { + authentication: 'Bearer my-bearer-token', +}; +let body; +let expectedResult; + +describe('GoogleActions Handler - onSync - openClose', () => { + beforeEach(() => { + sinon.reset(); + + body = { + requestId: 'request-id', + user: { + id: 'user-id', + selector: 'user-selector', + }, + }; + + expectedResult = { + requestId: 'request-id', + payload: { + agentUserId: 'user-id', + }, + }; + }); + + it('onSync', async () => { + const googleActionsHandler = new GoogleActionsHandler(gladys, serviceId); + const result = await googleActionsHandler.onSync(body); + + expectedResult.payload.devices = [ + { + id: 'device-1', + type: 'action.devices.types.CURTAIN', + traits: ['action.devices.traits.OpenClose'], + attributes: { + commandOnlyOpenClose: false, + }, + name: { + name: 'Device 1', + }, + deviceInfo: { + model: 'device-model', + }, + roomHint: 'living-room', + willReportState: true, + }, + ]; + expect(result).to.deep.eq(expectedResult); + assert.calledOnce(gladys.stateManager.state.device.device_1.get); + }); + + it('onQuery', async () => { + body.inputs = [ + { + payload: { + devices: [ + { + id: 'device-1', + }, + ], + }, + }, + ]; + + const googleActionsHandler = new GoogleActionsHandler(gladys, serviceId); + const result = await googleActionsHandler.onQuery(body, headers); + + expectedResult.payload.devices = { + 'device-1': { + online: true, + openPercent: 73, + }, + }; + expect(result).to.deep.eq(expectedResult); + assert.calledOnce(gladys.stateManager.get); + }); + + it('onExecute', async () => { + body.inputs = [ + { + payload: { + commands: [ + { + devices: [{ id: 'device-1' }], + execution: [ + { + command: 'action.devices.commands.OpenClose', + params: { + openPercent: 12, + }, + }, + ], + }, + ], + }, + }, + ]; + + const googleActionsHandler = new GoogleActionsHandler(gladys, serviceId); + const result = await googleActionsHandler.onExecute(body, headers); + + expectedResult.payload.commands = [ + { + ids: ['device-1'], + status: 'PENDING', + }, + ]; + expect(result).to.deep.eq(expectedResult); + assert.calledWith(gladys.event.emit, EVENTS.ACTION.TRIGGERED, { + device: 'device-1', + feature_category: DEVICE_FEATURE_CATEGORIES.CURTAIN, + feature_type: DEVICE_FEATURE_TYPES.CURTAIN.POSITION, + status: 'pending', + type: 'device.set-value', + value: 12, + }); + }); +}); diff --git a/server/test/services/google-actions/lib/smarthome/devices_and_traits/googleActions.openCloseTrait.trait.shutter.test.js b/server/test/services/google-actions/lib/smarthome/devices_and_traits/googleActions.openCloseTrait.trait.shutter.test.js new file mode 100644 index 0000000000..52552b290d --- /dev/null +++ b/server/test/services/google-actions/lib/smarthome/devices_and_traits/googleActions.openCloseTrait.trait.shutter.test.js @@ -0,0 +1,160 @@ +const sinon = require('sinon'); +const { expect } = require('chai'); + +const { assert, fake } = sinon; +const GoogleActionsHandler = require('../../../../../../services/google-actions/lib'); +const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES, EVENTS } = require('../../../../../../utils/constants'); + +const device = { + name: 'Device 1', + selector: 'device-1', + external_id: 'device-1-external-id', + features: [ + { + category: DEVICE_FEATURE_CATEGORIES.SHUTTER, + type: DEVICE_FEATURE_TYPES.SHUTTER.POSITION, + last_value: 73, + }, + ], + model: 'device-model', + room: { + name: 'living-room', + }, +}; + +const gladys = { + event: { + emit: fake.resolves(null), + }, + stateManager: { + get: fake.returns(device), + state: { + device: { + device_1: { + get: fake.returns(device), + }, + }, + }, + }, +}; +const serviceId = 'd1e45425-fe25-4968-ac0f-bc695d5202d9'; + +const headers = { + authentication: 'Bearer my-bearer-token', +}; +let body; +let expectedResult; + +describe('GoogleActions Handler - onSync - openClose', () => { + beforeEach(() => { + sinon.reset(); + + body = { + requestId: 'request-id', + user: { + id: 'user-id', + selector: 'user-selector', + }, + }; + + expectedResult = { + requestId: 'request-id', + payload: { + agentUserId: 'user-id', + }, + }; + }); + + it('onSync', async () => { + const googleActionsHandler = new GoogleActionsHandler(gladys, serviceId); + const result = await googleActionsHandler.onSync(body); + + expectedResult.payload.devices = [ + { + id: 'device-1', + type: 'action.devices.types.SHUTTER', + traits: ['action.devices.traits.OpenClose'], + attributes: { + commandOnlyOpenClose: true, + }, + name: { + name: 'Device 1', + }, + deviceInfo: { + model: 'device-model', + }, + roomHint: 'living-room', + willReportState: true, + }, + ]; + expect(result).to.deep.eq(expectedResult); + assert.calledOnce(gladys.stateManager.state.device.device_1.get); + }); + + it('onQuery', async () => { + body.inputs = [ + { + payload: { + devices: [ + { + id: 'device-1', + }, + ], + }, + }, + ]; + + const googleActionsHandler = new GoogleActionsHandler(gladys, serviceId); + const result = await googleActionsHandler.onQuery(body, headers); + + expectedResult.payload.devices = { + 'device-1': { + online: true, + openPercent: 73, + }, + }; + expect(result).to.deep.eq(expectedResult); + assert.calledOnce(gladys.stateManager.get); + }); + + it('onExecute', async () => { + body.inputs = [ + { + payload: { + commands: [ + { + devices: [{ id: 'device-1' }], + execution: [ + { + command: 'action.devices.commands.OpenClose', + params: { + openPercent: 12, + }, + }, + ], + }, + ], + }, + }, + ]; + + const googleActionsHandler = new GoogleActionsHandler(gladys, serviceId); + const result = await googleActionsHandler.onExecute(body, headers); + + expectedResult.payload.commands = [ + { + ids: ['device-1'], + status: 'PENDING', + }, + ]; + expect(result).to.deep.eq(expectedResult); + assert.calledWith(gladys.event.emit, EVENTS.ACTION.TRIGGERED, { + device: 'device-1', + feature_category: DEVICE_FEATURE_CATEGORIES.SHUTTER, + feature_type: DEVICE_FEATURE_TYPES.SHUTTER.POSITION, + status: 'pending', + type: 'device.set-value', + value: 12, + }); + }); +}); From 3584ca7a47fea9b0619abe08b3deb7b01383b5a6 Mon Sep 17 00:00:00 2001 From: atrovato <1839717+atrovato@users.noreply.github.com> Date: Sat, 14 May 2022 21:24:12 +0200 Subject: [PATCH 2/2] Add television binary --- .../lib/deviceTypes/googleActions.tv.type.js | 13 ++ .../google-actions/lib/deviceTypes/index.js | 3 +- .../lib/traits/googleActions.on_off.trait.js | 8 + .../googleActions.onOff.trait.tv.test.js | 155 ++++++++++++++++++ .../smarthome/googleActions.onExecute.test.js | 2 +- 5 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 server/services/google-actions/lib/deviceTypes/googleActions.tv.type.js create mode 100644 server/test/services/google-actions/lib/smarthome/devices_and_traits/googleActions.onOff.trait.tv.test.js diff --git a/server/services/google-actions/lib/deviceTypes/googleActions.tv.type.js b/server/services/google-actions/lib/deviceTypes/googleActions.tv.type.js new file mode 100644 index 0000000000..2586146df2 --- /dev/null +++ b/server/services/google-actions/lib/deviceTypes/googleActions.tv.type.js @@ -0,0 +1,13 @@ +const { DEVICE_FEATURE_CATEGORIES } = require('../../../../utils/constants'); + +/** + * @see https://developers.google.com/assistant/smarthome/guides/tv + */ +const televisionType = { + key: 'action.devices.types.TV', + category: DEVICE_FEATURE_CATEGORIES.TELEVISION, +}; + +module.exports = { + televisionType, +}; diff --git a/server/services/google-actions/lib/deviceTypes/index.js b/server/services/google-actions/lib/deviceTypes/index.js index 4b94edd4f4..3c3f4e8610 100644 --- a/server/services/google-actions/lib/deviceTypes/index.js +++ b/server/services/google-actions/lib/deviceTypes/index.js @@ -2,5 +2,6 @@ const { curtainType } = require('./googleActions.curtain.type'); const { lightType } = require('./googleActions.light.type'); const { shutterType } = require('./googleActions.shutter.type'); const { switchType } = require('./googleActions.switch.type'); +const { televisionType } = require('./googleActions.tv.type'); -module.exports = [curtainType, lightType, shutterType, switchType]; +module.exports = [curtainType, lightType, shutterType, switchType, televisionType]; diff --git a/server/services/google-actions/lib/traits/googleActions.on_off.trait.js b/server/services/google-actions/lib/traits/googleActions.on_off.trait.js index 0d4b2641b0..0fb02f78db 100644 --- a/server/services/google-actions/lib/traits/googleActions.on_off.trait.js +++ b/server/services/google-actions/lib/traits/googleActions.on_off.trait.js @@ -14,6 +14,10 @@ const onOffTrait = { category: DEVICE_FEATURE_CATEGORIES.LIGHT, type: DEVICE_FEATURE_TYPES.LIGHT.BINARY, }, + { + category: DEVICE_FEATURE_CATEGORIES.TELEVISION, + type: DEVICE_FEATURE_TYPES.TELEVISION.BINARY, + }, ], states: [ { @@ -38,6 +42,10 @@ const onOffTrait = { category: DEVICE_FEATURE_CATEGORIES.LIGHT, type: DEVICE_FEATURE_TYPES.LIGHT.BINARY, }, + { + category: DEVICE_FEATURE_CATEGORIES.TELEVISION, + type: DEVICE_FEATURE_TYPES.TELEVISION.BINARY, + }, ], }, }, diff --git a/server/test/services/google-actions/lib/smarthome/devices_and_traits/googleActions.onOff.trait.tv.test.js b/server/test/services/google-actions/lib/smarthome/devices_and_traits/googleActions.onOff.trait.tv.test.js new file mode 100644 index 0000000000..7e54a341c6 --- /dev/null +++ b/server/test/services/google-actions/lib/smarthome/devices_and_traits/googleActions.onOff.trait.tv.test.js @@ -0,0 +1,155 @@ +const sinon = require('sinon'); +const { expect } = require('chai'); + +const { assert, fake } = sinon; +const GoogleActionsHandler = require('../../../../../../services/google-actions/lib'); +const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES, EVENTS } = require('../../../../../../utils/constants'); + +const device = { + name: 'Device 1', + selector: 'device-1', + external_id: 'device-1-external-id', + features: [ + { + category: DEVICE_FEATURE_CATEGORIES.TELEVISION, + type: DEVICE_FEATURE_TYPES.TELEVISION.BINARY, + last_value: 1, + }, + ], + model: 'device-model', + room: { + name: 'living-room', + }, +}; + +const gladys = { + event: { + emit: fake.resolves(null), + }, + stateManager: { + get: fake.returns(device), + state: { + device: { + device_1: { + get: fake.returns(device), + }, + }, + }, + }, +}; +const serviceId = 'd1e45425-fe25-4968-ac0f-bc695d5202d9'; + +let body; +let expectedResult; + +describe('GoogleActions Handler - onSync - onOff (tv)', () => { + beforeEach(() => { + sinon.reset(); + + body = { + requestId: 'request-id', + user: { + id: 'user-id', + selector: 'user-selector', + }, + }; + + expectedResult = { + requestId: 'request-id', + payload: { + agentUserId: 'user-id', + }, + }; + }); + + it('onSync', async () => { + const googleActionsHandler = new GoogleActionsHandler(gladys, serviceId); + const result = await googleActionsHandler.onSync(body); + + expectedResult.payload.devices = [ + { + id: 'device-1', + type: 'action.devices.types.TV', + traits: ['action.devices.traits.OnOff'], + attributes: {}, + name: { + name: 'Device 1', + }, + deviceInfo: { + model: 'device-model', + }, + roomHint: 'living-room', + willReportState: true, + }, + ]; + expect(result).to.deep.eq(expectedResult); + assert.calledOnce(gladys.stateManager.state.device.device_1.get); + }); + + it('onQuery', async () => { + body.inputs = [ + { + payload: { + devices: [ + { + id: 'device-1', + }, + ], + }, + }, + ]; + + const googleActionsHandler = new GoogleActionsHandler(gladys, serviceId); + const result = await googleActionsHandler.onQuery(body); + + expectedResult.payload.devices = { + 'device-1': { + online: true, + on: true, + }, + }; + expect(result).to.deep.eq(expectedResult); + assert.calledOnce(gladys.stateManager.get); + }); + + it('onExecute', async () => { + body.inputs = [ + { + payload: { + commands: [ + { + devices: [{ id: 'device-1' }], + execution: [ + { + command: 'action.devices.commands.OnOff', + params: { + on: false, + }, + }, + ], + }, + ], + }, + }, + ]; + + const googleActionsHandler = new GoogleActionsHandler(gladys, serviceId); + const result = await googleActionsHandler.onExecute(body); + + expectedResult.payload.commands = [ + { + ids: ['device-1'], + status: 'PENDING', + }, + ]; + expect(result).to.deep.eq(expectedResult); + assert.calledWith(gladys.event.emit, EVENTS.ACTION.TRIGGERED, { + device: 'device-1', + feature_category: DEVICE_FEATURE_CATEGORIES.TELEVISION, + feature_type: DEVICE_FEATURE_TYPES.TELEVISION.BINARY, + status: 'pending', + type: 'device.set-value', + value: 0, + }); + }); +}); diff --git a/server/test/services/google-actions/lib/smarthome/googleActions.onExecute.test.js b/server/test/services/google-actions/lib/smarthome/googleActions.onExecute.test.js index 8ac8091656..b293ed5f9d 100644 --- a/server/test/services/google-actions/lib/smarthome/googleActions.onExecute.test.js +++ b/server/test/services/google-actions/lib/smarthome/googleActions.onExecute.test.js @@ -245,6 +245,6 @@ describe('GoogleActions Handler - onExecute', () => { }, }; expect(result).to.deep.eq(exptectedResult); - assert.callCount(gladys.event.emit, 2); + assert.callCount(gladys.event.emit, 3); }); });