Merge branch 'hdc302_component' into dev_environment

This commit is contained in:
csawatzky 2025-12-19 13:49:01 -06:00
commit 1ad0f26af8
22 changed files with 1770 additions and 638 deletions

2
package-lock.json generated
View file

@ -10953,7 +10953,7 @@
}, },
"node_modules/protobuf-ts": { "node_modules/protobuf-ts": {
"version": "1.0.0", "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": { "dependencies": {
"protobufjs": "^6.8.8" "protobufjs": "^6.8.8"
} }

View file

@ -1,6 +1,6 @@
// import { useAuth0 } from "@auth0/auth0-react"; // import { useAuth0 } from "@auth0/auth0-react";
import { Typography } from "@mui/material"; import { Typography } from "@mui/material";
import Tour from "common/Tour"; import Tour, { TourStep } from "common/Tour";
import { random } from "lodash"; import { random } from "lodash";
import moment from "moment"; import moment from "moment";
import { useGlobalState, useSnackbar, useUserAPI } from "providers"; import { useGlobalState, useSnackbar, useUserAPI } from "providers";
@ -8,8 +8,13 @@ import React, { useEffect, useState } from "react";
import { Step } from "react-joyride"; import { Step } from "react-joyride";
// import Emoji from "react-emoji-render"; // 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 { userID } = useAuth0();
const {setDetailTabState} = props
const [{ user }, dispatch] = useGlobalState(); const [{ user }, dispatch] = useGlobalState();
const userID = user.id() const userID = user.id()
const { error } = useSnackbar(); const { error } = useSnackbar();
@ -35,8 +40,8 @@ export default function BinTour() {
} }
}; };
const getTourSteps = (): Step[] => { const getTourSteps = (): TourStep[] => {
let steps: Step[] = [ let steps: TourStep[] = [
{ {
title: ( title: (
<React.Fragment> <React.Fragment>
@ -103,24 +108,12 @@ export default function BinTour() {
disableBeacon: false disableBeacon: false
}, },
{ {
title: "Graphs", title: "View Other Data",
content: ( content: (
<React.Fragment> <React.Fragment>
<Typography variant="body2" paragraph> <Typography variant="body2" paragraph>
Bin related analytics are displayed here. Use these tabs to view other Bin Details such as inventory levels over time,
</Typography> alerts and interactions for connected components, and data for attached sensors
</React.Fragment>
),
target: "#tour-graphs",
placement: "left",
disableBeacon: false
},
{
title: "Choose your graphs",
content: (
<React.Fragment>
<Typography variant="body2" paragraph>
Use this tab to view other sets of data. Sensors must be attached to view sensor data.
</Typography> </Typography>
</React.Fragment> </React.Fragment>
), ),
@ -128,6 +121,122 @@ export default function BinTour() {
placement: "bottom", placement: "bottom",
disableBeacon: false disableBeacon: false
}, },
{
title: "Inventory",
content: (
<React.Fragment>
<Typography variant="body2" paragraph>
The inventory tab shows your bins inventory level over a specified window as well as the times the bin mode was changed.
</Typography>
</React.Fragment>
),
target: "#tour-details",
placement: "left-start",
disableBeacon: false,
onNext: () => {
if (setDetailTabState) setDetailTabState("sensors", 1)
}
},
{
title: "Sensors",
content: (
<React.Fragment>
<Typography variant="body2" paragraph>
The sensors tab shows readings from attached sensors over time
</Typography>
</React.Fragment>
),
target: "#tour-details",
placement: "left-start",
disableBeacon: false,
onNext: () => {
if (setDetailTabState) setDetailTabState("analytics", 2)
}
},
{
title: "Analytics",
content: (
<React.Fragment>
<Typography variant="body2" paragraph>
The analysis tab shows analytic bin data using attached sensors
</Typography>
</React.Fragment>
),
target: "#tour-details",
placement: "left-start",
disableBeacon: false,
onNext: () => {
if (setDetailTabState) setDetailTabState("alerts", 3)
}
},
{
title: "Alerts and Interactions",
content: (
<React.Fragment>
<Typography variant="body2" paragraph>
The Alerts tab allows you to view or add any new interactions or alerts for connected components
and displays recent notifications from those alerts
</Typography>
</React.Fragment>
),
target: "#tour-details",
placement: "left-start",
disableBeacon: false,
onNext: () => {
if (setDetailTabState) setDetailTabState("presets", 4)
}
},
{
title: "Bin Mode Presets",
content: (
<React.Fragment>
<Typography variant="body2" paragraph>
The Presets tab allows you to create custom presets for the interactions for a device when changing bin modes
</Typography>
</React.Fragment>
),
target: "#tour-details",
placement: "left-start",
disableBeacon: false,
onNext: () => {
if (setDetailTabState) setDetailTabState("transactions", 5)
}
},
{
title: "Bin Transactions",
content: (
<React.Fragment>
<Typography variant="body2" paragraph>
The transactions tab allows view any pending transaction when using a hybrid inventory control
</Typography>
</React.Fragment>
),
target: "#tour-details",
placement: "left-start",
disableBeacon: false,
onNext: () => {
if (setDetailTabState) setDetailTabState("inventory", 0)
}
},
{
title: "Bin Conditions",
content: (
<React.Fragment>
<Typography variant="body2" paragraph>
This section will display conditions based on the bin mode
</Typography>
<Typography variant="body2" paragraph>
Storage Mode: simply displays the Storage conditions by itself with the target temperature and moisture set in the bin settings
</Typography>
<Typography variant="body2" paragraph>
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
</Typography>
</React.Fragment>
),
target: "#tour-conditions",
placement: "right",
disableBeacon: false
},
{ {
title: "Change Mode", title: "Change Mode",
content: ( content: (
@ -138,12 +247,15 @@ export default function BinTour() {
<ul> <ul>
<li>Storage mode: default</li> <li>Storage mode: default</li>
<li> <li>
Drying mode: use heat to dry grain Cooldown mode: use fans to hold bin temperature lower
{/* <Emoji text=" ❆❅" /> */}
</li>
<li>
Drying mode: use heat to dry grain (only visible when the target moisture is lower than the starting moisture in the bin settings)
{/* <Emoji text=" ☀️" /> */} {/* <Emoji text=" ☀️" /> */}
</li> </li>
<li> <li>
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)
{/* <Emoji text=" ❆❅" /> */}
</li> </li>
</ul> </ul>
</React.Fragment> </React.Fragment>

View file

@ -1738,6 +1738,7 @@ export default function BinVisualizer(props: Props) {
{modeChangeInProgress && {modeChangeInProgress &&
<CircularProgress color="primary" size={25}/> <CircularProgress color="primary" size={25}/>
} }
<Box id="tour-bin-mode">
<ButtonGroup <ButtonGroup
disableAll={modeChangeInProgress} disableAll={modeChangeInProgress}
buttons={[ buttons={[
@ -1765,6 +1766,7 @@ export default function BinVisualizer(props: Props) {
toggle toggle
/> />
</Box> </Box>
</Box>
); );
}; };

View file

@ -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 * 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 disableAll?: boolean
} }
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {

View file

@ -226,7 +226,6 @@ export default function DeviceWizard(props: Props) {
*/ */
const adjustAvailablePositions = (port: PortInformation, availability: DeviceAvailabilityMap) => { const adjustAvailablePositions = (port: PortInformation, availability: DeviceAvailabilityMap) => {
let currentAvailability = availability; let currentAvailability = availability;
console.log(port)
switch (port.addressType) { switch (port.addressType) {
case quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY: case quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY:
currentAvailability.set(port.addressType, [{ address: port.address, label: port.label }]); currentAvailability.set(port.addressType, [{ address: port.address, label: port.label }]);

View file

@ -221,6 +221,7 @@ export default function DeviceScannedComponents(props: Props){
deviceAPI.removeAllFoundComponents(device.settings.deviceId) deviceAPI.removeAllFoundComponents(device.settings.deviceId)
.then(resp => { .then(resp => {
console.log("Cleared all scans") console.log("Cleared all scans")
refreshCallback()
}).catch(err => { }).catch(err => {
console.log("There was a problem clearing scans") console.log("There was a problem clearing scans")
}) })
@ -233,7 +234,10 @@ export default function DeviceScannedComponents(props: Props){
<DialogContent>This action will clear all scans for this device.</DialogContent> <DialogContent>This action will clear all scans for this device.</DialogContent>
<DialogActions> <DialogActions>
<Button variant="contained" onClick={()=>{setClearOpen(false)}}>Close</Button> <Button variant="contained" onClick={()=>{setClearOpen(false)}}>Close</Button>
<Button variant="contained" color="primary" onClick={()=>{removeAllScans()}}>Continue</Button> <Button variant="contained" color="primary" onClick={()=>{
removeAllScans()
setClearOpen(false)
}}>Continue</Button>
</DialogActions> </DialogActions>
</ResponsiveDialog> </ResponsiveDialog>
) )

View file

@ -3125,6 +3125,14 @@
"rpm": 1750, "rpm": 1750,
"diameter": 0, "diameter": 0,
"horsepower": 30 "horsepower": 30
},
{
"id": 445,
"name": "CALDWELL GGL-81011",
"kind": "centrifugal(low speed)",
"rpm": 0,
"diameter": 0,
"horsepower": 10
} }
] ]
} }

View file

@ -181,6 +181,59 @@ export class Device {
return max 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 { public featureSupported(feature: string): boolean {
let versions = featureVersions.get(feature); let versions = featureVersions.get(feature);
if (!versions) { if (!versions) {
@ -188,27 +241,27 @@ export class Device {
} }
switch (this.settings.platform) { switch (this.settings.platform) {
case pond.DevicePlatform.DEVICE_PLATFORM_PHOTON: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: case pond.DevicePlatform.DEVICE_PLATFORM_V2_ETHERNET_BLUE:
return this.status.firmwareVersion >= versions.v2EthBlue; return this.versionComparison(this.status.firmwareVersion, versions.v2EthBlue);
default: default:
return false; return false;
} }

View file

@ -1,6 +1,7 @@
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { or } from "utils/types"; import { or } from "utils/types";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { Result } from "pbHelpers/Enums";
export class Interaction { export class Interaction {
public settings: pond.InteractionSettings = pond.InteractionSettings.create(); public settings: pond.InteractionSettings = pond.InteractionSettings.create();
@ -53,6 +54,18 @@ export class Interaction {
return subtype; 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 { public static upToSubtype(nodeNumber: number): number {
let subtype = 0; let subtype = 0;
switch (nodeNumber) { switch (nodeNumber) {

View file

@ -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 (
<Box>
<Typography>{type + " " + comparison + " " + value + describer.GetUnit()}</Typography>
</Box>
);
};
const alertAccordion = (alert: Alert) => {
return (
<Accordion style={{ width: "100%" }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Grid2 container direction="column">
{alert.conditions.map((condition, i) => (
<Grid2 key={i}>
{readableCondition(condition)}
</Grid2>
))}
</Grid2>
</AccordionSummary>
<AccordionDetails>
<List>
<ListSubheader>Reporting Components</ListSubheader>
{alert.sourceComponents.map((comp, i) => (
<ListItem key={i}>{comp.name()}</ListItem>
))}
</List>
</AccordionDetails>
</Accordion>
);
};
return (
<React.Fragment>
<List style={{ margin: 5 }}>
<ListSubheader sx={{paddingY: 1, display: "flex", justifyContent: "space-between", alignItems: "center"}}>
<Typography>
Alerts
</Typography>
{addNew &&
<Button
disabled={!permissions.includes(pond.Permission.PERMISSION_WRITE)}
onClick={()=>{addNew()}}
variant="contained"
color="primary">
<Add /> Alert
</Button>
}
</ListSubheader>
{alerts.map((alert, i) => (
<ListItem key={i}>{alertAccordion(alert)}</ListItem>
))}
</List>
</React.Fragment>
);
}

View file

@ -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<Map<number, Control[]>>(new Map())
const [openDeviceMenu, setOpenDeviceMenu] = useState(false)
const [anchor, setAnchor] = useState<null | HTMLElement>(null)
useEffect(()=>{
let map: Map<number, Control[]> = 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 (
<Accordion sx={{width: "100%"}}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Box>
<Typography>
{resultText}:
</Typography>
<Typography>
{readableConditions(control)}
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<List>
<ListSubheader>Controlling Components</ListSubheader>
{control.sourceComponents.map((comp, i) => (
<ListItem key={i}>{comp.name()}</ListItem>
))}
</List>
</AccordionDetails>
</Accordion>
)
}
const deviceMenu = () => {
return (
<Menu
open={openDeviceMenu}
anchorEl={anchor}
onClose={() => {
setAnchor(null)
setOpenDeviceMenu(false)
}}>
{devices.map(dev => (
<MenuItem
key={dev.id()}
onClick={() => {
setAnchor(null)
setOpenDeviceMenu(false)
if (addNew) addNew(dev)
}}>
{dev.name()}
</MenuItem>
))}
</Menu>
)
}
return (
<React.Fragment>
{deviceMenu()}
<List style={{ margin: 5 }}>
<ListSubheader sx={{paddingY: 1, display: "flex", justifyContent: "space-between", alignItems: "center"}}>
<Typography>
Controls
</Typography>
{addNew &&
<Button
disabled={!permissions.includes(pond.Permission.PERMISSION_WRITE)}
variant="contained"
onClick={(e)=>{
setOpenDeviceMenu(true)
setAnchor(e.currentTarget)
}}
color="primary">
<Add />Control
</Button>
}
</ListSubheader>
{devices.map(device => (
<React.Fragment key={device.id()}>
<Typography sx={{paddingY: 1, paddingX: 2}}>{device.name()}</Typography>
<Divider />
{deviceControls.get(device.id()) ? deviceControls.get(device.id())?.map((control, i) => (
<ListItem key={i}>{controlAccordion(control)}</ListItem>
))
:
<Typography sx={{paddingY: 1, paddingX: 2}}>No Control Interactions For Device</Typography>
}
</React.Fragment>
))}
</List>
</React.Fragment>
)
}

View file

@ -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<pond.Notification[]>([]);
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 (
<List style={{ margin: 5 }}>
<ListSubheader>
<Typography sx={{paddingY: 2}}>
Notifications
</Typography>
</ListSubheader>
{recentNotifications && (
<React.Fragment>
{recentNotifications.map((notification, i) => (
<ListItem key={i} className={i % 2 === 0 ? classes.dark : classes.light}>
<Typography style={{ fontWeight: 650 }}>
{moment(notification.status?.timestamp).format("MMMM DD, YYYY - h:mm a")}
</Typography>{" "}
:{" "}
<Typography>
{notification.settings?.title + " - " + notification.settings?.subtitle}
</Typography>
</ListItem>
))}
{recentNotifications.length < totalNotifications && (
<ListItem key={"button"}>
<Button onClick={loadMoreNotifications} disabled={notificationsLoading}>
Get More
</Button>
</ListItem>
)}
</React.Fragment>
)}
</List>
);
}

View file

@ -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<string, Component>; //the component key to the component object
componentDevices: Map<string, number>;
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<Alert[]>([]);
//list of interactions relating to a controller
const [controls, setControls] = useState<Control[]>([]);
const interactionsAPI = useInteractionsAPI();
const [openNewInteraction, setOpenNewInteraction] = useState(false)
const [allSinks, setAllSinks] = useState<Component[]>([]);
// const [typeOptions, setTypeOptions] = useState<Option[]>([]);
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>(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<string, string>();
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 (
<React.Fragment>
<NewObjectInteraction
open={openNewInteraction}
onClose={(refresh) => {
setOpenNewInteraction(false)
if(refresh){
load()
}
}}
linkedComponents={linkedComponents}
componentDevices={componentDevices}
device={selectedDevice}/>
<Card raised style={{ margin: 5 }}>
<Box padding={1}>
<Typography style={{ fontWeight: 650, fontSize: 25 }}>Interactions</Typography>
<Controls
controls={controls}
permissions={permissions}
devices={devices}
addNew={(device) => {
setSelectedDevice(device)
setOpenNewInteraction(true)
}}/>
<Alerts
alerts={alerts}
permissions={permissions}
addNew={() => {
setSelectedDevice(undefined)
setOpenNewInteraction(true)
}}/>
<Notifications objectKey={objectKey} objectType={objectType}/>
</Box>
</Card>
</React.Fragment>
)
}

View file

@ -50,11 +50,9 @@ import { GrainCable } from "models/GrainCable";
// import { Controller } from "models/Controller"; // import { Controller } from "models/Controller";
import { Pressure } from "models/Pressure"; import { Pressure } from "models/Pressure";
import { CheckCircle, ExpandMore, Warning } from "@mui/icons-material"; import { CheckCircle, ExpandMore, Warning } from "@mui/icons-material";
import { getThemeType } from "theme";
import BinGraphs from "bin/graphs/BinGraphs"; import BinGraphs from "bin/graphs/BinGraphs";
import BinTour from "bin/BinTour"; import BinTour from "bin/BinTour";
import { getBinModel } from "common/DataImports/BinCables/BinCableImporter"; import { getBinModel } from "common/DataImports/BinCables/BinCableImporter";
import ObjectAlerts from "objects/ObjectAlerts";
import DevicePresetController from "device/presets/devicePresetController"; import DevicePresetController from "device/presets/devicePresetController";
import { DevicePreset } from "models/DevicePreset"; import { DevicePreset } from "models/DevicePreset";
import BinVisualizerV2 from "bin/BinVisualizerV2"; import BinVisualizerV2 from "bin/BinVisualizerV2";
@ -69,6 +67,7 @@ import TaskViewer from "tasks/TaskViewer";
import ButtonGroup from "common/ButtonGroup"; import ButtonGroup from "common/ButtonGroup";
import BinTransactions from "bin/BinTransactions"; import BinTransactions from "bin/BinTransactions";
import { CO2 } from "models/CO2"; import { CO2 } from "models/CO2";
import ObjectInteractions from "objects/objectInteractions/ObjectInteractions";
interface TabPanelProps { interface TabPanelProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -196,6 +195,7 @@ export default function Bin(props: Props) {
const [headspaceCO2, setHeadspaceCO2] = useState<CO2[]>([]); const [headspaceCO2, setHeadspaceCO2] = useState<CO2[]>([]);
const [heaters, setHeaters] = useState<Controller[]>([]); const [heaters, setHeaters] = useState<Controller[]>([]);
const [fans, setFans] = useState<Controller[]>([]); const [fans, setFans] = useState<Controller[]>([]);
const [activeDetails, setActiveDetails] = useState([0])
const [detail, setDetail] = useState< const [detail, setDetail] = useState<
"inventory" | "sensors" | "analytics" | "presets" | "alerts" | "transactions" "inventory" | "sensors" | "analytics" | "presets" | "alerts" | "transactions"
>("inventory"); >("inventory");
@ -806,7 +806,7 @@ export default function Bin(props: Props) {
{overview()} {overview()}
{preferences && binComponents(preferences)} {preferences && binComponents(preferences)}
</Grid> </Grid>
<Grid size={{ xs: 12, sm: 12, md: 12, lg: 2.6 }}> <Grid id="tour-conditions" size={{ xs: 12, sm: 12, md: 12, lg: 2.6 }}>
{(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING || {(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING ||
bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING || bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING ||
bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) && ( bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) && (
@ -826,82 +826,66 @@ export default function Bin(props: Props) {
<BinStorageConditions bin={bin} headspaceCO2={headspaceCO2} cables={grainCables} /> <BinStorageConditions bin={bin} headspaceCO2={headspaceCO2} cables={grainCables} />
</Box> </Box>
</Grid> </Grid>
<Grid id="tour-graphs" size={{ xs: 12, sm: 12, md: 12, lg: 5.3 }}> <Grid id="tour-details" size={{ xs: 12, sm: 12, md: 12, lg: 5.3 }}>
<Box paddingBottom={2} display="flex"> <Box id="tour-graph-tabs" paddingBottom={2} display="flex">
<ButtonGroup <ButtonGroup
toggle toggle
toggledButtons={activeDetails}
buttons={[ buttons={[
{ {
title: "Inventory", title: "Inventory",
function: () => setDetail("inventory") function: () => {
setDetail("inventory")
setActiveDetails([0])
}
}, },
{ {
title: "Sensors", title: "Sensors",
function: () => setDetail("sensors") function: () => {
setDetail("sensors")
setActiveDetails([1])
}
}, },
{ {
title: "Analysis", title: "Analysis",
function: () => setDetail("analytics") function: () => {
setDetail("analytics")
setActiveDetails([2])
}
}, },
{ {
title: "Alerts", title: "Alerts",
function: () => setDetail("alerts") function: () => {
setDetail("alerts")
setActiveDetails([3])
}
}, },
{ {
title: "Presets", title: "Presets",
function: () => setDetail("presets") function: () => {
setDetail("presets")
setActiveDetails([4])
}
}, },
{ {
title: "Transactions", title: "Transactions",
function: () => setDetail("transactions") function: () => {
setDetail("transactions")
setActiveDetails([5])
}
} }
]} ]}
/> />
{/* <StyledToggleButtonGroup
id="tour-graph-tabs"
value={detail}
exclusive
size="small"
aria-label="detail">
<StyledToggle
value={"inventory"}
aria-label="Inventory Details"
onClick={() => setDetail("inventory")}>
Inventory
</StyledToggle>
<StyledToggle
onClick={() => setDetail("sensors")}
value={"sensors"}
aria-label="Bin Sensor Graphs">
Sensors
</StyledToggle>
<StyledToggle
onClick={() => setDetail("analytics")}
value={"analytics"}
aria-label="Bin Analysis Graphs">
Analysis
</StyledToggle>
<StyledToggle
onClick={() => setDetail("alerts")}
value={"alerts"}
aria-label="Device Alerts">
Alerts
</StyledToggle>
<StyledToggle
onClick={() => setDetail("presets")}
value={"presets"}
aria-label="Bin Mode Presets">
Presets
</StyledToggle>
</StyledToggleButtonGroup> */}
</Box> </Box>
{detail === "alerts" && ( {detail === "alerts" && (
<Box> <Box>
<ObjectAlerts <ObjectInteractions
linkedComponents={components} linkedComponents={components}
componentDevices={componentDevices} componentDevices={componentDevices}
objectType={pond.ObjectType.OBJECT_TYPE_BIN} devices={devices}
objectKey={bin.key()} objectKey={bin.key()}
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
permissions={permissions}
/> />
</Box> </Box>
)} )}
@ -917,7 +901,13 @@ export default function Bin(props: Props) {
/> />
</Box> </Box>
)} )}
{detail === "transactions" &&
<Box>
<BinTransactions bin={bin} permissions={permissions} refresh={refresh}/>
</Box>
}
{(detail === "inventory" || detail === "sensors" || detail === "analytics") && ( {(detail === "inventory" || detail === "sensors" || detail === "analytics") && (
<Box>
<BinGraphs <BinGraphs
bin={bin} bin={bin}
binLoading={binLoading} binLoading={binLoading}
@ -930,10 +920,8 @@ export default function Bin(props: Props) {
compMap={components} compMap={components}
compositionNameMap={compositionNameMap} compositionNameMap={compositionNameMap}
/> />
</Box>
)} )}
{detail === "transactions" &&
<BinTransactions bin={bin} permissions={permissions} refresh={refresh}/>
}
</Grid> </Grid>
</Grid> </Grid>
</Card> </Card>
@ -1035,11 +1023,13 @@ export default function Bin(props: Props) {
</TabPanelMine> </TabPanelMine>
<TabPanelMine value={mobileTab} index={4}> <TabPanelMine value={mobileTab} index={4}>
<Box> <Box>
<ObjectAlerts <ObjectInteractions
linkedComponents={components} linkedComponents={components}
componentDevices={componentDevices} componentDevices={componentDevices}
objectType={pond.ObjectType.OBJECT_TYPE_BIN} devices={devices}
objectKey={bin.key()} objectKey={bin.key()}
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
permissions={permissions}
/> />
</Box> </Box>
</TabPanelMine> </TabPanelMine>
@ -1177,7 +1167,11 @@ export default function Bin(props: Props) {
</Box> </Box>
{objectTeams()} {objectTeams()}
{deviceMenu()} {deviceMenu()}
<BinTour /> <BinTour
setDetailTabState={(detail, buttonIndex) => {
setDetail(detail)
setActiveDetails([buttonIndex])
}}/>
</PageContainer> </PageContainer>
); );
} }

View file

@ -169,7 +169,6 @@ export default function DevicePage() {
const loadPortScan = () => { const loadPortScan = () => {
deviceAPI.listFoundComponents(parseInt(deviceID)) deviceAPI.listFoundComponents(parseInt(deviceID))
.then(resp => { .then(resp => {
console.log(resp.data)
if (resp.data.foundComponents?.i2c || resp.data.foundComponents?.oneWire){ if (resp.data.foundComponents?.i2c || resp.data.foundComponents?.oneWire){
setScannedAddresses(pond.ListFoundComponentsResponse.fromObject(resp.data).foundComponents ?? undefined) setScannedAddresses(pond.ListFoundComponentsResponse.fromObject(resp.data).foundComponents ?? undefined)
} }

View file

@ -1,13 +1,15 @@
import { useDeviceAPI, useMobile, useUserAPI } from "hooks"; import { useDeviceAPI, useMobile, useUserAPI } from "hooks";
import PageContainer from "./PageContainer"; import PageContainer from "./PageContainer";
import { useEffect, useState } from "react"; 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 { useBinAPI, useGlobalState, useTeamAPI } from "providers";
import { CheckCircleOutline } from "@mui/icons-material"; import { CheckCircleOutline } from "@mui/icons-material";
import { green } from "@mui/material/colors"; import { green } from "@mui/material/colors";
import Tour, { TourStep } from "common/Tour"; import Tour, { TourStep } from "common/Tour";
import Emoji from "react-emoji-render"; import Emoji from "react-emoji-render";
import { getWhitelabel, IsAdaptiveAgriculture } from "services/whiteLabel"; import { getWhitelabel, IsAdaptiveAgriculture } from "services/whiteLabel";
import { pond } from "protobuf-ts/pond";
import { User } from "models";
// import { color } from "framer-motion"; // import { color } from "framer-motion";
// interface Props { // interface Props {
@ -21,7 +23,7 @@ export default function SignupCallback () {
const deviceAPI = useDeviceAPI(); const deviceAPI = useDeviceAPI();
const binAPI = useBinAPI(); const binAPI = useBinAPI();
const isMobile = useMobile() const isMobile = useMobile()
const [{ user }] = useGlobalState(); const [globalState, dispatch] = useGlobalState();
const whiteLabel = getWhitelabel() const whiteLabel = getWhitelabel()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@ -35,6 +37,7 @@ export default function SignupCallback () {
const [doneBins, setDoneBins] = useState(false) const [doneBins, setDoneBins] = useState(false)
const [doneTeams, setDoneTeams] = useState(false) const [doneTeams, setDoneTeams] = useState(false)
const [doneDevices, setDoneDevices] = useState(false) const [doneDevices, setDoneDevices] = useState(false)
const [user, setUser] = useState<User>(globalState.user);
useEffect(() => { useEffect(() => {
setLoading(true) setLoading(true)
@ -55,6 +58,124 @@ export default function SignupCallback () {
return loading === false && doneBins && doneTeams && doneDevices 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 (
<Grid2 container>
<Grid2 size={{ xs: 12 }}>
<TextField
select
fullWidth
id="pressureUnit"
name="pressureUnit"
label="Pressure Unit"
value={pressureUnit ? pressureUnit : pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER}
onChange={event => changePressureUnit(event.target.value)}
margin="normal"
variant="outlined"
InputLabelProps={{ shrink: true }}>
<MenuItem value={pond.PressureUnit.PRESSURE_UNIT_KILOPASCALS}>
Kilopascals (kPa)
</MenuItem>
<MenuItem value={pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER}>
Inches of Water (iwg)
</MenuItem>
</TextField>
</Grid2>
<Grid2 size={{ xs: 12 }}>
<TextField
select
fullWidth
id="temperatureUnit"
name="temperatureUnit"
label="Temperature Unit"
value={
temperatureUnit ? temperatureUnit : pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS
}
onChange={event => changeTemperatureUnit(event.target.value)}
margin="normal"
variant="outlined"
InputLabelProps={{ shrink: true }}>
<MenuItem value={pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS}>Celsius (°C)</MenuItem>
<MenuItem value={pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT}>
Fahrenheit (°F)
</MenuItem>
</TextField>
</Grid2>
<Grid2 size={{ xs: 12 }}>
<TextField
select
fullWidth
id="distanceUnit"
name="distanceUnit"
label="Distance Unit"
value={distanceUnit ? distanceUnit : pond.DistanceUnit.DISTANCE_UNIT_METERS}
// onChange={event => changeDistanceUnit(event.target.value)}
margin="normal"
variant="outlined"
InputLabelProps={{ shrink: true }}>
<MenuItem value={pond.DistanceUnit.DISTANCE_UNIT_METERS}>Meters (m)</MenuItem>
<MenuItem value={pond.DistanceUnit.DISTANCE_UNIT_FEET}>Feet (ft)</MenuItem>
</TextField>
</Grid2>
{IsAdaptiveAgriculture() && (
<Grid2 size={{ xs: 12 }}>
<TextField
select
fullWidth
id="grainUnit"
name="grainUnit"
label="Grain Unit"
value={grainUnit ? grainUnit : pond.GrainUnit.GRAIN_UNIT_BUSHELS}
// onChange={event => changeGrainUnit(event.target.value)}
margin="normal"
variant="outlined"
InputLabelProps={{ shrink: true }}>
<MenuItem value={pond.GrainUnit.GRAIN_UNIT_BUSHELS}>Bushels (bu)</MenuItem>
<MenuItem value={pond.GrainUnit.GRAIN_UNIT_WEIGHT}>Tonnes (mT)</MenuItem>
</TextField>
</Grid2>
)}
</Grid2>
);
};
const content = () => { const content = () => {
return ( return (
<DialogContent sx={{ margin: "auto", textAlign: "center" }}> <DialogContent sx={{ margin: "auto", textAlign: "center" }}>
@ -104,12 +225,17 @@ export default function SignupCallback () {
content: ( content: (
<> <>
<Typography variant="body2" > <Typography variant="body2" >
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.
</Typography> </Typography>
<br/> <br/>
<Typography variant="body2" > <Typography variant="body2" >
If you are part of a team, you can select it here as well. If you are part of a team, you can select it here as well.
</Typography> </Typography>
<br />
<Typography variant="body2" >
Set your unit preferences here now, they can be changed later from this menu:
</Typography>
{unitPreferences()}
</> </>
), ),
target: "#tour-user-menu", target: "#tour-user-menu",
@ -150,7 +276,59 @@ export default function SignupCallback () {
target: "#tour-dashboard", target: "#tour-dashboard",
placement: "right" placement: "right"
}) })
if (IsAdaptiveAgriculture()) steps.push({ steps.push({
title: "Tasks",
content: (
<>
<Typography variant="body2" >
You can view and create tasks here and assign a start and end time for them.
</Typography>
<br/>
<Typography>
If you are viewing as a team you will see the teams tasks, you can even assign tasks to team members.
</Typography>
</>
),
target: "#tour-tasks",
placement: "right"
})
//tasks step
if (IsAdaptiveAgriculture()) {
steps.push({
title: "Visual Farm",
content: (
<>
<Typography variant="body2" >
You can view your Visual Farm here, it will allow tou to draw fields and plot bins an a map.
</Typography>
<br/>
<Typography>
If you are viewing as a team, your team's fields and bins will be displayed here instead.
</Typography>
</>
),
target: "#tour-visual-farm",
placement: "right",
disableBeacon: true,
})
steps.push({
title: "Fields",
content: (
<>
<Typography variant="body2" >
You can view your fields you have drawn on the map here.
</Typography>
<br/>
<Typography>
If you are viewing as a team, your team's fields will be displayed here instead.
</Typography>
</>
),
target: "#tour-field-list",
placement: "right",
disableBeacon: true,
})
steps.push({
title: "Bins", title: "Bins",
content: ( content: (
<> <>
@ -167,18 +345,35 @@ export default function SignupCallback () {
placement: "right", placement: "right",
disableBeacon: true, disableBeacon: true,
}) })
steps.push({
title: "Contracts",
content: (
<>
<Typography variant="body2" >
You can view and track any contracts you have created here.
</Typography>
<br/>
<Typography>
If you are viewing as a team, your team's contracts will be displayed here instead.
</Typography>
</>
),
target: "#tour-contracts",
placement: "right",
disableBeacon: true,
})
}
return steps; return steps;
}; };
const endTour = () => { const endTour = () => {
setIsTourRunning(false); setIsTourRunning(false);
// if (user) { if (user) {
// user.status.finishedBinIntro = moment().toJSON(); userAPI
// userAPI .updateUser(user.id(), user.protobuf())
// .updateUser(userID, user.protobuf()) .then(() => dispatch({ key: "user", value: user }))
// .then(() => dispatch({ key: "user", value: user })) .catch((err: any) => console.error(err));
// .catch((err: any) => error(err)); }
// }
}; };
const closeDialog = () => { const closeDialog = () => {

View file

@ -18,7 +18,8 @@ export const I2C: AddressTypeExtension = {
[quack.ComponentType.COMPONENT_TYPE_SEN5X, 0x68], [quack.ComponentType.COMPONENT_TYPE_SEN5X, 0x68],
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, 0x6b], [quack.ComponentType.COMPONENT_TYPE_AIRFLOW, 0x6b],
[quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, 0x70], [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); const offset = offsets.get(componentType);

View file

@ -60,6 +60,7 @@ import { LineData, Point } from "charts/measurementCharts/MultiLineGraph";
import moment, { Moment } from "moment"; import moment, { Moment } from "moment";
import { avg } from "utils"; import { avg } from "utils";
import { Airflow } from "./ComponentTypes/Airflow"; import { Airflow } from "./ComponentTypes/Airflow";
import { TempHumidity } from "./ComponentTypes/TempHumidity";
const COMPONENT_TYPE_MAP = new Map<quack.ComponentType, Function>([ const COMPONENT_TYPE_MAP = new Map<quack.ComponentType, Function>([
[quack.ComponentType.COMPONENT_TYPE_INVALID, Invalid], [quack.ComponentType.COMPONENT_TYPE_INVALID, Invalid],
@ -96,7 +97,8 @@ const COMPONENT_TYPE_MAP = new Map<quack.ComponentType, Function>([
[quack.ComponentType.COMPONENT_TYPE_SEN5X, Sen5x], [quack.ComponentType.COMPONENT_TYPE_SEN5X, Sen5x],
[quack.ComponentType.COMPONENT_TYPE_VIBRATION_CHAIN, VibrationCable], [quack.ComponentType.COMPONENT_TYPE_VIBRATION_CHAIN, VibrationCable],
[quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, dragerGasDongle], [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 { export interface Subtype {

View file

@ -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<Array<Summary>> {
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;
}
};
}

View file

@ -64,7 +64,8 @@ const DefaultAvailability: DeviceAvailabilityMap = new Map<quack.AddressType, De
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]], [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]],
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, [0x6c]], [quack.ComponentType.COMPONENT_TYPE_AIRFLOW, [0x6c]],
[quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, [0x71]], // the address cables will use when they are plugged into the expander with V2 device only [quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, [0x71]], // the address cables will use when they are plugged into the expander with V2 device only
[quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, [0x6d]] [quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, [0x6d]],
[quack.ComponentType.COMPONENT_TYPE_TEMP_HUMIDITY, [0x44]]
]) ])
], ],
[quack.AddressType.ADDRESS_TYPE_DAC, [0, 1]], [quack.AddressType.ADDRESS_TYPE_DAC, [0, 1]],

View file

@ -252,7 +252,9 @@ export const BindaptV2MonitorAvailability: DeviceAvailabilityMap = new Map<
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62, 0x66, 0x10]], [quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62, 0x66, 0x10]],
[quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]], [quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]], [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]],
[quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, [0x71]] [quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, [0x71]],
[quack.ComponentType.COMPONENT_TYPE_TEMP_HUMIDITY, [0x44]]
]) ])
], ],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]], [quack.AddressType.ADDRESS_TYPE_POWER, [0]],
@ -285,7 +287,8 @@ export const BindaptV2AutomateAvailability: DeviceAvailabilityMap = new Map<
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62, 0x66, 0x10]], [quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62, 0x66, 0x10]],
[quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]], [quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]], [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]],
[quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, [0x71]] [quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, [0x71]],
[quack.ComponentType.COMPONENT_TYPE_TEMP_HUMIDITY, [0x44]]
]) ])
], ],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]], [quack.AddressType.ADDRESS_TYPE_POWER, [0]],