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: [], order: [], automationName: 'New Automation', editName: false, newTrigger: {}, 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); } 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) { 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'); } const isNotDuplicate = !this.state.triggerList.some( (t) => t.device === trigger.device && t.operand === trigger.operand, ); return { result: isNotDuplicate, message: isNotDuplicate ? null : 'You have already created a trigger for this device with the same conditions', }; } 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 ? ( ); } render() { return (
{this.state.editName ? ( ) : ( this.state.automationName )}
)}
); } } 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;