diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json
index 5699ffef2a..f0afb1eebc 100644
--- a/front/src/config/i18n/de.json
+++ b/front/src/config/i18n/de.json
@@ -2021,6 +2021,7 @@
"description": "Diese Aktion lässt Gladys auf dem ausgewählten Lautsprecher sprechen.",
"needGladysPlus": "Erfordert Gladys Plus, da die Text-to-Speech-APIs kostenpflichtig sind.",
"deviceLabel": "Lautsprecher",
+ "volumeLabel": "Volumen",
"textLabel": "Nachricht zum Sprechen auf dem Lautsprecher",
"variablesExplanation": "Um eine Variable einzufügen, gib \"{{\" ein. Hinweis: Du musst vor diesem Block eine Variable in einer \"Letzten Zustand abrufen\"-Aktion definiert haben."
}
diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json
index 79fdb14478..c77eddbbbd 100644
--- a/front/src/config/i18n/en.json
+++ b/front/src/config/i18n/en.json
@@ -2021,6 +2021,7 @@
"description": "This action will make Gladys speak on the selected speaker.",
"needGladysPlus": "Requires Gladys Plus as Text-To-Speech APIs are paid.",
"deviceLabel": "Speaker",
+ "volumeLabel": "Volume",
"textLabel": "Message to speak on the speaker",
"variablesExplanation": "To inject a variable, type '{{ '. Note: You must have defined a variable beforehand in a 'Retrieve Last State' action placed before this message block."
}
diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json
index 0ba3a52b4a..56ed687d54 100644
--- a/front/src/config/i18n/fr.json
+++ b/front/src/config/i18n/fr.json
@@ -2021,6 +2021,7 @@
"description": "Cette action fera parler Gladys sur l'enceinte sélectionnée.",
"needGladysPlus": "Nécessite Gladys Plus car les API de \"Text To Speech\" sont payantes.",
"deviceLabel": "Enceinte",
+ "volumeLabel": "Volume",
"textLabel": "Message à dire sur l'enceinte",
"variablesExplanation": "Pour injecter une variable, tapez '{{ '. Attention, vous devez avoir défini une variable auparavant dans une action 'Récupérer le dernier état' placé avant ce bloc message."
}
diff --git a/front/src/routes/scene/edit-scene/actions/PlayNotification.jsx b/front/src/routes/scene/edit-scene/actions/PlayNotification.jsx
index 96cb7eff9c..07d3bec503 100644
--- a/front/src/routes/scene/edit-scene/actions/PlayNotification.jsx
+++ b/front/src/routes/scene/edit-scene/actions/PlayNotification.jsx
@@ -26,6 +26,9 @@ class PlayNotification extends Component {
console.error(e);
}
};
+ updateVolume = e => {
+ this.props.updateActionProperty(this.props.columnIndex, this.props.index, 'volume', e.target.value);
+ };
updateText = text => {
this.props.updateActionProperty(this.props.columnIndex, this.props.index, 'text', text);
};
@@ -84,6 +87,24 @@ class PlayNotification extends Component {
onChange={this.handleDeviceChange}
/>
+
+
+
+
+
+
+
+
+
+
{' '}
diff --git a/server/lib/device/device.setValue.js b/server/lib/device/device.setValue.js
index dc472c2078..b2d97cca3f 100644
--- a/server/lib/device/device.setValue.js
+++ b/server/lib/device/device.setValue.js
@@ -7,10 +7,11 @@ const { NotFoundError } = require('../../utils/coreErrors');
* @param {object} device - The device to control.
* @param {object} deviceFeature - The deviceFeature to control.
* @param {string|number} value - The new state to set.
+ * @param {object} options - Optional configs.
* @example
* device.setValue(device, deviceFeature);
*/
-async function setValue(device, deviceFeature, value) {
+async function setValue(device, deviceFeature, value, options = {}) {
const service = this.serviceManager.getService(device.service.name);
if (service === null) {
throw new NotFoundError(`Service ${device.service.name} was not found.`);
@@ -18,7 +19,7 @@ async function setValue(device, deviceFeature, value) {
if (typeof get(service, 'device.setValue') !== 'function') {
throw new NotFoundError(`Function device.setValue in service ${device.service.name} does not exist.`);
}
- await service.device.setValue(device, deviceFeature, value);
+ await service.device.setValue(device, deviceFeature, value, options);
// If device has feedback, the feedback will be sent and saved
// If value is a string, no need to save it
// @ts-ignore
diff --git a/server/lib/scene/scene.actions.js b/server/lib/scene/scene.actions.js
index 6f214b803c..30dbe20346 100644
--- a/server/lib/scene/scene.actions.js
+++ b/server/lib/scene/scene.actions.js
@@ -587,7 +587,7 @@ const actionsFunc = {
// Get TTS URL
const { url } = await self.gateway.getTTSApiUrl({ text: messageWithVariables });
// Play TTS Notification on device
- await self.device.setValue(device, deviceFeature, url);
+ await self.device.setValue(device, deviceFeature, url, { volume: action.volume });
},
[ACTIONS.SMS.SEND]: async (self, action, scope) => {
const freeMobileService = self.service.getService('free-mobile');
diff --git a/server/models/scene.js b/server/models/scene.js
index 7c91a90c7b..e3f1ad617f 100644
--- a/server/models/scene.js
+++ b/server/models/scene.js
@@ -65,6 +65,10 @@ const actionSchema = Joi.array().items(
message: Joi.string().allow(''),
blinking_time: Joi.number(),
blinking_speed: Joi.string().valid('slow', 'medium', 'fast'),
+ volume: Joi.number()
+ .integer()
+ .max(100)
+ .min(0),
}),
),
);
diff --git a/server/services/airplay/lib/airplay.setValue.js b/server/services/airplay/lib/airplay.setValue.js
index e534c53d2d..1923aca9dc 100644
--- a/server/services/airplay/lib/airplay.setValue.js
+++ b/server/services/airplay/lib/airplay.setValue.js
@@ -6,10 +6,11 @@ const logger = require('../../../utils/logger');
* @param {object} device - Updated Gladys device.
* @param {object} deviceFeature - Updated Gladys device feature.
* @param {string|number} value - The new device feature value.
+ * @param {object} options - Optional configs.
* @example
- * setValue(device, deviceFeature, 0);
+ * setValue(device, deviceFeature, 0, 30);
*/
-async function setValue(device, deviceFeature, value) {
+async function setValue(device, deviceFeature, value, options) {
const deviceName = device.external_id.split(':')[1];
const ipAddress = this.deviceIpAddresses.get(deviceName);
if (!ipAddress) {
@@ -19,7 +20,7 @@ async function setValue(device, deviceFeature, value) {
if (deviceFeature.type === DEVICE_FEATURE_TYPES.MUSIC.PLAY_NOTIFICATION) {
const client = new this.Airtunes();
const airplayDevice = client.add(ipAddress, {
- volume: 70,
+ volume: options?.volume || 70,
});
let decodeProcess;
diff --git a/server/services/google-cast/lib/google_cast.setValue.js b/server/services/google-cast/lib/google_cast.setValue.js
index 9224d8de11..ee787918fc 100644
--- a/server/services/google-cast/lib/google_cast.setValue.js
+++ b/server/services/google-cast/lib/google_cast.setValue.js
@@ -1,3 +1,5 @@
+const { promisify } = require('util');
+
const { DEVICE_FEATURE_TYPES } = require('../../../utils/constants');
const logger = require('../../../utils/logger');
/**
@@ -5,10 +7,11 @@ const logger = require('../../../utils/logger');
* @param {object} device - Updated Gladys device.
* @param {object} deviceFeature - Updated Gladys device feature.
* @param {string|number} value - The new device feature value.
+ * @param {object} options - Optional configs.
* @example
* setValue(device, deviceFeature, 0);
*/
-async function setValue(device, deviceFeature, value) {
+async function setValue(device, deviceFeature, value, options) {
const deviceName = device.external_id.split(':')[1];
const ipAddress = this.deviceIpAddresses.get(deviceName);
if (!ipAddress) {
@@ -19,8 +22,16 @@ async function setValue(device, deviceFeature, value) {
const { Client, DefaultMediaReceiver } = this.googleCastLib;
const client = new Client();
- client.connect(ipAddress, () => {
+ client.connect(ipAddress, async () => {
logger.debug('Google Cast Connected, launching app ...');
+ const getVolume = promisify(client.getVolume.bind(client));
+ const setVolume = promisify(client.setVolume.bind(client));
+
+ const { level } = await getVolume();
+
+ if (options.volume) {
+ await setVolume({ level: options.volume / 100 });
+ }
client.launch(DefaultMediaReceiver, (err, player) => {
const media = {
@@ -38,8 +49,11 @@ async function setValue(device, deviceFeature, value) {
},
};
- player.on('status', (status) => {
+ player.on('status', async (status) => {
logger.debug('status broadcast playerState=%s', status.playerState);
+ if (status.idleReason === 'FINISHED') {
+ await setVolume({ level });
+ }
});
logger.debug('app "%s" launched, loading media %s ...', player.session.displayName, media.contentId);
diff --git a/server/services/sonos/lib/sonos.setValue.js b/server/services/sonos/lib/sonos.setValue.js
index d24064f7f5..0243448407 100644
--- a/server/services/sonos/lib/sonos.setValue.js
+++ b/server/services/sonos/lib/sonos.setValue.js
@@ -4,10 +4,11 @@ const { DEVICE_FEATURE_TYPES } = require('../../../utils/constants');
* @param {object} device - Updated Gladys device.
* @param {object} deviceFeature - Updated Gladys device feature.
* @param {string|number} value - The new device feature value.
+ * @param {object} options - Optional configs.
* @example
* setValue(device, deviceFeature, 0);
*/
-async function setValue(device, deviceFeature, value) {
+async function setValue(device, deviceFeature, value, options) {
const deviceUuid = device.external_id.split(':')[1];
const sonosDevice = this.manager.Devices.find((d) => d.uuid === deviceUuid);
if (deviceFeature.type === DEVICE_FEATURE_TYPES.MUSIC.PLAY) {
@@ -30,7 +31,7 @@ async function setValue(device, deviceFeature, value) {
await sonosDevice.PlayNotification({
trackUri: value,
onlyWhenPlaying: false,
- volume: 45, // Set the volume for the notification (and revert back afterwards)
+ volume: options?.volume || 45, // Set the volume for the notification (and revert back afterwards)
timeout: 20, // If the events don't work (to see when it stops playing) or if you turned on a stream,
// it will revert back after this amount of seconds.
delayMs: 700, // Pause between commands in ms, (when sonos fails to play sort notification sounds).
diff --git a/server/test/services/airplay/lib/airplay.setValue.test.js b/server/test/services/airplay/lib/airplay.setValue.test.js
index eeb91d02f0..848b4fe509 100644
--- a/server/test/services/airplay/lib/airplay.setValue.test.js
+++ b/server/test/services/airplay/lib/airplay.setValue.test.js
@@ -88,12 +88,18 @@ describe('AirplayHandler.setValue', () => {
await airplayHandler.setValue(device, device.features[0], 'http://play-url.com');
sinon.assert.calledOnce(pipe);
});
+ it('should talk on speaker with custom volume', async () => {
+ airplayHandler.scanTimeout = 1;
+ const devices = await airplayHandler.scan();
+ const device = devices[0];
+ await airplayHandler.setValue(device, device.features[0], 'http://play-url.com', { volume: 30 });
+ });
it('should return device not found', async () => {
airplayHandler.scanTimeout = 1;
const device = {
external_id: 'airplay:toto',
};
- const promise = airplayHandler.setValue(device, {}, 'http://play-url.com');
+ const promise = airplayHandler.setValue(device, {}, 'http://play-url.com', { volume: 30 });
await assert.isRejected(promise, 'Device not found on network');
});
});
diff --git a/server/test/services/google-cast/lib/google_cast.setValue.test.js b/server/test/services/google-cast/lib/google_cast.setValue.test.js
index 0059dd0ab3..36b28aa24a 100644
--- a/server/test/services/google-cast/lib/google_cast.setValue.test.js
+++ b/server/test/services/google-cast/lib/google_cast.setValue.test.js
@@ -37,6 +37,16 @@ class GoogleCastClient {
cb({ message: 'this is an error' });
}
+ // eslint-disable-next-line class-methods-use-this
+ getVolume(cb) {
+ cb(null, { level: 1 });
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ setVolume(volume, cb) {
+ cb(null, 30);
+ }
+
// eslint-disable-next-line class-methods-use-this
close() {}
}
@@ -80,14 +90,14 @@ describe('GoogleCastHandler.setValue', () => {
googleCastHandler.scanTimeout = 1;
const devices = await googleCastHandler.scan();
const device = devices[0];
- await googleCastHandler.setValue(device, device.features[0], 'http://play-url.com');
+ await googleCastHandler.setValue(device, device.features[0], 'http://play-url.com', { volume: 30 });
});
it('should return device not found', async () => {
googleCastHandler.scanTimeout = 1;
const device = {
external_id: 'google_cast:toto',
};
- const promise = googleCastHandler.setValue(device, {}, 'http://play-url.com');
+ const promise = googleCastHandler.setValue(device, {}, 'http://play-url.com', { volume: 30 });
await assert.isRejected(promise, 'Device not found on network');
});
});
diff --git a/server/test/services/sonos/lib/sonos.setValue.test.js b/server/test/services/sonos/lib/sonos.setValue.test.js
index b1e332a53f..4fef0f080c 100644
--- a/server/test/services/sonos/lib/sonos.setValue.test.js
+++ b/server/test/services/sonos/lib/sonos.setValue.test.js
@@ -197,4 +197,31 @@ describe('SonosHandler.setValue', () => {
delayMs: 700,
});
});
+ it('should play notification on Sonos and change volume', async () => {
+ const device = {
+ name: 'My sonos',
+ external_id: 'sonos:test-uuid',
+ service_id: 'ffa13430-df93-488a-9733-5c540e9558e0',
+ should_poll: false,
+ };
+ const deviceFeature = {
+ name: 'My sonos - Play notification',
+ external_id: 'sonos:test-uuid:play-notification',
+ category: 'music',
+ type: 'play_notification',
+ min: 1,
+ max: 1,
+ keep_history: false,
+ read_only: false,
+ has_feedback: false,
+ };
+ await sonosHandler.setValue(device, deviceFeature, 'http://test.com', { volume: 30 });
+ assert.calledWith(devicePlayNotification, {
+ onlyWhenPlaying: false,
+ timeout: 20,
+ trackUri: 'http://test.com',
+ volume: 30,
+ delayMs: 700,
+ });
+ });
});