diff --git a/package-lock.json b/package-lock.json
index 9073bc0..dcd3dab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10953,7 +10953,7 @@
},
"node_modules/protobuf-ts": {
"version": "1.0.0",
- "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#fa7ac81420665424e5490346c6a6e871480a1643",
+ "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#213cea04f662037bdc0816910adba50d81c068df",
"dependencies": {
"protobufjs": "^6.8.8"
}
diff --git a/src/bin/BinTour.tsx b/src/bin/BinTour.tsx
index a685ada..e885527 100644
--- a/src/bin/BinTour.tsx
+++ b/src/bin/BinTour.tsx
@@ -1,6 +1,6 @@
// import { useAuth0 } from "@auth0/auth0-react";
import { Typography } from "@mui/material";
-import Tour from "common/Tour";
+import Tour, { TourStep } from "common/Tour";
import { random } from "lodash";
import moment from "moment";
import { useGlobalState, useSnackbar, useUserAPI } from "providers";
@@ -8,8 +8,13 @@ import React, { useEffect, useState } from "react";
import { Step } from "react-joyride";
// import Emoji from "react-emoji-render";
-export default function BinTour() {
+interface Props {
+ setDetailTabState?: ( detail: "inventory" | "sensors" | "analytics" | "presets" | "alerts" | "transactions", buttonIndex: number) => void
+}
+
+export default function BinTour(props: Props) {
// const { userID } = useAuth0();
+ const {setDetailTabState} = props
const [{ user }, dispatch] = useGlobalState();
const userID = user.id()
const { error } = useSnackbar();
@@ -35,8 +40,8 @@ export default function BinTour() {
}
};
- const getTourSteps = (): Step[] => {
- let steps: Step[] = [
+ const getTourSteps = (): TourStep[] => {
+ let steps: TourStep[] = [
{
title: (
@@ -103,24 +108,12 @@ export default function BinTour() {
disableBeacon: false
},
{
- title: "Graphs",
+ title: "View Other Data",
content: (
- Bin related analytics are displayed here.
-
-
- ),
- target: "#tour-graphs",
- placement: "left",
- disableBeacon: false
- },
- {
- title: "Choose your graphs",
- content: (
-
-
- Use this tab to view other sets of data. Sensors must be attached to view sensor data.
+ Use these tabs to view other Bin Details such as inventory levels over time,
+ alerts and interactions for connected components, and data for attached sensors
),
@@ -128,6 +121,122 @@ export default function BinTour() {
placement: "bottom",
disableBeacon: false
},
+ {
+ title: "Inventory",
+ content: (
+
+
+ The inventory tab shows your bins inventory level over a specified window as well as the times the bin mode was changed.
+
+
+ ),
+ target: "#tour-details",
+ placement: "left-start",
+ disableBeacon: false,
+ onNext: () => {
+ if (setDetailTabState) setDetailTabState("sensors", 1)
+ }
+ },
+ {
+ title: "Sensors",
+ content: (
+
+
+ The sensors tab shows readings from attached sensors over time
+
+
+ ),
+ target: "#tour-details",
+ placement: "left-start",
+ disableBeacon: false,
+ onNext: () => {
+ if (setDetailTabState) setDetailTabState("analytics", 2)
+ }
+ },
+ {
+ title: "Analytics",
+ content: (
+
+
+ The analysis tab shows analytic bin data using attached sensors
+
+
+ ),
+ target: "#tour-details",
+ placement: "left-start",
+ disableBeacon: false,
+ onNext: () => {
+ if (setDetailTabState) setDetailTabState("alerts", 3)
+ }
+ },
+ {
+ title: "Alerts and Interactions",
+ content: (
+
+
+ The Alerts tab allows you to view or add any new interactions or alerts for connected components
+ and displays recent notifications from those alerts
+
+
+ ),
+ target: "#tour-details",
+ placement: "left-start",
+ disableBeacon: false,
+ onNext: () => {
+ if (setDetailTabState) setDetailTabState("presets", 4)
+ }
+ },
+ {
+ title: "Bin Mode Presets",
+ content: (
+
+
+ The Presets tab allows you to create custom presets for the interactions for a device when changing bin modes
+
+
+ ),
+ target: "#tour-details",
+ placement: "left-start",
+ disableBeacon: false,
+ onNext: () => {
+ if (setDetailTabState) setDetailTabState("transactions", 5)
+ }
+ },
+ {
+ title: "Bin Transactions",
+ content: (
+
+
+ The transactions tab allows view any pending transaction when using a hybrid inventory control
+
+
+ ),
+ target: "#tour-details",
+ placement: "left-start",
+ disableBeacon: false,
+ onNext: () => {
+ if (setDetailTabState) setDetailTabState("inventory", 0)
+ }
+ },
+ {
+ title: "Bin Conditions",
+ content: (
+
+
+ This section will display conditions based on the bin mode
+
+
+ Storage Mode: simply displays the Storage conditions by itself with the target temperature and moisture set in the bin settings
+
+
+ Other Modes: will display the interactions for the bins devices that are controlling a fan or heater on the bin as well as the storage conditions
+
+
+ ),
+ target: "#tour-conditions",
+ placement: "right",
+ disableBeacon: false
+ },
{
title: "Change Mode",
content: (
@@ -138,12 +247,15 @@ export default function BinTour() {
Storage mode: default
- Drying mode: use heat to dry grain ☀️
+ Cooldown mode: use fans to hold bin temperature lower ❆❅
+ {/* */}
+
+
+ Drying mode: use heat to dry grain ☀️ (only visible when the target moisture is lower than the starting moisture in the bin settings)
{/* */}
- Cooldown mode: use fans to hold bin temperature lower ❆❅
- {/* */}
+ Hydrating mode: use humid air to hydrate grain (only visible when the target moisture is higher than the starting moisture in the bin settings)
diff --git a/src/bin/BinVisualizerV2.tsx b/src/bin/BinVisualizerV2.tsx
index 7ce1d7b..f1addd8 100644
--- a/src/bin/BinVisualizerV2.tsx
+++ b/src/bin/BinVisualizerV2.tsx
@@ -1738,32 +1738,34 @@ export default function BinVisualizer(props: Props) {
{modeChangeInProgress &&
}
- {
- setModeStorage()
+
+ {
+ setModeStorage()
+ }
+ },
+ {
+ title: "Cooldown",
+ function: () => {
+ setModeCooldown()
+ }
+ },
+ {
+ title: bin.settings.inventory && bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? "Hydrating" : "Drying",
+ function: () => {
+ // setShowInputMoisture(true)
+ setModeConditioning() //will set it to either drying or hydrating based on the initial and target moisture
+ }
}
- },
- {
- title: "Cooldown",
- function: () => {
- setModeCooldown()
- }
- },
- {
- title: bin.settings.inventory && bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? "Hydrating" : "Drying",
- function: () => {
- // setShowInputMoisture(true)
- setModeConditioning() //will set it to either drying or hydrating based on the initial and target moisture
- }
- }
- ]}
- toggledButtons={determineToggle(mode)}
- toggle
- />
+ ]}
+ toggledButtons={determineToggle(mode)}
+ toggle
+ />
+
);
};
diff --git a/src/common/ButtonGroup.tsx b/src/common/ButtonGroup.tsx
index 3c2c64c..e326d64 100644
--- a/src/common/ButtonGroup.tsx
+++ b/src/common/ButtonGroup.tsx
@@ -57,6 +57,7 @@ interface Props {
* When true will disable all of the buttons in the group, will be overridded by individual buttons disabled state in the button data
*/
disableAll?: boolean
+
}
const useStyles = makeStyles((theme: Theme) => {
diff --git a/src/device/DeviceWizard.tsx b/src/device/DeviceWizard.tsx
index cf0a061..3bb7fb9 100644
--- a/src/device/DeviceWizard.tsx
+++ b/src/device/DeviceWizard.tsx
@@ -226,7 +226,6 @@ export default function DeviceWizard(props: Props) {
*/
const adjustAvailablePositions = (port: PortInformation, availability: DeviceAvailabilityMap) => {
let currentAvailability = availability;
- console.log(port)
switch (port.addressType) {
case quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY:
currentAvailability.set(port.addressType, [{ address: port.address, label: port.label }]);
diff --git a/src/device/autoDetect/DeviceScannedComponents.tsx b/src/device/autoDetect/DeviceScannedComponents.tsx
index cda95ae..2be89a9 100644
--- a/src/device/autoDetect/DeviceScannedComponents.tsx
+++ b/src/device/autoDetect/DeviceScannedComponents.tsx
@@ -221,6 +221,7 @@ export default function DeviceScannedComponents(props: Props){
deviceAPI.removeAllFoundComponents(device.settings.deviceId)
.then(resp => {
console.log("Cleared all scans")
+ refreshCallback()
}).catch(err => {
console.log("There was a problem clearing scans")
})
@@ -233,7 +234,10 @@ export default function DeviceScannedComponents(props: Props){
This action will clear all scans for this device.
{setClearOpen(false)}}>Close
- {removeAllScans()}}>Continue
+ {
+ removeAllScans()
+ setClearOpen(false)
+ }}>Continue
)
diff --git a/src/fans/fans_client.json b/src/fans/fans_client.json
index 58f4040..cdc21f4 100644
--- a/src/fans/fans_client.json
+++ b/src/fans/fans_client.json
@@ -3125,6 +3125,14 @@
"rpm": 1750,
"diameter": 0,
"horsepower": 30
+ },
+ {
+ "id": 445,
+ "name": "CALDWELL GGL-81011",
+ "kind": "centrifugal(low speed)",
+ "rpm": 0,
+ "diameter": 0,
+ "horsepower": 10
}
]
}
diff --git a/src/models/Device.ts b/src/models/Device.ts
index 6ee074d..f4ea4ee 100644
--- a/src/models/Device.ts
+++ b/src/models/Device.ts
@@ -181,6 +181,59 @@ export class Device {
return max
}
+ private versionComparison(deviceVersion: string, requiredVersion: string): boolean {
+ const parse = (v: string) => {
+ const [core, pre] = v.split("-");
+ const parts = core.split(".").map(n => parseInt(n, 10));
+
+ let preLabel = "";
+ let preNum = 0;
+
+ if (pre) {
+ const match = pre.match(/^([a-zA-Z]+)(\d*)$/);
+ if (match) {
+ preLabel = match[1].toLowerCase();
+ preNum = match[2] ? parseInt(match[2], 10) : 0;
+ }
+ }
+
+ return { parts, preLabel, preNum };
+ };
+
+ const rank = (label: string): number => {
+ switch (label) {
+ case "alpha": return 1;
+ case "beta": return 2;
+ case "rc": return 3;
+ default: return 0; // release
+ }
+ };
+
+ const a = parse(deviceVersion);
+ const b = parse(requiredVersion);
+
+ // Compare major/minor/patch
+ const len = Math.max(a.parts.length, b.parts.length);
+ for (let i = 0; i < len; i++) {
+ const av = a.parts[i] ?? 0;
+ const bv = b.parts[i] ?? 0;
+ if (av > bv) return true;
+ if (av < bv) return false;
+ }
+
+ // Handle prerelease vs release
+ if (!a.preLabel && b.preLabel) return true; // release > prerelease
+ if (a.preLabel && !b.preLabel) return false;
+
+ // Both prereleases → compare rank
+ if (a.preLabel !== b.preLabel) {
+ return rank(a.preLabel) >= rank(b.preLabel);
+ }
+
+ // Same prerelease label → compare numeric suffix
+ return a.preNum >= b.preNum;
+ }
+
public featureSupported(feature: string): boolean {
let versions = featureVersions.get(feature);
if (!versions) {
@@ -188,27 +241,27 @@ export class Device {
}
switch (this.settings.platform) {
case pond.DevicePlatform.DEVICE_PLATFORM_PHOTON:
- return this.status.firmwareVersion >= versions.photon;
+ return this.versionComparison(this.status.firmwareVersion, versions.photon);
case pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON:
- return this.status.firmwareVersion >= versions.electron;
+ return this.versionComparison(this.status.firmwareVersion, versions.electron);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR:
- return this.status.firmwareVersion >= versions.v2Cell;
+ return this.versionComparison(this.status.firmwareVersion, versions.v2Cell);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI:
- return this.status.firmwareVersion >= versions.v2Wifi;
+ return this.versionComparison(this.status.firmwareVersion, versions.v2Wifi);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_S3:
- return this.status.firmwareVersion >= versions.v2WifiS3;
+ return this.versionComparison(this.status.firmwareVersion, versions.v2WifiS3);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_S3:
- return this.status.firmwareVersion >= versions.v2CellS3;
+ return this.versionComparison(this.status.firmwareVersion, versions.v2CellS3);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLACK:
- return this.status.firmwareVersion >= versions.v2CellBlack;
+ return this.versionComparison(this.status.firmwareVersion, versions.v2CellBlack);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_GREEN:
- return this.status.firmwareVersion >= versions.v2CellGreen;
+ return this.versionComparison(this.status.firmwareVersion, versions.v2CellGreen);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE:
- return this.status.firmwareVersion >= versions.v2WifiBlue;
+ return this.versionComparison(this.status.firmwareVersion, versions.v2WifiBlue);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE:
- return this.status.firmwareVersion >= versions.v2CellBlue;
+ return this.versionComparison(this.status.firmwareVersion, versions.v2CellBlue);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_ETHERNET_BLUE:
- return this.status.firmwareVersion >= versions.v2EthBlue;
+ return this.versionComparison(this.status.firmwareVersion, versions.v2EthBlue);
default:
return false;
}
diff --git a/src/models/Interaction.ts b/src/models/Interaction.ts
index 659ca70..f9edba9 100644
--- a/src/models/Interaction.ts
+++ b/src/models/Interaction.ts
@@ -1,6 +1,7 @@
import { pond } from "protobuf-ts/pond";
import { or } from "utils/types";
import { cloneDeep } from "lodash";
+import { Result } from "pbHelpers/Enums";
export class Interaction {
public settings: pond.InteractionSettings = pond.InteractionSettings.create();
@@ -53,6 +54,18 @@ export class Interaction {
return subtype;
}
+ //these two are not mutually exclusive, it is possible to be an alert and a control
+ public isAlert(): boolean {
+ if(this.settings.result?.type === Result.report) return true
+ if(this.settings.notifications?.reports && this.settings.notifications.notify) return true
+ return false
+ }
+
+ public isControl(): boolean {
+ if(this.settings.result?.type !== Result.report) return true //as of right now all interactions that are not report are some sort of control
+ return false
+ }
+
public static upToSubtype(nodeNumber: number): number {
let subtype = 0;
switch (nodeNumber) {
diff --git a/src/objects/objectInteractions/Alerts.tsx b/src/objects/objectInteractions/Alerts.tsx
new file mode 100644
index 0000000..8546363
--- /dev/null
+++ b/src/objects/objectInteractions/Alerts.tsx
@@ -0,0 +1,89 @@
+import { Add, ExpandMore } from "@mui/icons-material";
+import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Grid2, IconButton, List, ListItem, ListSubheader, Typography } from "@mui/material";
+import { Component } from "models";
+import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
+import { pond, quack } from "protobuf-ts/pond";
+import React, { useState } from "react";
+import NewObjectInteraction from "./NewObjectInteraction";
+import { Option } from "common/SearchSelect";
+
+export interface Alert {
+ conditions: pond.InteractionCondition[];
+ sourceComponents: Component[];
+}
+
+interface Props {
+ alerts: Alert[]
+ permissions: pond.Permission[]
+ addNew?: () => void
+}
+
+export default function Alerts(props: Props){
+ const {alerts, addNew, permissions} = props
+ // const [openNewAlert, setOpenNewAlert] = useState(false)
+
+ const readableCondition = (condition: pond.InteractionCondition) => {
+ let describer = describeMeasurement(condition.measurementType);
+ let type = describer.label();
+ let comparison =
+ condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO
+ ? "Exactly"
+ : condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
+ ? "Above"
+ : "Below";
+ let value = describer.toDisplay(condition.value);
+ return (
+
+ {type + " " + comparison + " " + value + describer.GetUnit()}
+
+ );
+ };
+
+ const alertAccordion = (alert: Alert) => {
+ return (
+
+ }>
+
+ {alert.conditions.map((condition, i) => (
+
+ {readableCondition(condition)}
+
+ ))}
+
+
+
+
+ Reporting Components
+ {alert.sourceComponents.map((comp, i) => (
+ {comp.name()}
+ ))}
+
+
+
+ );
+ };
+
+ return (
+
+
+
+
+ Alerts
+
+ {addNew &&
+ {addNew()}}
+ variant="contained"
+ color="primary">
+ Alert
+
+ }
+
+ {alerts.map((alert, i) => (
+ {alertAccordion(alert)}
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/objects/objectInteractions/Controls.tsx b/src/objects/objectInteractions/Controls.tsx
new file mode 100644
index 0000000..0346c87
--- /dev/null
+++ b/src/objects/objectInteractions/Controls.tsx
@@ -0,0 +1,152 @@
+import { Add, ExpandMore } from "@mui/icons-material";
+import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Divider, IconButton, List, ListItem, ListSubheader, Menu, MenuItem, Typography } from "@mui/material";
+import { Component, Device, Interaction } from "models";
+import { interactionResultText } from "pbHelpers/Interaction";
+import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
+import { pond, quack } from "protobuf-ts/pond";
+import React, { useEffect, useState } from "react";
+
+export interface Control {
+ conditions: pond.InteractionCondition[];
+ sourceComponents: Component[];
+ result: pond.InteractionResult | null;
+ sinkComponent: Component
+ device: Device
+}
+
+interface Props {
+ devices: Device[]
+ controls: Control[]
+ permissions: pond.Permission[]
+ addNew?: (device: Device) => void
+}
+
+export default function Controls(props: Props){
+ const { devices, controls, addNew, permissions } = props
+ const [deviceControls, setDeviceControls] = useState>(new Map())
+ const [openDeviceMenu, setOpenDeviceMenu] = useState(false)
+ const [anchor, setAnchor] = useState(null)
+
+
+ useEffect(()=>{
+ let map: Map = new Map()
+ controls.forEach(control => {
+ let mappedControls = map.get(control.device.id())
+ if(mappedControls){
+ mappedControls.push(control)
+ }else{
+ map.set(control.device.id(), [control])
+ }
+ })
+ setDeviceControls(map)
+ },[controls])
+
+ const readableConditions = (control: Control) => {
+ let conditions: string = ""
+ let firstSource = control.sourceComponents[0]
+ control.conditions.forEach((condition, index) => {
+ let describer = describeMeasurement(condition.measurementType, firstSource.type(), firstSource.subType());
+ let type = describer.label();
+ let comparison =
+ condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO
+ ? "Exactly"
+ : condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
+ ? "Above"
+ : "Below";
+ let value = describer.toDisplay(condition.value);
+ conditions = conditions + (index !== 0 ? " and " : "") + type + " " + comparison + " " + value + describer.GetUnit()
+ })
+ return conditions
+ };
+
+ const controlAccordion = (control: Control) => {
+ let tempInteraction = Interaction.create()
+ tempInteraction.settings.result = control.result
+ let resultText = interactionResultText(tempInteraction, control.sinkComponent)
+
+ return (
+
+ }>
+
+
+ {resultText}:
+
+
+ {readableConditions(control)}
+
+
+
+
+
+ Controlling Components
+ {control.sourceComponents.map((comp, i) => (
+ {comp.name()}
+ ))}
+
+
+
+ )
+ }
+
+ const deviceMenu = () => {
+ return (
+ {
+ setAnchor(null)
+ setOpenDeviceMenu(false)
+ }}>
+ {devices.map(dev => (
+ {
+ setAnchor(null)
+ setOpenDeviceMenu(false)
+ if (addNew) addNew(dev)
+
+ }}>
+ {dev.name()}
+
+ ))}
+
+ )
+ }
+
+ return (
+
+ {deviceMenu()}
+
+
+
+ Controls
+
+ {addNew &&
+ {
+ setOpenDeviceMenu(true)
+ setAnchor(e.currentTarget)
+ }}
+ color="primary">
+ Control
+
+ }
+
+ {devices.map(device => (
+
+ {device.name()}
+
+ {deviceControls.get(device.id()) ? deviceControls.get(device.id())?.map((control, i) => (
+ {controlAccordion(control)}
+ ))
+ :
+ No Control Interactions For Device
+ }
+
+ ))}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/objects/ObjectAlerts.tsx b/src/objects/objectInteractions/NewObjectInteraction.tsx
similarity index 51%
rename from src/objects/ObjectAlerts.tsx
rename to src/objects/objectInteractions/NewObjectInteraction.tsx
index ef443dc..8e76c25 100644
--- a/src/objects/ObjectAlerts.tsx
+++ b/src/objects/objectInteractions/NewObjectInteraction.tsx
@@ -1,71 +1,40 @@
-import {
- Accordion,
- AccordionDetails,
- AccordionSummary,
- Box,
- Button,
- Card,
- Checkbox,
- DialogActions,
- DialogContent,
- DialogTitle,
- FormControl,
- FormControlLabel,
- FormHelperText,
- FormLabel,
- Grid2 as Grid,
- IconButton,
- InputAdornment,
- List,
- ListItem,
- ListItemIcon,
- ListItemText,
- ListSubheader,
- MenuItem,
- Select,
- TextField,
- Theme,
- Tooltip,
- Typography
-} from "@mui/material";
-import { ExpandMore, RemoveCircle as RemoveIcon, AddCircle as AddIcon } from "@mui/icons-material";
+import { Button, Checkbox, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, FormHelperText, FormLabel, Grid2, IconButton, InputAdornment, List, ListItem, ListItemIcon, ListItemText, MenuItem, Select, Switch, TextField, Theme, Tooltip, Typography } from "@mui/material";
+import { makeStyles } from "@mui/styles";
+import classNames from "classnames";
import ResponsiveDialog from "common/ResponsiveDialog";
import { Option } from "common/SearchSelect";
-import classNames from "classnames";
-import { Component, Interaction } from "models";
-import moment from "moment";
-import { getFriendlyName, getMeasurements } from "pbHelpers/ComponentType";
-import { Measurement, Operator } from "pbHelpers/Enums";
-import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
+import { capitalize, cloneDeep } from "lodash";
+import { Component, Device, Interaction } from "models";
+import { extension, getMeasurements } from "pbHelpers/ComponentType";
+import { ComponentType, Measurement, Operator } from "pbHelpers/Enums";
+import { describeMeasurement, MeasurementDescriber } from "pbHelpers/MeasurementDescriber";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
-import { useGlobalState, useInteractionsAPI, useSnackbar } from "providers";
-import { useNotificationAPI } from "providers/pond/notificationAPI";
-import React, { useEffect, useState } from "react";
-import { capitalize, cloneDeep } from "lodash";
+import React from "react";
+import { useEffect, useState } from "react";
+import AddIcon from "@mui/icons-material/AddCircle";
+import RemoveIcon from "@mui/icons-material/RemoveCircle";
+import { green, red } from "@mui/material/colors";
+import { or } from "utils";
+import moment from "moment";
import { timeOfDayDescriptor } from "pbHelpers/Interaction";
import { TimePicker } from "@mui/x-date-pickers";
-import { getThemeType } from "theme/themeType";
-import { green, red } from "@mui/material/colors";
-import { makeStyles } from "@mui/styles";
-
-
+import { useInteractionsAPI, useSnackbar } from "hooks";
+import { useGlobalState } from "providers";
interface Props {
- linkedComponents: Map;
- componentDevices: Map;
- objectType: pond.ObjectType;
- objectKey: string;
-}
-
-interface Alert {
- conditions: pond.InteractionCondition[];
- components: Component[];
+ open: boolean
+ onClose: (refreshCallback: boolean) => void
+ linkedComponents: Map
+ componentDevices: Map
+ //if we pass in a device id we could filter the components to only be for that device, this could be used for the control part
+ device?: Device
}
const useStyles = makeStyles((theme: Theme) => {
- return ({
- borderedContainer: {
+ return (
+ {
+ borderedContainer: {
padding: theme.spacing(1),
marginBottom: theme.spacing(2),
border: "1px solid rgba(255, 255, 255, 0.12)",
@@ -80,176 +49,104 @@ const useStyles = makeStyles((theme: Theme) => {
noPadding: {
padding: 0
},
+ forPrompt: {
+ fontSize: "0.875em",
+ marginTop: "10px"
+ },
timeRange: {
fontSize: "0.875em",
marginTop: "20px"
},
- dark: {
- backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)",
- padding: 5
- },
- light: {
- backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)",
- padding: 5
}
- });
-});
+ )
+})
-export default function ObjectAlerts(props: Props) {
- const { linkedComponents, componentDevices, objectKey, objectType } = props;
- const classes = useStyles();
- const { openSnack } = useSnackbar();
+export default function NewObjectInteraction(props: Props){
+ const { open, onClose, device, linkedComponents, componentDevices } = props
+ const classes = useStyles()
const [{ as }] = useGlobalState();
+ const {openSnack} = useSnackbar();
const interactionsAPI = useInteractionsAPI();
- const [interactions, setInteractions] = useState([]);
- const [typeOptions, setTypeOptions] = useState([]);
- const [alerts, setAlerts] = useState([]);
- //the interaction key to the component it is set on
- const [interactionComponent, setInteractionComponent] = useState>(new Map());
-
- //state variables for notifications
- const notificationAPI = useNotificationAPI();
- const [recentNotifications, setRecentNotifications] = useState([]);
- const [notificationsLoading, setNotificationsLoading] = useState(false);
- const [totalNotifications, setTotalNotifications] = useState(0);
-
- //stat variables for adding new alerts
- const [newAlertOpen, setNewAlertOpen] = useState(false);
- const [selectedAlertComponents, setSelectedAlertComponents] = useState([]);
const [newAlertComponentType, setNewAlertComponentType] = useState(
quack.ComponentType.COMPONENT_TYPE_INVALID
);
const [conditions, setConditions] = useState([]);
+ const [selectedAlertComponents, setSelectedAlertComponents] = useState([]);
+ const [validOptions, setValidOptions] = useState([])
+ const [validComponents, setValidComponents] = useState([])
+ //condition value strings - so that decimals are easier to type into the field
+ const [valStrings, setValStrings] = useState(["0", "0"]);
+ const [numConditions, setNumConditions] = useState(0)
const [nodeOptions, setNodeOptions] = useState([]);
const [subtypeDropdown, setSubtypeDropdown] = useState(0);
- //condition value strings - so that decimals are easier to type into the field
- const [valStrings, setValStrings] = useState(["0", "0"]);
- //the nodes for the subnode interactions
- const [nodeOne, setNodeOne] = useState(0);
- const [nodeTwo, setNodeTwo] = useState(0);
-
+ const [nodeOne, setNodeOne] = useState(0)
+ const [nodeTwo, setNodeTwo] = useState(0)
+ const [maxConditions, setMaxConditions] = useState(2)
+ const [resultType, setResultType] = useState(quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT)
+ const [sinkMotor, setSinkMotor] = useState(false)
+ const [selectedSink, setSelectedSink] = useState("")//the key of the component to be the sink
+ const [sinkOptions, setSinkOptions] = useState([])
+ const [resultMode, setResultMode] = useState(0)
+ const [resultValue, setResultValue] = useState("")
+ const [dutyCycleEnabled, setDutyCycleEnabled] = useState(false)
+ const [dutyCycle, setDutyCycle] = useState("");
+ const [reporting, setReporting] = useState(true)
+ const [notify, setNotify] = useState(true)
//schedule variables
const [schedule, setSchedule] = useState(
pond.InteractionSchedule.create({
weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]
})
);
+
+
useEffect(() => {
- //loop through the linked components to get each of their interactions
- //TODO-CS: consider making an api function to get the interactions for multiple components, possibly based off objects, in one call,
- //to get all interactions for bin components this would solve the issue of making multiple calls as well as not having to use async await
- let newInteractions: Interaction[] = [];
- let icMap: Map = new Map();
- let includedOps: quack.ComponentType[] = [];
- let typeOps: Option[] = [];
- let index = 0;
- //use async to make sure to wait for the response from the api call and have the interactions set before going to get the next one
- //this prevents seperate responses from assigning to the array without knowing about interactions from those seperate responses
- //for example: without async two responses come back at the same time, one has two interactions and another has one but, neither have put their interactions
- //into the array yet so they both reference an empty array, the first response puts its two into an empty array and sets it but then the next one also puts
- //its response into an empty array as well and sets it causing the first two to be lost
- linkedComponents.forEach(async (comp, key) => {
- index++;
- //get the interactions for the component
- let device = componentDevices.get(key);
- if (device !== undefined) {
- await interactionsAPI.listInteractionsByComponent(device, comp.location(), undefined, as).then(resp => {
- if (resp.length > 0) {
- resp.forEach(interaction => {
- icMap.set(interaction.key(), key);
- });
- newInteractions = newInteractions.concat(resp);
- }
- });
- }
- //set the state variables if it is the last one in the list
- if (index === linkedComponents.size) {
- setInteractionComponent(icMap);
- setInteractions([...newInteractions]);
- }
- //use the component type to the type options if it is not already present
- if (!includedOps.includes(comp.type())) {
- includedOps.push(comp.type());
- typeOps.push({
- label: getFriendlyName(comp.type()),
- value: comp.type()
- });
- }
- });
- setTypeOptions(typeOps);
- }, [linkedComponents, interactionsAPI, componentDevices]);
+ //if the device is passed in need to filter out possible components
+ let validCompList: Component[] = []
+ let validOptions: Option[] = []
+ let sinkOptions: Component[] = []
- const matchConditions = (interaction: Interaction, alert: Alert) => {
- //if the number of conditions are not the same they are different
- if (interaction.settings.conditions.length !== alert.conditions.length) {
- return false;
+ if(device){
+ setMaxConditions(device.maxConditions())
}
- //continure with the comparison
- let matching = true;
- interaction.settings.conditions.forEach((condition, i) => {
- if (
- condition.comparison !== alert.conditions[i].comparison ||
- condition.measurementType !== alert.conditions[i].measurementType ||
- condition.value !== alert.conditions[i].value
- ) {
- matching = false;
- }
- });
- return matching;
- };
-
- //build the alerts to display
- useEffect(() => {
- //loop through the interactions
- let currentAlerts: Alert[] = [];
- interactions.forEach(interaction => {
- //if the interaction sends notifications
- if (interaction.settings.notifications && interaction.settings.notifications.notify) {
- //determine if the interaction already has an alert in the alerts array
- let similarAlert: number = -1;
- currentAlerts.forEach((alert, i) => {
- if (matchConditions(interaction, alert)) {
- similarAlert = i;
- }
- });
-
- //if no current alert matched the conditions add a new alert to the array
- let compKey = interactionComponent.get(interaction.key());
- if (compKey) {
- let component = linkedComponents.get(compKey);
- if (component) {
- if (similarAlert === -1) {
- let newAlert: Alert = {
- conditions: interaction.settings.conditions,
- components: [component]
- };
- currentAlerts.push(newAlert);
- } else {
- currentAlerts[similarAlert].components.push(component);
- }
- }
+
+ linkedComponents.forEach((comp, key) => {
+ if(componentDevices.get(key) === device?.id() || !device){
+ let ext = extension(comp.type(), comp.subType())
+ if(ext.isController){
+ sinkOptions.push(comp)
}
+ validCompList.push(comp)
+ }
+ })
+
+ let included: quack.ComponentType[] = []
+ validCompList.forEach(comp => {
+ if(included.includes(comp.type())) return
+ let ext = extension(comp.type(), comp.subType())
+ if(device){
+ if(!ext.isController){
+ included.push(comp.type())
+ validOptions.push({
+ label: ext.friendlyName,
+ value: comp.type()
+ })
+ }
+ }else{
+ included.push(comp.type())
+ validOptions.push({
+ label: ext.friendlyName,
+ value: comp.type()
+ })
}
- });
- setAlerts(currentAlerts);
- }, [interactions, interactionComponent, linkedComponents]);
- //get the notifications for the components on an object
- useEffect(() => {
- if (notificationsLoading) return;
- setNotificationsLoading(true);
- notificationAPI
- .listObjectNotifications(objectKey, objectType, 10, 0, undefined, undefined, undefined, as)
- .then(resp => {
- setRecentNotifications(resp.data.notifications);
- setTotalNotifications(resp.data.total);
- })
- .catch(_err => {})
- .finally(() => {
- setNotificationsLoading(false);
- });
- }, [objectKey, objectType, notificationAPI]); //eslint-disable-line react-hooks/exhaustive-deps
+ })
+
+ setValidOptions(validOptions)
+ setValidComponents(validCompList)
+ setSinkOptions(sinkOptions)
+ },[device, linkedComponents, componentDevices])
//use the selected components to determine the lowest amount of nodes for the options
useEffect(() => {
@@ -257,7 +154,7 @@ export default function ObjectAlerts(props: Props) {
setNodeTwo(0);
setSubtypeDropdown(0);
let nodeCount = 12; //start with the maximum number of nodes for a cable
- linkedComponents.forEach(comp => {
+ validComponents.forEach(comp => {
if (selectedAlertComponents.includes(comp.key())) {
if (comp.lastMeasurement[0] && comp.lastMeasurement[0].values[0]) {
let nodes = comp.lastMeasurement[0].values[0].values.length;
@@ -274,112 +171,53 @@ export default function ObjectAlerts(props: Props) {
);
}
setNodeOptions(options);
- }, [linkedComponents, selectedAlertComponents]);
+ }, [validComponents, selectedAlertComponents]);
+
- const loadMoreNotifications = () => {
- console.log("load more");
- let current = recentNotifications;
- setNotificationsLoading(true);
- notificationAPI
- .listObjectNotifications(
- objectKey,
- objectType,
- 10,
- current.length,
- undefined,
- undefined,
- undefined,
- as
- )
- .then(resp => {
- if (resp.data.notifications.length > 0) {
- let c = current.concat(resp.data.notifications);
- setRecentNotifications([...c]);
- }
- })
- .catch(_err => {})
- .finally(() => {
- setNotificationsLoading(false);
- });
+ const close = (refresh: boolean) => {
+ //reset the state variables to their default values
+ setNewAlertComponentType(quack.ComponentType.COMPONENT_TYPE_INVALID);
+ setConditions([]);
+ setSelectedAlertComponents([]);
+ setValidOptions([])
+ setValidComponents([])
+ setValStrings(["0", "0"]);
+ setNumConditions(0)
+ setNodeOptions([]);
+ setSubtypeDropdown(0);
+ setNodeOne(0)
+ setNodeTwo(0)
+ setMaxConditions(2)
+ setResultType(quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT)
+ setSinkMotor(false)
+ setSelectedSink("")//the key of the component to be the sink
+ setSinkOptions([])
+ setResultMode(0)
+ setResultValue("")
+ setDutyCycleEnabled(false)
+ setDutyCycle("");
+ setReporting(true)
+ setNotify(true)
+ onClose(refresh)
+ }
+ const availableMeasurementTypes = (type: quack.ComponentType): quack.MeasurementType[] => {
+ return getMeasurements(type).map(m => m.measurementType);
};
-
- const readableCondition = (condition: pond.InteractionCondition) => {
- let describer = describeMeasurement(condition.measurementType);
- let type = describer.label();
- let comparison =
- condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO
- ? "Exactly"
- : condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
- ? "Above"
- : "Below";
- let value = describer.toDisplay(condition.value);
- return (
-
- {type + " " + comparison + " " + value + describer.GetUnit()}
-
- );
- };
-
- const alertAccordion = (alert: Alert) => {
- return (
-
- }>
-
- {alert.conditions.map((condition, i) => (
-
- {readableCondition(condition)}
-
- ))}
-
-
-
-
- Reporting Components
- {alert.components.map((comp, i) => (
- {comp.name()}
- ))}
-
-
-
- );
- };
-
- const addInteractionToComponents = () => {
- //array of device and component ids ie, [4:9-5-12, 4:9-5-13, 3:9-5-12]
- let compIds: string[] = [];
- selectedAlertComponents.forEach(comp => {
- let devID = componentDevices.get(comp);
- let component = linkedComponents.get(comp);
- if (devID && component) {
- compIds.push(devID + ":" + component.locationString());
- }
+
+ const initialConditions = (type: quack.ComponentType): pond.InteractionCondition[] => {
+ let measurementType = availableMeasurementTypes(type)[0];
+ let condition = pond.InteractionCondition.create({
+ measurementType: measurementType,
+ comparison: measurementType === Measurement.boolean ? Operator.equals : Operator.less,
+ value: 0
});
- let newAlert = pond.InteractionSettings.create();
- newAlert.conditions = conditions;
- newAlert.instance = 1;
- newAlert.schedule = schedule;
- newAlert.subtype = Interaction.create().subtypeFromNodes(nodeOne, nodeTwo);
- newAlert.result = pond.InteractionResult.create({
- type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT
- });
- newAlert.notifications = pond.InteractionNotifications.create({
- notify: true
- });
- interactionsAPI
- .addInteractionToComponents(compIds, newAlert, as)
- .then(_resp => {
- openSnack("interaction added to selected components");
- })
- .catch(_err => {
- openSnack("there was a problem adding the interaction to the selected components");
- });
- setNewAlertOpen(false);
+ return [condition];
};
//display the components that match the type that was selected for the new alert
const listMatchingComponents = () => {
let matching: Component[] = [];
- linkedComponents.forEach(comp => {
+ validComponents.forEach(comp => {
if (comp.type() === newAlertComponentType) {
matching.push(comp);
}
@@ -409,20 +247,6 @@ export default function ObjectAlerts(props: Props) {
);
};
- const availableMeasurementTypes = (type: quack.ComponentType): quack.MeasurementType[] => {
- return getMeasurements(type).map(m => m.measurementType);
- };
-
- const initialConditions = (type: quack.ComponentType): pond.InteractionCondition[] => {
- let measurementType = availableMeasurementTypes(type)[0];
- let condition = pond.InteractionCondition.create({
- measurementType: measurementType,
- comparison: measurementType === Measurement.boolean ? Operator.equals : Operator.less,
- value: 0
- });
- return [condition];
- };
-
const measurementTypeMenuItems = () => {
return availableMeasurementTypes(newAlertComponentType).map(measurementType => (
@@ -443,18 +267,18 @@ export default function ObjectAlerts(props: Props) {
let isBoolean = condition.measurementType === Measurement.boolean;
return (
-
{index > 0 && (
-
+
AND
-
+
)}
-
+
{measurementTypeMenuItems()}
-
-
+
+
-
-
+
+
]}
-
-
- {index === 0 ? (
-
- 1}
- aria-label="Add another condition"
- onClick={() => {
- let c = cloneDeep(conditions);
- let newConditions = c.concat(initialConditions(newAlertComponentType));
- setConditions(newConditions);
- }}
- className={classNames(classes.greenButton, classes.noPadding)}>
-
-
-
- ) : (
-
- {
- let c = conditions;
- c.splice(index, 1);
- setConditions([...c]);
- }}
- className={classNames(classes.redButton, classes.noPadding)}>
-
-
-
- )}
-
-
+
+
+
+ {
+ conditions.splice(index, 1)
+ setNumConditions(conditions.length)
+ }}
+ className={classNames(classes.redButton, classes.noPadding)}>
+
+
+
+
+
{nodeOptions.length > 2 && (
-
-
+
-
-
+
+
{(subtypeDropdown === 2 || subtypeDropdown === 3 || subtypeDropdown === 4) && (
)}
-
-
+
+
{subtypeDropdown === 3 && (
)}
-
-
+
+
)}
);
};
const conditionInput = () => {
- // const conditionGroups: JSX.Element[] = [];
- // for (let i = 0; i < conditions.length; i++) {
- // conditionGroups[i] = conditionGroup(i);
- // }
return (
You must select a source before adding conditions
)}
+
+ = maxConditions}
+ aria-label="Add another condition"
+ onClick={() => {
+ let newConditions = conditions.concat(initialConditions(newAlertComponentType));
+ setConditions(newConditions);
+ setNumConditions(newConditions.length)
+ }}
+ className={classNames(classes.greenButton, classes.noPadding)}>
+
+
+
);
};
+ const sinkSelector = () => {
+ return (
+ {
+ setSelectedSink(e.target.value)
+ setSinkMotor(linkedComponents.get(e.target.value)?.type() === ComponentType.stepperMotor)
+ }}
+ fullWidth
+ autoFocus={false}
+ margin="dense"
+ variant="standard"
+ InputLabelProps={{ shrink: true }}>
+
+ Select Controller
+
+ {sinkOptions.map(comp => (
+
+ {comp.name()}
+
+ ))}
+
+ );
+ }
+
+ const describeSink = (): MeasurementDescriber => {
+ const sink = linkedComponents.get(selectedSink);
+ return describeMeasurement(
+ Measurement.boolean,
+ or(sink, Component.create()).settings.type,
+ or(sink, Component.create()).settings.subtype
+ );
+ };
+
+ const dutyCycleInput = () => {
+ return (
+
+
+ {
+ setDutyCycleEnabled(checked)
+ }}
+ value="enableDutyCycle"
+ color="secondary"
+ />
+ }
+ label="Once every"
+ />
+
+
+ {
+ setDutyCycle(e.target.value)
+ }}
+ fullWidth
+ autoFocus={false}
+ margin="dense"
+ variant="standard"
+ disabled={!dutyCycleEnabled}
+ error={isNaN(Number(dutyCycle)) || Number(dutyCycle) < 0}
+ InputLabelProps={{
+ shrink: true
+ }}
+ InputProps={{
+ endAdornment: seconds
+ }}>
+
+
+ );
+ };
+
+ const notificationInput = () => {
+ return (
+
+ {resultType !== quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT && (
+
+ {
+ setReporting(checked)
+ }}
+ value="notificationReports"
+ color="secondary"
+ />
+ }
+ label="Report"
+ />
+
+ )}
+
+ {
+ setNotify(checked)
+ }}
+ value="interactionNotification"
+ color="secondary"
+ />
+ }
+ label="Notify"
+ disabled={!reporting}
+ />
+
+ {notify && (
+
+
+ Everyone with notifications enabled for this device will receive an email and/or SMS
+ each time this result occurs
+
+
+ )}
+
+ );
+ };
+
+ const resultInput = () => {
+ return (
+
+ Result
+
+
+ {
+ setResultType(+e.target.value as quack.InteractionResultType)
+ }}
+ autoFocus={false}
+ margin="dense"
+ variant="standard"
+ fullWidth
+ InputLabelProps={{ shrink: true }}>
+ Report
+ Set
+ Toggle
+ Run
+
+
+ {resultType === quack.InteractionResultType.INTERACTION_RESULT_TYPE_RUN && (
+
+
+ {sinkSelector()}
+
+ {sinkMotor && (
+
+ {
+ setResultMode(+e.target.value)
+ }}
+ fullWidth
+ autoFocus={false}
+ margin="dense"
+ variant="standard"
+ InputLabelProps={{ shrink: true }}>
+
+ Clockwise
+
+
+ Counter Clockwise
+
+
+
+ )}
+
+
+ for
+
+
+
+ {
+ setResultValue(e.target.value)
+ }}
+ autoFocus={false}
+ margin="dense"
+ variant="standard"
+ fullWidth
+ InputProps={{
+ endAdornment: sec
+ }}
+ InputLabelProps={{ shrink: true }}
+ />
+
+
+ )}
+ {resultType === quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET && (
+
+
+ {sinkSelector()}
+
+ {linkedComponents.get(selectedSink)?.type() !==
+ quack.ComponentType.COMPONENT_TYPE_INTERNAL_FUNCTION && (
+
+
+ to
+
+
+ )}
+ {linkedComponents.get(selectedSink)?.type() !==
+ quack.ComponentType.COMPONENT_TYPE_INTERNAL_FUNCTION && (
+
+ {
+ setResultValue(e.target.value)
+ }}
+ fullWidth
+ autoFocus={false}
+ margin="dense"
+ variant="standard"
+ InputLabelProps={{ shrink: true }}>
+ {extension(linkedComponents.get(selectedSink)?.type() ?? quack.ComponentType.COMPONENT_TYPE_INVALID).states.map((state, i) => (
+
+ {state}
+
+ ))}
+
+
+ )}
+
+ )}
+ {resultType === quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE && (
+
+
+ {sinkSelector()}
+
+
+ {
+ if(checked){
+ setResultValue('1')
+ }else{
+ setResultValue('0')
+ }
+ }}
+ color="secondary"
+ />
+ }
+ label={describeSink().enumerations()[isNaN(parseFloat(resultValue))?0:parseFloat(resultValue)]}
+ />
+
+
+ )}
+ {resultType === quack.InteractionResultType.INTERACTION_RESULT_TYPE_RUN &&
+ dutyCycleInput()}
+ {notificationInput()}
+
+
+ );
+ };
+
+ const toggleDaySelected = (day: string, event: any) => {
+ let updatedSchedule = cloneDeep(schedule);
+ if (event.target.checked) {
+ if (!updatedSchedule.weekdays.includes(day)) {
+ updatedSchedule.weekdays.push(day);
+ }
+ } else {
+ updatedSchedule.weekdays.splice(updatedSchedule.weekdays.indexOf(day), 1);
+ }
+ setSchedule(updatedSchedule);
+ };
+
+ const daySelector = (day: string, size: any) => {
+ //const schedule = or(interaction.settings.schedule, pond.InteractionSchedule.create());
+ return (
+
+ toggleDaySelected(day, event)}
+ value={day}
+ color="secondary"
+ />
+ }
+ label={capitalize(day.substr(0, 2))}
+ labelPlacement="bottom"
+ />
+
+ );
+ };
+
const setScheduleTime = (id: string, event: any) => {
let updatedSchedule = cloneDeep(schedule);
if (id === "shortcut") {
@@ -732,39 +870,6 @@ export default function ObjectAlerts(props: Props) {
setSchedule(updatedSchedule);
};
- const toggleDaySelected = (day: string, event: any) => {
- let updatedSchedule = cloneDeep(schedule);
- if (event.target.checked) {
- if (!updatedSchedule.weekdays.includes(day)) {
- updatedSchedule.weekdays.push(day);
- }
- } else {
- updatedSchedule.weekdays.splice(updatedSchedule.weekdays.indexOf(day), 1);
- }
- setSchedule(updatedSchedule);
- };
-
- const daySelector = (day: string, size: any) => {
- //const schedule = or(interaction.settings.schedule, pond.InteractionSchedule.create());
- return (
-
- toggleDaySelected(day, event)}
- value={day}
- color="secondary"
- />
- }
- label={capitalize(day.substr(0, 2))}
- labelPlacement="bottom"
- />
-
- );
- };
-
const scheduler = () => {
//let schedule = pond.InteractionSchedule.create();
let timezone = moment.tz.guess();
@@ -789,7 +894,7 @@ export default function ObjectAlerts(props: Props) {
return (
Schedule
-
+
{daySelector("sunday", 3)}
{daySelector("monday", 3)}
{daySelector("tuesday", 3)}
@@ -797,9 +902,9 @@ export default function ObjectAlerts(props: Props) {
{daySelector("thursday", 4)}
{daySelector("friday", 4)}
{daySelector("saturday", 4)}
-
-
-
+
+
+
-
+
{showStart && (
-
+
}
value={start}
onChange={(event: any) => setScheduleTime("start", event)}
/>
-
+
)}
{showBoth && (
-
+
to
-
+
)}
{showEnd && (
-
+
}
value={end}
onChange={(event: any) => setScheduleTime("end", event)}
/>
-
+
)}
-
+
);
};
- const newAlertDialog = () => {
- return (
+ const addInteractionToComponents = () => {
+ //array of device and component ids ie, [4:9-5-12, 4:9-5-13, 3:9-5-12]
+ let compIds: string[] = [];
+ selectedAlertComponents.forEach(comp => {
+ let devID = componentDevices.get(comp);
+ let component = linkedComponents.get(comp);
+ if (devID && component) {
+ compIds.push(devID + ":" + component.locationString());
+ }
+ });
+ let newInteraction = pond.InteractionSettings.create();
+ newInteraction.conditions = conditions;
+ newInteraction.instance = 1;
+ newInteraction.schedule = schedule;
+ newInteraction.subtype = Interaction.create().subtypeFromNodes(nodeOne, nodeTwo);
+ newInteraction.sink = linkedComponents.get(selectedSink)?.location()
+ newInteraction.result = pond.InteractionResult.create({
+ type: resultType,
+ mode: resultMode,
+ value: isNaN(parseFloat(resultValue)) ? 0 : parseFloat(resultValue),
+ dutyCycle: isNaN(parseFloat(dutyCycle)) ? 0 : parseFloat(dutyCycle)
+ });
+ newInteraction.notifications = pond.InteractionNotifications.create({
+ reports: reporting,
+ notify: reporting && notify
+ });
+ interactionsAPI
+ .addInteractionToComponents(compIds, newInteraction, as)
+ .then(_resp => {
+ openSnack("interaction added to selected components");
+ })
+ .catch(_err => {
+ openSnack("there was a problem adding the interaction to the selected components");
+ }).finally(() => {
+ close(true)
+ });
+ };
+
+ return (
{
- setNewAlertOpen(false);
+ close(false);
}}>
Set New Alert
@@ -882,25 +1024,23 @@ export default function ObjectAlerts(props: Props) {
Select Component Type
- {typeOptions.map(option => (
+ {validOptions.map(option => (
{option.label}
))}
- {/* list of checkboxes for the components that match that type */}
{listMatchingComponents()}
- {/* have the conditions set here */}
{conditionInput()}
- {/* have the schedule set here */}
+ {device && resultInput()}
{scheduler()}
- setNewAlertOpen(false)} variant="contained">
+ close(false)} variant="contained">
Cancel
@@ -908,66 +1048,5 @@ export default function ObjectAlerts(props: Props) {
- );
- };
-
- const alertDisplay = () => {
- return (
-
- Alerts
- {alerts.map((alert, i) => (
- {alertAccordion(alert)}
- ))}
-
- );
- };
-
- const notificationList = () => {
- return (
-
- Notifications
- {recentNotifications && (
-
- {recentNotifications.map((notification, i) => (
-
-
- {moment(notification.status?.timestamp).format("MMMM DD, YYYY - h:mm a")}
- {" "}
- :{" "}
-
- {notification.settings?.title + " - " + notification.settings?.subtitle}
-
-
- ))}
- {recentNotifications.length < totalNotifications && (
-
-
- Get More
-
-
- )}
-
- )}
-
- );
- };
-
- return (
-
-
- Alerts & Notifications
- {
- setNewAlertOpen(true);
- }}>
- New Alert
-
-
- {alertDisplay()}
- {notificationList()}
- {newAlertDialog()}
-
- );
-}
+ )
+}
\ No newline at end of file
diff --git a/src/objects/objectInteractions/Notifications.tsx b/src/objects/objectInteractions/Notifications.tsx
new file mode 100644
index 0000000..6cc65e8
--- /dev/null
+++ b/src/objects/objectInteractions/Notifications.tsx
@@ -0,0 +1,111 @@
+import { Button, List, ListItem, ListSubheader, Typography } from "@mui/material";
+import { makeStyles } from "@mui/styles";
+import moment from "moment";
+import { pond } from "protobuf-ts/pond";
+import { useGlobalState, useNotificationAPI } from "providers";
+import React, { useEffect, useRef, useState } from "react";
+import { getThemeType } from "theme/themeType";
+
+interface Props {
+ objectKey: string
+ objectType: pond.ObjectType
+}
+
+const useStyles = makeStyles(() => {
+ return ({
+ dark: {
+ backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)",
+ padding: 5
+ },
+ light: {
+ backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)",
+ padding: 5
+ },
+ })
+})
+
+export default function Notifications(props: Props){
+ const {objectKey, objectType} = props
+ const classes = useStyles();
+ const [recentNotifications, setRecentNotifications] = useState([]);
+ const [notificationsLoading, setNotificationsLoading] = useState(false)
+ const loadingRef = useRef(false)
+ const [{as}] = useGlobalState();
+ const notificationAPI = useNotificationAPI();
+ const [totalNotifications, setTotalNotifications] = useState(0)
+
+
+ //get the notifications for the components on an object
+ useEffect(() => {
+ if (loadingRef.current) return
+ loadingRef.current = true
+ notificationAPI
+ .listObjectNotifications(objectKey, objectType, 10, 0, undefined, undefined, undefined, as)
+ .then(resp => {
+ setRecentNotifications(resp.data.notifications);
+ setTotalNotifications(resp.data.total);
+ })
+ .catch(_err => {})
+ .finally(() => {
+ loadingRef.current = true;
+ });
+ }, [objectKey, objectType, notificationAPI]);
+
+ const loadMoreNotifications = () => {
+ let current = recentNotifications;
+ setNotificationsLoading(true);
+ notificationAPI
+ .listObjectNotifications(
+ objectKey,
+ objectType,
+ 10,
+ current.length,
+ undefined,
+ undefined,
+ undefined,
+ as
+ )
+ .then(resp => {
+ if (resp.data.notifications.length > 0) {
+ let c = current.concat(resp.data.notifications);
+ setRecentNotifications([...c]);
+ }
+ })
+ .catch(_err => {})
+ .finally(() => {
+ setNotificationsLoading(false);
+ });
+ };
+
+ return (
+
+
+
+ Notifications
+
+
+ {recentNotifications && (
+
+ {recentNotifications.map((notification, i) => (
+
+
+ {moment(notification.status?.timestamp).format("MMMM DD, YYYY - h:mm a")}
+ {" "}
+ :{" "}
+
+ {notification.settings?.title + " - " + notification.settings?.subtitle}
+
+
+ ))}
+ {recentNotifications.length < totalNotifications && (
+
+
+ Get More
+
+
+ )}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/objects/objectInteractions/ObjectInteractions.tsx b/src/objects/objectInteractions/ObjectInteractions.tsx
new file mode 100644
index 0000000..d50adb9
--- /dev/null
+++ b/src/objects/objectInteractions/ObjectInteractions.tsx
@@ -0,0 +1,229 @@
+import { Box, Button, Card, Typography } from "@mui/material";
+import { useCallback, useEffect, useState } from "react";
+import Alerts, { Alert } from "./Alerts";
+import Controls, { Control } from "./Controls";
+import { Component, Device, Interaction } from "models";
+import { quack } from "protobuf-ts/quack";
+import { Option } from "common/SearchSelect";
+import { useInteractionsAPI } from "hooks";
+import { useGlobalState } from "providers";
+import { extension, getFriendlyName } from "pbHelpers/ComponentType";
+import { sameComponentID } from "pbHelpers/Component";
+import Notifications from "./Notifications";
+import { pond } from "protobuf-ts/pond";
+import React from "react";
+import NewObjectInteraction from "./NewObjectInteraction";
+
+interface Props {
+ linkedComponents: Map; //the component key to the component object
+ componentDevices: Map;
+ objectKey: string
+ objectType: pond.ObjectType
+ devices: Device[];
+ permissions: pond.Permission[]
+}
+
+export default function ObjectInteractions(props: Props) {
+ const { linkedComponents, componentDevices, devices, objectKey, objectType, permissions } = props
+ const [{as}] = useGlobalState()
+ //list of alerts, each alert contains a list of interactions with matching conditions
+ const [alerts, setAlerts] = useState([]);
+ //list of interactions relating to a controller
+ const [controls, setControls] = useState([]);
+ const interactionsAPI = useInteractionsAPI();
+ const [openNewInteraction, setOpenNewInteraction] = useState(false)
+ const [allSinks, setAllSinks] = useState([]);
+ // const [typeOptions, setTypeOptions] = useState([]);
+ const [selectedDevice, setSelectedDevice] = useState(undefined)
+
+
+ const matchConditions = (interaction: Interaction, set: Alert | Control) => {
+ //if the number of conditions are not the same they are different
+ if (interaction.settings.conditions.length !== set.conditions.length) {
+ return false;
+ }
+ //continue with the comparison
+ let matching = true;
+ interaction.settings.conditions.forEach((condition, i) => {
+ if (
+ condition.comparison !== set.conditions[i].comparison ||
+ condition.measurementType !== set.conditions[i].measurementType ||
+ condition.value !== set.conditions[i].value
+ ) {
+ matching = false;
+ }
+ });
+ return matching;
+ };
+
+ const matchResult = (interaction: Interaction, set: Control) => {
+ let interactionResult = interaction.settings.result
+ let setResult = set.result
+ let matching = true
+ // if anything in the result for the interaction is different from the set make matching false
+ if(interactionResult?.type !== setResult?.type ||
+ interactionResult?.mode !== setResult?.mode ||
+ interactionResult?.value !== setResult?.value ||
+ interactionResult?.dutyCycle !== setResult?.dutyCycle
+ ){
+ matching = false
+ }
+ return matching
+ }
+
+ const findDevice = (deviceID: number) => {
+ for (let device of devices) {
+ if (device.id() === deviceID) return device
+ }
+ }
+
+ const findSinkComponent = (sinks: Component[], interactionSink: quack.ComponentID, sourceDevice: number) => {
+ for (let component of sinks) {
+ if (sameComponentID(component.location(), interactionSink) && sourceDevice === componentDevices.get(component.key())){
+ return component
+ }
+ }
+ }
+
+ const load = useCallback(()=>{
+const newInteractions: Interaction[] = [];
+ const interactionSourceMap = new Map();
+ const sinkOptions: Component[] = [];
+ const alerts: Alert[] = [];
+ const controls: Control[] = [];
+
+ const run = async () => {
+ // Run all component API fetches in parallel
+ await Promise.all(
+ [...linkedComponents].map(async ([key, comp]) => {
+ const device = componentDevices.get(key);
+
+ if (device) {
+ const resp = await interactionsAPI.listInteractionsByComponent(
+ device,
+ comp.location(),
+ undefined,
+ as
+ );
+
+ resp.forEach(interaction => interactionSourceMap.set(interaction.key(), key));
+ newInteractions.push(...resp);
+ }
+
+ // Collect controller components
+ if (extension(comp.type()).isController) {
+ sinkOptions.push(comp);
+ }
+ })
+ );
+ };
+
+ run().then(() => {
+ newInteractions.forEach(interaction => {
+ //alert and control are not mutally exclusive, it is possible to be both if the control is sending notifications
+ if (interaction.isAlert()) {
+ // Find matching alert
+ let similarAlertIndex = alerts.findIndex(alert =>
+ matchConditions(interaction, alert)
+ );
+
+ const compKey = interactionSourceMap.get(interaction.key());
+ const component = compKey ? linkedComponents.get(compKey) : undefined;
+
+ if (component) {
+ if (similarAlertIndex === -1) {
+ alerts.push({
+ conditions: interaction.settings.conditions,
+ sourceComponents: [component],
+ });
+ } else {
+ alerts[similarAlertIndex].sourceComponents.push(component);
+ }
+ }
+
+ }
+ if (interaction.isControl()) {
+ if (!interaction.settings.sink) return //if there is no sink it cant be a control so move on to the next
+ const compKey = interactionSourceMap.get(interaction.key());
+ if (!compKey) return //we have to have a valid component key
+ const component = linkedComponents.get(compKey);
+ const deviceID = componentDevices.get(compKey);
+ if (!deviceID) return //we have to know which device it is for
+ const controlDevice = findDevice(deviceID)
+ if (!controlDevice) return //failed to get the device from the array
+ const sinkComponent = findSinkComponent(sinkOptions, interaction.settings.sink, deviceID)
+ if (!sinkComponent) return //failed to get the correct sink component
+
+ let similarControlIndex = controls.findIndex(control =>
+ matchConditions(interaction, control) &&
+ matchResult(interaction, control) &&
+ sameComponentID(interaction.settings.sink, control.sinkComponent.location()) && //if the sink for the interaction is the same address as the existing control
+ deviceID === control.device.id() //if the device id for source component is the same as the device id in the control
+ );
+
+ if (component && interaction.settings.result) {
+ if (similarControlIndex === -1) {
+ controls.push({
+ conditions: interaction.settings.conditions,
+ result: interaction.settings.result,
+ sourceComponents: [component],
+ sinkComponent: sinkComponent,
+ device: controlDevice
+ });
+ } else {
+ controls[similarControlIndex].sourceComponents.push(component);
+ }
+ }
+ }
+ });
+ setAllSinks(sinkOptions);
+ setAlerts(alerts);
+ setControls(controls);
+ }).catch(err => {
+ console.error("Interaction fetch error:", err)
+ })
+ },[linkedComponents, interactionsAPI, componentDevices, as])
+
+ useEffect(() => {
+ load()
+ }, [load]);
+
+
+
+ return (
+
+ {
+ setOpenNewInteraction(false)
+ if(refresh){
+ load()
+ }
+ }}
+ linkedComponents={linkedComponents}
+ componentDevices={componentDevices}
+ device={selectedDevice}/>
+
+
+ Interactions
+ {
+ setSelectedDevice(device)
+ setOpenNewInteraction(true)
+ }}/>
+ {
+ setSelectedDevice(undefined)
+ setOpenNewInteraction(true)
+ }}/>
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx
index a7c6816..a1dc0e3 100644
--- a/src/pages/Bin.tsx
+++ b/src/pages/Bin.tsx
@@ -50,11 +50,9 @@ import { GrainCable } from "models/GrainCable";
// import { Controller } from "models/Controller";
import { Pressure } from "models/Pressure";
import { CheckCircle, ExpandMore, Warning } from "@mui/icons-material";
-import { getThemeType } from "theme";
import BinGraphs from "bin/graphs/BinGraphs";
import BinTour from "bin/BinTour";
import { getBinModel } from "common/DataImports/BinCables/BinCableImporter";
-import ObjectAlerts from "objects/ObjectAlerts";
import DevicePresetController from "device/presets/devicePresetController";
import { DevicePreset } from "models/DevicePreset";
import BinVisualizerV2 from "bin/BinVisualizerV2";
@@ -69,6 +67,7 @@ import TaskViewer from "tasks/TaskViewer";
import ButtonGroup from "common/ButtonGroup";
import BinTransactions from "bin/BinTransactions";
import { CO2 } from "models/CO2";
+import ObjectInteractions from "objects/objectInteractions/ObjectInteractions";
interface TabPanelProps {
children?: React.ReactNode;
@@ -196,6 +195,7 @@ export default function Bin(props: Props) {
const [headspaceCO2, setHeadspaceCO2] = useState([]);
const [heaters, setHeaters] = useState([]);
const [fans, setFans] = useState([]);
+ const [activeDetails, setActiveDetails] = useState([0])
const [detail, setDetail] = useState<
"inventory" | "sensors" | "analytics" | "presets" | "alerts" | "transactions"
>("inventory");
@@ -806,7 +806,7 @@ export default function Bin(props: Props) {
{overview()}
{preferences && binComponents(preferences)}
-
+
{(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING ||
bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING ||
bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) && (
@@ -826,82 +826,66 @@ export default function Bin(props: Props) {
-
-
-
+
+ setDetail("inventory")
+ function: () => {
+ setDetail("inventory")
+ setActiveDetails([0])
+ }
},
{
title: "Sensors",
- function: () => setDetail("sensors")
+ function: () => {
+ setDetail("sensors")
+ setActiveDetails([1])
+ }
},
{
title: "Analysis",
- function: () => setDetail("analytics")
+ function: () => {
+ setDetail("analytics")
+ setActiveDetails([2])
+ }
},
{
title: "Alerts",
- function: () => setDetail("alerts")
+ function: () => {
+ setDetail("alerts")
+ setActiveDetails([3])
+ }
},
{
title: "Presets",
- function: () => setDetail("presets")
+ function: () => {
+ setDetail("presets")
+ setActiveDetails([4])
+ }
},
{
title: "Transactions",
- function: () => setDetail("transactions")
+ function: () => {
+ setDetail("transactions")
+ setActiveDetails([5])
+ }
}
]}
/>
- {/*
- setDetail("inventory")}>
- Inventory
-
- setDetail("sensors")}
- value={"sensors"}
- aria-label="Bin Sensor Graphs">
- Sensors
-
- setDetail("analytics")}
- value={"analytics"}
- aria-label="Bin Analysis Graphs">
- Analysis
-
- setDetail("alerts")}
- value={"alerts"}
- aria-label="Device Alerts">
- Alerts
-
- setDetail("presets")}
- value={"presets"}
- aria-label="Bin Mode Presets">
- Presets
-
- */}
{detail === "alerts" && (
-
)}
@@ -917,23 +901,27 @@ export default function Bin(props: Props) {
/>
)}
- {(detail === "inventory" || detail === "sensors" || detail === "analytics") && (
-
- )}
{detail === "transactions" &&
+
+
}
+ {(detail === "inventory" || detail === "sensors" || detail === "analytics") && (
+
+
+
+ )}
@@ -1035,11 +1023,13 @@ export default function Bin(props: Props) {
-
@@ -1177,7 +1167,11 @@ export default function Bin(props: Props) {
{objectTeams()}
{deviceMenu()}
-
+ {
+ setDetail(detail)
+ setActiveDetails([buttonIndex])
+ }}/>
);
}
diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx
index 7344659..ef9b673 100644
--- a/src/pages/Device.tsx
+++ b/src/pages/Device.tsx
@@ -169,7 +169,6 @@ export default function DevicePage() {
const loadPortScan = () => {
deviceAPI.listFoundComponents(parseInt(deviceID))
.then(resp => {
- console.log(resp.data)
if (resp.data.foundComponents?.i2c || resp.data.foundComponents?.oneWire){
setScannedAddresses(pond.ListFoundComponentsResponse.fromObject(resp.data).foundComponents ?? undefined)
}
diff --git a/src/pages/SignupCallback.tsx b/src/pages/SignupCallback.tsx
index a22dbfc..43c8c5b 100644
--- a/src/pages/SignupCallback.tsx
+++ b/src/pages/SignupCallback.tsx
@@ -1,13 +1,15 @@
import { useDeviceAPI, useMobile, useUserAPI } from "hooks";
import PageContainer from "./PageContainer";
import { useEffect, useState } from "react";
-import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Typography } from "@mui/material";
+import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Grid2, MenuItem, TextField, Typography } from "@mui/material";
import { useBinAPI, useGlobalState, useTeamAPI } from "providers";
import { CheckCircleOutline } from "@mui/icons-material";
import { green } from "@mui/material/colors";
import Tour, { TourStep } from "common/Tour";
import Emoji from "react-emoji-render";
import { getWhitelabel, IsAdaptiveAgriculture } from "services/whiteLabel";
+import { pond } from "protobuf-ts/pond";
+import { User } from "models";
// import { color } from "framer-motion";
// interface Props {
@@ -21,7 +23,7 @@ export default function SignupCallback () {
const deviceAPI = useDeviceAPI();
const binAPI = useBinAPI();
const isMobile = useMobile()
- const [{ user }] = useGlobalState();
+ const [globalState, dispatch] = useGlobalState();
const whiteLabel = getWhitelabel()
const [loading, setLoading] = useState(false)
@@ -35,6 +37,7 @@ export default function SignupCallback () {
const [doneBins, setDoneBins] = useState(false)
const [doneTeams, setDoneTeams] = useState(false)
const [doneDevices, setDoneDevices] = useState(false)
+ const [user, setUser] = useState(globalState.user);
useEffect(() => {
setLoading(true)
@@ -55,6 +58,124 @@ export default function SignupCallback () {
return loading === false && doneBins && doneTeams && doneDevices
}
+ const changePressureUnit = (value: any) => {
+ let updatedUser = User.clone(user)
+ let pressureUnit: pond.PressureUnit;
+ switch (value) {
+ case 1:
+ pressureUnit = pond.PressureUnit.PRESSURE_UNIT_KILOPASCALS;
+ break;
+ case 2:
+ pressureUnit = pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER;
+ break;
+ default:
+ pressureUnit = pond.PressureUnit.PRESSURE_UNIT_UNKNOWN;
+ break;
+ }
+ updatedUser.settings.pressureUnit = pressureUnit;
+ setUser(updatedUser)
+ };
+
+ const changeTemperatureUnit = (value: any) => {
+ let updatedUser = User.clone(user);
+ let temperatureUnit: pond.TemperatureUnit;
+ switch (value) {
+ case 1:
+ temperatureUnit = pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS;
+ break;
+ case 2:
+ temperatureUnit = pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT;
+ break;
+ default:
+ temperatureUnit = pond.TemperatureUnit.TEMPERATURE_UNIT_UNKNOWN;
+ break;
+ }
+ updatedUser.settings.temperatureUnit = temperatureUnit;
+ setUser(updatedUser);
+ };
+
+ const unitPreferences = () => {
+ const { pressureUnit, temperatureUnit, distanceUnit, grainUnit } = user.settings;
+ return (
+
+
+ changePressureUnit(event.target.value)}
+ margin="normal"
+ variant="outlined"
+ InputLabelProps={{ shrink: true }}>
+
+ Kilopascals (kPa)
+
+
+ Inches of Water (iwg)
+
+
+
+
+ changeTemperatureUnit(event.target.value)}
+ margin="normal"
+ variant="outlined"
+ InputLabelProps={{ shrink: true }}>
+ Celsius (°C)
+
+ Fahrenheit (°F)
+
+
+
+
+ changeDistanceUnit(event.target.value)}
+ margin="normal"
+ variant="outlined"
+ InputLabelProps={{ shrink: true }}>
+ Meters (m)
+ Feet (ft)
+
+
+ {IsAdaptiveAgriculture() && (
+
+ changeGrainUnit(event.target.value)}
+ margin="normal"
+ variant="outlined"
+ InputLabelProps={{ shrink: true }}>
+ Bushels (bu)
+ Tonnes (mT)
+
+
+ )}
+
+ );
+ };
+
const content = () => {
return (
@@ -104,12 +225,17 @@ export default function SignupCallback () {
content: (
<>
- In the user menu, you can make changes to your profile, and adjust unit preferences.
+ In the user menu, you can make changes to your profile, such as adjusting unit preferences.
If you are part of a team, you can select it here as well.
+
+
+ Set your unit preferences here now, they can be changed later from this menu:
+
+ {unitPreferences()}
>
),
target: "#tour-user-menu",
@@ -150,35 +276,104 @@ export default function SignupCallback () {
target: "#tour-dashboard",
placement: "right"
})
- if (IsAdaptiveAgriculture()) steps.push({
- title: "Bins",
+ steps.push({
+ title: "Tasks",
content: (
<>
- You can view your bins and their inventory here.
+ You can view and create tasks here and assign a start and end time for them.
- If you are viewing as a team, your team's bins will be displayed here instead.
+ If you are viewing as a team you will see the teams tasks, you can even assign tasks to team members.
>
),
- target: "#tour-bins",
- placement: "right",
- disableBeacon: true,
+ target: "#tour-tasks",
+ placement: "right"
})
+ //tasks step
+ if (IsAdaptiveAgriculture()) {
+ steps.push({
+ title: "Visual Farm",
+ content: (
+ <>
+
+ You can view your Visual Farm here, it will allow tou to draw fields and plot bins an a map.
+
+
+
+ If you are viewing as a team, your team's fields and bins will be displayed here instead.
+
+ >
+ ),
+ target: "#tour-visual-farm",
+ placement: "right",
+ disableBeacon: true,
+ })
+ steps.push({
+ title: "Fields",
+ content: (
+ <>
+
+ You can view your fields you have drawn on the map here.
+
+
+
+ If you are viewing as a team, your team's fields will be displayed here instead.
+
+ >
+ ),
+ target: "#tour-field-list",
+ placement: "right",
+ disableBeacon: true,
+ })
+ steps.push({
+ title: "Bins",
+ content: (
+ <>
+
+ You can view your bins and their inventory here.
+
+
+
+ If you are viewing as a team, your team's bins will be displayed here instead.
+
+ >
+ ),
+ target: "#tour-bins",
+ placement: "right",
+ disableBeacon: true,
+ })
+ steps.push({
+ title: "Contracts",
+ content: (
+ <>
+
+ You can view and track any contracts you have created here.
+
+
+
+ If you are viewing as a team, your team's contracts will be displayed here instead.
+
+ >
+ ),
+ target: "#tour-contracts",
+ placement: "right",
+ disableBeacon: true,
+ })
+ }
return steps;
};
const endTour = () => {
setIsTourRunning(false);
- // if (user) {
- // user.status.finishedBinIntro = moment().toJSON();
- // userAPI
- // .updateUser(userID, user.protobuf())
- // .then(() => dispatch({ key: "user", value: user }))
- // .catch((err: any) => error(err));
- // }
+ if (user) {
+ userAPI
+ .updateUser(user.id(), user.protobuf())
+ .then(() => dispatch({ key: "user", value: user }))
+ .catch((err: any) => console.error(err));
+ }
};
const closeDialog = () => {
diff --git a/src/pbHelpers/AddressTypes/I2C.ts b/src/pbHelpers/AddressTypes/I2C.ts
index b6bf585..d2f1733 100644
--- a/src/pbHelpers/AddressTypes/I2C.ts
+++ b/src/pbHelpers/AddressTypes/I2C.ts
@@ -18,7 +18,8 @@ export const I2C: AddressTypeExtension = {
[quack.ComponentType.COMPONENT_TYPE_SEN5X, 0x68],
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, 0x6b],
[quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, 0x70],
- [quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, 0x6c]
+ [quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, 0x6c],
+ [quack.ComponentType.COMPONENT_TYPE_TEMP_HUMIDITY, 0x43]
]);
const offset = offsets.get(componentType);
diff --git a/src/pbHelpers/ComponentType.tsx b/src/pbHelpers/ComponentType.tsx
index 03be638..25a0440 100644
--- a/src/pbHelpers/ComponentType.tsx
+++ b/src/pbHelpers/ComponentType.tsx
@@ -60,6 +60,7 @@ import { LineData, Point } from "charts/measurementCharts/MultiLineGraph";
import moment, { Moment } from "moment";
import { avg } from "utils";
import { Airflow } from "./ComponentTypes/Airflow";
+import { TempHumidity } from "./ComponentTypes/TempHumidity";
const COMPONENT_TYPE_MAP = new Map([
[quack.ComponentType.COMPONENT_TYPE_INVALID, Invalid],
@@ -96,7 +97,8 @@ const COMPONENT_TYPE_MAP = new Map([
[quack.ComponentType.COMPONENT_TYPE_SEN5X, Sen5x],
[quack.ComponentType.COMPONENT_TYPE_VIBRATION_CHAIN, VibrationCable],
[quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, dragerGasDongle],
- [quack.ComponentType.COMPONENT_TYPE_AIRFLOW, Airflow]
+ [quack.ComponentType.COMPONENT_TYPE_AIRFLOW, Airflow],
+ [quack.ComponentType.COMPONENT_TYPE_TEMP_HUMIDITY, TempHumidity]
]);
export interface Subtype {
diff --git a/src/pbHelpers/ComponentTypes/TempHumidity.ts b/src/pbHelpers/ComponentTypes/TempHumidity.ts
new file mode 100644
index 0000000..50542ed
--- /dev/null
+++ b/src/pbHelpers/ComponentTypes/TempHumidity.ts
@@ -0,0 +1,85 @@
+import TemperatureHumidityDarkIcon from "assets/components/temperatureHumidityDark.png";
+import TemperatureHumidityLightIcon from "assets/components/temperatureHumidityLight.png";
+import { convertedUnitMeasurement } from "models/UnitMeasurement";
+import {
+ AreaChartData,
+ ComponentTypeExtension,
+ GraphFilters,
+ LineChartData,
+ simpleAreaChartData,
+ simpleLineChartData,
+ simpleMeasurements,
+ simpleSummaries,
+ Summary,
+ unitMeasurementSummaries
+} from "pbHelpers/ComponentType";
+import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
+import { pond } from "protobuf-ts/pond";
+import { quack } from "protobuf-ts/quack";
+
+export function TempHumidity(subtype: number = 0): ComponentTypeExtension {
+ let temperature = describeMeasurement(
+ quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
+ quack.ComponentType.COMPONENT_TYPE_TEMP_HUMIDITY,
+ subtype
+ );
+ let humidity = describeMeasurement(
+ quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
+ quack.ComponentType.COMPONENT_TYPE_TEMP_HUMIDITY,
+ subtype
+ );
+ return {
+ type: quack.ComponentType.COMPONENT_TYPE_TEMP_HUMIDITY,
+ subtypes: [
+ {
+ key: quack.TempHumiditySubtype.TEMP_HUMIDITY_SUBTYPE_HDC302,
+ value: "TEMP_HUMIDITY_SUBTYPE_HDC302",
+ friendlyName: "HDC302"
+ }
+ ],
+ friendlyName: "Temperature/Humidity",
+ description: "Measures the temperature and humidity",
+ isController: false,
+ isSource: true,
+ isCalibratable: true,
+ addressTypes: [quack.AddressType.ADDRESS_TYPE_I2C],
+ interactionResultTypes: [],
+ states: [],
+ measurements: simpleMeasurements(temperature, humidity),
+ measurementSummary: async function(measurement: quack.Measurement): Promise> {
+ return simpleSummaries(measurement, temperature, humidity);
+ },
+ unitMeasurementSummary: (
+ measurements: convertedUnitMeasurement
+ ): Summary[] => {
+ return unitMeasurementSummaries(
+ measurements,
+ quack.ComponentType.COMPONENT_TYPE_TEMP_HUMIDITY,
+ subtype
+ );
+ },
+ areaChartData: (
+ measurement: pond.UnitMeasurementsForComponent,
+ smoothingAverages?: number,
+ filters?: GraphFilters
+ ): AreaChartData => {
+ return simpleAreaChartData(measurement, smoothingAverages, filters);
+ },
+ lineChartData: (
+ measurement: pond.UnitMeasurementsForComponent,
+ smoothingAverages?: number,
+ filters?: GraphFilters
+ ): LineChartData => {
+ return simpleLineChartData(
+ quack.ComponentType.COMPONENT_TYPE_TEMP_HUMIDITY,
+ measurement,
+ smoothingAverages,
+ filters
+ );
+ },
+ minMeasurementPeriodMs: 1000,
+ icon: (theme?: "light" | "dark"): string | undefined => {
+ return theme === "light" ? TemperatureHumidityDarkIcon : TemperatureHumidityLightIcon;
+ }
+ };
+}
diff --git a/src/pbHelpers/DeviceAvailability.ts b/src/pbHelpers/DeviceAvailability.ts
index 972c2e2..6ade54b 100644
--- a/src/pbHelpers/DeviceAvailability.ts
+++ b/src/pbHelpers/DeviceAvailability.ts
@@ -64,7 +64,8 @@ const DefaultAvailability: DeviceAvailabilityMap = new Map