import React, {Component, useState, useRef} from 'react'; import {connect} from 'react-redux'; import {RemoteService} from '../../remote'; import update from 'immutability-helper'; import './Automations.css'; import { Segment, Grid, Icon, Header, Input, Button, Modal, List, Divider, Menu, Form, Dropdown, Checkbox, } from 'semantic-ui-react'; export 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 operandsRef = useRef(null); const valuesRef = useRef(null); const notAdmitedDevices = ['buttonDimmer']; const hasOperand = new Set([ 'knobDimmer', 'dimmableLight', 'curtains', 'sensor', ]); const deviceList = Object.values(props.devices) .map((device) => ({ key: device.id, text: device.name, value: device.id, kind: device.kind, })) .filter((e) => !notAdmitedDevices.includes(e.kind)); const onChange = (e, val) => { props.inputChange(val); setActiveOperand(hasOperand.has(props.devices[val.value].kind)); if (operandsRef.current) operandsRef.current.setValue(''); if (valuesRef.current) valuesRef.current.inputRef.current.valueAsNumber = undefined; }; return (
{activeOperand ? ( <> props.inputChange(val)} ref={operandsRef} name="operand" compact selection options={operands} /> { props.inputChange(val); }} ref={valuesRef} name="value" type="number" placeholder="Value" /> ) : ( props.inputChange(val)} placeholder="State" name="on" compact selection options={deviceStateOptions} /> )}
); }; const SceneItem = (props) => { const 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, on} = trigger; let symbol; if (operand) { symbol = operands.filter((opt) => opt.key === operand)[0].text; } return ( {deviceName} {operand ? {symbol} : ''} {operand ? value : on ? 'on' : 'off'} onRemove(index)} className="remove-icon" name="remove" /> ); }; class AutomationSaveModal extends Component { constructor(props) { super(props); this.state = { triggerList: [], conditionsList: [], order: [], automationName: 'New Automation', editName: false, newTrigger: {}, newCondition: {}, scenesFilter: null, openModal: false, }; if (this.props.automation) { this.state.automationName = this.props.automation.name; for (const scenePriority of this.props.automation.scenes) { this.state.order[scenePriority.priority] = scenePriority.sceneId; } for (const trigger of this.props.automation.triggers) { this.state.triggerList.push( { device: trigger.deviceId, kind: trigger.kind, ...(trigger.kind === 'booleanTrigger' ? {on: trigger.on} : { operand: trigger.operator, value: trigger.value, }), }, ); } } this.setTrigger = this._setter('triggerList'); this.setOrder = this._setter('order'); this.setautomationName = this._setter('automationName'); this.setEditName = this._setter('editName'); this.setNewTrigger = this._setter('newTrigger'); this.addTrigger = this.addTrigger.bind(this); this.removeTrigger = this.removeTrigger.bind(this); this.onInputChange = this.onInputChange.bind(this); this.searchScenes = this.searchScenes.bind(this); this.orderScenes = this.orderScenes.bind(this); this.onChangeName = this.onChangeName.bind(this); //Conditions this.setNewCondition = this._setter("newCondition"); this.addCondition = this.addCondition.bind(this); this.removeCondition = this.removeCondition.bind(this); } openModal = (e) => { this.setState({openModal: true}); }; closeModal = (e) => { this.setState({openModal: false}); }; get deviceList() { return Object.values(this.props.devices); } _setter(property) { return (value) => this.setState(update(this.state, {[property]: {$set: value}})); } triggerKind(trigger) { if ('operand' in trigger && 'value' in trigger) { return 'rangeTrigger'; } if ('on' in trigger) { return 'booleanTrigger'; } return false; // throw new Error("Trigger kind not handled"); } _checkNewTrigger(trigger, isCondition = false) { const error = { result: false, message: 'There are missing fields!', }; const device = Object.values(this.props.devices).filter( (d) => d.id === trigger.device, )[0]; const triggerKind = this.triggerKind(trigger); if (!device || !triggerKind) { error.message = 'There are missing fields'; return error; } const deviceKind = device.kind; const devicesWithPercentage = ['dimmableLight', 'curtains', 'knobDimmer']; switch (triggerKind) { case 'booleanTrigger': if (!trigger.device || trigger.on === null || trigger.on === undefined) return error; break; case 'rangeTrigger': if (!trigger.device || !trigger.operand || !trigger.value) { return error; } if (trigger.value < 0) { error.message = 'Values cannot be negative'; return error; } // If the device's range is a percentage, values cannot exceed 100 if ( devicesWithPercentage.includes(deviceKind) && trigger.value > 100 ) { error.message = "The value can't exceed 100, as it's a percentage"; return error; } if ( deviceKind === 'sensor' && device.sensor === 'HUMIDITY' && trigger.value > 100 ) { error.message = "The value can't exceed 100, as it's a percentage"; return error; } break; default: throw new Error('theoretically unreachable statement'); } let isNotDuplicate = null; if(isCondition === true){ isNotDuplicate = !this.state.conditionsList.some( (t) => t.device === trigger.device && t.operand === trigger.operand, ); }else{ isNotDuplicate = !this.state.triggerList.some( (t) => t.device === trigger.device && t.operand === trigger.operand, ); } const type = isCondition ? "condition" : "trigger" const duplicationMessage = `You have already created a ${type} for this device with the same conditions`; return { result: isNotDuplicate, message: isNotDuplicate ? null : duplicationMessage }; } addTrigger() { const {result, message} = this._checkNewTrigger(this.state.newTrigger); if (result) { this.setState( update(this.state, { triggerList: {$push: [this.state.newTrigger]}, }), ); } else { alert(message); } } removeTrigger(index) { this.setState( update(this.state, {triggerList: {$splice: [[index, 1]]}}), ); } // This gets triggered when the devices dropdown changes the value. onInputChange(val) { if (val.name === 'device') { this.setNewTrigger({[val.name]: val.value}); } else { this.setNewTrigger({ ...this.state.newTrigger, [val.name]: val.value, }); } } onChangeName(_, val) { this.setautomationName(val.value); } orderScenes = (id, checked) => { if (checked) { this.setState(update(this.state, {order: {$push: [id]}})); } else { this.setState( update(this.state, { order: (prevList) => prevList.filter((e) => e !== id), }), ); } }; searchScenes(_, {value}) { this.setState(update(this.state, {scenesFilter: {$set: value}})); this.forceUpdate(); } get sceneList() { if (!this.scenesFilter) { return this.props.scenes; } return this.props.scenes.filter((e) => e.name.includes(this.scenesFilter)); } _generateKey = (trigger) => { switch (this.triggerKind(trigger)) { case 'booleanTrigger': return `${trigger.device}${trigger.on}`; case 'rangeTrigger': return `${trigger.device}${trigger.operand}${trigger.value}`; default: throw new Error('theoretically unreachable statement'); } }; checkBeforeSave = () => { if (!this.state.automationName) { alert('Give a name to the automation'); return false; } if (this.state.triggerList.length <= 0) { alert('You have to create a trigger'); return false; } if (this.state.order.length <= 0) { alert('You need at least one active scene'); return false; } return true; }; saveAutomation = () => { if (this.checkBeforeSave()) { const automation = { name: this.state.automationName, }; if (this.props.id) { automation.id = this.props.id; automation.triggers = []; automation.scenes = []; automation.conditions = []; for (let i = 0; i < this.state.order.length; i++) { automation.scenes.push({ priority: i, sceneId: this.state.order[i], }); } for (const trigger of this.state.triggerList) { const kind = trigger.kind || this.triggerKind(trigger); automation.triggers.push( { deviceId: trigger.device, kind, ...(kind === 'booleanTrigger' ? {on: trigger.on} : { operator: trigger.operand, range: parseInt(trigger.value), }), }, ); } this.props .fastUpdateAutomation(automation) .then(this.closeModal) .catch(console.error); } else { this.props .saveAutomation({ automation, triggerList: this.state.triggerList, order: this.state.order, }) .then(this.closeModal) .catch(console.error); } } }; get trigger() { return this.props.id ? ( ); } // CONDITIONS addCondition() { // Same method used to check triggers and conditions, not a mistake const {result, message} = this._checkNewTrigger(this.state.newCondition, true); if (result) { this.setState( update(this.state, { conditionsList: {$push: [this.state.newCondition]}, }), ); } else { alert(message); } } removeCondition(index) { this.setState( update(this.state, {conditionsList: {$splice: [[index, 1]]}}), ); } onInputChangeCondition = (val) => { if (val.name === 'device') { this.setNewCondition({[val.name]: val.value}); } else { this.setNewCondition({ ...this.state.newCondition, [val.name]: val.value, }); } } render() { return (
{this.state.editName ? ( ) : ( this.state.automationName )}
)}
Add Conditions
{this.state.conditionsList.length > 0 && this.state.conditionsList.map((condition, i) => { const deviceName = this.deviceList.filter( (d) => d.id === condition.device, )[0].name; const key = this._generateKey(condition); return ( ); })}
); } } const mapStateToProps = (state, ownProps) => ({ scenes: Object.values(state.scenes), devices: state.devices, automation: ownProps.id ? state.automations[ownProps.id] : null, }); const AutomationSaveModalContainer = connect( mapStateToProps, RemoteService, )(AutomationSaveModal); export default AutomationSaveModalContainer;