diff --git a/smart-hut/src/App.js b/smart-hut/src/App.js index 1365896..602e77f 100644 --- a/smart-hut/src/App.js +++ b/smart-hut/src/App.js @@ -69,7 +69,7 @@ class App extends Component { - + diff --git a/smart-hut/src/components/AutomationModal.js b/smart-hut/src/components/AutomationModal.js index 626a6c5..5f8fbd5 100644 --- a/smart-hut/src/components/AutomationModal.js +++ b/smart-hut/src/components/AutomationModal.js @@ -1,9 +1,7 @@ import React, { Component } from "react"; -import { Button, Header, Modal, Icon, Responsive } from "semantic-ui-react"; import { connect } from "react-redux"; import { RemoteService } from "../remote"; import { appActions } from "../storeActions"; -//import { update } from "immutability-helper"; class AutomationModal extends Component { constructor(props) { @@ -84,6 +82,7 @@ class AutomationModal extends Component { render() { return (
+ {/* {!this.props.nicolaStop ? (
@@ -167,7 +166,7 @@ class AutomationModal extends Component { {this.type === "new" ? "Add automation" : "Save changes"} - + */}
); } 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 index 4fbf809..2a7b683 100644 --- a/smart-hut/src/components/dashboard/AutomationsPanel.js +++ b/smart-hut/src/components/dashboard/AutomationsPanel.js @@ -1,20 +1,567 @@ -import React, { Component } from "react"; +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, diff --git a/smart-hut/src/remote.js b/smart-hut/src/remote.js index af0353d..c238c56 100644 --- a/smart-hut/src/remote.js +++ b/smart-hut/src/remote.js @@ -291,7 +291,7 @@ export const RemoteService = { /** * 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 room to which fetch devices + * @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 @@ -307,6 +307,51 @@ export const RemoteService = { }; }, + /** + * 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. @@ -457,6 +502,82 @@ export const RemoteService = { }; }, + /** + * 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, @@ -617,7 +738,6 @@ export const RemoteService = { }); }; }, - /** * Deletes a room * @param {Number} roomId the id of the room to delete @@ -635,6 +755,18 @@ export const RemoteService = { }; }, + 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 diff --git a/smart-hut/src/store.js b/smart-hut/src/store.js index 8b27e6b..eade971 100644 --- a/smart-hut/src/store.js +++ b/smart-hut/src/store.js @@ -255,6 +255,19 @@ function reducer(previousState, action) { 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); @@ -287,8 +300,18 @@ function reducer(previousState, action) { } 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": - console.log("Store", action.sceneState); change = { sceneStates: { [action.sceneState.id]: { $set: action.sceneState } }, }; @@ -337,6 +360,17 @@ function reducer(previousState, action) { 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)) { @@ -454,7 +488,7 @@ function reducer(previousState, action) { break; case "REDUX_WEBSOCKET::MESSAGE": const devices = JSON.parse(action.payload.message); - console.log(devices); + newState = reducer(previousState, { type: "DEVICES_UPDATE", partial: true, diff --git a/smart-hut/src/storeActions.js b/smart-hut/src/storeActions.js index 8b7c155..86ac83e 100644 --- a/smart-hut/src/storeActions.js +++ b/smart-hut/src/storeActions.js @@ -25,6 +25,24 @@ const actions = { 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, @@ -61,6 +79,10 @@ const actions = { type: "ROOM_DELETE", roomId, }), + automationDelete: (id) => ({ + type: "AUTOMATION_DELETE", + id, + }), sceneDelete: (sceneId) => ({ type: "SCENE_DELETE", sceneId, diff --git a/smart-hut/src/views/Dashboard.js b/smart-hut/src/views/Dashboard.js index 651a1e5..1d97806 100644 --- a/smart-hut/src/views/Dashboard.js +++ b/smart-hut/src/views/Dashboard.js @@ -21,7 +21,7 @@ class Dashboard extends Component { super(props); this.state = this.initialState; this.setInitialState(); - + this.activeTab = "Automations"; //TODO Remove this to not put automations first this.selectTab = this.selectTab.bind(this); }