Merge branch 'hdc302_component' into dev_environment

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

View file

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

View file

@ -1738,32 +1738,34 @@ export default function BinVisualizer(props: Props) {
{modeChangeInProgress &&
<CircularProgress color="primary" size={25}/>
}
<ButtonGroup
disableAll={modeChangeInProgress}
buttons={[
{
title: "Storage",
function: () => {
setModeStorage()
<Box id="tour-bin-mode">
<ButtonGroup
disableAll={modeChangeInProgress}
buttons={[
{
title: "Storage",
function: () => {
setModeStorage()
}
},
{
title: "Cooldown",
function: () => {
setModeCooldown()
}
},
{
title: bin.settings.inventory && bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? "Hydrating" : "Drying",
function: () => {
// setShowInputMoisture(true)
setModeConditioning() //will set it to either drying or hydrating based on the initial and target moisture
}
}
},
{
title: "Cooldown",
function: () => {
setModeCooldown()
}
},
{
title: bin.settings.inventory && bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? "Hydrating" : "Drying",
function: () => {
// setShowInputMoisture(true)
setModeConditioning() //will set it to either drying or hydrating based on the initial and target moisture
}
}
]}
toggledButtons={determineToggle(mode)}
toggle
/>
]}
toggledButtons={determineToggle(mode)}
toggle
/>
</Box>
</Box>
);
};

View file

@ -57,6 +57,7 @@ interface Props {
* When true will disable all of the buttons in the group, will be overridded by individual buttons disabled state in the button data
*/
disableAll?: boolean
}
const useStyles = makeStyles((theme: Theme) => {

View file

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

View file

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

View file

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

View file

@ -181,6 +181,59 @@ export class Device {
return max
}
private versionComparison(deviceVersion: string, requiredVersion: string): boolean {
const parse = (v: string) => {
const [core, pre] = v.split("-");
const parts = core.split(".").map(n => parseInt(n, 10));
let preLabel = "";
let preNum = 0;
if (pre) {
const match = pre.match(/^([a-zA-Z]+)(\d*)$/);
if (match) {
preLabel = match[1].toLowerCase();
preNum = match[2] ? parseInt(match[2], 10) : 0;
}
}
return { parts, preLabel, preNum };
};
const rank = (label: string): number => {
switch (label) {
case "alpha": return 1;
case "beta": return 2;
case "rc": return 3;
default: return 0; // release
}
};
const a = parse(deviceVersion);
const b = parse(requiredVersion);
// Compare major/minor/patch
const len = Math.max(a.parts.length, b.parts.length);
for (let i = 0; i < len; i++) {
const av = a.parts[i] ?? 0;
const bv = b.parts[i] ?? 0;
if (av > bv) return true;
if (av < bv) return false;
}
// Handle prerelease vs release
if (!a.preLabel && b.preLabel) return true; // release > prerelease
if (a.preLabel && !b.preLabel) return false;
// Both prereleases → compare rank
if (a.preLabel !== b.preLabel) {
return rank(a.preLabel) >= rank(b.preLabel);
}
// Same prerelease label → compare numeric suffix
return a.preNum >= b.preNum;
}
public featureSupported(feature: string): boolean {
let versions = featureVersions.get(feature);
if (!versions) {
@ -188,27 +241,27 @@ export class Device {
}
switch (this.settings.platform) {
case pond.DevicePlatform.DEVICE_PLATFORM_PHOTON:
return this.status.firmwareVersion >= versions.photon;
return this.versionComparison(this.status.firmwareVersion, versions.photon);
case pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON:
return this.status.firmwareVersion >= versions.electron;
return this.versionComparison(this.status.firmwareVersion, versions.electron);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR:
return this.status.firmwareVersion >= versions.v2Cell;
return this.versionComparison(this.status.firmwareVersion, versions.v2Cell);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI:
return this.status.firmwareVersion >= versions.v2Wifi;
return this.versionComparison(this.status.firmwareVersion, versions.v2Wifi);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_S3:
return this.status.firmwareVersion >= versions.v2WifiS3;
return this.versionComparison(this.status.firmwareVersion, versions.v2WifiS3);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_S3:
return this.status.firmwareVersion >= versions.v2CellS3;
return this.versionComparison(this.status.firmwareVersion, versions.v2CellS3);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLACK:
return this.status.firmwareVersion >= versions.v2CellBlack;
return this.versionComparison(this.status.firmwareVersion, versions.v2CellBlack);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_GREEN:
return this.status.firmwareVersion >= versions.v2CellGreen;
return this.versionComparison(this.status.firmwareVersion, versions.v2CellGreen);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE:
return this.status.firmwareVersion >= versions.v2WifiBlue;
return this.versionComparison(this.status.firmwareVersion, versions.v2WifiBlue);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE:
return this.status.firmwareVersion >= versions.v2CellBlue;
return this.versionComparison(this.status.firmwareVersion, versions.v2CellBlue);
case pond.DevicePlatform.DEVICE_PLATFORM_V2_ETHERNET_BLUE:
return this.status.firmwareVersion >= versions.v2EthBlue;
return this.versionComparison(this.status.firmwareVersion, versions.v2EthBlue);
default:
return false;
}

View file

@ -1,6 +1,7 @@
import { pond } from "protobuf-ts/pond";
import { or } from "utils/types";
import { cloneDeep } from "lodash";
import { Result } from "pbHelpers/Enums";
export class Interaction {
public settings: pond.InteractionSettings = pond.InteractionSettings.create();
@ -53,6 +54,18 @@ export class Interaction {
return subtype;
}
//these two are not mutually exclusive, it is possible to be an alert and a control
public isAlert(): boolean {
if(this.settings.result?.type === Result.report) return true
if(this.settings.notifications?.reports && this.settings.notifications.notify) return true
return false
}
public isControl(): boolean {
if(this.settings.result?.type !== Result.report) return true //as of right now all interactions that are not report are some sort of control
return false
}
public static upToSubtype(nodeNumber: number): number {
let subtype = 0;
switch (nodeNumber) {

View file

@ -0,0 +1,89 @@
import { Add, ExpandMore } from "@mui/icons-material";
import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Grid2, IconButton, List, ListItem, ListSubheader, Typography } from "@mui/material";
import { Component } from "models";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { pond, quack } from "protobuf-ts/pond";
import React, { useState } from "react";
import NewObjectInteraction from "./NewObjectInteraction";
import { Option } from "common/SearchSelect";
export interface Alert {
conditions: pond.InteractionCondition[];
sourceComponents: Component[];
}
interface Props {
alerts: Alert[]
permissions: pond.Permission[]
addNew?: () => void
}
export default function Alerts(props: Props){
const {alerts, addNew, permissions} = props
// const [openNewAlert, setOpenNewAlert] = useState(false)
const readableCondition = (condition: pond.InteractionCondition) => {
let describer = describeMeasurement(condition.measurementType);
let type = describer.label();
let comparison =
condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO
? "Exactly"
: condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
? "Above"
: "Below";
let value = describer.toDisplay(condition.value);
return (
<Box>
<Typography>{type + " " + comparison + " " + value + describer.GetUnit()}</Typography>
</Box>
);
};
const alertAccordion = (alert: Alert) => {
return (
<Accordion style={{ width: "100%" }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Grid2 container direction="column">
{alert.conditions.map((condition, i) => (
<Grid2 key={i}>
{readableCondition(condition)}
</Grid2>
))}
</Grid2>
</AccordionSummary>
<AccordionDetails>
<List>
<ListSubheader>Reporting Components</ListSubheader>
{alert.sourceComponents.map((comp, i) => (
<ListItem key={i}>{comp.name()}</ListItem>
))}
</List>
</AccordionDetails>
</Accordion>
);
};
return (
<React.Fragment>
<List style={{ margin: 5 }}>
<ListSubheader sx={{paddingY: 1, display: "flex", justifyContent: "space-between", alignItems: "center"}}>
<Typography>
Alerts
</Typography>
{addNew &&
<Button
disabled={!permissions.includes(pond.Permission.PERMISSION_WRITE)}
onClick={()=>{addNew()}}
variant="contained"
color="primary">
<Add /> Alert
</Button>
}
</ListSubheader>
{alerts.map((alert, i) => (
<ListItem key={i}>{alertAccordion(alert)}</ListItem>
))}
</List>
</React.Fragment>
);
}

View file

@ -0,0 +1,152 @@
import { Add, ExpandMore } from "@mui/icons-material";
import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Divider, IconButton, List, ListItem, ListSubheader, Menu, MenuItem, Typography } from "@mui/material";
import { Component, Device, Interaction } from "models";
import { interactionResultText } from "pbHelpers/Interaction";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { pond, quack } from "protobuf-ts/pond";
import React, { useEffect, useState } from "react";
export interface Control {
conditions: pond.InteractionCondition[];
sourceComponents: Component[];
result: pond.InteractionResult | null;
sinkComponent: Component
device: Device
}
interface Props {
devices: Device[]
controls: Control[]
permissions: pond.Permission[]
addNew?: (device: Device) => void
}
export default function Controls(props: Props){
const { devices, controls, addNew, permissions } = props
const [deviceControls, setDeviceControls] = useState<Map<number, Control[]>>(new Map())
const [openDeviceMenu, setOpenDeviceMenu] = useState(false)
const [anchor, setAnchor] = useState<null | HTMLElement>(null)
useEffect(()=>{
let map: Map<number, Control[]> = new Map()
controls.forEach(control => {
let mappedControls = map.get(control.device.id())
if(mappedControls){
mappedControls.push(control)
}else{
map.set(control.device.id(), [control])
}
})
setDeviceControls(map)
},[controls])
const readableConditions = (control: Control) => {
let conditions: string = ""
let firstSource = control.sourceComponents[0]
control.conditions.forEach((condition, index) => {
let describer = describeMeasurement(condition.measurementType, firstSource.type(), firstSource.subType());
let type = describer.label();
let comparison =
condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO
? "Exactly"
: condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
? "Above"
: "Below";
let value = describer.toDisplay(condition.value);
conditions = conditions + (index !== 0 ? " and " : "") + type + " " + comparison + " " + value + describer.GetUnit()
})
return conditions
};
const controlAccordion = (control: Control) => {
let tempInteraction = Interaction.create()
tempInteraction.settings.result = control.result
let resultText = interactionResultText(tempInteraction, control.sinkComponent)
return (
<Accordion sx={{width: "100%"}}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Box>
<Typography>
{resultText}:
</Typography>
<Typography>
{readableConditions(control)}
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<List>
<ListSubheader>Controlling Components</ListSubheader>
{control.sourceComponents.map((comp, i) => (
<ListItem key={i}>{comp.name()}</ListItem>
))}
</List>
</AccordionDetails>
</Accordion>
)
}
const deviceMenu = () => {
return (
<Menu
open={openDeviceMenu}
anchorEl={anchor}
onClose={() => {
setAnchor(null)
setOpenDeviceMenu(false)
}}>
{devices.map(dev => (
<MenuItem
key={dev.id()}
onClick={() => {
setAnchor(null)
setOpenDeviceMenu(false)
if (addNew) addNew(dev)
}}>
{dev.name()}
</MenuItem>
))}
</Menu>
)
}
return (
<React.Fragment>
{deviceMenu()}
<List style={{ margin: 5 }}>
<ListSubheader sx={{paddingY: 1, display: "flex", justifyContent: "space-between", alignItems: "center"}}>
<Typography>
Controls
</Typography>
{addNew &&
<Button
disabled={!permissions.includes(pond.Permission.PERMISSION_WRITE)}
variant="contained"
onClick={(e)=>{
setOpenDeviceMenu(true)
setAnchor(e.currentTarget)
}}
color="primary">
<Add />Control
</Button>
}
</ListSubheader>
{devices.map(device => (
<React.Fragment key={device.id()}>
<Typography sx={{paddingY: 1, paddingX: 2}}>{device.name()}</Typography>
<Divider />
{deviceControls.get(device.id()) ? deviceControls.get(device.id())?.map((control, i) => (
<ListItem key={i}>{controlAccordion(control)}</ListItem>
))
:
<Typography sx={{paddingY: 1, paddingX: 2}}>No Control Interactions For Device</Typography>
}
</React.Fragment>
))}
</List>
</React.Fragment>
)
}

View file

@ -0,0 +1,111 @@
import { Button, List, ListItem, ListSubheader, Typography } from "@mui/material";
import { makeStyles } from "@mui/styles";
import moment from "moment";
import { pond } from "protobuf-ts/pond";
import { useGlobalState, useNotificationAPI } from "providers";
import React, { useEffect, useRef, useState } from "react";
import { getThemeType } from "theme/themeType";
interface Props {
objectKey: string
objectType: pond.ObjectType
}
const useStyles = makeStyles(() => {
return ({
dark: {
backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)",
padding: 5
},
light: {
backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)",
padding: 5
},
})
})
export default function Notifications(props: Props){
const {objectKey, objectType} = props
const classes = useStyles();
const [recentNotifications, setRecentNotifications] = useState<pond.Notification[]>([]);
const [notificationsLoading, setNotificationsLoading] = useState(false)
const loadingRef = useRef(false)
const [{as}] = useGlobalState();
const notificationAPI = useNotificationAPI();
const [totalNotifications, setTotalNotifications] = useState(0)
//get the notifications for the components on an object
useEffect(() => {
if (loadingRef.current) return
loadingRef.current = true
notificationAPI
.listObjectNotifications(objectKey, objectType, 10, 0, undefined, undefined, undefined, as)
.then(resp => {
setRecentNotifications(resp.data.notifications);
setTotalNotifications(resp.data.total);
})
.catch(_err => {})
.finally(() => {
loadingRef.current = true;
});
}, [objectKey, objectType, notificationAPI]);
const loadMoreNotifications = () => {
let current = recentNotifications;
setNotificationsLoading(true);
notificationAPI
.listObjectNotifications(
objectKey,
objectType,
10,
current.length,
undefined,
undefined,
undefined,
as
)
.then(resp => {
if (resp.data.notifications.length > 0) {
let c = current.concat(resp.data.notifications);
setRecentNotifications([...c]);
}
})
.catch(_err => {})
.finally(() => {
setNotificationsLoading(false);
});
};
return (
<List style={{ margin: 5 }}>
<ListSubheader>
<Typography sx={{paddingY: 2}}>
Notifications
</Typography>
</ListSubheader>
{recentNotifications && (
<React.Fragment>
{recentNotifications.map((notification, i) => (
<ListItem key={i} className={i % 2 === 0 ? classes.dark : classes.light}>
<Typography style={{ fontWeight: 650 }}>
{moment(notification.status?.timestamp).format("MMMM DD, YYYY - h:mm a")}
</Typography>{" "}
:{" "}
<Typography>
{notification.settings?.title + " - " + notification.settings?.subtitle}
</Typography>
</ListItem>
))}
{recentNotifications.length < totalNotifications && (
<ListItem key={"button"}>
<Button onClick={loadMoreNotifications} disabled={notificationsLoading}>
Get More
</Button>
</ListItem>
)}
</React.Fragment>
)}
</List>
);
}

View file

@ -0,0 +1,229 @@
import { Box, Button, Card, Typography } from "@mui/material";
import { useCallback, useEffect, useState } from "react";
import Alerts, { Alert } from "./Alerts";
import Controls, { Control } from "./Controls";
import { Component, Device, Interaction } from "models";
import { quack } from "protobuf-ts/quack";
import { Option } from "common/SearchSelect";
import { useInteractionsAPI } from "hooks";
import { useGlobalState } from "providers";
import { extension, getFriendlyName } from "pbHelpers/ComponentType";
import { sameComponentID } from "pbHelpers/Component";
import Notifications from "./Notifications";
import { pond } from "protobuf-ts/pond";
import React from "react";
import NewObjectInteraction from "./NewObjectInteraction";
interface Props {
linkedComponents: Map<string, Component>; //the component key to the component object
componentDevices: Map<string, number>;
objectKey: string
objectType: pond.ObjectType
devices: Device[];
permissions: pond.Permission[]
}
export default function ObjectInteractions(props: Props) {
const { linkedComponents, componentDevices, devices, objectKey, objectType, permissions } = props
const [{as}] = useGlobalState()
//list of alerts, each alert contains a list of interactions with matching conditions
const [alerts, setAlerts] = useState<Alert[]>([]);
//list of interactions relating to a controller
const [controls, setControls] = useState<Control[]>([]);
const interactionsAPI = useInteractionsAPI();
const [openNewInteraction, setOpenNewInteraction] = useState(false)
const [allSinks, setAllSinks] = useState<Component[]>([]);
// const [typeOptions, setTypeOptions] = useState<Option[]>([]);
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>(undefined)
const matchConditions = (interaction: Interaction, set: Alert | Control) => {
//if the number of conditions are not the same they are different
if (interaction.settings.conditions.length !== set.conditions.length) {
return false;
}
//continue with the comparison
let matching = true;
interaction.settings.conditions.forEach((condition, i) => {
if (
condition.comparison !== set.conditions[i].comparison ||
condition.measurementType !== set.conditions[i].measurementType ||
condition.value !== set.conditions[i].value
) {
matching = false;
}
});
return matching;
};
const matchResult = (interaction: Interaction, set: Control) => {
let interactionResult = interaction.settings.result
let setResult = set.result
let matching = true
// if anything in the result for the interaction is different from the set make matching false
if(interactionResult?.type !== setResult?.type ||
interactionResult?.mode !== setResult?.mode ||
interactionResult?.value !== setResult?.value ||
interactionResult?.dutyCycle !== setResult?.dutyCycle
){
matching = false
}
return matching
}
const findDevice = (deviceID: number) => {
for (let device of devices) {
if (device.id() === deviceID) return device
}
}
const findSinkComponent = (sinks: Component[], interactionSink: quack.ComponentID, sourceDevice: number) => {
for (let component of sinks) {
if (sameComponentID(component.location(), interactionSink) && sourceDevice === componentDevices.get(component.key())){
return component
}
}
}
const load = useCallback(()=>{
const newInteractions: Interaction[] = [];
const interactionSourceMap = new Map<string, string>();
const sinkOptions: Component[] = [];
const alerts: Alert[] = [];
const controls: Control[] = [];
const run = async () => {
// Run all component API fetches in parallel
await Promise.all(
[...linkedComponents].map(async ([key, comp]) => {
const device = componentDevices.get(key);
if (device) {
const resp = await interactionsAPI.listInteractionsByComponent(
device,
comp.location(),
undefined,
as
);
resp.forEach(interaction => interactionSourceMap.set(interaction.key(), key));
newInteractions.push(...resp);
}
// Collect controller components
if (extension(comp.type()).isController) {
sinkOptions.push(comp);
}
})
);
};
run().then(() => {
newInteractions.forEach(interaction => {
//alert and control are not mutally exclusive, it is possible to be both if the control is sending notifications
if (interaction.isAlert()) {
// Find matching alert
let similarAlertIndex = alerts.findIndex(alert =>
matchConditions(interaction, alert)
);
const compKey = interactionSourceMap.get(interaction.key());
const component = compKey ? linkedComponents.get(compKey) : undefined;
if (component) {
if (similarAlertIndex === -1) {
alerts.push({
conditions: interaction.settings.conditions,
sourceComponents: [component],
});
} else {
alerts[similarAlertIndex].sourceComponents.push(component);
}
}
}
if (interaction.isControl()) {
if (!interaction.settings.sink) return //if there is no sink it cant be a control so move on to the next
const compKey = interactionSourceMap.get(interaction.key());
if (!compKey) return //we have to have a valid component key
const component = linkedComponents.get(compKey);
const deviceID = componentDevices.get(compKey);
if (!deviceID) return //we have to know which device it is for
const controlDevice = findDevice(deviceID)
if (!controlDevice) return //failed to get the device from the array
const sinkComponent = findSinkComponent(sinkOptions, interaction.settings.sink, deviceID)
if (!sinkComponent) return //failed to get the correct sink component
let similarControlIndex = controls.findIndex(control =>
matchConditions(interaction, control) &&
matchResult(interaction, control) &&
sameComponentID(interaction.settings.sink, control.sinkComponent.location()) && //if the sink for the interaction is the same address as the existing control
deviceID === control.device.id() //if the device id for source component is the same as the device id in the control
);
if (component && interaction.settings.result) {
if (similarControlIndex === -1) {
controls.push({
conditions: interaction.settings.conditions,
result: interaction.settings.result,
sourceComponents: [component],
sinkComponent: sinkComponent,
device: controlDevice
});
} else {
controls[similarControlIndex].sourceComponents.push(component);
}
}
}
});
setAllSinks(sinkOptions);
setAlerts(alerts);
setControls(controls);
}).catch(err => {
console.error("Interaction fetch error:", err)
})
},[linkedComponents, interactionsAPI, componentDevices, as])
useEffect(() => {
load()
}, [load]);
return (
<React.Fragment>
<NewObjectInteraction
open={openNewInteraction}
onClose={(refresh) => {
setOpenNewInteraction(false)
if(refresh){
load()
}
}}
linkedComponents={linkedComponents}
componentDevices={componentDevices}
device={selectedDevice}/>
<Card raised style={{ margin: 5 }}>
<Box padding={1}>
<Typography style={{ fontWeight: 650, fontSize: 25 }}>Interactions</Typography>
<Controls
controls={controls}
permissions={permissions}
devices={devices}
addNew={(device) => {
setSelectedDevice(device)
setOpenNewInteraction(true)
}}/>
<Alerts
alerts={alerts}
permissions={permissions}
addNew={() => {
setSelectedDevice(undefined)
setOpenNewInteraction(true)
}}/>
<Notifications objectKey={objectKey} objectType={objectType}/>
</Box>
</Card>
</React.Fragment>
)
}

View file

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

View file

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

View file

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

View file

@ -18,7 +18,8 @@ export const I2C: AddressTypeExtension = {
[quack.ComponentType.COMPONENT_TYPE_SEN5X, 0x68],
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, 0x6b],
[quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, 0x70],
[quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, 0x6c]
[quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, 0x6c],
[quack.ComponentType.COMPONENT_TYPE_TEMP_HUMIDITY, 0x43]
]);
const offset = offsets.get(componentType);

View file

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

View file

@ -0,0 +1,85 @@
import TemperatureHumidityDarkIcon from "assets/components/temperatureHumidityDark.png";
import TemperatureHumidityLightIcon from "assets/components/temperatureHumidityLight.png";
import { convertedUnitMeasurement } from "models/UnitMeasurement";
import {
AreaChartData,
ComponentTypeExtension,
GraphFilters,
LineChartData,
simpleAreaChartData,
simpleLineChartData,
simpleMeasurements,
simpleSummaries,
Summary,
unitMeasurementSummaries
} from "pbHelpers/ComponentType";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
export function TempHumidity(subtype: number = 0): ComponentTypeExtension {
let temperature = describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
quack.ComponentType.COMPONENT_TYPE_TEMP_HUMIDITY,
subtype
);
let humidity = describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
quack.ComponentType.COMPONENT_TYPE_TEMP_HUMIDITY,
subtype
);
return {
type: quack.ComponentType.COMPONENT_TYPE_TEMP_HUMIDITY,
subtypes: [
{
key: quack.TempHumiditySubtype.TEMP_HUMIDITY_SUBTYPE_HDC302,
value: "TEMP_HUMIDITY_SUBTYPE_HDC302",
friendlyName: "HDC302"
}
],
friendlyName: "Temperature/Humidity",
description: "Measures the temperature and humidity",
isController: false,
isSource: true,
isCalibratable: true,
addressTypes: [quack.AddressType.ADDRESS_TYPE_I2C],
interactionResultTypes: [],
states: [],
measurements: simpleMeasurements(temperature, humidity),
measurementSummary: async function(measurement: quack.Measurement): Promise<Array<Summary>> {
return simpleSummaries(measurement, temperature, humidity);
},
unitMeasurementSummary: (
measurements: convertedUnitMeasurement
): Summary[] => {
return unitMeasurementSummaries(
measurements,
quack.ComponentType.COMPONENT_TYPE_TEMP_HUMIDITY,
subtype
);
},
areaChartData: (
measurement: pond.UnitMeasurementsForComponent,
smoothingAverages?: number,
filters?: GraphFilters
): AreaChartData => {
return simpleAreaChartData(measurement, smoothingAverages, filters);
},
lineChartData: (
measurement: pond.UnitMeasurementsForComponent,
smoothingAverages?: number,
filters?: GraphFilters
): LineChartData => {
return simpleLineChartData(
quack.ComponentType.COMPONENT_TYPE_TEMP_HUMIDITY,
measurement,
smoothingAverages,
filters
);
},
minMeasurementPeriodMs: 1000,
icon: (theme?: "light" | "dark"): string | undefined => {
return theme === "light" ? TemperatureHumidityDarkIcon : TemperatureHumidityLightIcon;
}
};
}

View file

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

View file

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