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, 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 }, }, }, }); } 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] = {}; change.sceneStates[state.id] = { $set: state.id }; }; 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 "SCENES_UPDATE": newState = previousState; for (const scene of action.scenes) { createOrUpdateScene(scene); } break; case "STATE_UPDATE": console.log(action.sceneStates); newState = previousState; change = null; // if room is given, delete all devices in that room // and remove any join between that room and deleted // devices change = { scenes: { [action.sceneId]: { sceneStates: { $set: new Set() } } }, sceneStates: { $unset: [] }, }; const scene = newState.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; sceneStates.$add = sceneStates.$add || []; sceneStates.$add.push(sceneState); } 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]), }; } else { change.pendingJoins.scenes[sceneState.sceneId].$set.add(sceneState); } } } 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].roomId; 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; 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 "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 "STATE_SAVE": console.log("Store", action.sceneState); 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 "SCENE_DELETE": console.log("SCENE", action.sceneId); 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 = { states: { $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 "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_ROOM": newState = update(previousState, { active: { activeRoom: { $set: action.activeRoom, }, }, }); break; case "SET_ACTIVE_TAB": newState = update(previousState, { active: { activeTab: { $set: action.activeTab, }, }, }); break; case "SET_ACTIVE_SCENE": newState = update(previousState, { active: { activeScene: { $set: action.activeScene, }, }, }); break; case "SET_ACTIVE_AUTOMATION": newState = update(previousState, { active: { activeAutomation: { $set: action.activeAutomation, }, }, }); break; case "REDUX_WEBSOCKET::MESSAGE": const devices = JSON.parse(action.payload.message); console.log(devices); newState = reducer(previousState, { type: "DEVICES_UPDATE", partial: true, devices, }); 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, }, login: { loggedIn: false, token: null, }, userInfo: null, /** @type {[integer]Room} */ rooms: {}, /** @type {[integer]Scene} */ scenes: {}, /** @type {[integer]Automation} */ automations: {}, /** @type {[integer]Device} */ devices: {}, /** @type {[integer]SceneState} */ sceneStates: {}, }; 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;