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 (
+
+
+ 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 (
+
+
+
+
+ );
+};
+
+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 (
+
+
+
+ removeAutomation(automation.id)}
+ color="red"
+ size="small"
+ icon={"trash alternate outline"}
+ />
+
+
+
+
+
+
+
+ {triggers !== undefined &&
+ triggers.map((trigger) => {
+ const device = devices.filter(
+ (d) => d.id === trigger.deviceId
+ )[0];
+ return (
+
+ );
+ })}
+
+
+
+
+
+ {scenePriorities !== undefined &&
+ scenePriorities.map((sp) => {
+ const sceneData = scenes.filter(
+ (s) => s.id === sp.sceneId
+ )[0];
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+ );
+};
class AutomationsPanel extends Component {
constructor(props) {
super(props);
+ this.state = { openModal: false };
+ this.getDevices();
+ this.getScenes();
+ this.getAutomations();
}
+ getScenes() {
+ this.props.fetchAllScenes().catch(console.error);
+ }
+
+ getDevices() {
+ this.props
+ .fetchDevices()
+ .catch((err) => console.error(`error fetching devices:`, err));
+ }
+
+ getAutomations() {
+ this.props
+ .fetchAutomations()
+ .catch((err) => console.error(`error fetching automations:`, err));
+ }
+
+ removeAutomation = (id) => {
+ this.props
+ .deleteAutomation(id)
+ .catch((err) => console.error(`error removing automation ${id}:`, err));
+ };
+
render() {
- return
AUTOMATIONS
;
+ return (
+
+
+
+ {!this.state.openModal ? (
+
+
+
+ ) : (
+ CREATE AUTOMATION
+ )}
+
+
+
+ {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);
}