diff --git a/front/src/assets/integrations/cover/free-mobile.jpg b/front/src/assets/integrations/cover/free-mobile.jpg
new file mode 100644
index 0000000000..ec4dd245c1
Binary files /dev/null and b/front/src/assets/integrations/cover/free-mobile.jpg differ
diff --git a/front/src/components/app.jsx b/front/src/components/app.jsx
index 2962f58aae..876d096dae 100644
--- a/front/src/components/app.jsx
+++ b/front/src/components/app.jsx
@@ -166,6 +166,9 @@ import MELCloudDiscoverPage from '../routes/integration/all/melcloud/discover-pa
// NodeRed integration
import NodeRedPage from '../routes/integration/all/node-red/setup-page';
+// Free Mobile integration
+import FreeMobilePage from '../routes/integration/all/free-mobile';
+
const defaultState = getDefaultState();
const store = createStore(defaultState);
@@ -275,6 +278,8 @@ const AppRouter = connect(
+
+
diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json
index 2ef3b832c0..55113f427f 100644
--- a/front/src/config/i18n/de.json
+++ b/front/src/config/i18n/de.json
@@ -488,6 +488,17 @@
"configurationSuccess": "Konfiguration erfolgreich gesichert.",
"buttonSave": "Sichern"
},
+ "free-mobile": {
+ "title": "Free Mobile",
+ "description": "SMS von Gladys über Free Mobile senden.",
+ "documentation": "Free Mobile Dokumentation",
+ "introduction": "Dieses Plugin ermöglicht es Ihnen, SMS an Ihr Free-Handy über den Benachrichtigungsdienst von Free zu senden. Geben Sie Ihre Kundennummer und den Identifizierungsschlüssel unten ein, den Sie auf der FreeMobile-Website finden.",
+ "username": "Free Mobile Kundennummer",
+ "key": "Identifizierungsschlüssel für den Dienst",
+ "configurationError": "Wir konnten diese Konfiguration nicht speichern.",
+ "configurationSuccess": "Die Kontokonfiguration wurde erfolgreich gespeichert.",
+ "saveButton": "Sichern"
+ },
"philipsHue": {
"title": "Philips Hue",
"description": "Steuere Philips-Hue-Lichter und -Steckdosen mit der offiziellen Bridge.",
@@ -1800,6 +1811,11 @@
"textPlaceholder": "Nachrichtentext",
"explanationText": "Um eine Variable einzufügen, geben Sie '{{' ein. Achten Sie darauf, dass Sie zuvor eine Variable in einer Aktion 'Letzten Zustand abrufen' definiert haben, die vor diesem Nachrichtenblock platziert wurde."
},
+ "smsSend": {
+ "textLabel": "Nachricht",
+ "textPlaceholder": "Nachrichtentext",
+ "explanationText": "Um eine Variable in den Text einzufügen, gib \"{{\" ein. Um einen Variablenwert festzulegen, musst du zuerst das Feld \"Gerätewert abrufen\" verwenden."
+ },
"turnOnLights": {
"label": "Wähle die Lichter aus, die eingeschaltet werden sollen"
},
@@ -1976,6 +1992,9 @@
"send": "Nachricht senden",
"send-camera": "Kameraaufnahme senden"
},
+ "sms": {
+ "send": "SMS senden"
+ },
"delay": "Warten",
"light": {
"turn-on": "Licht einschalten",
diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json
index 05c245aa73..8cbc1b48ff 100644
--- a/front/src/config/i18n/en.json
+++ b/front/src/config/i18n/en.json
@@ -488,6 +488,17 @@
"configurationSuccess": "Successfully saved configuration.",
"buttonSave": "Save"
},
+ "free-mobile": {
+ "title": "Free Mobile",
+ "description": "Send SMS from Gladys using Free Mobile.",
+ "documentation": "Free Mobile Documentation",
+ "introduction": "This plugin allows you to send SMS to your Free cell phone via the notification service provided by Free. Enter your customer ID and the identification key below, which you can find on the FreeMobile website.",
+ "username": "Free Mobile Customer ID",
+ "key": "Identification key for the service",
+ "configurationError": "We could not save this configuration.",
+ "configurationSuccess": "Account configuration saved successfully.",
+ "saveButton": "Save"
+ },
"philipsHue": {
"title": "Philips Hue",
"description": "Control Philips Hue Lights and plugs with the official hub",
@@ -1800,6 +1811,11 @@
"textPlaceholder": "Message text",
"explanationText": "To insert a variable, type '{{'. Be careful, you must have defined a variable beforehand in a 'Retrieve the last state' action placed before this message block."
},
+ "smsSend": {
+ "textLabel": "Message",
+ "textPlaceholder": "Message text",
+ "explanationText": "To inject a variable in the text, press '{{'. To set a variable value, you need to use the 'Get device value' box before this one."
+ },
"turnOnLights": {
"label": "Select the lights you want to turn on"
},
@@ -1976,6 +1992,9 @@
"send": "Send Message",
"send-camera": "Send a camera image"
},
+ "sms": {
+ "send": "Send SMS"
+ },
"delay": "Wait",
"light": {
"turn-on": "Turn On the Lights",
diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json
index a23d5b0bac..7edc68425f 100644
--- a/front/src/config/i18n/fr.json
+++ b/front/src/config/i18n/fr.json
@@ -616,6 +616,17 @@
"configurationSuccess": "Sauvegarde de la configuration du compte terminée.",
"buttonSave": "Sauvegarder"
},
+ "free-mobile": {
+ "title": "Free Mobile",
+ "description": "Envoyer des sms depuis Gladys grâce à Free Mobile.",
+ "documentation": "Documentation Free Mobile",
+ "introduction": "Ce plugin vous permet d’envoyer des sms à votre portable Free via le service de notification proposé par Free. Entrez votre identifiant client et la clé d'identification ci-dessous que vous retrouverez sur le site FreeMobile.",
+ "username": "Identifiant client Free Mobile",
+ "key": "Clé d'identification au service",
+ "configurationError": "Nous n'avons pas pu sauvegarder cette configuration.",
+ "configurationSuccess": "Sauvegarde de la configuration du compte terminée.",
+ "saveButton": "Sauvegarder"
+ },
"philipsHue": {
"title": "Philips Hue",
"description": "Contrôler les lumières Philips Hue.",
@@ -1800,6 +1811,11 @@
"textPlaceholder": "Texte du message",
"explanationText": "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."
},
+ "smsSend": {
+ "textLabel": "Message",
+ "textPlaceholder": "Texte du message",
+ "explanationText": "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."
+ },
"turnOnLights": {
"label": "Sélectionnez les lumières que vous souhaitez allumer"
},
@@ -1976,6 +1992,9 @@
"send": "Envoyer un message",
"send-camera": "Envoyer une image de caméra"
},
+ "sms": {
+ "send": "Envoyer un sms"
+ },
"delay": "Attendre",
"light": {
"turn-on": "Allumer les lumières",
diff --git a/front/src/config/integrations/devices.json b/front/src/config/integrations/devices.json
index 945a3b34f0..6ab76f65f1 100644
--- a/front/src/config/integrations/devices.json
+++ b/front/src/config/integrations/devices.json
@@ -93,5 +93,10 @@
"key": "google-cast",
"link": "google-cast",
"img": "/assets/integrations/cover/google-cast.jpg"
+ },
+ {
+ "key": "free-mobile",
+ "link": "free-mobile",
+ "img": "/assets/integrations/cover/free-mobile.jpg"
}
]
diff --git a/front/src/routes/integration/all/free-mobile/FreeMobile.jsx b/front/src/routes/integration/all/free-mobile/FreeMobile.jsx
new file mode 100644
index 0000000000..ec2d83193b
--- /dev/null
+++ b/front/src/routes/integration/all/free-mobile/FreeMobile.jsx
@@ -0,0 +1,112 @@
+import { Text, MarkupText, Localizer } from 'preact-i18n';
+import cx from 'classnames';
+import { RequestStatus } from '../../../../utils/consts';
+import DeviceConfigurationLink from '../../../../components/documentation/DeviceConfigurationLink';
+
+const FreeMobilePage = ({ children, ...props }) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {props.freeMobileSaveSettingsStatus === RequestStatus.Error && (
+
+
+
+ )}
+ {props.freeMobileSaveSettingsStatus === RequestStatus.Success && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export default FreeMobilePage;
diff --git a/front/src/routes/integration/all/free-mobile/actions.js b/front/src/routes/integration/all/free-mobile/actions.js
new file mode 100644
index 0000000000..1f5e40ed81
--- /dev/null
+++ b/front/src/routes/integration/all/free-mobile/actions.js
@@ -0,0 +1,68 @@
+import { RequestStatus } from '../../../../utils/consts';
+
+const actions = store => ({
+ updateFreeMobileUsername(state, e) {
+ store.setState({
+ freeMobileUsername: e.target.value
+ });
+ },
+
+ updateFreeMobileAccessToken(state, e) {
+ store.setState({
+ freeMobileAccessToken: e.target.value
+ });
+ },
+
+ async getFreeMobileSettings(state) {
+ store.setState({
+ freeMobileGetSettingsStatus: RequestStatus.Getting
+ });
+ try {
+ const username = await state.httpClient.get('/api/v1/service/free-mobile/variable/FREE_MOBILE_USERNAME');
+ store.setState({
+ freeMobileUsername: username.value
+ });
+
+ const accessToken = await state.httpClient.get('/api/v1/service/free-mobile/variable/FREE_MOBILE_ACCESS_TOKEN');
+ store.setState({
+ freeMobileAccessToken: accessToken.value,
+ freeMobileGetSettingsStatus: RequestStatus.Success
+ });
+ } catch (e) {
+ store.setState({
+ freeMobileGetSettingsStatus: RequestStatus.Error
+ });
+ }
+ },
+
+ async saveFreeMobileSettings(state, e) {
+ e.preventDefault();
+ store.setState({
+ freeMobileSaveSettingsStatus: RequestStatus.Getting
+ });
+ try {
+ store.setState({
+ freeMobileUsername: state.freeMobileUsername.trim(),
+ freeMobileAccessToken: state.freeMobileAccessToken.trim()
+ });
+ await state.httpClient.post('/api/v1/service/free-mobile/variable/FREE_MOBILE_USERNAME', {
+ value: state.freeMobileUsername.trim()
+ });
+ await state.httpClient.post('/api/v1/service/free-mobile/variable/FREE_MOBILE_ACCESS_TOKEN', {
+ value: state.freeMobileAccessToken.trim()
+ });
+
+ // start service
+ await state.httpClient.post('/api/v1/service/free-mobile/start');
+ store.setState({
+ freeMobileSaveSettingsStatus: RequestStatus.Success
+ });
+ } catch (e) {
+ store.setState({
+ freeMobileSaveSettingsStatus: RequestStatus.Error
+ });
+ }
+ }
+});
+
+export default actions;
diff --git a/front/src/routes/integration/all/free-mobile/index.js b/front/src/routes/integration/all/free-mobile/index.js
new file mode 100644
index 0000000000..0c48b2cebe
--- /dev/null
+++ b/front/src/routes/integration/all/free-mobile/index.js
@@ -0,0 +1,23 @@
+import { Component } from 'preact';
+import { connect } from 'unistore/preact';
+import actions from './actions';
+import FreeMobilePage from './FreeMobile';
+import { RequestStatus } from '../../../../utils/consts';
+
+class FreeMobileIntegration extends Component {
+ componentWillMount() {
+ this.props.getFreeMobileSettings();
+ }
+
+ render(props, {}) {
+ const loading =
+ props.freeMobileGetSettingsStatus === RequestStatus.Getting ||
+ props.freeMobileSaveSettingsStatus === RequestStatus.Getting;
+ return ;
+ }
+}
+
+export default connect(
+ 'user,freeMobileUsername,freeMobileAccessToken,freeMobileGetSettingsStatus,freeMobileSaveSettingsStatus',
+ actions
+)(FreeMobileIntegration);
diff --git a/front/src/routes/scene/edit-scene/ActionCard.jsx b/front/src/routes/scene/edit-scene/ActionCard.jsx
index 7fae038dba..f9d4ebf431 100644
--- a/front/src/routes/scene/edit-scene/ActionCard.jsx
+++ b/front/src/routes/scene/edit-scene/ActionCard.jsx
@@ -32,6 +32,7 @@ import SendMqttMessage from './actions/SendMqttMessage';
import PlayNotification from './actions/PlayNotification';
import EdfTempoCondition from './actions/EdfTempoCondition';
import AskAI from './actions/AskAI';
+import SendSms from './actions/SendSms';
const deleteActionFromColumn = (columnIndex, rowIndex, deleteAction) => () => {
deleteAction(columnIndex, rowIndex);
@@ -66,7 +67,8 @@ const ACTION_ICON = {
[ACTIONS.ALARM.SET_ALARM_MODE]: 'fe fe-bell',
[ACTIONS.MQTT.SEND]: 'fe fe-message-square',
[ACTIONS.MUSIC.PLAY_NOTIFICATION]: 'fe fe-speaker',
- [ACTIONS.AI.ASK]: 'fe fe-cpu'
+ [ACTIONS.AI.ASK]: 'fe fe-cpu',
+ [ACTIONS.SMS.SEND]: 'fe fe-message-circle'
};
const ACTION_CARD_TYPE = 'ACTION_CARD_TYPE';
@@ -104,11 +106,13 @@ const ActionCard = ({ children, ...props }) => {
props.action.type === ACTIONS.MESSAGE.SEND ||
props.action.type === ACTIONS.CALENDAR.IS_EVENT_RUNNING ||
props.action.type === ACTIONS.MQTT.SEND ||
- props.action.type === ACTIONS.LIGHT.BLINK,
+ props.action.type === ACTIONS.LIGHT.BLINK ||
+ props.action.type === ACTIONS.SMS.SEND,
'col-lg-4':
props.action.type !== ACTIONS.CONDITION.ONLY_CONTINUE_IF &&
props.action.type !== ACTIONS.MESSAGE.SEND &&
- props.action.type !== ACTIONS.CALENDAR.IS_EVENT_RUNNING
+ props.action.type !== ACTIONS.CALENDAR.IS_EVENT_RUNNING &&
+ props.action.type !== ACTIONS.SMS.SEND
})}
>
{
triggersVariables={props.triggersVariables}
/>
)}
+ {props.action.type === ACTIONS.SMS.SEND && (
+
+ )}
diff --git a/front/src/routes/scene/edit-scene/actions/ChooseActionTypeCard.jsx b/front/src/routes/scene/edit-scene/actions/ChooseActionTypeCard.jsx
index 786248fb80..3735db6bbb 100644
--- a/front/src/routes/scene/edit-scene/actions/ChooseActionTypeCard.jsx
+++ b/front/src/routes/scene/edit-scene/actions/ChooseActionTypeCard.jsx
@@ -34,7 +34,8 @@ const ACTION_LIST = [
ACTIONS.ALARM.SET_ALARM_MODE,
ACTIONS.MQTT.SEND,
ACTIONS.MUSIC.PLAY_NOTIFICATION,
- ACTIONS.AI.ASK
+ ACTIONS.AI.ASK,
+ ACTIONS.SMS.SEND
];
const TRANSLATIONS = ACTION_LIST.reduce((acc, action) => {
diff --git a/front/src/routes/scene/edit-scene/actions/SendSms.jsx b/front/src/routes/scene/edit-scene/actions/SendSms.jsx
new file mode 100644
index 0000000000..496e82a2b3
--- /dev/null
+++ b/front/src/routes/scene/edit-scene/actions/SendSms.jsx
@@ -0,0 +1,40 @@
+import { Component } from 'preact';
+import { connect } from 'unistore/preact';
+import { Text } from 'preact-i18n';
+
+import TextWithVariablesInjected from '../../../../components/scene/TextWithVariablesInjected';
+
+class SendSms extends Component {
+ updateText = text => {
+ this.props.updateActionProperty(this.props.columnIndex, this.props.index, 'text', text);
+ };
+
+ render(props, {}) {
+ return (
+
+ );
+ }
+}
+
+export default connect('httpClient', {})(SendSms);
diff --git a/server/lib/scene/scene.actions.js b/server/lib/scene/scene.actions.js
index dafbcc422e..5ba3d6105a 100644
--- a/server/lib/scene/scene.actions.js
+++ b/server/lib/scene/scene.actions.js
@@ -225,6 +225,7 @@ const actionsFunc = {
}
setTimeout(resolve, timeToWaitMilliseconds);
}),
+
[ACTIONS.SCENE.START]: async (self, action, scope) => {
if (scope.alreadyExecutedScenes && scope.alreadyExecutedScenes.has(action.scene)) {
logger.info(
@@ -580,6 +581,14 @@ const actionsFunc = {
// Play TTS Notification on device
await self.device.setValue(device, deviceFeature, url);
},
+ [ACTIONS.SMS.SEND]: async (self, action, scope) => {
+ const freeMobileService = self.service.getService('free-mobile');
+
+ if (freeMobileService) {
+ const textWithVariables = Handlebars.compile(action.text)(scope);
+ freeMobileService.sms.send(textWithVariables);
+ }
+ },
};
module.exports = {
diff --git a/server/services/free-mobile/index.js b/server/services/free-mobile/index.js
new file mode 100644
index 0000000000..f0640bdabc
--- /dev/null
+++ b/server/services/free-mobile/index.js
@@ -0,0 +1,69 @@
+const logger = require('../../utils/logger');
+const { ServiceNotConfiguredError } = require('../../utils/coreErrors');
+
+module.exports = function FreeMobileService(gladys, serviceId) {
+ const axios = require('axios');
+ let username;
+ let accessToken;
+
+ /**
+ * @public
+ * @description This function starts the FreeMobile service.
+ * @example
+ * gladys.services.free-mobile.start();
+ */
+ async function start() {
+ logger.info('Starting Free Mobile service');
+ username = await gladys.variable.getValue('FREE_MOBILE_USERNAME', serviceId);
+ accessToken = await gladys.variable.getValue('FREE_MOBILE_ACCESS_TOKEN', serviceId);
+
+ if (!username || username.length === 0) {
+ throw new ServiceNotConfiguredError('No FreeMobile username found. Not starting Free Mobile service');
+ }
+
+ if (!accessToken || accessToken.length === 0) {
+ throw new ServiceNotConfiguredError('No FreeMobile access_token found. Not starting Free Mobile service');
+ }
+ }
+
+ /**
+ * @description Send a sms.
+ * @param {string} message - The message to send.
+ * @example
+ * gladys.services.free-mobile.sms.send('hello')
+ */
+ async function send(message) {
+ const url = 'https://smsapi.free-mobile.fr/sendmsg';
+
+ const data = {
+ user: username,
+ pass: accessToken,
+ msg: message,
+ };
+
+ try {
+ const response = await axios.post(url, data);
+ logger.debug('SMS successfully sent:', response.data);
+ } catch (e) {
+ logger.error('Error sending SMS:', e);
+ }
+ }
+
+ /**
+ * @public
+ * @description This function stops the FreeMobile service.
+ * @example
+ * gladys.services.free-mobile.stop();
+ */
+ async function stop() {
+ logger.info('Stopping Free Mobile service');
+ }
+
+ return Object.freeze({
+ start,
+ stop,
+ sms: {
+ send,
+ },
+ });
+};
diff --git a/server/services/free-mobile/package.json b/server/services/free-mobile/package.json
new file mode 100644
index 0000000000..a339d55687
--- /dev/null
+++ b/server/services/free-mobile/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "gladys-free-mobile",
+ "version": "1.0.0",
+ "main": "index.js",
+ "os": [
+ "darwin",
+ "linux",
+ "win32"
+ ],
+ "cpu": [
+ "x64",
+ "arm",
+ "arm64"
+ ],
+ "dependencies": {
+ "axios": "^1.4.0"
+ }
+}
diff --git a/server/services/index.js b/server/services/index.js
index dec39add7c..e10c13917d 100644
--- a/server/services/index.js
+++ b/server/services/index.js
@@ -28,3 +28,4 @@ module.exports.netatmo = require('./netatmo');
module.exports.sonos = require('./sonos');
module.exports['zwavejs-ui'] = require('./zwavejs-ui');
module.exports['google-cast'] = require('./google-cast');
+module.exports['free-mobile'] = require('./free-mobile');
diff --git a/server/test/lib/scene/actions/scene.action.sendSms.test.js b/server/test/lib/scene/actions/scene.action.sendSms.test.js
new file mode 100644
index 0000000000..869c6cfbbc
--- /dev/null
+++ b/server/test/lib/scene/actions/scene.action.sendSms.test.js
@@ -0,0 +1,86 @@
+const { fake, assert } = require('sinon');
+const EventEmitter = require('events');
+
+const { ACTIONS } = require('../../../../utils/constants');
+const { executeActions } = require('../../../../lib/scene/scene.executeActions');
+
+const StateManager = require('../../../../lib/state');
+
+const event = new EventEmitter();
+
+describe('scene.send-sms', () => {
+ it('should send message with value injected from device get-value', async () => {
+ const stateManager = new StateManager(event);
+ stateManager.setState('deviceFeature', 'my-device-feature', {
+ category: 'light',
+ type: 'binary',
+ last_value: 15,
+ });
+ const freeMobileService = {
+ sms: {
+ send: fake.resolves(null),
+ },
+ };
+ const service = {
+ getService: fake.returns(freeMobileService),
+ };
+ const scope = {};
+ await executeActions(
+ { stateManager, event, service },
+ [
+ [
+ {
+ type: ACTIONS.DEVICE.GET_VALUE,
+ device_feature: 'my-device-feature',
+ },
+ ],
+ [
+ {
+ type: ACTIONS.SMS.SEND,
+ text: 'Temperature in the living room is {{0.0.last_value}} °C.',
+ },
+ ],
+ ],
+ scope,
+ );
+ assert.calledWith(freeMobileService.sms.send, 'Temperature in the living room is 15 °C.');
+ });
+
+ it('should send message with value injected from http-request', async () => {
+ const stateManager = new StateManager(event);
+ const http = {
+ request: fake.resolves({ result: [15], error: null }),
+ };
+ const freeMobileService = {
+ sms: {
+ send: fake.resolves(null),
+ },
+ };
+ const service = {
+ getService: fake.returns(freeMobileService),
+ };
+ const scope = {};
+ await executeActions(
+ { stateManager, event, service, http },
+ [
+ [
+ {
+ type: ACTIONS.HTTP.REQUEST,
+ method: 'post',
+ url: 'http://test.test',
+ body: '{"toto":"toto"}',
+ headers: [],
+ },
+ ],
+ [
+ {
+ type: ACTIONS.SMS.SEND,
+ text: 'Temperature in the living room is {{0.0.result.[0]}} °C.',
+ },
+ ],
+ ],
+ scope,
+ );
+ assert.calledWith(freeMobileService.sms.send, 'Temperature in the living room is 15 °C.');
+ });
+});
diff --git a/server/test/services/free-mobile/index.test.js b/server/test/services/free-mobile/index.test.js
new file mode 100644
index 0000000000..06d4ddab61
--- /dev/null
+++ b/server/test/services/free-mobile/index.test.js
@@ -0,0 +1,103 @@
+const sinon = require('sinon');
+const { expect } = require('chai');
+
+const axios = require('axios');
+const logger = require('../../../utils/logger');
+const FreeMobileService = require('../../../services/free-mobile');
+const { ServiceNotConfiguredError } = require('../../../utils/coreErrors');
+
+const serviceId = 'f87b7af2-ca8e-44fc-b754-444354b42fee';
+
+describe('free-mobile', () => {
+ let gladys;
+ let freeMobileService;
+
+ beforeEach(() => {
+ gladys = {
+ variable: {
+ getValue: sinon.stub(),
+ },
+ };
+ freeMobileService = FreeMobileService(gladys, serviceId);
+ });
+
+ describe('start', () => {
+ it('should throw ServiceNotConfiguredError if username is missing', async () => {
+ gladys.variable.getValue.resolves(null);
+
+ try {
+ await freeMobileService.start();
+ throw new Error('Expected ServiceNotConfiguredError to be thrown');
+ } catch (error) {
+ expect(error).to.be.instanceOf(ServiceNotConfiguredError);
+ }
+ });
+
+ it('should throw ServiceNotConfiguredError if accessToken is missing', async () => {
+ gladys.variable.getValue
+ .onFirstCall()
+ .resolves('validUsername')
+ .onSecondCall()
+ .resolves(null);
+
+ try {
+ await freeMobileService.start();
+ throw new Error('Expected ServiceNotConfiguredError to be thrown');
+ } catch (error) {
+ expect(error).to.be.instanceOf(ServiceNotConfiguredError);
+ }
+ });
+ });
+
+ describe('send', () => {
+ it('should send SMS successfully', async () => {
+ gladys.variable.getValue
+ .onFirstCall()
+ .resolves('validUsername')
+ .onSecondCall()
+ .resolves('validAccessToken');
+
+ const axiosPostStub = sinon.stub(axios, 'post').resolves({ data: 'success' });
+
+ await freeMobileService.start();
+ await freeMobileService.sms.send('Hello World');
+
+ const callArgs = axiosPostStub.getCall(0).args;
+ expect(callArgs[0]).to.equal('https://smsapi.free-mobile.fr/sendmsg');
+ expect(callArgs[1]).to.deep.equal({
+ user: 'validUsername',
+ pass: 'validAccessToken',
+ msg: 'Hello World',
+ });
+
+ axiosPostStub.restore();
+ });
+
+ it('should log an error if SMS fails', async () => {
+ gladys.variable.getValue
+ .onFirstCall()
+ .resolves('validUsername')
+ .onSecondCall()
+ .resolves('validAccessToken');
+
+ const axiosPostStub = sinon.stub(axios, 'post').rejects(new Error('Network error'));
+ const loggerErrorStub = sinon.stub(logger, 'error');
+
+ await freeMobileService.start();
+ await freeMobileService.sms.send('Hello World');
+
+ const errorArgs = loggerErrorStub.getCall(0).args;
+ expect(errorArgs[0]).to.equal('Error sending SMS:');
+ expect(errorArgs[1]).to.be.instanceOf(Error);
+
+ axiosPostStub.restore();
+ loggerErrorStub.restore();
+ });
+ });
+
+ describe('stop', () => {
+ it('should stopping service', async () => {
+ await freeMobileService.stop();
+ });
+ });
+});
diff --git a/server/utils/constants.js b/server/utils/constants.js
index d669799326..d515e4f418 100644
--- a/server/utils/constants.js
+++ b/server/utils/constants.js
@@ -407,6 +407,9 @@ const ACTIONS = {
MUSIC: {
PLAY_NOTIFICATION: 'music.play-notification',
},
+ SMS: {
+ SEND: 'sms.send',
+ },
};
const INTENTS = {