diff --git a/README.md b/README.md index 5aac65c..89ce48d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1 @@ # frontend - diff --git a/smart-hut/package-lock.json b/smart-hut/package-lock.json index b2d2c82..d39ace7 100644 --- a/smart-hut/package-lock.json +++ b/smart-hut/package-lock.json @@ -10922,6 +10922,11 @@ "scheduler": "^0.18.0" } }, + "react-dom-factories": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/react-dom-factories/-/react-dom-factories-1.0.2.tgz", + "integrity": "sha1-63cFxNs2+1AbOqOP91lhaqD/luA=" + }, "react-error-overlay": { "version": "6.0.6", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.6.tgz", @@ -10932,6 +10937,23 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==" }, + "react-modal": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-2.2.2.tgz", + "integrity": "sha512-tdgyEyfbyfzDUj40XtWldAQe7e+yhJDUtVSlsQ9AQCGifzWck6v1XTtIVGViVftOsEA3cBWCZCjF3rq6FPJzMg==", + "requires": { + "exenv": "1.2.0", + "prop-types": "^15.5.10", + "react-dom-factories": "^1.0.0" + }, + "dependencies": { + "exenv": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.0.tgz", + "integrity": "sha1-ODXxJ6vwdb/ggtCu1EhAV8eOPIk=" + } + } + }, "react-popper": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz", diff --git a/smart-hut/package.json b/smart-hut/package.json index 9591d77..502c36b 100644 --- a/smart-hut/package.json +++ b/smart-hut/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@giantmachines/redux-websocket": "^1.1.7", "@material-ui/core": "^4.9.4", "@material-ui/icons": "^4.9.1", "@testing-library/jest-dom": "^4.2.4", @@ -10,6 +11,7 @@ "@testing-library/user-event": "^7.1.2", "axios": "^0.19.2", "classnames": "^2.2.6", + "immutability-helper": "^3.0.2", "material-ui-image": "^3.2.3", "react": "^16.12.0", "react-axios": "^2.0.3", @@ -17,10 +19,14 @@ "react-circular-slider-svg": "^0.1.5", "react-device-detect": "^1.11.14", "react-dom": "^16.12.0", + "react-modal": "^2.2.2", + "react-redux": "^7.2.0", "react-round-slider": "^1.0.1", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", "react-scripts": "3.4.0", + "redux": "^4.0.5", + "redux-thunk": "^2.3.0", "semantic-ui-react": "^0.88.2", "styled-components": "^5.0.1" }, diff --git a/smart-hut/src/App.js b/smart-hut/src/App.js index c39324d..602e77f 100644 --- a/smart-hut/src/App.js +++ b/smart-hut/src/App.js @@ -11,45 +11,19 @@ import ConfirmForgotPasswrod from "./views/ConfirmForgotPassword"; import ConfirmRegistration from "./views/ConfirmRegistration"; import ConfirmResetPassword from "./views/ConfirmResetPassword"; import Instruction from "./views/Instruction"; +import Videocam from "./views/Videocam"; import queryString from "query-string"; - -import { call } from "./client_server"; - -/*let userJsonString = JSON.parse(localStorage.getItem("token")); - let date = new Date().getTime().toString(); - let delta = date - userJsonString.timestamp; - if (delta < 5*60*60*1000) { - loggedIn = true; - }*/ +import { RemoteService } from "./remote"; +import { connect } from "react-redux"; class App extends Component { - constructor(props) { - super(props); - - let loggedIn = false; - let token = undefined; - - try { - let userJsonString = localStorage.getItem("token"); - let exp = localStorage.getItem("exp"); - let date = new Date().getTime(); - if (userJsonString && exp && date < exp) { - loggedIn = true; - token = userJsonString; - } else { - localStorage.removeItem("token"); - localStorage.removeItem("exp"); - } - } catch (exception) {} + constructor(props, context) { + super(props, context); this.state = { - loggedIn: loggedIn, - token: token, + query: "", info: "", }; - - this.auth = this.auth.bind(this); - this.logout = this.logout.bind(this); } componentDidMount() { @@ -58,70 +32,21 @@ class App extends Component { this.setState({ query: values, }); - } else { - this.setState({ - query: "ciao", - }); } } - auth(data) { - return call - .login(data.params) - .then((res) => { - if (res.data && res.status === 200) { - let expire = new Date().getTime() + 60 * 60 * 5 * 1000; - localStorage.setItem("token", res.data.jwttoken); - localStorage.setItem("exp", expire); - call.setToken(res.data.jwttoken); - this.setState({ - user: data.params.user, - token: res.data.jwttoken, - loggedIn: true, - }); - this.getInfo(); - return res; - //this.props.history.push("/dashboard"); - } else { - this.setState({ - error: res.data.message, - }); - return res.status; - } - }) - .catch((err) => { - return err; - }); - } - - logout() { - this.setState({ - loggedIn: false, - }); - - localStorage.removeItem("token"); - localStorage.removeItem("exp"); - } - render() { + console.log("rendering root", this.props.loggedIn, this.state.query); return ( - {this.state.loggedIn && this.state.token ? ( - - ) : ( - - )} + {this.props.loggedIn ? : } - {this.state.loggedIn ? ( - - ) : ( - - )} + {this.props.loggedIn ? : } @@ -142,6 +67,9 @@ class App extends Component { + + + @@ -149,4 +77,6 @@ class App extends Component { } } -export default App; +const mapStateToProps = (state, _) => ({ loggedIn: state.login.loggedIn }); +const AppContainer = connect(mapStateToProps, RemoteService)(App); +export default AppContainer; diff --git a/smart-hut/src/App.test.js b/smart-hut/src/App.test.js index 18413f8..135fe7a 100644 --- a/smart-hut/src/App.test.js +++ b/smart-hut/src/App.test.js @@ -3,12 +3,16 @@ import { render } from "@testing-library/react"; import { Router } from "react-router"; import { createMemoryHistory } from "history"; import App from "./App"; +import { Provider } from "react-redux"; +import smartHutStore from "./store"; test("redirects to homepage", () => { const history = createMemoryHistory(); render( - + + + ); expect(history.location.pathname).toBe("/"); diff --git a/smart-hut/src/client_server.js b/smart-hut/src/client_server.js deleted file mode 100644 index 2598b53..0000000 --- a/smart-hut/src/client_server.js +++ /dev/null @@ -1,285 +0,0 @@ -// vim: set ts=2 sw=2 et tw=80: - -import axios from "axios"; - -let config; -if (window.BACKEND_URL !== "__BACKEND_URL__") { - config = window.BACKEND_URL + "/"; -} else { - config = "http://localhost:8080/"; -} -var tkn = localStorage.getItem("token"); - -/** the ServiceSocket instance valid for the current session */ -var socket; - -// requests data devices -/* - - { - params : data, - device: 'tipoDiDevice', - id: se serve - } - - - device routes: - - buttonDimmer - - dimmableLight - - knobDimmer - - motionSensor - - regularLight - - sensor - - smartPlug - - switch - -*/ - -/** The number of times a connection to the socket was tried */ -var retries = 0; - -/** Class to handle connection with the sensor socket */ -class ServiceSocket { - /** - * Create a new sensor socket connection - * @param {string} token - The JWT token (needed for authentication) - * @param {Object.|null} callbacks - A callback map from - * device id to callback function - */ - constructor(token, callbacks) { - this.token = token; - this.authenticated = false; - this.callbacks = callbacks || {}; - - this.connection = new WebSocket("ws://localhost:8080/sensor-socket"); - - this.connection.onopen = (evt) => { - this.connection.send(JSON.stringify({ token })); - }; - - this.connection.onmessage = (evt) => { - let data = JSON.parse(evt.data); - - if (!this.authenticated) { - if (data.authenticated) { - this.authenticated = true; - retries = 0; - } else { - console.error("socket authentication failed"); - } - } else { - this.invokeCallbacks(data); - } - }; - - this.connection.onerror = (evt) => { - if (retries >= 5) { - console.error("too many socket connection retries"); - return; - } - retries++; - socket = new ServiceSocket(this.token, this.callbacks); - }; - } - - invokeCallbacks(data) { - if (data.id && this.callbacks[data.id]) { - this.callbacks[data.id].forEach((f) => f(data)); - } - } - - /** - * Registers a new callback function to be called when updates on the device - * with the id given are recieved - * @param {number} id - the id of the device to check updates for - * @param {function} stateCallback - a function that recieves a device as the - * first parameter, that will be called whenever a update is recieved - */ - subscribe(id, stateCallback) { - if (this.callbacks[id] === undefined) { - this.callbacks[id] = []; - } - - this.callbacks[id].push(stateCallback); - } - - /** - * Unregisters a function previously registered with `subscribe(...)`. - * @param {number} id - the id of the device to stop checking updates for - * @param {function} stateCallback - the callback to unregister - */ - unsubscribe(id, stateCallback) { - this.callbacks[id].splice(this.callbacks[id].indexOf(stateCallback), 1); - } - - /** - * Closes the underlying websocket connection - */ - close() { - this.connection.close(); - } -} - -if (tkn) { - socket = new ServiceSocket(tkn); -} - -export var call = { - setToken: function (token) { - tkn = token; - if (tkn) { - if (socket) { - socket.close(); - } - socket = new ServiceSocket(tkn); - } - }, - - /** - * Registers a new callback function to be called when updates on the device - * with the id given are recieved - * @param {number} id - the id of the device to check updates for - * @param {function} stateCallback - a function that recieves a device as the - * first parameter, that will be called whenever a update is recieved - */ - socketSubscribe: function (id, callback) { - socket.subscribe(id, callback); - }, - - /** - * Unregisters a function previously registered with `subscribe(...)`. - * @param {number} id - the id of the device to stop checking updates for - * @param {function} stateCallback - the callback to unregister - */ - socketUnsubscribe: function (id, callback) { - socket.unsubscribe(id, callback); - }, - login: function (data, headers) { - return axios.post(config + "auth/login", data); - }, - register: function (data, headers) { - return axios.post(config + "register", data); - }, - getUserInfo: function (token) { - if (!token) { - token = tkn; - } - return axios.get(config + "auth/profile", { - headers: { Authorization: "Bearer " + token }, - }); - }, - initResetPassword: function (data, headers) { - return axios.post(config + "register/init-reset-password", data); - }, - resetPassword: function (data, headers) { - return axios.put(config + "register/reset-password", data); - }, - getAllRooms: function (token) { - if (!token) { - token = tkn; - } - return axios.get(config + "room", { - headers: { Authorization: "Bearer " + token }, - }); - }, - getAllDevices: function (token) { - if (!token) { - token = tkn; - } - return axios.get(config + "device", { - headers: { Authorization: "Bearer " + token }, - }); - }, - getAllDevicesByRoom: function (id, token) { - if (!token) { - token = tkn; - } - return axios.get(config + "room/" + id + "/devices", { - headers: { Authorization: "Bearer " + token }, - }); - }, - createRoom: function (data, headers) { - return axios.post(config + "room", data, { - headers: { Authorization: "Bearer " + tkn }, - }); - }, - updateRoom: function (data, headers) { - return axios.put(config + "room/" + data.id, data, { - headers: { Authorization: "Bearer " + tkn }, - }); - }, - deleteRoom: function (data, headers) { - return axios.delete(config + "room/" + data.id, { - headers: { Authorization: "Bearer " + tkn }, - }); - }, - devicePost: function (data, headers) { - return axios - .post(config + data.device, data.params, { - headers: { Authorization: "Bearer " + tkn }, - }) - .then((res) => { - if ( - res.status === 200 && - (data.device === "switch" || - data.device === "buttonDimmer" || - data.device === "knobDimmer") - ) { - let type = "lightId="; - if (data.device === "switch") { - type = "switchableId="; - } - data.params.lights.forEach((e) => { - let urlUp = - config + data.device + "/" + res.data.id + "/lights?" + type + e; - axios.post( - urlUp, - {}, - { headers: { Authorization: "Bearer " + tkn } } - ); - }); - } - return res; - }); - }, - deviceUpdate: function (data, typeDevice) { - let url = "device"; - if (typeDevice) { - url = typeDevice; - } - let promiseRes = axios.put(config + url, data, { - headers: { Authorization: "Bearer " + tkn }, - }); - // also for btn/knob dimmer - if ( - typeDevice === "switch/operate" || - typeDevice === "buttonDimmer/dim" || - typeDevice === "knobDimmer/dimTo" - ) { - promiseRes = promiseRes.then((e) => { - if (e.status === 200) { - e.data.forEach((device) => socket.invokeCallbacks(device)); - } - return e; - }); - } - - return promiseRes; - }, - deviceDelete: function (data, headers) { - return axios.delete(config + data.device + "/" + data.id, { - headers: { Authorization: "Bearer " + tkn }, - }); - }, - deviceGetById: function (data, headers) { - return axios.get(config + data.device + "/" + data.id); - }, - deviceGetAll: function (data, headers) { - return axios.get(config + data.device); - }, - smartPlugReset: function (id) { - return axios.delete(config + "smartPlug/" + id + "/meter", { - headers: { Authorization: "Bearer " + tkn }, - }); - }, -}; diff --git a/smart-hut/src/components/AutomationModal.js b/smart-hut/src/components/AutomationModal.js new file mode 100644 index 0000000..5f8fbd5 --- /dev/null +++ b/smart-hut/src/components/AutomationModal.js @@ -0,0 +1,189 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { RemoteService } from "../remote"; +import { appActions } from "../storeActions"; + +class AutomationModal extends Component { + constructor(props) { + super(props); + this.state = this.initialState; + this.setInitialState(); + + this.addAutomationModal = this.addAutomationModal.bind(this); + this.modifyAutomationModal = this.modifyAutomationModal.bind(this); + this.deleteAutomation = this.deleteAutomation.bind(this); + } + + get initialState() { + return { + //INITIAL STATE HERE + }; + } + + setInitialState() { + this.setState(this.initialState); + } + + get type() { + return !this.props.id ? "new" : "modify"; + } + + addAutomationModal = (e) => { + /*let data = { + // DATA HERE + };*/ + // TODO CALL TO REMOTE SERVER TO ADD SCENE + /*this.props + .saveRoom(data, null) + .then(() => { + this.setInitialState(); + this.closeModal(); + }) + .catch((err) => console.error("error in creating room", err));*/ + }; + + modifyAutomationModal = (e) => { + /* let data = { + // DATA HERE + };*/ + // TODO CALL TO REMOTE SERVER TO MODIFY SCENE + /*this.props + .saveRoom(data, this.props.id) + .then(() => { + this.setInitialState(); + this.closeModal(); + }) + .catch((err) => console.error("error in updating room", err));*/ + }; + + deleteAutomation = (e) => { + // TODO CALL TO REMOTE SERVER TO DELETE SCENE + /* + this.props + .deleteRoom(this.props.id) + .then(() => this.closeModal()) + .catch((err) => console.error("error in deleting room", err));*/ + }; + + changeSomething = (event) => { + let nam = event.target.name; + let val = event.target.value; + this.setState({ [nam]: val }); + }; + + closeModal = (e) => { + this.setState({ openModal: false }); + }; + + openModal = (e) => { + this.setState({ openModal: true }); + }; + + render() { + return ( +
+ {/* + {!this.props.nicolaStop ? ( +
+ + {this.type === "new" ? ( + + ) : ( + + )} + + + {this.type === "new" ? ( + + ) : ( + + )} + +
+ ) : null} + + +
+ {this.type === "new" ? "Add new automation" : "Modify automation"} +
+ + { + //TODO FORM TO ADD OR MODIFY SCENE + } + + {this.type === "modify" ? ( + + ) : null} + + + + + + +
*/} +
+ ); + } +} + +const setActiveAutomation = (activeAutomation) => { + return (dispatch) => + dispatch(appActions.setActiveAutomation(activeAutomation)); +}; + +const mapStateToProps = (state, ownProps) => ({ + automations: ownProps.id ? state.automations[ownProps.id] : null, +}); +const AutomationModalContainer = connect( + mapStateToProps, + { ...RemoteService, setActiveAutomation }, + null, + { forwardRef: true } +)(AutomationModal); +export default AutomationModalContainer; diff --git a/smart-hut/src/components/HeaderController.js b/smart-hut/src/components/HeaderController.js index d5a5e19..1d84c6b 100644 --- a/smart-hut/src/components/HeaderController.js +++ b/smart-hut/src/components/HeaderController.js @@ -1,7 +1,9 @@ import React from "react"; import { Grid, Divider, Button, Label, Responsive } from "semantic-ui-react"; import { Segment, Image } from "semantic-ui-react"; -import { call } from "../client_server"; +import { RemoteService } from "../remote"; +import { withRouter } from "react-router-dom"; +import { connect } from "react-redux"; const IconHomeImage = () => ( ( const TitleImage = () => ; -export default class MyHeader extends React.Component { +export class MyHeader extends React.Component { constructor(props) { super(props); - this.state = { - username: "", - }; this.getInfo(); + this.logout = this.logout.bind(this); } + + logout() { + this.props.logout().then(() => this.props.history.push("/")); + } + getInfo() { - call.getUserInfo(this.state.token).then((res) => { - if (res.status === 200) { - this.setState({ - username: res.data.username, - }); - } - }); + this.props + .fetchUserInfo() + .catch((err) => console.error("MyHeader fetch user info error", err)); } + render() { return (
@@ -52,10 +54,12 @@ export default class MyHeader extends React.Component { - + @@ -74,10 +78,10 @@ export default class MyHeader extends React.Component { - + @@ -86,3 +90,13 @@ export default class MyHeader extends React.Component { ); } } + +const mapStateToProps = (state, _) => ({ + username: + state.userInfo && state.userInfo.username ? state.userInfo.username : "", +}); +const LoginContainer = connect( + mapStateToProps, + RemoteService +)(withRouter(MyHeader)); +export default LoginContainer; diff --git a/smart-hut/src/components/modalform.js b/smart-hut/src/components/RoomModal.js similarity index 64% rename from smart-hut/src/components/modalform.js rename to smart-hut/src/components/RoomModal.js index 87835a3..eabdb89 100644 --- a/smart-hut/src/components/modalform.js +++ b/smart-hut/src/components/RoomModal.js @@ -10,31 +10,50 @@ import { Image, } from "semantic-ui-react"; import SelectIcons from "./SelectIcons"; +import { connect } from "react-redux"; +import { RemoteService } from "../remote"; +import { appActions } from "../storeActions"; +import { update } from "immutability-helper"; const NO_IMAGE = "https://react.semantic-ui.com/images/wireframe/image.png"; -export default class ModalWindow extends Component { +class RoomModal extends Component { constructor(props) { super(props); - - if (typeof this.props.idRoom === "function") { - this.idRoom = this.props.idRoom(); - } else { - this.idRoom = this.props.idRoom; - } - - this.state = { - id: "", - selectedIcon: "", - name: this.props.type === "new" ? "New Room" : this.idRoom.name, - img: this.props.type === "new" ? null : this.idRoom.image, - openModal: false, - }; + this.state = this.initialState; + this.setInitialState(); this.fileInputRef = React.createRef(); this.addRoomModal = this.addRoomModal.bind(this); this.updateIcon = this.updateIcon.bind(this); + this.removeImage = this.removeImage.bind(this); + } + + get initialState() { + return { + selectedIcon: this.type === "new" ? "home" : this.props.room.icon, + name: this.type === "new" ? "New Room" : this.props.room.name, + img: this.type === "new" ? null : this.props.room.image, + openModal: false, + }; + } + + removeImage(e) { + e.preventDefault(); + this.setState( + update(this.state, { + image: { $set: null }, + }) + ); + } + + setInitialState() { + this.setState(this.initialState); + } + + get type() { + return !this.props.id ? "new" : "modify"; } addRoomModal = (e) => { @@ -43,29 +62,39 @@ export default class ModalWindow extends Component { name: this.state.name, image: this.state.img, }; - this.props.addRoom(data); - this.setState({ - name: "Device", - }); - this.closeModal(); + + this.props + .saveRoom(data, null) + .then(() => { + this.setInitialState(); + this.closeModal(); + }) + .catch((err) => console.error("error in creating room", err)); }; modifyRoomModal = (e) => { let data = { - icon: - this.state.selectedIcon === "" - ? this.idRoom.icon - : this.state.selectedIcon, - name: this.state.name === "" ? this.idRoom.name : this.state.name, + icon: this.state.selectedIcon, + name: this.state.name, image: this.state.img, }; - this.props.updateRoom(data); - this.closeModal(); + + console.log("data", data); + + this.props + .saveRoom(data, this.props.id) + .then(() => { + this.setInitialState(); + this.closeModal(); + }) + .catch((err) => console.error("error in updating room", err)); }; deleteRoom = (e) => { - this.props.deleteRoom(); - this.closeModal(); + this.props + .deleteRoom(this.props.id) + .then(() => this.closeModal()) + .catch((err) => console.error("error in deleting room", err)); }; changeSomething = (event) => { @@ -76,7 +105,6 @@ export default class ModalWindow extends Component { closeModal = (e) => { this.setState({ openModal: false }); - this.updateIcon("home"); }; openModal = (e) => { @@ -107,7 +135,7 @@ export default class ModalWindow extends Component { {!this.props.nicolaStop ? (
- {this.props.type === "new" ? ( + {this.type === "new" ? ( + ) : null}
@@ -189,12 +220,12 @@ export default class ModalWindow extends Component {
- {this.props.type === "modify" ? ( + {this.type === "modify" ? ( @@ -230,3 +259,18 @@ export default class ModalWindow extends Component { ); } } + +const setActiveRoom = (activeRoom) => { + return (dispatch) => dispatch(appActions.setActiveRoom(activeRoom)); +}; + +const mapStateToProps = (state, ownProps) => ({ + room: ownProps.id ? state.rooms[ownProps.id] : null, +}); +const RoomModalContainer = connect( + mapStateToProps, + { ...RemoteService, setActiveRoom }, + null, + { forwardRef: true } +)(RoomModal); +export default RoomModalContainer; diff --git a/smart-hut/src/components/SceneModal.js b/smart-hut/src/components/SceneModal.js new file mode 100644 index 0000000..9183a58 --- /dev/null +++ b/smart-hut/src/components/SceneModal.js @@ -0,0 +1,204 @@ +import React, { Component } from "react"; +import { + Button, + Header, + Modal, + Icon, + Responsive, + Form, + Input, +} from "semantic-ui-react"; +import { connect } from "react-redux"; +import { RemoteService } from "../remote"; +import { appActions } from "../storeActions"; +//import { update } from "immutability-helper"; + +class SceneModal extends Component { + constructor(props) { + super(props); + this.state = this.initialState; + this.setInitialState(); + + this.addSceneModal = this.addSceneModal.bind(this); + this.modifySceneModal = this.modifySceneModal.bind(this); + this.deleteScene = this.deleteScene.bind(this); + } + + get initialState() { + return { + name: this.type === "new" ? "New Scene" : this.props.scene.name, + openModal: false, + }; + } + + setInitialState() { + this.setState(this.initialState); + } + + get type() { + return !this.props.id ? "new" : "modify"; + } + + addSceneModal = (e) => { + let data = { + name: this.state.name, + }; + + this.props + .saveScene(data, null) + .then(() => { + this.setInitialState(); + this.closeModal(); + }) + .catch((err) => console.error("error in creating room", err)); + }; + + modifySceneModal = (e) => { + let data = { + name: this.state.name, + }; + + this.props + .saveScene(data, this.props.id) + .then(() => { + this.setInitialState(); + this.closeModal(); + }) + .catch((err) => console.error("error in updating room", err)); + }; + + deleteScene = (e) => { + this.props + .deleteScene(this.props.id) + .then(() => this.closeModal()) + .catch((err) => console.error("error in deleting room", err)); + }; + + changeSomething = (event) => { + let nam = event.target.name; + let val = event.target.value; + this.setState({ [nam]: val }); + }; + + closeModal = (e) => { + this.setState({ openModal: false }); + }; + + openModal = (e) => { + this.setState({ openModal: true }); + }; + + render() { + return ( +
+ {!this.props.nicolaStop ? ( +
+ + {this.type === "new" ? ( + + ) : ( + + )} + + + {this.type === "new" ? ( + + ) : ( + + )} + +
+ ) : null} + + +
+ {this.type === "new" ? "Add new scene" : "Modify scene"} +
+ +
+

Insert the name of the scene:

+ + + +
+ + {this.type === "modify" ? ( + + ) : null} +
+ + + + + +
+
+ ); + } +} + +const setActiveScene = (activeScene) => { + return (dispatch) => dispatch(appActions.setActiveScene(activeScene)); +}; + +const mapStateToProps = (state, ownProps) => ({ + scene: ownProps.id ? state.scenes[ownProps.id] : null, +}); +const SceneModalContainer = connect( + mapStateToProps, + { ...RemoteService, setActiveScene }, + null, + { forwardRef: true } +)(SceneModal); +export default SceneModalContainer; diff --git a/smart-hut/src/components/VideoTest.js b/smart-hut/src/components/VideoTest.js new file mode 100644 index 0000000..38fa8a1 --- /dev/null +++ b/smart-hut/src/components/VideoTest.js @@ -0,0 +1,12 @@ +import React from "react"; +import DevicePanel from "./dashboard/DevicePanel"; + +export default class VideoTest extends React.Component { + render() { + return ( +
+ +
+ ); + } +} diff --git a/smart-hut/src/components/dashboard/Automations.css b/smart-hut/src/components/dashboard/Automations.css new file mode 100644 index 0000000..508b97a --- /dev/null +++ b/smart-hut/src/components/dashboard/Automations.css @@ -0,0 +1,18 @@ +.segment-automations { + top: 10%; +} + +.list-index { + font-size: 1.5rem; +} + +.remove-icon { + display: inline !important; + margin-left: 1rem !important; +} + +.trigger-item { + display: flex !important; + justify-content: center !important; + align-items: center !important; +} diff --git a/smart-hut/src/components/dashboard/AutomationsPanel.js b/smart-hut/src/components/dashboard/AutomationsPanel.js new file mode 100644 index 0000000..2a7b683 --- /dev/null +++ b/smart-hut/src/components/dashboard/AutomationsPanel.js @@ -0,0 +1,570 @@ +import React, { Component, useState, useEffect } from "react"; +import { connect } from "react-redux"; +import { RemoteService } from "../../remote"; +import "./Automations.css"; + +import { + Segment, + Grid, + Icon, + Header, + Input, + Button, + List, + Dropdown, + Form, + Divider, + Checkbox, + Menu, +} from "semantic-ui-react"; + +const operands = [ + { key: "EQUAL", text: "=", value: "EQUAL" }, + { + key: "GREATER_EQUAL", + text: "\u2265", + value: "GREATER_EQUAL", + }, + { + key: "GREATER", + text: ">", + value: "GREATER", + }, + { + key: "LESS_EQUAL", + text: "\u2264", + value: "LESS_EQUAL", + }, + { + key: "LESS", + text: "<", + value: "LESS", + }, +]; + +const deviceStateOptions = [ + { key: "off", text: "off", value: false }, + { key: "on", text: "on", value: true }, +]; + +const CreateTrigger = (props) => { + const [activeOperand, setActiveOperand] = useState(true); + const admitedDevices = ["sensor", "regularLight", "dimmableLight"]; // TODO Complete this list + const deviceList = props.devices + .map((device) => { + return { + key: device.id, + text: device.name, + value: device.id, + kind: device.kind, + }; + }) + .filter((e) => admitedDevices.includes(e.kind)); + + const onChange = (e, val) => { + props.inputChange(val); + if ( + props.devices.filter((d) => d.id === val.value)[0].hasOwnProperty("on") + ) { + setActiveOperand(false); + } else { + setActiveOperand(true); + } + }; + + return ( + + +
+ + + + + {activeOperand ? ( + + + props.inputChange(val)} + name="operand" + compact + selection + options={operands} + /> + + + props.inputChange(val)} + name="value" + type="number" + placeholder="Value" + /> + + + ) : ( + + props.inputChange(val)} + placeholder="State" + name="value" + compact + selection + options={deviceStateOptions} + /> + + )} + +
+
+
+ ); +}; + +const SceneItem = (props) => { + let position = props.order.indexOf(props.scene.id); + return ( + + + + + + + props.orderScenes(props.scene.id, val.checked) + } + checked={position + 1 > 0} + /> + + +

{props.scene.name}

+
+ +

{position !== -1 ? "# " + (position + 1) : ""}

+
+
+
+
+
+ ); +}; + +const Trigger = ({ deviceName, trigger, onRemove, index }) => { + const { operand, value } = trigger; + let symbol; + if (operand) { + symbol = operands.filter((opt) => opt.key === operand)[0].text; + } + return ( + + + {deviceName} + {operand ? {symbol} : ""} + + {operand ? value : value ? "on" : "off"} + + + onRemove(index)} + className="remove-icon" + name="remove" + /> + + ); +}; + +export const CreateAutomation = (props) => { + const [triggerList, setTrigger] = useState([]); + const [order, setOrder] = useState([]); + const [stateScenes, setScenes] = useState(props.scenes); + const [automationName, setautomationName] = useState("New Automation"); + const [editName, setEditName] = useState(false); + const [newTrigger, setNewTrigger] = useState({}); + + useEffect(() => { + setScenes(props.scenes); + }, [props]); + + const _checkNewTrigger = (trigger) => { + const auxDevice = props.devices.filter((d) => d.id === trigger.device)[0]; + if (auxDevice && auxDevice.hasOwnProperty("on")) { + if (!trigger.device || !trigger.value == null) { + return { + result: false, + message: "There are missing fields!", + }; + } + } else { + if (!trigger.device || !trigger.operand || !trigger.value) { + return { + result: false, + message: "There are missing fields", + }; + } + } + const result = !triggerList.some( + (t) => t.device === trigger.device && t.operand === trigger.operand + ); + return { + result: result, + message: result + ? "" + : "You have already created a trigger for this device with the same conditions", + }; + }; + const addTrigger = () => { + const { result, message } = _checkNewTrigger(newTrigger); + const auxTrigger = newTrigger; + if (result) { + if ( + props.devices + .filter((d) => d.id === newTrigger.device)[0] + .hasOwnProperty("on") + ) { + delete auxTrigger.operand; + } + setTrigger((prevList) => [...prevList, auxTrigger]); + } else { + alert(message); + } + }; + + const removeTrigger = (index) => { + setTrigger((prevList) => prevList.filter((t, i) => i !== index)); + }; + + // This gets triggered when the devices dropdown changes the value. + const onInputChange = (val) => { + setNewTrigger({ ...newTrigger, [val.name]: val.value }); + }; + const onChangeName = (e, val) => setautomationName(val.value); + + const orderScenes = (id, checked) => { + if (checked) { + setOrder((prevList) => [...prevList, id]); + } else { + setOrder((prevList) => prevList.filter((e) => e !== id)); + } + }; + const searchScenes = (e, { value }) => { + if (value.length > 0) { + setScenes((prevScenes) => { + return stateScenes.filter((e) => { + return e.name.includes(value); + }); + }); + } else { + setScenes(props.scenes); + } + }; + + const _generateKey = (trigger) => { + if (trigger.hasOwnProperty("operand")) { + return trigger.device + trigger.operand + trigger.value; + } + return trigger.device + trigger.value; + }; + + /*const checkBeforeSave = () => { + if (automationName.length <= 0) { + alert("Give a name to the automation"); + return false; + } + if (triggerList.length <= 0) { + alert("You have to create a trigger"); + return false; + } + if (order.length <= 0) { + alert("You need at least one active scene"); + return false; + } + return true; + };*/ + + const saveAutomation = () => { + //if(checkBeforeSave()){ + const automation = { + name: automationName, + }; + props.save({ automation, triggerList, order }); + //} + }; + + return ( + +
+ {editName ? ( + + ) : ( + automationName + )} +
+ + +
+ )} + + + + + + + + + + + + + ); +}; + +const Automation = ({ automation, devices, scenes, removeAutomation }) => { + const { triggers } = automation; + const scenePriorities = automation.scenes; + const getOperator = (operand) => + operands.filter((o) => o.key === operand)[0].text; + + return ( + +
+ {automation.name} +
+ + )} + + + + {this.props.automations.map((automation, i) => { + console.log(23, automation, i, this.props.automations); + return ( + + + + ); + })} + + + ); + } +} + +const mapStateToProps = (state, _) => ({ + activeRoom: state.active.activeRoom, + activeTab: state.active.activeTab, + get scenes() { + return Object.values(state.scenes); + }, + get devices() { + return Object.values(state.devices); + }, + get automations() { + console.log(state.automations); + return Object.values(state.automations); + }, +}); +const AutomationsPanelContainer = connect( + mapStateToProps, + RemoteService +)(AutomationsPanel); +export default AutomationsPanelContainer; diff --git a/smart-hut/src/components/dashboard/DevicePanel.js b/smart-hut/src/components/dashboard/DevicePanel.js index f4f3633..449642e 100644 --- a/smart-hut/src/components/dashboard/DevicePanel.js +++ b/smart-hut/src/components/dashboard/DevicePanel.js @@ -2,182 +2,64 @@ import React, { Component } from "react"; import { Grid } from "semantic-ui-react"; -import { editButtonStyle, panelStyle } from "./devices/styleComponents"; -import { checkMaxLength, DEVICE_NAME_MAX_LENGTH } from "./devices/constants"; -import DeviceType from "./devices/DeviceTypeController"; +import Device from "./devices/Device"; import NewDevice from "./devices/NewDevice"; -import SettingsModal from "./devices/SettingsModal"; -import { call } from "../../client_server"; +import { connect } from "react-redux"; +import { RemoteService } from "../../remote"; -export default class DevicePanel extends Component { +class DevicePanel extends Component { constructor(props) { super(props); - this.state = { - editMode: false, - }; - this.addDevice = this.addDevice.bind(this); + this.getDevices(); } - editModeController = (e) => - this.setState((prevState) => ({ editMode: !prevState.editMode })); - - openModal = (settingsDeviceId) => { - this.setState((prevState) => ({ - openSettingsModal: !prevState.openSettingsModal, - settingsDeviceId: settingsDeviceId, - })); - }; - - changeDeviceData = (deviceId, newSettings) => { - console.log(newSettings.name, " <-- new name --> ", deviceId); - this.props.devices.map((device) => { - if (device.id === deviceId) { - for (let prop in newSettings) { - if (device.hasOwnProperty(prop)) { - if (prop === "name") { - if (checkMaxLength(newSettings[prop])) { - device[prop] = newSettings[prop]; - } else { - alert( - "Name must be less than " + - DEVICE_NAME_MAX_LENGTH + - " characters." - ); - } - } else { - device[prop] = newSettings[prop]; - } - } - } - } - return null; - }); - this.forceUpdate(); - }; - - getDevices = () => { - if (this.props.activeItem === -1) { - call - .getAllDevices() - .then((res) => { - if (res.status === 200) { - this.setState({ - devices: res.data, - }); - } - }) - .catch((err) => { - console.log(err); - }); - } else { - call - .getAllDevicesByRoom(this.props.activeItem) - .then((res) => { - if (res.status === 200) { - this.setState({ - devices: res.data, - }); - } - }) - .catch((err) => {}); + getDevices() { + if (this.props.tab === "Devices") { + this.props + .fetchDevices() + .catch((err) => console.error(`error fetching devices:`, err)); } - }; - - async addDevice(data) { - const ds = await this.props.addDevice(data); - this.setState({ - devices: ds, - }); - this.forceUpdate(); } - updateDevice = (data) => { - const roomId = this.props.devices.filter( - (d) => d.id === this.state.settingsDeviceId - )[0].roomId; - data["id"] = this.state.settingsDeviceId; - data["roomId"] = roomId; - call - .deviceUpdate(data) - .then((res) => { - if (res.status === 200) { - this.getDevices(); - this.forceUpdate(); - } - }) - .catch((err) => {}); - }; - - removeDevice = () => { - const item = this.props.devices.filter( - (d) => d.id === this.state.settingsDeviceId - )[0]; - const data = { - device: item.kind, - id: this.state.settingsDeviceId, - }; - - call - .deviceDelete(data) - .then((res) => { - if (res.status === 200) { - this.openModal(); - this.getDevices(); - this.forceUpdate(); - } - }) - .catch((err) => {}); - }; - render() { - const edit = { - mode: this.state.editMode, - openModal: this.openModal, - }; - - /*var backGroundImg = - this.props.activeItem === -1 ? "" : this.props.room.image;*/ - const ds = this.state.devices ? this.state.devices : this.props.devices; - return ( -
- - - {this.state.openSettingsModal ? ( - d.id === this.state.settingsDeviceId)[0]} - /> - ) : ( - "" - )} - {ds - ? ds.map((e, i) => { - return ( - - - - ); - }) - : null} - {this.props.activeItem !== -1 ? ( - - + + {this.props.devices.map((e, i) => { + return ( + + - ) : null} - -
+ ); + })} + {!this.props.isActiveRoomHome ? ( + + + + ) : null} + ); } } + +const mapStateToProps = (state, _) => ({ + get devices() { + if (state.active.activeRoom === -1) { + return Object.values(state.devices); + } else { + const deviceArray = [ + ...state.rooms[state.active.activeRoom].devices, + ].sort(); + return deviceArray.map((id) => state.devices[id]); + } + }, + get isActiveRoomHome() { + return state.active.activeRoom === -1; + }, + activeRoom: state.active.activeRoom, +}); +const DevicePanelContainer = connect( + mapStateToProps, + RemoteService +)(DevicePanel); +export default DevicePanelContainer; diff --git a/smart-hut/src/components/dashboard/NewSceneDevice.js b/smart-hut/src/components/dashboard/NewSceneDevice.js new file mode 100644 index 0000000..cef04f9 --- /dev/null +++ b/smart-hut/src/components/dashboard/NewSceneDevice.js @@ -0,0 +1,144 @@ +import React, { Component } from "react"; +import { Button, Modal, Icon, Image, Form, Dropdown } from "semantic-ui-react"; +import { connect } from "react-redux"; +import { RemoteService } from "../../remote"; +import styled from "styled-components"; +//import { appActions } from "../../storeActions"; + +const StyledDiv = styled.div` + background-color: #505bda; + padding: 3rem; + width: 10rem; + height: 10rem; + border-radius: 100%; + border: none; + position: relative; + box-shadow: 3px 2px 10px 5px #ccc; + transition: all 0.3s ease-out; + :hover { + background-color: #4345d9; + } + :active { + transform: translate(0.3px, 0.8px); + box-shadow: 0.5px 0.5px 7px 3.5px #ccc; + } +`; + +class NewSceneDevice extends Component { + constructor(props) { + super(props); + + this.state = { + openModal: false, + sceneDevices: this.props.scene ? this.props.scene.sceneStates : {}, + deviceName: "", + }; + this.getDevices(); + + this.setSceneState = this.setSceneState.bind(this); + this.createState = this.createState.bind(this); + } + + getDevices() { + this.props + .fetchDevices() + .catch((err) => console.error(`error fetching devices:`, err)); + } + + handleOpen = () => { + this.setState({ openModal: true }); + }; + handleClose = () => { + this.setState({ openModal: false }); + }; + + resetState = () => { + this.setState(this.baseState); + this.handleClose(); + }; + + setSceneState(e, d) { + this.setState({ devicesAttached: d.value }); + } + + createState() { + const device = this.props.devices.filter( + (e) => this.state.devicesAttached[0] === e.id + ); + let data = { + sceneId: this.props.activeScene, + id: device[0].id, + kind: device[0].kind, + }; + this.props + .saveState(data) + .catch((err) => console.error("error in creating state", err)); + this.resetState(); + } + + render() { + const availableDevices = []; + this.props.devices.forEach((e) => { + if (!Object.keys(this.state.sceneDevices).find((d) => e.id === d)) { + if (e.flowType === "OUTPUT") { + availableDevices.push({ + key: e.id, + text: e.name, + value: e.id, + }); + } + } + }); + return ( + + + + } + centered={true} + > + Add a New Scene State + +
+ + + + +
+
+ + + +
+ ); + } +} + +const mapStateToProps = (state, _) => ({ + devices: Object.values(state.devices), + activeScene: state.active.activeScene, +}); +const NewSceneDeviceContainer = connect( + mapStateToProps, + RemoteService +)(NewSceneDevice); +export default NewSceneDeviceContainer; diff --git a/smart-hut/src/components/dashboard/ScenesPanel.js b/smart-hut/src/components/dashboard/ScenesPanel.js new file mode 100644 index 0000000..d8bcfd5 --- /dev/null +++ b/smart-hut/src/components/dashboard/ScenesPanel.js @@ -0,0 +1,73 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { RemoteService } from "../../remote"; +import Device from "./devices/Device"; +import NewSceneDevice from "./NewSceneDevice"; +import { Grid, Button } from "semantic-ui-react"; + +class ScenesPanel extends Component { + constructor(props) { + super(props); + this.applyScene = this.applyScene.bind(this); + } + + applyScene() { + console.log(this.props.activeScene); + this.props.sceneApply(this.props.activeScene).then(() => { + alert("Scene applied."); + }); + } + + render() { + return ( + + {!this.props.isActiveDefaultScene ? ( + + + + ) : null} + {!this.props.isActiveDefaultScene + ? this.props.sceneStates.map((e, i) => { + return ( + + + + ); + }) + : null} + {!this.props.isActiveDefaultScene ? ( + + + + ) : ( + Welcome to the Scene View, you add a Scene + )} + + ); + } +} + +const mapStateToProps = (state, _) => ({ + get sceneStates() { + if (state.active.activeScene !== -1) { + const stateArray = [ + ...state.scenes[state.active.activeScene].sceneStates, + ].sort(); + console.log("STATESCENE", stateArray); + return stateArray.map((id) => state.sceneStates[id]); + } else { + return []; + } + }, + get isActiveDefaultScene() { + return state.active.activeScene === -1; + }, + activeScene: state.active.activeScene, +}); +const ScenesPanelContainer = connect( + mapStateToProps, + RemoteService +)(ScenesPanel); +export default ScenesPanelContainer; diff --git a/smart-hut/src/components/dashboard/devices/Curtain.js b/smart-hut/src/components/dashboard/devices/Curtain.js new file mode 100644 index 0000000..d475455 --- /dev/null +++ b/smart-hut/src/components/dashboard/devices/Curtain.js @@ -0,0 +1,126 @@ +import React, { Component } from "react"; +import "./Curtains.css"; +import { RemoteService } from "../../../remote"; +import { connect } from "react-redux"; + +class Curtain extends Component { + constructor(props) { + super(props); + this.state = { + intensity: this.props.stateOrDevice.intensity, + timeout: null, + }; + + this.setIntensity = this.setIntensity.bind(this); + } + + //getters + get turnedOn() { + return this.props.stateOrDevice.on; + } + + get intensity() { + return this.props.stateOrDevice.intensity || 0; + } + + onClickDevice = () => { + const on = !this.turnedOn; + if (this.props.tab === "Devices") { + this.props + .saveDevice({ ...this.props.stateOrDevice, on }) + .catch((err) => console.error("curtains update error", err)); + } else { + this.props.updateState( + { id: this.props.stateOrDevice.id, on: on }, + this.props.stateOrDevice.kind + ); + } + }; + + setIntensity(intensity) { + intensity *= 100; + + if (this.state.timeout) { + clearTimeout(this.state.timeout); + } + + this.setState({ + intensity, + timeout: setTimeout(() => { + this.saveIntensity(); + this.setState({ + intensity: this.state.intensity, + timeout: null, + }); + }, 100), + }); + } + + saveIntensity = () => { + const intensity = Math.round(this.state.intensity); + if (this.props.tab === "Devices") { + this.props + .saveDevice({ ...this.props.stateOrDevice, intensity }) + .catch((err) => console.error("curtain update error", err)); + } else { + this.props.updateState( + { id: this.props.stateOrDevice.id, intensity: intensity }, + this.props.stateOrDevice.kind + ); + } + }; + + helper = () => { + if (this.props.device.intensity >= 90) { + this.setIntensity(1); + this.saveIntensity(); + } else { + this.setIntensity(this.props.stateOrDevice.intensity / 100 + 0.1); + this.saveIntensity(); + } + }; + + ///*this took me way too much more time than it should have*/ + + handleChange = (a) => { + this.setIntensity(a.target.value / 100); + this.saveIntensity(); + }; + + render() { + return ( +
+
{" "} + + {Math.round(this.props.stateOrDevice.intensity)}% + + +
+ ); + } +} + +const mapStateToProps = (state, ownProps) => ({ + get stateOrDevice() { + if (state.active.activeTab === "Devices") { + return state.devices[ownProps.id]; + } else { + return state.sceneStates[ownProps.id]; + } + }, + //device: state.devices[ownProps.id], +}); +const CurtainContainer = connect(mapStateToProps, RemoteService)(Curtain); +export default CurtainContainer; diff --git a/smart-hut/src/components/dashboard/devices/Curtains.css b/smart-hut/src/components/dashboard/devices/Curtains.css new file mode 100644 index 0000000..e98e37f --- /dev/null +++ b/smart-hut/src/components/dashboard/devices/Curtains.css @@ -0,0 +1,69 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.container.curtain-container { + position: relative; + margin-top: 10%; + width: 18rem; + height: 9rem; + background-color: #f7f7f7; + border-radius: 5px; + box-shadow: 10px 10px 30px 15px rgba(0, 0, 0, 0.247); +} + +.open-container { + position: absolute; + width: 18rem; + background-color: #f79071; + border-radius: 5px; +} + +.slider { + -webkit-appearance: none; + width: 9rem; + position: absolute; + left: 75%; + top: 50%; + transform: translateY(-50%) rotateZ(90deg); + background: transparent; + outline: none; +} + +.slider::-webkit-slider-runnable-track { + -webkit-appearance: none; + height: 5px; + background-color: #1b1c1d; + border-radius: 50px; + cursor: pointer; +} + +.slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: #1b1c1d; + position: relative; + transition: all; + top: -5.5px; +} + +.slider::-webkit-slider-thumb:hover { + transform: scale(1.2); +} + +.span-open { + -webkit-user-select: none; + font-family: "Lato"; + font-weight: bold; + font-size: 3rem; + text-emphasis: none; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} diff --git a/smart-hut/src/components/dashboard/devices/Device.js b/smart-hut/src/components/dashboard/devices/Device.js new file mode 100644 index 0000000..43efc7f --- /dev/null +++ b/smart-hut/src/components/dashboard/devices/Device.js @@ -0,0 +1,226 @@ +import React from "react"; +import Light from "./Light"; +import SmartPlug from "./SmartPlug"; +import Sensor from "./Sensor"; +import { ButtonDimmer, KnobDimmer } from "./Dimmer"; +import Switcher from "./Switch"; +import Videocam from "./Videocam"; +import Curtains from "./Curtain"; +import Thermostat from "./Thermostats"; +import { Segment, Grid, Header, Button, Icon } from "semantic-ui-react"; +import { RemoteService } from "../../../remote"; +import { connect } from "react-redux"; +import DeviceSettingsModal from "./DeviceSettingsModal"; + +class Device extends React.Component { + constructor(props) { + super(props); + + this.modalRef = React.createRef(); + this.edit = this.edit.bind(this); + this.resetSmartPlug = this.resetSmartPlug.bind(this); + this.deleteState = this.deleteState.bind(this); + } + + edit() { + console.log("editing device with id=" + this.props.id); + this.modalRef.current.openModal(); + } + + resetSmartPlug() { + this.props + .smartPlugReset(this.props.id) + .catch((err) => console.error(`Smart plug reset error`, err)); + } + + deleteState() { + //console.log("alpaca "+this.props); + this.props.deleteState(this.props.id, this.props.type); + } + + renderDeviceComponent() { + switch ( + this.props.tab === "Devices" + ? this.props.stateOrDevice.kind + : this.props.type + ) { + case "curtains": + return ( + + ); + case "thermostat": + return ( + + ); + case "regularLight": + return ( + + ); + case "sensor": + return ( + + ); + case "motionSensor": + return ; + case "buttonDimmer": + return ( + + ); + case "knobDimmer": + return ( + + ); + case "smartPlug": + return ( + + ); + case "switch": + return ( + + ); + case "dimmableLight": + return ; + case "securityCamera": + return ( + + ); + default: + //throw new Error("Device type unknown"); + return undefined; + } + } + + render() { + { + if (this.props.type !== "") { + return ( + + + {this.renderDeviceComponent()} + {this.props.tab === "Devices" ? ( + +
{this.props.stateOrDevice.name}
+ + {this.props.stateOrDevice.kind === "smartPlug" ? ( + + ) : null} +
+ ) : ( + +
{this.props.device.name}
+ {this.props.tab === "Scenes" ? ( +
{this.props.roomName}
+ ) : ( + "" + )} + +
+ )} +
+ {this.props.stateOrDevice && this.props.tab === "Devices" ? ( + + ) : ( + "" + )} +
+ ); + } else { + return null; + } + } + } +} +/* + +{this.props.stateOrDevice ? + : + "" + } +*/ +const mapStateToProps = (state, ownProps) => ({ + get stateOrDevice() { + if (state.active.activeTab === "Devices") { + return state.devices[ownProps.id]; + } else { + return state.sceneStates[ownProps.id]; + } + }, + get device() { + if (state.active.activeTab === "Devices") { + return state.devices[ownProps.id]; + } else { + return state.devices[state.sceneStates[ownProps.id].deviceId]; + } + }, + get roomName() { + if (state.active.activeTab === "Scenes") { + const device = state.devices[state.sceneStates[ownProps.id].deviceId]; + return state.rooms[device.roomId].name; + } else { + return ""; + } + }, + get type() { + console.log("ALPACA", state, ownProps); + if (state.active.activeTab === "Scenes") { + if (state.sceneStates[ownProps.id]) { + //console.log(state.sceneStates[ownProps.id], ownProps.id); + const id = state.sceneStates[ownProps.id].deviceId; + //console.log(id, state.devices[id].kind); + return state.devices[id].kind; + } else { + return ""; + } + } else { + return null; + } + }, +}); +const DeviceContainer = connect(mapStateToProps, RemoteService)(Device); +export default DeviceContainer; diff --git a/smart-hut/src/components/dashboard/devices/DeviceSettings.js b/smart-hut/src/components/dashboard/devices/DeviceSettings.js deleted file mode 100644 index 5967b39..0000000 --- a/smart-hut/src/components/dashboard/devices/DeviceSettings.js +++ /dev/null @@ -1,26 +0,0 @@ -import React, { Component } from "react"; -import { editModeIconStyle, editModeStyle } from "./styleComponents"; - -export default class Settings extends Component { - constructor(props) { - super(props); - this.state = { - displayForm: true, - }; - } - - displayForm = () => { - this.setState((prevState) => ({ displayForm: !prevState.displayForm })); - }; - - render() { - const view = ( -
this.props.edit.openModal(this.props.deviceId)}> - - - -
- ); - return {this.props.edit.mode ? view : ""}; - } -} diff --git a/smart-hut/src/components/dashboard/devices/DeviceSettingsModal.js b/smart-hut/src/components/dashboard/devices/DeviceSettingsModal.js new file mode 100644 index 0000000..162e242 --- /dev/null +++ b/smart-hut/src/components/dashboard/devices/DeviceSettingsModal.js @@ -0,0 +1,135 @@ +import React, { Component, useState } from "react"; +import { Button, Form, Icon, Header, Modal, Input } from "semantic-ui-react"; +import { connect } from "react-redux"; +import { RemoteService } from "../../../remote"; + +const DeleteModal = (props) => ( + + + Delete device + + } + closeIcon + > +
+ + + + + +); + +const SettingsForm = (props) => { + const handleInputChange = (e) => { + const { name, value } = e.target; + setValues({ ...values, [name]: value }); + }; + + const [values, setValues] = useState({ name: "" }); + + return ( +
+ + + + + + + props.removeDevice(values)} /> + + +
+ ); +}; + +class DeviceSettingsModal extends Component { + constructor(props) { + super(props); + this.state = { + open: false, + name: this.props.device.name, + }; + + this.updateDevice = this.updateDevice.bind(this); + this.deleteDevice = this.deleteDevice.bind(this); + } + + closeModal = (e) => { + this.setState({ openModal: false }); + }; + + openModal = (e) => { + this.setState({ openModal: true }); + }; + + updateDevice(values) { + if (values.name.length === 0) return; + this.props + .saveDevice({ ...this.props.device, name: values.name }) + .then(() => this.setState({ openModal: false })) + .catch((err) => + console.error( + `settings modal for device ${this.props.id} deletion error`, + err + ) + ); + } + + deleteDevice() { + this.props + .deleteDevice(this.props.device) + .then(() => this.setState({ open: false })) + .catch((err) => + console.error( + `settings modal for device ${this.props.id} deletion error`, + err + ) + ); + } + + render() { + const SettingsModal = () => ( + + Settings of {this.props.device.name} + + + + + ); + return ; + } +} + +const mapStateToProps = (state, ownProps) => ({ + device: state.devices[ownProps.id], +}); +const DeviceSettingsModalContainer = connect( + mapStateToProps, + RemoteService, + null, + { forwardRef: true } +)(DeviceSettingsModal); +export default DeviceSettingsModalContainer; diff --git a/smart-hut/src/components/dashboard/devices/DeviceTypeController.js b/smart-hut/src/components/dashboard/devices/DeviceTypeController.js deleted file mode 100644 index ba446d1..0000000 --- a/smart-hut/src/components/dashboard/devices/DeviceTypeController.js +++ /dev/null @@ -1,87 +0,0 @@ -import React from "react"; -import Light from "./Light"; -import SmartPlug from "./SmartPlug"; -import Sensor from "./Sensor"; -import { ButtonDimmer, KnobDimmer } from "./Dimmer"; -import Switcher from "./Switch"; - -const DeviceType = (props) => { - switch (props.type) { - case "regularLight": - return ( - - ); - case "sensor": - return ( - - ); - case "motionSensor": - return ( - - ); - case "buttonDimmer": - return ( - - ); - case "knobDimmer": - return ( - - ); - case "smartPlug": - return ( - - ); - case "switch": - return ( - - ); - case "dimmableLight": - return ( - - ); - default: - return ""; - } -}; - -export default DeviceType; diff --git a/smart-hut/src/components/dashboard/devices/DigitalSensor.js b/smart-hut/src/components/dashboard/devices/DigitalSensor.js deleted file mode 100644 index 7268057..0000000 --- a/smart-hut/src/components/dashboard/devices/DigitalSensor.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Users can add sensors in their rooms. - * Sensors typically measure physical quantities in a room. - * You must support temperature sensors, humidity sensors, light sensors (which measure luminosity1). - * Sensors have an internal state that cannot be changed by the user. - * For this story, make the sensors return a constant value with some small random error. - */ - -import React, { Component } from "react"; -import { - CircularInput, - CircularProgress, - CircularTrack, -} from "react-circular-input"; -import { errorStyle, sensorText, style, valueStyle } from "./SensorStyle"; -import { StyledDiv } from "./styleComponents"; -import Settings from "./DeviceSettings"; -import { Image } from "semantic-ui-react"; -import { imageStyle, nameStyle } from "./DigitalSensorStyle"; - -export default class DigitalSensor extends Component { - constructor(props) { - super(props); - this.state = { - value: false, // This value is a boolean, was this type of sensor returns presence/absence - }; - - this.iconOn = "/img/sensorOn.svg"; - this.iconOff = "/img/sensorOff.svg"; - } - - setName = () => { - if (this.props.device.name.length > 15) { - return this.props.device.name.slice(0, 12) + "..."; - } - return this.props.device.name; - }; - - getIcon = () => { - if (this.state.value) { - return this.iconOn; - } - return this.iconOff; - }; - - componentDidMount() {} - - render() { - return ( - - - this.props.onChangeData(id, newSettings) - } - /> - -
{this.props.device.name}
-
- ); - } -} diff --git a/smart-hut/src/components/dashboard/devices/DigitalSensorStyle.js b/smart-hut/src/components/dashboard/devices/DigitalSensorStyle.js deleted file mode 100644 index f83c954..0000000 --- a/smart-hut/src/components/dashboard/devices/DigitalSensorStyle.js +++ /dev/null @@ -1,17 +0,0 @@ -export const imageStyle = { - width: "3.5rem", - height: "auto", - position: "absolute", - top: "20%", - left: "50%", - transform: "translateX(-50%)", - filter: "drop-shadow( 1px 1px 0.5px rgba(0, 0, 0, .25))", -}; - -export const nameStyle = { - color: "black", - position: "absolute", - top: "40%", - left: "50%", - transform: "translateX(-50%)", -}; diff --git a/smart-hut/src/components/dashboard/devices/Dimmer.js b/smart-hut/src/components/dashboard/devices/Dimmer.js index 7dd7877..e9d7a72 100644 --- a/smart-hut/src/components/dashboard/devices/Dimmer.js +++ b/smart-hut/src/components/dashboard/devices/Dimmer.js @@ -19,7 +19,6 @@ import { PlusPanel, ThumbText, } from "./styleComponents"; -import Settings from "./DeviceSettings"; import { CircularThumbStyle, KnobDimmerStyle, @@ -28,52 +27,27 @@ import { knobIcon, knobContainer, } from "./DimmerStyle"; +import { RemoteService } from "../../../remote"; +import { connect } from "react-redux"; -import { call } from "../../../client_server"; - -export class ButtonDimmer extends Component { - constructor(props) { - super(props); - this.state = {}; - } - +export class ButtonDimmerComponent extends Component { increaseIntensity = () => { - let data = { - dimType: "UP", - id: this.props.device.id, - }; - call.deviceUpdate(data, "buttonDimmer/dim").then((res) => { - if (res.status === 200) { - } - }); - }; - decreaseIntensity = () => { - let data = { - dimType: "DOWN", - id: this.props.device.id, - }; - call.deviceUpdate(data, "buttonDimmer/dim").then((res) => { - if (res.status === 200) { - } - }); + this.props + .buttonDimmerDim(this.props.id, "UP") + .catch((err) => console.error("button dimmer increase error", err)); }; - componentDidMount() {} + decreaseIntensity = () => { + this.props + .buttonDimmerDim(this.props.id, "DOWN") + .catch((err) => console.error("button dimmer decrease error", err)); + }; render() { return ( - - this.props.onChangeData(id, newSettings) - } - /> icon - - {this.props.device.name} ({this.props.device.id}) - + Button Dimmer + @@ -85,49 +59,51 @@ export class ButtonDimmer extends Component { } } -export class KnobDimmer extends Component { +export class KnobDimmerComponent extends Component { constructor(props) { super(props); + this.state = { - pointingDevices: [], - value: 1, + intensity: this.props.stateOrDevice.intensity || 0, + timeout: null, }; + + this.saveIntensity = this.saveIntensity.bind(this); + this.setIntensity = this.setIntensity.bind(this); } - setIntensity = (newValue) => { - let val = Math.round(newValue * 100) <= 1 ? 1 : Math.round(newValue * 100); - let data = { - id: this.props.device.id, - intensity: val, - }; - call.deviceUpdate(data, "knobDimmer/dimTo").then((res) => { - if (res.status === 200) { - this.setState({ - value: val, - }); - } - }); - }; + setIntensity(intensity) { + intensity *= 100; + + if (this.state.timeout) { + clearTimeout(this.state.timeout); + } - componentDidMount() { this.setState({ - value: 1, + intensity, + timeout: setTimeout(() => { + this.saveIntensity(); + this.setState({ + intensity: this.state.intensity, + timeout: null, + }); + }, 100), }); } + saveIntensity() { + const val = Math.round(this.state.intensity); + this.props + .knobDimmerDimTo(this.props.id, val) + .catch((err) => console.error("knob dimmer set intensity error", err)); + } + render() { return (
- - this.props.onChangeData(id, newSettings) - } - /> - {this.props.device.name} ({this.props.device.id}) + Knob Dimmer @@ -151,3 +127,17 @@ export class KnobDimmer extends Component { ); } } + +const mapStateToProps = (state, ownProps) => ({ + get stateOrDevice(){ + if(state.active.activeTab==="Devices"){ + return state.devices[ownProps.id]; + }else{ + return state.sceneStates[ownProps.id]; + } + }, +}); +const conn = connect(mapStateToProps, RemoteService); + +export const KnobDimmer = conn(KnobDimmerComponent); +export const ButtonDimmer = conn(ButtonDimmerComponent); diff --git a/smart-hut/src/components/dashboard/devices/Light.js b/smart-hut/src/components/dashboard/devices/Light.js index be4e8b3..54af2e7 100644 --- a/smart-hut/src/components/dashboard/devices/Light.js +++ b/smart-hut/src/components/dashboard/devices/Light.js @@ -7,7 +7,6 @@ * Lights have an internal state that can be changed and it must * be shown accordingly in the SmartHut views (house view and room views). */ - import React, { Component } from "react"; import { iconStyle, @@ -15,7 +14,6 @@ import { BottomPanel, ThumbText, } from "./styleComponents"; -import Settings from "./DeviceSettings"; import { Image } from "semantic-ui-react"; import { CircularInput, @@ -31,77 +29,116 @@ import { CircularThumbStyle, knobIcon, } from "./LightStyle"; -import { call } from "../../../client_server"; +import { RemoteService } from "../../../remote"; +import { connect } from "react-redux"; -export default class Light extends Component { +class Light extends Component { constructor(props) { super(props); this.state = { - turnedOn: false, - intensity: props.device.intensity, + intensity: this.props.stateOrDevice.intensity, + timeout: null, }; + this.iconOn = "/img/lightOn.svg"; this.iconOff = "/img/lightOff.svg"; - this.stateCallback = (e) => { - this.setState( - Object.assign(this.state, { - intensity: e.intensity, - turnedOn: e.on, - }) - ); - }; + this.setIntensity = this.setIntensity.bind(this); + } - call.socketSubscribe(this.props.device.id, this.stateCallback); + componentDidUpdate(prevProps, prevState) { + if ( + this.props.stateOrDevice.intensity !== prevProps.stateOrDevice.intensity + ) { + this.setState({ + intensity: this.props.stateOrDevice.intensity, + timeout: null, + }); + } + } + + get turnedOn() { + return this.props.stateOrDevice.on; + } + + get intensity() { + return this.props.stateOrDevice.intensity || 0; } onClickDevice = () => { - this.props.device.on = !this.state.turnedOn; - call.deviceUpdate(this.props.device, "regularLight").then((res) => { - if (res.status === 200) { - this.setState((prevState) => ({ turnedOn: !prevState.turnedOn })); + const on = !this.turnedOn; + if (this.props.tab === "Devices") { + this.props + .saveDevice({ ...this.props.stateOrDevice, on }) + .catch((err) => console.error("regular light update error", err)); + } else { + if (this.props.device.kind === "regularLight") { + this.props + .updateState( + { + id: this.props.stateOrDevice.id, + on: on, + sceneId: this.props.stateOrDevice.sceneId, + }, + this.props.stateOrDevice.kind + ) + .then((res) => { + console.log(res); + }); } - }); + } }; getIcon = () => { - if (this.state.turnedOn) { - return this.iconOn; + return this.turnedOn ? this.iconOn : this.iconOff; + }; + + setIntensity(intensity) { + intensity *= 100; + + if (this.state.timeout) { + clearTimeout(this.state.timeout); } - return this.iconOff; - }; - setIntensity = (newValue) => { - this.props.device.intensity = - Math.round(newValue * 100) <= 1 ? 1 : Math.round(newValue * 100); - call.deviceUpdate(this.props.device, "dimmableLight").then((res) => { - if (res.status === 200) { + this.setState({ + intensity, + timeout: setTimeout(() => { + this.saveIntensity(); this.setState({ - intensity: - Math.round(newValue * 100) <= 1 ? 1 : Math.round(newValue * 100), + intensity: this.state.intensity, + timeout: null, }); - } + }, 100), }); - }; - - get intensity() { - return isNaN(this.state.intensity) ? 0 : this.state.intensity; } + saveIntensity = () => { + const intensity = Math.round(this.state.intensity); + if (this.props.tab === "Devices") { + this.props + .saveDevice({ ...this.props.stateOrDevice, intensity }) + .catch((err) => console.error("regular light update error", err)); + } else { + console.log("CIAOOOOOOOOO", this.props.stateOrDevice); + this.props + .updateState( + { id: this.props.stateOrDevice.id, intensity: intensity }, + this.props.stateOrDevice.kind + ) + .then((res) => { + console.log(res, this.props.stateOrDevice.kind); + }); + } + }; + render() { const intensityLightView = (
- - this.props.onChangeData(id, newSettings) - } - /> - {this.props.device.name}
({this.props.device.id}) + Intensity light
@@ -128,19 +165,10 @@ export default class Light extends Component { const normalLightView = ( - - this.props.onChangeData(id, newSettings) - } - /> -
{} : this.onClickDevice}> +
-
- {this.props.device.name} ({this.props.device.id}) -
+
Light
@@ -155,3 +183,23 @@ export default class Light extends Component { ); } } + +const mapStateToProps = (state, ownProps) => ({ + get stateOrDevice() { + if (state.active.activeTab === "Devices") { + return state.devices[ownProps.id]; + } else { + return state.sceneStates[ownProps.id]; + } + }, + get device() { + if (state.active.activeTab === "Devices") { + return state.devices[ownProps.id]; + } else { + return state.devices[state.sceneStates[ownProps.id].deviceId]; + } + }, +}); + +const LightContainer = connect(mapStateToProps, RemoteService)(Light); +export default LightContainer; diff --git a/smart-hut/src/components/dashboard/devices/NewDevice.js b/smart-hut/src/components/dashboard/devices/NewDevice.js index 5616ae0..7897c9e 100644 --- a/smart-hut/src/components/dashboard/devices/NewDevice.js +++ b/smart-hut/src/components/dashboard/devices/NewDevice.js @@ -9,6 +9,8 @@ import { Input, Modal, } from "semantic-ui-react"; +import { RemoteService } from "../../../remote"; +import { connect } from "react-redux"; const StyledDiv = styled.div` background-color: #505bda; @@ -29,7 +31,7 @@ const StyledDiv = styled.div` } `; -export default class NewDevice extends Component { +class NewDevice extends Component { constructor(props) { super(props); this.state = { @@ -86,69 +88,79 @@ export default class NewDevice extends Component { this.setState({ lightsAttached: d.value }); }; - createDevice() { + async createDevice() { // Connect to the backend and create device here. const data = { - params: { - name: this.state.deviceName, - }, - device: this.state.motion ? "motionSensor" : this.state.typeOfDevice, + id: null, + roomId: this.props.activeRoom, + name: this.state.deviceName, + kind: this.state.motion ? "motionSensor" : this.state.typeOfDevice, + }; + let outputs = null; + + const defaultNames = { + regularLight: "New regular light", + dimmableLight: "New intensity light", + smartPlug: "New smart Plug", + sensor: "New sensor", + switch: "New switch", + buttonDimmer: "New button dimmer", + knobDimmer: "New knob dimmer", + securityCamera: "New security camera", }; + if (this.state.deviceName === "") { + data.name = defaultNames[this.state.typeOfDevice]; + } + console.log("-------------------------"); + console.log(this.state.typeOfDevice); + switch (this.state.typeOfDevice) { - case "regularLight": - if (this.state.deviceName === "") { - data.params["name"] = "Regular Light"; - } - break; - case "smartPlug": - if (this.state.deviceName === "") { - data.params["name"] = "Smart Plug"; - } + //trying to make securityCamera work + //case "securityCamera": + // data.path="/security_camera_videos/security_camera_1.mp4"; + // data.on=false; + //break; + //trying to make thermostat work + case "thermostat": + data.targetTemperature = 0; + data.measuredTemperature = 0; break; case "dimmableLight": - if (this.state.deviceName === "") { - data.params["name"] = "Dimmable Light"; - } - data.params["intensity"] = 0; + data.intensity = 0; break; case "sensor": - if (this.state.deviceName === "") { - data.params["name"] = "Sensor"; - } if (!this.state.motion) { - data.params["sensor"] = this.state.typeOfSensor; - data.params["value"] = 0; + data.sensor = this.state.typeOfSensor; + data.value = 0; } break; case "switch": - if (this.state.deviceName === "") { - data.params["name"] = "Switch"; - } - data.params["lights"] = this.state.lightsAttached; - break; case "buttonDimmer": - if (this.state.deviceName === "") { - data.params["name"] = "Button Dimmer"; - } - data.params["lights"] = this.state.lightsAttached; - break; case "knobDimmer": - if (this.state.deviceName === "") { - data.params["name"] = "Knob Dimmer"; - } - data.params["lights"] = this.state.lightsAttached; + outputs = this.state.lightsAttached; break; default: break; } - this.props.addDevice(data); - this.resetState(); + try { + let newDevice = await this.props.saveDevice(data); + if (outputs) { + await this.props.connectOutputs(newDevice, outputs); + } + this.resetState(); + } catch (e) { + console.error("device creation error: ", e); + } } render() { const deviceOptions = [ + //stuff + { key: "thermostat", text: "Thermostat", value: "thermostat", image: {} }, + { key: "curtains", text: "Curtain", value: "curtains", image: {} }, + //stuff ends { key: "light", text: "Normal Light", @@ -191,6 +203,12 @@ export default class NewDevice extends Component { value: "buttonDimmer", image: { avatar: true, src: "/img/plusMinus.svg" }, }, + { + key: "securityCamera", + text: "Security Camera", + value: "securityCamera", + image: { avatar: true, src: "/img/plusMinus.svg" }, + }, ]; const sensorOptions = [ { @@ -376,3 +394,10 @@ export default class NewDevice extends Component { ); } } + +const mapStateToProps = (state, _) => ({ + devices: Object.values(state.devices), + activeRoom: state.active.activeRoom, +}); +const NewDeviceContainer = connect(mapStateToProps, RemoteService)(NewDevice); +export default NewDeviceContainer; diff --git a/smart-hut/src/components/dashboard/devices/SecurityCamera b/smart-hut/src/components/dashboard/devices/SecurityCamera new file mode 100644 index 0000000..c1ed4f8 --- /dev/null +++ b/smart-hut/src/components/dashboard/devices/SecurityCamera @@ -0,0 +1,36 @@ +import React, {Component} from "react"; + +import { RemoteService } from "../../../remote"; +import { connect } from "react-redux"; + + + + +class SecurityCamera extends Component{ + constructor (props){ + super(props); + this.state = { path: this.props.device.path, on:this.props.device.on, timeout: null }; + + // this.setIntensity = this.setIntensity.bind(this); + } + + + + render(){ + return

"mode is: "{this.props.device.mode}

+

"internalsensortemperature is: "{this.props.device.internalSensorTemperature}

+

"targetTemperature is: "{this.props.device.targetTemperature}

+

"measuredtemperature is: "{this.props.device.measuredTemperature}

+

{this.props.device.on}

+ +
; + } +} + + +const mapStateToProps = (state, ownProps) => ({ + device: state.devices[ownProps.id], +}); + +const SecurityCameraContainer = connect(mapStateToProps, RemoteService)(SecurityCamera); +export default SecurityCameraContainer; \ No newline at end of file diff --git a/smart-hut/src/components/dashboard/devices/Sensor.js b/smart-hut/src/components/dashboard/devices/Sensor.js index f927ffb..ad80a4c 100644 --- a/smart-hut/src/components/dashboard/devices/Sensor.js +++ b/smart-hut/src/components/dashboard/devices/Sensor.js @@ -35,11 +35,11 @@ import { humiditySensorColors, iconSensorStyle, } from "./SensorStyle"; -import Settings from "./DeviceSettings"; -import { call } from "../../../client_server"; import { Image } from "semantic-ui-react"; +import { RemoteService } from "../../../remote"; +import { connect } from "react-redux"; -export default class Sensor extends Component { +class Sensor extends Component { constructor(props) { super(props); this.state = { @@ -53,48 +53,63 @@ export default class Sensor extends Component { this.colors = temperatureSensorColors; this.icon = "temperatureIcon.svg"; - - call.socketSubscribe(this.props.device.id, this.stateCallback); + this.name = "Sensor"; } - componentWillUnmount() { - call.socketUnsubscribe(this.props.device.id, this.stateCallback); - } + // setName = () => { + // if (this.props.device.name.length > 15) { + // return this.props.device.name.slice(0, 12) + "..."; + // } + // return this.props.device.name; + // }; - setName = () => { - if (this.props.device.name.length > 15) { - return this.props.device.name.slice(0, 12) + "..."; + componentDidUpdate(prevProps) { + if ( + this.props.stateOrDevice.kind === "sensor" && + this.props.stateOrDevice.value !== prevProps.stateOrDevice.value + ) { + this.setState({ value: this.props.stateOrDevice.value }); + } else if ( + this.props.stateOrDevice.kind === "motionSensor" && + this.props.stateOrDevice.detected !== prevProps.stateOrDevice.detected + ) { + this.setState({ + motion: true, + detected: this.props.stateOrDevice.detected, + }); } - return this.props.device.name; - }; + } componentDidMount() { - if (this.props.device.kind === "sensor") { - switch (this.props.device.sensor) { + if (this.props.stateOrDevice.kind === "sensor") { + switch (this.props.stateOrDevice.sensor) { case "TEMPERATURE": this.units = "ºC"; this.colors = temperatureSensorColors; this.icon = "temperatureIcon.svg"; + this.name = "Temperature Sensor"; break; case "HUMIDITY": this.units = "%"; this.colors = humiditySensorColors; this.icon = "humidityIcon.svg"; + this.name = "Humidity Sensor"; break; case "LIGHT": this.units = "lm"; this.colors = lightSensorColors; this.icon = "lightSensorIcon.svg"; + this.name = "Light Sensor"; break; default: this.units = ""; } this.setState({ - value: this.props.device.value, + value: this.props.stateOrDevice.value, }); } else { this.setState({ - detected: this.props.device.detected, + detected: this.props.stateOrDevice.detected, motion: true, }); } @@ -123,7 +138,7 @@ export default class Sensor extends Component { }} > - {this.props.device.name} + Motion Sensor
); @@ -131,20 +146,13 @@ export default class Sensor extends Component { return (
- - this.props.onChangeData(id, newSettings) - } - /> {this.state.motion ? ( ) : ( - {this.setName()} ({this.props.device.id}) + {this.name} @@ -185,3 +193,15 @@ export default class Sensor extends Component { ); } } + +const mapStateToProps = (state, ownProps) => ({ + get stateOrDevice() { + if (state.active.activeTab === "Devices") { + return state.devices[ownProps.id]; + } else { + return state.sceneStates[ownProps.id]; + } + }, +}); +const SensorContainer = connect(mapStateToProps, RemoteService)(Sensor); +export default SensorContainer; diff --git a/smart-hut/src/components/dashboard/devices/SettingsModal.js b/smart-hut/src/components/dashboard/devices/SettingsModal.js deleted file mode 100644 index 1c460d2..0000000 --- a/smart-hut/src/components/dashboard/devices/SettingsModal.js +++ /dev/null @@ -1,111 +0,0 @@ -import React, { Component, useState } from "react"; -import { Button, Checkbox, Form, Icon, Header, Modal } from "semantic-ui-react"; - -const DeleteModal = (props) => ( - Remove} closeIcon> -
- - - - - -); - -const SettingsForm = (props) => { - const handleInputChange = (e) => { - const { name, value } = e.target; - setValues({ ...values, [name]: value }); - }; - - const handleCheckboxChange = (e, d) => { - const { name, checked } = d; - setValues({ ...values, [name]: checked }); - }; - - const [values, setValues] = useState({ name: "" }); - - return ( -
- - - - - - {props.type === "smart-plug" ? ( - - - - ) : ( - "" - )} - - props.removeDevice(values)} /> - - -
- ); -}; - -export default class SettingsModal extends Component { - constructor(props) { - super(props); - this.state = { - open: true, - }; - } - - handleClose = () => { - this.setState({ open: false }); - }; - - saveSettings = (device) => { - // TODO Here there should be all the connections to save the data in the backend - console.log("SAVED: ", device); - if (device.name.length > 0) { - this.props.updateDevice(device); - } - - this.props.openModal(); - }; - - render() { - const SettingsModal = () => ( - - Settings of {this.props.device.name} - - - - - ); - return ; - } -} diff --git a/smart-hut/src/components/dashboard/devices/SmartPlug.js b/smart-hut/src/components/dashboard/devices/SmartPlug.js index f5d3513..c14773c 100644 --- a/smart-hut/src/components/dashboard/devices/SmartPlug.js +++ b/smart-hut/src/components/dashboard/devices/SmartPlug.js @@ -5,13 +5,7 @@ The user can reset this value. **/ import React, { Component } from "react"; -import { - BottomPanel, - StyledDiv, - editModeIconStyle, - editModeStyleLeft, -} from "./styleComponents"; -import Settings from "./DeviceSettings"; +import { BottomPanel, StyledDiv } from "./styleComponents"; import { Image } from "semantic-ui-react"; import { energyConsumedStyle, @@ -19,99 +13,71 @@ import { kwhStyle, nameStyle, } from "./SmartPlugStyle"; -import { call } from "../../../client_server"; +import { RemoteService } from "../../../remote"; +import { connect } from "react-redux"; -export default class SmartPlug extends Component { +class SmartPlug extends Component { constructor(props) { super(props); - this.state = { - turnedOn: false, - energyConsumed: 0, // kWh - }; this.iconOn = "/img/smart-plug.svg"; this.iconOff = "/img/smart-plug-off.svg"; - - this.stateCallback = (e) => { - this.setState( - Object.assign(this.state, { - energyConsumed: (e.totalConsumption / 1000).toFixed(3), - turnedOn: e.on, - }) - ); - }; - - call.socketSubscribe(this.props.device.id, this.stateCallback); } - componentWillUnmount() { - call.socketUnsubscribe(this.props.device.id, this.stateCallback); + get turnedOn() { + return this.props.stateOrDevice.on; } + + get energyConsumed() { + return (this.props.stateOrDevice.totalConsumption / 1000).toFixed(3); + } + onClickDevice = () => { - this.props.device.on = !this.state.turnedOn; - call.deviceUpdate(this.props.device, "smartPlug").then((res) => { - if (res.status === 200) { - this.setState((prevState) => ({ turnedOn: !prevState.turnedOn })); - } - }); - }; - - resetSmartPlug = () => { - call.smartPlugReset(this.props.device.id).then((res) => { - if (res.status === 200) { - this.setState({ - energyConsumed: (res.data.totalConsumption / 1000).toFixed(3), - }); - } - }); + const on = !this.turnedOn; + if (this.props.tab === "Devices") { + this.props + .saveDevice({ ...this.props.stateOrDevice, on }) + .catch((err) => console.error("smart plug update error", err)); + } else { + this.props.updateState( + { id: this.props.stateOrDevice.id, on: on }, + this.props.stateOrDevice.kind + ); + } }; getIcon = () => { - if (this.state.turnedOn) { - return this.iconOn; - } - return this.iconOff; + return this.turnedOn ? this.iconOn : this.iconOff; }; - componentDidMount() { - this.setState({ - turnedOn: this.props.device.on, - energyConsumed: (this.props.device.totalConsumption / 1000).toFixed(3), - }); - } - render() { return ( - {} : this.onClickDevice}> - - this.props.onChangeData(id, newSettings) - } - /> - {this.props.edit.mode ? ( - - - - ) : ( - "" - )} + - - {this.props.device.name} ({this.props.device.id}) - + Smart Plug - {this.state.energyConsumed} + {this.energyConsumed} KWh ); } } + +const mapStateToProps = (state, ownProps) => ({ + get stateOrDevice() { + if (state.active.activeTab === "Devices") { + return state.devices[ownProps.id]; + } else { + return state.sceneStates[ownProps.id]; + } + }, +}); +const SmartPlugContainer = connect(mapStateToProps, RemoteService)(SmartPlug); +export default SmartPlugContainer; diff --git a/smart-hut/src/components/dashboard/devices/Switch.js b/smart-hut/src/components/dashboard/devices/Switch.js index 848d43f..113c1f1 100644 --- a/smart-hut/src/components/dashboard/devices/Switch.js +++ b/smart-hut/src/components/dashboard/devices/Switch.js @@ -7,82 +7,56 @@ import React, { Component } from "react"; import { BottomPanel, StyledDiv } from "./styleComponents"; -import Settings from "./DeviceSettings"; import { Image } from "semantic-ui-react"; import { imageStyle, nameStyle, turnedOnStyle } from "./SwitchStyle"; -import { call } from "../../../client_server"; +import { RemoteService } from "../../../remote"; +import { connect } from "react-redux"; -export default class Switch extends Component { +class Switch extends Component { constructor(props) { super(props); - this.state = { - turnedOn: false, - pointingLights: [], - }; this.iconOn = "/img/switchOn.svg"; this.iconOff = "/img/switchOff.svg"; } + get turnedOn() { + return this.props.device.on; + } + getIcon = () => { - if (this.state.turnedOn) { - return this.iconOn; - } - return this.iconOff; + return this.turnedOn ? this.iconOn : this.iconOff; }; onClickDevice = () => { - this.props.device.on = !this.state.turnedOn; - let state = ""; - if (this.props.device.on) { - state = "ON"; - } else { - state = "OFF"; - } - let data = { - type: state, - id: this.props.device.id, - }; - call.deviceUpdate(data, "switch/operate").then((res) => { - if (res.status === 200) { - this.setState((prevState) => ({ turnedOn: !prevState.turnedOn })); - } - }); + const newOn = !this.turnedOn; + const type = newOn ? "ON" : "OFF"; + this.props + .switchOperate(this.props.id, type) + .catch((err) => console.error("switch operate failed", err)); }; - componentDidMount() { - this.setState({ - turnedOn: this.props.device.on, - }); - } - render() { return ( - {} : this.onClickDevice}> - - this.props.onChangeData(id, newSettings) - } - /> - + - - {this.props.device.name} ({this.props.device.id}) - + Switch - - {this.state.turnedOn ? "ON" : "OFF"} - + {this.turnedOn ? "ON" : "OFF"} ); } } + +const mapStateToProps = (state, ownProps) => ({ + device: state.devices[ownProps.id], +}); +const SwitchContainer = connect(mapStateToProps, RemoteService)(Switch); +export default SwitchContainer; diff --git a/smart-hut/src/components/dashboard/devices/Thermostat.css b/smart-hut/src/components/dashboard/devices/Thermostat.css new file mode 100644 index 0000000..9c7097b --- /dev/null +++ b/smart-hut/src/components/dashboard/devices/Thermostat.css @@ -0,0 +1,38 @@ +.slider-css { + -webkit-appearance: none; + width: 20rem; + font-family: "Lato"; + position: absolute; + margin-top: 27%; + margin-left: 50%; + transform: translate(-50%, -50%); +} + +.slider-css::-webkit-slider-thumb { + -webkit-appearance: none; + border: 5px solid #ffffff; + width: 18px; + height: 18px; + border-radius: 10px; + background-color: rgba(94, 246, 152, 1); + cursor: pointer; + box-shadow: 1px 1px 15px 2px rgba(0, 0, 0, 0.4); + margin-top: -7px; +} + +.slider-css:focus { + -webkit-appearance: none; + + outline: none; +} + +.slider-css::-webkit-slider-runnable-track { + -webkit-appearance: none; + outline: none; + width: 100%; + height: 7px; + cursor: pointer; + box-shadow: 4.5px 4.5px 20px 1px rgba(0, 0, 0, 0.3); + background: white; + border-radius: 5px; +} diff --git a/smart-hut/src/components/dashboard/devices/ThermostatStyle.js b/smart-hut/src/components/dashboard/devices/ThermostatStyle.js new file mode 100644 index 0000000..934f258 --- /dev/null +++ b/smart-hut/src/components/dashboard/devices/ThermostatStyle.js @@ -0,0 +1,73 @@ +export const container = { + position: "relative", + width: "25rem", + height: "12rem", + boxShadow: "22px 18px 54px -7px rgba(102,102,102,1)", + borderRadius: "30px", + backgroundColor: "white", +}; + +export const line = { + width: "250px", + height: "47px", + borderBottom: "1px solid #646464", + position: "absolute", + left: "5%", +}; + +export const deviceName = { + fontFamily: "Lato", + position: "absolute", + marginTop: "5%", + marginLeft: "8%", + fontSize: "1rem", + fontWeight: "bold", + color: "#646464", +}; + +export const targetTemperature = { + fontFamily: "Lato", + position: "absolute", + marginTop: "20%", + marginLeft: "50%", + transform: "translate(-50%,-50%)", + fontSize: "2rem", + fontWeight: "bold", + color: "#646464", +}; + +export const slider = { + width: "25rem", + fontFamily: "Lato", + position: "absolute", + marginTop: "35%", + marginLeft: "50%", + transform: "translate(-50%,-50%)", +}; + +export const stateTagContainer = { + textAlign: "center", + position: "absolute", + //width: "3rem", + height: "2rem", + marginTop: "35%", + marginLeft: "50%", + transform: "translate(-50%,-50%)", + padding: "0.5rem 4rem", + backgroundColor: "rgba(94,246,152,1)", + borderRadius: "50px", +}; + +export const stateTag = { + fontFamily: "Lato", + fontSize: "1.2rem", + color: "white", + textTransform: "uppercase", +}; + +export const toggle = { + position: "absolute", + top: "10%", + left: "35%", + transform: "rotate(-360deg)", +}; diff --git a/smart-hut/src/components/dashboard/devices/Thermostats.js b/smart-hut/src/components/dashboard/devices/Thermostats.js new file mode 100644 index 0000000..4a628b7 --- /dev/null +++ b/smart-hut/src/components/dashboard/devices/Thermostats.js @@ -0,0 +1,224 @@ +import React, { Component } from "react"; +import { Checkbox } from "semantic-ui-react"; +import { RemoteService } from "../../../remote"; +import { connect } from "react-redux"; +import "./Thermostat.css"; +import { + stateTag, + container, + deviceName, + targetTemperature, + slider, + line, + toggle, + stateTagContainer, +} from "./ThermostatStyle"; + +//not quite working yet +class Thermostats extends Component { + constructor(props) { + super(props); + this.state = { + targetTemperature: this.props.stateOrDevice.targetTemperature, + internalSensorTemperature: this.props.stateOrDevice + .internalSensorTemperature, + mode: this.props.stateOrDevice.mode, + measuredTemperature: this.props.stateOrDevice.measuredTemperature, + useExternalSensors: this.props.stateOrDevice.useExternalSensors, + timeout: null, + }; + this.setMode = this.setMode.bind(this); + this.setTargetTemperature = this.setTargetTemperature.bind(this); + this.setInternalSensorTemperature = this.setInternalSensorTemperature.bind( + this + ); + } + + //getters + get getMode() { + return this.props.stateOrDevice.mode; + } + + get getTargetTemperature() { + return this.props.stateOrDevice.targetTemperature; + } + + get getInternalSensorTemperature() { + return this.props.stateOrDevice.internalSensorTemperature; + } + + get getMeasuredTemperature() { + return this.props.stateOrDevice.measuredTemperature; + } + + get getUseExternalSensors() { + return this.props.stateOrDevice.useExternalSensors; + } + + setMode(mode) { + if (this.state.timeout) { + clearTimeout(this.state.timeout); + } + console.log(mode); + + //i came to the conclusion that is not possible to set mode. + //this.mode = "HEATING"; + const turnOn = mode; + console.log(turnOn); + if (this.props.tab === "Devices") { + this.props + .saveDevice({ ...this.props.stateOrDevice, turnOn }) + .catch((err) => console.error("thermostat update error", err)); + } else { + this.props.updateState( + { id: this.props.stateOrDevice.id, turnOn: turnOn }, + this.props.stateOrDevice.kind + ); + } + } + + onClickDevice = () => { + const on = !this.turnedOn; + if (this.props.tab === "Devices") { + this.props + .saveDevice({ ...this.props.stateOrDevice, on }) + .catch((err) => console.error("thermostat update error", err)); + } else { + this.props.updateState( + { id: this.props.stateOrDevice.id, on: on }, + this.props.stateOrDevice.kind + ); + } + }; + + //It seems to work + saveTargetTemperature(targetTemperature) { + const turn = this.props.stateOrDevice.mode !== "OFF" ? true : false; + if (this.props.tab === "Devices") { + this.props + .saveDevice({ + ...this.props.stateOrDevice, + targetTemperature, + turnOn: turn, + }) + .catch((err) => console.error("thermostat update error", err)); + } else { + this.props.updateState( + { + id: this.props.stateOrDevice.id, + targetTemperature: targetTemperature, + }, + this.props.stateOrDevice.kind + ); + } + } + + setTargetTemperature(newTemp) { + if (this.state.timeout) { + clearTimeout(this.state.timeout); + } + + this.setState({ + newTemp, + timeout: setTimeout(() => { + this.saveTargetTemperature(newTemp); + this.setState({ + targetTemperature: this.state.targetTemperature, + timeout: null, + }); + }, 100), + }); + } + + //I have no idea why it doesn't want to update the temperature + saveInternalSensorTemperature(internalSensorTemperature) { + if (this.props.tab === "Devices") { + this.props + .saveDevice({ ...this.props.device, internalSensorTemperature }) + .catch((err) => console.error("thermostat update error", err)); + } else { + this.props.updateState( + { + id: this.props.stateOrDevice.id, + internalSensorTemperature: internalSensorTemperature, + }, + this.props.stateOrDevice.kind + ); + } + } + + setInternalSensorTemperature(newTemp) { + if (this.state.timeout) { + clearTimeout(this.state.timeout); + } + + this.setState({ + newTemp, + timeout: setTimeout(() => { + this.saveInternalSensorTemperature(newTemp); + this.setState({ + internalSensorTemperature: this.state.internalSensorTemperature, + timeout: null, + }); + }, 100), + }); + } + + helperMode = () => { + //this.setMode("HEATING"); + this.setTargetTemperature(20); + //this.setInternalSensorTemperature(42); + }; + + handleChange = (value) => { + this.setTargetTemperature(value); + }; + + render() { + return ( +
+

{this.props.stateOrDevice.name}

+
+ this.setMode(val.checked)} + /> + + + {this.props.stateOrDevice.targetTemperature}ºC + + this.handleChange(event.target.value)} + id="targetTemperature" + /> +
+ {this.props.stateOrDevice.mode} +
+
+ ); + } +} + +const mapStateToProps = (state, ownProps) => ({ + get stateOrDevice() { + if (state.active.activeTab === "Devices") { + return state.devices[ownProps.id]; + } else { + return state.sceneStates[ownProps.id]; + } + }, +}); + +const ThermostatContainer = connect( + mapStateToProps, + RemoteService +)(Thermostats); +export default ThermostatContainer; diff --git a/smart-hut/src/components/dashboard/devices/Videocam.js b/smart-hut/src/components/dashboard/devices/Videocam.js new file mode 100644 index 0000000..889233f --- /dev/null +++ b/smart-hut/src/components/dashboard/devices/Videocam.js @@ -0,0 +1,65 @@ +// vim: set ts=2 sw=2 et tw=80: + +import React, { Component } from "react"; +import { StyledDivCamera } from "./styleComponents"; +import { Grid } from "semantic-ui-react"; +import { RemoteService } from "../../../remote"; +import { endpointURL } from "../../../endpoint"; +import { connect } from "react-redux"; +import VideocamModal from "./VideocamModal"; + +class Videocam extends Component { + constructor(props) { + super(props); + this.state = { selectedVideo: undefined }; + } + + openModal = () => { + this.setState((state) => { + return { selectedVideo: true }; + }); + }; + + closeModal = () => { + this.setState((state) => { + return { selectedVideo: undefined }; + }); + }; + + get url() { + return endpointURL() + this.props.device.path; + } + + render() { + const VideocamView = ( +
+ +
+ +
+
+ + + + + + + +
+ ); + + return
{VideocamView}
; + } +} + +const mapStateToProps = (state, ownProps) => ({ + device: state.devices[ownProps.id], +}); +const VideocamContainer = connect(mapStateToProps, RemoteService)(Videocam); +export default VideocamContainer; diff --git a/smart-hut/src/components/dashboard/devices/VideocamModal.js b/smart-hut/src/components/dashboard/devices/VideocamModal.js new file mode 100644 index 0000000..7a1e553 --- /dev/null +++ b/smart-hut/src/components/dashboard/devices/VideocamModal.js @@ -0,0 +1,35 @@ +import React from "react"; +import Modal from "react-modal"; +import { Button } from "semantic-ui-react"; + +const ModalStyle = { + content: { + top: "20%", + left: "45%", + right: "auto", + bottom: "auto", + marginRight: "-40%", + width: "80%", + transform: "translate(-40%, -10%)", + }, +}; + +const VideocamModal = (props) => ( + + {props.selectedVideo && ( + + )} + + +); + +export default VideocamModal; diff --git a/smart-hut/src/components/dashboard/devices/styleComponents.js b/smart-hut/src/components/dashboard/devices/styleComponents.js index 9c052f7..7815ce7 100644 --- a/smart-hut/src/components/dashboard/devices/styleComponents.js +++ b/smart-hut/src/components/dashboard/devices/styleComponents.js @@ -18,11 +18,19 @@ export const editButtonStyle = { }; export const panelStyle = { - position: "relative", backgroundColor: "#fafafa", - height: "100vh", - width: "auto", + height: "85vh", padding: "0rem 3rem", + color: "#000000", + overflow: "auto", + maxHeight: "75vh", +}; + +export const mobilePanelStyle = { + backgroundColor: "#fafafa", + minHeight: "100vh", + padding: "0rem 3rem", + color: "#000000", }; export const editModeStyle = { @@ -114,6 +122,25 @@ export const StyledDiv = styled.div` } `; +export const StyledDivCamera = styled.div` + cursor: pointer; + background-color: white; + padding: 0.5rem; + margin-bottom: 0.5rem; + border: none; + position: relative; + box-shadow: 3px 2px 10px 5px #ccc; + transition: all 0.3s ease-out; + text-align: center; + :hover { + background-color: #f2f2f2; + } + :active { + transform: translate(0.3px, 0.8px); + box-shadow: 0.5px 0.5px 7px 3.5px #ccc; + } +`; + export const ButtonDimmerContainer = styled.div` background-color: white; padding: 3rem; diff --git a/smart-hut/src/endpoint.js b/smart-hut/src/endpoint.js new file mode 100644 index 0000000..6dbc3b0 --- /dev/null +++ b/smart-hut/src/endpoint.js @@ -0,0 +1,19 @@ +/** + * Returns the endpoint URL (SmartHut backend URL) + * @returns {String} endpoint URL + */ +export function endpointURL() { + return window.BACKEND_URL !== "__BACKEND_URL__" + ? window.BACKEND_URL + : "http://localhost:8080"; +} + +export function socketURL(token) { + const httpURL = new URL(endpointURL()); + const isSecure = httpURL.protocol === "https:"; + const protocol = isSecure ? "wss:" : "ws:"; + const port = httpURL.port || (isSecure ? 443 : 80); + const url = `${protocol}//${httpURL.hostname}:${port}/sensor-socket?token=${token}`; + console.log("socket url: ", url); + return url; +} diff --git a/smart-hut/src/index.js b/smart-hut/src/index.js index 12af31f..111847a 100644 --- a/smart-hut/src/index.js +++ b/smart-hut/src/index.js @@ -2,10 +2,14 @@ import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; import * as serviceWorker from "./serviceWorker"; -//React Router -//import { BrowserRouter, Route, Switch } from "react-router-dom"; +import { Provider } from "react-redux"; +import smartHutStore from "./store"; -const index = ; +const index = ( + + + +); ReactDOM.render(index, document.getElementById("root")); serviceWorker.unregister(); diff --git a/smart-hut/src/remote.js b/smart-hut/src/remote.js new file mode 100644 index 0000000..c238c56 --- /dev/null +++ b/smart-hut/src/remote.js @@ -0,0 +1,881 @@ +import smartHutStore from "./store"; +import actions from "./storeActions"; +import axios from "axios"; +import { endpointURL, socketURL } from "./endpoint"; +import { connect, disconnect } from "@giantmachines/redux-websocket"; + +/** + * 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; + + constructor(messages) { + super(messages.join(" - ")); + this.messages = messages; + } +} + +const Endpoint = { + axiosInstance: axios.create({ + baseURL: endpointURL(), + validateStatus: (status) => status >= 200 && status < 300, + }), + + /** + * Returns token for current session, null if logged out + * @returns {String|null} the token + */ + get token() { + return smartHutStore.getState().login.token; + }, + + /** + * 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 + */ + send: (method, route, query = {}, body = null) => { + if (!Endpoint.token) { + throw new Error("No token while performing authenticated request"); + } + + return Endpoint.axiosInstance(route, { + method: method, + params: query, + data: ["put", "post"].indexOf(method) !== -1 ? body : null, + headers: { + Authorization: `Bearer ${Endpoint.token}`, + }, + }).then((res) => { + if (!res.data && method !== "delete") { + console.error("Response body is empty"); + return null; + } else { + return res; + } + }); + }, + + /** + * 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 + */ + sendNA: (method, route, query = {}, body = null) => { + return Endpoint.axiosInstance(route, { + method: method, + params: query, + data: ["put", "post"].indexOf(method) !== -1 ? body : null, + }).then((res) => { + if (!res.data) { + console.error("Response body is empty"); + return null; + } else { + return res; + } + }); + }, + + /** + * Performs login + * @param {String} usernameOrEmail + * @param {String} password + * @returns {Promise} promise that resolves to the token string + * and rejects to the axios error. + */ + login: (usernameOrEmail, password) => { + return Endpoint.axiosInstance + .post(`/auth/login`, { + usernameOrEmail, + password, + }) + .then((res) => { + localStorage.setItem("token", res.data.jwttoken); + localStorage.setItem("exp", new Date().getTime() + 5 * 60 * 60 * 1000); + return res.data.jwttoken; + }); + }, + + /** + * Returns an immediately resolved promise for the socket logouts + * @return {Promise} An always-resolved promise + */ + logout: () => { + localStorage.removeItem("token"); + localStorage.removeItem("exp"); + return Promise.resolve(void 0); + }, + + /** + * 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 + */ + get(route, query = {}) { + return this.send("get", route, query); + }, + + /** + * 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 + */ + post(route, query, body) { + return this.send("post", route, query, body); + }, + + /** + * 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) { + return this.sendNA("post", route, query, body); + }, + + /** + * 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 + */ + put(route, query = {}, body = {}) { + return this.send("put", route, query, body); + }, + + /** + * 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 = {}) { + return this.sendNA("put", route, query, body); + }, + + /** + * 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 + */ + delete(route, query = {}) { + return this.send("delete", route, query); + }, +}; + +/** + * 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 + */ +function parseValidationErrors(err) { + if ( + err.response && + err.response.status === 400 && + err.response.data && + Array.isArray(err.response.data.errors) + ) { + throw new RemoteError([ + ...new Set(err.response.data.errors.map((e) => e.defaultMessage)), + ]); + } else { + console.warn("Non validation error", err); + throw new RemoteError(["Network error"]); + } +} + +export const RemoteService = { + /** + * Performs login + * @param {String} usernameOrEmail + * @param {String} password + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + login: (usernameOrEmail, password) => { + return (dispatch) => { + return Endpoint.login(usernameOrEmail, password) + .then((token) => { + dispatch(actions.loginSuccess(token)); + dispatch(connect(socketURL(token))); + }) + .catch((err) => { + console.warn("login error", err); + throw new RemoteError([ + err.response && err.response.status === 401 + ? "Wrong credentials" + : "An error occurred while logging in", + ]); + }); + }; + }, + + /** + * Performs logout + */ + logout: () => { + return (dispatch) => + Endpoint.logout().then(() => { + dispatch(disconnect()); + dispatch(actions.logout()); + }); + }, + + /** + * Fetches user information via REST calls, if it is logged in + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + fetchUserInfo: () => { + return (dispatch) => { + return Endpoint.get("/auth/profile") + .then((res) => void dispatch(actions.userInfoUpdate(res.data))) + .catch((err) => { + console.warn("Fetch user info error", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * Fetches all rooms that belong to this user. This call does not + * populate the devices attribute in rooms. + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + fetchAllRooms: () => { + return (dispatch) => { + return Endpoint.get("/room") + .then((res) => void dispatch(actions.roomsUpdate(res.data))) + .catch((err) => { + console.error("Fetch all rooms error", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * Fetches all scenes that belong to this user. This call does not + * populate the devices attribute in scenes. + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + fetchAllScenes: () => { + return (dispatch) => { + return Endpoint.get("/scene") + .then((res) => void dispatch(actions.scenesUpdate(res.data))) + .catch((err) => { + console.error("Fetch all scenes error", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * Fetches all devices in a particular room, or fetches all devices. + * This also updates the devices attribute on values in the map rooms. + * @param {Number|null} roomId the rsoom to which fetch devices + * from, null to fetch from all rooms + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + fetchDevices: (roomId = null) => { + return (dispatch) => { + return Endpoint.get(roomId ? `/room/${roomId}/device` : "/device") + .then((res) => void dispatch(actions.devicesUpdate(roomId, res.data))) + .catch((err) => { + console.error(`Fetch devices roomId=${roomId} error`, err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * Fetches all the automations + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + fetchAutomations: () => { + return (dispatch) => { + return Endpoint.get("/automation/") + .then((res) => { + const length = res.data.length; + const automations = []; + + res.data.forEach((a, index) => { + const { id, name } = a; + const automation = { + name, + id, + triggers: [], + scenes: [], + }; + + return Endpoint.get(`/booleanTrigger/${id}`).then((res) => { + automation.triggers.push(...res.data); + return Endpoint.get(`/rangeTrigger/${id}`).then((res) => { + automation.triggers.push(...res.data); + return Endpoint.get(`/scenePriority/${id}`).then((res) => { + automation.scenes.push(...res.data); + automations.push(automation); + if (index + 1 === length) { + return void dispatch( + actions.automationsUpdate(automations) + ); + } + }); + }); + }); + }); + }) + .catch((err) => { + console.error(`Fetch automations error`, err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * 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} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + fetchStates: (sceneId) => { + return (dispatch) => { + return Endpoint.get(`/scene/${sceneId}/states`) + .then((res) => void dispatch(actions.statesUpdate(sceneId, res.data))) + .catch((err) => { + console.error(`Fetch devices sceneId=${sceneId} error`, err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * 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} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + saveRoom: (data, roomId = null) => { + return (dispatch) => { + data = { + name: data.name, + icon: data.icon, + image: data.image, + }; + + return (roomId + ? Endpoint.put(`/room/${roomId}`, {}, data) + : Endpoint.post(`/room`, {}, data) + ) + .then((res) => void dispatch(actions.roomSave(res.data))) + .catch(parseValidationErrors); + }; + }, + + /** + * 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 + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + saveScene: (data, sceneId = null) => { + return (dispatch) => { + data = { + name: data.name, + }; + + return (sceneId + ? Endpoint.put(`/scene/${sceneId}`, {}, data) + : Endpoint.post(`/scene`, {}, data) + ) + .then((res) => void dispatch(actions.sceneSave(res.data))) + .catch(parseValidationErrors); + }; + }, + + // + updateState: (data, type) => { + return (dispatch) => { + let url; + if (data.on) { + url = "/switchableState"; + } else { + url = "/dimmableState"; + } + + return Endpoint.put(url, {}, data) + .then((res) => { + dispatch(actions.stateSave(res.data)); + return res.data; + }) + .catch((err) => { + console.warn("Update device: ", data, "error: ", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + deleteState: (id, type) => { + return (dispatch) => { + let url; + if (type === "dimmableState") { + url = "/dimmableState"; + } else { + url = "/switchableState"; + } + return Endpoint.delete(url + `/${id}`) + .then((_) => dispatch(actions.stateDelete(id))) + .catch((err) => { + console.warn("state delete error", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + sceneApply: (id) => { + return (dispatch) => { + let url = `/scene/${id}/apply`; + + return Endpoint.post(url) + .then((res) => dispatch(actions.deviceOperationUpdate(res.data))) + .catch((err) => { + console.warn("scene apply error", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * 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 "/" + * endpoints are used for creation. + * @param {Device} data the device to update. + * @returns {Promise} promise that resolves to the saved device and rejects + * with user-fiendly errors as a RemoteError + */ + saveDevice: (data) => { + return (dispatch) => { + let url = "/device"; + if ((data.id && data.flowType === "OUTPUT") || !data.id) { + url = "/" + data.kind; + } + + return Endpoint[data.id ? "put" : "post"](url, {}, data) + .then((res) => { + dispatch(actions.deviceSave(res.data)); + return res.data; + }) + .catch((err) => { + console.warn("Update device: ", data, "error: ", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * 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. + * @param {Automation} data the device to update. + * @returns {Promise} promise that resolves to the saved device and rejects + * with user-fiendly errors as a RemoteError + */ + saveAutomation: (data) => { + const { automation, triggerList, order } = data; + console.log("automation: ", automation, triggerList, order); + automation.triggers = []; + automation.scenes = []; + return (dispatch) => { + let urlAutomation = "/automation"; + let urlBooleanTrigger = "/booleanTrigger"; + let urlRangeTrigger = "/rangeTrigger"; + let urlScenePriority = "/scenePriority"; + + let rangeTriggerList = triggerList.filter((trigger) => + trigger.hasOwnProperty("operand") + ); + let booleanTriggerList = triggerList.filter( + (trigger) => !trigger.hasOwnProperty("operand") + ); + + return Endpoint["post"](urlAutomation, {}, automation).then( + async (automationRes) => { + const { id } = automationRes.data; + // Introduce the range triggers in the automation + for (let t of rangeTriggerList) { + const trigger = { + automationId: id, + deviceId: t.device, + operator: t.operand, + range: t.value, + }; + let resRange = await Endpoint.post(urlRangeTrigger, {}, trigger); + automation.triggers.push(resRange.data); + } + + for (let t of booleanTriggerList) { + const trigger = { + automationId: id, + deviceId: t.device, + on: t.value, + }; + let resBoolean = await Endpoint.post( + urlBooleanTrigger, + {}, + trigger + ); + automation.triggers.push(resBoolean.data); + console.log("TRIGGERS: ", automation); + } + + for (let [priority, sceneId] of order.entries()) { + const scenePriority = { + automationId: id, + priority, + sceneId, + }; + let resScenes = await Endpoint["post"]( + urlScenePriority, + {}, + scenePriority + ); + automation.scenes.push(resScenes.data); + } + automation.id = id; + dispatch(actions.automationSave(automation)); + } + ); + }; + }, + + /** + * 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 "/" + * endpoints are used for creation. + * @param {State} data the device to update. + * @returns {Promise} promise that resolves to the saved device and rejects + * with user-fiendly errors as a RemoteError + */ + saveState: (data) => { + return (dispatch) => { + let url = + "/" + data.kind + "/" + data.id + "/state?sceneId=" + data.sceneId; + + return Endpoint["post"](url, {}, data) + .then((res) => { + dispatch(actions.stateSave(res.data)); + return res.data; + }) + .catch((err) => { + console.warn("Update device: ", data, "error: ", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * 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} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + connectOutputs: (newDevice, outputs) => { + return (dispatch) => { + let url = `/${newDevice.kind}/${newDevice.id}/lights`; + + return Endpoint.post(url, {}, outputs) + .then((res) => { + dispatch(actions.deviceOperationUpdate(res.data)); + return res.data; + }) + .catch((err) => { + console.warn( + "ConnectOutputs of ", + newDevice.id, + " with outputs: ", + outputs, + "error: ", + err + ); + throw new RemoteError(["Network error"]); + }); + }; + }, + + _operateInput: (url, getUrl, action) => { + return (dispatch) => { + return Endpoint.put(url, {}, action) + .then(async (res) => { + const inputDevice = await Endpoint.get(getUrl); + delete inputDevice.outputs; + dispatch( + actions.deviceOperationUpdate([...res.data, inputDevice.data]) + ); + }) + .catch((err) => { + console.warn(`${url} error`, err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * 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} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + switchOperate: (switchId, type) => { + return RemoteService._operateInput( + "/switch/operate", + `/switch/${switchId}`, + { + type: type.toUpperCase(), + id: switchId, + } + ); + }, + + /** + * 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} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + knobDimmerDimTo: (dimmerId, intensity) => { + return RemoteService._operateInput( + "/knobDimmer/dimTo", + `/knobDimmer/${dimmerId}`, + { + intensity, + id: dimmerId, + } + ); + }, + + /** + * 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} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + buttonDimmerDim: (dimmerId, dimType) => { + return RemoteService._operateInput( + "/buttonDimmer/dim", + `/buttonDimmer/${dimmerId}`, + { + dimType, + id: dimmerId, + } + ); + }, + + /** + * Resets the meter on a smart plug + * + * @param {Number} smartPlugId the smart plug to reset + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + smartPlugReset(smartPlugId) { + return (dispatch) => { + return Endpoint.delete(`/smartPlug/${smartPlugId}/meter`) + .then((res) => dispatch(actions.deviceOperationUpdate([res.data]))) + .catch((err) => { + console.warn(`Smartplug reset error`, err); + throw new RemoteError(["Network error"]); + }); + }; + }, + /** + * Deletes a room + * @param {Number} roomId the id of the room to delete + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + deleteRoom: (roomId) => { + return (dispatch) => { + return Endpoint.delete(`/room/${roomId}`) + .then((_) => dispatch(actions.roomDelete(roomId))) + .catch((err) => { + console.warn("Room deletion error", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + deleteAutomation: (id) => { + console.log("ID OF AUTO ", id); + return (dispatch) => { + return Endpoint.delete(`/automation/${id}`) + .then((_) => dispatch(actions.automationDelete(id))) + .catch((err) => { + console.warn("Automation deletion error", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * Deletes a scene + * @param {Number} sceneId the id of the scene to delete + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + deleteScene: (sceneId) => { + return (dispatch) => { + return Endpoint.delete(`/scene/${sceneId}`) + .then((_) => dispatch(actions.sceneDelete(sceneId))) + .catch((err) => { + console.warn("Scene deletion error", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + + /** + * Deletes a device + * @param {Device} device the device to delete + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + deleteDevice: (device) => { + return (dispatch) => { + return Endpoint.delete(`/${device.kind}/${device.id}`) + .then((_) => dispatch(actions.deviceDelete(device.id))) + .catch((err) => { + console.warn("Device deletion error", err); + throw new RemoteError(["Network error"]); + }); + }; + }, +}; + +for (const key in RemoteService) { + RemoteService[key] = RemoteService[key].bind(RemoteService); +} + +export class Forms { + /** + * 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} promise that resolves to void and rejects + * with validation errors as a String array + */ + static submitRegistration(data) { + return Endpoint.postNA( + "/register", + {}, + { + username: data.username, + password: data.password, + name: data.name, + email: data.email, + } + ) + .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} promise that resolves to void and rejects + * with validation errors as a String array + */ + static submitInitResetPassword(email) { + return Endpoint.postNA( + "/register/init-reset-password", + {}, + { + email: email, + } + ) + .then((_) => void 0) + .catch((err) => { + console.warn("Init reset password failed", err); + throw new RemoteError(["Network error"]); + }); + } + + /** + * 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 + * @param {String} password the new password + * @returns {Promise} promise that resolves to void and rejects + * with validation errors as a String array + */ + static submitResetPassword(confirmationToken, password) { + return Endpoint.putNA( + "/register/reset-password", + {}, + { + confirmationToken, + password, + } + ) + .then((_) => void 0) + .catch(parseValidationErrors); + } +} diff --git a/smart-hut/src/store.js b/smart-hut/src/store.js new file mode 100644 index 0000000..eade971 --- /dev/null +++ b/smart-hut/src/store.js @@ -0,0 +1,564 @@ +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] = {}; + 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 "SCENES_UPDATE": + newState = previousState; + for (const scene of action.scenes) { + createOrUpdateScene(scene); + } + break; + case "STATES_UPDATE": + //console.log(action.sceneStates); + 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 = 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; + 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].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 "AUTOMATION_UPDATE": + newState = previousState; + 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 "AUTOMATION_SAVE": + console.log("ID: ", action.automation.id); + 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]) { + console.log("PREVSTATE", change, previousState); + 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); + console.log("NEWSTATE ", newState); + 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] }, + }; + + console.log("CHANGE ", change); + console.log("Action id: ", action.id); + newState = update(previousState, change); + console.log("NEW STATE ", newState); + 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 "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 "SCENE_APPLY": + console.log(action); + //checking that the scene actually exists + /*if (!(action.sceneId in previousState.scenes)) { + console.warn(`Scene ${action.sceneId} does not exist`); + break; + }*/ + + 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); + + newState = reducer(previousState, { + type: "DEVICES_UPDATE", + partial: true, + devices, + }); + break; + default: + console.warn(`Action type ${action.type} unknown`, action); + return previousState; + } + console.log("THETRUEALPACA", newState, action.type, action); + 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; diff --git a/smart-hut/src/storeActions.js b/smart-hut/src/storeActions.js new file mode 100644 index 0000000..86ac83e --- /dev/null +++ b/smart-hut/src/storeActions.js @@ -0,0 +1,120 @@ +const actions = { + loginSuccess: (token) => ({ + type: "LOGIN_UPDATE", + login: { + loggedIn: true, + token, + }, + }), + logout: () => ({ + type: "LOGOUT", + }), + userInfoUpdate: (userInfo) => ({ + type: "USER_INFO_UPDATE", + userInfo, + }), + roomSave: (room) => ({ + type: "ROOM_SAVE", + room, + }), + sceneSave: (scene) => ({ + type: "SCENE_SAVE", + scene, + }), + deviceSave: (device) => ({ + type: "DEVICE_SAVE", + device, + }), + triggerSave: (automation) => ({ + type: "TRIGGER_SAVE", + automation, + }), + + scenePrioritySave: (automation) => ({ + type: "SCENE_PRIORITY_SAVE", + automation, + }), + + automationSave: (automation) => ({ + type: "AUTOMATION_SAVE", + automation, + }), + automationsUpdate: (automations) => ({ + type: "AUTOMATION_UPDATE", + automations, + }), + stateSave: (sceneState) => ({ + type: "STATE_SAVE", + sceneState, + }), + statesUpdate: (sceneId, sceneStates) => ({ + type: "STATES_UPDATE", + sceneId, + sceneStates, + }), + devicesUpdate: (roomId, devices, partial = false) => ({ + type: "DEVICES_UPDATE", + roomId, + devices, + partial, + }), + stateDelete: (stateId) => ({ + type: "STATE_DELETE", + stateId, + }), + sceneApply: (sceneId) => ({ + type: "SCENE_APPLY", + sceneId, + }), + deviceOperationUpdate: (devices) => ({ + type: "DEVICES_UPDATE", + devices, + partial: true, + }), + roomsUpdate: (rooms) => ({ + type: "ROOMS_UPDATE", + rooms, + }), + roomDelete: (roomId) => ({ + type: "ROOM_DELETE", + roomId, + }), + automationDelete: (id) => ({ + type: "AUTOMATION_DELETE", + id, + }), + sceneDelete: (sceneId) => ({ + type: "SCENE_DELETE", + sceneId, + }), + scenesUpdate: (scenes) => ({ + type: "SCENES_UPDATE", + scenes, + }), + deviceDelete: (deviceId) => ({ + type: "DEVICE_DELETE", + deviceId, + }), +}; + +export const appActions = { + // -1 for home view + setActiveRoom: (activeRoom = -1) => ({ + type: "SET_ACTIVE_ROOM", + activeRoom, + }), + setActiveTab: (activeTab) => ({ + type: "SET_ACTIVE_TAB", + activeTab, + }), + setActiveScene: (activeScene = -1) => ({ + type: "SET_ACTIVE_SCENE", + activeScene, + }), + setActiveAutomations: (activeAutomation = -1) => ({ + type: "SET_ACTIVE_AUTOMATION", + activeAutomation, + }), +}; + +export default actions; diff --git a/smart-hut/src/views/AutomationsNavbar.js b/smart-hut/src/views/AutomationsNavbar.js new file mode 100644 index 0000000..3378a90 --- /dev/null +++ b/smart-hut/src/views/AutomationsNavbar.js @@ -0,0 +1,165 @@ +import React, { Component } from "react"; +import { + Menu, + Button, + Grid, + Icon, + Responsive, + Dropdown, +} from "semantic-ui-react"; +import { editButtonStyle } from "../components/dashboard/devices/styleComponents"; +import AutomationModal from "../components/AutomationModal"; +import { RemoteService } from "../remote"; +import { connect } from "react-redux"; +import { appActions } from "../storeActions"; + +class AutomationsNavbar extends Component { + constructor(props) { + super(props); + this.state = { + editMode: false, + }; + + this.toggleEditMode = this.toggleEditMode.bind(this); + this.openCurrentModalMobile = this.openCurrentModalMobile.bind(this); + } + + get activeItemAutomation() { + return this.props.activeAutomation; + } + + set activeItemAutomation(item) { + this.props.setActiveAutomation(item); + } + + get activeItemAutomationsName() { + if (this.props.activeAutomation === -1) return "Home"; + return this.props.automations[this.props.activeAutomation].name; + } + + openCurrentModalMobile() { + console.log(this.activeItemAutomation, this.props.automationsModalRefs); + const currentModal = this.props.automationsModalRefs[ + this.activeItemAutomation + ].current; + currentModal.openModal(); + } + + toggleEditMode(e) { + this.setState((prevState) => ({ editMode: !prevState.editMode })); + } + + render() { + return ( +
+ + + + + + + + + + + + + + AUTOMATIONS + + + + { + //INSERT LIST OF AUTOMATIONS HERE + } + + + + + + + + + + + + + + + + + + + + + + + Automations + + + + + { + //INSERT LIST OF AUTOMATIONS HERE + } + + + + + + + + + {this.activeItemAutomation !== -1 ? ( + + + + ) : null} + + + +
+ ); + } +} + +const setActiveAutomation = (activeAutomation) => { + return (dispatch) => + dispatch(appActions.setActiveAutomation(activeAutomation)); +}; + +const mapStateToProps = (state, _) => ({ + automations: state.automations, + activeAutomation: state.active.activeAutomation, + automationModalRefs: Object.keys(state.automations).reduce( + (acc, key) => ({ ...acc, [key]: React.createRef() }), + {} + ), +}); +const AutomationsNavbarContainer = connect(mapStateToProps, { + ...RemoteService, + setActiveAutomation, +})(AutomationsNavbar); +export default AutomationsNavbarContainer; diff --git a/smart-hut/src/views/ConfirmForgotPassword.js b/smart-hut/src/views/ConfirmForgotPassword.js index 99b194b..1f4d0ec 100644 --- a/smart-hut/src/views/ConfirmForgotPassword.js +++ b/smart-hut/src/views/ConfirmForgotPassword.js @@ -1,79 +1,40 @@ import React, { Component } from "react"; -import HomeNavbar from "./../components/HomeNavbar"; -import { Image, Divider, Message, Grid } from "semantic-ui-react"; - -class Paragraph extends Component { - state = { visible: true }; - - handleDismiss = () => { - this.setState({ visible: false }); - - setTimeout(() => { - this.setState({ visible: true }); - }, 2000); - }; +import { + Image, + Grid, + Button, + Icon, + Header, + Container, +} from "semantic-ui-react"; +export default class ConfirmForgotPasswrod extends Component { render() { - if (this.state.visible) { - return ( - - ); - } - return ( -

-
- The message will return in 2s -
-
-

+ + + + +
+ Link has been sent! +
+ +

+ An E-mail has been sent to your address, please follow the + instructions to create a new password. If you don't find the + E-mail please check also the spam folder. +

+
+
+
+
); } } - -const MessageReg = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default class ConfirmForgotPasswrod extends React.Component { - render() { - return ; - } -} diff --git a/smart-hut/src/views/ConfirmRegistration.js b/smart-hut/src/views/ConfirmRegistration.js index 13af1b5..05adedd 100644 --- a/smart-hut/src/views/ConfirmRegistration.js +++ b/smart-hut/src/views/ConfirmRegistration.js @@ -1,79 +1,40 @@ import React, { Component } from "react"; -import HomeNavbar from "./../components/HomeNavbar"; -import { Image, Divider, Message, Grid } from "semantic-ui-react"; - -class Paragraph extends Component { - state = { visible: true }; - - handleDismiss = () => { - this.setState({ visible: false }); - - setTimeout(() => { - this.setState({ visible: true }); - }, 2000); - }; +import { + Image, + Grid, + Button, + Icon, + Header, + Container, +} from "semantic-ui-react"; +export default class ConfirmRegistration extends Component { render() { - if (this.state.visible) { - return ( - - ); - } - return ( -

-
- The message will return in 2s -
-
-

+ + + + +
+ Congratulation! +
+ +

+ An E-mail has been sent to your address, confirm your + registration by following the enclosed link. If you don't find + the E-mail please check also the spam folder. +

+
+
+
+
); } } - -const MessageReg = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default class ConfirmRegistration extends React.Component { - render() { - return ; - } -} diff --git a/smart-hut/src/views/ConfirmResetPassword.js b/smart-hut/src/views/ConfirmResetPassword.js index 0d6123d..75c0bdc 100644 --- a/smart-hut/src/views/ConfirmResetPassword.js +++ b/smart-hut/src/views/ConfirmResetPassword.js @@ -1,79 +1,36 @@ import React, { Component } from "react"; -import HomeNavbar from "./../components/HomeNavbar"; -import { Image, Divider, Message, Grid } from "semantic-ui-react"; - -class Paragraph extends Component { - state = { visible: true }; - - handleDismiss = () => { - this.setState({ visible: false }); - - setTimeout(() => { - this.setState({ visible: true }); - }, 2000); - }; - - render() { - if (this.state.visible) { - return ( - - - Congratulations! - - Your password has been successfully reset - - ); - } - - return ( -

-
- The message will return in 2s -
-
-

- ); - } -} - -const MessageReg = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); +import { + Image, + Grid, + Button, + Icon, + Header, + Container, +} from "semantic-ui-react"; export default class ConfirmResetPassword extends Component { render() { - return ; + return ( + + + + +
+ Congratulation! +
+ +

Your password has been successfully reset.

+
+
+
+
+ ); } } diff --git a/smart-hut/src/views/Dashboard.js b/smart-hut/src/views/Dashboard.js index 2f75f32..1d97806 100644 --- a/smart-hut/src/views/Dashboard.js +++ b/smart-hut/src/views/Dashboard.js @@ -1,222 +1,125 @@ import React, { Component } from "react"; import DevicePanel from "../components/dashboard/DevicePanel"; +import ScenesPanel from "../components/dashboard/ScenesPanel"; +import AutomationsPanel from "../components/dashboard/AutomationsPanel"; import Navbar from "./Navbar"; +import ScenesNavbar from "./ScenesNavbar"; +import AutomationsNavbar from "./AutomationsNavbar"; import MyHeader from "../components/HeaderController"; +import { Grid, Responsive, Button } from "semantic-ui-react"; +import { + panelStyle, + mobilePanelStyle, +} from "../components/dashboard/devices/styleComponents"; -import { call } from "../client_server"; -import { Grid, Responsive } from "semantic-ui-react"; -/* - rooms -> actual rooms - activeItem -> the current room in view - devices -> current device in current room view +import { RemoteService } from "../remote"; +import { connect } from "react-redux"; +import { appActions } from "../storeActions"; - - id of Home is -1 -*/ - -export default class Dashboard extends Component { +class Dashboard extends Component { constructor(props) { super(props); - this.state = { - rooms: [], - activeItem: -1, - devices: [], - image: "", - tkn: this.props.tkn, - }; - - this.addRoom = this.addRoom.bind(this); - this.deleteRoom = this.deleteRoom.bind(this); - this.updateRoom = this.updateRoom.bind(this); - this.addDevice = this.addDevice.bind(this); - this.handleItemClick = this.handleItemClick.bind(this); + this.state = this.initialState; + this.setInitialState(); + this.activeTab = "Automations"; //TODO Remove this to not put automations first + this.selectTab = this.selectTab.bind(this); } - getDevices() { - if (this.state.activeItem === -1) { - call - .getAllDevices() - .then((res) => { - if (res.status === 200) { - //console.log(res.data); - this.setState({ - devices: res.data, - }); - } - }) - .catch((err) => { - //console.log(err); - }); - } else { - call - .getAllDevicesByRoom(this.state.activeItem) - .then((res) => { - if (res.status === 200) { - //console.log(res.data); - this.setState({ - devices: res.data, - }); - } - }) - .catch((err) => {}); + get initialState() { + return { + activeTab: this.activeTab, + }; + } + + setInitialState() { + this.setState(this.initialState); + } + + get activeTab() { + return this.props.activeTab; + } + + set activeTab(tab) { + this.props.setActiveTab(tab); + } + + selectTab(e, { name }) { + this.setState({ activeTab: name }); + this.activeTab = name; + } + + renderTab(tab) { + switch (tab) { + case "Devices": + return ; + case "Scenes": + return ; + case "Automations": + return ; + default: + return

ERROR

; } } - getRooms() { - call - .getAllRooms(this.props.tkn) - .then((res) => { - this.setState({ - rooms: res.data, - }); - }) - .catch((err) => {}); - } - - componentDidMount() { - this.getRooms(); - this.getDevices(); - } - - addRoom(data) { - call - .createRoom(data) - .then((res) => { - if (res.status === 200 && res.data) { - this.getRooms(); - this.handleItemClick(-1); - } - }) - .catch((err) => { - //console.log(err); - }); - } - - deleteRoom() { - let data = { - id: this.state.activeItem, - }; - call - .deleteRoom(data) - .then((res) => { - //remove room in state.rooms - this.getRooms(); - this.setState({ - activeItem: -1, - }); - this.handleItemClick(-1); - }) - .catch((err) => { - //console.log(err); - }); - } - - updateRoom(data) { - data.id = this.state.activeItem; - call - .updateRoom(data) - .then((res) => { - if (res.status === 200 && res.data) { - this.getRooms(); - this.forceUpdate(); - } - }) - .catch((err) => {}); - } - - handleItemClick(id) { - this.setState({ - activeItem: id, - }); - if (id === -1) { - call - .getAllDevices(this.props.tkn) - .then((res) => { - if (res.status === 200) { - this.setState({ - devices: res.data, - }); - } - }) - .catch((err) => { - //console.log(err); - }); - } else { - call - .getAllDevicesByRoom(id, this.props.tkn) - .then((res) => { - if (res.status === 200) { - this.setState({ - devices: res.data, - }); - this.forceUpdate(); - } - }) - .catch((err) => { - //console.log(err); - }); + renderNavbar(tab) { + switch (tab) { + case "Devices": + return ; + case "Scenes": + return ; + case "Automations": + return ; + default: + return

ERROR

; } - this.state.rooms.forEach((item) => { - if (item.id === id) { - this.setState({ room: item }); - } - }); } - addDevice(data) { - data.params["roomId"] = this.state.activeItem; - call - .devicePost(data, this.props.tkn) - .then((res) => { - this.getDevices(); - }) - .catch((err) => {}); - } - - updateDeviceUi = (data) => { - let ds = this.state.devices; - ds.forEach((e) => { - if (e.id === data.id) { - e = data; - } - }); - this.setState({ - devices: ds, - }); - - return; - }; - render() { return ( -
+
- + + + + + + + @@ -63,61 +72,51 @@ class Navbar extends Component { - HOME + House View - {this.props.rooms - ? this.props.rooms.map((e, i) => { - return ( - - - - - - - {e.name} - - {this.state.editMode ? ( - - ) : null} - - - - - ); - }) - : null} + {Object.values(this.props.rooms).map((e, i) => { + return ( + + + + + + + {e.name} + + {this.state.editMode ? ( + + ) : null} + + + + + ); + })} - + - + @@ -128,14 +127,14 @@ class Navbar extends Component { - + @@ -147,55 +146,46 @@ class Navbar extends Component { - {this.props.rooms - ? this.props.rooms.map((e, i) => { - if (!this.roomRefs[e.id]) - this.roomRefs[e.id] = React.createRef(); - return ( - - - - - - - {e.name} - - - - - ); - }) - : null} + {Object.values(this.props.rooms).map((e, i) => { + return ( + + + + + + + {e.name} + + + + + ); + })} - + - {this.state.activeItem !== -1 ? ( + {this.activeItem !== -1 ? ( + + + + + SCENES + + + {Object.values(this.props.scenes).map((e, i) => { + return ( + + + + {e.name} + + {this.state.editMode ? ( + + ) : null} + + + + + ); + })} + + + + + + + + + + + + + + + + + + + + + Scene + + + + + {Object.values(this.props.scenes).map((e, i) => { + return ( + + + + {e.name} + + + + + ); + })} + + + + + + + + + {this.activeItemScene !== -1 ? ( + + + + ) : null} + + + +
+ ); + } +} + +const setActiveScene = (activeScene) => { + return (dispatch) => dispatch(appActions.setActiveScene(activeScene)); +}; + +const mapStateToProps = (state, _) => ({ + scenes: state.scenes, + sceneModalRefs: Object.keys(state.scenes).reduce( + (acc, key) => ({ ...acc, [key]: React.createRef() }), + {} + ), + get isActiveDefaultScene() { + return state.active.activeScene === -1; + }, + activeScene: state.active.activeScene, +}); +const ScenesNavbarContainer = connect(mapStateToProps, { + ...RemoteService, + setActiveScene, +})(ScenesNavbar); +export default ScenesNavbarContainer; diff --git a/smart-hut/src/views/Signup.js b/smart-hut/src/views/Signup.js index 625dd80..5ad8af5 100644 --- a/smart-hut/src/views/Signup.js +++ b/smart-hut/src/views/Signup.js @@ -10,7 +10,7 @@ import { Message, } from "semantic-ui-react"; import { Redirect } from "react-router-dom"; -import { call } from "../client_server"; +import { Forms } from "../remote"; export default class Signup extends Component { constructor(props) { @@ -34,20 +34,11 @@ export default class Signup extends Component { username: this.state.username, }; - call - .register(params) - .then((res) => { - if (res.status === 200 && res.data) { - this.setState({ success: true }); - } - }) - .catch((err) => { - //console.log(err); - let errs = [ - ...new Set(err.response.data.errors.map((e) => e.defaultMessage)), - ]; - this.setState({ error: { state: true, message: errs } }); - }); + Forms.submitRegistration(params) + .then(() => this.setState({ success: true })) + .catch((err) => + this.setState({ error: { state: true, message: err.messages } }) + ); }; onChangeHandler = (event) => { diff --git a/smart-hut/src/views/Videocam.js b/smart-hut/src/views/Videocam.js new file mode 100644 index 0000000..45905ec --- /dev/null +++ b/smart-hut/src/views/Videocam.js @@ -0,0 +1,12 @@ +import React from "react"; +import VideoTest from "../components/VideoTest"; + +export default class TestHeaderController extends React.Component { + render() { + return ( +
+ ; +
+ ); + } +} diff --git a/smart-hut/yarn.lock b/smart-hut/yarn.lock index 9f36482..4364834 100644 --- a/smart-hut/yarn.lock +++ b/smart-hut/yarn.lock @@ -973,6 +973,13 @@ resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== +"@giantmachines/redux-websocket@^1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@giantmachines/redux-websocket/-/redux-websocket-1.1.7.tgz#8c045a359cd3f9a73ef141ce722fd14ae754cd1b" + integrity sha512-t90k+NcVInXvppMVpU3c7ZC6i58S/jBPqltckAlKfrtc92YmGZ/He3qYT9OiemlvS+/d+R6P/Ed4yEqKVevYdg== + dependencies: + redux "~4" + "@hapi/address@2.x.x": version "2.1.4" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" @@ -4236,7 +4243,7 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -exenv@^1.2.2: +exenv@^1.2.0, exenv@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d" integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50= @@ -5011,7 +5018,7 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -5228,6 +5235,13 @@ immer@1.10.0: resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== +immutability-helper@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-3.0.2.tgz#e9187158b47c93368a92e84c31714c4b3dff30b0" + integrity sha512-fcrJ26wpvUcuGRpoGY4hyQ/JOeR1HAunMmE3C0XYXSe6plAGtgTlB2S4BzueBANCPrDJ7AByL1yrIRLIlVfwpA== + dependencies: + invariant "^2.2.4" + import-cwd@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -8504,7 +8518,7 @@ prop-types@15.6.2: loose-envify "^1.3.1" object-assign "^4.1.1" -prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -8747,6 +8761,19 @@ react-is@^16.8.1, react-is@^16.8.4: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== +react-is@^16.9.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-modal@^2.2.2: + version "2.4.1" + resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-2.4.1.tgz#cb09b26711b148eb9f59cb180e1b7d82980ded05" + integrity sha512-3WQCn3xqkbEUvxRUO3nkeqxMNgt1F4CyEU3BiUIrg7C71tnqjQIuSE7+JXp85yFl8X1iRTJouySNpwNqv4kiWg== + dependencies: + exenv "^1.2.0" + prop-types "^15.5.10" + react-popper@^1.3.4: version "1.3.7" resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324" @@ -8760,6 +8787,17 @@ react-popper@^1.3.4: typed-styles "^0.0.7" warning "^4.0.2" +react-redux@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d" + integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA== + dependencies: + "@babel/runtime" "^7.5.5" + hoist-non-react-statics "^3.3.0" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.9.0" + react-round-slider@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/react-round-slider/-/react-round-slider-1.0.1.tgz#2f6f14f4e7ce622cc7e450911a163b5841b3fd88" @@ -8980,6 +9018,19 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +redux-thunk@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" + integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== + +redux@^4.0.5, redux@~4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" + integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + regenerate-unicode-properties@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e" @@ -10100,6 +10151,11 @@ svgo@^1.0.0, svgo@^1.2.2: unquote "~1.1.1" util.promisify "~1.0.0" +symbol-observable@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + symbol-tree@^3.2.2: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"