From 11492ae9ea574fc5f4089bf05caecb986b900730 Mon Sep 17 00:00:00 2001 From: William Deren Date: Mon, 4 Nov 2024 00:26:55 +0100 Subject: [PATCH 01/10] add new service Free Mobile SMS --- .../assets/integrations/cover/free-mobile.jpg | Bin 0 -> 51951 bytes front/src/components/app.jsx | 5 + front/src/config/i18n/de.json | 19 +++ front/src/config/i18n/en.json | 19 +++ front/src/config/i18n/fr.json | 19 +++ front/src/config/integrations/devices.json | 5 + .../all/free-mobile/FreeMobile.jsx | 112 ++++++++++++++++++ .../integration/all/free-mobile/actions.js | 68 +++++++++++ .../integration/all/free-mobile/index.js | 23 ++++ .../routes/scene/edit-scene/ActionCard.jsx | 21 +++- .../actions/ChooseActionTypeCard.jsx | 3 +- .../scene/edit-scene/actions/SendSms.jsx | 40 +++++++ server/lib/scene/scene.actions.js | 9 ++ server/services/free-mobile/index.js | 69 +++++++++++ server/services/free-mobile/package.json | 18 +++ server/services/index.js | 1 + .../actions/scene.action.sendSms.test.js | 86 ++++++++++++++ .../test/services/free-mobile/index.test.js | 103 ++++++++++++++++ server/utils/constants.js | 3 + 19 files changed, 619 insertions(+), 4 deletions(-) create mode 100644 front/src/assets/integrations/cover/free-mobile.jpg create mode 100644 front/src/routes/integration/all/free-mobile/FreeMobile.jsx create mode 100644 front/src/routes/integration/all/free-mobile/actions.js create mode 100644 front/src/routes/integration/all/free-mobile/index.js create mode 100644 front/src/routes/scene/edit-scene/actions/SendSms.jsx create mode 100644 server/services/free-mobile/index.js create mode 100644 server/services/free-mobile/package.json create mode 100644 server/test/lib/scene/actions/scene.action.sendSms.test.js create mode 100644 server/test/services/free-mobile/index.test.js 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 0000000000000000000000000000000000000000..ec4dd245c1d4c28281dae2bd7e4c2027a23562e1 GIT binary patch literal 51951 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 + + diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json index 5744a1f368..1463c35b1d 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.", @@ -1801,6 +1812,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" }, @@ -1984,6 +2000,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 7357e830ab..fa30676f68 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", @@ -1801,6 +1812,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" }, @@ -1984,6 +2000,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 98625dd217..02dbb4b44c 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.", @@ -1801,6 +1812,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" }, @@ -1984,6 +2000,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 && ( +
+ +
+ )} +
+
+
+ +
+ + } + onInput={props.updateFreeMobileUsername} + value={props.freeMobileUsername} + /> + +
+ +
+
+ +
+ + } + onInput={props.updateFreeMobileAccessToken} + value={props.freeMobileAccessToken} + /> + +
+ +
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+); + +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..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 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..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 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 = { From 1b45d48bf544dc03698482253b0bf3ec251571c5 Mon Sep 17 00:00:00 2001 From: William Deren Date: Thu, 7 Nov 2024 01:10:42 +0100 Subject: [PATCH 02/10] fix test --- server/test/services/free-mobile/index.test.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/server/test/services/free-mobile/index.test.js b/server/test/services/free-mobile/index.test.js index 06d4ddab61..84227c8d48 100644 --- a/server/test/services/free-mobile/index.test.js +++ b/server/test/services/free-mobile/index.test.js @@ -11,6 +11,8 @@ const serviceId = 'f87b7af2-ca8e-44fc-b754-444354b42fee'; describe('free-mobile', () => { let gladys; let freeMobileService; + let axiosPostStub; + let loggerErrorStub; beforeEach(() => { gladys = { @@ -19,6 +21,14 @@ describe('free-mobile', () => { }, }; freeMobileService = FreeMobileService(gladys, serviceId); + + axiosPostStub = sinon.stub(axios, 'post'); + loggerErrorStub = sinon.stub(logger, 'error'); + }); + + afterEach(() => { + axiosPostStub.restore(); + loggerErrorStub.restore(); }); describe('start', () => { @@ -57,7 +67,7 @@ describe('free-mobile', () => { .onSecondCall() .resolves('validAccessToken'); - const axiosPostStub = sinon.stub(axios, 'post').resolves({ data: 'success' }); + axiosPostStub.resolves({ data: 'success' }); await freeMobileService.start(); await freeMobileService.sms.send('Hello World'); @@ -70,7 +80,6 @@ describe('free-mobile', () => { msg: 'Hello World', }); - axiosPostStub.restore(); }); it('should log an error if SMS fails', async () => { @@ -80,8 +89,7 @@ describe('free-mobile', () => { .onSecondCall() .resolves('validAccessToken'); - const axiosPostStub = sinon.stub(axios, 'post').rejects(new Error('Network error')); - const loggerErrorStub = sinon.stub(logger, 'error'); + axiosPostStub.rejects(new Error('Network error')); await freeMobileService.start(); await freeMobileService.sms.send('Hello World'); @@ -90,8 +98,6 @@ describe('free-mobile', () => { expect(errorArgs[0]).to.equal('Error sending SMS:'); expect(errorArgs[1]).to.be.instanceOf(Error); - axiosPostStub.restore(); - loggerErrorStub.restore(); }); }); From 8230b9d8053db2146628ce99973501c1b2190761 Mon Sep 17 00:00:00 2001 From: William Deren Date: Thu, 7 Nov 2024 01:19:27 +0100 Subject: [PATCH 03/10] fix prettier --- server/test/services/free-mobile/index.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/test/services/free-mobile/index.test.js b/server/test/services/free-mobile/index.test.js index 84227c8d48..0bb85cd298 100644 --- a/server/test/services/free-mobile/index.test.js +++ b/server/test/services/free-mobile/index.test.js @@ -79,7 +79,6 @@ describe('free-mobile', () => { pass: 'validAccessToken', msg: 'Hello World', }); - }); it('should log an error if SMS fails', async () => { @@ -97,7 +96,6 @@ describe('free-mobile', () => { const errorArgs = loggerErrorStub.getCall(0).args; expect(errorArgs[0]).to.equal('Error sending SMS:'); expect(errorArgs[1]).to.be.instanceOf(Error); - }); }); From 240ca142f2ab5e013ae4f3c9f507d1536c297877 Mon Sep 17 00:00:00 2001 From: William Deren Date: Sun, 10 Nov 2024 14:58:30 +0100 Subject: [PATCH 04/10] modify test --- server/test/services/free-mobile/index.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/test/services/free-mobile/index.test.js b/server/test/services/free-mobile/index.test.js index 0bb85cd298..756029112e 100644 --- a/server/test/services/free-mobile/index.test.js +++ b/server/test/services/free-mobile/index.test.js @@ -29,6 +29,7 @@ describe('free-mobile', () => { afterEach(() => { axiosPostStub.restore(); loggerErrorStub.restore(); + sinon.restore(); }); describe('start', () => { @@ -72,6 +73,8 @@ describe('free-mobile', () => { await freeMobileService.start(); await freeMobileService.sms.send('Hello World'); + expect(axiosPostStub.calledOnce).to.equal(true); + const callArgs = axiosPostStub.getCall(0).args; expect(callArgs[0]).to.equal('https://smsapi.free-mobile.fr/sendmsg'); expect(callArgs[1]).to.deep.equal({ @@ -93,6 +96,8 @@ describe('free-mobile', () => { await freeMobileService.start(); await freeMobileService.sms.send('Hello World'); + expect(loggerErrorStub.calledOnce).to.equal(true); + const errorArgs = loggerErrorStub.getCall(0).args; expect(errorArgs[0]).to.equal('Error sending SMS:'); expect(errorArgs[1]).to.be.instanceOf(Error); From 8de5e0cdc17ab7dff44dd6323237e09d852dd1c1 Mon Sep 17 00:00:00 2001 From: William Deren Date: Sun, 10 Nov 2024 17:25:58 +0100 Subject: [PATCH 05/10] add log --- server/test/services/free-mobile/index.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/test/services/free-mobile/index.test.js b/server/test/services/free-mobile/index.test.js index 756029112e..b26c061e8e 100644 --- a/server/test/services/free-mobile/index.test.js +++ b/server/test/services/free-mobile/index.test.js @@ -71,11 +71,16 @@ describe('free-mobile', () => { axiosPostStub.resolves({ data: 'success' }); await freeMobileService.start(); + + logger.debug('username:', await gladys.variable.getValue.firstCall.returnValue); + logger.debug('accessToken:', await gladys.variable.getValue.secondCall.returnValue); + await freeMobileService.sms.send('Hello World'); expect(axiosPostStub.calledOnce).to.equal(true); const callArgs = axiosPostStub.getCall(0).args; + logger.debug('Arguments de l’appel à axios.post:', callArgs); expect(callArgs[0]).to.equal('https://smsapi.free-mobile.fr/sendmsg'); expect(callArgs[1]).to.deep.equal({ user: 'validUsername', From 2065f9f7d058d0c4e1cf67b4f2031046a1d4ed33 Mon Sep 17 00:00:00 2001 From: William Deren Date: Mon, 11 Nov 2024 14:00:35 +0100 Subject: [PATCH 06/10] change axios post by get and modify test --- server/services/free-mobile/index.js | 4 +- .../test/services/free-mobile/index.test.js | 140 ++++++++++-------- 2 files changed, 82 insertions(+), 62 deletions(-) diff --git a/server/services/free-mobile/index.js b/server/services/free-mobile/index.js index f0640bdabc..afec783621 100644 --- a/server/services/free-mobile/index.js +++ b/server/services/free-mobile/index.js @@ -35,14 +35,14 @@ module.exports = function FreeMobileService(gladys, serviceId) { async function send(message) { const url = 'https://smsapi.free-mobile.fr/sendmsg'; - const data = { + const params = { user: username, pass: accessToken, msg: message, }; try { - const response = await axios.post(url, data); + const response = await axios.get(url, { params }); logger.debug('SMS successfully sent:', response.data); } catch (e) { logger.error('Error sending SMS:', e); diff --git a/server/test/services/free-mobile/index.test.js b/server/test/services/free-mobile/index.test.js index b26c061e8e..2469ed7a31 100644 --- a/server/test/services/free-mobile/index.test.js +++ b/server/test/services/free-mobile/index.test.js @@ -1,112 +1,132 @@ const sinon = require('sinon'); const { expect } = require('chai'); - -const axios = require('axios'); -const logger = require('../../../utils/logger'); -const FreeMobileService = require('../../../services/free-mobile'); +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('free-mobile', () => { +describe('FreeMobileService', () => { + let FreeMobileService; + let axiosStub; let gladys; let freeMobileService; - let axiosPostStub; - let loggerErrorStub; beforeEach(() => { + axiosStub = { + get: async () => { + return { data: 'OK' }; + }, + }; + + FreeMobileService = proxyquire('../../../services/free-mobile', { + axios: axiosStub, + }); + gladys = { variable: { - getValue: sinon.stub(), + getValue: async (key) => { + if (key === 'FREE_MOBILE_USERNAME') { + return 'validUsername'; + } + if (key === 'FREE_MOBILE_ACCESS_TOKEN') { + return 'validAccessToken'; + } + return null; + }, }, }; - freeMobileService = FreeMobileService(gladys, serviceId); - - axiosPostStub = sinon.stub(axios, 'post'); - loggerErrorStub = sinon.stub(logger, 'error'); - }); - afterEach(() => { - axiosPostStub.restore(); - loggerErrorStub.restore(); - sinon.restore(); + freeMobileService = FreeMobileService(gladys, serviceId); }); 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.resolves(null); + // gladys.variable.getValue.resolves(null); + 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 (error) { - expect(error).to.be.instanceOf(ServiceNotConfiguredError); + } catch (e) { + expect(e).instanceOf(ServiceNotConfiguredError); } }); it('should throw ServiceNotConfiguredError if accessToken is missing', async () => { + /* gladys.variable.getValue .onFirstCall() .resolves('validUsername') .onSecondCall() .resolves(null); - + */ + 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 (error) { - expect(error).to.be.instanceOf(ServiceNotConfiguredError); + } catch (e) { + expect(e).instanceOf(ServiceNotConfiguredError); } }); + }); describe('send', () => { - it('should send SMS successfully', async () => { - gladys.variable.getValue - .onFirstCall() - .resolves('validUsername') - .onSecondCall() - .resolves('validAccessToken'); - - axiosPostStub.resolves({ data: 'success' }); - + beforeEach(async () => { await freeMobileService.start(); + }); - logger.debug('username:', await gladys.variable.getValue.firstCall.returnValue); - logger.debug('accessToken:', await gladys.variable.getValue.secondCall.returnValue); - - await freeMobileService.sms.send('Hello World'); - - expect(axiosPostStub.calledOnce).to.equal(true); - - const callArgs = axiosPostStub.getCall(0).args; - logger.debug('Arguments de l’appel à axios.post:', callArgs); - expect(callArgs[0]).to.equal('https://smsapi.free-mobile.fr/sendmsg'); - expect(callArgs[1]).to.deep.equal({ - user: 'validUsername', - pass: 'validAccessToken', - msg: 'Hello World', - }); + 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 () => { - gladys.variable.getValue - .onFirstCall() - .resolves('validUsername') - .onSecondCall() - .resolves('validAccessToken'); - - axiosPostStub.rejects(new Error('Network error')); - - await freeMobileService.start(); + axiosStub.get = async () => { + throw new Error('Network error'); + }; + const loggerErrorStub = sinon.stub(logger, 'error'); await freeMobileService.sms.send('Hello World'); - - expect(loggerErrorStub.calledOnce).to.equal(true); - const errorArgs = loggerErrorStub.getCall(0).args; expect(errorArgs[0]).to.equal('Error sending SMS:'); expect(errorArgs[1]).to.be.instanceOf(Error); }); + }); describe('stop', () => { @@ -114,4 +134,4 @@ describe('free-mobile', () => { await freeMobileService.stop(); }); }); -}); +}); \ No newline at end of file From 6a71f7fdd0c0a031fb2f9fa1b052f64d1c926648 Mon Sep 17 00:00:00 2001 From: William Deren Date: Mon, 11 Nov 2024 22:39:20 +0100 Subject: [PATCH 07/10] fix prettier --- server/test/services/free-mobile/index.test.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/server/test/services/free-mobile/index.test.js b/server/test/services/free-mobile/index.test.js index 2469ed7a31..b3bc89fcbf 100644 --- a/server/test/services/free-mobile/index.test.js +++ b/server/test/services/free-mobile/index.test.js @@ -15,8 +15,8 @@ describe('FreeMobileService', () => { beforeEach(() => { axiosStub = { - get: async () => { - return { data: 'OK' }; + get: async () => { + return { data: 'OK' }; }, }; @@ -44,7 +44,7 @@ describe('FreeMobileService', () => { 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'); }); @@ -86,7 +86,7 @@ describe('FreeMobileService', () => { } return null; }; - + try { await freeMobileService.start(); throw new Error('Expected ServiceNotConfiguredError to be thrown'); @@ -94,7 +94,6 @@ describe('FreeMobileService', () => { expect(e).instanceOf(ServiceNotConfiguredError); } }); - }); describe('send', () => { @@ -126,7 +125,6 @@ describe('FreeMobileService', () => { expect(errorArgs[0]).to.equal('Error sending SMS:'); expect(errorArgs[1]).to.be.instanceOf(Error); }); - }); describe('stop', () => { @@ -134,4 +132,4 @@ describe('FreeMobileService', () => { await freeMobileService.stop(); }); }); -}); \ No newline at end of file +}); From 53e85faa87a6216c863a7bf86bff1d88d6b9e278 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Fri, 22 Nov 2024 09:49:48 +0100 Subject: [PATCH 08/10] Use stub sandbox --- server/test/services/free-mobile/index.test.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/server/test/services/free-mobile/index.test.js b/server/test/services/free-mobile/index.test.js index b3bc89fcbf..6bae0d0479 100644 --- a/server/test/services/free-mobile/index.test.js +++ b/server/test/services/free-mobile/index.test.js @@ -12,8 +12,10 @@ describe('FreeMobileService', () => { let axiosStub; let gladys; let freeMobileService; + let sandbox; beforeEach(() => { + sandbox = sinon.createSandbox(); axiosStub = { get: async () => { return { data: 'OK' }; @@ -41,6 +43,10 @@ describe('FreeMobileService', () => { freeMobileService = FreeMobileService(gladys, serviceId); }); + afterEach(() => { + sandbox.restore(); + }); + describe('start', () => { it('should start service with success', async () => { await freeMobileService.start(); @@ -50,7 +56,6 @@ describe('FreeMobileService', () => { }); it('should throw ServiceNotConfiguredError if username is missing', async () => { - // gladys.variable.getValue.resolves(null); gladys.variable.getValue = async (key) => { if (key === 'FREE_MOBILE_USERNAME') { return null; @@ -70,13 +75,6 @@ describe('FreeMobileService', () => { }); it('should throw ServiceNotConfiguredError if accessToken is missing', async () => { - /* - gladys.variable.getValue - .onFirstCall() - .resolves('validUsername') - .onSecondCall() - .resolves(null); - */ gladys.variable.getValue = async (key) => { if (key === 'FREE_MOBILE_USERNAME') { return 'validUsername'; @@ -119,7 +117,7 @@ describe('FreeMobileService', () => { axiosStub.get = async () => { throw new Error('Network error'); }; - const loggerErrorStub = sinon.stub(logger, '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:'); From cca4a453db3551f5580507f2cf83b6c6a0d8bdd5 Mon Sep 17 00:00:00 2001 From: William Deren Date: Fri, 22 Nov 2024 21:25:02 +0100 Subject: [PATCH 09/10] add placeholder --- front/src/config/i18n/de.json | 4 ++-- front/src/config/i18n/en.json | 4 ++-- front/src/config/i18n/fr.json | 4 ++-- front/src/routes/scene/edit-scene/actions/SendSms.jsx | 7 ++++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json index 22700437e6..c5ba36a19c 100644 --- a/front/src/config/i18n/de.json +++ b/front/src/config/i18n/de.json @@ -1849,8 +1849,8 @@ }, "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." + "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" diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 2d24e28db1..6519ac2266 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -1849,8 +1849,8 @@ }, "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." + "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" diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index ea9fb7ca2d..c4f9e78920 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -1849,8 +1849,8 @@ }, "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." + "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" diff --git a/front/src/routes/scene/edit-scene/actions/SendSms.jsx b/front/src/routes/scene/edit-scene/actions/SendSms.jsx index 496e82a2b3..38f5128043 100644 --- a/front/src/routes/scene/edit-scene/actions/SendSms.jsx +++ b/front/src/routes/scene/edit-scene/actions/SendSms.jsx @@ -1,6 +1,6 @@ import { Component } from 'preact'; import { connect } from 'unistore/preact'; -import { Text } from 'preact-i18n'; +import { Localizer, Text } from 'preact-i18n'; import TextWithVariablesInjected from '../../../../components/scene/TextWithVariablesInjected'; @@ -22,15 +22,16 @@ class SendSms extends Component {
-
+ } /> -
+ ); From 33aabdee5aee7eff006e701f4d6c2a9421402c3e Mon Sep 17 00:00:00 2001 From: William Deren Date: Sat, 23 Nov 2024 00:16:07 +0100 Subject: [PATCH 10/10] move free-mobile integration in the communication tab --- front/src/config/integrations/communications.json | 5 +++++ front/src/config/integrations/devices.json | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) 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/config/integrations/devices.json b/front/src/config/integrations/devices.json index d310cbcfcd..1adff7c565 100644 --- a/front/src/config/integrations/devices.json +++ b/front/src/config/integrations/devices.json @@ -98,10 +98,5 @@ "key": "airplay", "link": "airplay", "img": "/assets/integrations/cover/airplay.jpg" - }, - { - "key": "free-mobile", - "link": "free-mobile", - "img": "/assets/integrations/cover/free-mobile.jpg" } ]