frontend/smart-hut/src/store.js

642 lines
18 KiB
JavaScript

import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import update from 'immutability-helper';
import reduxWebSocket, { connect } from '@giantmachines/redux-websocket';
import { socketURL } from './endpoint';
function reducer(previousState, action) {
let newState; let
change;
const createOrUpdateRoom = (room) => {
if (!newState.rooms[room.id]) {
newState = update(newState, {
rooms: { [room.id]: { $set: { ...room, devices: new Set() } } },
});
} else {
newState = update(newState, {
rooms: {
[room.id]: {
name: { $set: room.name },
image: { $set: room.image },
icon: { $set: room.icon },
},
},
});
}
if (newState.pendingJoins.rooms[room.id]) {
newState = update(newState, {
pendingJoins: { rooms: { $unset: [room.id] } },
rooms: {
[room.id]: {
devices: {
$add: [...newState.pendingJoins.rooms[room.id]],
},
},
},
});
}
};
const createOrUpdateScene = (scene) => {
if (!newState.scenes[scene.id]) {
newState = update(newState, {
scenes: { [scene.id]: { $set: { ...scene, sceneStates: new Set() } } },
});
} else {
newState = update(newState, {
scenes: {
[scene.id]: {
name: { $set: scene.name },
icon: { $set: scene.icon },
guestAccessEnabled: { $set: scene.guestAccessEnabled },
},
},
});
}
if (newState.pendingJoins.scenes[scene.id]) {
newState = update(newState, {
pendingJoins: { scenes: { $unset: [scene.id] } },
scenes: {
[scene.id]: {
sceneStates: {
$add: [...newState.pendingJoins.scenes[scene.id]],
},
},
},
});
}
};
const updateDeviceProps = (device) => {
// In some updates the information regarding a device is incomplete
// due to a fault in the type system and JPA repository management
// in the backend. Therefore to solve this avoid to delete existing
// attributes of this device in the previous state, but just update
// the new ones.
change.devices[device.id] = {};
for (const key in device) {
change.devices[device.id][key] = { $set: device[key] };
}
};
const updateSceneStateProps = (state) => {
change.sceneStates[state.id] = {};
for (const key in state) {
change.sceneStates[state.id][key] = { $set: state[key] };
}
};
switch (action.type) {
case 'LOGIN_UPDATE':
newState = update(previousState, { login: { $set: action.login } });
break;
case 'USER_INFO_UPDATE':
newState = update(previousState, {
userInfo: { $set: action.userInfo },
});
break;
case 'ROOMS_UPDATE':
newState = previousState;
for (const room of action.rooms) {
createOrUpdateRoom(room);
}
break;
case 'HOST_ROOMS_UPDATE':
change = {
hostRooms: {
[action.hostId]: { $set: {} },
},
};
const rooms = change.hostRooms[action.hostId].$set;
for (const room of action.rooms) {
rooms[room.id] = room;
}
newState = update(previousState, change);
break;
case 'SCENES_UPDATE':
newState = previousState;
for (const scene of action.scenes) {
createOrUpdateScene(scene);
}
break;
case 'HOST_SCENES_UPDATE':
change = {
hostScenes: {
[action.hostId]: { $set: action.scenes }, // stored as array
},
};
newState = update(previousState, change);
break;
case 'STATES_UPDATE':
// console.log(action.sceneStates);
change = null;
// if scene is given, delete all sceneStates in that scene
// and remove any join between that scene and deleted
// sceneStates
change = {
scenes: {
[action.sceneId]: { sceneStates: { $set: new Set() } },
},
sceneStates: { $unset: [] },
};
const scene = previousState.scenes[action.sceneId];
for (const stateId of scene.sceneStates) {
change.sceneStates.$unset.push(stateId);
}
newState = update(previousState, change);
change = {
sceneStates: {},
scenes: {},
pendingJoins: { scenes: {} },
};
for (const sceneState of action.sceneStates) {
if (!newState.sceneStates[sceneState.id]) {
change.sceneStates[sceneState.id] = {
$set: sceneState,
};
} else {
updateSceneStateProps(sceneState);
}
if (sceneState.sceneId in newState.scenes) {
change.scenes[sceneState.sceneId] = change.scenes[sceneState.sceneId] || {};
change.scenes[sceneState.sceneId].sceneStates = change.scenes[sceneState.sceneId].sceneStates || {};
const { sceneStates } = change.scenes[sceneState.sceneId];
sceneStates.$add = sceneStates.$add || [];
sceneStates.$add.push(sceneState.id);
} else {
// room does not exist yet, so add to the list of pending
// joins
if (!change.pendingJoins.scenes[sceneState.sceneId]) {
change.pendingJoins.scenes[sceneState.sceneId] = {
$set: new Set([sceneState.id]),
};
} else {
change.pendingJoins.scenes[sceneState.sceneId].$set.add(
sceneState.id,
);
}
}
}
newState = update(newState, change);
break;
case 'DEVICES_UPDATE':
change = null;
// if room is given, delete all devices in that room
// and remove any join between that room and deleted
// devices
if (action.roomId) {
change = {
rooms: {
[action.roomId]: { devices: { $set: new Set() } },
},
devices: { $unset: [] },
};
const room = newState.rooms[action.roomId];
for (const deviceId of room.devices) {
change.devices.$unset.push(deviceId);
}
} else if (action.partial) {
// if the update is partial and caused by an operation on an input
// device (like /switch/operate), iteratively remove deleted
// devices and their join with their corresponding room.
change = {
devices: { $unset: [] },
rooms: {},
};
for (const device of action.devices) {
if (!previousState.devices[device.id]) continue;
change.devices.$unset.push(device.id);
const { roomId } = previousState.devices[device.id];
if (roomId in previousState.rooms) {
change.rooms[roomId] = change.rooms[roomId] || {
devices: { $remove: [] },
};
change.rooms[roomId].devices.$remove.push(device.id);
}
}
} else {
// otherwise, just delete all devices and all joins
// between rooms and devices
change = {
devices: { $set: {} },
rooms: {},
};
for (const room of Object.values(previousState.rooms)) {
if (change.rooms[room.id]) {
change.rooms[room.id].devices = { $set: new Set() };
}
}
}
newState = update(previousState, change);
change = {
devices: {},
rooms: {},
pendingJoins: { rooms: {} },
};
for (const device of action.devices) {
if (!newState.devices[device.id]) {
change.devices[device.id] = { $set: device };
} else {
updateDeviceProps(device);
}
if (device.roomId in newState.rooms) {
change.rooms[device.roomId] = change.rooms[device.roomId] || {};
change.rooms[device.roomId].devices = change.rooms[device.roomId].devices || {};
const { devices } = change.rooms[device.roomId];
devices.$add = devices.$add || [];
devices.$add.push(device.id);
} else {
// room does not exist yet, so add to the list of pending
// joins
if (!change.pendingJoins.rooms[device.roomId]) {
change.pendingJoins.rooms[device.roomId] = {
$set: new Set([device.id]),
};
} else {
change.pendingJoins.rooms[device.roomId].$set.add(device.id);
}
}
}
newState = update(newState, change);
break;
case 'HOST_DEVICES_UPDATE':
newState = action.partial
? previousState
: update(previousState, {
hostDevices: { [action.hostId]: { $set: {} } },
});
newState.hostDevices[action.hostId] = newState.hostDevices[action.hostId] || {};
change = {
hostDevices: {
[action.hostId]: {},
},
};
const deviceMap = change.hostDevices[action.hostId];
for (const device of action.devices) {
deviceMap[device.id] = { $set: device };
}
newState = update(newState, change);
break;
case 'AUTOMATION_UPDATE':
const automations = {};
for (const automation of action.automations) {
automations[automation.id] = automation;
}
change = {
automations: { $set: automations },
};
newState = update(previousState, change);
break;
case 'ROOM_SAVE':
newState = previousState;
createOrUpdateRoom(action.room);
break;
case 'SCENE_SAVE':
newState = previousState;
createOrUpdateScene(action.scene);
break;
case 'DEVICE_SAVE':
change = {
devices: { [action.device.id]: { $set: action.device } },
};
if (previousState.rooms[action.device.roomId]) {
change.rooms = {
[action.device.roomId]: {
devices: {
$add: [action.device.id],
},
},
};
} else {
change.pendingJoins = {
rooms: {
[action.device.roomId]: {
$add: [action.device.id],
},
},
};
}
newState = update(previousState, change);
break;
case 'HOST_DEVICE_SAVE':
change = {
hostDevices: {
[action.hostId]: {
[action.device.id]: {
$set: action.device,
},
},
},
};
newState = update(previousState, change);
break;
case 'HOST_DEVICES_DELETE':
change = {
hostDevices: {
[action.hostId]: {
$unset: [action.deviceIds],
},
},
};
newState = update(previousState, change);
break;
case 'AUTOMATION_SAVE':
change = {
automations: {
[action.automation.id]: { $set: action.automation },
},
};
newState = update(previousState, change);
break;
case 'STATE_SAVE':
change = {
sceneStates: {
[action.sceneState.id]: { $set: action.sceneState },
},
};
if (previousState.scenes[action.sceneState.sceneId]) {
change.scenes = {
[action.sceneState.sceneId]: {
sceneStates: {
$add: [action.sceneState.id],
},
},
};
} else {
change.pendingJoins = {
scenes: {
[action.sceneState.sceneId]: {
$add: [action.sceneState.id],
},
},
};
}
newState = update(previousState, change);
break;
case 'ROOM_DELETE':
if (!(action.roomId in previousState.rooms)) {
console.warn(`Room to delete ${action.roomId} does not exist`);
break;
}
// This update does not ensure the consistent update of switchId/dimmerId properties
// on output devices connected to an input device in this room. Please manually request
// all devices again if consistent update is desired
change = { devices: { $unset: [] } };
for (const id of previousState.rooms[action.roomId].devices) {
change.devices.$unset.push(id);
}
change.rooms = { $unset: [action.roomId] };
if (previousState.active.activeRoom === action.roomId) {
change.active = { activeRoom: { $set: -1 } };
}
newState = update(previousState, change);
break;
case 'AUTOMATION_DELETE':
change = {
automations: { $unset: [action.id] },
};
newState = update(previousState, change);
break;
case 'SCENE_DELETE':
if (!(action.sceneId in previousState.scenes)) {
console.warn(`Scene to delete ${action.sceneId} does not exist`);
break;
}
// This update does not ensure the consistent update of switchId/dimmerId properties
// on output devices connected to an input device in this room. Please manually request
// all devices again if consistent update is desired
change = { sceneStates: { $unset: [] } };
for (const id of previousState.scenes[action.sceneId].sceneStates) {
change.sceneStates.$unset.push(id);
}
change.scenes = { $unset: [action.sceneId] };
if (previousState.active.activeScene === action.sceneId) {
change.active = { activeScene: { $set: -1 } };
}
newState = update(previousState, change);
break;
case 'STATE_DELETE':
if (!(action.stateId in previousState.sceneStates)) {
console.warn(`State to delete ${action.stateId} does not exist`);
break;
}
change = {
sceneStates: { $unset: [action.stateId] },
};
if (
previousState.scenes[previousState.sceneStates[action.stateId].sceneId]
) {
change.scenes = {
[previousState.sceneStates[action.stateId].sceneId]: {
sceneStates: { $remove: [action.stateId] },
},
};
}
newState = update(previousState, change);
break;
case 'DEVICE_DELETE':
if (!(action.deviceId in previousState.devices)) {
console.warn(`Device to delete ${action.deviceId} does not exist`);
break;
}
change = {
devices: { $unset: [action.deviceId] },
};
if (previousState.rooms[previousState.devices[action.deviceId].roomId]) {
change.rooms = {
[previousState.devices[action.deviceId].roomId]: {
devices: { $remove: [action.deviceId] },
},
};
}
newState = update(previousState, change);
break;
case 'LOGOUT':
newState = update(initState, {});
break;
case 'SET_ACTIVE':
newState = update(previousState, {
active: {
[action.key]: {
$set: action.value,
},
},
});
break;
case 'REDUX_WEBSOCKET::MESSAGE':
const allDevices = JSON.parse(action.payload.message);
const devices = allDevices.filter(
(d) => (d.fromHostId === null || d.fromHostId === undefined) && !d.deleted,
);
const hostDevicesMapByHostId = allDevices
.filter((d) => d.fromHostId)
.reduce((a, e) => {
const hostId = e.fromHostId;
// delete e.fromHostId;
a[hostId] = a[hostId] || { updated: [], deletedIds: [] };
if (e.deleted) {
a[hostId].deletedIds.push(e.id);
} else {
a[hostId].updated.push(e);
}
return a;
}, {});
newState = reducer(previousState, {
type: 'DEVICES_UPDATE',
partial: true,
devices,
});
for (const hostId in hostDevicesMapByHostId) {
if (hostDevicesMapByHostId[hostId].updated.length > 0) {
newState = reducer(newState, {
type: 'HOST_DEVICES_UPDATE',
devices: hostDevicesMapByHostId[hostId].updated,
partial: true,
hostId,
});
}
if (hostDevicesMapByHostId[hostId].deletedIds.length > 0) {
newState = reducer(newState, {
type: 'HOST_DEVICES_DELETE',
deviceIds: hostDevicesMapByHostId[hostId].deletedIds,
partial: true,
hostId,
});
}
}
break;
case 'HG_UPDATE':
newState = update(previousState, {
[action.key]: { $set: action.value },
});
break;
default:
console.warn(`Action type ${action.type} unknown`, action);
return previousState;
}
return newState;
}
const initState = {
pendingJoins: {
rooms: {},
scenes: {},
automations: {},
},
active: {
activeRoom: -1,
activeTab: 'Devices',
activeScene: -1,
activeAutomation: -1,
activeHost: -1,
},
login: {
loggedIn: false,
token: null,
},
userInfo: null,
/** @type {[integer]Room} */
rooms: {},
/** @type {[integer]Scene} */
scenes: {},
hostScenes: {},
/** @type {[integer]Automation} */
automations: {},
/** @type {[integer]Device} */
devices: {},
/** @type {[integer]SceneState} */
sceneStates: {},
/** @type {User[]} */
guests: [],
/** @type {User[]} */
hosts: [],
/** @type {[integer]Device} */
hostDevices: {},
/** @type {[integer]Eoom} */
hostRooms: {},
};
function createSmartHutStore() {
const token = localStorage.getItem('token');
const exp = localStorage.getItem('exp');
const initialState = update(initState, {
login: {
token: { $set: token },
loggedIn: { $set: !!(token && exp > new Date().getTime()) },
},
});
if (!initialState.login.loggedIn) {
localStorage.removeItem('token');
localStorage.removeItem('exp');
initialState.login.token = null;
}
const store = createStore(
reducer,
initialState,
compose(applyMiddleware(thunk), applyMiddleware(reduxWebSocket())),
);
if (initialState.login.loggedIn) {
store.dispatch(connect(socketURL(token)));
}
return store;
}
const smartHutStore = createSmartHutStore();
export default smartHutStore;