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 47a60bbb73..27cb81b191 100644
--- a/front/src/components/app.jsx
+++ b/front/src/components/app.jsx
@@ -170,6 +170,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);
@@ -279,6 +282,8 @@ const AppRouter = connect(
+
+
diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json
index 58336f23fd..c5ba36a19c 100644
--- a/front/src/config/i18n/de.json
+++ b/front/src/config/i18n/de.json
@@ -489,6 +489,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.",
@@ -1836,6 +1847,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",
+ "explanationText": "Um eine Variable in den Text einzufügen, gib \"{{\" ein. Um einen Variablenwert festzulegen, musst du zuerst das Feld \"Gerätewert abrufen\" verwenden.",
+ "messagePlaceholder": "Meine Message"
+ },
"turnOnLights": {
"label": "Wähle die Lichter aus, die eingeschaltet werden sollen"
},
@@ -2019,6 +2035,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 999ac437ec..6519ac2266 100644
--- a/front/src/config/i18n/en.json
+++ b/front/src/config/i18n/en.json
@@ -489,6 +489,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",
@@ -1836,6 +1847,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",
+ "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.",
+ "messagePlaceholder": "My message"
+ },
"turnOnLights": {
"label": "Select the lights you want to turn on"
},
@@ -2019,6 +2035,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 9fe345132e..c4f9e78920 100644
--- a/front/src/config/i18n/fr.json
+++ b/front/src/config/i18n/fr.json
@@ -617,6 +617,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.",
@@ -1836,6 +1847,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",
+ "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.",
+ "messagePlaceholder": "Mon message"
+ },
"turnOnLights": {
"label": "Sélectionnez les lumières que vous souhaitez allumer"
},
@@ -2019,6 +2035,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/communications.json b/front/src/config/integrations/communications.json
index ea2b3cd5af..9f4db29dad 100644
--- a/front/src/config/integrations/communications.json
+++ b/front/src/config/integrations/communications.json
@@ -22,5 +22,10 @@
{
"key": "openai",
"img": "/assets/integrations/cover/openai.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 e7cfb314b4..b3cfeb1283 100644
--- a/front/src/routes/scene/edit-scene/ActionCard.jsx
+++ b/front/src/routes/scene/edit-scene/ActionCard.jsx
@@ -33,6 +33,7 @@ import SendZigbee2MqttMessage from './actions/SendZigbee2MqttMessage';
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);
@@ -68,7 +69,8 @@ const ACTION_ICON = {
[ACTIONS.MQTT.SEND]: 'fe fe-message-square',
[ACTIONS.MUSIC.PLAY_NOTIFICATION]: 'fe fe-speaker',
[ACTIONS.ZIGBEE2MQTT.SEND]: 'fe fe-message-square',
- [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';
@@ -107,11 +109,13 @@ const ActionCard = ({ children, ...props }) => {
props.action.type === ACTIONS.CALENDAR.IS_EVENT_RUNNING ||
props.action.type === ACTIONS.MQTT.SEND ||
props.action.type === ACTIONS.ZIGBEE2MQTT.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 dbb88aea3f..15575b7eba 100644
--- a/front/src/routes/scene/edit-scene/actions/ChooseActionTypeCard.jsx
+++ b/front/src/routes/scene/edit-scene/actions/ChooseActionTypeCard.jsx
@@ -35,7 +35,8 @@ const ACTION_LIST = [
ACTIONS.MQTT.SEND,
ACTIONS.ZIGBEE2MQTT.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..38f5128043
--- /dev/null
+++ b/front/src/routes/scene/edit-scene/actions/SendSms.jsx
@@ -0,0 +1,41 @@
+import { Component } from 'preact';
+import { connect } from 'unistore/preact';
+import { Localizer, 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 99df9acc72..6f214b803c 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(
@@ -588,6 +589,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..afec783621
--- /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 params = {
+ user: username,
+ pass: accessToken,
+ msg: message,
+ };
+
+ try {
+ const response = await axios.get(url, { params });
+ 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 dcb78ee016..61b1991007 100644
--- a/server/services/index.js
+++ b/server/services/index.js
@@ -29,3 +29,4 @@ module.exports.sonos = require('./sonos');
module.exports['zwavejs-ui'] = require('./zwavejs-ui');
module.exports['google-cast'] = require('./google-cast');
module.exports.airplay = require('./airplay');
+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..6bae0d0479
--- /dev/null
+++ b/server/test/services/free-mobile/index.test.js
@@ -0,0 +1,133 @@
+const sinon = require('sinon');
+const { expect } = require('chai');
+const assert = require('assert');
+const proxyquire = require('proxyquire').noCallThru();
+const { ServiceNotConfiguredError } = require('../../../utils/coreErrors');
+const logger = require('../../../utils/logger');
+
+const serviceId = 'f87b7af2-ca8e-44fc-b754-444354b42fee';
+
+describe('FreeMobileService', () => {
+ let FreeMobileService;
+ let axiosStub;
+ let gladys;
+ let freeMobileService;
+ let sandbox;
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ axiosStub = {
+ get: async () => {
+ return { data: 'OK' };
+ },
+ };
+
+ FreeMobileService = proxyquire('../../../services/free-mobile', {
+ axios: axiosStub,
+ });
+
+ gladys = {
+ variable: {
+ getValue: async (key) => {
+ if (key === 'FREE_MOBILE_USERNAME') {
+ return 'validUsername';
+ }
+ if (key === 'FREE_MOBILE_ACCESS_TOKEN') {
+ return 'validAccessToken';
+ }
+ return null;
+ },
+ },
+ };
+
+ freeMobileService = FreeMobileService(gladys, serviceId);
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ describe('start', () => {
+ it('should start service with success', async () => {
+ await freeMobileService.start();
+
+ assert.strictEqual(await gladys.variable.getValue('FREE_MOBILE_USERNAME', serviceId), 'validUsername');
+ assert.strictEqual(await gladys.variable.getValue('FREE_MOBILE_ACCESS_TOKEN', serviceId), 'validAccessToken');
+ });
+
+ it('should throw ServiceNotConfiguredError if username is missing', async () => {
+ gladys.variable.getValue = async (key) => {
+ if (key === 'FREE_MOBILE_USERNAME') {
+ return null;
+ }
+ if (key === 'FREE_MOBILE_ACCESS_TOKEN') {
+ return 'validAccessToken';
+ }
+ return null;
+ };
+
+ try {
+ await freeMobileService.start();
+ throw new Error('Expected ServiceNotConfiguredError to be thrown');
+ } catch (e) {
+ expect(e).instanceOf(ServiceNotConfiguredError);
+ }
+ });
+
+ it('should throw ServiceNotConfiguredError if accessToken is missing', async () => {
+ gladys.variable.getValue = async (key) => {
+ if (key === 'FREE_MOBILE_USERNAME') {
+ return 'validUsername';
+ }
+ if (key === 'FREE_MOBILE_ACCESS_TOKEN') {
+ return null;
+ }
+ return null;
+ };
+
+ try {
+ await freeMobileService.start();
+ throw new Error('Expected ServiceNotConfiguredError to be thrown');
+ } catch (e) {
+ expect(e).instanceOf(ServiceNotConfiguredError);
+ }
+ });
+ });
+
+ describe('send', () => {
+ beforeEach(async () => {
+ await freeMobileService.start();
+ });
+
+ it('should send SMS successfully', async () => {
+ axiosStub.get = async (url, options) => {
+ assert.strictEqual(url, 'https://smsapi.free-mobile.fr/sendmsg');
+ assert.deepStrictEqual(options.params, {
+ user: 'validUsername',
+ pass: 'validAccessToken',
+ msg: 'Hello',
+ });
+ return { data: 'OK' };
+ };
+
+ await freeMobileService.sms.send('Hello');
+ });
+
+ it('should log an error if SMS fails', async () => {
+ axiosStub.get = async () => {
+ throw new Error('Network error');
+ };
+ const loggerErrorStub = sandbox.stub(logger, 'error');
+ 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);
+ });
+ });
+
+ describe('stop', () => {
+ it('should stopping service', async () => {
+ await freeMobileService.stop();
+ });
+ });
+});
diff --git a/server/utils/constants.js b/server/utils/constants.js
index aaba8e41e1..a185c249e6 100644
--- a/server/utils/constants.js
+++ b/server/utils/constants.js
@@ -418,6 +418,9 @@ const ACTIONS = {
MUSIC: {
PLAY_NOTIFICATION: 'music.play-notification',
},
+ SMS: {
+ SEND: 'sms.send',
+ },
};
const INTENTS = {