Skip to content

Commit

Permalink
Google Home: Add Curtain/Shutter devices (#1752)
Browse files Browse the repository at this point in the history
  • Loading branch information
atrovato authored May 18, 2023
1 parent 0ae4d71 commit 2725637
Show file tree
Hide file tree
Showing 8 changed files with 624 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -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,
};
Original file line number Diff line number Diff line change
@@ -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,
};
4 changes: 3 additions & 1 deletion server/services/google-actions/lib/deviceTypes/index.js
Original file line number Diff line number Diff line change
@@ -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];
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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 attributes = {
discreteOnlyOpenClose: true,
queryOnlyOpenClose: true,
};

const positionFeature = device.features.find(
({ category, type }) =>
(category === DEVICE_FEATURE_CATEGORIES.CURTAIN && type === DEVICE_FEATURE_TYPES.CURTAIN.POSITION) ||
(category === DEVICE_FEATURE_CATEGORIES.SHUTTER && type === DEVICE_FEATURE_TYPES.SHUTTER.POSITION),
);
if (positionFeature) {
const { read_only: readOnly } = positionFeature;

attributes.discreteOnlyOpenClose = false;
attributes.queryOnlyOpenClose = readOnly;
}

return attributes;
},
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,
},
],
},
followUpToken: {
writeValue: () => null,
features: [],
},
},
},
};

module.exports = {
openCloseTrait,
};
7 changes: 4 additions & 3 deletions server/services/google-actions/lib/traits/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
const { colorSettingTrait } = require('./googleActions.colorSetting.trait');
const { brightnessTrait } = require('./googleActions.brightness.trait');
const { onOffTrait } = require('./googleActions.on_off.trait');
const { colorSettingTrait } = require('./googleActions.colorSetting.trait');
const { onOffTrait } = require('./googleActions.onOff.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) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
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 serviceId = 'd1e45425-fe25-4968-ac0f-bc695d5202d9';
const headers = {
authentication: 'Bearer my-bearer-token',
};

describe('GoogleActions Handler - onSync - openClose - curtain', () => {
let gladys;
let device;
let body;
let expectedResult;

beforeEach(() => {
device = {
name: 'Device 1',
selector: 'device-1',
external_id: 'device-1-external-id',
features: [],
model: 'device-model',
room: {
name: 'living-room',
},
};

gladys = {
event: {
emit: fake.resolves(null),
},
stateManager: {
get: fake.returns(device),
state: {
device: {
device_1: {
get: fake.returns(device),
},
},
},
},
};

body = {
requestId: 'request-id',
user: {
id: 'user-id',
selector: 'user-selector',
},
};

expectedResult = {
requestId: 'request-id',
payload: {
agentUserId: 'user-id',
},
};
});

afterEach(() => {
sinon.reset();
});

it('onSync - position only - actionnable', async () => {
device.features = [
{
category: DEVICE_FEATURE_CATEGORIES.CURTAIN,
type: DEVICE_FEATURE_TYPES.CURTAIN.POSITION,
last_value: 73,
read_only: false,
},
];

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: {
discreteOnlyOpenClose: false,
queryOnlyOpenClose: 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('onSync - position only - not actionnable', async () => {
device.features = [
{
category: DEVICE_FEATURE_CATEGORIES.CURTAIN,
type: DEVICE_FEATURE_TYPES.CURTAIN.POSITION,
last_value: 73,
read_only: true,
},
];

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: {
discreteOnlyOpenClose: false,
queryOnlyOpenClose: 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 - with position', async () => {
device.features = [
{
category: DEVICE_FEATURE_CATEGORIES.CURTAIN,
type: DEVICE_FEATURE_TYPES.CURTAIN.POSITION,
last_value: 73,
read_only: false,
},
];

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 - openPercent', async () => {
body.inputs = [
{
payload: {
commands: [
{
devices: [{ id: 'device-1' }],
execution: [
{
command: 'action.devices.commands.OpenClose',
params: {
openPercent: 73,
},
},
],
},
],
},
},
];

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.calledTwice(gladys.event.emit);
assert.calledWithExactly(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: 73,
});
assert.calledWithExactly(gladys.event.emit, EVENTS.ACTION.TRIGGERED, {
device: 'device-1',
feature_category: DEVICE_FEATURE_CATEGORIES.SHUTTER,
feature_type: DEVICE_FEATURE_TYPES.CURTAIN.POSITION,
status: 'pending',
type: 'device.set-value',
value: 73,
});
});

it('onExecute - followUpToken', async () => {
body.inputs = [
{
payload: {
commands: [
{
devices: [{ id: 'device-1' }],
execution: [
{
command: 'action.devices.commands.OpenClose',
params: {
followUpToken: '456',
},
},
],
},
],
},
},
];

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.notCalled(gladys.event.emit);
});
});
Loading

0 comments on commit 2725637

Please sign in to comment.