Skip to content

Commit

Permalink
Merge pull request #319 from michikrug/state-errors
Browse files Browse the repository at this point in the history
  • Loading branch information
michikrug authored Mar 13, 2022
2 parents e025b7d + 6458a88 commit 6fc2ec5
Show file tree
Hide file tree
Showing 24 changed files with 436 additions and 252 deletions.
41 changes: 22 additions & 19 deletions docs/USAGE.md

Large diffs are not rendered by default.

33 changes: 3 additions & 30 deletions functions/commands/armdisarm.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,37 +60,10 @@ class ArmDisarm extends DefaultCommand {
return true;
}

static validateStateChange(params, item, device) {
let isCurrentlyArmed;
let currentLevel;

if (this.getDeviceType(device) === 'SecuritySystem') {
const members = SecuritySystem.getMembers(item);
isCurrentlyArmed =
(SecuritySystem.armedMemberName in members && members[SecuritySystem.armedMemberName].state) ===
(this.isInverted(device) ? 'OFF' : 'ON');
currentLevel =
(SecuritySystem.armLevelMemberName in members && members[SecuritySystem.armLevelMemberName].state) || undefined;
} else {
isCurrentlyArmed = item.state === (this.isInverted(device) ? 'OFF' : 'ON');
}

if (params.armLevel && this.getDeviceType(device) === 'SecuritySystem') {
if (params.arm && isCurrentlyArmed && params.armLevel === currentLevel) {
throw { errorCode: 'alreadyInState' };
}
return true;
}

if (params.arm && isCurrentlyArmed) {
throw { errorCode: 'alreadyArmed' };
static checkCurrentState(target, state, params) {
if (target === state) {
throw { errorCode: params.armLevel ? 'alreadyInState' : params.arm ? 'alreadyArmed' : 'alreadyDisarmed' };
}

if (!params.arm && !isCurrentlyArmed) {
throw { errorCode: 'alreadyDisarmed' };
}

return true;
}

static validateUpdate(params, item, device) {
Expand Down
44 changes: 31 additions & 13 deletions functions/commands/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,23 @@ class DefaultCommand {
}

/**
* Is the requested new state valid
* @param {object} params Requested change params
* @param {object} item Current state of item
* @returns {boolean} true if state change is valid otherwise throws error
* Is the requested new state change valid?
* @param {string} target Requested target state
* @param {string} state Current state of item
* @param {object} params Parameters of the command
* @returns {void} returns if current state is different otherwise throws error
*/
static validateStateChange(params, item, device) {
return true;
static checkCurrentState(target, state, params) {
if (target === state) {
throw { errorCode: 'alreadyInState' };
}
}

/**
* @param {object} item
*/
static getNormalizedState(item) {
return item.type.startsWith('Number:') ? item.state.split(' ')[0] : item.state;
}

static get requiresUpdateValidation() {
Expand Down Expand Up @@ -94,7 +104,7 @@ class DefaultCommand {
* @param {object} device
*/
static isInverted(device) {
return device.customData && device.customData.inverted === true;
return !!(device.customData && device.customData.inverted);
}

/**
Expand Down Expand Up @@ -202,7 +212,6 @@ class DefaultCommand {
* @param {object} challenge
*/
static execute(apiHandler, devices, params, challenge) {
// console.log(`openhabGoogleAssistant - ${this.type}: ${JSON.stringify({ devices: devices, params: params })}`);
const commandsResponse = [];
const promises = devices.map((device) => {
const authPinResponse = this.handleAuthPin(device, challenge, params);
Expand All @@ -217,14 +226,25 @@ class DefaultCommand {
(device.customData.ackNeeded || device.customData.tfaAck) &&
!(challenge && challenge.ack);

let getItemPromise = Promise.resolve({ name: device.id });
if (this.requiresItem(device) || ackWithState || this.requiresUpdateValidation) {
const shouldCheckState = device.customData && device.customData.checkState;

let getItemPromise = Promise.resolve({ name: device.id, state: null, members: [] });
if (this.requiresItem(device) || ackWithState || shouldCheckState) {
getItemPromise = apiHandler.getItem(device.id);
}

return getItemPromise
.then((item) => {
this.validateStateChange(params, item, device);
const targetItem = this.getItemName(item, device, params);
const targetValue = this.convertParamsToValue(params, item, device);
if (shouldCheckState) {
let currentState = this.getNormalizedState(item);
if (targetItem !== device.id && item.members && item.members.length) {
const member = item.members.find((m) => m.name === targetItem);
currentState = member ? this.getNormalizedState(member) : currentState;
}
this.checkCurrentState(targetValue, currentState, params);
}

const responseStates = this.getResponseStates(params, item, device);
if (Object.keys(responseStates).length) {
Expand All @@ -237,8 +257,6 @@ class DefaultCommand {
return;
}

const targetItem = this.getItemName(item, device, params);
const targetValue = this.convertParamsToValue(params, item, device);
let sendCommandPromise = Promise.resolve();
if (typeof targetItem === 'string' && typeof targetValue === 'string') {
sendCommandPromise = apiHandler.sendCommand(targetItem, targetValue);
Expand Down
6 changes: 6 additions & 0 deletions functions/commands/lockunlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ class LockUnlock extends DefaultCommand {
isLocked: params.lock
};
}

static checkCurrentState(target, state, params) {
if (target === state) {
throw { errorCode: params.lock ? 'alreadyLocked' : 'alreadyUnlocked' };
}
}
}

module.exports = LockUnlock;
6 changes: 6 additions & 0 deletions functions/commands/onoff.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ class OnOff extends DefaultCommand {
on: params.on
};
}

static checkCurrentState(target, state, params) {
if (target === state) {
throw { errorCode: params.on ? 'alreadyOn' : 'alreadyOff' };
}
}
}

module.exports = OnOff;
8 changes: 7 additions & 1 deletion functions/commands/openclose.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class OpenClose extends DefaultCommand {
openPercent = 100 - openPercent;
}
if (itemType === 'Rollershutter') {
return openPercent === 0 ? 'DOWN' : openPercent === 100 ? 'UP' : (100 - openPercent).toString();
return (100 - openPercent).toString();
}
if (itemType === 'Switch') {
return openPercent === 0 ? 'OFF' : 'ON';
Expand All @@ -32,6 +32,12 @@ class OpenClose extends DefaultCommand {
openPercent: params.openPercent
};
}

static checkCurrentState(target, state, params) {
if (target === state) {
throw { errorCode: params.openPercent === 0 ? 'alreadyClosed' : 'alreadyOpen' };
}
}
}

module.exports = OpenClose;
8 changes: 8 additions & 0 deletions functions/commands/setvolume.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ class SetVolume extends DefaultCommand {
currentVolume: params.volumeLevel
};
}

static checkCurrentState(target, state) {
if (target === state) {
throw {
errorCode: state === '100' ? 'volumeAlreadyMax' : state === '0' ? 'volumeAlreadyMin' : 'alreadyInState'
};
}
}
}

module.exports = SetVolume;
6 changes: 6 additions & 0 deletions functions/commands/startstop.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ class StartStop extends DefaultCommand {
isPaused: !params.start
};
}

static checkCurrentState(target, state, params) {
if (target === state) {
throw { errorCode: params.start ? 'alreadyStarted' : 'alreadyStopped' };
}
}
}

module.exports = StartStop;
8 changes: 8 additions & 0 deletions functions/commands/volumerelative.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ class VolumeRelative extends DefaultCommand {
currentVolume: parseInt(this.convertParamsToValue(params, item, device))
};
}

static checkCurrentState(target, state) {
if (target === state) {
throw {
errorCode: state === '100' ? 'volumeAlreadyMax' : state === '0' ? 'volumeAlreadyMin' : 'alreadyInState'
};
}
}
}

module.exports = VolumeRelative;
2 changes: 1 addition & 1 deletion functions/devices/charger.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Charger extends DefaultDevice {
const config = this.getConfig(item);
const members = this.getMembers(item);
const attributes = {
isRechargeable: config.isRechargeable === true,
isRechargeable: !!config.isRechargeable,
queryOnlyEnergyStorage: !('chargerCharging' in members)
};
return attributes;
Expand Down
3 changes: 3 additions & 0 deletions functions/devices/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ class DefaultDevice {
if (config.inverted === true) {
metadata.customData.inverted = true;
}
if (config.checkState === true) {
metadata.customData.checkState = true;
}
if (config.ackNeeded === true || config.tfaAck === true) {
metadata.customData.ackNeeded = true;
}
Expand Down
146 changes: 24 additions & 122 deletions tests/commands/armdisarm.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,128 +112,30 @@ describe('ArmDisarm Command', () => {
expect(Command.bypassPin({ customData: { pinOnDisarmOnly: true } }, { armLevel: 'L1' })).toBe(true);
});

describe('validateStateChange', () => {
test('validateStateChange SimpleSecuritySystem', () => {
expect.assertions(6);

const item = { type: 'Switch', state: 'ON' };

expect(Command.validateStateChange({ arm: false }, item, {})).toBe(true);
try {
Command.validateStateChange({ arm: true }, item, {});
} catch (e) {
expect(e.errorCode).toBe('alreadyArmed');
}

item.state = 'OFF';
expect(Command.validateStateChange({ arm: true }, item, {})).toBe(true);
try {
Command.validateStateChange({ arm: false }, item, {});
} catch (e) {
expect(e.errorCode).toBe('alreadyDisarmed');
}

item.state = 'ON';
expect(
Command.validateStateChange({ arm: true }, item, {
customData: { inverted: true }
})
).toBe(true);
item.state = 'OFF';
try {
Command.validateStateChange({ arm: true }, item, {
customData: { inverted: true }
});
} catch (e) {
expect(e.errorCode).toBe('alreadyArmed');
}
});

describe('validateStateChange SecuritySystem', () => {
const item = {
members: [
{
state: 'ON',
metadata: { ga: { value: SecuritySystem.armedMemberName } }
},
{
state: 'L0',
metadata: { ga: { value: SecuritySystem.armLevelMemberName } }
}
]
};

test('arming without level', () => {
expect.assertions(2);
item.members[0].state = 'ON';
try {
Command.validateStateChange({ arm: true }, item, { customData: { deviceType: 'SecuritySystem' } });
} catch (e) {
expect(e.errorCode).toBe('alreadyArmed');
}

item.members[0].state = 'OFF';
expect(Command.validateStateChange({ arm: true }, item, { customData: { deviceType: 'SecuritySystem' } })).toBe(
true
);
});

test('arming without level inverted', () => {
expect.assertions(2);
item.members[0].state = 'OFF';
try {
Command.validateStateChange({ arm: true }, item, {
customData: { deviceType: 'SecuritySystem', inverted: true }
});
} catch (e) {
expect(e.errorCode).toBe('alreadyArmed');
}

item.members[0].state = 'ON';
expect(
Command.validateStateChange({ arm: true }, item, {
customData: { deviceType: 'SecuritySystem', inverted: true }
})
).toBe(true);
});

test('arming with level', () => {
expect.assertions(2);
item.members[0].state = 'OFF';
item.members[1].state = 'L1';
expect(
Command.validateStateChange({ arm: true, armLevel: 'L2' }, item, {
customData: { deviceType: 'SecuritySystem' }
})
).toBe(true);

item.members[0].state = 'ON';
try {
Command.validateStateChange({ arm: true, armLevel: 'L1' }, item, {
customData: { deviceType: 'SecuritySystem' }
});
} catch (e) {
expect(e.errorCode).toBe('alreadyInState');
}
});

test('disarming', () => {
expect.assertions(2);
item.members[0].state = 'ON';
expect(
Command.validateStateChange({ arm: false }, item, { customData: { deviceType: 'SecuritySystem' } })
).toBe(true);

item.members[0].state = 'OFF';
try {
Command.validateStateChange({ arm: false }, item, {
customData: { deviceType: 'SecuritySystem' }
});
} catch (e) {
expect(e.errorCode).toBe('alreadyDisarmed');
}
});
});
test('checkCurrentState', () => {
expect.assertions(6);

expect(Command.checkCurrentState('ON', 'OFF', { arm: true })).toBeUndefined();
try {
Command.checkCurrentState('ON', 'ON', { arm: true });
} catch (e) {
expect(e.errorCode).toBe('alreadyArmed');
}

expect(Command.checkCurrentState('OFF', 'ON', { arm: false })).toBeUndefined();
try {
Command.checkCurrentState('OFF', 'OFF', { arm: false });
} catch (e) {
expect(e.errorCode).toBe('alreadyDisarmed');
}

expect(Command.checkCurrentState('L2', 'L1', { arm: true, armLevel: 'L2' })).toBeUndefined();

try {
Command.checkCurrentState('L2', 'L2', { arm: true, armLevel: 'L2' });
} catch (e) {
expect(e.errorCode).toBe('alreadyInState');
}
});

describe('validateUpdate', () => {
Expand Down
Loading

0 comments on commit 6fc2ec5

Please sign in to comment.