frontend/smart-hut/src/remote.js

976 lines
33 KiB
JavaScript
Raw Normal View History

2020-05-12 13:18:33 +00:00
import axios from 'axios';
import { connect, disconnect } from '@giantmachines/redux-websocket';
import smartHutStore from './store';
import actions from './storeActions';
import { endpointURL, socketURL } from './endpoint';
2020-04-12 15:46:16 +00:00
/**
* An object returned by promise rejections in remoteservice
* @typedef {Error} RemoteError
* @property {String[]} messages a list of user-friendly error messages to show;
*/
class RemoteError extends Error {
messages;
2020-04-07 10:46:55 +00:00
constructor(messages) {
2020-05-02 11:58:02 +00:00
super(
messages && Array.isArray(messages)
2020-05-12 13:18:33 +00:00
? messages.join(' - ')
: JSON.stringify(messages, null, 2),
2020-05-02 11:58:02 +00:00
);
this.messages = messages;
2020-04-07 10:46:55 +00:00
}
}
const Endpoint = {
axiosInstance: axios.create({
baseURL: endpointURL(),
validateStatus: (status) => status >= 200 && status < 300,
}),
2020-04-07 10:46:55 +00:00
/**
* Returns token for current session, null if logged out
* @returns {String|null} the token
*/
get token() {
2020-04-07 10:46:55 +00:00
return smartHutStore.getState().login.token;
},
2020-04-07 10:46:55 +00:00
/**
* Performs an authenticated request
* @param {get|post|put|delete} the desired method
* @param {String} route the desired route (e.g. "/rooms/1/devices")
* @param {[String]String} query query ('?') parameters (no params by default)
* @param {any} body the JSON request body
*/
2020-04-16 15:07:56 +00:00
send: (method, route, query = {}, body = null) => {
if (!Endpoint.token) {
2020-05-12 13:18:33 +00:00
throw new Error('No token while performing authenticated request');
2020-04-07 10:46:55 +00:00
}
return Endpoint.axiosInstance(route, {
2020-05-12 13:18:33 +00:00
method,
params: query,
2020-05-12 13:18:33 +00:00
data: ['put', 'post'].indexOf(method) !== -1 ? body : null,
2020-04-16 15:07:56 +00:00
headers: {
Authorization: `Bearer ${Endpoint.token}`,
},
}).then((res) => {
2020-05-12 13:18:33 +00:00
if (!res.data && method !== 'delete') {
console.error('Response body is empty');
return null;
}
2020-05-12 13:18:33 +00:00
return res;
});
},
2020-04-16 15:07:56 +00:00
/**
* Performs a non-authenticated post and put request for registration, reset-password
* @param {post} the desired method
* @param {String} route the desired route (e.g. "/rooms/1/devices")
* @param {[String]String} query query ('?') parameters (no params by default)
* @param {any} body the JSON request body
*/
2020-05-12 13:18:33 +00:00
sendNA: (method, route, query = {}, body = null) => Endpoint.axiosInstance(route, {
method,
2020-04-16 15:07:56 +00:00
params: query,
2020-05-12 13:18:33 +00:00
data: ['put', 'post'].indexOf(method) !== -1 ? body : null,
2020-04-16 15:07:56 +00:00
}).then((res) => {
if (!res.data) {
2020-05-12 13:18:33 +00:00
console.error('Response body is empty');
2020-04-16 15:07:56 +00:00
return null;
}
2020-05-12 13:18:33 +00:00
return res;
}),
2020-04-16 15:07:56 +00:00
/**
* Performs login
* @param {String} usernameOrEmail
* @param {String} password
* @returns {Promise<String, *>} promise that resolves to the token string
* and rejects to the axios error.
*/
2020-05-12 13:18:33 +00:00
login: (usernameOrEmail, password) => Endpoint.axiosInstance
.post('/auth/login', {
usernameOrEmail,
password,
})
.then((res) => {
2020-05-12 13:18:33 +00:00
localStorage.setItem('token', res.data.jwttoken);
localStorage.setItem('exp', new Date().getTime() + 5 * 60 * 60 * 1000);
return res.data.jwttoken;
2020-05-12 13:18:33 +00:00
}),
/**
* Returns an immediately resolved promise for the socket logouts
* @return {Promise<Undefined, _>} An always-resolved promise
*/
logout: () => {
2020-05-12 13:18:33 +00:00
localStorage.removeItem('token');
localStorage.removeItem('exp');
return Promise.resolve(void 0);
},
2020-04-07 10:46:55 +00:00
/**
* Performs an authenticated GET request
* @param {String} route the desired route (e.g. "/rooms/1/devices")
* @param {[String]String} query query ('?') parameters (no params by default)
* @returns {Promise<*, *>} The Axios-generated promise
2020-04-07 10:46:55 +00:00
*/
get(route, query = {}) {
2020-05-12 13:18:33 +00:00
return this.send('get', route, query);
},
2020-04-07 10:46:55 +00:00
/**
* Performs an authenticated POST request
* @param {String} route the desired route (e.g. "/rooms/1/devices")
* @param {[String]String} query query ('?') parameters (no params by default)
* @param {any} body the JSON request body
* @returns {Promise<*, *>} The Axios-generated promise
2020-04-07 10:46:55 +00:00
*/
post(route, query, body) {
2020-05-12 13:18:33 +00:00
return this.send('post', route, query, body);
2020-04-16 15:07:56 +00:00
},
/**
* Performs a non-authenticated POST request
* @param {String} route the desired route (e.g. "/rooms/1/devices")
* @param {[String]String} query query ('?') parameters (no params by default)
* @param {any} body the JSON request body
* @returns {Promise<*, *>} The Axios-generated promise
*/
postNA(route, query, body) {
2020-05-12 13:18:33 +00:00
return this.sendNA('post', route, query, body);
},
2020-04-07 10:46:55 +00:00
/**
* Performs an authenticated PUT request
* @param {String} route the desired route (e.g. "/rooms/1/devices")
* @param {[String]String} query query ('?') parameters (no params by default)
* @param {any} body the JSON request body
* @returns {Promise<*, *>} The Axios-generated promise
2020-04-07 10:46:55 +00:00
*/
put(route, query = {}, body = {}) {
2020-05-12 13:18:33 +00:00
return this.send('put', route, query, body);
},
2020-04-07 10:46:55 +00:00
2020-04-16 15:07:56 +00:00
/**
* Performs a non-authenticated PUT request
* @param {String} route the desired route (e.g. "/rooms/1/devices")
* @param {[String]String} query query ('?') parameters (no params by default)
* @param {any} body the JSON request body
* @returns {Promise<*, *>} The Axios-generated promise
*/
putNA(route, query = {}, body = {}) {
2020-05-12 13:18:33 +00:00
return this.sendNA('put', route, query, body);
2020-04-16 15:07:56 +00:00
},
2020-04-07 10:46:55 +00:00
/**
* Performs an authenticated DELETE request
* @param {get|post|put|delete} the desired method
* @param {String} route the desired route (e.g. "/rooms/1/devices")
* @param {[String]String} query query ('?') parameters (no params by default)
* @returns {Promise<*, *>} The Axios-generated promise
2020-04-07 10:46:55 +00:00
*/
delete(route, query = {}) {
2020-05-12 13:18:33 +00:00
return this.send('delete', route, query);
},
};
2020-04-07 10:46:55 +00:00
/**
* Given an error response, returns an array of user
* friendly messages to display to the user
* @param {*} err the Axios error reponse object
* @returns {RemoteError} user friendly error messages
2020-04-07 10:46:55 +00:00
*/
function parseValidationErrors(err) {
if (
2020-05-12 13:18:33 +00:00
err.response
&& err.response.status === 400
&& err.response.data
&& Array.isArray(err.response.data.errors)
2020-04-07 10:46:55 +00:00
) {
throw new RemoteError([
...new Set(err.response.data.errors.map((e) => e.defaultMessage)),
]);
2020-04-07 10:46:55 +00:00
} else {
2020-05-12 13:18:33 +00:00
console.warn('Non validation error', err);
throw new RemoteError(['Network error']);
2020-04-07 10:46:55 +00:00
}
}
export const RemoteService = {
2020-04-07 10:46:55 +00:00
/**
* Performs login
* @param {String} usernameOrEmail
* @param {String} password
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
2020-04-07 10:46:55 +00:00
*/
2020-05-12 13:18:33 +00:00
login: (usernameOrEmail, password) => (dispatch) => Endpoint.login(usernameOrEmail, password)
2020-04-12 15:46:16 +00:00
.then((token) => {
2020-04-12 15:49:29 +00:00
dispatch(actions.loginSuccess(token));
dispatch(connect(socketURL(token)));
2020-04-12 15:46:16 +00:00
})
2020-04-07 10:46:55 +00:00
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.warn('login error', err);
throw new RemoteError([
err.response && err.response.status === 401
2020-05-12 13:18:33 +00:00
? 'Wrong credentials'
: 'An error occurred while logging in',
]);
2020-05-12 13:18:33 +00:00
}),
2020-04-07 10:46:55 +00:00
/**
* Performs logout
*/
2020-05-12 13:18:33 +00:00
logout: () => (dispatch) => Endpoint.logout().then(() => {
2020-04-12 15:49:29 +00:00
dispatch(disconnect());
dispatch(actions.logout());
2020-05-12 13:18:33 +00:00
}),
2020-04-07 10:46:55 +00:00
2020-05-05 13:53:07 +00:00
/**
* Fetches user information via REST calls, if it is logged in
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
*/
2020-05-12 13:18:33 +00:00
userPermissions: (data) => (dispatch) => Endpoint.put('/user/permissions', {}, data).catch((err) => {
console.warn('Fetch user info error', err);
throw new RemoteError(['Network error']);
}),
2020-05-05 13:53:07 +00:00
2020-04-07 10:46:55 +00:00
/**
* Fetches user information via REST calls, if it is logged in
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
2020-04-07 10:46:55 +00:00
*/
2020-05-12 13:18:33 +00:00
fetchUserInfo: () => (dispatch) => Endpoint.get('/auth/profile')
2020-04-07 10:46:55 +00:00
.then((res) => void dispatch(actions.userInfoUpdate(res.data)))
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.warn('Fetch user info error', err);
throw new RemoteError(['Network error']);
}),
2020-04-07 10:46:55 +00:00
/**
* Fetches all rooms that belong to this user. This call does not
* populate the devices attribute in rooms.
* @param {Number|null} hostId the user id of the host we need to fetch the rooms from.
* Null if we need to fetch our own rooms.
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
2020-04-07 10:46:55 +00:00
*/
2020-05-12 13:18:33 +00:00
fetchAllRooms: (hostId = null) => (dispatch) => Endpoint.get('/room', hostId ? { hostId } : null)
.then(
2020-05-12 13:18:33 +00:00
(res) => void dispatch(
hostId
? actions.hostRoomsUpdate(hostId, res.data)
2020-05-12 13:18:33 +00:00
: actions.roomsUpdate(res.data),
),
)
2020-04-07 10:46:55 +00:00
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.error('Fetch all rooms error', err);
throw new RemoteError(['Network error']);
}),
2020-04-07 10:46:55 +00:00
2020-04-18 14:26:12 +00:00
/**
* Fetches all scenes that belong to this user. This call does not
* populate the devices attribute in scenes.
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
*/
2020-05-12 13:18:33 +00:00
fetchAllScenes: (hostId = null) => (dispatch) => Endpoint.get('/scene', hostId ? { hostId } : {})
2020-05-05 15:04:53 +00:00
.then(
2020-05-12 13:18:33 +00:00
(res) => void dispatch(
2020-05-05 15:04:53 +00:00
!hostId
? actions.scenesUpdate(res.data)
2020-05-12 13:18:33 +00:00
: actions.hostScenesUpdate(hostId, res.data),
),
2020-05-05 15:04:53 +00:00
)
2020-04-18 14:26:12 +00:00
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.error('Fetch all scenes error', err);
throw new RemoteError(['Network error']);
}),
2020-04-18 14:26:12 +00:00
2020-04-07 10:46:55 +00:00
/**
* Fetches all devices in a particular room, or fetches all devices.
* This also updates the devices attribute on values in the map rooms.
2020-04-28 10:04:01 +00:00
* @param {Number|null} roomId the rsoom to which fetch devices
2020-04-07 10:46:55 +00:00
* from, null to fetch from all rooms
* @param {Number|null} hostId the user id of the owner of the devices to get
* (can be used for host view)
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
2020-04-07 10:46:55 +00:00
*/
2020-05-12 13:18:33 +00:00
fetchDevices: (roomId = null, hostId = null) => (dispatch) => Endpoint.get(
roomId ? `/room/${roomId}/device` : '/device',
hostId ? { hostId } : null,
)
.then(
2020-05-12 13:18:33 +00:00
(res) => void dispatch(
!hostId
? actions.devicesUpdate(roomId, res.data, hostId)
2020-05-12 13:18:33 +00:00
: actions.hostDevicesUpdate(hostId, res.data),
),
)
2020-04-07 10:46:55 +00:00
.catch((err) => {
console.error(`Fetch devices roomId=${roomId} error`, err);
2020-05-12 13:18:33 +00:00
throw new RemoteError(['Network error']);
}),
2020-04-07 10:46:55 +00:00
2020-04-28 10:04:01 +00:00
/**
* Fetches all the automations
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
*/
2020-05-12 13:18:33 +00:00
fetchAutomations: () => (dispatch) => Endpoint.get('/automation/')
2020-05-01 18:05:39 +00:00
.then((res) => void dispatch(actions.automationsUpdate(res.data)))
2020-04-28 10:04:01 +00:00
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.error('Fetch automations error', err);
throw new RemoteError(['Network error']);
}),
2020-04-28 10:04:01 +00:00
2020-04-18 14:26:12 +00:00
/**
* Fetches all devices in a particular scene, or fetches all devices.
* This also updates the devices attribute on values in the map scenes.
* @param {Number} sceneId the scene to which fetch devices
* from, null to fetch from all scenes
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
*/
2020-05-12 13:18:33 +00:00
fetchStates: (sceneId) => (dispatch) => Endpoint.get(`/scene/${sceneId}/states`)
2020-04-18 14:26:12 +00:00
.then((res) => void dispatch(actions.statesUpdate(sceneId, res.data)))
.catch((err) => {
console.error(`Fetch devices sceneId=${sceneId} error`, err);
2020-05-12 13:18:33 +00:00
throw new RemoteError(['Network error']);
}),
2020-04-18 14:26:12 +00:00
/**
2020-04-25 15:46:52 +00:00
* Fetches all hosts of a particular user.
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
*/
2020-05-12 13:18:33 +00:00
fetchHosts: () => (dispatch) => Endpoint.get('/user/hosts')
2020-05-02 20:37:20 +00:00
.then((res) => void dispatch(actions.hostsUpdate(res.data)))
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.error('Fetch hosts error', err);
throw new RemoteError(['Network error']);
}),
2020-05-02 20:37:20 +00:00
/**
* Fetches all guests of a particular user.
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
*/
2020-05-12 13:18:33 +00:00
fetchGuests: () => (dispatch) => Endpoint.get('/user/guests')
2020-05-02 20:37:20 +00:00
.then((res) => void dispatch(actions.guestsUpdate(res.data)))
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.error('Fetch guests error', err);
throw new RemoteError(['Network error']);
}),
2020-05-02 20:37:20 +00:00
2020-05-02 14:40:29 +00:00
/**
* Adds the current user as a guest to another user
2020-05-02 20:37:20 +00:00
* identified through a user id.
* @param {Number[]} userId the users to add.
2020-05-02 14:40:29 +00:00
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
*/
2020-05-12 13:18:33 +00:00
updateGuests: (userIds) => (dispatch) => Endpoint.put('/user/guests', {}, { ids: userIds })
2020-05-02 20:37:20 +00:00
.then((res) => void dispatch(actions.guestsUpdate(res.data)))
2020-04-25 15:46:52 +00:00
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.error('Guest save error', err);
throw new RemoteError(['Network Error']);
}),
2020-04-25 15:46:52 +00:00
2020-04-07 10:46:55 +00:00
/**
* Creates/Updates a room with the given data
* @param {String} data.name the room's name,
* @param {String} data.icon the room's icon name in SemanticUI icons
* @param {String} data.image ths room's image, as base64
* @param {Number|null} roomId the room's id if update, null for creation
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
2020-04-07 10:46:55 +00:00
*/
2020-05-12 13:18:33 +00:00
saveRoom: (data, roomId = null) => (dispatch) => {
2020-04-07 10:46:55 +00:00
data = {
name: data.name,
icon: data.icon,
image: data.image,
};
return (roomId
? Endpoint.put(`/room/${roomId}`, {}, data)
2020-05-12 13:18:33 +00:00
: Endpoint.post('/room', {}, data)
2020-04-07 10:46:55 +00:00
)
.then((res) => void dispatch(actions.roomSave(res.data)))
.catch(parseValidationErrors);
2020-05-12 13:18:33 +00:00
},
2020-04-07 10:46:55 +00:00
2020-04-18 14:26:12 +00:00
/**
* Creates/Updates a scene with the given data
* @param {String} data.name the scene's name,
* @param {Number|null} sceneId the scene's id if update, null for creation
2020-05-04 14:09:56 +00:00
* @param {Number|null} copyFrom the id of the scene from which the states must be copied from.
* (ignored for updates)
2020-04-18 14:26:12 +00:00
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
*/
2020-05-12 13:18:33 +00:00
saveScene: (data, sceneId = null, copyFrom = null) => (dispatch) => {
2020-05-04 14:09:56 +00:00
copyFrom = sceneId === null ? copyFrom : null;
2020-04-18 14:26:12 +00:00
data = {
name: data.name,
2020-05-04 13:08:17 +00:00
icon: data.icon,
2020-05-05 13:53:07 +00:00
guestAccessEnabled: sceneId ? data.guestAccessEnabled : false,
2020-04-18 14:26:12 +00:00
};
return (sceneId
? Endpoint.put(`/scene/${sceneId}`, {}, data)
2020-05-12 13:18:33 +00:00
: Endpoint.post('/scene', {}, data)
2020-04-18 14:26:12 +00:00
)
2020-05-04 14:09:56 +00:00
.then(async (res) => {
let states = [];
if (copyFrom) {
const sceneId = res.data.id;
try {
const res = await Endpoint.post(
2020-05-12 13:18:33 +00:00
`/scene/${sceneId}/copyFrom/${copyFrom}`,
2020-05-04 14:09:56 +00:00
);
states = res.data;
} catch (e) {
2020-05-12 13:18:33 +00:00
console.warn(`Error in state cloning from scene ${copyFrom}`, e);
throw new RemoteError(['Network error']);
2020-05-04 14:09:56 +00:00
}
}
dispatch(actions.sceneSave(res.data));
if (states.length > 0) {
dispatch(actions.statesUpdate(sceneId, states));
}
})
2020-04-18 14:26:12 +00:00
.catch(parseValidationErrors);
2020-05-12 13:18:33 +00:00
},
2020-04-18 14:26:12 +00:00
2020-05-12 13:18:33 +00:00
updateState: (data, type) => (dispatch) => {
2020-04-25 13:37:40 +00:00
let url;
2020-05-01 17:55:08 +00:00
if (data.on !== undefined) {
2020-05-12 13:18:33 +00:00
url = '/switchableState';
2020-04-28 08:59:47 +00:00
} else {
2020-05-12 13:18:33 +00:00
url = '/dimmableState';
2020-04-25 13:37:40 +00:00
}
2020-04-25 15:46:04 +00:00
return Endpoint.put(url, {}, data)
.then((res) => {
2020-04-27 12:44:48 +00:00
dispatch(actions.stateSave(res.data));
2020-04-25 15:46:04 +00:00
return res.data;
})
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.warn('Update device: ', data, 'error: ', err);
throw new RemoteError(['Network error']);
2020-04-25 15:46:04 +00:00
});
2020-05-12 13:18:33 +00:00
},
2020-04-25 13:37:40 +00:00
2020-05-12 13:18:33 +00:00
deleteState: (id, type) => (dispatch) => {
2020-04-25 16:44:54 +00:00
let url;
2020-05-12 13:18:33 +00:00
if (type === 'dimmableState') {
url = '/dimmableState';
2020-04-25 16:44:54 +00:00
} else {
2020-05-12 13:18:33 +00:00
url = '/switchableState';
2020-04-25 16:44:54 +00:00
}
2020-05-12 13:18:33 +00:00
return Endpoint.delete(`${url}/${id}`)
2020-04-26 11:38:54 +00:00
.then((_) => dispatch(actions.stateDelete(id)))
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.warn('state delete error', err);
throw new RemoteError(['Network error']);
2020-04-26 11:38:54 +00:00
});
2020-05-12 13:18:33 +00:00
},
2020-04-25 15:52:50 +00:00
2020-05-12 13:18:33 +00:00
sceneApply: (id, hostId = null) => (dispatch) => {
2020-04-26 11:38:54 +00:00
let url = `/scene/${id}/apply`;
2020-05-08 11:45:13 +00:00
if (hostId) {
2020-05-12 13:18:33 +00:00
url = `${url}?hostId=${hostId}`;
2020-05-08 11:45:13 +00:00
}
2020-04-26 09:09:49 +00:00
return Endpoint.post(url)
2020-05-12 13:18:33 +00:00
.then((res) => dispatch(
2020-05-08 12:55:26 +00:00
hostId
? actions.hostDevicesUpdate(hostId, res.data, true)
2020-05-12 13:18:33 +00:00
: actions.deviceOperationUpdate(res.data),
))
2020-04-26 11:38:54 +00:00
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.warn('scene apply error', err);
throw new RemoteError(['Network error']);
2020-04-26 11:38:54 +00:00
});
2020-05-12 13:18:33 +00:00
},
2020-04-26 09:09:49 +00:00
2020-04-07 10:46:55 +00:00
/**
* Creates/Updates a device with the given data. If
* data.id is truthy, then a update call is performed,
* otherwise a create call is performed. The update URL
* is computed based on data.kind when data.flowType =
* 'OUTPUT', otherwise the PUT "/device" endpoint
* is used for updates and the POST "/<device.kind>"
* endpoints are used for creation.
* @param {Device} data the device to update.
2020-04-11 16:29:32 +00:00
* @returns {Promise<Device, RemoteError>} promise that resolves to the saved device and rejects
* with user-fiendly errors as a RemoteError
2020-04-07 10:46:55 +00:00
*/
2020-05-12 13:18:33 +00:00
saveDevice: (data, hostId = null) => (dispatch) => {
let url = '/device';
if ((data.id && data.flowType === 'OUTPUT') || !data.id) {
url = `/${data.kind}`;
2020-04-07 10:46:55 +00:00
}
2020-05-12 13:18:33 +00:00
return Endpoint[data.id ? 'put' : 'post'](
2020-05-04 09:13:19 +00:00
url,
hostId ? { hostId } : {},
2020-05-12 13:18:33 +00:00
data,
2020-05-04 09:13:19 +00:00
)
2020-04-11 16:29:32 +00:00
.then((res) => {
2020-05-04 09:13:19 +00:00
dispatch(
hostId
? actions.hostDeviceSave(hostId, res.data)
2020-05-12 13:18:33 +00:00
: actions.deviceSave(res.data),
2020-05-04 09:13:19 +00:00
);
2020-04-11 16:29:32 +00:00
return res.data;
})
2020-04-07 10:46:55 +00:00
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.warn('Update device: ', data, 'error: ', err);
throw new RemoteError(['Network error']);
});
2020-05-12 13:18:33 +00:00
},
2020-05-27 10:04:16 +00:00
updateSimulation: (data) => (dispatch) => {
const url = `/sensor/${data.id}/simulation`;
2020-05-27 16:46:40 +00:00
const param = {
typical: data.typical,
error: data.err,
};
return Endpoint.put(url, {}, param)
2020-05-27 10:04:16 +00:00
.then((res) => {
dispatch(actions.deviceSave(res.data));
return res.data;
})
.catch((err) => {
console.warn('Update device: ', data, 'error: ', err);
throw new RemoteError(['Network error']);
});
},
2020-05-12 13:18:33 +00:00
fastUpdateAutomation: (automation) => (dispatch) => Endpoint.put('/automation/fast', {}, automation)
2020-05-01 18:05:39 +00:00
.then((res) => dispatch(actions.automationSave(res.data)))
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.warn('Update automation: ', automation, 'error: ', err);
throw new RemoteError(['Network error']);
}),
2020-05-01 18:05:39 +00:00
2020-04-28 10:04:01 +00:00
/**
* Creates/Updates an automation with the given data. If
* data.id is truthy, then a update call is performed,
* otherwise a create call is performed.
2020-05-01 18:05:39 +00:00
* @param {Automation} data the automation to update.
2020-04-28 10:04:01 +00:00
* @returns {Promise<Device, RemoteError>} promise that resolves to the saved device and rejects
* with user-fiendly errors as a RemoteError
*/
saveAutomation: (data) => {
const {
automation, triggerList, order, conditionList,
} = data;
2020-04-28 10:04:01 +00:00
automation.triggers = [];
automation.scenes = [];
automation.condition = [];
2020-04-28 10:04:01 +00:00
return (dispatch) => {
2020-05-12 13:18:33 +00:00
const urlAutomation = '/automation';
const urlBooleanTrigger = '/booleanTrigger';
const urlRangeTrigger = '/rangeTrigger';
const urlScenePriority = '/scenePriority';
// conditions
const urlRangeCondition = '/rangeCondition';
const urlBooleanCondition = '/booleanCondition';
const urlThermostatCondition = '/thermostatCondition';
2020-05-12 13:18:33 +00:00
2020-05-12 14:09:59 +00:00
const rangeTriggerList = triggerList.filter((trigger) => 'operand' in trigger);
2020-05-12 13:18:33 +00:00
const booleanTriggerList = triggerList.filter(
2020-05-12 14:09:59 +00:00
(trigger) => !('operand' in trigger),
2020-04-28 10:04:01 +00:00
);
const rangeConditionList = conditionList.filter((condition) => 'operand' in condition && 'value' in condition);
const booleanConditionList = conditionList.filter((condition) => 'on' in condition);
const thermostatConditionList = conditionList.filter((condition) => 'operand' in condition && 'mode' in condition);
2020-04-28 10:04:01 +00:00
2020-05-12 13:18:33 +00:00
return Endpoint.post(urlAutomation, {}, automation).then(
2020-04-28 10:04:01 +00:00
async (automationRes) => {
const { id } = automationRes.data;
// Introduce the range triggers in the automation
2020-05-12 14:09:59 +00:00
const resRangeTriggers = [];
2020-05-12 13:18:33 +00:00
for (const t of rangeTriggerList) {
2020-04-28 10:04:01 +00:00
const trigger = {
automationId: id,
deviceId: t.device,
operator: t.operand,
range: t.value,
};
2020-05-12 14:09:59 +00:00
resRangeTriggers.push(Endpoint.post(urlRangeTrigger, {}, trigger));
}
automation.triggers = (await Promise.all(resRangeTriggers)).map((v) => v.data);
2020-05-12 14:23:27 +00:00
2020-05-12 14:09:59 +00:00
const resBoolTriggers = [];
2020-05-12 13:18:33 +00:00
for (const t of booleanTriggerList) {
2020-04-28 10:04:01 +00:00
const trigger = {
automationId: id,
deviceId: t.device,
on: t.on,
};
2020-05-12 14:09:59 +00:00
resBoolTriggers.push(Endpoint.post(
2020-04-28 10:04:01 +00:00
urlBooleanTrigger,
{},
2020-05-12 13:18:33 +00:00
trigger,
2020-05-12 14:09:59 +00:00
));
2020-04-28 10:04:01 +00:00
}
2020-05-12 14:09:59 +00:00
automation.triggers.push(...((await Promise.all(resBoolTriggers)).map((v) => v.data)));
2020-04-28 10:04:01 +00:00
// Conditions
const resRangeConditions = [];
for (const t of rangeConditionList) {
const condition = {
automationId: id,
deviceId: t.device,
operator: t.operand,
range: t.value,
};
resRangeConditions.push(Endpoint.post(urlRangeCondition, {}, condition));
}
automation.conditions = (await Promise.all(resRangeConditions)).map((v) => v.data);
const resBoolConditions = [];
for (const t of booleanConditionList) {
const condition = {
automationId: id,
deviceId: t.device,
on: t.on,
};
resBoolConditions.push(Endpoint.post(
urlBooleanCondition,
{},
condition,
));
}
automation.conditions.push(...((await Promise.all(resBoolConditions)).map((v) => v.data)));
const resThermoConditions = [];
for (const t of thermostatConditionList) {
const condition = {
automationId: id,
deviceId: t.device,
mode: t.mode,
operator: t.operand,
};
resThermoConditions.push(Endpoint.post(
urlThermostatCondition,
{},
condition,
));
}
automation.conditions.push(...((await Promise.all(resThermoConditions)).map((v) => v.data)));
2020-05-12 14:09:59 +00:00
const resScenePriorities = [];
2020-05-12 13:18:33 +00:00
for (const [priority, sceneId] of order.entries()) {
2020-04-28 10:04:01 +00:00
const scenePriority = {
automationId: id,
priority,
sceneId,
};
2020-05-12 14:09:59 +00:00
resScenePriorities.push(Endpoint.post(
2020-04-28 10:04:01 +00:00
urlScenePriority,
{},
2020-05-12 13:18:33 +00:00
scenePriority,
2020-05-12 14:09:59 +00:00
));
2020-04-28 10:04:01 +00:00
}
2020-05-12 14:09:59 +00:00
automation.scenes = (await Promise.all(resScenePriorities)).map((v) => v.data);
2020-04-28 10:04:01 +00:00
automation.id = id;
dispatch(actions.automationSave(automation));
2020-05-12 13:18:33 +00:00
},
2020-04-28 10:04:01 +00:00
);
};
},
/**
* Creates/Updates a state with the given data. If
* data.id is truthy, then a update call is performed,
* otherwise a create call is performed. The update URL
* is computed based on data.kind when data.flowType =
* 'OUTPUT', otherwise the PUT "/device" endpoint
* is used for updates and the POST "/<device.kind>"
* endpoints are used for creation.
* @param {State} data the device to update.
* @returns {Promise<Device, RemoteError>} promise that resolves to the saved device and rejects
* with user-fiendly errors as a RemoteError
*/
2020-05-12 13:18:33 +00:00
saveState: (data) => (dispatch) => {
const url = `/${data.kind}/${data.id}/state?sceneId=${data.sceneId}`;
2020-05-12 13:18:33 +00:00
return Endpoint.post(url, {}, data)
.then((res) => {
dispatch(actions.stateSave(res.data));
return res.data;
})
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.warn('Update device: ', data, 'error: ', err);
throw new RemoteError(['Network error']);
2020-04-07 10:46:55 +00:00
});
2020-05-12 13:18:33 +00:00
},
2020-04-07 10:46:55 +00:00
2020-04-11 16:29:32 +00:00
/**
* Connetcs a series of output devices to an input device.
* Output devices for Switch input can be: Normal Light, Dimmable Light, Smart Plug.
* Output devices for Dimmers input can be: Dimmable Light.
*
* @typedef {"switch" | "buttonDimmer" | "knobDimmer"} ConnectableInput
*
* @param {ConnectableInput} newDevice.kind kind of the input device
* @param {Integer} newDevice.id id of the input device
* @param {Integer[]} outputs ids of the output device
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
*/
2020-05-12 13:18:33 +00:00
connectOutputs: (newDevice, outputs) => (dispatch) => {
const url = `/${newDevice.kind}/${newDevice.id}/lights`;
2020-04-11 16:29:32 +00:00
return Endpoint.post(url, {}, outputs)
.then((res) => {
dispatch(actions.deviceOperationUpdate(res.data));
return res.data;
})
.catch((err) => {
console.warn(
2020-05-12 13:18:33 +00:00
'ConnectOutputs of ',
2020-04-11 16:29:32 +00:00
newDevice.id,
2020-05-12 13:18:33 +00:00
' with outputs: ',
2020-04-11 16:29:32 +00:00
outputs,
2020-05-12 13:18:33 +00:00
'error: ',
err,
2020-04-11 16:29:32 +00:00
);
2020-05-12 13:18:33 +00:00
throw new RemoteError(['Network error']);
2020-04-11 16:29:32 +00:00
});
2020-05-12 13:18:33 +00:00
},
2020-04-11 16:29:32 +00:00
2020-05-12 13:18:33 +00:00
_operateInput: (url, getUrl, action) => (dispatch) => Endpoint.put(url, {}, action)
2020-04-07 10:46:55 +00:00
.then(async (res) => {
const inputDevice = await Endpoint.get(getUrl);
2020-04-07 10:46:55 +00:00
delete inputDevice.outputs;
2020-04-11 16:29:32 +00:00
dispatch(
2020-05-12 13:18:33 +00:00
actions.deviceOperationUpdate([...res.data, inputDevice.data]),
2020-04-11 16:29:32 +00:00
);
2020-04-07 10:46:55 +00:00
})
.catch((err) => {
console.warn(`${url} error`, err);
2020-05-12 13:18:33 +00:00
throw new RemoteError(['Network error']);
}),
2020-04-07 10:46:55 +00:00
/**
* Changes the state of a switch, by turning it on, off or toggling it.
*
* @typedef {"ON" | "OFF" | "TOGGLE"} SwitchOperation
*
* @param {Number} switchId the switch device id
* @param {SwitchOperation} type the operation to perform on the switch
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
2020-04-07 10:46:55 +00:00
*/
2020-05-12 13:18:33 +00:00
switchOperate: (switchId, type) => RemoteService._operateInput(
'/switch/operate',
2020-04-11 16:29:32 +00:00
`/switch/${switchId}`,
{
type: type.toUpperCase(),
id: switchId,
2020-05-12 13:18:33 +00:00
},
),
2020-04-07 10:46:55 +00:00
/**
* Turns a knob dimmer to a specific amount
*
* @param {Number} dimmerId the knob dimmer id
* @param {number} intensity the absolute intensity to dim to. Must be >=0 and <= 100.
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
2020-04-07 10:46:55 +00:00
*/
2020-05-12 13:18:33 +00:00
knobDimmerDimTo: (dimmerId, intensity) => RemoteService._operateInput(
'/knobDimmer/dimTo',
2020-04-11 16:29:32 +00:00
`/knobDimmer/${dimmerId}`,
{
intensity,
id: dimmerId,
2020-05-12 13:18:33 +00:00
},
),
2020-04-07 10:46:55 +00:00
/**
* Turns a button dimmer up or down
*
* @typedef {"UP" | "DOWN"} ButtonDimmerDimType
*
* @param {Number} dimmerId the button dimmer id
* @param {ButtonDimmerDimType} dimType the type of dim to perform
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
2020-04-07 10:46:55 +00:00
*/
2020-05-12 13:18:33 +00:00
buttonDimmerDim: (dimmerId, dimType) => RemoteService._operateInput(
'/buttonDimmer/dim',
2020-04-07 10:46:55 +00:00
`/buttonDimmer/${dimmerId}`,
{
dimType,
id: dimmerId,
2020-05-12 13:18:33 +00:00
},
),
2020-04-07 10:46:55 +00:00
/**
* Resets the meter on a smart plug
*
* @param {Number} smartPlugId the smart plug to reset
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
2020-04-07 10:46:55 +00:00
*/
smartPlugReset(smartPlugId) {
2020-05-12 13:18:33 +00:00
return (dispatch) => Endpoint.delete(`/smartPlug/${smartPlugId}/meter`)
2020-04-07 10:46:55 +00:00
.then((res) => dispatch(actions.deviceOperationUpdate([res.data])))
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.warn('Smartplug reset error', err);
throw new RemoteError(['Network error']);
2020-04-07 10:46:55 +00:00
});
},
2020-04-07 10:46:55 +00:00
/**
* Deletes a room
* @param {Number} roomId the id of the room to delete
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
2020-04-07 10:46:55 +00:00
*/
2020-05-12 13:18:33 +00:00
deleteRoom: (roomId) => (dispatch) => Endpoint.delete(`/room/${roomId}`)
2020-04-07 10:46:55 +00:00
.then((_) => dispatch(actions.roomDelete(roomId)))
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.warn('Room deletion error', err);
throw new RemoteError(['Network error']);
}),
2020-04-07 10:46:55 +00:00
2020-04-28 10:04:01 +00:00
deleteAutomation: (id) => {
2020-05-12 13:18:33 +00:00
console.log('ID OF AUTO ', id);
return (dispatch) => Endpoint.delete(`/automation/${id}`)
2020-04-28 10:04:01 +00:00
.then((_) => dispatch(actions.automationDelete(id)))
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.warn('Automation deletion error', err);
throw new RemoteError(['Network error']);
2020-04-28 10:04:01 +00:00
});
},
2020-04-18 14:26:12 +00:00
/**
* Deletes a scene
* @param {Number} sceneId the id of the scene to delete
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
*/
2020-05-12 13:18:33 +00:00
deleteScene: (sceneId) => (dispatch) => Endpoint.delete(`/scene/${sceneId}`)
2020-04-18 14:26:12 +00:00
.then((_) => dispatch(actions.sceneDelete(sceneId)))
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.warn('Scene deletion error', err);
throw new RemoteError(['Network error']);
}),
2020-04-18 14:26:12 +00:00
2020-04-07 10:46:55 +00:00
/**
* Deletes a device
2020-04-10 15:25:52 +00:00
* @param {Device} device the device to delete
* @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a RemoteError
2020-04-07 10:46:55 +00:00
*/
2020-05-12 13:18:33 +00:00
deleteDevice: (device) => (dispatch) => Endpoint.delete(`/${device.kind}/${device.id}`)
2020-04-10 15:25:52 +00:00
.then((_) => dispatch(actions.deviceDelete(device.id)))
2020-04-07 10:46:55 +00:00
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.warn('Device deletion error', err);
throw new RemoteError(['Network error']);
}),
};
for (const key in RemoteService) {
RemoteService[key] = RemoteService[key].bind(RemoteService);
2020-04-07 10:46:55 +00:00
}
export class Forms {
2020-05-02 20:37:20 +00:00
static fetchAllUsers() {
2020-05-12 13:18:33 +00:00
return Endpoint.get('/user')
2020-05-02 20:37:20 +00:00
.then((res) => res.data)
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.error('Fetch users error', err);
throw new RemoteError(['Network error']);
2020-05-02 20:37:20 +00:00
});
}
2020-04-07 10:46:55 +00:00
/**
* Attempts to create a new user from the given data.
* This method does not update the global state,
* please check its return value.
* @param {String} data.username the chosen username
* @param {String} data.password the chosen password
* @param {String} data.email the chosen email
* @param {String} data.name the chosen full name
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
* with validation errors as a String array
*/
static submitRegistration(data) {
2020-04-16 15:07:56 +00:00
return Endpoint.postNA(
2020-05-12 13:18:33 +00:00
'/register',
2020-04-07 10:46:55 +00:00
{},
{
username: data.username,
password: data.password,
name: data.name,
email: data.email,
2020-05-12 13:18:33 +00:00
},
2020-04-07 10:46:55 +00:00
)
.then((_) => void 0)
.catch(parseValidationErrors);
}
/**
* Sends a request to perform a password reset.
* This method does not update the global state,
* please check its return value.
* @param {String} email the email to which perform the reset
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
* with validation errors as a String array
*/
static submitInitResetPassword(email) {
2020-04-16 15:07:56 +00:00
return Endpoint.postNA(
2020-05-12 13:18:33 +00:00
'/register/init-reset-password',
2020-04-07 10:46:55 +00:00
{},
{
2020-05-12 13:18:33 +00:00
email,
},
2020-04-07 10:46:55 +00:00
)
.then((_) => void 0)
.catch((err) => {
2020-05-12 13:18:33 +00:00
console.warn('Init reset password failed', err);
throw new RemoteError(['Network error']);
2020-04-07 10:46:55 +00:00
});
}
/**
* Sends the password for the actual password reset, haviug already
* performed email verification
* This method does not update the global state,
* please check its return value.
* @param {String} confirmationToken the confirmation token got from the email
2020-04-07 10:46:55 +00:00
* @param {String} password the new password
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects
* with validation errors as a String array
*/
static submitResetPassword(confirmationToken, password) {
2020-04-16 15:07:56 +00:00
return Endpoint.putNA(
2020-05-12 13:18:33 +00:00
'/register/reset-password',
2020-04-07 10:46:55 +00:00
{},
{
confirmationToken,
2020-04-07 10:46:55 +00:00
password,
2020-05-12 13:18:33 +00:00
},
2020-04-07 10:46:55 +00:00
)
.then((_) => void 0)
.catch(parseValidationErrors);
}
}