diff --git a/README.md b/README.md
index 74872fd..b7a7555 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,43 @@ Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+### White-labeling
+
+Steps to add a new white label
+
+1. Client Instructions:
+
+- supply assets and colours to be used for the white-label (favicon if necessary)
+- determine a sub-domain to use as mask, dashboard is preferred (ex: dashboard.example.com)
+- add a `CNAME` record pointing their custom subdomain (ex: dashboard.example.com) to brandxtech.ca
+- add a login button on their website point to `/login` of their custom subdomain (ex: dashboard.example.com/login)
+
+
+2. Create a PWA folder in `public` that is named after the client and generate PWA assets using [this](https://realfavicongenerator.net/) website (use the company logo but you might need to use a custom favicon depending on the result)
+
+- https://maskable.app/ also create a maskable icon for PWA
+- create a 512x512 png as well for PWA
+
+3. Create a white-label branding folder in `src/assets/whitelabels` and create a `darkLogo.png` and `lightLogo.png` from the client's logo
+4. In `src/services/whiteLabel.ts`, add a new `Whitelabel` instance with the required fields and add its hostname mapping to the `whitelabels` map (it is very similar to step one but had some constraints when dealing with the `public` folder)
+5. If there are any pages specific to the whitelabel be sure to update the side and bottom navigator
+
+
+6. In `dynamic-config.prod.yml`, add a router entry for the new whitelabel
+7. In `pond/http/server.go`, add the sub-domain to the `allowedOrigins`
+8. In `pond/whitelabel.go`, add an entry for the new white label
+
+7. Create a new `Auth0` application for the client
+ - use a image bucket like [postimg](https://postimages.org/) to store their company logo and favicon
+ - add a whitelabel logo
+ - allow necessary `origins`, `callback URLs`, and `CORS domains` to the application settings (see the default application as a guideline)
+ - customize the Universal Login page to handle the new whitelabel
+ - check if the client ID matches this client's Auth0 application
+ - change the Universal Login theme using white-label colours and images stored in a bucket (ex: [postimg](https://postimages.org/))
+8. Enable the white-label sub-domain for CORS in the backend
+
+- in [http.go](https://gitlab.com/brandx/backend/blob/master/pond/http.go), add the white-label sub-domain to the `AllowedOrigins` list
+
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
diff --git a/package-lock.json b/package-lock.json
index a46ad55..795220e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -43,7 +43,7 @@
"mui-tel-input": "^7.0.0",
"notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10",
- "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master",
+ "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
"query-string": "^9.2.1",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
@@ -8306,10 +8306,12 @@
}
},
"node_modules/date-fns-jalali": {
- "version": "4.1.0-0",
- "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
- "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
- "license": "MIT"
+ "version": "4.0.0-0",
+ "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.0.0-0.tgz",
+ "integrity": "sha512-EczB+gWceuWCRlacE4T+WmdP+BV/IUQpjQW9aBa9DNcXkKuZFv3WBDqeP2Ew+6YFBtPRRcH5U22+C6gcpwgG8A==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
"node_modules/dayjs": {
"version": "1.11.19",
@@ -11965,7 +11967,7 @@
},
"node_modules/protobuf-ts": {
"version": "1.0.0",
- "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#bcf085706791036841f7047a5b37f00c18e54574",
+ "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#c9ef7906fd97cda8ef4bd149ec4a796159a7c067",
"dependencies": {
"protobufjs": "^6.8.8"
}
@@ -12174,6 +12176,12 @@
"react": ">=16.8.0"
}
},
+ "node_modules/react-day-picker/node_modules/date-fns-jalali": {
+ "version": "4.1.0-0",
+ "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
+ "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
+ "license": "MIT"
+ },
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
diff --git a/package.json b/package.json
index 7f5044c..90ac71b 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,7 @@
"mui-tel-input": "^7.0.0",
"notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10",
- "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master",
+ "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
"query-string": "^9.2.1",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
diff --git a/public/MiPCA/android-chrome-192x192.png b/public/MiPCA/android-chrome-192x192.png
new file mode 100644
index 0000000..4ed494c
Binary files /dev/null and b/public/MiPCA/android-chrome-192x192.png differ
diff --git a/public/MiPCA/android-chrome-512x512.png b/public/MiPCA/android-chrome-512x512.png
new file mode 100644
index 0000000..72ba797
Binary files /dev/null and b/public/MiPCA/android-chrome-512x512.png differ
diff --git a/public/MiPCA/apple-touch-icon.png b/public/MiPCA/apple-touch-icon.png
new file mode 100644
index 0000000..c5d4d8b
Binary files /dev/null and b/public/MiPCA/apple-touch-icon.png differ
diff --git a/public/MiPCA/browserconfig.xml b/public/MiPCA/browserconfig.xml
new file mode 100644
index 0000000..b3930d0
--- /dev/null
+++ b/public/MiPCA/browserconfig.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+ #da532c
+
+
+
diff --git a/public/MiPCA/favicon-16x16.png b/public/MiPCA/favicon-16x16.png
new file mode 100644
index 0000000..f54fc91
Binary files /dev/null and b/public/MiPCA/favicon-16x16.png differ
diff --git a/public/MiPCA/favicon-32x32.png b/public/MiPCA/favicon-32x32.png
new file mode 100644
index 0000000..48b5897
Binary files /dev/null and b/public/MiPCA/favicon-32x32.png differ
diff --git a/public/MiPCA/favicon.ico b/public/MiPCA/favicon.ico
new file mode 100644
index 0000000..48e87f6
Binary files /dev/null and b/public/MiPCA/favicon.ico differ
diff --git a/public/MiPCA/manifest.json b/public/MiPCA/manifest.json
new file mode 100644
index 0000000..d98df92
--- /dev/null
+++ b/public/MiPCA/manifest.json
@@ -0,0 +1,33 @@
+{
+ "name": "MiPCA",
+ "short_name": "MiPCA",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "48x48",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ },
+ {
+ "src": "maskable_icon.png",
+ "sizes": "196x196",
+ "type": "image/png",
+ "purpose": "any maskable"
+ }
+ ],
+ "start_url": "/",
+ "theme_color": "#272727",
+ "background_color": "#303030",
+ "display": "standalone",
+ "orientation": "any"
+ }
+
diff --git a/public/MiPCA/mstile-150x150.png b/public/MiPCA/mstile-150x150.png
new file mode 100644
index 0000000..d5b8d98
Binary files /dev/null and b/public/MiPCA/mstile-150x150.png differ
diff --git a/public/MiPCA/safari-pinned-tab.svg b/public/MiPCA/safari-pinned-tab.svg
new file mode 100644
index 0000000..2758652
--- /dev/null
+++ b/public/MiPCA/safari-pinned-tab.svg
@@ -0,0 +1,50 @@
+
+
+
+
+Created by potrace 1.14, written by Peter Selinger 2001-2017
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/whitelabels/MiPCA/MiPCALogo.png b/src/assets/whitelabels/MiPCA/MiPCALogo.png
new file mode 100644
index 0000000..c828035
Binary files /dev/null and b/src/assets/whitelabels/MiPCA/MiPCALogo.png differ
diff --git a/src/bin/BinCardV2.tsx b/src/bin/BinCardV2.tsx
index 776af57..f39c553 100644
--- a/src/bin/BinCardV2.tsx
+++ b/src/bin/BinCardV2.tsx
@@ -200,25 +200,27 @@ export default function BinCard(props: Props) {
const typeDisplay = () => {
let grainType = bin.settings.inventory?.grainType ?? pond.Grain.GRAIN_NONE;
- if (bin.storage() === pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN) {
- return (
-
-
- {GrainDescriber(grainType).name}
-
-
- {bin.settings.inventory?.grainSubtype}
-
-
+ switch (bin.storage()){
+ case pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN:
+ return (
+
+
+ {GrainDescriber(grainType).name}
+
+
+ {bin.settings.inventory?.grainSubtype}
+
+
+ )
+ default:
+ return (
+
+
+ {bin.grainName()}
+
+
);
}
- return (
-
-
- {bin.settings.inventory?.customTypeName}
-
-
- );
};
const tempDisplay = () => {
@@ -425,6 +427,7 @@ export default function BinCard(props: Props) {
hottestNodeTemp={valDisplay === "high" ? hotNode?.temp : undefined}
coldestNodeTemp={valDisplay === "low" ? coldNode?.temp : undefined}
inventoryControl={bin.inventoryControl()}
+ customGrain={bin.customGrain()}
/>
diff --git a/src/bin/BinConditioningCard.tsx b/src/bin/BinConditioningCard.tsx
index cf3b3aa..8caac52 100644
--- a/src/bin/BinConditioningCard.tsx
+++ b/src/bin/BinConditioningCard.tsx
@@ -39,11 +39,12 @@ interface Props {
heaters?: Controller[];
fans?: Controller[];
grain?: pond.Grain;
+ customGrain?: pond.GrainSettings
}
export default function BinConditioningCard(props: Props) {
const classes = useStyles();
- const { interactions, interactionDevices, plenums, ambients, heaters, fans, grain, mode } = props;
+ const { interactions, interactionDevices, plenums, ambients, heaters, fans, grain, mode, customGrain } = props;
const [sourceMap, setSourceMap] = useState>(new Map());
const [sinkMap, setSinkMap] = useState>(new Map());
@@ -87,6 +88,7 @@ export default function BinConditioningCard(props: Props) {
sink={sink}
source={source}
grain={grain}
+ customGrain={customGrain}
/>
);
}
diff --git a/src/bin/BinConditioningInteraction.tsx b/src/bin/BinConditioningInteraction.tsx
index 7fe8bea..bd6c3c7 100644
--- a/src/bin/BinConditioningInteraction.tsx
+++ b/src/bin/BinConditioningInteraction.tsx
@@ -84,10 +84,11 @@ interface Props {
source: Component;
sink: Component;
grain?: pond.Grain;
+ customGrain?: pond.GrainSettings
}
export default function BinConditioningInteraction(props: Props) {
- const { interaction, source, sink, grain, deviceId } = props;
+ const { interaction, source, sink, grain, deviceId, customGrain } = props;
const [sliderVals, setSliderVals] = useState>(new Map());
const [sliderMarks, setSliderMarks] = useState>(new Map());
//this is the emc value calculated from the interactions temp and humidity conditions
@@ -127,7 +128,6 @@ export default function BinConditioningInteraction(props: Props) {
if (
grain !== undefined &&
grain !== pond.Grain.GRAIN_INVALID &&
- grain !== pond.Grain.GRAIN_CUSTOM &&
temp &&
hum
) {
@@ -135,8 +135,9 @@ export default function BinConditioningInteraction(props: Props) {
//the emc calc needs the temp to be in celsius
temp = fahrenheitToCelsius(temp);
}
- let emc = ExtractMoisture(grain, temp, hum);
- setBaseEMC(emc);
+ let emc = ExtractMoisture(grain, temp, hum, customGrain)
+ setBaseEMC(emc === hum ? undefined : emc);
+
}
}, [sliderVals, grain]);
diff --git a/src/bin/BinSVGV2.tsx b/src/bin/BinSVGV2.tsx
index f3c3454..ecbeb53 100644
--- a/src/bin/BinSVGV2.tsx
+++ b/src/bin/BinSVGV2.tsx
@@ -147,6 +147,7 @@ interface Props {
hottestNodeTemp?: number;
coldestNodeTemp?: number;
inventoryControl?: pond.BinInventoryControl;
+ customGrain?: pond.GrainSettings;
}
interface GrainNodePoint {
@@ -211,7 +212,8 @@ export default function BinSVGV2(props: Props) {
hottestNodeTemp,
coldestNodeTemp,
inventoryControl,
- co2Sensors
+ co2Sensors,
+ customGrain
} = props;
const [{ user }] = useGlobalState();
const [extraDetails, setExtraDetails] = useState<"temps" | "hums">("temps");
@@ -683,7 +685,7 @@ export default function BinSVGV2(props: Props) {
x: cablePos,
y: nodeY + 10,
stroke: "green",
- value: ExtractMoisture(props.grainType, temp, cable.humidities[index] ?? 0).toFixed(1)+"%",
+ value: ExtractMoisture(props.grainType, temp, cable.humidities[index] ?? 0, customGrain).toFixed(1)+"%",
})
!showTempHum &&
nodeClickData.push({
diff --git a/src/bin/BinSettings.tsx b/src/bin/BinSettings.tsx
index 61389f9..4192974 100644
--- a/src/bin/BinSettings.tsx
+++ b/src/bin/BinSettings.tsx
@@ -65,6 +65,7 @@ import { getDistanceUnit, or, getTemperatureUnit, getGrainUnit } from "utils";
import { makeStyles } from "@mui/styles";
import { useNavigate } from "react-router-dom";
import BinSelector from "./BinSelector";
+import CustomGrainSelector from "grain/CustomGrainSelector";
// import BinSelector from "./BinSelector";
const useStyles = makeStyles((theme: Theme) => {
@@ -205,6 +206,7 @@ export default function BinSettings(props: Props) {
const libracartAPI = useLibraCartProxyAPI()
const [lcDestination, setlcDestination] = useState()
const [lcDestinationOptions, setlcDestinationOptions] = useState ([])
+ const [customGrain, setCustomGrain] = useState()
useEffect(() => {
if (open) {
@@ -236,6 +238,9 @@ export default function BinSettings(props: Props) {
initForm.inventory.grainType
).bushelsPerTonne;
}
+ if (initForm.inventory.customGrain){
+ setCustomGrain(initForm.inventory.customGrain)
+ }
//if the target temp is not set (older bins) make it the midpoint of the high and low temps assuming they are set otherwise make it 15
if (!initForm.inventory.targetTemperature || initForm.inventory.targetTemperature) {
if (initForm.highTemp && initForm.lowTemp) {
@@ -444,6 +449,11 @@ export default function BinSettings(props: Props) {
form.inventory.inventoryControl = inventoryControl
form.inventory.autoThreshold = autoFillThreshold
form.inventory.lidarDropCm = getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? lidarDropDistance * 30.48 : lidarDropDistance
+ if(form.inventory.grainType !== pond.Grain.GRAIN_CUSTOM){
+ form.inventory.customGrain = undefined
+ }else{
+ form.inventory.customGrain = customGrain
+ }
}
binAPI
@@ -493,6 +503,11 @@ export default function BinSettings(props: Props) {
form.inventory.autoThreshold = autoFillThreshold
//if the users preferences are in feet convert the distance to cm otherwise it was entered as cm so use that
form.inventory.lidarDropCm = getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? lidarDropDistance * 30.48 : lidarDropDistance
+ if(form.inventory.grainType !== pond.Grain.GRAIN_CUSTOM){
+ form.inventory.customGrain = undefined
+ }else{
+ form.inventory.customGrain = customGrain
+ }
}
binAPI
.updateBin(bin.key(), form, as)
@@ -1014,7 +1029,37 @@ export default function BinSettings(props: Props) {
{isCustom ? "Custom" : "Grain"}
*/}
- {!isCustomInventory && (
+ {isCustomInventory ?
+
+ {storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER ?
+
+ {
+ updateForm(
+ "inventory",
+ pond.BinInventory.create({
+ ...form.inventory,
+ customTypeName: event.target.value
+ })
+ );
+ updateFormExtension("customTypeName", event.target.value);
+ }}
+ fullWidth
+ variant="outlined"
+ className={classes.bottomSpacing}
+ />
+
+ :
+
+ {setCustomGrain(newGrainSettings)}}/>
+
+ }
+
+ :
+
- )}
- {isCustomInventory ? (
-
- {
- updateForm(
- "inventory",
- pond.BinInventory.create({
- ...form.inventory,
- customTypeName: event.target.value
- })
- );
- updateFormExtension("customTypeName", event.target.value);
- }}
- fullWidth
- variant="outlined"
- className={classes.bottomSpacing}
- />
- {storageType !== pond.BinStorage.BIN_STORAGE_FERTILIZER && (
- {
- updateForm(
- "inventory",
- pond.BinInventory.create({
- ...form.inventory,
- bushelsPerTonne: +event.target.value
- })
- );
- updateFormExtension("bushelsPerTonne", event.target.value);
- }}
- fullWidth
- variant="outlined"
- className={classes.bottomSpacing}
- />
- )}
-
- ) : (
- )}
+
+ }
{getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_BUSHELS ||
storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER ||
formExtension.bushelsPerTonne === "0" ||
diff --git a/src/bin/BinVisualizerV2.tsx b/src/bin/BinVisualizerV2.tsx
index 9036b91..d91f698 100644
--- a/src/bin/BinVisualizerV2.tsx
+++ b/src/bin/BinVisualizerV2.tsx
@@ -70,6 +70,7 @@ import Edit from "@mui/icons-material/Edit";
import { makeStyles } from "@mui/styles";
import ButtonGroup from "common/ButtonGroup";
import ModeChangeDialog from "./conditioning/modeChangeDialog";
+import CustomGrainSelector from "grain/CustomGrainSelector";
const useStyles = makeStyles((theme: Theme) => {
return ({
@@ -286,6 +287,7 @@ export default function BinVisualizer(props: Props) {
const [openModeChange, setOpenModeChange] = useState(false)
const [modeChangeInProgress, setModeChangeInProgress] = useState(false)
+ const [newGrainSettings, setNewGrainSettings] = useState()
useEffect(() => {
setModeTime(moment(bin.status.lastModeChange));
@@ -327,16 +329,26 @@ export default function BinVisualizer(props: Props) {
setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2));
setGrainSubtype(bin.subtype());
setNewBinMode(bin.settings.mode);
+ setNewGrainSettings(bin.customGrain());
}
if (bin.settings) {
- let t = bin.settings.outdoorTemp;
+ let t = 0
+ let h = 0
+ if(ambient){
+ t = ambient.temperature
+ h = ambient.humidity
+ }else{
+ t = bin.settings.outdoorTemp;//use what is already set in the bin settings
+ h = bin.settings.outdoorHumidity
+ }
+
if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
t = CtoF(t);
}
setOutdoorTemp(t);
+ setOutdoorHumidity(h)
}
- if (bin.settings) setOutdoorHumidity(bin.settings.outdoorHumidity);
- }, [bin, user]);
+ }, [bin, user, ambient]);
useEffect(() => {
//if(loadingTrend) return
@@ -515,18 +527,13 @@ export default function BinVisualizer(props: Props) {
const capacity = bin.settings.specs?.bushelCapacity ?? 0;
const grainBushels = bin.bushels();
const isEmpty = bin.settings.inventory?.empty === true || !grainBushels || grainBushels <= 0;
- const grainType = bin.settings.inventory?.grainType;
- const grainTypeName = isEmpty || !grainType ? "" : GrainDescriber(grainType).name;
- const customTypeName = bin.settings.inventory?.customTypeName;
const grainSubtype = bin.settings.inventory?.grainSubtype;
return (
{/* grain display */}
- {bin.storage() === pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN
- ? grainTypeName
- : customTypeName}
+ {isEmpty ? "" : bin.grainName()}
{grainSubtype !== "" ? " - " + grainSubtype : ""}
-
- )}
+ )} */}
+ {setNewGrainSettings(settings)}}/>
) : (
@@ -1574,6 +1586,12 @@ export default function BinVisualizer(props: Props) {
const fanPerformance = () => {
let totalCFM = bin.status.fanCfm;
let bushelCFM = bin.status.cfmPerBushel;
+ let emc = ExtractMoisture(
+ bin.grain(),
+ activePlenum?.tempHumidity?.temperature ?? 0,
+ activePlenum?.tempHumidity?.humidity ?? 0,
+ bin.customGrain()
+ )
return (
@@ -1616,20 +1634,18 @@ export default function BinVisualizer(props: Props) {
{cfmDryWarning(bushelCFM)}
-
-
-
- {ExtractMoisture(
- bin.grain(),
- activePlenum?.tempHumidity?.temperature ?? 0,
- activePlenum?.tempHumidity?.humidity ?? 0
- ).toFixed(2)}
- %
-
-
- Plenum EMC
-
-
+
+ {emc !== activePlenum?.tempHumidity?.humidity &&
+
+
+ {emc.toFixed(2)}
+ %
+
+
+ Plenum EMC
+
+
+ }
@@ -1638,6 +1654,12 @@ export default function BinVisualizer(props: Props) {
};
const ambientDisplay = () => {
+ let emc = ExtractMoisture(
+ bin.grain(),
+ ambient?.temperature ?? 0,
+ ambient?.humidity ?? 0,
+ bin.customGrain()
+ )
return (
@@ -1677,19 +1699,17 @@ export default function BinVisualizer(props: Props) {
-
-
- {ExtractMoisture(
- bin.grain(),
- ambient?.temperature ?? 0,
- ambient?.humidity ?? 0
- ).toFixed(2)}
- %
-
-
- Ambient EMC
-
-
+ {emc !== ambient?.humidity &&
+
+
+ {emc.toFixed(2)}
+ %
+
+
+ Ambient EMC
+
+
+ }
@@ -1968,6 +1988,7 @@ export default function BinVisualizer(props: Props) {
? 0
: parseFloat(bushPerTonne);
b.settings.inventory.grainSubtype = grainSubtype;
+ b.settings.inventory.customGrain = newGrainSettings
}
if (b.settings.outdoorTemp !== undefined) {
@@ -1986,7 +2007,7 @@ export default function BinVisualizer(props: Props) {
}
b.settings.mode = newBinMode;
}
-
+ console.log(b.settings)
binAPI.updateBin(bin.key(), b.settings, as).then(resp => {
refresh();
});
@@ -2013,8 +2034,8 @@ export default function BinVisualizer(props: Props) {
if(refresh) updateBin();
}}
defaultTargetMoisture={bin.settings.inventory?.initialMoisture}
- // defaultOutdoorTemp={get this from any ambient sensors}
- // defaultOutdoorHumidity={get this from any ambient sensors}
+ defaultOutdoorTemp={outdoorTemp}
+ defaultOutdoorHumidity={outdoorHumidity}
changeTargetMoisture={newMoisture => {
setTargetMoisture(newMoisture)
}}
diff --git a/src/bin/conditioning/modeChangeDialog.tsx b/src/bin/conditioning/modeChangeDialog.tsx
index 2913e4a..b31c8f8 100644
--- a/src/bin/conditioning/modeChangeDialog.tsx
+++ b/src/bin/conditioning/modeChangeDialog.tsx
@@ -108,9 +108,9 @@ export default function ModeChangeDialog(props: Props){
const [deviceOption, setDeviceOption] = useState()
useEffect(()=>{
- setMoistureInput(defaultTargetMoisture?.toFixed(2) ?? "0")
- setOutdoorTempInput(defaultOutdoorTemp?.toFixed(2) ?? "0")
- setOutdoorHumidityInput(defaultOutdoorHumidity?.toFixed(2) ?? "0")
+ setMoistureInput(defaultTargetMoisture?.toString() ?? "0")
+ setOutdoorTempInput(defaultOutdoorTemp?.toString() ?? "0")
+ setOutdoorHumidityInput(defaultOutdoorHumidity?.toString() ?? "0")
},[defaultTargetMoisture, defaultOutdoorTemp, defaultOutdoorHumidity])
//get the interactions and components for the device when it is selected from the dropdown
@@ -436,7 +436,9 @@ if (!selectedDevice) return;
}
stage1.steps.push(newStep);
});
- stages.push(stage1)
+ if(stage1.steps.length > 0){
+ stages.push(stage1)
+ }
//stage two is to add the new interactions
let stage2: Stage = {
title: "Add New Interactions",
@@ -447,7 +449,9 @@ if (!selectedDevice) return;
}
]
}
- stages.push(stage2)
+ if(stage2.steps.length > 0){
+ stages.push(stage2)
+ }
//stage three is to update the controller components
let stage3: Stage = {
@@ -463,7 +467,9 @@ if (!selectedDevice) return;
stage3.steps.push(newStep)
})
})
- stages.push(stage3)
+ if(stage3.steps.length > 0){
+ stages.push(stage3)
+ }
//set those stages to a state variable
setPromiseStages(stages)
}
@@ -618,11 +624,7 @@ if (!selectedDevice) return;
onChange={event => {
setOutdoorTempInput(event.target.value)
if(changeOutdoorTemp && !isNaN(parseFloat(event.target.value))){
- //if the users prefs are F will need to convert it to C when sending it back
let val = parseFloat(event.target.value)
- if(getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){
- val = fahrenheitToCelsius(val)
- }
changeOutdoorTemp(val)
}
}}
@@ -721,7 +723,7 @@ if (!selectedDevice) return;
{
startChange()
diff --git a/src/bin/graphs/BinGraphs.tsx b/src/bin/graphs/BinGraphs.tsx
index e28c3ae..253ee13 100644
--- a/src/bin/graphs/BinGraphs.tsx
+++ b/src/bin/graphs/BinGraphs.tsx
@@ -170,18 +170,18 @@ export default function BinGraphs(props: Props) {
setEndDate(newEndDate);
};
- const determineGrainColour = () => {
- let col = "yellow";
- let binInv = bin.settings.inventory;
- if (binInv) {
- if (binInv.grainType === pond.Grain.GRAIN_CUSTOM) {
- col = stringToMaterialColour(binInv.customTypeName);
- } else if (binInv.grainType !== pond.Grain.GRAIN_NONE) {
- col = GrainDescriber(binInv.grainType).colour;
- }
- }
- return col;
- };
+ // const determineGrainColour = () => {
+ // let col = "yellow";
+ // let binInv = bin.settings.inventory;
+ // if (binInv) {
+ // if (binInv.grainType === pond.Grain.GRAIN_CUSTOM) {
+ // col = stringToMaterialColour(binInv.customTypeName);
+ // } else if (binInv.grainType !== pond.Grain.GRAIN_NONE) {
+ // col = GrainDescriber(binInv.grainType).colour;
+ // }
+ // }
+ // return col;
+ // };
const inventoryGraph = () => {
return (
@@ -193,7 +193,7 @@ export default function BinGraphs(props: Props) {
endDate={endDate}
binLoading={binLoading}
bin={bin}
- colour={determineGrainColour()}
+ colour={bin.grainColour()}
fertilizerBin={bin.settings.storage === pond.BinStorage.BIN_STORAGE_FERTILIZER}
/>
diff --git a/src/bin/graphs/BinGraphsTrending.tsx b/src/bin/graphs/BinGraphsTrending.tsx
index 6b3aed5..ff024b2 100644
--- a/src/bin/graphs/BinGraphsTrending.tsx
+++ b/src/bin/graphs/BinGraphsTrending.tsx
@@ -92,7 +92,8 @@ export default function BinGraphsTrending(props: Props) {
trend: ExtractMoisture(
bin.grain(),
val.values[0],
- plenumHumidity.values[i].values[0]
+ plenumHumidity.values[i].values[0],
+ bin.customGrain()
)
};
trendData.push(trendPoint);
diff --git a/src/common/DeviceLinkDrawer.tsx b/src/common/DeviceLinkDrawer.tsx
index b01113c..e5d6fd8 100644
--- a/src/common/DeviceLinkDrawer.tsx
+++ b/src/common/DeviceLinkDrawer.tsx
@@ -21,6 +21,7 @@ import { pond } from "protobuf-ts/pond";
import React, { useEffect, useState } from "react";
import { makeStyles } from "@mui/styles";
import { useGlobalState } from "providers";
+import { getContextKeys, getContextTypes } from "pbHelpers/Context";
interface TabPanelProps {
children?: React.ReactNode;
@@ -116,13 +117,7 @@ export default function DeviceLinkDrawer(props: Props) {
undefined,
undefined,
undefined,
- true,
- undefined,
- undefined,
- undefined,
- undefined,
- undefined,
- as
+ true
)
.then(resp => {
let devMap = new Map();
diff --git a/src/common/PromiseProgress.tsx b/src/common/PromiseProgress.tsx
index 41b71cd..0bc39e9 100644
--- a/src/common/PromiseProgress.tsx
+++ b/src/common/PromiseProgress.tsx
@@ -1,5 +1,5 @@
import { AxiosResponse } from "axios"
-import { Box, Button, CircularProgress, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material"
+import { Box, Button, CircularProgress, DialogActions, DialogContent, DialogTitle, Divider, Typography } from "@mui/material"
import { useEffect, useState } from "react"
import { CheckCircle, Error, Pending } from "@mui/icons-material"
import React from "react"
@@ -18,7 +18,7 @@ export interface Step {
interface Props {
stages: Stage[]
- automaticStart: boolean
+ description?: string
failFast?: boolean
onStart?: () => void
onComplete?: () => void
@@ -32,7 +32,7 @@ enum progress {
}
export function PromiseProgress(props: Props){
- const {stages, failFast, onStart, onComplete, automaticStart} = props
+ const {stages, failFast, onStart, onComplete, description } = props
const [completion, setCompletion] = useState>(new Map())
const execute = () => {
@@ -84,12 +84,6 @@ export function PromiseProgress(props: Props){
});
}
- useEffect(() => {
- if(automaticStart){
- execute()
- }
- },[automaticStart, execute])
-
const getCompletion = (completion?: progress) => {
switch(completion){
case progress.InProgress:
@@ -123,16 +117,17 @@ export function PromiseProgress(props: Props){
Progress
- {stages.map((stage, number) => displayStage(stage, number))}
+ {description}
+
+ {stages.map((stage, number) => displayStage(stage, number))}
+
- {!automaticStart &&
- {
- execute()
- }}>
- Start
-
- }
+ {
+ execute()
+ }}>
+ Start
+
)
diff --git a/src/component/ComponentForm.tsx b/src/component/ComponentForm.tsx
index e542ed0..21ca747 100644
--- a/src/component/ComponentForm.tsx
+++ b/src/component/ComponentForm.tsx
@@ -4,6 +4,7 @@ import {
AccordionSummary,
Alert,
AlertTitle,
+ Box,
Button,
Collapse,
FormControl,
@@ -47,6 +48,7 @@ import { getDistanceUnit } from "utils";
import { GrainOptions } from "grain";
import CompModes from "component/ComponentMode.json";
import FanPicker from "fans/fanPicker";
+import CustomGrainSelector from "grain/CustomGrainSelector";
const useStyles = makeStyles((theme: Theme) => {
return ({
@@ -136,6 +138,7 @@ export default function ComponentForm(props: Props) {
sensorDistance: "0",
});
const [compMode, setCompMode] = useState();
+ const [useCustomGrain, setUseCustomGrain] = useState(false)
//const [numCalibrations, setNumCalibrations] = useState(0)
useEffect(() => {
@@ -192,6 +195,10 @@ export default function ComponentForm(props: Props) {
sensorDistance = sensorDistance / 30.48;
}
+ if(formComponent.settings.customGrain){
+ setUseCustomGrain(true)
+ }
+
setForm({
component: formComponent,
measure: formComponent.settings.measurementPeriodMs > 0,
@@ -383,6 +390,13 @@ export default function ComponentForm(props: Props) {
setForm(f);
};
+ const updateCustomGrain = (grainSettings?: pond.GrainSettings) => {
+ let f = cloneDeep(form)
+ f.component.settings.customGrain = grainSettings
+ f.component.settings.grainType = pond.Grain.GRAIN_CUSTOM
+ setForm(f)
+ }
+
const updateGrainType = (option: Option | null) => {
let f = cloneDeep(form);
@@ -622,13 +636,36 @@ export default function ComponentForm(props: Props) {
const grainSelect = () => {
let selected = findSelectedGrain(grainOptions);
return (
-
+
+ {
+ setUseCustomGrain(!useCustomGrain);
+ updateCustomGrain()
+ }}
+ />
+ }
+ label="Custom Grain"
+ labelPlacement="start"
+ />
+ {useCustomGrain ?
+
+
+
+ :
+
+ }
+
);
};
diff --git a/src/component/UnitMeasurementSummary.tsx b/src/component/UnitMeasurementSummary.tsx
index 722a514..ca2dcf0 100644
--- a/src/component/UnitMeasurementSummary.tsx
+++ b/src/component/UnitMeasurementSummary.tsx
@@ -10,6 +10,7 @@ import { Pressure } from "models/Pressure";
import { GrainCable } from "models/GrainCable";
import { extension, Summary } from "pbHelpers/ComponentType";
import { makeStyles } from "@mui/styles";
+import { stringToMaterialColour } from "utils";
interface Props {
component: Component | Plenum | Pressure | GrainCable;
@@ -100,13 +101,26 @@ export default function UnitMeasurementSummary(props: Props) {
);
};
+ const grainName = () => {
+ if(component.settings.grainType === pond.Grain.GRAIN_CUSTOM && component.settings.customGrain?.name){
+ return component.settings.customGrain.name
+ }
+ return GrainDescriber(component.settings.grainType).name
+ }
+
+ const grainColor = () => {
+ if(component.settings.grainType === pond.Grain.GRAIN_CUSTOM && component.settings.customGrain?.name){
+ return stringToMaterialColour(component.settings.customGrain.name)
+ }
+ return GrainDescriber(component.settings.grainType).colour
+ }
+
const getSummaryComponent = (summaries: Summary[]): any => {
if (summaries.length < 1) {
return noSummary();
}
let overlays: JSX.Element[] = [];
- let grain = GrainDescriber(component.settings.grainType);
return (
@@ -170,7 +184,7 @@ export default function UnitMeasurementSummary(props: Props) {
component.settings.grainType !== pond.Grain.GRAIN_INVALID && (
- Grain Type: {grain.name}
+ Grain Type: {grainName()}
)}
diff --git a/src/gate/GateDeviceInteraction.tsx b/src/gate/GateDeviceInteraction.tsx
index ca05a58..23669f3 100644
--- a/src/gate/GateDeviceInteraction.tsx
+++ b/src/gate/GateDeviceInteraction.tsx
@@ -1,4 +1,5 @@
-import { Button, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material";
+import { CheckBox } from "@mui/icons-material";
+import { Button, Checkbox, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, FormLabel, InputAdornment, TextField, Typography } from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { Component, Device } from "models";
import { Gate } from "models/Gate";
@@ -6,6 +7,7 @@ import moment from "moment";
import { pond, quack } from "protobuf-ts/pond";
import { useGlobalState, useInteractionsAPI, useSnackbar } from "providers";
import React, { useEffect, useState } from "react";
+import { getPressureUnit } from "utils";
interface Props {
open: boolean;
@@ -37,6 +39,7 @@ const densityMap = new Map([
export default function GateDeviceInteraction(props: Props) {
const { open, close, gate, compDevice, densityTemp } = props;
const [{ user, as }] = useGlobalState();
+ const [device, setDevice] = useState(Device.create())
const [lowDelta, setLowDelta] = useState(0);
const [highDelta, setHighDelta] = useState(0);
const [greenComponent, setGreenComponent] = useState();
@@ -44,7 +47,12 @@ export default function GateDeviceInteraction(props: Props) {
const [pressureComponent, setPressureComponent] = useState();
const interactionsAPI = useInteractionsAPI();
const [adding, setAdding] = useState(false);
+ //boolean to determine if a third condition should be put on the interaction for the red light, requires a V2 device running at least fromware version 2.1.9
+ const [useRedOffCondition, setUseRedOffCondition] = useState(false)
+ //if the pressure is under this value then the red light should be off
+ const [redThreshold, setRedThreshold] = useState("0")
const { openSnack } = useSnackbar();
+ const [pressureSource, setPressureSource] = useState(quack.ComponentID.create())
useEffect(() => {
//math to determine what the delta pressures to set will be
@@ -95,46 +103,64 @@ export default function GateDeviceInteraction(props: Props) {
gate.preferences[component.key()] === pond.GateComponentType.GATE_COMPONENT_TYPE_PRESSURE
) {
setPressureComponent(component);
+ setPressureSource(
+ quack.ComponentID.create({
+ type: component.type(),
+ address: component.settings.address,
+ addressType: component.settings.addressType
+ })
+ )
}
}
});
+
+ if(compDevice.device){
+ setDevice(Device.create(compDevice.device))
+ }
}, [gate, densityTemp, user, compDevice]);
- // useEffect(() => {
- // //load current interactions for the device
- // let deviceID = Device.any(compDevice.device).id();
- // interactionsAPI.listInteractionsByDevice(deviceID).then(resp => {
- // setCurrentInteractions(resp);
- // });
- // }, [compDevice.device, interactionsAPI]);
-
- // const removeCurrentInteractions = () => {
- // let deviceID = Device.any(compDevice.device).id();
- // currentInteractions.forEach(interaction => {
- // interactionsAPI.removeInteraction(deviceID, interaction.key());
- // });
- // };
+ const buttonDisabled = () => {
+ if (greenComponent === undefined) return true
+ if (redComponent === undefined) return true
+ if (adding) return true
+ if (useRedOffCondition && isNaN(parseFloat(redThreshold))) return true
+ return false
+ }
const createInteractions = async () => {
//the interactions to be made
- //TOGGLE green ON when pressure within range of upper and lower
if (
greenComponent !== undefined &&
redComponent !== undefined &&
pressureComponent !== undefined
) {
- let greenToggle: pond.InteractionSettings = pond.InteractionSettings.create({
- sink: quack.ComponentID.create({
- type: greenComponent.type(),
- address: greenComponent.settings.address,
- addressType: greenComponent.settings.addressType
- }),
- source: quack.ComponentID.create({
- type: pressureComponent.type(),
- address: pressureComponent.settings.address,
- addressType: pressureComponent.settings.addressType
- }),
+
+ let lightInteractions: pond.InteractionSettings[] = []
+ //making variables for the parameters of the interactions
+ const redSink = quack.ComponentID.create({
+ type: redComponent.type(),
+ address: redComponent.settings.address,
+ addressType: redComponent.settings.addressType
+ })
+
+ const greenSink = quack.ComponentID.create({
+ type: greenComponent.type(),
+ address: greenComponent.settings.address,
+ addressType: greenComponent.settings.addressType
+ })
+
+ const lightSchedule = pond.InteractionSchedule.create({
+ timezone: moment.tz.guess(),
+ weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"],
+ timeOfDayStart: "00:00",
+ timeOfDayEnd: "24:00"
+ })
+
+ //TOGGLE green ON when pressure within range of upper and lower, this will always be the same regardless
+ lightInteractions.push(pond.InteractionSettings.create({
+ sink: greenSink,
+ source: pressureSource,
conditions: [
pond.InteractionCondition.create({
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
@@ -150,61 +176,146 @@ export default function GateDeviceInteraction(props: Props) {
nodeOne: 2,
nodeTwo: 1,
subtype: 18,
- schedule: pond.InteractionSchedule.create({
- timezone: moment.tz.guess(),
- weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"],
- timeOfDayStart: "00:00",
- timeOfDayEnd: "24:00"
- }),
+ schedule: lightSchedule,
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
value: 1
})
- });
+ }))
- //TOGGLE red OFF when pressure within range of upper and lower
- let redToggle: pond.InteractionSettings = pond.InteractionSettings.create({
- sink: quack.ComponentID.create({
- type: redComponent.type(),
- address: redComponent.settings.address,
- addressType: redComponent.settings.addressType
- }),
- source: quack.ComponentID.create({
- type: pressureComponent.type(),
- address: pressureComponent.settings.address,
- addressType: pressureComponent.settings.addressType
- }),
- conditions: [
- pond.InteractionCondition.create({
- comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
- value: -highDelta,
- measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
- }),
- pond.InteractionCondition.create({
- comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
- value: -lowDelta,
- measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
+ //if they want the red light off when below a certain pressure, it will need 4 seperate SET interactions
+ if(useRedOffCondition){
+ //need to make sure the redthreshold is in pascals
+ let thresholdPascals = 0
+ if(!isNaN(parseFloat(redThreshold))){
+ if(getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_KILOPASCALS){
+ thresholdPascals = parseFloat(redThreshold)*1000
+ }else if (getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER){
+ thresholdPascals = parseFloat(redThreshold)*249
+ }
+ }
+
+ //SET red on when above high
+ lightInteractions.push(pond.InteractionSettings.create({
+ sink: redSink,
+ source: pressureSource,
+ conditions: [
+ pond.InteractionCondition.create({
+ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
+ value: -highDelta,
+ measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
+ })
+ ],
+ nodeOne: 2,
+ nodeTwo: 1,
+ subtype: 18,
+ schedule: lightSchedule,
+ result: pond.InteractionResult.create({
+ type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET,
+ value: 1
})
- ],
- nodeOne: 2,
- nodeTwo: 1,
- subtype: 18,
- schedule: pond.InteractionSchedule.create({
- timezone: moment.tz.guess(),
- weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"],
- timeOfDayStart: "00:00",
- timeOfDayEnd: "24:00"
- }),
- result: pond.InteractionResult.create({
- type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
- value: 0
- })
- });
+ }))
+ //SET red off when below high and above low
+ lightInteractions.push(pond.InteractionSettings.create({
+ sink: redSink,
+ source: pressureSource,
+ conditions: [
+ pond.InteractionCondition.create({
+ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
+ value: -highDelta,
+ measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
+ }),
+ pond.InteractionCondition.create({
+ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
+ value: -lowDelta,
+ measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
+ })
+ ],
+ nodeOne: 2,
+ nodeTwo: 1,
+ subtype: 18,
+ schedule: lightSchedule,
+ result: pond.InteractionResult.create({
+ type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET,
+ value: 0
+ })
+ }))
+ //SET red on when below low and above threshold
+ lightInteractions.push(pond.InteractionSettings.create({
+ sink: redSink,
+ source: pressureSource,
+ conditions: [
+ pond.InteractionCondition.create({
+ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
+ value: -lowDelta,
+ measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
+ }),
+ pond.InteractionCondition.create({
+ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
+ value: -thresholdPascals,
+ measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
+ }),
+ ],
+ nodeOne: 2,
+ nodeTwo: 1,
+ subtype: 18,
+ schedule: lightSchedule,
+ result: pond.InteractionResult.create({
+ type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET,
+ value: 1
+ })
+ }))
+ //SET red off when below threshold
+ lightInteractions.push(pond.InteractionSettings.create({
+ sink: redSink,
+ source: pressureSource,
+ conditions: [
+ pond.InteractionCondition.create({
+ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
+ value: thresholdPascals,
+ measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
+ }),
+ ],
+ subtype: 1,
+ schedule: lightSchedule,
+ result: pond.InteractionResult.create({
+ type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET,
+ value: 0
+ })
+ }))
+ }else{
+ //otherwise use the regular single TOGGLE interaction
+ //TOGGLE red OFF when pressure within range of upper and lower
+ lightInteractions.push(pond.InteractionSettings.create({
+ sink: redSink,
+ source: pressureSource,
+ conditions: [
+ pond.InteractionCondition.create({
+ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
+ value: -highDelta,
+ measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
+ }),
+ pond.InteractionCondition.create({
+ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
+ value: -lowDelta,
+ measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
+ })
+ ],
+ nodeOne: 2,
+ nodeTwo: 1,
+ subtype: 18,
+ schedule: lightSchedule,
+ result: pond.InteractionResult.create({
+ type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
+ value: 0
+ })
+ }))
+ }
let deviceID = Device.any(compDevice.device).id();
if (deviceID !== undefined) {
let multi = pond.MultiInteractionSettings.create({
- interactions: [greenToggle, redToggle]
+ interactions: lightInteractions
});
interactionsAPI
.addMultiInteractions(deviceID, multi, as)
@@ -230,11 +341,51 @@ export default function GateDeviceInteraction(props: Props) {
}}>
Set Interaction For Light Toggle
- Your Delta Pressures, in pascals, will be:
+ {/* Your Delta Pressures, in pascals, will be:
low = {lowDelta}
high = {highDelta}
{greenComponent ? "" : "Green LED Component not found"}
- {redComponent ? "" : "Red LED Component not found"}
+ {redComponent ? "" : "Red LED Component not found"} */}
+
+ This will clear existing interactions on the pressure chain and set new ones to toggle the green light on and the red light off
+ when the difference between the pressures falls within this range and vice versa:
+
+
+ Upper Limit: {getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER ? (highDelta/249.089).toFixed(2) + " iwg" : highDelta/1000 + " kPa"}
+ Lower Limit: {getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER ? (lowDelta/249.089).toFixed(2) + " iwg" : lowDelta/1000 + " kPa"}
+
+
+ If you would like to have the interactions set in such a way that if the average pressure falls below a given value set the use off condition and set the value to use
+ {
+ setUseRedOffCondition(checked)
+ }}
+ />
+ }
+ label={Use Off Condition }
+ />
+
+ {getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER ? "iwg" : "kPa"}
+
+ )
+ }}
+ onChange={e => {
+ setRedThreshold(e.target.value)
+ }}
+ />
+
{
+ createInteractions();
+ }).catch(err => {
+ console.error(err)
+ })
}}
- disabled={greenComponent === undefined || redComponent === undefined || adding}>
+ disabled={buttonDisabled()}>
{adding ? "Adding Interaction" : "Create Interaction"}
diff --git a/src/gate/GateFlowGraph.tsx b/src/gate/GateFlowGraph.tsx
index 6835b63..e6cdaf4 100644
--- a/src/gate/GateFlowGraph.tsx
+++ b/src/gate/GateFlowGraph.tsx
@@ -70,13 +70,19 @@ export default function GateFlowGraph(props: Props) {
const classes = useStyles();
const [flowEvents, setFlowEvents] = useState([]);
const [eventsLoading, setEventsLoading] = useState(false);
- //these two constants could be entered by the user at time of retrieval
+ //these two constants could be entered by the user at time of retrieval or set in the gates settings
const eventThreshold = 5; //the threshold that if crossed from one measurement to the next during a flow event will end the current flow event and start a new one
- const idleFlow = 3.5; //the mass air flow that must be crossed in order to start a flow event, anything below this will not start an event but must go below this to end an event
-
+ //const idleFlow = 3.5; //the mass air flow that must be crossed in order to start a flow event, anything below this will not start an event but must go below this to end an event
+ const [idleFlow, setIdleFlow] = useState(3.5)
+
useEffect(() => {
if (loadingChartData) return;
if (ambient && pressureComponent) {
+ let gateIdle = idleFlow
+ if(gate.settings.idleFlow){
+ gateIdle = gate.settings.idleFlow
+ }
+
let recent: SSAreaDataPoint | undefined;
setLoadingChartData(true);
gateAPI
@@ -110,11 +116,11 @@ export default function GateFlowGraph(props: Props) {
/** determine runtime */
// set the start time if the values is greater than the idleFlow and start is not already set
- if (val.airFlow >= idleFlow && !start) {
+ if (val.airFlow >= gateIdle && !start) {
start = time;
}
// set the stop time when off or at the last measurements and the start time is set
- if ((val.airFlow < idleFlow || i === resp.data.values.length - idleFlow) && start) {
+ if ((val.airFlow < gateIdle || i === resp.data.values.length - gateIdle) && start) {
stop = time;
}
// if both start and stop are set calculate add the timeframe to the total runtime
diff --git a/src/gate/GateList.tsx b/src/gate/GateList.tsx
index 5c548e2..3e3b330 100644
--- a/src/gate/GateList.tsx
+++ b/src/gate/GateList.tsx
@@ -11,8 +11,10 @@ import ResponsiveTable, { Column } from "common/ResponsiveTable";
import AddGateFab from "./AddGateFab";
import GateSettings from "./GateSettings";
import { useGateAPI, useGlobalState } from "providers";
-import { Settings } from "@mui/icons-material";
+import { CheckCircleOutline, DoNotDisturb, ErrorOutline, HelpOutlineOutlined, Settings } from "@mui/icons-material";
import { cloneDeep } from "lodash";
+import moment from "moment";
+import { pond } from "protobuf-ts/pond";
interface Props {
//gates: Gate[];
@@ -89,6 +91,19 @@ export default function GateList(props: Props) {
setPageSize(event.target.value);
};
+ const displayPCAStatus = (state: pond.PCAState) => {
+ switch(state){
+ case pond.PCAState.PCA_STATE_IN_BOUNDS:
+ return
+ case pond.PCAState.PCA_STATE_OUT_BOUNDS:
+ return
+ case pond.PCAState.PCA_STATE_OFF:
+ return
+ default:
+ return
+ }
+ }
+
const desktopCols = (): Column[] => {
return [
{
@@ -151,6 +166,29 @@ export default function GateList(props: Props) {
)
},
+ {
+ title: "PCA Status",
+ render: gate => (
+
+ {displayPCAStatus(gate.status.pcaState)}
+
+ )
+ },
+ {
+ title: "Last FLow",
+ render: gate => (
+
+
+ Last Flow:
+ {gate.status.lastMassAirflow.toFixed(2)}
+
+
+ Last Reading:
+ {gate.status.lastUpdate !== "" ? moment(gate.status.lastUpdate).fromNow() : "No Reading Yet"}
+
+
+ )
+ }
]
}
const mobileCols = (): Column[] => {
@@ -162,13 +200,16 @@ export default function GateList(props: Props) {
{gate.name}
- {
- event.stopPropagation()
- setSelectedGate(gate)
- setGateDialog(true)
- }}>
-
-
+
+ {displayPCAStatus(gate.status.pcaState)}
+ {
+ event.stopPropagation()
+ setSelectedGate(gate)
+ setGateDialog(true)
+ }}>
+
+
+
)
}
@@ -198,6 +239,14 @@ export default function GateList(props: Props) {
PCA Unit:
{gate.settings.pcaType !== "" ? gate.settings.pcaType : "None"}
+
+ Last Flow:
+ {gate.status.lastMassAirflow}
+
+
+ Last Reading:
+ {gate.status.lastUpdate !== "" ? moment(gate.status.lastUpdate).fromNow() : "No PCA reading yet"}
+
//
diff --git a/src/gate/GateSettings.tsx b/src/gate/GateSettings.tsx
index b4a1d92..c2ae4fc 100644
--- a/src/gate/GateSettings.tsx
+++ b/src/gate/GateSettings.tsx
@@ -76,6 +76,7 @@ export default function GateSettings(props: Props) {
//user set identifier to be shown on the marker on the map
const [terminalIdentifier, setTerminalIdentifier] = useState("");
const [gateIdentifier, setGateIdentifier] = useState("");
+ const [idleLimit, setIdleLimit] = useState(0);
const [hourlyPCA, setHourlyPCA] = useState(0);
const [hourlyAPU, setHourlyAPU] = useState(0);
@@ -140,6 +141,7 @@ export default function GateSettings(props: Props) {
settings.thermalResistance = ductProps.thermalResistance;
settings.lowerFlow = lowerFlowBound;
settings.upperFlow = upperFlowBound;
+ settings.idleFlow = idleLimit;
settings.ductName = ductName;
settings.pcaType = pcaUnit;
settings.letterIdentifier = terminalIdentifier;
@@ -167,6 +169,7 @@ export default function GateSettings(props: Props) {
thermalResistance: ductProps.thermalResistance,
lowerFlow: lowerFlowBound,
upperFlow: upperFlowBound,
+ idleFlow: idleLimit,
ductName: ductName,
pcaType: pcaUnit,
letterIdentifier: terminalIdentifier,
@@ -283,16 +286,18 @@ export default function GateSettings(props: Props) {
return (
setGateName(e.target.value)}
/>
- {
setTerminalKey(e.target.value as string);
@@ -305,8 +310,9 @@ export default function GateSettings(props: Props) {
{terminal.name}
))}
-
+
setLowerFlowBound(+e.target.value)}
/>
setUpperFlowBound(+e.target.value)}
/>
setIdleLimit(+e.target.value)}
+ />
+ setPcaUnit(e.target.value)}
/>
setHourlyPCA(+e.target.value)}
/>
void
+}
+
+export default function CustomGrainForm(props: Props) {
+ const {grainSettings, onGrainSettingsChange} = props
+ const [newGrainSettings, setNewGrainSettings] = useState(pond.GrainSettings.create())
+ const [name, setName] = useState("")
+ const [group, setGroup] = useState("")
+ const [equation, setEquation] = useState(pond.MoistureEquation.MOISTURE_EQUATION_NONE)
+ const [constantA, setConstantA] = useState("0")
+ const [constantB, setConstantB] = useState("0")
+ const [constantC, setConstantC] = useState("0")
+ const [kgPerBushel, setKgPerBushel] = useState("0")
+ const [bushelsPerTonne, setBushelsPerTonne] = useState("0")
+ const valid = useRef(false)
+
+ useEffect(()=>{
+ if(grainSettings){
+ setName(grainSettings.name)
+ setGroup(grainSettings.group)
+ setEquation(grainSettings.equation)
+ setConstantA(grainSettings.a.toString())
+ setConstantB(grainSettings.b.toString())
+ setConstantC(grainSettings.c.toString())
+ setKgPerBushel(grainSettings.kgPerBushel.toString())
+ setBushelsPerTonne(grainSettings.bushelsPerTonne.toString())
+ setNewGrainSettings(grainSettings)
+ }
+ },[grainSettings])
+
+ const validate = (name: string, group: string, constA: string, constB: string, constC: string, kgPerBushel: string, bushelsPerTonne: string) => {
+ if (name === "") return false
+ if (group === "") return false
+ if (isNaN(parseFloat(constA))) return false
+ if (isNaN(parseFloat(constB))) return false
+ if (isNaN(parseFloat(constC))) return false
+ if (isNaN(parseFloat(kgPerBushel))) return false
+ if (isNaN(parseFloat(bushelsPerTonne))) return false
+ return true
+ }
+
+ const settingsChanged = (newSettings: pond.GrainSettings) => {
+ setNewGrainSettings(newSettings)
+ onGrainSettingsChange(newSettings, valid.current)
+ }
+
+ const grainForm = () => {
+ return (
+
+ {
+ let name = e.target.value
+ setName(name)
+ let settings = cloneDeep(newGrainSettings)
+ settings.name = name
+ valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsPerTonne)
+ settingsChanged(settings)
+ }}/>
+ {
+ let group = e.target.value
+ setGroup(group)
+ let settings = cloneDeep(newGrainSettings)
+ settings.group = group
+ valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsPerTonne)
+ settingsChanged(settings)
+ }}/>
+ {
+ let enumVal = parseFloat(e.target.value)
+ setEquation(enumVal)
+ let settings = cloneDeep(newGrainSettings)
+ settings.equation = enumVal
+ valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsPerTonne)
+ settingsChanged(settings)
+ }}
+ select>
+
+ None
+
+
+ Chung-Pfost
+
+
+ Henderson
+
+
+ Halsey
+
+
+ Oswin
+
+
+ {
+ let val = e.target.value
+ setConstantA(val)
+ valid.current = validate(name, group, val, constantB, constantC, kgPerBushel, bushelsPerTonne)
+ let settings = cloneDeep(newGrainSettings)
+ if(!isNaN(parseFloat(val))){
+ settings.a = parseFloat(val)
+ }
+ settingsChanged(settings)
+ }}
+ label="Constant A"/>
+ {
+ let val = e.target.value
+ setConstantB(val)
+ valid.current = validate(name, group, constantA, val, constantC, kgPerBushel, bushelsPerTonne)
+ let settings = cloneDeep(newGrainSettings)
+ if(!isNaN(parseFloat(val))){
+ settings.b = parseFloat(val)
+ }
+ settingsChanged(settings)
+ }}
+ label="Constant B"/>
+ {
+ let val = e.target.value
+ setConstantC(val)
+ valid.current = validate(name, group, constantA, constantB, val, kgPerBushel, bushelsPerTonne)
+ let settings = cloneDeep(newGrainSettings)
+ if(!isNaN(parseFloat(val))){
+ settings.c = parseFloat(val)
+ }
+ settingsChanged(settings)
+ }}
+ label="Constant C"/>
+ {
+ let val = e.target.value
+ setKgPerBushel(val)
+ valid.current = validate(name, group, constantA, constantB, constantC, val, bushelsPerTonne)
+ let settings = cloneDeep(newGrainSettings)
+ if(!isNaN(parseFloat(val))){
+ settings.kgPerBushel = parseFloat(val)
+ }
+ settingsChanged(settings)
+ }}
+ label="kg per Bushel"/>
+ {
+ let val = e.target.value
+ setBushelsPerTonne(val)
+ valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, val)
+ let settings = cloneDeep(newGrainSettings)
+ if(!isNaN(parseFloat(val))){
+ settings.bushelsPerTonne = parseFloat(val)
+ }
+ settingsChanged(settings)
+ }}
+ label="Bushels per Tonne"/>
+
+ )
+ }
+
+ return (
+
+ {grainForm()}
+
+ )
+}
\ No newline at end of file
diff --git a/src/grain/CustomGrainSelector.tsx b/src/grain/CustomGrainSelector.tsx
new file mode 100644
index 0000000..236b975
--- /dev/null
+++ b/src/grain/CustomGrainSelector.tsx
@@ -0,0 +1,155 @@
+import { ExpandMore } from "@mui/icons-material"
+import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Typography } from "@mui/material"
+import SearchSelect, { Option } from "common/SearchSelect"
+import { pond } from "protobuf-ts/pond"
+import { useGlobalState } from "providers"
+import { useGrainAPI } from "providers/pond/grainAPI"
+import React, { useEffect, useState } from "react"
+import CustomGrainForm from "./CustomGrainForm"
+
+interface Props {
+ initialGrain?: pond.GrainSettings
+ onGrainSettingsChange: (settings: pond.GrainSettings) => void
+}
+
+export default function CustomGrainSelector(props: Props) {
+ const {initialGrain, onGrainSettingsChange} = props
+ const grainAPI = useGrainAPI()
+ const [{as}] = useGlobalState()
+ const [newGrainSettings, setNewGrainSettings] = useState(initialGrain ?? pond.GrainSettings.create())
+ const [formValid, setFormValid] = useState(true)
+ const [grainMap, setGrainMap] = useState>(
+ initialGrain ?
+ new Map([
+ ["Active", pond.GrainObject.create({key: "Active", name: initialGrain.name, settings: initialGrain})]
+ ])
+ :
+ new Map())
+ const [grainOptions, setGrainOptions] = useState(
+ initialGrain ?
+ [
+ {
+ label: initialGrain.name,
+ value: "Active",
+ group: "Active"
+ }
+ ]
+ :
+ []
+ )
+ const [isNew, setIsNew] = useState(false)
+ const [selectedOption, setSelectedOption] = useState (initialGrain ? {
+ label: initialGrain.name,
+ value: "Active",
+ group: "Active"
+ } : null)
+
+ useEffect(()=>{
+ grainAPI.listGrains(0, 0, "asc", "name", undefined, undefined, undefined, as).then(resp => {
+ let options: Option[] = grainOptions
+ let map: Map = grainMap
+ if (resp.data.grains){
+ resp.data.grains.forEach(grain => {
+ map.set(grain.key, grain)
+ options.push({
+ label: grain.name,
+ value: grain.key,
+ group: grain.settings?.group,
+ })
+ })
+ }
+ setGrainOptions([...options])
+ setGrainMap(map)
+ })
+ },[grainAPI])
+
+ const saveGrain = () => {
+ // console.log(newGrainSettings)
+ grainAPI.addGrain(newGrainSettings, as).then(resp => {
+ console.log("grain added")
+ }).catch(err => {
+ console.log("err")
+ })
+ }
+
+ const settingsChanged = (newSettings: pond.GrainSettings, formValid: boolean) => {
+ setNewGrainSettings(newSettings)
+ setIsNew(true)
+ setFormValid(formValid)
+ onGrainSettingsChange(newSettings)
+ }
+
+ const grainAccordion = () => {
+ return (
+
+ }>Custom Grain Properties
+
+
+ {isNew &&
+
+ {
+ setIsNew(false)
+ saveGrain()
+ }}
+ color="primary">
+ Save Custom Grain
+
+
+ }
+
+
+ )
+ }
+
+ return (
+
+ {grainOptions.length === 0 ?
+
+ Custom Grain Properties
+
+ {isNew &&
+
+ {
+ setIsNew(false)
+ }}
+ color="primary">
+ Save Custom Grain
+
+
+ }
+
+ :
+
+
+ {
+ setIsNew(false)
+ setSelectedOption(option)
+ //when an option is selected set all of the state variables controlling the form entries
+ let grain = grainMap.get(option?.value)
+ if(grain && grain.settings){
+ //the the new grain settings object
+ setNewGrainSettings(grain.settings)
+ //pass that settings object back up
+ settingsChanged(grain.settings, true)
+ }
+ }}
+ group
+ options={grainOptions}
+ />
+
+ {grainAccordion()}
+
+
+ }
+
+ )
+}
\ No newline at end of file
diff --git a/src/grain/GrainDescriber.ts b/src/grain/GrainDescriber.ts
index 5698cb7..a462bdc 100644
--- a/src/grain/GrainDescriber.ts
+++ b/src/grain/GrainDescriber.ts
@@ -641,6 +641,16 @@ export function GetGrainExtensionMap(): Map {
return cloneDeep(GrainExtensions);
}
+export function GetValidGrainExtensions(): GrainExtension[] {
+ let validGrains: GrainExtension[] = []
+ GrainExtensions.forEach((ext, key) => {
+ if(key !== pond.Grain.GRAIN_NONE && key !== pond.Grain.GRAIN_INVALID && key !== pond.Grain.GRAIN_CUSTOM){
+ validGrains.push(ext)
+ }
+ })
+ return validGrains
+}
+
export default function GrainDescriber(type: pond.Grain): GrainExtension {
let describer = GrainExtensions.get(type);
//console.log(describer)
diff --git a/src/grain/GrainMoisture.ts b/src/grain/GrainMoisture.ts
index abfd653..808f302 100644
--- a/src/grain/GrainMoisture.ts
+++ b/src/grain/GrainMoisture.ts
@@ -16,7 +16,8 @@ const toERH = (humidity: number): number => {
export function ExtractMoisture(
type: pond.Grain | undefined,
celsius: number,
- humidity: number
+ humidity: number,
+ customGrain?: pond.GrainSettings
): number {
if (humidity <= 0) {
return 0;
@@ -26,20 +27,43 @@ export function ExtractMoisture(
type === undefined ||
type === pond.Grain.GRAIN_NONE ||
type === pond.Grain.GRAIN_INVALID ||
- type === pond.Grain.GRAIN_CUSTOM
+ (type === pond.Grain.GRAIN_CUSTOM && customGrain === undefined)
) {
return humidity;
}
let dry = humidity;
- let ctx = GrainDescriber(type);
- dry = toDryMoisture(ctx.equation, celsius, humidity, ctx.a, ctx.b, ctx.c);
+ let eq: pond.MoistureEquation
+ let constA: number
+ let constB: number
+ let constC: number
+ if(type === pond.Grain.GRAIN_CUSTOM && customGrain){
+ if(customGrain.equation === pond.MoistureEquation.MOISTURE_EQUATION_NONE) return humidity
+ eq = customGrain.equation
+ constA = customGrain.a
+ constB = customGrain.b
+ constC = customGrain.c
+ }else{
+ let ctx = GrainDescriber(type);
+ eq = ctx.equation
+ constA = ctx.a
+ constB = ctx.b
+ constC = ctx.c
+ }
+
+ dry = toDryMoisture(eq, celsius, humidity, constA, constB, constC);
return dryToWet(dry);
}
-export function WaterContent(type: pond.Grain, bushels: number, moistureContent: number): number {
- let grain = GrainDescriber(type);
- return bushels * grain.weightConversionKg * moistureContent;
+export function WaterContent(type: pond.Grain, bushels: number, moistureContent: number, customGrain?: pond.GrainSettings): number {
+ //kg per bushel conversion
+ let conversion = 0
+ if(type === pond.Grain.GRAIN_CUSTOM && customGrain){
+ conversion = customGrain.kgPerBushel
+ }else{
+ conversion = GrainDescriber(type).weightConversionKg;
+ }
+ return bushels * conversion * moistureContent;
}
export function MoistureToHumidity(
@@ -69,7 +93,7 @@ function wetToDry(wetMC: number): number {
}
function toDryMoisture(
- eq: Equation,
+ eq: pond.MoistureEquation,
T: number,
RH: number,
a: number,
@@ -78,13 +102,13 @@ function toDryMoisture(
): number {
const ERH: number = toERH(RH);
switch (eq) {
- case Equation.chungPfost:
+ case pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST:
return chungPfost(T, ERH, a, b, c);
- case Equation.halsey:
+ case pond.MoistureEquation.MOISTURE_EQUATION_HALSEY:
return halsey(T, ERH, a, b, c);
- case Equation.henderson:
+ case pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON:
return henderson(T, ERH, a, b, c);
- case Equation.oswin:
+ case pond.MoistureEquation.MOISTURE_EQUATION_OSWIN:
return oswin(T, ERH, a, b, c);
default:
return RH;
@@ -92,7 +116,7 @@ function toDryMoisture(
}
function dryMoistureToHumidity(
- eq: Equation,
+ eq: pond.MoistureEquation,
celsius: number,
dryMC: number,
a: number,
@@ -101,16 +125,16 @@ function dryMoistureToHumidity(
): number {
let ERH = 0;
switch (eq) {
- case Equation.chungPfost:
+ case pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST:
ERH = chungPfostInverse(celsius, dryMC, a, b, c);
break;
- case Equation.halsey:
+ case pond.MoistureEquation.MOISTURE_EQUATION_HALSEY:
ERH = halseyInverse(celsius, dryMC, a, b, c);
break;
- case Equation.henderson:
+ case pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON:
ERH = hendersonInverse(celsius, dryMC, a, b, c);
break;
- case Equation.oswin:
+ case pond.MoistureEquation.MOISTURE_EQUATION_OSWIN:
ERH = oswinInverse(celsius, dryMC, a, b, c);
break;
default:
diff --git a/src/grain/GrainTransaction.tsx b/src/grain/GrainTransaction.tsx
index f39cdf4..66fd329 100644
--- a/src/grain/GrainTransaction.tsx
+++ b/src/grain/GrainTransaction.tsx
@@ -125,17 +125,19 @@ export default function GrainTransaction(props: Props) {
.then(resp => {
//let sourceOps: Option[] = sourceOptions
let binOps: Option[] = [];
- resp.data.bins.forEach(bin => {
- let b = Bin.create(bin);
- if (mainObject.key() !== b.key()) {
- let op: Option = {
- label: b.name(),
- value: b,
- group: "Bins"
- };
- (!restrictMatching || grainTypesMatch(op.value, mainObject)) && binOps.push(op);
- }
- });
+ if(resp.data.bins){
+ resp.data.bins.forEach(bin => {
+ let b = Bin.create(bin);
+ if (mainObject.key() !== b.key()) {
+ let op: Option = {
+ label: b.name(),
+ value: b,
+ group: "Bins"
+ };
+ (!restrictMatching || grainTypesMatch(op.value, mainObject) || op.value.grainName() === mainObject.grainName()) && binOps.push(op);
+ }
+ });
+ }
setBinOptions([...binOps]);
})
.finally(() => {
@@ -151,17 +153,19 @@ export default function GrainTransaction(props: Props) {
.then(resp => {
//let sourceOps: Option[] = sourceOptions
let bagOps: Option[] = [];
- resp.data.grainBags.forEach(bag => {
- let b = GrainBag.create(bag);
- if (mainObject.key() !== b.key()) {
- let op: Option = {
- label: b.name(),
- value: b,
- group: "Grain Bags"
- };
- (!restrictMatching || grainTypesMatch(op.value, mainObject)) && bagOps.push(op);
- }
- });
+ if(resp.data.grainBags){
+ resp.data.grainBags.forEach(bag => {
+ let b = GrainBag.create(bag);
+ if (mainObject.key() !== b.key()) {
+ let op: Option = {
+ label: b.name(),
+ value: b,
+ group: "Grain Bags"
+ };
+ (!restrictMatching || grainTypesMatch(op.value, mainObject) || op.value.grainName() === mainObject.grainName()) && bagOps.push(op);
+ }
+ });
+ }
setBagOptions([...bagOps]);
})
.finally(() => {
@@ -184,7 +188,7 @@ export default function GrainTransaction(props: Props) {
value: f,
group: "Fields"
};
- (!restrictMatching || grainTypesMatch(op.value, mainObject)) && fieldOps.push(op);
+ (!restrictMatching || grainTypesMatch(op.value, mainObject) || op.value.grainName() === mainObject.grainName()) && fieldOps.push(op);
});
setFieldOptions([...fieldOps]);
})
@@ -356,13 +360,6 @@ export default function GrainTransaction(props: Props) {
if (source.grain() === destination.grain()) {
matching = true;
}
- } else if (source.storage() === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN) {
- if (
- source.customType().toLowerCase() === destination.customType().toLowerCase() &&
- source.bushelsPerTonne() === destination.bushelsPerTonne()
- ) {
- matching = true;
- }
}
}
return matching;
@@ -373,7 +370,7 @@ export default function GrainTransaction(props: Props) {
if (grainTypesMatch(selectedSource.value, selectedDestination.value)) {
updateInventory();
} else {
- //open dialog saying that the destinations grain type will change from this action
+ //open dialog saying that the grain types do not match or because it is a custom type
setGrainChangeDialog(true);
}
} else {
@@ -381,6 +378,36 @@ export default function GrainTransaction(props: Props) {
}
};
+ /**
+ * the function is used to determine what text to display to the user when the grain types dont match
+ * or when it sees any of the objects involved as having a custom grain type
+ * @param source the source of the grain
+ * @param destination the destination of the grain
+ * @returns the text describing the situation
+ */
+ const confirmationText = () => {
+ if(selectedSource && selectedDestination){
+
+ let source = selectedSource?.value
+ let destination = selectedDestination?.value
+ //both are custom
+ if(source.storage() === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN && destination.storage() === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN){
+ return "Both source and destination are using custom grain types, they may not match. Would you like to continue with the transaction?"
+ }
+ //source is custom destination is not
+ if(source.storage() === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN){
+ return "The source is using a custom grain type and may not match the destination. Would you like to continue with the transaction?"
+ }
+ //destination is custom source is not
+ if(destination.storage() === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN){
+ return "The destination is using a custom grain type and may not match the source. Would you like to continue with the transaction?"
+ }
+ }
+
+ //neither are custom
+ return "The Grain types do not match. Are you sure you would like to perform this transaction?"
+ }
+
const grainChange = () => {
return (
- The grain type of the destination may change as a result of this action.
+ {confirmationText()}
= "2.1.9" ? 4 : 2;
+ max = this.versionComparison(this.status.firmwareVersion, "2.1.9") ? 4 : 2;
}
return max
}
private versionComparison(deviceVersion: string, requiredVersion: string): boolean {
+ // Feature explicitly not supported on this platform
+ if (!requiredVersion || requiredVersion === "N/A") {
+ return false;
+ }
+
+ // Device firmware missing / unset
+ if (!deviceVersion || deviceVersion.trim() === "") {
+ return false;
+ }
+
+ //compare the versions
const parse = (v: string) => {
const [core, pre] = v.split("-");
const parts = core.split(".").map(n => parseInt(n, 10));
diff --git a/src/models/Gate.ts b/src/models/Gate.ts
index 8842e68..ccd0e77 100644
--- a/src/models/Gate.ts
+++ b/src/models/Gate.ts
@@ -5,22 +5,24 @@ import { or } from "utils/types";
export class Gate {
public settings: pond.GateSettings = pond.GateSettings.create();
+ public status: pond.GateStatus = pond.GateStatus.create();
public name: string = "Gate";
public key: string = "";
public preferences: any = {};
public gateMutations: any = {};
- public pcaState: pond.PCAState = pond.PCAState.PCA_STATE_UNKNOWN;
+ // public pcaState: pond.PCAState = pond.PCAState.PCA_STATE_UNKNOWN;
public static create(pb?: pond.Gate): Gate {
let my = new Gate();
if (pb) {
let g = pond.Gate.fromObject(pb);
my.settings = pond.GateSettings.fromObject(cloneDeep(or(g.settings, {})));
+ my.status = pond.GateStatus.fromObject(cloneDeep(or(g.status, {})));
my.name = g.name;
my.key = g.key;
my.preferences = g.componentPreferences;
my.gateMutations = g.gateMutations;
- my.pcaState = g.pcaState;
+ // my.pcaState = g.pcaState;
}
return my;
}
@@ -77,7 +79,7 @@ export class Gate {
}
public gateMarkerColour(): string {
- switch (this.pcaState) {
+ switch (this.status.pcaState) {
case pond.PCAState.PCA_STATE_OFF:
return "grey";
case pond.PCAState.PCA_STATE_IN_BOUNDS:
diff --git a/src/navigation/BottomNavigator.tsx b/src/navigation/BottomNavigator.tsx
index 9e36fe8..b03a153 100644
--- a/src/navigation/BottomNavigator.tsx
+++ b/src/navigation/BottomNavigator.tsx
@@ -13,7 +13,7 @@ import BinsIcon from "products/Bindapt/BinsIcon";
import { useGlobalState } from "providers";
import { useCallback, useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
-import { IsAdaptiveAgriculture, isBXT, IsMiVent, IsAdCon, IsOmniAir } from "services/whiteLabel";
+import { IsAdaptiveAgriculture, isBXT, IsMiVent, IsAdCon, IsOmniAir, IsMiPCA } from "services/whiteLabel";
import FieldsIcon from "products/AgIcons/FieldsIcon";
import NexusSTIcon from "products/Construction/NexusSTIcon";
import OmniAirDeviceIcon from "products/AviationIcons/OmniAirDeviceIcon";
@@ -40,6 +40,7 @@ export default function BottomNavigator(props: Props) {
const isMiVent = IsMiVent();
const isAdCon = IsAdCon();
const isOmni = IsOmniAir();
+ const isMiPCA = IsMiPCA();
const reRoute = useCallback(
(path: string) => {
@@ -117,14 +118,14 @@ export default function BottomNavigator(props: Props) {
value="constructionMap"
/>
)}
- {isOmni && (
+ {(isOmni || isMiPCA) && (
}
value="aviationMap"
/>
)}
- {isOmni && (
+ {(isOmni || isMiPCA) && (
}
@@ -139,7 +140,7 @@ export default function BottomNavigator(props: Props) {
) : isAdCon ? (
- ) : isOmni ? (
+ ) : (isOmni || isMiPCA) ? (
) : (
diff --git a/src/navigation/Router.tsx b/src/navigation/Router.tsx
index c54c291..cb97f67 100644
--- a/src/navigation/Router.tsx
+++ b/src/navigation/Router.tsx
@@ -48,6 +48,7 @@ const JohnDeere = lazy(() => import("pages/JohnDeere"));
const CNHi = lazy(() => import("pages/CNHi"));
const LibraCart = lazy(() => import("pages/LibraCart"));
const FieldPage = lazy(()=>import("pages/Field"));
+const GrainsPage = lazy(()=>import("pages/Grains"));
export const appendToUrl = (appendage: number | string) => {
const basePath = location.pathname.replace(/\/$/, "");
@@ -360,6 +361,7 @@ export default function Router() {
{user.hasFeature("libra-cart") &&
} />
}
+ } />
{/* Map routes */}
} />
} />
diff --git a/src/navigation/SideNavigator.tsx b/src/navigation/SideNavigator.tsx
index f3e95a4..6bab36c 100644
--- a/src/navigation/SideNavigator.tsx
+++ b/src/navigation/SideNavigator.tsx
@@ -1,4 +1,4 @@
-import { ChevronRight, Code, Memory, People, Person, SyncAlt, TapAndPlay } from "@mui/icons-material";
+import { ChevronRight, Code, Grain, Memory, People, Person, SyncAlt, TapAndPlay } from "@mui/icons-material";
import ChevronLeft from "@mui/icons-material/ChevronLeft";
import {
darken,
@@ -27,6 +27,7 @@ import {
IsAdaptiveAgriculture,
// hasTutorialPlaylist,
IsAdCon,
+ IsMiPCA,
// isBXT,
IsMiVent,
IsOmniAir,
@@ -166,7 +167,8 @@ export default function SideNavigator(props: Props) {
const isMiVent = IsMiVent();
const isAg = IsAdaptiveAgriculture()
const isStreamline = IsStreamline()
- const isMiPCA = IsOmniAir()
+ const isOmni = IsOmniAir()
+ const isMiPCA = IsMiPCA()
const isAdCon = IsAdCon()
return (
@@ -184,7 +186,7 @@ export default function SideNavigator(props: Props) {
)}
- {(isMiPCA || user.hasFeature("admin")) && (
+ {((isOmni || isMiPCA) || user.hasFeature("admin")) && (
)}
- {(isMiPCA || user.hasFeature("admin")) && (
+ {((isOmni || isMiPCA) || user.hasFeature("admin")) && (
)}
+ {(isAg || isStreamline || user.hasFeature("admin")) && (
+
+ goTo("/grains")}
+ classes={getClasses("/grains")}
+ >
+
+
+
+ {open && }
+
+
+ )}
{(isAdCon || user.hasFeature("admin")) && (
diff --git a/src/objectHeater/ObjectHeaterCard.tsx b/src/objectHeater/ObjectHeaterCard.tsx
index 651ae7c..92e899e 100644
--- a/src/objectHeater/ObjectHeaterCard.tsx
+++ b/src/objectHeater/ObjectHeaterCard.tsx
@@ -109,7 +109,7 @@ export default function ObjectHeaterCard(props: Props) {
});
}
}
- if (heaterData.devices[0]) {
+ if (heaterData.devices && heaterData.devices[0]) {
setConnectedDevice(Device.any(heaterData.devices[0].device));
heaterData.devices[0].components.forEach(c => {
//loop through the components and assign them accordingly
@@ -171,7 +171,7 @@ export default function ObjectHeaterCard(props: Props) {
const goToHeater = () => {
//history.push("/objectHeaters/" + heater.key);
- let path = "/objectHeaters/" + heater.key;
+ let path = "/heaters/" + heater.key;
navigate(path, { state: {heater: heater} });
};
diff --git a/src/objects/objectInteractions/NewObjectInteraction.tsx b/src/objects/objectInteractions/NewObjectInteraction.tsx
index 8e76c25..64a63b0 100644
--- a/src/objects/objectInteractions/NewObjectInteraction.tsx
+++ b/src/objects/objectInteractions/NewObjectInteraction.tsx
@@ -179,7 +179,6 @@ export default function NewObjectInteraction(props: Props){
setNewAlertComponentType(quack.ComponentType.COMPONENT_TYPE_INVALID);
setConditions([]);
setSelectedAlertComponents([]);
- setValidOptions([])
setValidComponents([])
setValStrings(["0", "0"]);
setNumConditions(0)
diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx
index cc667d8..a1dc0e3 100644
--- a/src/pages/Bin.tsx
+++ b/src/pages/Bin.tsx
@@ -819,6 +819,7 @@ export default function Bin(props: Props) {
fans={fans}
heaters={heaters}
grain={bin.grain()}
+ customGrain={bin.customGrain()}
/>
)}
@@ -968,6 +969,7 @@ export default function Bin(props: Props) {
fans={fans}
heaters={heaters}
grain={bin.grain()}
+ customGrain={bin.customGrain()}
/>
)}
diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx
index ee4ae56..0a47a05 100644
--- a/src/pages/Device.tsx
+++ b/src/pages/Device.tsx
@@ -128,10 +128,9 @@ export default function DevicePage() {
interaction.settings.nodeOne > interaction.settings.nodeTwo &&
interaction.settings.nodeTwo !== 0
) {
- //flip operator and send negative comparitor to save
+ //flip operator to display what is actually happening
interaction.settings.conditions.forEach(condition => {
- //coming back from the backend as a string for some reason
- if (condition.comparison.toString() === "RELATIONAL_OPERATOR_GREATER_THAN") {
+ if (condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN) {
condition.comparison = quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN;
} else {
condition.comparison = quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN;
diff --git a/src/pages/Gate.tsx b/src/pages/Gate.tsx
index 3d1a3ea..767bb22 100644
--- a/src/pages/Gate.tsx
+++ b/src/pages/Gate.tsx
@@ -192,7 +192,7 @@ export default function Gate(props: Props) {
const deviceDrawer = () => {
return (
()
+ const [formValid, setFormValid] = useState(false)
+ const [selectedGrain, setSelectedGrain] = useState()
+ const [displayCustom, setDisplayCustom] = useState(false)
+ const { openSnack } = useSnackbar()
+ const [tableSize, setTableSize] = useState(10)
+ const [tablePage, setTablePage] = useState(0)
+ const [tableTotal, setTableTotal] = useState(0)
+ const grainAPI = useGrainAPI()
+ const userAPI = useUserAPI()
+ const [{as, user}] = useGlobalState();
+ const [permissions, setPermissions] = useState([])
+ const [openRemoveConfirmation, setOpenRemoveConfirmation] = useState(false)
+ const [openUpdateConfirmation, setOpenUpdateConfirmation] = useState(false)
+ const [grainObjectTableData, setGrainObjectTableData] = useState([])
+ const [supportedGrainTableData, setSupportedGrainTableData] = useState([])
+ // const [selectedGrainExtension, setSelectedGrainExtension] = useState()
+ const [accordionKey, setAccordionKey] = useState("allClosed")
+ const [openNewGrainDialog, setOpenNewGrainDialog] = useState(false)
+ const isMobile = useMobile()
+ const extraLoad = 10 //the amount of more to load on the mobile view when load more is clicked
+
+ //function to load the first set of custom grains
+ const loadCustomGrain = useCallback(() => {
+ grainAPI.listGrains(tableSize, tablePage * tableSize, "asc", "name").then(resp => {
+ setGrainObjectTableData(resp.data.grains)
+ setTableTotal(resp.data.total)
+ }).catch(err => {
+ openSnack("There was an issue loading your custom grains")
+ })
+ },[tableSize, tablePage])
+
+ useEffect(() => {
+ //load the custom grains
+ if(displayCustom){
+ loadCustomGrain()
+ }else{
+ let displayedGrains: GrainExtension[] = []
+ let offset = tablePage * tableSize
+ let limit = tableSize
+ let grainList = GetValidGrainExtensions()
+ let sortedGrain = grainList.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1))
+ sortedGrain.forEach((ext, i) => {
+ if(i >= offset && displayedGrains.length < limit){
+ displayedGrains.push(ext)
+ }
+ })
+ setSupportedGrainTableData(displayedGrains)
+ setTableTotal(sortedGrain.length)
+ }
+ },[loadCustomGrain, tableSize, tablePage, displayCustom, as])
+
+ useEffect(()=>{
+ //if the user is viewing as a team get their permissions to the team, if they are not then we will get their permission to the grain type individually when a type is selected
+ if(as){
+ userAPI.getUser(user.id(), { key: as, kind: "team" } as Scope).then(resp => {
+ setPermissions(resp.permissions);
+ });
+ }
+ },[as, userAPI])
+
+ const equationName = (enumVal: number) => {
+ switch(enumVal){
+ case pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST:
+ return "Chung-Pfost"
+ case pond.MoistureEquation.MOISTURE_EQUATION_HALSEY:
+ return "Halsey"
+ case pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON:
+ return "Henderson"
+ case pond.MoistureEquation.MOISTURE_EQUATION_OSWIN:
+ return "Oswin"
+ default:
+ return "None"
+ }
+ }
+
+ //function to update
+
+ //table that will display the custom grains created by the user
+ const grainObjectTable = () => {
+ return (
+
+ rows={grainObjectTableData}
+ noDataMessage="No Grain Types Found. Add a new custom grain to see them here"
+ loadMore={() => {
+ setTableSize(tableSize + extraLoad)
+ }}
+ renderMobile={(row) => {
+ //close it if it is already open
+ if(accordionKey === row.key){
+ setAccordionKey("allClosed")
+ }else{
+ setAccordionKey(row.key)
+ }
+ setCurrentCustomGrain(row.settings ?? pond.GrainSettings.create())
+ }}>
+ }>{row.name}
+
+ {customGrainDisplay()}
+
+ }
+ onRowClick={(grainObj) => {
+ setSelectedGrain(grainObj)
+ setFormValid(true)
+ /** if they are viewin as a team leave the permissions alone, if not then use the users permissions to the grain type,
+ * for the moment grain types are not shareable so this will be unused, but this will cover our bases if we want to implement sharing in the future
+ * */
+ if (!as) {
+ userAPI.getUser(user.id(), { key: grainObj.key, kind: "grain" } as Scope).then(resp => {
+ setPermissions(resp.permissions);
+ });
+ }
+ }}
+ columns={[
+ {
+ title: "Name",
+ render: row =>
+
+ {row.name}
+
+
+ },
+ {
+ title: "Group",
+ render: row =>
+
+ {row.settings?.group}
+
+
+ },
+ {
+ title: "Equation",
+ render: row =>
+
+ {equationName(row.settings?.equation ?? 0)}
+
+
+ }
+ ]}
+ page={tablePage}
+ pageSize={tableSize}
+ handleRowsPerPageChange={(e) => {
+ setTableSize(e.target.value)
+ }}
+ setPage={(page) => {
+ setTablePage(page)
+ }}
+ total={tableTotal}
+ />
+ )
+ }
+
+ //table that will display the grain extensions provided by us
+ const supportedGrainTable = () => {
+ return (
+
+ rows={supportedGrainTableData}
+ page={tablePage}
+ // onRowClick={(ext) => {setSelectedGrainExtension(ext)}}
+ loadMore={() => {
+ setTableSize(tableSize + extraLoad)
+ }}
+ renderMobile={(row) =>
+ }>{row.name}
+
+ {supportedDisplay(row)}
+
+ }
+ pageSize={tableSize}
+ columns={[
+ {
+ title: "Name",
+ render: row =>
+
+ {row.name}
+
+
+ },
+ {
+ title: "Group",
+ render: row =>
+
+ {row.group}
+
+
+ },
+ {
+ title: "Equation",
+ render: row =>
+
+ {equationName(row.equation ?? 0)}
+
+
+ },
+ {
+ title: "Kg Per Bushel",
+ render: row =>
+
+ {row.weightConversionKg.toFixed(2)}
+
+
+ },
+ {
+ title: "Bushels Per Tonne",
+ render: row =>
+
+ {row.bushelsPerTonne.toFixed(2)}
+
+
+ }
+ ]}
+ handleRowsPerPageChange={(e) => {
+ setTableSize(e.target.value)
+ }}
+ setPage={(page) => {
+ setTablePage(page)
+ }}
+ total={tableTotal}
+ />
+ )
+ }
+
+ //the grain display for supported grain type to show the rest of the data such as what constants are used in the formula
+ const supportedDisplay = (grain?: GrainExtension) => {
+ return (
+
+ {grain ?
+
+
+ Info
+
+
+
+ Name: {grain.name}
+ Group: {grain.group}
+
+
+ Formula
+
+
+
+ Equation: {equationName(grain.equation)}
+ {/* Constant A: {grain.a}
+ Constant B: {grain.b}
+ Constant C: {grain.c} */}
+
+
+ Conversions
+
+
+
+ Kg per Bushel: {grain.weightConversionKg.toFixed(2)}
+ Bushels per Tonne: {grain.bushelsPerTonne.toFixed(2)}
+
+
+ :
+ Select a Grain from the table to view its details
+ }
+
+ )
+ }
+
+ const updateGrain = () => {
+ if(selectedGrain && currentCustomGrain){
+ grainAPI.updateGrain(selectedGrain.key, currentCustomGrain).then(resp => {
+ openSnack("Grain type updated")
+ }).catch(err => {
+ openSnack("There was an issue updating the selected grain type")
+ }).finally(() => {
+ setOpenUpdateConfirmation(false)
+ loadCustomGrain()
+ })
+ }
+ }
+ const updateGrainConfirmation = () => {
+ return (
+ {setOpenUpdateConfirmation(false)}}>
+ Update Custom Grain
+
+ Update existing custom grain type?
+
+ Note that updating the grain here will not change any bins or components this type was used for, those will retain the old settings.
+
+
+ {setOpenUpdateConfirmation(false)}}>Cancel
+ {updateGrain()}}>Confirm
+
+
+ )
+ }
+
+ //because the form is embedded on desktop this dialog is specifically for mobile view
+ const newGrainDialog = () => {
+ return (
+ setOpenNewGrainDialog(false)}>
+ Add New Grain
+
+ {customGrainDisplay()}
+
+
+
+ )
+ }
+
+ const addNewGrain = () => {
+ if(currentCustomGrain){
+ grainAPI.addGrain(currentCustomGrain).then(resp => {
+ //new grain added
+ openSnack("New grain type added")
+ }).catch(err => {
+ openSnack("There was a problem creating the new grain type")
+ }).finally(() => {
+ loadCustomGrain()
+ setOpenNewGrainDialog(false)//only for the mobile view since the desktop has it embedded
+ })
+ }
+ }
+
+ const removeGrain = () => {
+ if(selectedGrain){
+ grainAPI.removeGrain(selectedGrain.key).then(resp => {
+ openSnack("Grain type removed")
+ }).catch(err => {
+ openSnack("There was a problem removing the selected grain type")
+ }).finally(() => {
+ setOpenRemoveConfirmation(false)
+ loadCustomGrain()
+ })
+ }
+ }
+
+
+ const removeConfirmation = () => {
+ return (
+ {setOpenRemoveConfirmation(false)}}>
+ Remove Custom Grain
+
+ Remove custom grain type?
+
+ Note that removing the grain here will not change any bins or components this type was used for, those will retain the old settings.
+
+
+ {setOpenRemoveConfirmation(false)}}>Cancel
+ {removeGrain()}}>Confirm
+
+
+ )
+ }
+
+ const customGrainActions = () => {
+ return (
+
+
+ {selectedGrain &&
+ {setOpenRemoveConfirmation(true)}}>Remove Grain
+ }
+
+
+ {selectedGrain &&
+ setOpenUpdateConfirmation(true)} disabled={!formValid || !permissions.includes(pond.Permission.PERMISSION_WRITE)} variant="contained" color="primary">Update
+ }
+ {isMobile && !selectedGrain && {setOpenNewGrainDialog(false)}}>Cancel }
+ addNewGrain()} disabled={!formValid || (as !== "" && !permissions.includes(pond.Permission.PERMISSION_WRITE))} variant="contained" color="primary">Save New
+
+
+ )
+ }
+
+ //the grain display for custom grain the user has defined
+ const customGrainDisplay = () => {
+ return (
+
+ {
+ setCurrentCustomGrain(newSettings)
+ setFormValid(formValid)
+ }}
+ />
+ {customGrainActions()}
+
+ )
+ }
+
+ const desktopView = () => {
+ if(displayCustom){
+ return (
+
+
+ {grainObjectTable()}
+
+
+ {customGrainDisplay()}
+
+
+ )
+ }else{
+ return (
+
+ {supportedGrainTable()}
+
+ )
+ }
+ }
+
+ const mobileView = () => {
+ return (
+
+ {displayCustom ? grainObjectTable() : supportedGrainTable()}
+
+ )
+ }
+
+ return (
+
+
+
+ Adaptive Grains
+
+
+ {
+ setDisplayCustom(false)
+ setTablePage(0)
+ }
+ },
+ {
+ title: "Custom",
+ function: () => {
+ setDisplayCustom(true)
+ setTablePage(0)
+ }
+ }
+ ]}
+ />
+ {isMobile && displayCustom &&
+ {
+ setSelectedGrain(undefined)
+ setCurrentCustomGrain(undefined)
+ setOpenNewGrainDialog(true)
+ }}>
+
+
+ }
+
+ {!isMobile ? desktopView() : mobileView()}
+
+ {updateGrainConfirmation()}
+ {removeConfirmation()}
+ {newGrainDialog()}
+
+ )
+}
\ No newline at end of file
diff --git a/src/pages/Site.tsx b/src/pages/Site.tsx
index 2685f4d..b09de66 100644
--- a/src/pages/Site.tsx
+++ b/src/pages/Site.tsx
@@ -56,8 +56,8 @@ export default function Site() {
siteAPI
.getSitePage(siteKey, as)
.then(resp => {
- setSite(ISite.any(resp.data.site));
- setHeaters(resp.data.heaterData);
+ if(resp.data.site) setSite(ISite.any(resp.data.site));
+ if(resp.data.heaterData) setHeaters(resp.data.heaterData);
setLoadingSite(false);
})
.catch(err => {
diff --git a/src/pages/Terminals.tsx b/src/pages/Terminals.tsx
index dd90aac..94464d5 100644
--- a/src/pages/Terminals.tsx
+++ b/src/pages/Terminals.tsx
@@ -182,7 +182,6 @@ export default function Terminals(props: Props) {
if(loading) return
setLoading(true)
terminalAPI.listTerminals(200, 0, "asc", "name", undefined, as).then(resp => {
- console.log(resp.data)
if(resp.data.terminals){
setTerminals(resp.data.terminals.map(a => Terminal.any(a)));
}
diff --git a/src/providers/pond/gateAPI.tsx b/src/providers/pond/gateAPI.tsx
index 5b07606..ac63bb0 100644
--- a/src/providers/pond/gateAPI.tsx
+++ b/src/providers/pond/gateAPI.tsx
@@ -138,6 +138,7 @@ export default function GateProvider(props: PropsWithChildren) {
return new Promise>((resolve, reject) => {
get(url).then(resp => {
resp.data = pond.ListGatesResponse.fromObject(resp.data)
+ console.log(resp)
return resolve(resp)
}).catch(err => {
return reject(err)
diff --git a/src/providers/pond/grainAPI.tsx b/src/providers/pond/grainAPI.tsx
new file mode 100644
index 0000000..0df0055
--- /dev/null
+++ b/src/providers/pond/grainAPI.tsx
@@ -0,0 +1,137 @@
+import { useHTTP } from "hooks";
+import { createContext, PropsWithChildren, useContext } from "react";
+import { pond } from "protobuf-ts/pond";
+import { pondURL } from "./pond";
+import { AxiosResponse } from "axios";
+import { useGlobalState } from "providers";
+import { or } from "utils";
+
+export interface IGrainInterface {
+ addGrain: (
+ settings: pond.GrainSettings,
+ otherTeam?: string
+ ) => Promise>;
+ getGrain: (key: string, otherTeam?: string) => Promise>
+ listGrains: (
+ limit: number,
+ offset: number,
+ order?: "asc" | "desc",
+ orderBy?: string,
+ search?: string,
+ keys?: string[],
+ types?: string[],
+ otherTeam?: string,
+ ) => Promise>;
+ updateGrain: (key: string, settings: pond.GrainSettings, otherTeam?: string) => Promise>
+ removeGrain: (key: string, otherTeam?: string) => Promise>;
+}
+
+export const GrainAPIcontext = createContext({} as IGrainInterface);
+
+interface Props {}
+
+export default function GrainProvider(props: PropsWithChildren) {
+ const { children } = props;
+ const { get, del, post, put } = useHTTP();
+ const [{ as }] = useGlobalState();
+
+ //add
+ const addGrain = (settings: pond.GrainSettings, otherTeam?: string) => {
+ const view = otherTeam ? otherTeam : as
+ if (view) {
+ return post(
+ pondURL("/grains?&as=" + view),
+ settings
+ );
+ }
+ return post(pondURL("/grains"), settings);
+ };
+
+ //get
+ const getGrain = (key: string, otherTeam?: string) => {
+ const view = otherTeam ? otherTeam : as
+ const url = "/grains/" + key + (view ? "?as=" + view : "")
+ return new Promise>((resolve, reject)=>{
+ get(pondURL(url))
+ .then(resp => {
+ if (resp.data.grain){
+ resp.data.grain = pond.GrainObject.fromObject(resp.data.grain)
+ }
+ resolve(resp)
+ })
+ .catch(err => {
+ reject(err)
+ })
+ })
+ }
+ //list
+ const listGrains = (
+ limit: number,
+ offset: number,
+ order?: "asc" | "desc",
+ orderBy?: string,
+ search?: string,
+ keys?: string[],
+ types?: string[],
+ otherTeam?: string
+ ) => {
+ return new Promise>((resolve, reject)=>{
+ const view = otherTeam ? otherTeam : as
+ get(
+ pondURL(
+ "/grains?limit=" +
+ limit +
+ "&offset=" +
+ offset +
+ ("&order=" + or(order, "asc")) +
+ ("&by=" + or(orderBy, "key")) +
+ (search ? "&search=" + search : "") +
+ (keys ? "&keys=" + keys.join(",") : "") +
+ (types ? "&types=" + types.join(",") : "") +
+ (view ? "&as=" + view : "")
+ )
+ ).then(resp => {
+ resp.data = pond.ListGrainsResponse.fromObject(resp.data);
+ return resolve(resp)
+ }).catch(err => {
+ return reject(err)
+ })
+ })
+ };
+
+ //update
+ const updateGrain = (key: string, settings: pond.GrainSettings, otherTeam?: string) => {
+ const view = otherTeam ? otherTeam : as
+ if (view) {
+ return put(
+ pondURL("/grains/"+ key + "?&as=" + view),
+ settings
+ );
+ }
+ return put(pondURL("/grains/" + key), settings);
+ }
+
+ //remove
+ const removeGrain = (key: string, otherTeam?: string) => {
+ const view = otherTeam ? otherTeam : as
+ if (view) {
+ return del(pondURL("/grains/" + key + "?as=" + view));
+ }
+ return del(pondURL("/grains/" + key));
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const useGrainAPI = () => useContext(GrainAPIcontext);
\ No newline at end of file
diff --git a/src/providers/pond/interactionsAPI.tsx b/src/providers/pond/interactionsAPI.tsx
index 26afb1e..5fa3705 100644
--- a/src/providers/pond/interactionsAPI.tsx
+++ b/src/providers/pond/interactionsAPI.tsx
@@ -36,6 +36,7 @@ export interface IInteractionsAPIContext {
alerts: pond.AlertData[],
otherTeam?: string
) => Promise>;
+ clearInteractions: (device: number, sources?: quack.ComponentID[], otherTeam?: string) => Promise>
}
export const InteractionsAPIContext = createContext(
@@ -285,6 +286,25 @@ export default function InteractionProvider(props: PropsWithChildren) {
})
};
+ const clearInteractions = (device: number, sources?: quack.ComponentID[], otherTeam?: string): Promise> => {
+ const view = otherTeam ? otherTeam : as
+ let sourceArray: string[] = []
+ if (sources){
+ sources.forEach(source => {
+ sourceArray.push(componentIDToString(source))
+ })
+ }
+ let url = pondURL("/devices/"+ device + "/interactions/clear" + (view ? "?as=" + view : "") + (sources ? "&sources=" + sourceArray.toString() : ""))
+ return new Promise>((resolve, reject) => {
+ post(url).then(resp => {
+ resp.data = pond.ClearInteractionsResponse.fromObject(resp.data)
+ return resolve(resp)
+ }).catch(err => {
+ return reject(err)
+ })
+ })
+ }
+
return (
) {
updateInteractionPondSettings,
removeInteraction,
listInteractionsByDevice,
- listInteractionsByComponent
+ listInteractionsByComponent,
+ clearInteractions
}}>
{children}
diff --git a/src/providers/pond/pond.tsx b/src/providers/pond/pond.tsx
index abe21ef..6bbb411 100644
--- a/src/providers/pond/pond.tsx
+++ b/src/providers/pond/pond.tsx
@@ -38,6 +38,7 @@ import JohnDeereProvider, { useJohnDeereProxyAPI } from "./johnDeereProxyAPI";
import LibraCartProvider, { useLibraCartProxyAPI } from "./libracartProxyAPI";
import CNHiProvider, { useCNHiProxyAPI } from "./cnhiProxyAPI";
import DevicePresetProvider, { useDevicePresetAPI } from "./devicePresetAPI";
+import GrainProvider, { useGrainAPI } from "./grainAPI";
// import NoteProvider from "providers/noteAPI";
export const pondURL = (partial: string, demo: boolean = false): string => {
@@ -93,7 +94,9 @@ export default function PondProvider(props: PropsWithChildren) {
- {children}
+
+ {children}
+
@@ -173,5 +176,6 @@ export {
useJohnDeereProxyAPI,
useCNHiProxyAPI,
useLibraCartProxyAPI,
- useDevicePresetAPI
+ useDevicePresetAPI,
+ useGrainAPI
};
diff --git a/src/services/whiteLabel.ts b/src/services/whiteLabel.ts
index 0cd84cf..d9aac95 100644
--- a/src/services/whiteLabel.ts
+++ b/src/services/whiteLabel.ts
@@ -10,7 +10,7 @@ import AeroGrowDarkLogo from "../assets/whitelabels/AeroGrow/darkLogo.png";
import AeroGrowLightLogo from "../assets/whitelabels/AeroGrow/lightLogo.png";
import MiVentLightLogo from "../assets/whitelabels/MiVent/lightLogo.png";
// import OmniAirLogo from "../assets/whitelabels/OmniAir/OmniAirLogo.png";
-import MiPCALogo from "../assets/whitelabels/OmniAir/MiPCALogo.png";
+import MiPCALogo from "../assets/whitelabels/MiPCA/MiPCALogo.png";
import StreamlineLogo from "../assets/whitelabels/Streamline/stream-logo.png"
// import { green, yellow } from "@mui/material/colors";
@@ -284,6 +284,32 @@ export function IsOmniAir(): boolean {
);
}
+const MIPCA_WHITE_LABEL: WhiteLabel = {
+ name: "MiPCA",
+ primaryColour: "#004f9b",
+ secondaryColour: "yellow",
+ signatureColour: "#272727",
+ signatureAccentColour: "#fff",
+ //omni air and MiPCA are the same client ID in Auth0, it is to replace it, once omniair gets removed we can re-name this
+ auth0ClientId: import.meta.env.VITE_AUTH0_OMNIAIR_CLIENT_ID,
+ redirectOnLogout: true,
+ logoutRedirectTarget: "https://mionetech.com",
+ darkLogo: MiPCALogo,
+ lightLogo: MiPCALogo,
+ transparentLogoBG: true,
+ blacklist: ["cost"],
+ docs: "MiPCA",
+ protips: protips.concat([])
+};
+
+export function IsMiPCA(): boolean {
+ return (
+ getName() === "MiPCA" ||
+ window.location.origin.includes("staging") ||
+ window.location.origin.includes("localhost")
+ );
+}
+
const whitelabels = new Map([
["streamline", STREAMLINE_WHITE_LABEL],
["adaptiveag", ADAPTIVE_AGRICULTURE_WHITE_LABEL],
@@ -296,7 +322,8 @@ const whitelabels = new Map([
["10.0", ADAPTIVE_CONSTRUCTION_WHITE_LABEL],
["mivent", MIVENT_WHITE_LABEL],
["adaptiveconstruction", ADAPTIVE_CONSTRUCTION_WHITE_LABEL],
- ["omniair", OMNIAIR_WHITE_LABEL]
+ ["omniair", OMNIAIR_WHITE_LABEL],
+ ["mipca", MIPCA_WHITE_LABEL]
]);
export function getWhitelabel(): WhiteLabel {
@@ -309,7 +336,7 @@ export function getWhitelabel(): WhiteLabel {
return BXT_WHITE_LABEL;
}
if (window.location.origin.includes("localhost")) {
- return STREAMLINE_WHITE_LABEL;
+ return MIPCA_WHITE_LABEL;
}
if (window.location.origin.includes("staging") || import.meta.env.VITE_LOCAL_STAGING=='true') {
return STAGING_WHITELABEL;