Merge branch 'hdc302_component' into dev_environment
This commit is contained in:
commit
1ad0f26af8
22 changed files with 1770 additions and 638 deletions
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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 }]);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
89
src/objects/objectInteractions/Alerts.tsx
Normal file
89
src/objects/objectInteractions/Alerts.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
152
src/objects/objectInteractions/Controls.tsx
Normal file
152
src/objects/objectInteractions/Controls.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
111
src/objects/objectInteractions/Notifications.tsx
Normal file
111
src/objects/objectInteractions/Notifications.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
229
src/objects/objectInteractions/ObjectInteractions.tsx
Normal file
229
src/objects/objectInteractions/ObjectInteractions.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = () => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
85
src/pbHelpers/ComponentTypes/TempHumidity.ts
Normal file
85
src/pbHelpers/ComponentTypes/TempHumidity.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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]],
|
||||||
|
|
|
||||||
|
|
@ -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]],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue