Compare commits
13 commits
master
...
bin_modes_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcbd0fd360 | ||
|
|
26dfd0db01 | ||
|
|
7ac469f9db | ||
|
|
1ac937e7db | ||
|
|
e91d2f6005 | ||
|
|
aa346a6a5a | ||
|
|
208a657528 | ||
|
|
2c6799bcc1 | ||
|
|
7e6b41273b | ||
|
|
bced38d454 | ||
|
|
19eb2a7faf | ||
|
|
179e9abbd9 | ||
|
|
6d1c9431f2 |
24 changed files with 2778 additions and 109 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -46,7 +46,7 @@
|
|||
"mui-tel-input": "^7.0.0",
|
||||
"notistack": "^3.0.1",
|
||||
"openweathermap-ts": "^1.2.10",
|
||||
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
|
||||
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#bin_modes_v2",
|
||||
"query-string": "^9.2.1",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
|
|
@ -11752,7 +11752,7 @@
|
|||
},
|
||||
"node_modules/protobuf-ts": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#100126cc7f3cdf18f68af6372e5db4113db012b7",
|
||||
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#8a8ec79e9cc72bb770856c9e8e66f5294141da66",
|
||||
"dependencies": {
|
||||
"protobufjs": "^6.8.8"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@
|
|||
"mui-tel-input": "^7.0.0",
|
||||
"notistack": "^3.0.1",
|
||||
"openweathermap-ts": "^1.2.10",
|
||||
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
|
||||
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#bin_modes_v2",
|
||||
"query-string": "^9.2.1",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React from "react";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useThree, useFrame } from "@react-three/fiber";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { Vector2, Vector3 } from "three";
|
||||
|
||||
interface CameraTarget {
|
||||
|
|
@ -45,6 +44,18 @@ interface Props {
|
|||
* Wire it by passing a ref setter: onReset={fn => resetFn.current = fn}
|
||||
*/
|
||||
onReset?: (resetFn: () => void) => void;
|
||||
/**
|
||||
* Automatically spin the camera around the target on mount.
|
||||
* Any user interaction (drag, pan, zoom, pinch) stops it permanently
|
||||
* until the camera is reset (reset restarts it if this is still true).
|
||||
* @default false
|
||||
*/
|
||||
autoRotate?: boolean;
|
||||
/**
|
||||
* Auto-rotate speed in radians per second.
|
||||
* @default 0.3
|
||||
*/
|
||||
autoRotateSpeed?: number;
|
||||
}
|
||||
|
||||
export default function OrbitCameraControls(props: Props) {
|
||||
|
|
@ -58,6 +69,8 @@ export default function OrbitCameraControls(props: Props) {
|
|||
initialRadius,
|
||||
maxRadius,
|
||||
onReset,
|
||||
autoRotate = false,
|
||||
autoRotateSpeed = 0.3,
|
||||
} = props;
|
||||
|
||||
const { camera, gl } = useThree();
|
||||
|
|
@ -75,6 +88,9 @@ export default function OrbitCameraControls(props: Props) {
|
|||
const panCameraPosition = useRef(new Vector3());
|
||||
const lastPinchDist = useRef(0);
|
||||
|
||||
// Auto-rotate: active until the user does *anything* (drag/pan/zoom/pinch)
|
||||
const autoRotateActive = useRef(autoRotate);
|
||||
|
||||
// Spherical coords
|
||||
const spherical = useRef({
|
||||
radius: initialRadius ?? 10,
|
||||
|
|
@ -82,6 +98,39 @@ export default function OrbitCameraControls(props: Props) {
|
|||
phi: Math.PI / 2, // vertical angle
|
||||
});
|
||||
|
||||
// Pulled out of the effect so both the DOM-event effect and the
|
||||
// useFrame auto-rotate loop below can call it.
|
||||
const updateCamera = useCallback(() => {
|
||||
const { radius, theta, phi } = spherical.current;
|
||||
|
||||
const x = radius * Math.sin(phi) * Math.sin(theta);
|
||||
const y = radius * Math.cos(phi);
|
||||
const z = radius * Math.sin(phi) * Math.cos(theta);
|
||||
|
||||
camera.position.set(
|
||||
targetRef.current.x + x,
|
||||
targetRef.current.y + y,
|
||||
targetRef.current.z + z
|
||||
);
|
||||
camera.lookAt(
|
||||
targetRef.current.x,
|
||||
targetRef.current.y,
|
||||
targetRef.current.z
|
||||
);
|
||||
|
||||
// Shift the projected image without affecting orbit
|
||||
const { width, height } = gl.domElement.getBoundingClientRect();
|
||||
(camera as any).setViewOffset(width, height, viewOffset ?? 0, 0, width, height);
|
||||
}, [camera, gl, viewOffset]);
|
||||
|
||||
// Drives the auto-spin every frame while it's active. Runs inside the
|
||||
// R3F render loop, so it's cheap and stays in sync with everything else.
|
||||
useFrame((_, delta) => {
|
||||
if (!autoRotateActive.current) return;
|
||||
spherical.current.theta += autoRotateSpeed * delta;
|
||||
updateCamera();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Sync radius whenever initialRadius prop changes (e.g. after bin data loads)
|
||||
spherical.current.radius = initialRadius ?? 10;
|
||||
|
|
@ -89,29 +138,6 @@ export default function OrbitCameraControls(props: Props) {
|
|||
const canvas = gl.domElement;
|
||||
canvas.addEventListener("contextmenu", e => e.preventDefault());
|
||||
|
||||
const updateCamera = () => {
|
||||
const { radius, theta, phi } = spherical.current;
|
||||
|
||||
const x = radius * Math.sin(phi) * Math.sin(theta);
|
||||
const y = radius * Math.cos(phi);
|
||||
const z = radius * Math.sin(phi) * Math.cos(theta);
|
||||
|
||||
camera.position.set(
|
||||
targetRef.current.x + x,
|
||||
targetRef.current.y + y,
|
||||
targetRef.current.z + z
|
||||
);
|
||||
camera.lookAt(
|
||||
targetRef.current.x,
|
||||
targetRef.current.y,
|
||||
targetRef.current.z
|
||||
);
|
||||
|
||||
// Shift the projected image without affecting orbit
|
||||
const { width, height } = gl.domElement.getBoundingClientRect();
|
||||
(camera as any).setViewOffset(width, height, viewOffset ?? 0, 0, width, height);
|
||||
};
|
||||
|
||||
// Hand the reset function to the caller so they can trigger it
|
||||
// (e.g. from a button in CameraOverlay) without needing an external ref.
|
||||
if (onReset) {
|
||||
|
|
@ -124,15 +150,25 @@ export default function OrbitCameraControls(props: Props) {
|
|||
y: (target?.y ?? 0) + (offset?.y ?? 0),
|
||||
z: (target?.z ?? 0) + (offset?.z ?? 0),
|
||||
};
|
||||
// Resuming auto-rotate on reset feels natural — remove this
|
||||
// line if you'd rather it stay stopped once the user has
|
||||
// touched the camera.
|
||||
autoRotateActive.current = autoRotate;
|
||||
updateCamera();
|
||||
});
|
||||
}
|
||||
|
||||
updateCamera();
|
||||
|
||||
const stopAutoRotate = () => {
|
||||
autoRotateActive.current = false;
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
if (e.target !== canvas) return;
|
||||
|
||||
stopAutoRotate();
|
||||
|
||||
lastPos.current = { x: e.clientX, y: e.clientY };
|
||||
panCameraPosition.current.copy(camera.position);
|
||||
|
||||
|
|
@ -213,6 +249,7 @@ export default function OrbitCameraControls(props: Props) {
|
|||
if (e.target !== canvas) return;
|
||||
|
||||
e.preventDefault();
|
||||
stopAutoRotate();
|
||||
|
||||
spherical.current.radius += e.deltaY * 0.01;
|
||||
spherical.current.radius = Math.max(
|
||||
|
|
@ -226,6 +263,7 @@ export default function OrbitCameraControls(props: Props) {
|
|||
const handleTouchStart = (e: TouchEvent) => {
|
||||
if (e.target !== canvas) return;
|
||||
e.preventDefault();
|
||||
stopAutoRotate();
|
||||
|
||||
if (e.touches.length === 1) {
|
||||
isDragging.current = true;
|
||||
|
|
@ -323,7 +361,7 @@ export default function OrbitCameraControls(props: Props) {
|
|||
canvas.removeEventListener("touchmove", handleTouchMove);
|
||||
canvas.removeEventListener("touchend", handleTouchEnd);
|
||||
};
|
||||
}, [camera, gl, target, clampVerticalRotation, minPhi, maxPhi, initialRadius, maxRadius]);
|
||||
}, [camera, gl, target, offset, clampVerticalRotation, minPhi, maxPhi, initialRadius, maxRadius, onReset, autoRotate, updateCamera]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -145,6 +145,8 @@ export default function Bin3dView(props: Props) {
|
|||
<Canvas style={{position: "absolute", inset: 0}}>
|
||||
<OrbitCameraControls
|
||||
clampVerticalRotation
|
||||
autoRotate
|
||||
autoRotateSpeed={0.5}
|
||||
initialRadius={initialCameraRadius}
|
||||
maxRadius={initialCameraRadius * 2}
|
||||
onReset={fn => { resetCameraFn.current = fn; }}
|
||||
|
|
|
|||
|
|
@ -85,10 +85,14 @@ interface Props {
|
|||
sink: Component;
|
||||
grain?: pond.Grain;
|
||||
customGrain?: pond.GrainSettings
|
||||
/**
|
||||
* if true, the button to update the interaction will be hidden because it will be assumed that the parent will handle the interaction
|
||||
*/
|
||||
parentUpdate?: boolean
|
||||
}
|
||||
|
||||
export default function BinConditioningInteraction(props: Props) {
|
||||
const { interaction, source, sink, grain, deviceId, customGrain } = props;
|
||||
const { interaction, source, sink, grain, deviceId, customGrain, parentUpdate} = props;
|
||||
const [sliderVals, setSliderVals] = useState<Map<quack.MeasurementType, number>>(new Map());
|
||||
const [sliderMarks, setSliderMarks] = useState<Map<quack.MeasurementType, number>>(new Map());
|
||||
//this is the emc value calculated from the interactions temp and humidity conditions
|
||||
|
|
@ -143,13 +147,13 @@ export default function BinConditioningInteraction(props: Props) {
|
|||
|
||||
const updateInteraction = () => {
|
||||
interactionAPI
|
||||
.updateInteraction(deviceId, interaction.settings, as)
|
||||
.then(resp => {
|
||||
openSnack("Updated Interaction Conditions");
|
||||
})
|
||||
.catch(err => {
|
||||
openSnack("Failed to Update Interaction Conditions");
|
||||
});
|
||||
.updateInteraction(deviceId, interaction.settings, as)
|
||||
.then(resp => {
|
||||
openSnack("Updated Interaction Conditions");
|
||||
})
|
||||
.catch(err => {
|
||||
openSnack("Failed to Update Interaction Conditions");
|
||||
});
|
||||
};
|
||||
|
||||
const customMark = (val: string, arrowColor: string) => {
|
||||
|
|
@ -238,6 +242,7 @@ export default function BinConditioningInteraction(props: Props) {
|
|||
max={describer.max()}
|
||||
value={sliderVals.get(condition.measurementType) ?? describer.min()}
|
||||
onChange={(_, val) => {
|
||||
//note that changing it here like this is what is changing it in the interaction itself
|
||||
condition.value = Math.round(describer.toStored(val as number));
|
||||
let sliders = cloneDeep(sliderVals);
|
||||
sliders.set(condition.measurementType, val as number);
|
||||
|
|
@ -249,14 +254,16 @@ export default function BinConditioningInteraction(props: Props) {
|
|||
);
|
||||
})}
|
||||
<Grid item xs={12}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
updateInteraction();
|
||||
}}>
|
||||
Update Conditions
|
||||
</Button>
|
||||
{!parentUpdate &&
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
updateInteraction();
|
||||
}}>
|
||||
Update Conditions
|
||||
</Button>
|
||||
}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { CableData } from "bin/3dView/Data/BuildCableData";
|
|||
import ModeChangeDialog from "bin/conditioning/modeChangeDialog";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { useMobile } from "hooks";
|
||||
import BinModeController from "./binModes/BinModeController";
|
||||
|
||||
interface Props {
|
||||
bin: Bin
|
||||
|
|
@ -35,6 +36,7 @@ export default function bin3dVisualizer(props: Props){
|
|||
const [showMoistureHeatmap, setShowMoistureHeatmap] = useState(false)
|
||||
const [binDisplay, setBinDisplay] = useState<string>("temp")
|
||||
const [binMode, setBinMode] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE)
|
||||
const [newMode, setNewMode] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE)
|
||||
const [openModeChange, setOpenModeChange] = useState(false)
|
||||
const [selectedCable, setSelectedCable] = useState<CableData | undefined>(undefined)
|
||||
const [selectedNode, setSelectedNode] = useState<NodeData | undefined>(undefined)
|
||||
|
|
@ -58,14 +60,6 @@ export default function bin3dVisualizer(props: Props){
|
|||
setBinMode(bin.settings.mode)
|
||||
},[bin])
|
||||
|
||||
const updateBin = () => {
|
||||
if(updateBinCallback){
|
||||
let clone = cloneDeep(bin)
|
||||
clone.settings.mode = binMode
|
||||
updateBinCallback(clone)
|
||||
}
|
||||
};
|
||||
|
||||
const binModeControl = () => {
|
||||
return (
|
||||
<Box>
|
||||
|
|
@ -86,8 +80,10 @@ export default function bin3dVisualizer(props: Props){
|
|||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': { border: 'none' },
|
||||
}}
|
||||
onChange={event => {
|
||||
setBinMode(event.target.value as pond.BinMode)
|
||||
setNewMode(event.target.value as pond.BinMode)
|
||||
setOpenModeChange(true)
|
||||
// for the re-built modes, each one will have its own dialog box and this will just control which one to open rather than the single dialog for all of them
|
||||
// and having the mode determine what to show in the dialog
|
||||
}}
|
||||
>
|
||||
<MenuItem value={pond.BinMode.BIN_MODE_NONE}>Select Mode..</MenuItem>
|
||||
|
|
@ -327,7 +323,7 @@ export default function bin3dVisualizer(props: Props){
|
|||
}}
|
||||
/>
|
||||
}
|
||||
<ModeChangeDialog
|
||||
{/* <ModeChangeDialog
|
||||
binKey={bin.key()}
|
||||
binMode={binMode}
|
||||
grain={bin.settings.inventory?.grainType}
|
||||
|
|
@ -361,7 +357,19 @@ export default function bin3dVisualizer(props: Props){
|
|||
changeComplete={() => {
|
||||
setModeChangeInProgress(false)
|
||||
}}
|
||||
|
||||
/> */}
|
||||
<BinModeController
|
||||
bin={bin}
|
||||
binPrefs={binPrefs}
|
||||
devices={devices}
|
||||
componentDevices={componentDevices}
|
||||
componentMap={componentMap}
|
||||
newMode={newMode}
|
||||
open={openModeChange}
|
||||
onClose={()=>{setOpenModeChange(false)}}
|
||||
modeUpdated={(newMode) => {
|
||||
setBinMode(newMode)
|
||||
}}
|
||||
/>
|
||||
<Box position={"relative"} height={600}>
|
||||
<Bin3dView
|
||||
|
|
|
|||
259
src/bin/binModes/BinModeController.tsx
Normal file
259
src/bin/binModes/BinModeController.tsx
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
import { Box, Button, DialogActions, DialogContent, DialogTitle } from "@mui/material";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import { Bin, Component, Device, Interaction } from "models";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import StorageMode from "./StorageMode";
|
||||
import DryingMode from "./DryingMode";
|
||||
import HydratingMode from "./HydratingMode";
|
||||
import CooldownMode from "./CooldownMode";
|
||||
import { PromiseProgress, Stage, Step } from "common/PromiseProgress";
|
||||
import { useBinAPI, useComponentAPI, useInteractionsAPI } from "providers";
|
||||
import { ComponentSet } from "bin/conditioning/pickComponentSet";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { Controller } from "models/Controller";
|
||||
import ThreePhaseFanDry from "./ThreePhaseFanDry";
|
||||
|
||||
export interface DeviceChangeData {
|
||||
/**
|
||||
* the id of the device changes are being made to
|
||||
*/
|
||||
deviceId: number
|
||||
/**
|
||||
* the array of interactions to remove from the device
|
||||
*/
|
||||
toRemove: Interaction[]
|
||||
/**
|
||||
* the new interactions to add to the device
|
||||
*/
|
||||
toAdd?: pond.MultiInteractionSettings
|
||||
/**
|
||||
* the controllers that need to have there state updated
|
||||
*/
|
||||
controllers: Controller[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
newMode: pond.BinMode //used to control which dialog to open
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
bin: Bin
|
||||
binPrefs?: Map<string, pond.BinComponentPreferences>
|
||||
devices: Device[]
|
||||
componentDevices: Map<string, number>
|
||||
componentMap: Map<string, Component>
|
||||
modeUpdated?: (mode: pond.BinMode) => void
|
||||
}
|
||||
|
||||
export default function BinModeController(props: Props) {
|
||||
const {open, onClose, newMode, devices, componentDevices, componentMap, binPrefs, bin, modeUpdated} = props
|
||||
const [showProgress, setShowProgress] = useState(false)
|
||||
const [deviceComponents, setDeviceComponents] = useState<Map<number, Component[]>>(new Map())
|
||||
const [promiseStages, setPromiseStages] = useState<Stage[]>([])
|
||||
const interactionAPI = useInteractionsAPI()
|
||||
const componentAPI = useComponentAPI()
|
||||
const binAPI = useBinAPI();
|
||||
|
||||
//the use effects for the initial setup of what will be needed
|
||||
useEffect(()=>{
|
||||
let newMap:Map<number, Component[]> = new Map()
|
||||
componentMap.forEach((comp, key) => {
|
||||
//first we need to get the device id this component belongs to
|
||||
let dev = componentDevices.get(key)
|
||||
if(dev){
|
||||
//check if the key exists in the new map yet
|
||||
if(newMap.has(dev)){
|
||||
newMap.get(dev)?.push(comp)
|
||||
}else{
|
||||
newMap.set(dev, [comp])
|
||||
}
|
||||
}
|
||||
})
|
||||
setDeviceComponents(newMap)
|
||||
},[componentDevices, componentMap])
|
||||
|
||||
//steps involved for devices when changing the bin mode:
|
||||
//remove conflicting interactions
|
||||
//add new interactions
|
||||
//updating the controllers
|
||||
|
||||
const closeDialog = () => {
|
||||
setShowProgress(false)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const buildStages = (deviceData: DeviceChangeData[]) => {
|
||||
//build the stages to pass into the PromiseProgress
|
||||
//stage one is to remove conflicting interactions
|
||||
let stages: Stage[] = []
|
||||
let stage1: Stage = {
|
||||
title: "Remove Interactions",
|
||||
steps: []
|
||||
}
|
||||
let stage2: Stage = {
|
||||
title: "Add New Interactions",
|
||||
steps: []
|
||||
}
|
||||
let stage3: Stage = {
|
||||
title: "Update Controllers",
|
||||
steps: []
|
||||
}
|
||||
deviceData.forEach(data => {
|
||||
data.toRemove.forEach(interaction => {
|
||||
let newStep: Step = {
|
||||
title: "Removing Interaction",
|
||||
promise: () => interactionAPI.removeInteraction(data.deviceId, interaction.key())
|
||||
}
|
||||
stage1.steps.push(newStep);
|
||||
})
|
||||
let newInteractions = data.toAdd
|
||||
if(newInteractions){
|
||||
stage2.steps.push({
|
||||
title: "Adding Interactions",
|
||||
promise: () => interactionAPI.addMultiInteractions(data.deviceId, newInteractions)
|
||||
})
|
||||
}
|
||||
|
||||
data.controllers.forEach(controller => {
|
||||
let newStep: Step = {
|
||||
title: "Updating " + controller.name(),
|
||||
promise: () => componentAPI.update(data.deviceId, controller.settings)
|
||||
}
|
||||
stage3.steps.push(newStep)
|
||||
})
|
||||
})
|
||||
|
||||
if(stage1.steps.length > 0){
|
||||
stages.push(stage1)
|
||||
}
|
||||
if(stage2.steps.length > 0){
|
||||
stages.push(stage2)
|
||||
}
|
||||
if(stage3.steps.length > 0){
|
||||
stages.push(stage3)
|
||||
}
|
||||
//stage 4 is to update the bin with the new mode
|
||||
let stage4: Stage = {
|
||||
title: "Update Bin With New Mode",
|
||||
steps: [
|
||||
{
|
||||
title: "Update " + bin.name(),
|
||||
promise: () => {
|
||||
let clone = cloneDeep(bin.settings)
|
||||
clone.mode = newMode
|
||||
//this will set the bin to use the auto top nodes for cables when the bin is going into storage mode, and turn them off if the bin is being conditioned
|
||||
if(newMode === pond.BinMode.BIN_MODE_STORAGE){
|
||||
clone.autoGrainNode = true
|
||||
}else{
|
||||
clone.autoGrainNode = false
|
||||
}
|
||||
return binAPI.updateBin(bin.key(), clone)
|
||||
},
|
||||
onComplete: () => modeUpdated?.(newMode)
|
||||
}
|
||||
]
|
||||
}
|
||||
stages.push(stage4)
|
||||
|
||||
//set those stages to a state variable
|
||||
setPromiseStages(stages)
|
||||
}
|
||||
|
||||
/**
|
||||
* the progress step is the last step of each mode change that shows what is being done and what succeeded/failed
|
||||
* initially i was going to put it here because it would effectively be the same thing for each mode, however depending
|
||||
* on how i decide to handle the api calls, and since i am thinking about passing the functions into the children it may go into each mode component
|
||||
*
|
||||
*/
|
||||
const progressContent = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle>Change Mode</DialogTitle>
|
||||
<DialogContent>
|
||||
<PromiseProgress
|
||||
stages={promiseStages}
|
||||
description="These are the changes that will occur in order to change your bin mode with the given options.
|
||||
It will allow the device complete control of the selected components. Your grain is still your responsibility.
|
||||
Press start to begin."
|
||||
failFast
|
||||
onStart={() => {
|
||||
//this is just a function that is rin as soon as the start button is clicked it can be used to disable things while the change is in progress if we want to
|
||||
}}
|
||||
onComplete={()=>{
|
||||
closeDialog()
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={closeDialog}>Close</Button>
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
const modeContent = () => {
|
||||
switch(newMode){
|
||||
case pond.BinMode.BIN_MODE_DRYING:
|
||||
//setNumSteps(DryingStepCount)
|
||||
return <DryingMode
|
||||
grain={bin.settings.inventory?.grainType}
|
||||
customGrain={bin.settings.inventory?.customGrain ?? undefined}
|
||||
devices={devices}
|
||||
binPrefs={binPrefs}
|
||||
deviceComponents={deviceComponents}
|
||||
cancel={closeDialog}
|
||||
confirm={(deviceData) => {
|
||||
buildStages(deviceData)
|
||||
setShowProgress(true)
|
||||
}}
|
||||
/>
|
||||
case pond.BinMode.BIN_MODE_HYDRATING:
|
||||
return <HydratingMode
|
||||
grain={bin.settings.inventory?.grainType}
|
||||
customGrain={bin.settings.inventory?.customGrain ?? undefined}
|
||||
devices={devices}
|
||||
binPrefs={binPrefs}
|
||||
deviceComponents={deviceComponents}
|
||||
cancel={closeDialog}
|
||||
confirm={(deviceData) => {
|
||||
buildStages(deviceData)
|
||||
setShowProgress(true)
|
||||
}}
|
||||
/>
|
||||
case pond.BinMode.BIN_MODE_COOLDOWN:
|
||||
return <CooldownMode
|
||||
grain={bin.settings.inventory?.grainType}
|
||||
customGrain={bin.settings.inventory?.customGrain ?? undefined}
|
||||
devices={devices}
|
||||
binPrefs={binPrefs}
|
||||
deviceComponents={deviceComponents}
|
||||
cancel={closeDialog}
|
||||
confirm={(deviceData) => {
|
||||
buildStages(deviceData)
|
||||
setShowProgress(true)
|
||||
}}
|
||||
/>
|
||||
case pond.BinMode.BIN_MODE_THREE_PHASE_FAN_DRYING:
|
||||
return <ThreePhaseFanDry
|
||||
deviceComponents={deviceComponents}
|
||||
/>
|
||||
default:
|
||||
return <StorageMode
|
||||
devices={devices}
|
||||
binPrefs={binPrefs}
|
||||
bin={bin}
|
||||
deviceComponents={deviceComponents}
|
||||
cancel={closeDialog}
|
||||
confirm={(deviceData)=>{
|
||||
buildStages(deviceData)
|
||||
setShowProgress(true)
|
||||
}}/>
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveDialog open={open} onClose={closeDialog}>
|
||||
{showProgress ? progressContent() : modeContent()}
|
||||
</ResponsiveDialog>
|
||||
)
|
||||
}
|
||||
394
src/bin/binModes/CooldownMode.tsx
Normal file
394
src/bin/binModes/CooldownMode.tsx
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
import { Box, Theme, Stepper, Step, StepLabel, Typography, Button, Autocomplete, TextField, DialogTitle, DialogContent, DialogActions, CircularProgress } from "@mui/material";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import ConditioningSelector from "bin/conditioning/conditioningSelector";
|
||||
import { Component, Device, Interaction } from "models";
|
||||
import { Ambient } from "models/Ambient";
|
||||
import { Controller } from "models/Controller";
|
||||
import { Plenum } from "models/Plenum";
|
||||
import { pond, quack } from "protobuf-ts/pond";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ComponentSet } from "bin/conditioning/pickComponentSet"
|
||||
import { componentIDToString, sameComponentID } from "pbHelpers/Component";
|
||||
import moment from "moment";
|
||||
import { lowerCase } from "lodash";
|
||||
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
|
||||
import { useGlobalState, useInteractionsAPI } from "providers";
|
||||
import ConditionDisplay from "./conditionDisplay";
|
||||
import React from "react";
|
||||
import { DeviceChangeData } from "./BinModeController";
|
||||
import { GrainCable } from "models/GrainCable";
|
||||
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return ({
|
||||
stepper: {
|
||||
padding: theme.spacing(0.5)
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
interface Step {
|
||||
label: string;
|
||||
completed?: boolean;
|
||||
}
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
device: Device;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
devices: Device[]
|
||||
deviceComponents: Map<number, Component[]>
|
||||
binPrefs?: Map<string, pond.BinComponentPreferences>
|
||||
grain?: pond.Grain
|
||||
customGrain?: pond.GrainSettings
|
||||
cancel: () => void
|
||||
confirm: (deviceData: DeviceChangeData[]) => void
|
||||
}
|
||||
|
||||
const steps = [{label: "Device"}, {label: "Interaction"}]
|
||||
|
||||
//this is our simple drying mode the way it currently works, just building the dialog to be a little more trnasparent and customizable as to what the interaction is doing
|
||||
export default function CooldownMode(props: Props){
|
||||
const {devices, deviceComponents, binPrefs, grain, customGrain, cancel, confirm} = props
|
||||
const classes = useStyles()
|
||||
const [{user}] = useGlobalState()
|
||||
const interactionAPI = useInteractionsAPI()
|
||||
const [options, setOptions] = useState<Option[]>([])
|
||||
const [deviceOption, setDeviceOption] = useState<Option>({device: Device.create(), label: "Select Device"})
|
||||
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>()
|
||||
const [plenums, setPlenums] = useState<Plenum[]>([])
|
||||
const [ambients, setAmbients] = useState<Ambient[]>([])
|
||||
const [fans, setFans] = useState<Controller[]>([])
|
||||
const [cables, setCables] = useState<GrainCable[]>([])
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [interactionsLoading,setInteractionsLoading] = useState(false)
|
||||
|
||||
// possibly all 4 of these will need to be passed up, at least the conflicting, toAdd, and sets will
|
||||
const [componentSets, setComponentSets] = useState<ComponentSet[]>([])
|
||||
const [existingInteractions, setExistingInteractions] = useState<Interaction[]>([])
|
||||
const [conflictingInteractions, setConflictingInteractions] = useState<Interaction[]>([])
|
||||
const [interactionsToAdd, setInteractionsToAdd] = useState<pond.MultiInteractionSettings>()
|
||||
|
||||
|
||||
const [sourceMap, setSourceMap] = useState<Map<string, Component>>(new Map());
|
||||
const [sinkMap, setSinkMap] = useState<Map<string, Component>>(new Map());
|
||||
|
||||
|
||||
|
||||
//sort the components according to the bin preferences
|
||||
useEffect(()=>{
|
||||
if (!selectedDevice) return;
|
||||
if (!deviceComponents.get(selectedDevice.id())) return;
|
||||
var plenums: Plenum[] = [];
|
||||
var ambients: Ambient[] = [];
|
||||
var grainCables: GrainCable[] = [];
|
||||
var fans: Controller[] = [];
|
||||
var sinkMap: Map<string, Component> = new Map()
|
||||
var sourceMap: Map<string, Component> = new Map()
|
||||
|
||||
deviceComponents.get(selectedDevice.id())!.forEach(comp => {
|
||||
let pref = binPrefs?.get(comp.key());
|
||||
if (pref) {
|
||||
if (pref.type) {
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_PLENUM){
|
||||
plenums.push(Plenum.create(comp));
|
||||
sourceMap.set(comp.locationString(), comp)
|
||||
}
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_AMBIENT){
|
||||
ambients.push(Ambient.create(comp));
|
||||
sourceMap.set(comp.locationString(), comp)
|
||||
}
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE){
|
||||
grainCables.push(GrainCable.create(comp));
|
||||
sourceMap.set(comp.locationString(), comp)
|
||||
}
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_FAN) {
|
||||
let fan = Controller.create(comp)
|
||||
fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF;
|
||||
fans.push(fan);
|
||||
sinkMap.set(comp.locationString(), comp)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
setPlenums(plenums);
|
||||
setAmbients(ambients);
|
||||
setFans(fans);
|
||||
setSourceMap(sourceMap)
|
||||
setSinkMap(sinkMap)
|
||||
setCables(grainCables)
|
||||
//also load the interactions for the selected device so that we can find any conflicting ones
|
||||
setInteractionsLoading(true)
|
||||
interactionAPI.listInteractionsByDevice(selectedDevice.id()).then(resp => {
|
||||
setExistingInteractions(resp)
|
||||
}).finally(() => {
|
||||
setInteractionsLoading(false)
|
||||
})
|
||||
}, [deviceComponents, selectedDevice, binPrefs]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let o: Option[] = [];
|
||||
devices.forEach((device, i) => {
|
||||
let newOption: Option = {device: device, label: device.name()}
|
||||
if(i === 0) {
|
||||
setDeviceOption(newOption)
|
||||
setSelectedDevice(device)
|
||||
}
|
||||
o.push(newOption);
|
||||
});
|
||||
setOptions(o);
|
||||
}, [devices, setOptions]);
|
||||
|
||||
const buildFanInteraction = (
|
||||
sensor: Plenum | Ambient,
|
||||
fan: Controller,
|
||||
) => {
|
||||
let interaction = pond.InteractionSettings.create({
|
||||
source: sensor.location(),
|
||||
sink: fan.location(),
|
||||
schedule: pond.InteractionSchedule.create({
|
||||
timeOfDayStart: "00:00",
|
||||
timeOfDayEnd: "24:00",
|
||||
timezone: moment.tz.guess(),
|
||||
weekdays: moment.weekdays().map(d => lowerCase(d))
|
||||
}),
|
||||
notifications: pond.InteractionNotifications.create({
|
||||
reports: true
|
||||
}),
|
||||
result: pond.InteractionResult.create({
|
||||
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
|
||||
value: 1
|
||||
})
|
||||
});
|
||||
|
||||
let tempDefault = 15;
|
||||
|
||||
|
||||
let conditions = [];
|
||||
//may not actually need a condition on the humidity for cooldown
|
||||
// let humidityDefault = 60;
|
||||
// let fanConditionOne = pond.InteractionCondition.create({
|
||||
// measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
||||
// comparison:
|
||||
// humidityComparison === "greater"
|
||||
// ? quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
|
||||
// : quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
||||
// value: describeMeasurement(
|
||||
// quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
||||
// sensor.settings.type,
|
||||
// sensor.settings.subtype,
|
||||
// undefined,
|
||||
// user
|
||||
// ).toStored(humidityDefault)
|
||||
// });
|
||||
// conditions.push(fanConditionOne);
|
||||
|
||||
//since the measurement describers function toStored does a conversion into celsius for temperature if the users pref is fahrenheit we need to convert the preset value into fahrenheit
|
||||
if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
||||
tempDefault = Math.round((tempDefault * (9 / 5) + 32) * 100) / 100;
|
||||
}
|
||||
let tempVal = describeMeasurement(
|
||||
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
||||
sensor.settings.type,
|
||||
sensor.settings.subtype,
|
||||
undefined,
|
||||
user
|
||||
).toStored(tempDefault);
|
||||
|
||||
let fanConditionTwo = pond.InteractionCondition.create({
|
||||
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
||||
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
||||
value: Math.round(tempVal) //and then round the converted value since the interaction does not take decimals
|
||||
});
|
||||
conditions.push(fanConditionTwo);
|
||||
|
||||
interaction.conditions = conditions;
|
||||
//set the output mode to auto in the fan so that when the components are updated it uses the new mode
|
||||
fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO
|
||||
return interaction;
|
||||
};
|
||||
|
||||
//this function just takes in the sets as they change and creates the list of conflicting interactions and the settings to create the new ones and returns them
|
||||
const createInteractions = (sets: ComponentSet[]) => {
|
||||
if(!selectedDevice) return undefined //if there is no device that was selected, do nothing
|
||||
let multiInteractionSettings: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create()
|
||||
let conflictingInteractions: Interaction[] = []
|
||||
let linkedComponents = deviceComponents.get(selectedDevice.id());
|
||||
//loop through the sets to find interactions to remove as well as set the new ones
|
||||
sets.forEach((set) => {
|
||||
//loop through the controllers
|
||||
set.controllers.forEach(controller => {
|
||||
//filter the conflicting interactions for that controller
|
||||
let c = existingInteractions.filter(i => {
|
||||
let conflicting = false;
|
||||
if (linkedComponents) {
|
||||
linkedComponents.forEach(comp => {
|
||||
if (sameComponentID(comp.location(), i.settings.sink)) {
|
||||
conflicting = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return conflicting;
|
||||
});
|
||||
|
||||
conflictingInteractions = conflictingInteractions.concat(c)
|
||||
multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller))
|
||||
})
|
||||
})
|
||||
cables.forEach(cable => {
|
||||
console.log(cable.name())
|
||||
let c = existingInteractions.filter(i => {
|
||||
if (sameComponentID(cable.location(), i.settings.source) && !i.settings.sink && i.settings.notifications?.notify) {
|
||||
return true
|
||||
}
|
||||
});
|
||||
conflictingInteractions = conflictingInteractions.concat(c)
|
||||
})
|
||||
|
||||
setComponentSets([...sets])
|
||||
return {
|
||||
conflicting: conflictingInteractions,
|
||||
toAdd: multiInteractionSettings
|
||||
}
|
||||
}
|
||||
|
||||
const stepper = () => {
|
||||
return (
|
||||
<Stepper
|
||||
activeStep={currentStep}
|
||||
alternativeLabel
|
||||
classes={{
|
||||
root: classes.stepper
|
||||
}}>
|
||||
{steps.map((s, i) => (
|
||||
<Step key={i}>
|
||||
<StepLabel>{s.label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const deviceSelector = () => {
|
||||
return (
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
options={options}
|
||||
value={deviceOption}
|
||||
fullWidth
|
||||
getOptionLabel={option => option.label || ""}
|
||||
onChange={(_, newValue) => {
|
||||
if(newValue){
|
||||
setSelectedDevice(newValue.device);
|
||||
}
|
||||
}}
|
||||
renderInput={params => <TextField {...params} variant="outlined" label="Device" />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const deviceStep = () => {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography>Select the device to condition the bin</Typography>
|
||||
{deviceSelector()}
|
||||
{interactionsLoading ? <CircularProgress /> :
|
||||
<ConditioningSelector
|
||||
plenums={plenums}
|
||||
ambients={ambients}
|
||||
fans={fans}
|
||||
heaters={[]}
|
||||
//presets={presets}
|
||||
binMode={pond.BinMode.BIN_MODE_COOLDOWN}
|
||||
updateSets={(sets) => {
|
||||
//update the component sets that will be used for the interactions
|
||||
//when the sets change it will create the interactions and any conflicting ones that need to be removed will be put into a list
|
||||
//then when the submit button gets clicked it will use the conflicting list and toAdd to remove conflicting interactions and add the new ones
|
||||
let i = createInteractions(sets)
|
||||
if(i !== undefined){
|
||||
setConflictingInteractions(i.conflicting)
|
||||
setInteractionsToAdd(i.toAdd)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
//this step will use the conflicting interactions and interactions to add to display them and allow users to make changes
|
||||
const interactionStep = () => {
|
||||
return (
|
||||
<Box>
|
||||
<Typography>Interactions</Typography>
|
||||
{interactionsToAdd?.interactions.map((interaction, index) => {
|
||||
let temp = Interaction.create()
|
||||
temp.settings = interaction
|
||||
let sink = sinkMap.get(componentIDToString(interaction.sink))
|
||||
let source = sourceMap.get(componentIDToString(interaction.source))
|
||||
if(sink && source && selectedDevice){
|
||||
return (
|
||||
<ConditionDisplay
|
||||
key={index}
|
||||
grain={grain}
|
||||
customGrain={customGrain}
|
||||
interaction={temp}
|
||||
device={selectedDevice}
|
||||
sink={sink}
|
||||
source={source}
|
||||
changeConditions={(newSettings) => {
|
||||
console.log(newSettings)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const stepperContent = () => {
|
||||
switch(currentStep){
|
||||
case 1:
|
||||
return interactionStep()
|
||||
default:
|
||||
return deviceStep()
|
||||
}
|
||||
}
|
||||
|
||||
const actions = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* close - tells the parent to close the dialog without doing anything, always visible */}
|
||||
<Button onClick={cancel}>Close</Button>
|
||||
{/* back - goes back to the previous step, hidden on the forst step */}
|
||||
{currentStep !== 0 && <Button onClick={() => {setCurrentStep(currentStep-1)}}>Back</Button>}
|
||||
{/* next - goes to the next step, hidden on the last step */}
|
||||
{currentStep !== steps.length - 1 && <Button onClick={() => {setCurrentStep(currentStep+1)}}>Next</Button>}
|
||||
{/* confirm - tells the parent to build the stages using the data, only visible on the last step */}
|
||||
{currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && <Button onClick={() => {confirm([{ toRemove: conflictingInteractions, deviceId: selectedDevice.id(), toAdd: interactionsToAdd, controllers: componentSets.flatMap(set => set.controllers)}])}}>Confirm</Button>}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle>Bin Cooldown</DialogTitle>
|
||||
<DialogContent>
|
||||
{stepper()}
|
||||
{stepperContent()}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{actions()}
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
537
src/bin/binModes/DryingMode.tsx
Normal file
537
src/bin/binModes/DryingMode.tsx
Normal file
|
|
@ -0,0 +1,537 @@
|
|||
import { Box, Theme, Stepper, Step, StepLabel, Typography, RadioGroup, Radio, FormControlLabel, Button, Autocomplete, TextField, DialogTitle, DialogContent, DialogActions, CircularProgress } from "@mui/material";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import ConditioningSelector from "bin/conditioning/conditioningSelector";
|
||||
import { Component, Device, Interaction } from "models";
|
||||
import { Ambient } from "models/Ambient";
|
||||
import { Controller } from "models/Controller";
|
||||
import { Plenum } from "models/Plenum";
|
||||
import { pond, quack } from "protobuf-ts/pond";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ComponentSet } from "bin/conditioning/pickComponentSet"
|
||||
import { componentIDToString, sameComponentID } from "pbHelpers/Component";
|
||||
import moment from "moment";
|
||||
import { lowerCase } from "lodash";
|
||||
import { GetGrainExtensionMap } from "grain";
|
||||
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
|
||||
import { useGlobalState, useInteractionsAPI } from "providers";
|
||||
import ConditionDisplay from "./conditionDisplay";
|
||||
import React from "react";
|
||||
import { DeviceChangeData } from "./BinModeController";
|
||||
import { GrainCable } from "models/GrainCable";
|
||||
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return ({
|
||||
stepper: {
|
||||
padding: theme.spacing(0.5)
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
interface Step {
|
||||
label: string;
|
||||
completed?: boolean;
|
||||
}
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
device: Device;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
devices: Device[]
|
||||
deviceComponents: Map<number, Component[]>
|
||||
binPrefs?: Map<string, pond.BinComponentPreferences>
|
||||
grain?: pond.Grain
|
||||
customGrain?: pond.GrainSettings
|
||||
cancel: () => void
|
||||
confirm: (deviceData: DeviceChangeData[]) => void
|
||||
}
|
||||
|
||||
const steps = [{label: "Style"}, {label: "Device"}, {label: "Interaction"}]
|
||||
|
||||
//this is our simple drying mode the way it currently works, just building the dialog to be a little more trnasparent and customizable as to what the interaction is doing
|
||||
export default function DryingMode(props: Props){
|
||||
const {devices, deviceComponents, binPrefs, grain, customGrain, cancel, confirm} = props
|
||||
const grainExtensionMap = GetGrainExtensionMap();
|
||||
const classes = useStyles()
|
||||
const [{user}] = useGlobalState()
|
||||
const interactionAPI = useInteractionsAPI()
|
||||
const [dryingMethod, setDryingMethod] = useState("air")//whether they are using natural air or a heater
|
||||
const [options, setOptions] = useState<Option[]>([])
|
||||
const [deviceOption, setDeviceOption] = useState<Option>()
|
||||
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>()
|
||||
const [plenums, setPlenums] = useState<Plenum[]>([])
|
||||
const [ambients, setAmbients] = useState<Ambient[]>([])
|
||||
const [heaters, setHeaters] = useState<Controller[]>([])
|
||||
const [fans, setFans] = useState<Controller[]>([])
|
||||
const [cables, setCables] = useState<GrainCable[]>([])
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [interactionLoading,setInteractionsLoading] = useState(false)
|
||||
|
||||
// possibly all 4 of these will need to be passed up, at least the conflicting, toAdd, and sets will
|
||||
const [componentSets, setComponentSets] = useState<ComponentSet[]>([])
|
||||
const [existingInteractions, setExistingInteractions] = useState<Interaction[]>([])
|
||||
const [conflictingInteractions, setConflictingInteractions] = useState<Interaction[]>([])
|
||||
const [interactionsToAdd, setInteractionsToAdd] = useState<pond.MultiInteractionSettings>()
|
||||
|
||||
|
||||
const [sourceMap, setSourceMap] = useState<Map<string, Component>>(new Map());
|
||||
const [sinkMap, setSinkMap] = useState<Map<string, Component>>(new Map());
|
||||
|
||||
|
||||
|
||||
//sort the components according to the bin preferences
|
||||
useEffect(()=>{
|
||||
if (!selectedDevice) return;
|
||||
if (!deviceComponents.get(selectedDevice.id())) return;
|
||||
var plenums: Plenum[] = [];
|
||||
var ambients: Ambient[] = [];
|
||||
var grainCables: GrainCable[] = [];
|
||||
var heaters: Controller[] = [];
|
||||
var fans: Controller[] = [];
|
||||
var sinkMap: Map<string, Component> = new Map()
|
||||
var sourceMap: Map<string, Component> = new Map()
|
||||
|
||||
deviceComponents.get(selectedDevice.id())!.forEach(comp => {
|
||||
let pref = binPrefs?.get(comp.key());
|
||||
if (pref) {
|
||||
if (pref.type) {
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_PLENUM){
|
||||
plenums.push(Plenum.create(comp));
|
||||
sourceMap.set(comp.locationString(), comp)
|
||||
}
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_AMBIENT){
|
||||
ambients.push(Ambient.create(comp));
|
||||
sourceMap.set(comp.locationString(), comp)
|
||||
}
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE){
|
||||
grainCables.push(GrainCable.create(comp));
|
||||
sourceMap.set(comp.locationString(), comp)
|
||||
}
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_HEATER) {
|
||||
let heater = Controller.create(comp);
|
||||
heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF;
|
||||
heaters.push(heater);
|
||||
sinkMap.set(comp.locationString(), comp)
|
||||
}
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_FAN) {
|
||||
let fan = Controller.create(comp)
|
||||
fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF;
|
||||
fans.push(fan);
|
||||
sinkMap.set(comp.locationString(), comp)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
setPlenums(plenums);
|
||||
setAmbients(ambients);
|
||||
setHeaters(heaters);
|
||||
setFans(fans);
|
||||
setSourceMap(sourceMap)
|
||||
setSinkMap(sinkMap)
|
||||
setCables(grainCables)
|
||||
//also load the interactions for the selected device so that we can find any conflicting ones
|
||||
setInteractionsLoading(true)
|
||||
interactionAPI.listInteractionsByDevice(selectedDevice.id()).then(resp => {
|
||||
setExistingInteractions(resp)
|
||||
}).finally(() => {
|
||||
setInteractionsLoading(false)
|
||||
})
|
||||
}, [deviceComponents, selectedDevice, binPrefs]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let o: Option[] = [];
|
||||
devices.forEach(device => {
|
||||
let newOption: Option = {device: device, label: device.name()}
|
||||
if(deviceOption === undefined) {
|
||||
setDeviceOption(newOption)
|
||||
setSelectedDevice(device)
|
||||
}
|
||||
o.push(newOption);
|
||||
});
|
||||
setOptions(o);
|
||||
}, [devices, setOptions]);
|
||||
|
||||
const buildHeaterInteraction = (
|
||||
sensor: Plenum | Ambient,
|
||||
heater: Controller,
|
||||
): pond.InteractionSettings => {
|
||||
let interaction = pond.InteractionSettings.create({
|
||||
source: sensor.location(),
|
||||
sink: heater.location(),
|
||||
schedule: pond.InteractionSchedule.create({
|
||||
timeOfDayStart: "00:00",
|
||||
timeOfDayEnd: "24:00",
|
||||
timezone: moment.tz.guess(),
|
||||
weekdays: moment.weekdays().map(d => lowerCase(d))
|
||||
}),
|
||||
notifications: pond.InteractionNotifications.create({
|
||||
reports: true
|
||||
}),
|
||||
result: pond.InteractionResult.create({
|
||||
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
|
||||
value: 1
|
||||
})
|
||||
});
|
||||
|
||||
let temp = 0;
|
||||
let hum = 0;
|
||||
|
||||
if(grain){
|
||||
//get the values using the grain type
|
||||
let ext = grainExtensionMap.get(grain)
|
||||
if (ext) {
|
||||
temp = ext.setTempC
|
||||
hum = ext.targetMC
|
||||
}
|
||||
}else{//otherwise use the default drying interaction
|
||||
temp = 40
|
||||
hum = 20
|
||||
}
|
||||
|
||||
let conditions = [];
|
||||
let humidityCondition = pond.InteractionCondition.create({
|
||||
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
||||
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
|
||||
value: Math.round(
|
||||
describeMeasurement(
|
||||
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
||||
sensor.settings.type,
|
||||
sensor.settings.subtype,
|
||||
undefined,
|
||||
user
|
||||
).toStored(hum)
|
||||
)
|
||||
});
|
||||
conditions.push(humidityCondition);
|
||||
|
||||
//since the measurement describers function to stored does a conversion into celsius if the users pref is fahrenheit for temperature we need to convert the preset value into fahrenheit
|
||||
if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
||||
temp = Math.round((temp * (9 / 5) + 32) * 100) / 100;
|
||||
}
|
||||
let tempVal = describeMeasurement(
|
||||
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
||||
sensor.settings.type,
|
||||
sensor.settings.subtype,
|
||||
undefined,
|
||||
user
|
||||
).toStored(temp);
|
||||
|
||||
let tempCondition = pond.InteractionCondition.create({
|
||||
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
||||
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
||||
value: Math.round(tempVal) //and then round the converted value since interaction conditions wont take floats
|
||||
});
|
||||
conditions.push(tempCondition);
|
||||
interaction.conditions = conditions;
|
||||
//the the heater output to auto so that when the components are update it uses the new mode
|
||||
heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO
|
||||
return interaction;
|
||||
};
|
||||
|
||||
const buildFanInteraction = (
|
||||
sensor: Plenum | Ambient,
|
||||
fan: Controller,
|
||||
tempComparison: "greater" | "less",
|
||||
humidityComparison: "greater" | "less",
|
||||
) => {
|
||||
let interaction = pond.InteractionSettings.create({
|
||||
source: sensor.location(),
|
||||
sink: fan.location(),
|
||||
schedule: pond.InteractionSchedule.create({
|
||||
timeOfDayStart: "00:00",
|
||||
timeOfDayEnd: "24:00",
|
||||
timezone: moment.tz.guess(),
|
||||
weekdays: moment.weekdays().map(d => lowerCase(d))
|
||||
}),
|
||||
notifications: pond.InteractionNotifications.create({
|
||||
reports: true
|
||||
}),
|
||||
result: pond.InteractionResult.create({
|
||||
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
|
||||
value: 1
|
||||
})
|
||||
});
|
||||
|
||||
let tempPreset = 0;
|
||||
let humidityPreset = 0;
|
||||
if(grain){
|
||||
//get the values using the grain type
|
||||
let ext = grainExtensionMap.get(grain)
|
||||
if (ext) {
|
||||
tempPreset = ext.setTempC
|
||||
humidityPreset = ext.targetMC
|
||||
}
|
||||
}else{//otherwise use the default drying interaction
|
||||
tempPreset = 40
|
||||
humidityPreset = 20
|
||||
}
|
||||
|
||||
|
||||
let conditions = [];
|
||||
let fanConditionOne = pond.InteractionCondition.create({
|
||||
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
||||
comparison:
|
||||
humidityComparison === "greater"
|
||||
? quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
|
||||
: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
||||
value: describeMeasurement(
|
||||
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
||||
sensor.settings.type,
|
||||
sensor.settings.subtype,
|
||||
undefined,
|
||||
user
|
||||
).toStored(humidityPreset)
|
||||
});
|
||||
conditions.push(fanConditionOne);
|
||||
|
||||
//since the measurement describers function toStored does a conversion into celsius for temperature if the users pref is fahrenheit we need to convert the preset value into fahrenheit
|
||||
if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
||||
tempPreset = Math.round((tempPreset * (9 / 5) + 32) * 100) / 100;
|
||||
}
|
||||
let tempVal = describeMeasurement(
|
||||
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
||||
sensor.settings.type,
|
||||
sensor.settings.subtype,
|
||||
undefined,
|
||||
user
|
||||
).toStored(tempPreset);
|
||||
|
||||
let fanConditionTwo = pond.InteractionCondition.create({
|
||||
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
||||
comparison:
|
||||
tempComparison === "greater"
|
||||
? quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
|
||||
: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
||||
value: Math.round(tempVal) //and then round the converted value since the interaction does not take decimals
|
||||
});
|
||||
conditions.push(fanConditionTwo);
|
||||
|
||||
interaction.conditions = conditions;
|
||||
//set the output mode to auto in the fan so that when the components are updated it uses the new mode
|
||||
fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO
|
||||
return interaction;
|
||||
};
|
||||
|
||||
//this function just takes in the sets as they change and creates the list of conflicting interactions and the settings to create the new ones and returns them
|
||||
const createInteractions = (sets: ComponentSet[]) => {
|
||||
console.log("create interactions")
|
||||
if(!selectedDevice) return undefined //if there is no device that was selected, do nothing
|
||||
let multiInteractionSettings: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create()
|
||||
let conflictingInteractions: Interaction[] = []
|
||||
let linkedComponents = deviceComponents.get(selectedDevice.id());
|
||||
//loop through the sets to find interactions to remove as well as set the new ones
|
||||
sets.forEach((set) => {
|
||||
//loop through the controllers
|
||||
set.controllers.forEach(controller => {
|
||||
//filter the conflicting interactions for that controller
|
||||
let c = existingInteractions.filter(i => {
|
||||
let conflicting = false;
|
||||
if (linkedComponents) {
|
||||
linkedComponents.forEach(comp => {
|
||||
if (sameComponentID(comp.location(), i.settings.sink)) {
|
||||
conflicting = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return conflicting;
|
||||
});
|
||||
|
||||
conflictingInteractions = conflictingInteractions.concat(c)
|
||||
// if the controller is a heater create heater interaction
|
||||
if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){
|
||||
multiInteractionSettings.interactions.push(buildHeaterInteraction(set.sensor, controller))
|
||||
}else if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN ||
|
||||
controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_EXHAUST_FAN){// if the controller is a fan
|
||||
//if it is a combination set using a heater as well just turn the fan on
|
||||
if(set.combo){
|
||||
controller.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_ON
|
||||
}else{
|
||||
//otherwise make a fan interaction
|
||||
multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "greater", "less"))
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
console.log(cables)
|
||||
cables.forEach(cable => {
|
||||
console.log(cable.name())
|
||||
let c = existingInteractions.filter(i => {
|
||||
if (sameComponentID(cable.location(), i.settings.source) && !i.settings.sink && i.settings.notifications?.notify) {
|
||||
return true
|
||||
}
|
||||
});
|
||||
conflictingInteractions = conflictingInteractions.concat(c)
|
||||
})
|
||||
|
||||
setComponentSets([...sets])
|
||||
return {
|
||||
conflicting: conflictingInteractions,
|
||||
toAdd: multiInteractionSettings
|
||||
}
|
||||
}
|
||||
|
||||
const stepper = () => {
|
||||
return (
|
||||
<Stepper
|
||||
activeStep={currentStep}
|
||||
alternativeLabel
|
||||
classes={{
|
||||
root: classes.stepper
|
||||
}}>
|
||||
{steps.map((s, i) => (
|
||||
<Step key={i}>
|
||||
<StepLabel>{s.label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
);
|
||||
}
|
||||
|
||||
const styleStep = () => {
|
||||
return (
|
||||
<Box>
|
||||
<Typography>Select your drying method</Typography>
|
||||
<RadioGroup
|
||||
value={dryingMethod}
|
||||
onChange={(_, value) => {
|
||||
setDryingMethod(value)
|
||||
}}>
|
||||
<FormControlLabel
|
||||
control={<Radio />}
|
||||
value={"air"}
|
||||
label={"Natural Air"}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Radio />}
|
||||
value={"heat"}
|
||||
label={"Supplemental Heat"}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const deviceSelector = () => {
|
||||
return (
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
options={options}
|
||||
value={deviceOption}
|
||||
fullWidth
|
||||
getOptionLabel={option => option.label || ""}
|
||||
onChange={(_, newValue) => {
|
||||
if(newValue){
|
||||
setSelectedDevice(newValue.device);
|
||||
}
|
||||
}}
|
||||
renderInput={params => <TextField {...params} variant="outlined" label="Device" />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const deviceStep = () => {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography>Select the device to condition the bin</Typography>
|
||||
{deviceSelector()}
|
||||
{interactionLoading ? <CircularProgress /> :
|
||||
<ConditioningSelector
|
||||
plenums={plenums}
|
||||
ambients={ambients}
|
||||
fans={fans}
|
||||
heaters={heaters}
|
||||
//presets={presets}
|
||||
binMode={pond.BinMode.BIN_MODE_DRYING}
|
||||
updateSets={(sets) => {
|
||||
//update the component sets that will be used for the interactions
|
||||
//when the sets change it will create the interactions and any conflicting ones that need to be removed will be put into a list
|
||||
//then when the submit button gets clicked it will use the conflicting list and toAdd to remove conflicting interactions and add the new ones
|
||||
let i = createInteractions(sets)
|
||||
if(i !== undefined){
|
||||
setConflictingInteractions(i.conflicting)
|
||||
setInteractionsToAdd(i.toAdd)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
//this step will use the conflicting interactions and interactions to add to display them and allow users to make changes
|
||||
const interactionStep = () => {
|
||||
return (
|
||||
<Box>
|
||||
<Typography>Interactions</Typography>
|
||||
{interactionsToAdd?.interactions.map((interaction, index) => {
|
||||
let temp = Interaction.create()
|
||||
temp.settings = interaction
|
||||
let sink = sinkMap.get(componentIDToString(interaction.sink))
|
||||
let source = sourceMap.get(componentIDToString(interaction.source))
|
||||
if(sink && source && selectedDevice){
|
||||
return (
|
||||
<ConditionDisplay
|
||||
grain={grain}
|
||||
customGrain={customGrain}
|
||||
key={index}
|
||||
interaction={temp}//using temp because it wants a full interaction, not just its settings
|
||||
device={selectedDevice}
|
||||
sink={sink}
|
||||
source={source}
|
||||
changeConditions={(newSettings) => {
|
||||
console.log(newSettings)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const stepperContent = () => {
|
||||
switch(currentStep){
|
||||
case 1:
|
||||
return deviceStep()
|
||||
case 2:
|
||||
return interactionStep()
|
||||
default:
|
||||
return styleStep()
|
||||
}
|
||||
}
|
||||
|
||||
const actions = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* close - tells the parent to close the dialog without doing anything, always visible */}
|
||||
<Button onClick={cancel}>Close</Button>
|
||||
{/* back - goes back to the previous step, hidden on the forst step */}
|
||||
{currentStep !== 0 && <Button onClick={() => {setCurrentStep(currentStep-1)}}>Back</Button>}
|
||||
{/* next - goes to the next step, hidden on the last step */}
|
||||
{currentStep !== steps.length - 1 && <Button onClick={() => {setCurrentStep(currentStep+1)}}>Next</Button>}
|
||||
{/* confirm - tells the parent to build the stages using the data, only visible on the last step */}
|
||||
{currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && <Button onClick={() => {confirm([{ toRemove: conflictingInteractions, deviceId: selectedDevice.id(), toAdd: interactionsToAdd, controllers: componentSets.flatMap(set => set.controllers)}])}}>Confirm</Button>}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle>Bin Drying</DialogTitle>
|
||||
<DialogContent>
|
||||
{stepper()}
|
||||
{stepperContent()}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{actions()}
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
393
src/bin/binModes/HydratingMode.tsx
Normal file
393
src/bin/binModes/HydratingMode.tsx
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
import { Box, Theme, Stepper, Step, StepLabel, Typography, Button, Autocomplete, TextField, DialogTitle, DialogContent, DialogActions, CircularProgress } from "@mui/material";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import ConditioningSelector from "bin/conditioning/conditioningSelector";
|
||||
import { Component, Device, Interaction } from "models";
|
||||
import { Ambient } from "models/Ambient";
|
||||
import { Controller } from "models/Controller";
|
||||
import { Plenum } from "models/Plenum";
|
||||
import { pond, quack } from "protobuf-ts/pond";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ComponentSet } from "bin/conditioning/pickComponentSet"
|
||||
import { componentIDToString, sameComponentID } from "pbHelpers/Component";
|
||||
import moment from "moment";
|
||||
import { lowerCase } from "lodash";
|
||||
import { GetGrainExtensionMap } from "grain";
|
||||
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
|
||||
import { useGlobalState, useInteractionsAPI } from "providers";
|
||||
import ConditionDisplay from "./conditionDisplay";
|
||||
import React from "react";
|
||||
import { DeviceChangeData } from "./BinModeController";
|
||||
import { GrainCable } from "models/GrainCable";
|
||||
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return ({
|
||||
stepper: {
|
||||
padding: theme.spacing(0.5)
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
interface Step {
|
||||
label: string;
|
||||
completed?: boolean;
|
||||
}
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
device: Device;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
devices: Device[]
|
||||
deviceComponents: Map<number, Component[]>
|
||||
binPrefs?: Map<string, pond.BinComponentPreferences>
|
||||
grain?: pond.Grain
|
||||
customGrain?: pond.GrainSettings
|
||||
cancel: () => void
|
||||
confirm: (deviceData: DeviceChangeData[]) => void
|
||||
}
|
||||
|
||||
const steps = [{label: "Device"}, {label: "Interaction"}]
|
||||
|
||||
//this is our simple drying mode the way it currently works, just building the dialog to be a little more trnasparent and customizable as to what the interaction is doing
|
||||
export default function DryingMode(props: Props){
|
||||
const {devices, deviceComponents, binPrefs, grain, customGrain, cancel, confirm} = props
|
||||
const grainExtensionMap = GetGrainExtensionMap();
|
||||
const classes = useStyles()
|
||||
const [{user}] = useGlobalState()
|
||||
const interactionAPI = useInteractionsAPI()
|
||||
const [options, setOptions] = useState<Option[]>([])
|
||||
const [deviceOption, setDeviceOption] = useState<Option>({device: Device.create(), label: "Select Device"})
|
||||
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>()
|
||||
const [plenums, setPlenums] = useState<Plenum[]>([])
|
||||
const [ambients, setAmbients] = useState<Ambient[]>([])
|
||||
const [fans, setFans] = useState<Controller[]>([])
|
||||
const [cables, setCables] = useState<GrainCable[]>([])
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [interactionLoading,setInteractionsLoading] = useState(false)
|
||||
|
||||
// possibly all 4 of these will need to be passed up, at least the conflicting, toAdd, and sets will
|
||||
const [componentSets, setComponentSets] = useState<ComponentSet[]>([])
|
||||
const [existingInteractions, setExistingInteractions] = useState<Interaction[]>([])
|
||||
const [conflictingInteractions, setConflictingInteractions] = useState<Interaction[]>([])
|
||||
const [interactionsToAdd, setInteractionsToAdd] = useState<pond.MultiInteractionSettings>()
|
||||
|
||||
|
||||
const [sourceMap, setSourceMap] = useState<Map<string, Component>>(new Map());
|
||||
const [sinkMap, setSinkMap] = useState<Map<string, Component>>(new Map());
|
||||
|
||||
|
||||
|
||||
//sort the components according to the bin preferences
|
||||
useEffect(()=>{
|
||||
if (!selectedDevice) return;
|
||||
if (!deviceComponents.get(selectedDevice.id())) return;
|
||||
var plenums: Plenum[] = [];
|
||||
var ambients: Ambient[] = [];
|
||||
var grainCables: GrainCable[] = [];
|
||||
var fans: Controller[] = [];
|
||||
var sinkMap: Map<string, Component> = new Map()
|
||||
var sourceMap: Map<string, Component> = new Map()
|
||||
|
||||
deviceComponents.get(selectedDevice.id())!.forEach(comp => {
|
||||
let pref = binPrefs?.get(comp.key());
|
||||
if (pref) {
|
||||
if (pref.type) {
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_PLENUM){
|
||||
plenums.push(Plenum.create(comp));
|
||||
sourceMap.set(comp.locationString(), comp)
|
||||
}
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_AMBIENT){
|
||||
ambients.push(Ambient.create(comp));
|
||||
sourceMap.set(comp.locationString(), comp)
|
||||
}
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE){
|
||||
grainCables.push(GrainCable.create(comp));
|
||||
sourceMap.set(comp.locationString(), comp)
|
||||
}
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_FAN) {
|
||||
let fan = Controller.create(comp)
|
||||
fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF;
|
||||
fans.push(fan);
|
||||
sinkMap.set(comp.locationString(), comp)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
setPlenums(plenums);
|
||||
setAmbients(ambients);
|
||||
setFans(fans);
|
||||
setSourceMap(sourceMap)
|
||||
setSinkMap(sinkMap)
|
||||
setCables(grainCables)
|
||||
//also load the interactions for the selected device so that we can find any conflicting ones
|
||||
setInteractionsLoading(true)
|
||||
interactionAPI.listInteractionsByDevice(selectedDevice.id()).then(resp => {
|
||||
setExistingInteractions(resp)
|
||||
}).finally(() => {
|
||||
setInteractionsLoading(false)
|
||||
})
|
||||
}, [deviceComponents, selectedDevice, binPrefs]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let o: Option[] = [];
|
||||
devices.forEach((device, i) => {
|
||||
let newOption: Option = {device: device, label: device.name()}
|
||||
if(i === 0) {
|
||||
setDeviceOption(newOption)
|
||||
setSelectedDevice(device)
|
||||
}
|
||||
o.push(newOption);
|
||||
});
|
||||
setOptions(o);
|
||||
}, [devices, setOptions]);
|
||||
|
||||
const buildFanInteraction = (
|
||||
sensor: Plenum | Ambient,
|
||||
fan: Controller,
|
||||
) => {
|
||||
let interaction = pond.InteractionSettings.create({
|
||||
source: sensor.location(),
|
||||
sink: fan.location(),
|
||||
schedule: pond.InteractionSchedule.create({
|
||||
timeOfDayStart: "00:00",
|
||||
timeOfDayEnd: "24:00",
|
||||
timezone: moment.tz.guess(),
|
||||
weekdays: moment.weekdays().map(d => lowerCase(d))
|
||||
}),
|
||||
notifications: pond.InteractionNotifications.create({
|
||||
reports: true
|
||||
}),
|
||||
result: pond.InteractionResult.create({
|
||||
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
|
||||
value: 1
|
||||
})
|
||||
});
|
||||
|
||||
let tempDefault = 25;
|
||||
let humidityDefault = 60;
|
||||
|
||||
|
||||
let conditions = [];
|
||||
let fanConditionOne = pond.InteractionCondition.create({
|
||||
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
||||
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
|
||||
value: describeMeasurement(
|
||||
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
||||
sensor.settings.type,
|
||||
sensor.settings.subtype,
|
||||
undefined,
|
||||
user
|
||||
).toStored(humidityDefault)
|
||||
});
|
||||
conditions.push(fanConditionOne);
|
||||
|
||||
//since the measurement describers function toStored does a conversion into celsius for temperature if the users pref is fahrenheit we need to convert the preset value into fahrenheit
|
||||
if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
||||
tempDefault = Math.round((tempDefault * (9 / 5) + 32) * 100) / 100;
|
||||
}
|
||||
let tempVal = describeMeasurement(
|
||||
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
||||
sensor.settings.type,
|
||||
sensor.settings.subtype,
|
||||
undefined,
|
||||
user
|
||||
).toStored(tempDefault);
|
||||
|
||||
let fanConditionTwo = pond.InteractionCondition.create({
|
||||
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
||||
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
||||
value: Math.round(tempVal) //and then round the converted value since the interaction does not take decimals
|
||||
});
|
||||
conditions.push(fanConditionTwo);
|
||||
|
||||
interaction.conditions = conditions;
|
||||
//set the output mode to auto in the fan so that when the components are updated it uses the new mode
|
||||
fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO
|
||||
return interaction;
|
||||
};
|
||||
|
||||
//this function just takes in the sets as they change and creates the list of conflicting interactions and the settings to create the new ones and returns them
|
||||
const createInteractions = (sets: ComponentSet[]) => {
|
||||
if(!selectedDevice) return undefined //if there is no device that was selected, do nothing
|
||||
let multiInteractionSettings: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create()
|
||||
let conflictingInteractions: Interaction[] = []
|
||||
let linkedComponents = deviceComponents.get(selectedDevice.id());
|
||||
//loop through the sets to find interactions to remove as well as set the new ones
|
||||
sets.forEach((set) => {
|
||||
//loop through the controllers
|
||||
set.controllers.forEach(controller => {
|
||||
//filter the conflicting interactions for that controller
|
||||
let c = existingInteractions.filter(i => {
|
||||
let conflicting = false;
|
||||
if (linkedComponents) {
|
||||
linkedComponents.forEach(comp => {
|
||||
if (sameComponentID(comp.location(), i.settings.sink)) {
|
||||
conflicting = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return conflicting;
|
||||
});
|
||||
|
||||
conflictingInteractions = conflictingInteractions.concat(c)
|
||||
multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller))
|
||||
})
|
||||
})
|
||||
cables.forEach(cable => {
|
||||
console.log(cable.name())
|
||||
let c = existingInteractions.filter(i => {
|
||||
if (sameComponentID(cable.location(), i.settings.source) && !i.settings.sink && i.settings.notifications?.notify) {
|
||||
return true
|
||||
}
|
||||
});
|
||||
conflictingInteractions = conflictingInteractions.concat(c)
|
||||
})
|
||||
|
||||
setComponentSets([...sets])
|
||||
return {
|
||||
conflicting: conflictingInteractions,
|
||||
toAdd: multiInteractionSettings
|
||||
}
|
||||
}
|
||||
|
||||
const stepper = () => {
|
||||
return (
|
||||
<Stepper
|
||||
activeStep={currentStep}
|
||||
alternativeLabel
|
||||
classes={{
|
||||
root: classes.stepper
|
||||
}}>
|
||||
{steps.map((s, i) => (
|
||||
<Step key={i}>
|
||||
<StepLabel>{s.label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const deviceSelector = () => {
|
||||
return (
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
options={options}
|
||||
value={deviceOption}
|
||||
fullWidth
|
||||
getOptionLabel={option => option.label || ""}
|
||||
onChange={(_, newValue) => {
|
||||
if(newValue){
|
||||
setSelectedDevice(newValue.device);
|
||||
}
|
||||
}}
|
||||
renderInput={params => <TextField {...params} variant="outlined" label="Device" />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const deviceStep = () => {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography>Select the device to condition the bin</Typography>
|
||||
{deviceSelector()}
|
||||
{interactionLoading ? <CircularProgress /> :
|
||||
<ConditioningSelector
|
||||
plenums={plenums}
|
||||
ambients={ambients}
|
||||
fans={fans}
|
||||
heaters={[]}
|
||||
//presets={presets}
|
||||
binMode={pond.BinMode.BIN_MODE_HYDRATING}
|
||||
updateSets={(sets) => {
|
||||
//update the component sets that will be used for the interactions
|
||||
//when the sets change it will create the interactions and any conflicting ones that need to be removed will be put into a list
|
||||
//then when the submit button gets clicked it will use the conflicting list and toAdd to remove conflicting interactions and add the new ones
|
||||
let i = createInteractions(sets)
|
||||
if(i !== undefined){
|
||||
setConflictingInteractions(i.conflicting)
|
||||
setInteractionsToAdd(i.toAdd)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
//this step will use the conflicting interactions and interactions to add to display them and allow users to make changes
|
||||
const interactionStep = () => {
|
||||
console.log(interactionsToAdd)
|
||||
return (
|
||||
<Box>
|
||||
<Typography>Interactions</Typography>
|
||||
{interactionsToAdd?.interactions.map((interaction, index) => {
|
||||
let temp = Interaction.create()
|
||||
temp.settings = interaction
|
||||
let sink = sinkMap.get(componentIDToString(interaction.sink))
|
||||
let source = sourceMap.get(componentIDToString(interaction.source))
|
||||
if(sink && source && selectedDevice){
|
||||
return (
|
||||
<ConditionDisplay
|
||||
key={index}
|
||||
grain={grain}
|
||||
customGrain={customGrain}
|
||||
interaction={temp}
|
||||
device={selectedDevice}
|
||||
sink={sink}
|
||||
source={source}
|
||||
changeConditions={(newSettings) => {
|
||||
console.log(newSettings)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const stepperContent = () => {
|
||||
switch(currentStep){
|
||||
case 1:
|
||||
return interactionStep()
|
||||
default:
|
||||
return deviceStep()
|
||||
}
|
||||
}
|
||||
|
||||
const actions = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* close - tells the parent to close the dialog without doing anything, always visible */}
|
||||
<Button onClick={cancel}>Close</Button>
|
||||
{/* back - goes back to the previous step, hidden on the forst step */}
|
||||
{currentStep !== 0 && <Button onClick={() => {setCurrentStep(currentStep-1)}}>Back</Button>}
|
||||
{/* next - goes to the next step, hidden on the last step */}
|
||||
{currentStep !== steps.length - 1 && <Button onClick={() => {setCurrentStep(currentStep+1)}}>Next</Button>}
|
||||
{/* confirm - tells the parent to build the stages using the data, only visible on the last step */}
|
||||
{currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && <Button onClick={() => {confirm([{ toRemove: conflictingInteractions, deviceId: selectedDevice.id(), toAdd: interactionsToAdd, controllers: componentSets.flatMap(set => set.controllers)}])}}>Confirm</Button>}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle>Bin Hydration</DialogTitle>
|
||||
<DialogContent>
|
||||
{stepper()}
|
||||
{stepperContent()}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{actions()}
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
183
src/bin/binModes/StorageMode.tsx
Normal file
183
src/bin/binModes/StorageMode.tsx
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
import { DialogContent, DialogTitle, DialogActions, Button } from "@mui/material";
|
||||
import { Bin, Component, Device, Interaction } from "models";
|
||||
import { Ambient } from "models/Ambient";
|
||||
import { Controller } from "models/Controller";
|
||||
import { GrainCable } from "models/GrainCable";
|
||||
import { Plenum } from "models/Plenum";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { quack } from "protobuf-ts/quack";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { DeviceChangeData } from "./BinModeController";
|
||||
import { useInteractionsAPI } from "providers";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { componentIDToString } from "pbHelpers/Component";
|
||||
import { ComponentSet } from "bin/conditioning/pickComponentSet";
|
||||
import moment from "moment";
|
||||
import { lowerCase } from "lodash";
|
||||
|
||||
interface Props {
|
||||
// the devices connected to the bin
|
||||
devices: Device[]
|
||||
deviceComponents: Map<number, Component[]>
|
||||
bin: Bin
|
||||
binPrefs?: Map<string, pond.BinComponentPreferences>
|
||||
confirm: (deviceData: DeviceChangeData[]) => void
|
||||
cancel: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* when a bin goes into storage mode then the auto top node needs to be set, should be handled in the controller actually
|
||||
* interactions with connected controllers as sink should be deleted, and controllers should be set to off
|
||||
* @returns
|
||||
*/
|
||||
export default function StorageMode(props: Props){
|
||||
const {devices, deviceComponents, bin, binPrefs, confirm, cancel} = props
|
||||
const interactionsAPI = useInteractionsAPI()
|
||||
const [data, setData] = useState<DeviceChangeData[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [loadErr, setLoadErr] = useState(false)
|
||||
|
||||
useEffect(()=>{
|
||||
let interactionPromises: Promise<Interaction[]>[] = []
|
||||
let deviceContext: {device: Device, plenums: Plenum[], ambients: Ambient[], cables: GrainCable[], controllers: Controller[]}[] = []
|
||||
devices.forEach(device => {
|
||||
if (!deviceComponents.get(device.id())) return;
|
||||
let plenums: Plenum[] = []
|
||||
let ambients: Ambient[] = []
|
||||
let cables: GrainCable[] = [] //since this mode is going to add notification interactions to cables they should be filtered as well
|
||||
let controllers: Controller[] = [] //because storage is just setting things to false i dont think i need to seperate fans from heaters
|
||||
//sort the components into the plenum, ambient, grain cable, and controller arrays
|
||||
deviceComponents.get(device.id())!.forEach(comp => {
|
||||
let pref = binPrefs?.get(comp.key());
|
||||
if (pref) {
|
||||
if (pref.type) {
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_PLENUM){
|
||||
plenums.push(Plenum.create(comp));
|
||||
//sourceMap.set(comp.locationString(), comp)
|
||||
}
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_AMBIENT){
|
||||
ambients.push(Ambient.create(comp));
|
||||
//sourceMap.set(comp.locationString(), comp)
|
||||
}
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_HEATER || pref.type === pond.BinComponent.BIN_COMPONENT_FAN) {
|
||||
let controller = Controller.create(comp);
|
||||
controller.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF;
|
||||
controllers.push(controller);
|
||||
//sinkMap.set(comp.locationString(), comp)
|
||||
}
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE){
|
||||
let cable = GrainCable.create(comp)
|
||||
cables.push(cable)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
//after sorting it will need to load the interactions for the device
|
||||
deviceContext.push({device, plenums, ambients, cables, controllers})
|
||||
interactionPromises.push(interactionsAPI.listInteractionsByDevice(device.id()))
|
||||
})
|
||||
setLoading(true)
|
||||
let deviceChangeData: DeviceChangeData[] = []
|
||||
Promise.all(interactionPromises).then(resp => {
|
||||
//the index of this should match the device index in the devices array
|
||||
resp.forEach((interactions,i) => {
|
||||
let toRemove: Interaction[] = []
|
||||
let ctx = deviceContext[i]
|
||||
let sensorsAddr = ctx.plenums.concat(ctx.ambients).map(sensor => sensor.locationString())
|
||||
let controllerAddr = ctx.controllers.map(cont => cont.locationString())
|
||||
let cableAddr = ctx.cables.map(cab => cab.locationString())
|
||||
let cablesWithInteraction = new Set<string>()
|
||||
|
||||
interactions.forEach(interaction => {
|
||||
//first check if the interaction has a sink in the devices fans or heaters and is being controlled by a plenum or ambient
|
||||
//this should make it so that it only removes conditioning interactions and not all interactions on the device
|
||||
let source = interaction.settings.source
|
||||
let sink = interaction.settings.sink
|
||||
//this checks if the interaction is a 'conditioning' interaction that needs to be removed
|
||||
if(source && sink){
|
||||
if(controllerAddr.includes(componentIDToString(sink)) && sensorsAddr.includes(componentIDToString(source))){
|
||||
toRemove.push(interaction)
|
||||
}
|
||||
}
|
||||
//need to make sure that it is a notification only interaction and not a controlling one
|
||||
//it is possible to control things with cables and we want to leave those alone and not consider them notification interactions
|
||||
//the reason we check that there is no sink is because even control interactions can still send notifications
|
||||
//we want interactions that effectively ONLY send notifications
|
||||
if (source && !sink && cableAddr.includes(componentIDToString(source))) {
|
||||
//check if notifications is true
|
||||
if(interaction.settings.notifications?.notify){
|
||||
cablesWithInteraction.add(componentIDToString(source))
|
||||
}
|
||||
}
|
||||
})
|
||||
let cablesNeedingNotification = ctx.cables.filter(
|
||||
cable => !cablesWithInteraction.has(cable.locationString())
|
||||
)
|
||||
|
||||
let dd: DeviceChangeData = {
|
||||
deviceId: ctx.device.id(),
|
||||
toRemove: toRemove,
|
||||
controllers: ctx.controllers,
|
||||
|
||||
}
|
||||
|
||||
if(cablesNeedingNotification.length > 0){
|
||||
let toAdd: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create()
|
||||
cablesNeedingNotification.forEach(cable => {
|
||||
//create the new interaction here
|
||||
let notification: pond.InteractionSettings = pond.InteractionSettings.create({
|
||||
source: cable.location(),
|
||||
schedule: pond.InteractionSchedule.create({
|
||||
timeOfDayStart: "00:00",
|
||||
timeOfDayEnd: "24:00",
|
||||
timezone: moment.tz.guess(),
|
||||
weekdays: moment.weekdays().map(d => lowerCase(d))
|
||||
}),
|
||||
conditions: [pond.InteractionCondition.create({
|
||||
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
||||
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
|
||||
value: bin.settings.highTemp
|
||||
})],
|
||||
result: pond.InteractionResult.create({
|
||||
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT
|
||||
}),
|
||||
notifications: pond.InteractionNotifications.create({
|
||||
notify: true
|
||||
}),
|
||||
})
|
||||
|
||||
//push it to toAdd.interactions
|
||||
toAdd.interactions.push(notification)
|
||||
|
||||
})
|
||||
//add toAdd to dd
|
||||
dd.toAdd = toAdd
|
||||
}
|
||||
deviceChangeData.push(dd)
|
||||
})
|
||||
setData(deviceChangeData)
|
||||
}).catch(err => {
|
||||
//TODO: display a snackBar message saying there was an issue loading the data for one or more devices
|
||||
setData([])
|
||||
setLoadErr(true)
|
||||
}).finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
},[devices, binPrefs, deviceComponents])
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle>Storage Mode</DialogTitle>
|
||||
<DialogContent>
|
||||
{/* could have the explanation of what changing to storage mode does here */}
|
||||
Setting the bin to storage mode will remove any interactions for controllers connected to this bin and will set alerts on the cables that dont have any alerts set.
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => cancel}>Cancel</Button>
|
||||
<Button onClick={()=>{
|
||||
confirm(data)
|
||||
}} disabled={loading || loadErr}>Confirm</Button>
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
230
src/bin/binModes/ThreePhaseFanDry.tsx
Normal file
230
src/bin/binModes/ThreePhaseFanDry.tsx
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
import React, { useState } from "react"
|
||||
import { Autocomplete, Box, Button, CircularProgress, DialogActions, DialogContent, DialogTitle, Step, StepLabel, Stepper, TextField, Theme, Typography } from "@mui/material"
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import ConditioningSelector from "bin/conditioning/conditioningSelector";
|
||||
import { Component, Device, Interaction } from "models";
|
||||
import { Plenum } from "models/Plenum";
|
||||
import { Ambient } from "models/Ambient";
|
||||
import { Controller } from "models/Controller";
|
||||
import { GrainCable } from "models/GrainCable";
|
||||
import { ComponentSet } from "bin/conditioning/pickComponentSet";
|
||||
import { sameComponentID } from "pbHelpers/Component";
|
||||
|
||||
|
||||
const steps = [{label: "Device"}, {label: "Phase 1"}, {label: "Phase 2"}, {label: "Phase 3"}]
|
||||
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return ({
|
||||
stepper: {
|
||||
padding: theme.spacing(0.5)
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
device: Device;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
deviceComponents: Map<number, Component[]>
|
||||
}
|
||||
|
||||
export default function ThreePhaseFanDry(props: Props){
|
||||
const {deviceComponents} = props
|
||||
const classes = useStyles()
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [options, setOptions] = useState<Option[]>([])
|
||||
const [deviceOption, setDeviceOption] = useState<Option>({device: Device.create(), label: "Select Device"})
|
||||
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>()
|
||||
const [interactionsLoading,setInteractionsLoading] = useState(false)
|
||||
const [plenums, setPlenums] = useState<Plenum[]>([])
|
||||
const [ambients, setAmbients] = useState<Ambient[]>([])
|
||||
const [fans, setFans] = useState<Controller[]>([])
|
||||
const [cables, setCables] = useState<GrainCable[]>([])
|
||||
const [existingInteractions, setExistingInteractions] = useState<Interaction[]>([])
|
||||
const [componentSets, setComponentSets] = useState<ComponentSet[]>([])
|
||||
const [conflictingInteractions, setConflictingInteractions] = useState<Interaction[]>([])
|
||||
const [interactionsToAdd, setInteractionsToAdd] = useState<pond.MultiInteractionSettings>()
|
||||
|
||||
|
||||
|
||||
//each of the functions that builds the interactions
|
||||
const buildPhase1Interaction = () => {}
|
||||
const buildPhase2Interaction = () => {}
|
||||
const buildPhase3Interaction = () => {}
|
||||
|
||||
const phase1Step = () => {
|
||||
return (
|
||||
<React.Fragment></React.Fragment>
|
||||
)
|
||||
}
|
||||
const phase2Step = () => {
|
||||
return (
|
||||
<React.Fragment></React.Fragment>
|
||||
)
|
||||
}
|
||||
const phase3Step = () => {
|
||||
return (
|
||||
<React.Fragment></React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
const deviceSelector = () => {
|
||||
return (
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
options={options}
|
||||
value={deviceOption}
|
||||
fullWidth
|
||||
getOptionLabel={option => option.label || ""}
|
||||
onChange={(_, newValue) => {
|
||||
if(newValue){
|
||||
setSelectedDevice(newValue.device);
|
||||
}
|
||||
}}
|
||||
renderInput={params => <TextField {...params} variant="outlined" label="Device" />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
//this function just takes in the sets as they change and creates the list of conflicting interactions and the settings to create the new ones and returns them
|
||||
const createInteractions = (sets: ComponentSet[]) => {
|
||||
if(!selectedDevice) return undefined //if there is no device that was selected, do nothing
|
||||
let multiInteractionSettings: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create()
|
||||
let conflictingInteractions: Interaction[] = []
|
||||
let linkedComponents = deviceComponents.get(selectedDevice.id());
|
||||
//loop through the sets to find interactions to remove as well as set the new ones
|
||||
sets.forEach((set) => {
|
||||
//loop through the controllers
|
||||
set.controllers.forEach(controller => {
|
||||
//filter the conflicting interactions for that controller
|
||||
let c = existingInteractions.filter(i => {
|
||||
let conflicting = false;
|
||||
if (linkedComponents) {
|
||||
linkedComponents.forEach(comp => {
|
||||
if (sameComponentID(comp.location(), i.settings.sink)) {
|
||||
conflicting = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return conflicting;
|
||||
});
|
||||
|
||||
conflictingInteractions = conflictingInteractions.concat(c)
|
||||
//add the first phase interaction
|
||||
// multiInteractionSettings.interactions.push()
|
||||
//add the second phase interaction
|
||||
// multiInteractionSettings.interactions.push()
|
||||
//add the third phase interaction
|
||||
// multiInteractionSettings.interactions.push()
|
||||
})
|
||||
})
|
||||
|
||||
//removes notification interactions on the cables
|
||||
cables.forEach(cable => {
|
||||
console.log(cable.name())
|
||||
let c = existingInteractions.filter(i => {
|
||||
if (sameComponentID(cable.location(), i.settings.source) && !i.settings.sink && i.settings.notifications?.notify) {
|
||||
return true
|
||||
}
|
||||
});
|
||||
conflictingInteractions = conflictingInteractions.concat(c)
|
||||
})
|
||||
|
||||
setComponentSets([...sets])
|
||||
return {
|
||||
conflicting: conflictingInteractions,
|
||||
toAdd: multiInteractionSettings
|
||||
}
|
||||
}
|
||||
|
||||
const deviceStep = () => {
|
||||
return (
|
||||
<Box>
|
||||
<Typography>Select the device to condition the bin</Typography>
|
||||
{deviceSelector()}
|
||||
{interactionsLoading ? <CircularProgress /> :
|
||||
<ConditioningSelector
|
||||
plenums={plenums}
|
||||
ambients={ambients}
|
||||
fans={fans}
|
||||
heaters={[]}
|
||||
binMode={pond.BinMode.BIN_MODE_THREE_PHASE_FAN_DRYING}
|
||||
updateSets={(sets) => {
|
||||
//update the component sets that will be used for the interactions
|
||||
//when the sets change it will create the interactions and any conflicting ones that need to be removed will be put into a list
|
||||
//then when the submit button gets clicked it will use the conflicting list and toAdd to remove conflicting interactions and add the new ones
|
||||
let i = createInteractions(sets)
|
||||
if(i !== undefined){
|
||||
setConflictingInteractions(i.conflicting)
|
||||
setInteractionsToAdd(i.toAdd)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const actions = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* close - tells the parent to close the dialog without doing anything, always visible */}
|
||||
<Button>Close</Button>
|
||||
{/* back - goes back to the previous step, hidden on the forst step */}
|
||||
{currentStep !== 0 && <Button onClick={() => {setCurrentStep(currentStep-1)}}>Back</Button>}
|
||||
{/* next - goes to the next step, hidden on the last step */}
|
||||
{currentStep !== steps.length - 1 && <Button onClick={() => {setCurrentStep(currentStep+1)}}>Next</Button>}
|
||||
{/* confirm - tells the parent to build the stages using the data, only visible on the last step */}
|
||||
{currentStep === steps.length - 1 && <Button onClick={() => {}}>Confirm</Button>}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
const stepper = () => {
|
||||
return (
|
||||
<Stepper
|
||||
activeStep={currentStep}
|
||||
alternativeLabel
|
||||
classes={{
|
||||
root: classes.stepper
|
||||
}}>
|
||||
{steps.map((s, i) => (
|
||||
<Step key={i}>
|
||||
<StepLabel>{s.label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
);
|
||||
}
|
||||
|
||||
const stepperContent = () => {
|
||||
switch(currentStep){
|
||||
case 1:
|
||||
return phase1Step()
|
||||
case 2:
|
||||
return phase2Step()
|
||||
case 3:
|
||||
return phase3Step()
|
||||
default:
|
||||
return deviceStep()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle>Bin Cooldown</DialogTitle>
|
||||
<DialogContent>
|
||||
{stepper()}
|
||||
{stepperContent()}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{actions()}
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
507
src/bin/binModes/conditionDisplay.tsx
Normal file
507
src/bin/binModes/conditionDisplay.tsx
Normal file
|
|
@ -0,0 +1,507 @@
|
|||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Box,
|
||||
darken,
|
||||
Grid,
|
||||
IconButton,
|
||||
Slider,
|
||||
Theme,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import { AddCircle, ExpandMore, RemoveCircleOutline } from "@mui/icons-material";
|
||||
import { ExtractMoisture } from "grain";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { Component, Device, Interaction } from "models";
|
||||
import { interactionConditionText, interactionResultText } from "pbHelpers/Interaction";
|
||||
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
|
||||
import { pond, quack } from "protobuf-ts/pond";
|
||||
import { useGlobalState } from "providers";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { avg, fahrenheitToCelsius } from "utils";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return ({
|
||||
displayBG: {
|
||||
marginTop: 10,
|
||||
background: darken(theme.palette.background.default, 0.05),
|
||||
borderRadius: 5,
|
||||
padding: 10
|
||||
},
|
||||
markContainer: {
|
||||
zIndex: 2
|
||||
},
|
||||
arrowDown: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeft: "10px solid transparent",
|
||||
borderRight: "10px solid transparent"
|
||||
},
|
||||
sliderRoot: {},
|
||||
sliderThumb: {
|
||||
height: 15,
|
||||
width: 15,
|
||||
backgroundColor: "yellow"
|
||||
},
|
||||
sliderTrack: {
|
||||
height: 3,
|
||||
backgroundColor: "white"
|
||||
},
|
||||
sliderRail: {
|
||||
height: 3,
|
||||
backgroundColor: "white"
|
||||
},
|
||||
sliderValLabel: {
|
||||
left: -20,
|
||||
top: 40,
|
||||
background: "transparent",
|
||||
"& *": {
|
||||
color: "#fff"
|
||||
}
|
||||
},
|
||||
sliderMark: {
|
||||
visibility: "hidden"
|
||||
},
|
||||
sliderMarked: {
|
||||
marginTop: 25,
|
||||
marginBottom: 0
|
||||
},
|
||||
sliderMarkLabel: {
|
||||
top: -25
|
||||
},
|
||||
groupHeader: {
|
||||
marginTop: 10,
|
||||
marginBottom: 5,
|
||||
fontWeight: 650
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
interface Props {
|
||||
interaction: Interaction;
|
||||
device: Device;
|
||||
source: Component;
|
||||
sink: Component;
|
||||
grain?: pond.Grain;
|
||||
customGrain?: pond.GrainSettings
|
||||
changeConditions?: (newSettings: pond.InteractionSettings) => void
|
||||
}
|
||||
|
||||
// A "corner" pairing: one temp condition + one humidity condition whose
|
||||
// comparison directions are consistent with each other, so together they
|
||||
// bound a single EMC value.
|
||||
// temp > X && humid < Y -> bounds EMC from ABOVE (an upper limit)
|
||||
// temp < X && humid > Y -> bounds EMC from BELOW (a lower limit)
|
||||
interface EMCCornerPair {
|
||||
temp: pond.InteractionCondition;
|
||||
humid: pond.InteractionCondition;
|
||||
}
|
||||
|
||||
interface PairedConditions {
|
||||
upperBoundPair?: EMCCornerPair; // temp > X && humid < Y
|
||||
lowerBoundPair?: EMCCornerPair; // temp < X && humid > Y
|
||||
unmatched: pond.InteractionCondition[];
|
||||
}
|
||||
|
||||
const isGTorEQ = (op: quack.RelationalOperator) =>
|
||||
op === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN ||
|
||||
op === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO;
|
||||
|
||||
const isLTorEQ = (op: quack.RelationalOperator) =>
|
||||
op === quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN ||
|
||||
op === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO;
|
||||
|
||||
// Groups the flat condition array into (at most) two EMC-bounding corners,
|
||||
// based purely on comparison direction, not array position/order.
|
||||
function pairEMCConditions(conditions: pond.InteractionCondition[]): PairedConditions {
|
||||
const temps = conditions.filter(
|
||||
c => c.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE
|
||||
);
|
||||
const humids = conditions.filter(
|
||||
c => c.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT
|
||||
);
|
||||
|
||||
const hiTemp = temps.find(c => isGTorEQ(c.comparison)); // temp > X
|
||||
const loTemp = temps.find(c => isLTorEQ(c.comparison)); // temp < X
|
||||
const loHumid = humids.find(c => isLTorEQ(c.comparison)); // humid < Y
|
||||
const hiHumid = humids.find(c => isGTorEQ(c.comparison)); // humid > Y
|
||||
|
||||
const result: PairedConditions = { unmatched: [] };
|
||||
|
||||
if (hiTemp && loHumid) result.upperBoundPair = { temp: hiTemp, humid: loHumid };
|
||||
if (loTemp && hiHumid) result.lowerBoundPair = { temp: loTemp, humid: hiHumid };
|
||||
|
||||
const paired = new Set<pond.InteractionCondition>(
|
||||
[hiTemp, loHumid, loTemp, hiHumid].filter((c): c is pond.InteractionCondition => !!c)
|
||||
);
|
||||
result.unmatched = conditions.filter(c => !paired.has(c));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default function ConditionDisplay(props: Props) {
|
||||
const { interaction, device, source, sink, grain, customGrain, changeConditions } = props;
|
||||
const [sliderVals, setSliderVals] = useState<Map<pond.InteractionCondition, number>>(new Map());
|
||||
const [sliderMarks, setSliderMarks] = useState<Map<quack.MeasurementType, number>>(new Map());
|
||||
// EMC bound(s) calculated from the temp/humidity condition pairs.
|
||||
// If only one pair exists, only one of these will be set.
|
||||
const [upperEMC, setUpperEMC] = useState<number | undefined>();
|
||||
const [lowerEMC, setLowerEMC] = useState<number | undefined>();
|
||||
const [{ user }] = useGlobalState();
|
||||
const classes = useStyles();
|
||||
// Bumped whenever conditions are added/removed so `pairs` recomputes.
|
||||
// Needed because pushing into interaction.settings.conditions mutates
|
||||
// the array in place without changing the `interaction` reference, so
|
||||
// useMemo keyed only on [interaction] would never see the new conditions.
|
||||
const [conditionsVersion, setConditionsVersion] = useState(0);
|
||||
|
||||
const pairs = useMemo(
|
||||
() => pairEMCConditions(interaction.conditions()),
|
||||
[interaction, conditionsVersion]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let passedInteraction = interaction;
|
||||
let vals: Map<pond.InteractionCondition, number> = new Map();
|
||||
let marks: Map<quack.MeasurementType, number> = new Map();
|
||||
passedInteraction.conditions().forEach(condition => {
|
||||
let describer = describeMeasurement(condition.measurementType, source.type(), undefined, undefined, user);
|
||||
// NOTE: toDisplay will convert the temp value to fahrenheit
|
||||
// keyed by condition object itself (not measurementType) since there
|
||||
// can now be more than one condition per measurement type
|
||||
vals.set(condition, describer.toDisplay(condition.value));
|
||||
});
|
||||
|
||||
source.status.lastGoodMeasurement.forEach(measurement => {
|
||||
let m = pond.UnitMeasurementsForComponent.fromObject(measurement);
|
||||
if (m.values[0]) {
|
||||
let markVal = avg(m.values[0].values);
|
||||
// do this since this is how interactions handle the values so that the slider can use the toDisplay method of the describer for the marks
|
||||
if (m.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) markVal = markVal * 10;
|
||||
if (m.type === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) markVal = markVal * 100;
|
||||
marks.set(m.type, markVal);
|
||||
}
|
||||
});
|
||||
setSliderVals(vals);
|
||||
setSliderMarks(marks);
|
||||
}, [interaction, source]);
|
||||
|
||||
// Given a corner pair, calculate the EMC that pair implies. Returns
|
||||
// undefined if grain info or either value isn't available yet.
|
||||
const emcForPair = (pair?: EMCCornerPair): number | undefined => {
|
||||
if (!pair || grain === undefined || grain === pond.Grain.GRAIN_INVALID) return undefined;
|
||||
|
||||
let temp = sliderVals.get(pair.temp);
|
||||
let hum = sliderVals.get(pair.humid);
|
||||
if (temp === undefined || hum === undefined) return undefined;
|
||||
|
||||
if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
||||
// the emc calc needs the temp to be in celsius
|
||||
temp = fahrenheitToCelsius(temp);
|
||||
}
|
||||
let emc = ExtractMoisture(grain, temp, hum, customGrain);
|
||||
return emc === hum ? undefined : emc;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setUpperEMC(emcForPair(pairs.upperBoundPair));
|
||||
setLowerEMC(emcForPair(pairs.lowerBoundPair));
|
||||
}, [sliderVals, grain, user, pairs]);
|
||||
|
||||
// Builds a new InteractionCondition with sensible defaults, offset away
|
||||
// from an existing reference condition's value so the new slider doesn't
|
||||
// start stacked exactly on top of the one it's paired against.
|
||||
// ASSUMPTION: InteractionCondition is a plain settable class instance,
|
||||
// matching how the rest of this file mutates `condition.value` directly.
|
||||
// If your codebase instead uses a factory (e.g. pond.InteractionCondition.create(...)),
|
||||
// swap the `new ...()` + assignment below for that factory call.
|
||||
const buildCondition = (
|
||||
measurementType: quack.MeasurementType,
|
||||
comparison: quack.RelationalOperator,
|
||||
displayValue: number
|
||||
): pond.InteractionCondition => {
|
||||
let describer = describeMeasurement(measurementType, source?.type(), source.subType(), undefined, user);
|
||||
const condition = new pond.InteractionCondition();
|
||||
condition.measurementType = measurementType;
|
||||
condition.comparison = comparison;
|
||||
condition.value = Math.round(describer.toStored(displayValue));
|
||||
return condition;
|
||||
};
|
||||
|
||||
// Adds the missing opposite corner pair (temp + humidity condition) so
|
||||
// the EMC display becomes a "between" range instead of a single bound.
|
||||
// Only relevant when exactly one clean corner pair currently exists.
|
||||
const addOppositeCornerPair = () => {
|
||||
const existing = pairs.upperBoundPair ?? pairs.lowerBoundPair;
|
||||
if (!existing) return; // nothing to mirror off of -- shouldn't happen if button is only shown in this case
|
||||
|
||||
const addingLowerBound = !!pairs.upperBoundPair; // if we have the "less than" pair, we're adding the "greater than" one
|
||||
const tempComparison = addingLowerBound
|
||||
? quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN
|
||||
: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN;
|
||||
const humidComparison = addingLowerBound
|
||||
? quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
|
||||
: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN;
|
||||
|
||||
const tempDescriber = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, source?.type(), source.subType(), undefined, user);
|
||||
const humidDescriber = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT, source?.type(), source.subType(), undefined, user);
|
||||
|
||||
const existingTempVal = sliderVals.get(existing.temp) ?? tempDescriber.toDisplay(existing.temp.value);
|
||||
const existingHumidVal = sliderVals.get(existing.humid) ?? humidDescriber.toDisplay(existing.humid.value);
|
||||
|
||||
// offset 10% of the slider's range away from the existing threshold,
|
||||
// in the direction that keeps the two thresholds from overlapping
|
||||
const tempRange = tempDescriber.max() - tempDescriber.min();
|
||||
const humidRange = humidDescriber.max() - humidDescriber.min();
|
||||
const tempOffsetDir = addingLowerBound ? -1 : 1;
|
||||
const humidOffsetDir = addingLowerBound ? 1 : -1;
|
||||
|
||||
const newTempVal = existingTempVal + tempOffsetDir * tempRange * 0.1;
|
||||
const newHumidVal = existingHumidVal + humidOffsetDir * humidRange * 0.1;
|
||||
|
||||
const newTemp = buildCondition(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, tempComparison, newTempVal);
|
||||
const newHumid = buildCondition(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT, humidComparison, newHumidVal);
|
||||
|
||||
// ASSUMPTION: interaction.settings.conditions is the backing array for
|
||||
// interaction.conditions() -- push directly so changeConditions() sends
|
||||
// the full, updated list. Adjust if conditions are stored/added differently.
|
||||
interaction.settings.conditions.push(newTemp, newHumid);
|
||||
|
||||
const sliders = cloneDeep(sliderVals);
|
||||
sliders.set(newTemp, newTempVal);
|
||||
sliders.set(newHumid, newHumidVal);
|
||||
setSliderVals(sliders);
|
||||
setConditionsVersion(v => v + 1);
|
||||
|
||||
if (changeConditions) {
|
||||
changeConditions(interaction.settings);
|
||||
}
|
||||
};
|
||||
|
||||
// Removes a corner pair's two conditions, collapsing back to a single
|
||||
// bound. Only relevant when both pairs currently exist.
|
||||
const removeCornerPair = (pair: EMCCornerPair) => {
|
||||
interaction.settings.conditions = interaction.settings.conditions.filter(
|
||||
c => c !== pair.temp && c !== pair.humid
|
||||
);
|
||||
|
||||
const sliders = cloneDeep(sliderVals);
|
||||
sliders.delete(pair.temp);
|
||||
sliders.delete(pair.humid);
|
||||
setSliderVals(sliders);
|
||||
setConditionsVersion(v => v + 1);
|
||||
|
||||
if (changeConditions) {
|
||||
changeConditions(interaction.settings);
|
||||
}
|
||||
};
|
||||
|
||||
const renderConditionSlider = (
|
||||
condition: pond.InteractionCondition,
|
||||
key: React.Key,
|
||||
onRemove?: () => void,
|
||||
removeTooltip?: string
|
||||
) => {
|
||||
let describer = describeMeasurement(condition.measurementType, source?.type(), source.subType(), undefined, user);
|
||||
|
||||
return (
|
||||
<Grid key={key} container item xs={12} alignItems="center" wrap="nowrap">
|
||||
<Grid item xs style={{ marginBottom: 20 }}>
|
||||
<Slider
|
||||
classes={{
|
||||
root: classes.sliderRoot,
|
||||
rail: classes.sliderRail,
|
||||
track: classes.sliderTrack,
|
||||
thumb: classes.sliderThumb,
|
||||
valueLabel: classes.sliderValLabel,
|
||||
}}
|
||||
step={0.1}
|
||||
valueLabelDisplay="on"
|
||||
valueLabelFormat={value => {
|
||||
if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) {
|
||||
if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
||||
return value.toFixed(1) + "°F";
|
||||
} else {
|
||||
return value.toFixed(1) + "°C";
|
||||
}
|
||||
}
|
||||
if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) {
|
||||
return value.toFixed(1) + "%";
|
||||
}
|
||||
}}
|
||||
min={describer.min()}
|
||||
max={describer.max()}
|
||||
value={sliderVals.get(condition) ?? describer.min()}
|
||||
onChange={(_, val) => {
|
||||
// note that changing it here like this is what is changing it in the interaction itself
|
||||
condition.value = Math.round(describer.toStored(val as number));
|
||||
let sliders = cloneDeep(sliderVals);
|
||||
sliders.set(condition, val as number);
|
||||
setSliderVals(sliders);
|
||||
}}
|
||||
onChangeCommitted={() => {
|
||||
if (changeConditions) {
|
||||
changeConditions(interaction.settings);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
{onRemove && (
|
||||
<Grid item>
|
||||
<Tooltip title={removeTooltip ?? "Remove"}>
|
||||
<IconButton color="error" size="small" onClick={onRemove}>
|
||||
<RemoveCircleOutline />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
// Text like "Humidity is below 32.5% and above 60.0%" / "Humidity between 32.5% and 60.0%"
|
||||
// Falls back gracefully if only one of the pair exists yet (e.g. mid-edit).
|
||||
const groupLabel = (
|
||||
measurementType: quack.MeasurementType,
|
||||
loCondition: pond.InteractionCondition | undefined,
|
||||
hiCondition: pond.InteractionCondition | undefined
|
||||
) => {
|
||||
let describer = describeMeasurement(measurementType, source?.type(), source.subType(), undefined, user);
|
||||
let name = measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE ? "Temperature" : "Humidity";
|
||||
let unit = describer.unit();
|
||||
|
||||
if (loCondition && hiCondition) {
|
||||
let loVal = sliderVals.get(loCondition);
|
||||
let hiVal = sliderVals.get(hiCondition);
|
||||
if (loVal !== undefined && hiVal !== undefined) {
|
||||
return `${name} is between ${loVal.toFixed(1)}${unit} and ${hiVal.toFixed(1)}${unit}`;
|
||||
}
|
||||
}
|
||||
// fall back to whichever single condition is present
|
||||
let only = loCondition ?? hiCondition;
|
||||
if (only) return interactionConditionText(source, only, false, user);
|
||||
return name;
|
||||
};
|
||||
|
||||
const conditionDisplay = () => {
|
||||
// If both corner pairs exist, we have two temp conditions and two humid
|
||||
// conditions -- group them together (temp with temp, humid with humid)
|
||||
// instead of rendering per-condition like the single-pair case.
|
||||
const hasTwoPairs = !!pairs.upperBoundPair && !!pairs.lowerBoundPair;
|
||||
|
||||
if (hasTwoPairs) {
|
||||
// upperBoundPair.temp is "temp > X" (the low end of the temp range)
|
||||
// lowerBoundPair.temp is "temp < X" (the high end of the temp range)
|
||||
const tempLo = pairs.upperBoundPair!.temp;
|
||||
const tempHi = pairs.lowerBoundPair!.temp;
|
||||
// upperBoundPair.humid is "humid < Y" (the high end doesn't apply here -
|
||||
// it's the low end of the humidity range)
|
||||
const humidLo = pairs.upperBoundPair!.humid;
|
||||
const humidHi = pairs.lowerBoundPair!.humid;
|
||||
|
||||
return (
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Typography>{groupLabel(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, tempLo, tempHi)}</Typography>
|
||||
</Grid>
|
||||
{renderConditionSlider(tempLo, "temp-lo", () => removeCornerPair(pairs.upperBoundPair!), "Remove less than range (also removes matching humidity threshold)")}
|
||||
{renderConditionSlider(tempHi, "temp-hi", () => removeCornerPair(pairs.lowerBoundPair!), "Remove greater than range (also removes matching humidity threshold)")}
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Typography>{groupLabel(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT, humidLo, humidHi)}</Typography>
|
||||
</Grid>
|
||||
{renderConditionSlider(humidLo, "humid-lo", () => removeCornerPair(pairs.upperBoundPair!), "Remove less than range (also removes matching temperature threshold)")}
|
||||
{renderConditionSlider(humidHi, "humid-hi", () => removeCornerPair(pairs.lowerBoundPair!), "Remove greater than range (also removes matching temperature threshold)")}
|
||||
|
||||
{pairs.unmatched.map((c, i) => (
|
||||
<React.Fragment key={`unmatched-${i}`}>
|
||||
<Grid item xs={12}>
|
||||
<Typography>{interactionConditionText(source, c, false, user)}</Typography>
|
||||
</Grid>
|
||||
{renderConditionSlider(c, `unmatched-slider-${i}`)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
// single pair (or no clean pair) -- original flat rendering
|
||||
const hasExactlyOnePair =
|
||||
(!!pairs.upperBoundPair && !pairs.lowerBoundPair) ||
|
||||
(!pairs.upperBoundPair && !!pairs.lowerBoundPair);
|
||||
|
||||
return (
|
||||
<Grid container>
|
||||
{interaction.conditions().map((condition, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<Grid item xs={12}>
|
||||
<Typography>{interactionConditionText(source, condition, false, user)}</Typography>
|
||||
</Grid>
|
||||
{renderConditionSlider(condition, `slider-${i}`)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
{hasExactlyOnePair && (
|
||||
<Grid item xs={12} container justifyContent="center" style={{ marginTop: 10 }}>
|
||||
<Tooltip title={`Add ${pairs.upperBoundPair ? "greater than" : "less than"} range`}>
|
||||
<IconButton disabled={device.maxConditions() < 4} color="primary" onClick={addOppositeCornerPair}>
|
||||
<AddCircle fontSize="large" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const determineEMCLabel = () => {
|
||||
if (upperEMC !== undefined && lowerEMC !== undefined) return "Between: ";
|
||||
if (upperEMC !== undefined) return "Less Than: ";
|
||||
if (lowerEMC !== undefined) return "Greater Than: ";
|
||||
return "Approximately: ";
|
||||
};
|
||||
|
||||
const emcColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour();
|
||||
|
||||
const emcValueDisplay = () => {
|
||||
if (upperEMC !== undefined && lowerEMC !== undefined) {
|
||||
// present low-to-high regardless of which pair produced which,
|
||||
// since "greater than" bound is the smaller number and
|
||||
// "less than" bound is the larger number
|
||||
const lo = Math.min(upperEMC, lowerEMC);
|
||||
const hi = Math.max(upperEMC, lowerEMC);
|
||||
return `${lo.toFixed(1)}% and ${hi.toFixed(1)}%`;
|
||||
}
|
||||
const single = upperEMC ?? lowerEMC;
|
||||
return single !== undefined ? `${single.toFixed(1)}%` : "";
|
||||
};
|
||||
|
||||
const hasAnyEMC = upperEMC !== undefined || lowerEMC !== undefined;
|
||||
|
||||
return (
|
||||
<Grid key={interaction.key()} container className={classes.displayBG}>
|
||||
<Grid item xs={12}>
|
||||
<Typography align="center" style={{ margin: 5, marginBottom: 10, fontWeight: 650 }}>
|
||||
{interactionResultText(interaction, sink)}
|
||||
</Typography>
|
||||
{hasAnyEMC ? (
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||
<Box display="flex">
|
||||
<Typography style={{ fontWeight: 650 }}>EMC {determineEMCLabel()}</Typography>
|
||||
<Typography style={{ marginLeft: 5, fontWeight: 650, color: emcColour }}>
|
||||
{emcValueDisplay()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>{conditionDisplay()}</AccordionDetails>
|
||||
</Accordion>
|
||||
) : (
|
||||
<React.Fragment>{conditionDisplay()}</React.Fragment>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { AccessTime, MoreVert } from "@mui/icons-material";
|
||||
import { Avatar, Box, Button, Card, Checkbox, FormControlLabel, Grid2, IconButton, Menu, MenuItem, Typography, useTheme } from "@mui/material";
|
||||
import { Avatar, Box, Button, Card, Checkbox, DialogActions, DialogContent, DialogTitle, FormControlLabel, Grid2, IconButton, Menu, MenuItem, Typography, useTheme } from "@mui/material";
|
||||
import BinSensorCard from "bin/BinSensorCard";
|
||||
import { GetDefaultDateRange } from "common/time/DateRange";
|
||||
import { ExtractMoisture } from "grain";
|
||||
|
|
@ -21,6 +21,7 @@ import React, { useEffect, useState } from "react";
|
|||
import { avg } from "utils";
|
||||
import BinSensorGraph from "./graphs/BinSensorGraph";
|
||||
import TimeBar from "common/time/TimeBar";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
|
||||
interface Props {
|
||||
bin: Bin
|
||||
|
|
@ -72,8 +73,13 @@ export default function BinSensorsDisplay(props: Props){
|
|||
const [startDate, setStartDate] = useState<Moment>(defaultDateRange.start);
|
||||
const [endDate, setEndDate] = useState<Moment>(defaultDateRange.end);
|
||||
const [filterNodes, setFilterNodes] = useState(true)
|
||||
const [controllerConfirmation, setControllerConfirmation] = useState(false)
|
||||
const isMobile = useMobile()
|
||||
|
||||
//state variables fo controller updates using the switch on the card
|
||||
const [currentController, setCurrentController] = useState<Controller | undefined>()
|
||||
const [controllersNewMode, setControllersNewMode] = useState(quack.OutputMode.OUTPUT_MODE_OFF)
|
||||
|
||||
//organize the components into the correct 'positions' using the bins preferences
|
||||
useEffect(()=>{
|
||||
let unassigned: Component[] = []
|
||||
|
|
@ -499,6 +505,46 @@ export default function BinSensorsDisplay(props: Props){
|
|||
)
|
||||
}
|
||||
|
||||
const controllerDialog = () => {
|
||||
return (
|
||||
<ResponsiveDialog open={controllerConfirmation} onClose={()=>{setControllerConfirmation(false)}}>
|
||||
<DialogTitle>Changing Controller</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
This action will change the state of the controller
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={()=>{setControllerConfirmation(false)}}>Cancel</Button>
|
||||
<Button onClick={()=>{
|
||||
updateController()
|
||||
}}>Confirm</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
)
|
||||
}
|
||||
|
||||
const updateController = () => {
|
||||
if(!currentController) return
|
||||
const prefType = preferences?.get(currentController.key())?.type
|
||||
//this will update the components settings here to change the mode
|
||||
currentController.settings.defaultOutputState = controllersNewMode
|
||||
let device = componentDevices.get(currentController.key())
|
||||
if(device){
|
||||
componentAPI.update(device, currentController.settings).then(resp => {
|
||||
currentController.mode = controllersNewMode
|
||||
if (prefType === pond.BinComponent.BIN_COMPONENT_FAN) {
|
||||
setFans(prev => prev.map(f => f.key() === currentController.key() ? currentController : f))
|
||||
} else {
|
||||
setHeaters(prev => prev.map(h => h.key() === currentController.key() ? currentController : h))
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log("there was a problem updating the controller")
|
||||
}).finally(() => {
|
||||
setControllerConfirmation(false)
|
||||
})}
|
||||
}
|
||||
|
||||
const controllerCard = (controller: Controller) => {
|
||||
const component = controller.asComponent()
|
||||
let icon = GetComponentIcon(component.type(), component.subType(), themeType)
|
||||
|
|
@ -592,24 +638,9 @@ export default function BinSensorsDisplay(props: Props){
|
|||
key={label}
|
||||
component="button"
|
||||
onClick={() => {
|
||||
//this will update the components settings here to change the mode
|
||||
console.log("changing mode: " + mode)
|
||||
controller.settings.defaultOutputState = mode
|
||||
let device = componentDevices.get(controller.key())
|
||||
if(device){
|
||||
componentAPI.update(device, controller.settings).then(resp => {
|
||||
controller.mode = mode // ← update the field the UI reads
|
||||
|
||||
if (prefType === pond.BinComponent.BIN_COMPONENT_FAN) {
|
||||
setFans(prev => prev.map(f => f.key() === controller.key() ? controller : f))
|
||||
} else {
|
||||
setHeaters(prev => prev.map(h => h.key() === controller.key() ? controller : h))
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log("there was a problem updating the controller")
|
||||
})
|
||||
}
|
||||
|
||||
setCurrentController(controller)
|
||||
setControllersNewMode(mode)
|
||||
setControllerConfirmation(true)
|
||||
}}
|
||||
sx={{
|
||||
...segStyle(active, label === "Off" ? theme.palette.action.selected : undefined),
|
||||
|
|
@ -718,6 +749,7 @@ export default function BinSensorsDisplay(props: Props){
|
|||
|
||||
return (
|
||||
<Box padding={1}>
|
||||
{controllerDialog()}
|
||||
{assignmentMenu()}
|
||||
{sensorCount > 0 &&
|
||||
<Box>
|
||||
|
|
|
|||
|
|
@ -439,7 +439,7 @@ if (!selectedDevice) return;
|
|||
conflictingSetInteractions.forEach(i => {
|
||||
let newStep: PromiseStep = {
|
||||
title: "Removing Interaction",
|
||||
promise: interactionAPI.removeInteraction(selectedDevice.id(), i.key(), as)
|
||||
promise: () => interactionAPI.removeInteraction(selectedDevice.id(), i.key(), as)
|
||||
}
|
||||
stage1.steps.push(newStep);
|
||||
});
|
||||
|
|
@ -452,7 +452,7 @@ if (!selectedDevice) return;
|
|||
steps: [
|
||||
{
|
||||
title: "Adding Interactions",
|
||||
promise: interactionAPI.addMultiInteractions(selectedDevice.id(), interactionsToAdd)
|
||||
promise: () => interactionAPI.addMultiInteractions(selectedDevice.id(), interactionsToAdd)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -469,7 +469,7 @@ if (!selectedDevice) return;
|
|||
set.controllers.forEach(controller => {
|
||||
let newStep: PromiseStep = {
|
||||
title: "Updating " + controller.name(),
|
||||
promise: componentAPI.update(selectedDevice.id(), controller.settings)
|
||||
promise: () => componentAPI.update(selectedDevice.id(), controller.settings)
|
||||
}
|
||||
stage3.steps.push(newStep)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ export default function PickComponentSet(props: Props) {
|
|||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>Controllers Selected: {currentSet.controllers.length}</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{heaters && heaters.length > 0 && binMode !== pond.BinMode.BIN_MODE_HYDRATING &&
|
||||
{heaters && heaters.length > 0 && binMode === pond.BinMode.BIN_MODE_DRYING &&
|
||||
<React.Fragment>
|
||||
<Typography>Heaters</Typography>
|
||||
{heaters.map(heater => (
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export interface Stage {
|
|||
|
||||
export interface Step {
|
||||
title: string
|
||||
promise: Promise<AxiosResponse>
|
||||
promise: () => Promise<AxiosResponse>
|
||||
onComplete?: () => void
|
||||
onError?: () => void
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ export function PromiseProgress(props: Props){
|
|||
// mark step as in progress
|
||||
progMap.set(i+"-"+k, progress.InProgress);
|
||||
setCompletion(new Map(progMap));
|
||||
return step.promise
|
||||
return step.promise()
|
||||
.then(() => {
|
||||
// mark step as completed
|
||||
progMap.set(i+"-"+k, progress.Complete);
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ export default function InteractionSettings(props: Props) {
|
|||
const [timezone, setTimezone] = useState<string | undefined>(undefined);
|
||||
//whether is any node(0), average of all nodes(1), single node(2), or node diff(3)
|
||||
const [subtypeDropdown, setSubtypeDropdown] = useState<number>(0);
|
||||
const [interactionEnabled, setInteractionEnabled] = useState(true); //by default interactions are enabled
|
||||
|
||||
const initialConditions = useCallback(
|
||||
(type: quack.ComponentType): pond.InteractionCondition[] => {
|
||||
|
|
@ -168,6 +169,8 @@ export default function InteractionSettings(props: Props) {
|
|||
let interaction = getDefaultInteraction();
|
||||
if (initialInteraction && mode === "update") {
|
||||
interaction = initialInteraction;
|
||||
//set the switch's state value
|
||||
setInteractionEnabled(!interaction.settings.inactive);
|
||||
if (interaction.settings.subtype === 0 || interaction.settings.subtype === 1) {
|
||||
setSubtypeDropdown(interaction.settings.subtype);
|
||||
} else {
|
||||
|
|
@ -427,6 +430,13 @@ export default function InteractionSettings(props: Props) {
|
|||
setInteraction(updatedInteraction);
|
||||
};
|
||||
|
||||
const changeInactive = (event: any) => {
|
||||
let updatedInteraction = Interaction.clone(interaction);
|
||||
//the switch uses true for active but the settings use true for inactive, so need to use the opposite of the switch
|
||||
updatedInteraction.settings.inactive = !event.target.checked;
|
||||
setInteraction(updatedInteraction);
|
||||
};
|
||||
|
||||
const describeSource = (measurementType: quack.MeasurementType): MeasurementDescriber => {
|
||||
const source: Component = or(
|
||||
mappedComponents.get(componentIDToString(interaction.settings.source)),
|
||||
|
|
@ -1532,6 +1542,27 @@ export default function InteractionSettings(props: Props) {
|
|||
);
|
||||
};
|
||||
|
||||
const activeSwitch = () => {
|
||||
return (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
id="active"
|
||||
checked={interactionEnabled}
|
||||
onChange={(event) => {
|
||||
setInteractionEnabled(!interactionEnabled)
|
||||
changeInactive(event)
|
||||
}}
|
||||
value="active"
|
||||
color="secondary"
|
||||
/>
|
||||
}
|
||||
label="Active"
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const content = () => {
|
||||
return (
|
||||
<Grid container direction="row" spacing={2}>
|
||||
|
|
@ -1540,10 +1571,14 @@ export default function InteractionSettings(props: Props) {
|
|||
</Grid>
|
||||
{hasDeviceFeature(device.settings.upgradeChannel, "better-controls") && (
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<div style={{ display: "flex", flexDirection: "row" }}>
|
||||
<div style={{ display: "flex", flexDirection: "row", gap: 20 }}>
|
||||
{priorityInput()}
|
||||
<div style={{ marginLeft: theme.spacing(2) }}></div>
|
||||
{/* <div style={{ marginLeft: theme.spacing(2) }}></div> */}
|
||||
{sortingInput()}
|
||||
{/* this will do the check for the feature once we know what version the feature is supported on */}
|
||||
{/* {device.featureSupported("disableInteractions") && activeSwitch()} */}
|
||||
{/* this is for testing on dev */}
|
||||
{activeSwitch()}
|
||||
</div>
|
||||
</Grid>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -108,18 +108,25 @@ export class Ambient {
|
|||
return quack.ComponentID.fromObject({
|
||||
type: this.settings.type,
|
||||
addressType: this.settings.addressType,
|
||||
address: this.settings.address
|
||||
address: this.settings.address,
|
||||
expansionLine: this.settings.expansionLine,
|
||||
muxLine: this.settings.muxLine
|
||||
});
|
||||
}
|
||||
|
||||
public locationString(): string {
|
||||
return (
|
||||
or(this.settings.type, 0).toString() +
|
||||
let compositeLocation = or(this.settings.type, 0).toString() +
|
||||
"-" +
|
||||
or(this.settings.addressType, 0).toString() +
|
||||
"-" +
|
||||
or(this.settings.address, 0).toString()
|
||||
);
|
||||
if(this.settings.expansionLine){
|
||||
compositeLocation = compositeLocation + "-" + this.settings.expansionLine
|
||||
}
|
||||
if(this.settings.muxLine){
|
||||
compositeLocation = compositeLocation + ":" + this.settings.muxLine
|
||||
}
|
||||
return compositeLocation;
|
||||
}
|
||||
|
||||
public type(): quack.ComponentType {
|
||||
|
|
|
|||
|
|
@ -82,18 +82,25 @@ export class Controller {
|
|||
return quack.ComponentID.fromObject({
|
||||
type: this.settings.type,
|
||||
addressType: this.settings.addressType,
|
||||
address: this.settings.address
|
||||
address: this.settings.address,
|
||||
expansionLine: this.settings.expansionLine,
|
||||
muxLine: this.settings.muxLine
|
||||
});
|
||||
}
|
||||
|
||||
public locationString(): string {
|
||||
return (
|
||||
or(this.settings.type, 0).toString() +
|
||||
let compositeLocation = or(this.settings.type, 0).toString() +
|
||||
"-" +
|
||||
or(this.settings.addressType, 0).toString() +
|
||||
"-" +
|
||||
or(this.settings.address, 0).toString()
|
||||
);
|
||||
if(this.settings.expansionLine){
|
||||
compositeLocation = compositeLocation + "-" + this.settings.expansionLine
|
||||
}
|
||||
if(this.settings.muxLine){
|
||||
compositeLocation = compositeLocation + ":" + this.settings.muxLine
|
||||
}
|
||||
return compositeLocation;
|
||||
}
|
||||
|
||||
public type(): quack.ComponentType {
|
||||
|
|
|
|||
|
|
@ -79,6 +79,21 @@ const featureVersions: Map<string, FeatureVersionByPlatform> = new Map([
|
|||
v2CellBlue: "2.1.10",
|
||||
v2EthBlue: "2.1.10"
|
||||
}
|
||||
],[
|
||||
"disableInteractions",
|
||||
{
|
||||
photon: "N/A",
|
||||
electron: "N/A",
|
||||
v2Wifi: "N/A",
|
||||
v2Cell: "N/A",
|
||||
v2WifiS3: "N/A",
|
||||
v2CellS3: "N/A",
|
||||
v2CellBlack: "N/A",
|
||||
v2CellGreen: "N/A",
|
||||
v2WifiBlue: "N/A",
|
||||
v2CellBlue: "N/A",
|
||||
v2EthBlue: "N/A"
|
||||
}
|
||||
]
|
||||
]);
|
||||
export class Device {
|
||||
|
|
|
|||
|
|
@ -230,18 +230,25 @@ export class GrainCable {
|
|||
return quack.ComponentID.fromObject({
|
||||
type: this.settings.type,
|
||||
addressType: this.settings.addressType,
|
||||
address: this.settings.address
|
||||
address: this.settings.address,
|
||||
expansionLine: this.settings.expansionLine,
|
||||
muxLine: this.settings.muxLine
|
||||
});
|
||||
}
|
||||
|
||||
public locationString(): string {
|
||||
return (
|
||||
or(this.settings.type, 0).toString() +
|
||||
let compositeLocation = or(this.settings.type, 0).toString() +
|
||||
"-" +
|
||||
or(this.settings.addressType, 0).toString() +
|
||||
"-" +
|
||||
or(this.settings.address, 0).toString()
|
||||
);
|
||||
if(this.settings.expansionLine){
|
||||
compositeLocation = compositeLocation + "-" + this.settings.expansionLine
|
||||
}
|
||||
if(this.settings.muxLine){
|
||||
compositeLocation = compositeLocation + ":" + this.settings.muxLine
|
||||
}
|
||||
return compositeLocation;
|
||||
}
|
||||
|
||||
public type(): quack.ComponentType {
|
||||
|
|
|
|||
|
|
@ -108,18 +108,25 @@ export class Plenum {
|
|||
return quack.ComponentID.fromObject({
|
||||
type: this.settings.type,
|
||||
addressType: this.settings.addressType,
|
||||
address: this.settings.address
|
||||
address: this.settings.address,
|
||||
expansionLine: this.settings.expansionLine,
|
||||
muxLine: this.settings.muxLine
|
||||
});
|
||||
}
|
||||
|
||||
public locationString(): string {
|
||||
return (
|
||||
or(this.settings.type, 0).toString() +
|
||||
let compositeLocation = or(this.settings.type, 0).toString() +
|
||||
"-" +
|
||||
or(this.settings.addressType, 0).toString() +
|
||||
"-" +
|
||||
or(this.settings.address, 0).toString()
|
||||
);
|
||||
if(this.settings.expansionLine){
|
||||
compositeLocation = compositeLocation + "-" + this.settings.expansionLine
|
||||
}
|
||||
if(this.settings.muxLine){
|
||||
compositeLocation = compositeLocation + ":" + this.settings.muxLine
|
||||
}
|
||||
return compositeLocation;
|
||||
}
|
||||
|
||||
public type(): quack.ComponentType {
|
||||
|
|
|
|||
|
|
@ -417,7 +417,8 @@ export default function ComponentProvider(props: PropsWithChildren<Props>) {
|
|||
"&end=" +
|
||||
end +
|
||||
(keys ? "&keys=" + keys : "&keys=" + [deviceId.toString()]) +
|
||||
(types ? "&types=" + types : "&types=" + ["device"])
|
||||
(types ? "&types=" + types : "&types=" + ["device"]) +
|
||||
(as ? "&as=" + as : "")
|
||||
)
|
||||
return new Promise<AxiosResponse<pond.ListComponentHistoryBetweenResponse>>((resolve,reject) => {
|
||||
get<pond.ListComponentHistoryBetweenResponse>(url).then(resp => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue