diff --git a/server/config/brain/switch/answers.en.json b/server/config/brain/switch/answers.en.json new file mode 100644 index 0000000000..d2baa7d484 --- /dev/null +++ b/server/config/brain/switch/answers.en.json @@ -0,0 +1,22 @@ +[ + { + "label": "switch.turn-on.success", + "answers": ["The switch is turned on !"] + }, + { + "label": "switch.turn-on.fail", + "answers": ["I was unable to turn on the switch."] + }, + { + "label": "switch.turn-off.success", + "answers": ["The switch is turned off !"] + }, + { + "label": "switch.turn-off.fail", + "answers": ["I was unable to turn off the switch."] + }, + { + "label": "switch.not-found", + "answers": ["I didn't find any switch with this name."] + } +] diff --git a/server/config/brain/switch/answers.fr.json b/server/config/brain/switch/answers.fr.json new file mode 100644 index 0000000000..9a72d57382 --- /dev/null +++ b/server/config/brain/switch/answers.fr.json @@ -0,0 +1,22 @@ +[ + { + "label": "switch.turn-on.success", + "answers": ["La prise est allumée !"] + }, + { + "label": "switch.turn-on.fail", + "answers": ["Je n'ai pas réussi à allumer la prise."] + }, + { + "label": "switch.turn-off.success", + "answers": ["La prise est éteinte !"] + }, + { + "label": "switch.turn-off.fail", + "answers": ["Je n'ai pas réussi à éteindre la prise."] + }, + { + "label": "switch.not-found", + "answers": ["Je n'ai pas trouvé de prise avec ce nom."] + } +] diff --git a/server/config/brain/switch/questions.en.json b/server/config/brain/switch/questions.en.json new file mode 100644 index 0000000000..bcf5e3f4f5 --- /dev/null +++ b/server/config/brain/switch/questions.en.json @@ -0,0 +1,12 @@ +[ + { + "label": "switch.turn-on", + "questions": ["Turn on the switch %device%"], + "answers": ["Turning on the switch...", "I'm turning on the switch!"] + }, + { + "label": "switch.turn-off", + "questions": ["Turn off the switch %device%"], + "answers": ["Turning off the switch...", "I'm turning off the switch!"] + } +] diff --git a/server/config/brain/switch/questions.fr.json b/server/config/brain/switch/questions.fr.json new file mode 100644 index 0000000000..c6d75bbd38 --- /dev/null +++ b/server/config/brain/switch/questions.fr.json @@ -0,0 +1,12 @@ +[ + { + "label": "switch.turn-on", + "questions": ["Allume la prise %device%"], + "answers": ["Allumage en cours...", "J'allume la prise !"] + }, + { + "label": "switch.turn-off", + "questions": ["Eteins la prise %device%"], + "answers": ["Extinction en cours...", "J'éteins la prise !"] + } +] diff --git a/server/lib/device/index.js b/server/lib/device/index.js index f69c3aca2f..cad5d97541 100644 --- a/server/lib/device/index.js +++ b/server/lib/device/index.js @@ -6,6 +6,7 @@ const CameraManager = require('./camera'); const LightManager = require('./light'); const TemperatureSensorManager = require('./temperature-sensor'); const HumiditySensorManager = require('./humidity-sensor'); +const SwitchManager = require('./switch'); // Functions const { add } = require('./device.add'); @@ -66,6 +67,7 @@ const DeviceManager = function DeviceManager( this.lightManager = new LightManager(eventManager, messageManager, this); this.temperatureSensorManager = new TemperatureSensorManager(eventManager, messageManager, this); this.humiditySensorManager = new HumiditySensorManager(eventManager, messageManager, this); + this.switchManager = new SwitchManager(eventManager, stateManager, messageManager, this); this.purgeStatesByFeatureId = this.job.wrapper( JOB_TYPES.DEVICE_STATES_PURGE_SINGLE_FEATURE, diff --git a/server/lib/device/switch/index.js b/server/lib/device/switch/index.js new file mode 100644 index 0000000000..2b08d2b68f --- /dev/null +++ b/server/lib/device/switch/index.js @@ -0,0 +1,15 @@ +const { INTENTS } = require('../../../utils/constants'); +const { command } = require('./switch.command'); + +const SwitchManager = function SwitchManager(eventManager, stateManager, messageManager, deviceManager) { + this.eventManager = eventManager; + this.stateManager = stateManager; + this.messageManager = messageManager; + this.deviceManager = deviceManager; + this.eventManager.on(INTENTS.SWITCH.TURN_ON, this.command.bind(this)); + this.eventManager.on(INTENTS.SWITCH.TURN_OFF, this.command.bind(this)); +}; + +SwitchManager.prototype.command = command; + +module.exports = SwitchManager; diff --git a/server/lib/device/switch/switch.command.js b/server/lib/device/switch/switch.command.js new file mode 100644 index 0000000000..8beccf970c --- /dev/null +++ b/server/lib/device/switch/switch.command.js @@ -0,0 +1,51 @@ +const Promise = require('bluebird'); + +const { getDeviceFeature } = require('../../../utils/device'); +const logger = require('../../../utils/logger'); +const { NotFoundError } = require('../../../utils/coreErrors'); +const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES, STATE } = require('../../../utils/constants'); + +/** + * @description Command a switch. + * @param {object} message - The message sent by the user. + * @param {object} classification - The classification calculated by the brain. + * @param {object} context - The context object containing found variables in question. + * @returns {Promise} Resolve when the command was executed. + * @example + * light.command(message, classification, context); + */ +async function command(message, classification, context) { + const deviceEntity = classification.entities.find((entity) => entity.entity === 'device'); + try { + if (!deviceEntity) { + throw new NotFoundError('Device not found'); + } + const device = this.stateManager.get('device', deviceEntity.option); + if (!device) { + throw new NotFoundError('Device not found'); + } + const deviceFeature = getDeviceFeature( + device, + DEVICE_FEATURE_CATEGORIES.SWITCH, + DEVICE_FEATURE_TYPES.SWITCH.BINARY, + ); + if (!deviceFeature) { + throw new NotFoundError('Feature not found'); + } + if (classification.intent === 'switch.turn-on') { + await this.deviceManager.setValue(device, deviceFeature, STATE.ON); + this.messageManager.replyByIntent(message, 'switch.turn-on.success', context); + } else if (classification.intent === 'switch.turn-off') { + await this.deviceManager.setValue(device, deviceFeature, STATE.OFF); + this.messageManager.replyByIntent(message, 'switch.turn-off.success', context); + } + } catch (e) { + logger.debug(e); + this.messageManager.replyByIntent(message, `${classification.intent}.fail`, context); + } + return null; +} + +module.exports = { + command, +}; diff --git a/server/lib/gateway/gateway.forwardMessageToOpenAI.js b/server/lib/gateway/gateway.forwardMessageToOpenAI.js index 29e84052c3..6da8b66002 100644 --- a/server/lib/gateway/gateway.forwardMessageToOpenAI.js +++ b/server/lib/gateway/gateway.forwardMessageToOpenAI.js @@ -3,8 +3,12 @@ const { Error429 } = require('../../utils/httpErrors'); const intentTranslation = { SHOW_CAMERA: 'camera.get-image', - TURN_ON: 'light.turn-on', - TURN_OFF: 'light.turn-off', + TURN_ON: 'light.turn-on', // To be removed later, for backward compatibility + TURN_OFF: 'light.turn-off', // To be removed later, for backward compatibility + LIGHT_TURN_ON: 'light.turn-on', + LIGHT_TURN_OFF: 'light.turn-off', + SWITCH_TURN_ON: 'switch.turn-on', + SWITCH_TURN_OFF: 'switch.turn-off', GET_TEMPERATURE: 'temperature-sensor.get-in-room', GET_HUMIDITY: 'humidity-sensor.get-in-room', SCENE_START: 'scene.start', diff --git a/server/test/lib/device/switch/switch.command.test.js b/server/test/lib/device/switch/switch.command.test.js new file mode 100644 index 0000000000..2df59e1095 --- /dev/null +++ b/server/test/lib/device/switch/switch.command.test.js @@ -0,0 +1,251 @@ +const EventEmitter = require('events'); +const { assert, fake } = require('sinon'); +const Device = require('../../../../lib/device'); +const StateManager = require('../../../../lib/state'); +const Job = require('../../../../lib/job'); +const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../../../utils/constants'); + +const event = new EventEmitter(); +const job = new Job(event); + +const testService = { + device: { + setValue: fake.resolves(true), + }, +}; + +const testServiceBroken = { + device: { + setValue: fake.rejects(true), + }, +}; + +const service = { + getService: () => testService, +}; + +const serviceBroken = { + getService: () => testServiceBroken, +}; + +const message = { + text: 'turn on the switch test device', +}; + +const context = {}; + +describe('switch.command', () => { + it('should send a turn on command', async () => { + const stateManager = new StateManager(event); + const messageManager = { + replyByIntent: fake.resolves(true), + }; + const deviceManager = new Device(event, messageManager, stateManager, service, {}, {}, job); + stateManager.setState('device', 'test-device', { + selector: 'test-device', + service: { + name: 'mqtt', + }, + features: [ + { + has_feedback: true, + category: DEVICE_FEATURE_CATEGORIES.SWITCH, + type: DEVICE_FEATURE_TYPES.SWITCH.BINARY, + }, + ], + }); + await deviceManager.switchManager.command( + message, + { + intent: 'switch.turn-on', + entities: [ + { + start: 25, + end: 31, + len: 7, + levenshtein: 0, + accuracy: 1, + entity: 'device', + type: 'enum', + option: 'test-device', + sourceText: 'test device', + utteranceText: 'test device', + }, + ], + }, + context, + ); + assert.calledWith(messageManager.replyByIntent, message, 'switch.turn-on.success', context); + assert.called(testService.device.setValue); + }); + it('should send a turn off command', async () => { + const stateManager = new StateManager(event); + const messageManager = { + replyByIntent: fake.resolves(true), + }; + const deviceManager = new Device(event, messageManager, stateManager, service, {}, {}, job); + stateManager.setState('device', 'test-device', { + selector: 'test-device', + service: { + name: 'mqtt', + }, + features: [ + { + has_feedback: true, + category: DEVICE_FEATURE_CATEGORIES.SWITCH, + type: DEVICE_FEATURE_TYPES.SWITCH.BINARY, + }, + ], + }); + await deviceManager.switchManager.command( + message, + { + intent: 'switch.turn-off', + entities: [ + { + start: 25, + end: 31, + len: 7, + levenshtein: 0, + accuracy: 1, + entity: 'device', + type: 'enum', + option: 'test-device', + sourceText: 'test device', + utteranceText: 'test device', + }, + ], + }, + context, + ); + assert.calledWith(messageManager.replyByIntent, message, 'switch.turn-off.success', context); + assert.called(testService.device.setValue); + }); + it('should fail to send a turn on command (service is not responding)', async () => { + const stateManager = new StateManager(event); + const messageManager = { + replyByIntent: fake.resolves(true), + }; + const deviceManager = new Device(event, messageManager, stateManager, serviceBroken, {}, {}, job); + stateManager.setState('device', 'test-device', { + selector: 'test-device', + service: { + name: 'mqtt', + }, + features: [ + { + has_feedback: true, + category: DEVICE_FEATURE_CATEGORIES.SWITCH, + type: DEVICE_FEATURE_TYPES.SWITCH.BINARY, + }, + ], + }); + await deviceManager.switchManager.command( + message, + { + intent: 'switch.turn-on', + entities: [ + { + start: 25, + end: 31, + len: 7, + levenshtein: 0, + accuracy: 1, + entity: 'device', + type: 'enum', + option: 'test-device', + sourceText: 'test device', + utteranceText: 'test device', + }, + ], + }, + context, + ); + assert.calledWith(messageManager.replyByIntent, message, 'switch.turn-on.fail', context); + assert.called(testService.device.setValue); + }); + it('should fail to send turn on command (device not found)', async () => { + const stateManager = new StateManager(event); + const messageManager = { + replyByIntent: fake.resolves(true), + }; + const deviceManager = new Device(event, messageManager, stateManager, service, {}, {}, job); + await deviceManager.switchManager.command( + message, + { + intent: 'switch.turn-on', + entities: [ + { + start: 25, + end: 31, + len: 7, + levenshtein: 0, + accuracy: 1, + entity: 'device', + type: 'enum', + option: 'test-device', + sourceText: 'test device', + utteranceText: 'test device', + }, + ], + }, + context, + ); + assert.calledWith(messageManager.replyByIntent, message, 'switch.turn-on.fail', context); + assert.called(testService.device.setValue); + }); + it('should fail to send turn on command (no entities found)', async () => { + const stateManager = new StateManager(event); + const messageManager = { + replyByIntent: fake.resolves(true), + }; + const deviceManager = new Device(event, messageManager, stateManager, service, {}, {}, job); + await deviceManager.switchManager.command( + message, + { + intent: 'switch.turn-on', + entities: [], + }, + context, + ); + assert.calledWith(messageManager.replyByIntent, message, 'switch.turn-on.fail', context); + assert.called(testService.device.setValue); + }); + it('should fail to send a turn on command (feature not found)', async () => { + const stateManager = new StateManager(event); + const messageManager = { + replyByIntent: fake.resolves(true), + }; + const deviceManager = new Device(event, messageManager, stateManager, service, {}, {}, job); + stateManager.setState('device', 'test-device', { + selector: 'test-device', + service: { + name: 'mqtt', + }, + features: [], + }); + await deviceManager.switchManager.command( + message, + { + intent: 'switch.turn-on', + entities: [ + { + start: 25, + end: 31, + len: 7, + levenshtein: 0, + accuracy: 1, + entity: 'device', + type: 'enum', + option: 'test-device', + sourceText: 'test device', + utteranceText: 'test device', + }, + ], + }, + context, + ); + assert.calledWith(messageManager.replyByIntent, message, 'switch.turn-on.fail', context); + assert.called(testService.device.setValue); + }); +}); diff --git a/server/utils/constants.js b/server/utils/constants.js index fb095bbc06..7170e08960 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -459,6 +459,10 @@ const INTENTS = { SCENE: { START: 'intent.scene.start', }, + SWITCH: { + TURN_ON: 'intent.switch.turn-on', + TURN_OFF: 'intent.switch.turn-off', + }, }; const DEVICE_FEATURE_CATEGORIES = {