import { Button, Checkbox, DialogActions, DialogContent, DialogTitle, Divider, FormControl, FormControlLabel, FormHelperText, FormLabel, Grid2 as Grid, IconButton, InputAdornment, MenuItem, Switch, TextField, Theme, Tooltip, Typography, useTheme } from "@mui/material"; import AddIcon from "@mui/icons-material/AddCircle"; import RemoveIcon from "@mui/icons-material/RemoveCircle"; import classNames from "classnames"; import DeleteButton from "common/DeleteButton"; import ResponsiveDialog from "common/ResponsiveDialog"; import { coalesce, or, capitalize } from "utils"; import { useInteractionsAPI, usePrevious, useSnackbar } from "hooks"; import { Component, Device, Interaction } from "models"; import moment from "moment-timezone"; import { componentIDToString, emptyComponentId, getComponentIDString, sameComponentID, stringToComponentId } from "pbHelpers/Component"; import { extension, getMeasurements, GetNumNodes, hasInteractionResultType, isSource } from "pbHelpers/ComponentType"; import { ComponentType, Measurement, Operator, Result } from "pbHelpers/Enums"; import { getDefaultInteraction, isInteractionValid, isMeasurementTypeValid, isSourceValid, timeOfDayDescriptor } from "pbHelpers/Interaction"; import { describeMeasurement, MeasurementDescriber } from "pbHelpers/MeasurementDescriber"; import { pond } from "protobuf-ts/pond"; import { quack } from "protobuf-ts/quack"; import { useGlobalState } from "providers"; import React, { useCallback, useEffect, useState } from "react"; import { hasDeviceFeature } from "services/feature/service"; import RemoveInteraction from "./RemoveInteraction"; import { makeStyles } from "@mui/styles"; import { green, red } from "@mui/material/colors"; import { LocalizationProvider, TimePicker } from "@mui/x-date-pickers"; import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; // import dayjs from "dayjs"; import CancelSubmit from "common/CancelSubmit"; import { Sensor } from "ventilation/drawable/Sensor"; // import { Sensor } from "ventilation/drawable/Sensor"; const useStyles = makeStyles((theme: Theme) => { return ({ dialogContent: { marginTop: theme.spacing(1) }, borderedContainer: { padding: theme.spacing(1), marginBottom: theme.spacing(2), border: "1px solid rgba(255, 255, 255, 0.12)", borderRadius: "4px" }, greenButton: { color: green["600"] }, redButton: { color: red["600"] }, noPadding: { padding: 0 }, forPrompt: { fontSize: "0.875em", marginTop: "10px" }, timeRange: { fontSize: "0.875em", marginTop: "20px" }, singleInput: { minHeight: "48px" } }) }); interface Props { device: Device; initialComponent?: Component; initialInteraction?: Interaction; components: Component[]; mode?: string; isDialogOpen: boolean; closeDialogCallback: Function; refreshCallback: Function; canEdit?: boolean; sensor?: Sensor; } export default function InteractionSettings(props: Props) { const { device, initialComponent, initialInteraction, components, mode, isDialogOpen, closeDialogCallback, refreshCallback, canEdit, sensor } = props; const theme = useTheme(); const { success, error } = useSnackbar(); const prevInitialInteraction = usePrevious(initialInteraction); const prevComponents = usePrevious(components); const prevInitialComponent = usePrevious(initialComponent); const prevIsDialogOpen = usePrevious(isDialogOpen); const classes = useStyles(); const [{ user, as }] = useGlobalState(); const interactionsAPI = useInteractionsAPI(); const [interaction, setInteraction] = useState( Interaction.clone(initialInteraction) ); const [isRemoveInteractionOpen, setIsRemoveInteractionOpen] = useState(false); const [mappedComponents, setMappedComponents] = useState>(new Map()); const [numConditions, setNumConditions] = useState(0); const [rawConditionValues, setRawConditionValues] = useState([]); const [rawResultValue, setRawResultValue] = useState(""); const [dutyCycleEnabled, setDutyCycleEnabled] = useState(false); const [dutyCycle, setDutyCycle] = useState(""); const [timezone, setTimezone] = useState(undefined); //whether is any node(0), average of all nodes(1), single node(2), or node diff(3) const [subtypeDropdown, setSubtypeDropdown] = useState(0); const initialConditions = useCallback( (type: quack.ComponentType): pond.InteractionCondition[] => { let measurementType = availableMeasurementTypes(type)[0]; let condition = pond.InteractionCondition.create({ measurementType: measurementType, comparison: measurementType === Measurement.boolean ? Operator.equals : Operator.less, value: 0 }); return [condition]; }, [] ); const setDefaultState = useCallback(() => { let interaction = getDefaultInteraction(); if (initialInteraction && mode === "update") { interaction = initialInteraction; if (interaction.settings.subtype === 0 || interaction.settings.subtype === 1) { setSubtypeDropdown(interaction.settings.subtype); } else { if (interaction.settings.subtype >= 138) { setSubtypeDropdown(4); } else if (interaction.settings.subtype > 17) { setSubtypeDropdown(3); } else if (interaction.settings.subtype > 1) { setSubtypeDropdown(2); } } } if (initialComponent && mode === "add") { interaction.settings.result = pond.InteractionResult.create({ type: Result.report, value: 0, mode: 0 }); if (isSource(initialComponent.settings.type)) { interaction.settings.source = initialComponent.location(); interaction.settings.conditions = initialConditions(initialComponent.settings.type); } } let rawConditionValues = [] as Array; interaction.settings.conditions.forEach((condition: pond.InteractionCondition) => { let value = describeMeasurement( condition.measurementType, or(initialComponent, Component.create()).settings.type, or(initialComponent, Component.create()).settings.subtype, undefined, user ).toDisplay(condition.value); rawConditionValues.push(value.toString()); }); let numConditions = interaction.settings.conditions.length; let mappedComponents: Map = new Map(); components.forEach(component => mappedComponents.set(component.locationString(), component)); let interactionResult = pond.InteractionResult.create( or(interaction.settings.result, undefined) ); if (sensor) { interaction.settings.subtype = 2; interaction.settings.nodeOne = sensor.index; interaction.settings.nodeTwo = sensor.index; } setIsRemoveInteractionOpen(false); setNumConditions(numConditions); setRawConditionValues(rawConditionValues); setRawResultValue(interactionResult.value.toString()); setDutyCycleEnabled(interactionResult.dutyCycle > 0); setDutyCycle(interactionResult.dutyCycle.toString()); setMappedComponents(mappedComponents); setInteraction(interaction); }, [components, initialComponent, initialConditions, initialInteraction, mode, /*sensor*/]); const availableSources = () => { return components.filter(component => isSource(component.settings.type)); }; useEffect(() => { if (user && user.settings.timezone) { setTimezone(user.settings.timezone); } if (isDialogOpen && prevIsDialogOpen) return; if ( prevInitialInteraction !== initialInteraction || (prevComponents && prevComponents.length !== components.length) || getComponentIDString(prevInitialComponent) !== getComponentIDString(initialComponent) || isDialogOpen !== prevIsDialogOpen ) { setDefaultState(); } }, [ components.length, initialComponent, initialInteraction, prevComponents, prevInitialComponent, prevInitialInteraction, setDefaultState, user, isDialogOpen, prevIsDialogOpen ]); const getAvailableSinks = () => { let type = or(interaction.settings.result, pond.InteractionResult.create()).type; return components.filter(component => hasInteractionResultType(component.settings.type, type)); }; const close = () => { closeDialogCallback(); if (mode === "add") { setDefaultState(); } }; const submit = () => { let settings = interaction.settings; if (multiNodeSource() && (settings.nodeOne > 0 || settings.nodeTwo > 0)) { if (subtypeDropdown === 3 || subtypeDropdown === 2) { settings.subtype = interaction.subtypeFromNodes(settings.nodeOne, settings.nodeTwo); if ( interaction.settings.nodeOne > interaction.settings.nodeTwo && interaction.settings.nodeTwo !== 0 ) { //flip operator and send negative comparitor to save settings.conditions.forEach(condition => { if ( condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN ) { condition.comparison = quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN; } else if ( condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN ) { condition.comparison = quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN; } condition.value = condition.value * -1; }); } } else if (subtypeDropdown === 4) { settings.subtype = Interaction.upToSubtype(settings.nodeOne); } } else { settings.subtype = subtypeDropdown; } let sync = true; if (interaction) Object.keys(interaction?.settings).forEach(key => { // Check if the key value has changed let k = key as keyof pond.InteractionSettings; let initial = JSON.stringify(initialInteraction?.settings[k]); let after = JSON.stringify(interaction.settings[k]); if (initial !== after) { if (key !== "sortPriority") { sync = false; } } }); switch (mode) { case "add": interactionsAPI .addInteraction(Number(device.settings.deviceId), settings, as) .then(() => { success("Interaction was successfully created"); refreshCallback(); }) .catch(() => error("Error occured when creating an interaction")) .finally(() => close()); break; default: if (sync) { let s = pond.InteractionPondSettings.create(); s.sortPriority = interaction.settings.sortPriority; interactionsAPI .updateInteractionPondSettings(Number(device.settings.deviceId), interaction.key(), s, as) .then(() => { success("Interaction pond settings were successfully updated"); refreshCallback(); }) .catch(() => error("Error when updating interaction pond settings")) .finally(() => close()); } else { interactionsAPI .updateInteraction(Number(device.settings.deviceId), settings, as) .then(() => { success("Interaction was successfully updated"); refreshCallback(); }) .catch(() => error("Error occured when updating an interaction")) .finally(() => close()); } break; } }; const openRemoveInteraction = () => { setIsRemoveInteractionOpen(true); }; const closeRemoveInteraction = () => { setIsRemoveInteractionOpen(false); close(); }; const interactionRemoved = () => { refreshCallback(); }; const addCondition = () => { let updatedInteraction = Interaction.clone(interaction); let updatedRawConditionValues = rawConditionValues; if (numConditions < device.maxConditions() && interaction.settings.source) { let condition = pond.InteractionCondition.create(); condition.measurementType = getMeasurements( interaction.settings.source.type )[0].measurementType; condition.comparison = condition.measurementType === Measurement.boolean ? Operator.equals : Operator.less; updatedRawConditionValues.push(""); updatedInteraction.settings.conditions.push(condition); setInteraction(updatedInteraction); setNumConditions(numConditions + 1); setRawConditionValues(updatedRawConditionValues); } }; const removeCondition = (index: number) => { let updatedInteraction = Interaction.clone(interaction); let updatedRawConditionValues = rawConditionValues; if (numConditions > 1 && index < numConditions) { updatedInteraction.settings.conditions.splice(index, 1); updatedRawConditionValues.splice(index, 1); setInteraction(updatedInteraction); setNumConditions(numConditions - 1); setRawConditionValues(updatedRawConditionValues); } }; const availableMeasurementTypes = (type: quack.ComponentType): quack.MeasurementType[] => { return getMeasurements(type).map(m => m.measurementType); }; //changing the source also resets the conditions const changeSource = (event: any) => { let updatedInteraction = Interaction.clone(interaction); let changed = event.target.value !== componentIDToString(interaction.settings.source); if (!changed) return; updatedInteraction.settings.source = stringToComponentId(event.target.value.toString()); updatedInteraction.settings.conditions = initialConditions( or(interaction.settings.source, quack.ComponentID.create()).type ); let result = pond.InteractionResult.create(or(interaction.settings.result, undefined)); if (result.type === Result.report) { updatedInteraction.settings.sink = emptyComponentId(); } updatedInteraction.settings.result = result; setInteraction(updatedInteraction); setNumConditions(1); setRawConditionValues([""]); }; const changePriority = (event: any) => { let updatedInteraction = Interaction.clone(interaction); updatedInteraction.settings.priority = Number(event.target.value); setInteraction(updatedInteraction); }; const changeSortPriority = (event: any) => { let updatedInteraction = Interaction.clone(interaction); updatedInteraction.settings.sortPriority = Number(event.target.value); setInteraction(updatedInteraction); }; const describeSource = (measurementType: quack.MeasurementType): MeasurementDescriber => { const source: Component = or( mappedComponents.get(componentIDToString(interaction.settings.source)), Component.create() ); return describeMeasurement(measurementType, source.settings.type, source.settings.subtype, undefined, user); }; const describeSink = (): MeasurementDescriber => { const sink = mappedComponents.get(componentIDToString(interaction.settings.sink)); return describeMeasurement( Measurement.boolean, or(sink, Component.create()).settings.type, or(sink, Component.create()).settings.subtype, undefined, user ); }; const changeMeasurementType = (conditionIndex: number, event: any) => { let updatedInteraction = Interaction.clone(interaction); let measurementType = interaction.settings.conditions[conditionIndex].measurementType; let rawValue = rawConditionValues[conditionIndex] as any; updatedInteraction.settings.conditions[conditionIndex].value = describeSource( measurementType ).toStored(rawValue); updatedInteraction.settings.conditions[conditionIndex].measurementType = event.target.value; if (event.target.value === Measurement.boolean) { updatedInteraction.settings.conditions[conditionIndex].comparison = Operator.equals; } setInteraction(updatedInteraction); }; const changeComparison = (conditionIndex: number, event: any) => { let updatedInteraction = Interaction.clone(interaction); updatedInteraction.settings.conditions[conditionIndex].comparison = event.target.value; setInteraction(updatedInteraction); }; const changeValue = (conditionIndex: number, event: any) => { let updatedInteraction = Interaction.clone(interaction); let updatedRawConditionValues = rawConditionValues; let measurementType = interaction.settings.conditions[conditionIndex].measurementType; updatedRawConditionValues[conditionIndex] = event.target.value; updatedInteraction.settings.conditions[conditionIndex].value = describeSource( measurementType ).toStored(event.target.value); setInteraction(updatedInteraction); setRawConditionValues(updatedRawConditionValues); }; const changeResultValue = (event: any) => { let updatedInteraction = Interaction.clone(interaction); let value = event.target.value; if (event.target.type === "checkbox") { value = event.target.checked ? 1 : 0; } (updatedInteraction.settings.result as pond.InteractionResult).value = isNaN(value) ? 0 : Number(value); setInteraction(updatedInteraction); setRawResultValue(value); }; const changeSubtype = (event: any) => { let updatedInteraction = Interaction.clone(interaction); let value = event.target.value; if (!isNaN(value)) { if (value < 2) { updatedInteraction.settings.subtype = value; } setSubtypeDropdown(value); updatedInteraction.settings.nodeOne = 0; updatedInteraction.settings.nodeTwo = 0; } setInteraction(updatedInteraction); }; const changeNode = (event: any, node: number) => { let updatedInteraction = Interaction.clone(interaction); let value = event.target.value; if (!isNaN(value)) { if (node === 1) { updatedInteraction.settings.nodeOne = value; } else if (node === 2) { updatedInteraction.settings.nodeTwo = value; } } setInteraction(updatedInteraction); }; const changeResultType = (event: any) => { let updatedInteraction = Interaction.clone(interaction); let result = interaction.settings.result as pond.InteractionResult; result.type = event.target.value; if (result.type === Result.report) { updatedInteraction.settings.sink = null; } else if (result.type === Result.toggle) { let availableSinks = getAvailableSinks(); if (availableSinks.length > 0) { let firstSink = availableSinks[0]; updatedInteraction.settings.sink = quack.ComponentID.fromObject({ type: firstSink.settings.type, addressType: firstSink.settings.addressType, address: firstSink.settings.address }); } } updatedInteraction.settings.result = result; setInteraction(updatedInteraction); }; const changeResultMode = (event: any) => { let updatedInteraction = Interaction.clone(interaction); let result = interaction.settings.result as pond.InteractionResult; result.mode = event.target.value; updatedInteraction.settings.result = result; setInteraction(updatedInteraction); }; const changeSink = (event: any) => { let updatedInteraction = Interaction.clone(interaction); updatedInteraction.settings.sink = stringToComponentId(event.target.value.toString()); if ( updatedInteraction.settings.sink.type === quack.ComponentType.COMPONENT_TYPE_INTERNAL_FUNCTION ) { if ( updatedInteraction.settings.sink.addressType === quack.AddressType.ADDRESS_TYPE_INTERNAL_SLEEP ) { if (updatedInteraction.settings.result) { updatedInteraction.settings.result.value = 1; } } } setInteraction(updatedInteraction); }; const toggleReport = (_event: any) => { let updatedInteraction = Interaction.clone(interaction); let notifications = or( interaction.settings.notifications, pond.InteractionNotifications.create() ); notifications.reports = !notifications.reports; updatedInteraction.settings.notifications = notifications; setInteraction(updatedInteraction); }; const toggleNotify = (_event: any) => { let updatedInteraction = Interaction.clone(interaction); let notifications = or( updatedInteraction.settings.notifications, pond.InteractionNotifications.create() ); notifications.notify = !notifications.notify; updatedInteraction.settings.notifications = notifications; setInteraction(updatedInteraction); }; const toggleDaySelected = (day: string, event: any) => { let updatedInteraction = Interaction.clone(interaction); let schedule = or(updatedInteraction.settings.schedule, pond.InteractionSchedule.create()); if (event.target.checked) { if (!schedule.weekdays.includes(day)) { schedule.weekdays.push(day); } } else { schedule.weekdays.splice(schedule.weekdays.indexOf(day), 1); } updatedInteraction.settings.schedule = schedule; setInteraction(updatedInteraction); }; const setScheduleTime = (id: string, event: any) => { let updatedInteraction = Interaction.clone(interaction); let schedule = or(updatedInteraction.settings.schedule, pond.InteractionSchedule.create()); if (id === "shortcut") { let value = event.target.value; if (value === "allDay") { schedule.timeOfDayStart = "00:00"; schedule.timeOfDayEnd = "24:00"; } else if (value === "before") { schedule.timeOfDayStart = "00:00"; if (!schedule.timeOfDayEnd || schedule.timeOfDayEnd === "24:00") { schedule.timeOfDayEnd = "23:59"; } } else if (value === "after") { schedule.timeOfDayEnd = "24:00"; if (schedule.timeOfDayStart === "00:00") { schedule.timeOfDayStart = "00:01"; } } else { if (schedule.timeOfDayStart === "00:00") { schedule.timeOfDayStart = "00:01"; } if (schedule.timeOfDayEnd === "24:00") { schedule.timeOfDayEnd = "23:59"; } } } else { let timeOfDay = event .hour() .toString() .padStart(2, "0") + ":" + event .minute() .toString() .padStart(2, "0"); if (id === "start") { schedule.timeOfDayStart = timeOfDay; } else if (id === "end") { if (timeOfDay === "00:00") { timeOfDay = "24:00"; } schedule.timeOfDayEnd = timeOfDay; } } schedule.timezone = or(timezone, moment.tz.guess()); updatedInteraction.settings.schedule = schedule; setInteraction(updatedInteraction); }; const isInitialComponentTheSource = () => { if (!interaction || !initialComponent) return false; return sameComponentID(interaction.settings.source, initialComponent.location()); }; const isInitialComponentTheSink = () => { if (!interaction || !initialComponent) return false; let sinkID = JSON.stringify(quack.ComponentID.create(or(interaction.settings.sink, undefined))); let component = initialComponent; let componentID = JSON.stringify( quack.ComponentID.create({ type: component.settings.type, addressType: component.settings.addressType, address: component.settings.address }) ); return sinkID === componentID; }; const invalidConditionValue = (index: number) => { const conditions = interaction.settings.conditions; if (conditions.length <= index) return false; if (conditions[index].measurementType === Measurement.boolean) { return false; } let value = rawConditionValues[index]; return value === "" || isNaN(Number(value)); }; const invalidConditionValues = () => { let invalid = false; interaction.settings.conditions.forEach((_, i) => { if (invalidConditionValue(i)) { invalid = true; } }); return invalid; }; const invalidResultValue = () => { return isNaN(Number(rawResultValue)); }; const getNodeOptions = (describer?: MeasurementDescriber) => { let options = []; let numNodes = 0; if (initialComponent) { if (initialComponent.lastMeasurement[0] && initialComponent.lastMeasurement[0].values[0]) { numNodes = initialComponent.lastMeasurement[0].values[0].values.length; //use the new structure last measurement to determine the number of nodes } else { numNodes = GetNumNodes( initialComponent.type(), initialComponent.status.lastMeasurement?.measurement ); } } for (let i = 0; i <= numNodes; i++) { let label = i === 0 ? "None" : "Node " + i; if (describer && i > 0) { let details = describer.nodeDetails(); if (details && details.labels[i - 1]) { label = details.labels[i - 1]; } } options.push( {label} ); } return options; }; //determine if the source component has nodes const multiNodeSource = () => { return getNodeOptions().length > 1; }; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ UI BEGINS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ const title = () => { return ( {mode && mode === "add" ? "Add Interaction" : "Interaction Settings"} {/* {sensor ? "Subnode: " + sensor.name() : device.name()} */} ); }; const sourceInput = () => { return ( {sourceMenuItems()} ); }; const priorityInput = () => { return ( {priorityMenuItems()} ); }; const sortingInput = () => { return ( {sortingMenuItems()} ); }; const measurementTypeMenuItems = () => { const type = or(interaction.settings.source, quack.ComponentID.create()).type; return availableMeasurementTypes(type).map(measurementType => ( {describeSource(measurementType).label()} )); }; const sourceMenuItems = () => { return availableSources().map(source => ( {source.name()} )); }; const sinkMenuItems = () => { let sinks = getAvailableSinks().map(sink => ( {sink.name()} )); if (isResultType(Result.set)) { sinks.push( Stay Awake ); } return sinks; }; const priorityMenuItems = () => { return ["Low", "Medium", "High"].map((name: string, i: number) => ( {name} )); }; const sortingMenuItems = () => { return ["None", "1", "2", "3", "4", "5"].map((name: string, i: number) => ( {name} )); }; const isBooleanValue = (index: number) => { if (interaction.settings.conditions.length <= index) return false; return interaction.settings.conditions[index].measurementType === Measurement.boolean; }; const conditionValue = (index: number, interaction: Interaction, values: string[]) => { let condition = interaction.settings.conditions[index]; return isBooleanValue(index) ? condition.value.toString() : values[index]; }; const conditionGroup = (index: number) => { if (index >= numConditions) return; let measurementType = interaction.settings.conditions[index].measurementType; let source = describeSource(measurementType, ); let isBoolean = isBooleanValue(index); return ( {index > 0 && ( AND )} changeMeasurementType(index, event)} autoFocus={false} margin="dense" variant="standard" fullWidth InputLabelProps={{ shrink: true }}> {measurementTypeMenuItems()} changeComparison(index, event)} autoFocus={false} margin="dense" variant="standard" fullWidth InputLabelProps={{ shrink: true }}> {!isBoolean && [ {"is below"} , {"is above"} ]} {"is exactly"} changeValue(index, event)} autoFocus={false} margin="dense" variant="standard" fullWidth InputProps={{ endAdornment: {source.unit()} }} InputLabelProps={{ shrink: true }}> {isBoolean && [ {source.enumerations()[0]} , {source.enumerations()[1]} ]} removeCondition(index)} className={classNames(classes.redButton, classes.noPadding)}> {multiNodeSource() && !sensor && ( changeSubtype(event)} margin="dense" variant="standard" fullWidth InputLabelProps={{ shrink: true }}> Any Average Single Node Node Diff Up To {(subtypeDropdown === 2 || subtypeDropdown === 3 || subtypeDropdown === 4) && ( changeNode(event, 1)} margin="dense" variant="standard" fullWidth InputLabelProps={{ shrink: true }}> {getNodeOptions(source)} )} {subtypeDropdown === 3 && ( changeNode(event, 2)} margin="dense" variant="standard" fullWidth InputLabelProps={{ shrink: true }}> {getNodeOptions(source)} )} )} ); }; const conditionInput = () => { const conditionGroups = []; for (let i = 0; i < numConditions; i++) { conditionGroups[i] = conditionGroup(i); } return ( Conditions {isSourceValid(interaction.settings.source) ? ( {conditionGroups} ) : ( You must select a source before adding conditions )} = device.maxConditions() || !canEdit} aria-label="Add another condition" onClick={addCondition} className={classNames(classes.greenButton, classes.noPadding)}> ); }; const isResultType = (type: quack.InteractionResultType): boolean => { return or(interaction.settings.result, pond.InteractionResult.create()).type === type; }; const reportingEnabled = () => { return ( or(interaction.settings.notifications, pond.InteractionNotifications.create()).reports || or(interaction.settings.result, pond.InteractionResult.create()).type === Result.report ); }; const toggleDutyCycle = (event: any) => { let updatedInteraction = interaction; if (!interaction.settings.result) { updatedInteraction.settings.result = pond.InteractionResult.create(); } if (!event.target.checked) { if (!updatedInteraction.settings.result) { updatedInteraction.settings.result = pond.InteractionResult.create({ dutyCycle: 0 }); } else { updatedInteraction.settings.result.dutyCycle = 0; } } setDutyCycleEnabled(event.target.checked); setInteraction(updatedInteraction); }; const changeDutyCycle = (event: any) => { let dutyCycle = Number(event.target.value); let updatedInteraction = interaction; if (!isNaN(dutyCycle)) { if (!updatedInteraction.settings.result) { updatedInteraction.settings.result = pond.InteractionResult.create(); } else { updatedInteraction.settings.result.dutyCycle = dutyCycle; } } setDutyCycle(event.target.value); setInteraction(updatedInteraction); }; const dutyCycleInput = () => { return ( } label="Once every" disabled={!canEdit} /> seconds }}> ); }; const notificationInput = () => { const report = reportingEnabled(); const notify = or(interaction.settings.notifications, pond.InteractionNotifications.create()) .notify; return ( {!isResultType(Result.report) && ( } label="Report" disabled={!canEdit} /> )} } label="Notify" disabled={!canEdit || !report} /> {notify && ( Everyone with notifications enabled for this device will receive an email and/or SMS each time this result occurs )} ); }; const sinkSelector = () => { return ( {sinkMenuItems()} ); }; const setMenuItems = () => { let type = or(interaction.settings.sink, quack.ComponentID.create()).type; let states = extension(type).states.map((state, i) => ( {state} )); return states; }; const resultInput = () => { const sink = or(interaction.settings.sink, quack.ComponentID.create()); const result = or( interaction.settings.result, pond.InteractionResult.create({ type: Result.report }) ); const isRun = result.type === Result.run; const isMotor = sink.type === ComponentType.stepperMotor; return ( Result Report Set Toggle Run {isRun && ( {sinkSelector()} {isMotor && ( Clockwise Counter Clockwise )} for sec }} InputLabelProps={{ shrink: true }} disabled={!canEdit} /> )} {isResultType(Result.set) && ( {sinkSelector()} {interaction.settings.sink?.type !== quack.ComponentType.COMPONENT_TYPE_INTERNAL_FUNCTION && ( to )} {interaction.settings.sink?.type !== quack.ComponentType.COMPONENT_TYPE_INTERNAL_FUNCTION && ( {setMenuItems()} )} )} {isResultType(Result.toggle) && ( {sinkSelector()} } label={describeSink().enumerations()[result.value]} disabled={!canEdit} /> )} {hasDeviceFeature(device.settings.upgradeChannel, "better-controls") && isRun && dutyCycleInput()} {notificationInput()} ); }; const daySelector = (day: string, size: any) => { const schedule = or(interaction.settings.schedule, pond.InteractionSchedule.create()); return ( toggleDaySelected(day, event)} value={day} color="secondary" /> } label={capitalize(day.substr(0, 2))} labelPlacement="bottom" disabled={!canEdit} /> ); }; const scheduleInput = () => { let schedule = or(interaction.settings.schedule, pond.InteractionSchedule.create()); let timezone = coalesce(schedule.timezone, moment.tz.guess()); let todStart = coalesce(schedule.timeOfDayStart, "00:00").split(":"); let todEnd = coalesce(schedule.timeOfDayEnd, "23:59").split(":"); let start = moment .tz(timezone) .startOf("day") .add(todStart[0], "hours") .add(todStart[1], "minutes") .local(); let end = moment .tz(timezone) .startOf("day") .add(todEnd[0], "hours") .add(todEnd[1], "minutes") .local(); let descriptor = timeOfDayDescriptor(schedule); let showStart = descriptor === "after" || descriptor === "from"; let showEnd = descriptor === "before" || descriptor === "from"; let showBoth = showStart && showEnd; return ( Schedule {daySelector("sunday", 3)} {daySelector("monday", 3)} {daySelector("tuesday", 3)} {daySelector("wednesday", 3)} {daySelector("thursday", 4)} {daySelector("friday", 4)} {daySelector("saturday", 4)} setScheduleTime("shortcut", event)} fullWidth autoFocus={false} margin="normal" variant="standard" InputLabelProps={{ shrink: true }}> All Day Before After From {showStart && ( setScheduleTime("start", event)} slotProps={{ textField: (props) => , }} /> )} {showBoth && ( to )} {showEnd && ( setScheduleTime("end", event)} slotProps={{ textField: (props) => , }} /> )} ); }; const content = () => { return ( {sourceInput()} {hasDeviceFeature(device.settings.upgradeChannel, "better-controls") && (
{priorityInput()}
{sortingInput()}
)} {conditionInput()} {resultInput()} {scheduleInput()}
); }; const actions = () => { return ( {mode === "update" && canEdit && ( Delete )} {/* {canEdit && ( )} */} ); }; return ( {title()} {content()} {actions()} {mode === "update" && ( )} ); }