UNPKG

controlmyspajs

Version:
889 lines (754 loc) 23.5 kB
const querystring = require("querystring"); const axios = require("axios").default; const https = require("https"); class ControlMySpa { constructor(email, password, celsius = true) { this.celsius = celsius; this.email = email; this.password = password; // Access token data this.tokenData = null; // WhoAMI this.userInfo = null; this.currentSpa = null; // Urls this.tokenEndpoint = null; this.refreshEndpoint = null; this.whoami = null; // client info this.mobileClientId = null; this.mobileClientSecret = null; this.waitForResult = false; this.scheduleFilterIntervalEnum = null; this.createFilterScheduleIntervals(); this.instance = axios.create({ httpsAgent: new https.Agent({ rejectUnauthorized: false, }), }); } isLoggedIn() { return !!this.tokenData.access_token && this.tokenData.timestamp + this.tokenData.expires_in * 1000 > Date.now() ? true : false; } async init() { return ( (await this.idm()) && (await this.login()) && (await this.getWhoAmI()) && (await this.getSpa()) ); } async idm() { try { const req = await this.instance.get( "https://iot.controlmyspa.com/idm/tokenEndpoint" ); if (req.status === 200) { const body = req.data; this.mobileClientId = body.mobileClientId; this.mobileClientSecret = body.mobileClientSecret; this.tokenEndpoint = body._links.tokenEndpoint.href; this.refreshEndpoint = body._links.refreshEndpoint.href; this.whoami = body._links.whoami.href; } else { // error getting idm console.error("Error getting IDM info"); } return req.status === 200; } catch (error) { console.error(error); } } async login() { try { const form = { password: this.password, username: this.email, }; const formData = querystring.stringify(form); const contentLength = formData.length; const req = await this.instance.post(this.tokenEndpoint, formData, { headers: { "Content-Length": contentLength, "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${Buffer.from( this.mobileClientId + ":" + this.mobileClientSecret ).toString("base64")}`, }, }); const requestValid = req.status >= 200 && req.status < 400; if (requestValid) { const body = req.data; this.tokenData = { ...body, expires_in: 900, timestamp: Date.now() }; console.log("login success"); } else { // error getting idm console.error("failed to login"); } return requestValid; } catch (error) { console.error(error); return false; } } async getWhoAmI() { try { const req = await this.instance.get(this.whoami, { headers: { Authorization: "Bearer " + this.tokenData.access_token, }, }); if (req.status === 200) { const body = req.data; this.userInfo = body; this.spaId = this.userInfo.spaId; return this.userInfo; } else { // error getting idm console.error("failed to get WhoAmI"); } return false; } catch (error) { console.error(error); } } async getSpa() { try { if (!this.isLoggedIn()) { await this.login(); } const req = await this.instance.get(`https://iot.controlmyspa.com/spas`, { headers: { Authorization: "Bearer " + this.tokenData.access_token, }, params: { page: 0, pageSize: 20, }, }); const requestValid = req.status >= 200 && req.status < 400; if (requestValid) { const body = req.data; const spa = body._embedded && body._embedded.spas && body._embedded.spas.length > 0 ? body._embedded.spas[0] : {}; this.currentSpa = spa; return this.currentSpa; } else { // error getting idm console.error("failed to get spa data"); } return false; } catch (error) { console.error(error); } } /** * @description Call the CMS API to set time and 24h clock format * * @param {String} date in mm/dd/yyyy format * @param {String} time in HH:mm format * @param {Boolean} military_format true for 24h, false for 12h clock */ async setTime(date, time, military_format = true) { // Escape slashes in date before sending to api try { if (!this.isLoggedIn()) { await this.login(); } const timeData = { date: date.replace(/\//g, "\\/"), time: time, military_format: military_format ? "TRUE" : "FALSE", }; const req = await this.instance.post( "https://iot.controlmyspa.com/mobile/control/" + this.currentSpa._id + "/setTime", timeData, { headers: { Accept: "application/json", "User-Agent": "ControlMySpa/3.0.2 (com.controlmyspa.qa; build:1; iOS 14.2.0) Alamofire/5.2.2", "Accept-Encoding": "br;q=1.0, gzip;q=0.9, deflate;q=0.8", "Accept-Language": "cs-CZ;q=1.0", "Content-Length": JSON.stringify(timeData).length, "Content-Type": "application/json", Authorization: "Bearer " + this.tokenData.access_token, }, } ); const requestValid = req.status >= 200 && req.status < 400; if (requestValid) { const body = req.data; } else { // error getting idm console.error("failed to set time"); } return requestValid; } catch (error) { console.error(error); } } async setTemp(temp) { try { if (!this.isLoggedIn()) { await this.login(); } const tempData = { desiredTemp: temp.toFixed(1), }; const req = await this.instance.post( "https://iot.controlmyspa.com/mobile/control/" + this.currentSpa._id + "/setDesiredTemp", tempData, { headers: { Accept: "application/json", "User-Agent": "ControlMySpa/3.0.2 (com.controlmyspa.qa; build:1; iOS 14.2.0) Alamofire/5.2.2", "Accept-Encoding": "br;q=1.0, gzip;q=0.9, deflate;q=0.8", "Accept-Language": "cs-CZ;q=1.0", "Content-Length": JSON.stringify(tempData).length, "Content-Type": "application/json", Authorization: "Bearer " + this.tokenData.access_token, }, } ); const requestValid = req.status >= 200 && req.status < 400; if (requestValid) { const body = req.data; } else { // error getting idm console.error("failed to set temp"); } return requestValid; } catch (error) { console.error(error); } } async setTempRangeHigh() { return await this.setTempRange(true); } async setTempRangeLow() { return await this.setTempRange(false); } sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async setTempRange(high) { try { if (!this.isLoggedIn()) { await this.login(); } const tempData = { desiredState: high ? "HIGH" : "LOW", }; const req = await this.instance.post( "https://iot.controlmyspa.com/mobile/control/" + this.currentSpa._id + "/setTempRange", tempData, { headers: { Accept: "application/json", "User-Agent": "ControlMySpa/3.0.2 (com.controlmyspa.qa; build:1; iOS 14.2.0) Alamofire/5.2.2", "Accept-Encoding": "br;q=1.0, gzip;q=0.9, deflate;q=0.8", "Accept-Language": "cs-CZ;q=1.0", "Content-Length": JSON.stringify(tempData).length, "Content-Type": "application/json", Authorization: "Bearer " + this.tokenData.access_token, }, } ); const requestValid = req.status >= 200 && req.status < 400; if (requestValid) { if (this.waitForResult) { await this.sleep(5000); const newSpaData = await this.getSpa(); return newSpaData; } return true; } else { // error getting idm console.error("failed to set temp range"); } return false; } catch (error) { console.error(error); } } async lockPanel() { return await this.setPanelLock(true); } async unlockPanel() { return await this.setPanelLock(false); } async setPanelLock(locked) { try { if (!this.isLoggedIn()) { await this.login(); } const panelData = { desiredState: locked ? "LOCK_PANEL" : "UNLOCK_PANEL", }; const req = await this.instance.post( "https://iot.controlmyspa.com/mobile/control/" + this.currentSpa._id + "/setPanel", panelData, { headers: { Accept: "application/json", "User-Agent": "ControlMySpa/3.0.2 (com.controlmyspa.qa; build:1; iOS 14.2.0) Alamofire/5.2.2", "Accept-Encoding": "br;q=1.0, gzip;q=0.9, deflate;q=0.8", "Accept-Language": "cs-CZ;q=1.0", "Content-Length": JSON.stringify(panelData).length, "Content-Type": "application/json", Authorization: "Bearer " + this.tokenData.access_token, }, } ); const requestValid = req.status >= 200 && req.status < 400; if (requestValid) { const oldState = this.currentSpa.currentState.panelLock; if (this.waitForResult) { await this.sleep(5000); const newSpaData = await this.getSpa(); console.log( (oldState ? "LOCKED" : "UNLOCKED") + " => " + (newSpaData.currentState.panelLock ? "LOCKED" : "UNLOCKED") ); return newSpaData; } return true; } else { // error getting idm console.error("failed to set panel lock"); } return false; } catch (error) { console.error(error); } } async setJetState(deviceNumber, desiredState) { try { if (!this.isLoggedIn()) { await this.login(); } // numbers 0,1,2 || states: HIGH , OFF if (desiredState !== "OFF" && desiredState !== "HIGH") { console.error("Invalid value for desired state"); return false; } console.log(deviceNumber, desiredState); const jetState = { deviceNumber: deviceNumber.toString(), desiredState: desiredState, originatorId: "optional-Jet", }; const req = await this.instance.post( "https://iot.controlmyspa.com/mobile/control/" + this.currentSpa._id + "/setJetState", jetState, { headers: { Accept: "application/json", "User-Agent": "ControlMySpa/3.0.2 (com.controlmyspa.qa; build:1; iOS 14.2.0) Alamofire/5.2.2", "Accept-Encoding": "br;q=1.0, gzip;q=0.9, deflate;q=0.8", "Accept-Language": "cs-CZ;q=1.0", "Content-Length": JSON.stringify(jetState).length, "Content-Type": "application/json", Authorization: "Bearer " + this.tokenData.access_token, }, } ); const requestValid = req.status >= 200 && req.status < 400; if (requestValid) { const oldState = this.currentSpa.currentState.components.find( (el, id) => { return ( el.componentType === "PUMP" && el.port === deviceNumber.toString() ); } ); if (this.waitForResult) { await this.sleep(5000); const newSpaData = await this.getSpa(); const newState = newSpaData.currentState.components.find((el, id) => { return ( el.componentType === "PUMP" && el.port === deviceNumber.toString() ); }); console.log(oldState.value + " => " + newState.value); return newSpaData; } return true; } else { // error getting idm console.error("failed to set jet state"); } return false; } catch (error) { console.error(error); } } async setBlowerState(deviceNumber, desiredState) { try { if (!this.isLoggedIn()) { await this.login(); } // numbers 0,1,2 || states: HIGH , OFF if (desiredState !== "OFF" && desiredState !== "HIGH") { console.error("Invalid value for desired state"); return false; } console.log(deviceNumber, desiredState); const blowerState = { deviceNumber: deviceNumber.toString(), desiredState: desiredState, originatorId: "optional-Blower", }; const req = await this.instance.post( "https://iot.controlmyspa.com/mobile/control/" + this.currentSpa._id + "/setBlowerState", blowerState, { headers: { Accept: "application/json", "User-Agent": "ControlMySpa/3.0.2 (com.controlmyspa.qa; build:1; iOS 14.2.0) Alamofire/5.2.2", "Accept-Encoding": "br;q=1.0, gzip;q=0.9, deflate;q=0.8", "Accept-Language": "cs-CZ;q=1.0", "Content-Length": JSON.stringify(blowerState).length, "Content-Type": "application/json", Authorization: "Bearer " + this.tokenData.access_token, }, } ); const requestValid = req.status >= 200 && req.status < 400; if (requestValid) { const oldState = this.currentSpa.currentState.components.find( (el, id) => { return ( el.componentType === "BLOWER" && el.port === deviceNumber.toString() ); } ); if (this.waitForResult) { await this.sleep(5000); const newSpaData = await this.getSpa(); const newState = newSpaData.currentState.components.find((el, id) => { return ( el.componentType === "BLOWER" && el.port === deviceNumber.toString() ); }); console.log(oldState.value + " => " + newState.value); return newSpaData; } return true; } else { // error getting idm console.error("failed to set blower state"); } return false; } catch (error) { console.error(error); } } async setLightState(deviceNumber, desiredState) { try { if (!this.isLoggedIn()) { await this.login(); } // numbers 0,1,2 || states: HIGH , OFF if (desiredState !== "OFF" && desiredState !== "HIGH") { console.error("Invalid value for desired state"); return false; } const lightState = { deviceNumber: deviceNumber.toString(), desiredState: desiredState, originatorId: "optional-Light", }; const req = await this.instance.post( "https://iot.controlmyspa.com/mobile/control/" + this.currentSpa._id + "/setLightState", lightState, { headers: { Accept: "application/json", "User-Agent": "ControlMySpa/3.0.2 (com.controlmyspa.qa; build:1; iOS 14.2.0) Alamofire/5.2.2", "Accept-Encoding": "br;q=1.0, gzip;q=0.9, deflate;q=0.8", "Accept-Language": "cs-CZ;q=1.0", "Content-Length": JSON.stringify(lightState).length, "Content-Type": "application/json", Authorization: "Bearer " + this.tokenData.access_token, }, } ); const requestValid = req.status >= 200 && req.status < 400; if (requestValid) { const oldState = this.currentSpa.currentState.components.find( (el, id) => { return ( el.componentType === "LIGHT" && el.port === deviceNumber.toString() ); } ); if (this.waitForResult) { await this.sleep(5000); const newSpaData = await this.getSpa(); const newState = newSpaData.currentState.components.find((el, id) => { return ( el.componentType === "LIGHT" && el.port === deviceNumber.toString() ); }); console.log(oldState.value + " => " + newState.value); return newSpaData; } return true; } else { // error getting idm console.error("failed to set light state"); } return false; } catch (error) { console.error(error); } } async toggleHeaterMode() { try { if (!this.isLoggedIn()) { await this.login(); } const toggle = { originatorId: "", }; const req = await this.instance.post( "https://iot.controlmyspa.com/mobile/control/" + this.currentSpa._id + "/toggleHeaterMode", toggle, { headers: { Accept: "application/json", "User-Agent": "ControlMySpa/3.0.2 (com.controlmyspa.qa; build:1; iOS 14.2.0) Alamofire/5.2.2", "Accept-Encoding": "br;q=1.0, gzip;q=0.9, deflate;q=0.8", "Accept-Language": "cs-CZ;q=1.0", "Content-Length": JSON.stringify(toggle).length, "Content-Type": "application/json", Authorization: "Bearer " + this.tokenData.access_token, }, } ); const requestValid = req.status >= 200 && req.status < 400; if (requestValid) { const oldState = this.currentSpa.currentState.heaterMode; if (this.waitForResult) { await this.sleep(5000); const newSpaData = await this.getSpa(); const newState = newSpaData.currentState.heaterMode; console.log(oldState + " => " + newState); return newSpaData; } return true; } else { // error getting idm console.error("failed to set light state"); } return false; } catch (error) { console.error(error); } } async setFilterCycleIntervalSchedule( scheduleNumber, filterInterval, startTime ) { try { if (!this.isLoggedIn()) { await this.login(); } // scheduleNumber 0,1 || filterInterval: this.scheduleFilterIntervalEnum || time: 24 hour format eg 20:00 const schedule = { deviceNumber: scheduleNumber.toString(), // 0 - first always enabled , 1 - can be disabled by setting interval number to 0 originatorId: "optional-filtercycle", intervalNumber: filterInterval, time: startTime, }; const req = await this.instance.post( "https://iot.controlmyspa.com/mobile/control/" + this.currentSpa._id + "/setFilterCycleIntervalsSchedule", schedule, { headers: { Accept: "application/json", "User-Agent": "ControlMySpa/3.0.2 (com.controlmyspa.qa; build:1; iOS 14.2.0) Alamofire/5.2.2", "Accept-Encoding": "br;q=1.0, gzip;q=0.9, deflate;q=0.8", "Accept-Language": "en-US;q=1.0", "Content-Length": JSON.stringify(schedule).length, "Content-Type": "application/json", Authorization: "Bearer " + this.tokenData.access_token, }, } ); const requestValid = req.status >= 200 && req.status < 400; if (requestValid) { const oldState = this.currentSpa.currentState.components.find( (el, id) => { return ( el.componentType === "FILTER" && el.port === scheduleNumber.toString() ); } ); if (this.waitForResult) { await this.sleep(5000); const newSpaData = await this.getSpa(); const newState = newSpaData.currentState.components.find((el, id) => { return ( el.componentType === "FILTER" && el.port === scheduleNumber.toString() ); }); console.log(oldState.value + " => " + newState.value); return newSpaData; } return true; } else { // error getting idm console.error("failed to set filter schedule"); } return false; } catch (error) { console.error(error); } } createFilterScheduleIntervals() { this.scheduleFilterIntervalEnum = Object.freeze({ idisabled: 0, i15minutes: 1, i30minutes: 2, i45minutes: 3, i1hour: 4, i1hour15minutes: 5, i1hour30minutes: 6, i1hour45minutes: 7, i2hours: 8, i2hours15minutes: 9, i2hours30minutes: 10, i2hours45minutes: 11, i3hours: 12, i3hours15minutes: 13, i3hours30minutes: 14, i3hours45minutes: 15, i4hours: 16, i4hours15minutes: 17, i4hours30minutes: 18, i4hours45minutes: 19, i5hours: 20, i5hours15minutes: 21, i5hours30minutes: 22, i5hours45minutes: 23, i6hours: 24, i6hours15minutes: 25, i6hours30minutes: 26, i6hours45minutes: 27, i7hours: 28, i7hours15minutes: 29, i7hours30minutes: 30, i7hours45minutes: 31, i8hours: 32, i8hours15minutes: 33, i8hours30minutes: 34, i8hours45minutes: 35, i9hours: 36, i9hours15minutes: 37, i9hours30minutes: 38, i9hours45minutes: 39, i10hours: 40, i10hours15minutes: 41, i10hours30minutes: 42, i10hours45minutes: 43, i11hours: 44, i11hours15minutes: 45, i11hours30minutes: 46, i11hours45minutes: 47, i12hours: 48, i12hours15minutes: 49, i12hours30minutes: 50, i12hours45minutes: 51, i13hours: 52, i13hours15minutes: 53, i13hours30minutes: 54, i13hours45minutes: 55, i14hours: 56, i14hours15minutes: 57, i14hours30minutes: 58, i14hours45minutes: 59, i15hours: 60, i15hours15minutes: 61, i15hours30minutes: 62, i15hours45minutes: 63, i16hours: 64, i16hours15minutes: 65, i16hours30minutes: 66, i16hours45minutes: 67, i17hours: 68, i17hours15minutes: 69, i17hours30minutes: 70, i17hours45minutes: 71, i18hours: 72, i18hours15minutes: 73, i18hours30minutes: 74, i18hours45minutes: 75, i19hours: 76, i19hours15minutes: 77, i19hours30minutes: 78, i19hours45minutes: 79, i20hours: 80, i20hours15minutes: 81, i20hours30minutes: 82, i20hours45minutes: 83, i21hours: 84, i21hours15minutes: 85, i21hours30minutes: 86, i21hours45minutes: 87, i22hours: 88, i22hours15minutes: 89, i22hours30minutes: 90, i22hours45minutes: 91, i23hours: 92, i23hours15minutes: 93, i23hours30minutes: 94, i23hours45minutes: 95, i24hours: 96, }); } } module.exports = ControlMySpa;