frontend/src/pbHelpers/Interaction.ts

436 lines
13 KiB
TypeScript

import { Interaction, Component, User } from "models";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { or, notNull } from "utils/types";
import { sameComponentID, emptyComponentId, componentIDToString } from "pbHelpers/Component";
import moment, { weekdaysShort } from "moment-timezone";
import { describeMeasurement } from "./MeasurementDescriber";
import { capitalize } from "lodash";
export function getDefaultInteraction(): Interaction {
let interaction = new Interaction();
interaction.settings.schedule = {
weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"],
timeOfDayStart: "00:00",
timeOfDayEnd: "24:00",
timezone: moment.tz.guess()
} as pond.InteractionSchedule;
interaction.settings.result = {
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT
} as pond.InteractionResult;
return interaction;
}
export function isSource(componentID: quack.IComponentID, interaction: Interaction): boolean {
return interaction.settings.source
? sameComponentID(componentID, interaction.settings.source)
: false;
}
export function isSink(componentID: quack.IComponentID, interaction: Interaction): boolean {
return interaction.settings.sink
? sameComponentID(componentID, interaction.settings.sink)
: false;
}
export function getInteractionID(interaction: pond.IInteractionSettings): string {
const { source, sink } = interaction;
let sourceID = componentIDToString(or(source, {} as quack.IComponentID));
let sinkID = componentIDToString(or(sink, {} as quack.IComponentID));
return sourceID + ":" + sinkID + ":" + interaction.instance;
}
export function findInteractionsAsSource(
component: quack.IComponentID,
interactions: Interaction[]
): Interaction[] {
return interactions.filter(i => isSource(component, i));
}
export function findInteractionsAsSink(
component: quack.IComponentID,
interactions: Interaction[]
): Interaction[] {
return interactions.filter(i => isSink(component, i));
}
export function findInteractions(
component: quack.IComponentID,
interactions: Interaction[]
): Interaction[] {
return interactions.filter(i => isSource(component, i) || isSink(component, i));
}
export function HasInteraction(
component: quack.IComponentID,
interactions: Interaction[]
): boolean {
if (!component || !interactions) {
return false;
}
let hasInteraction = false;
interactions.forEach(i => {
if (isSource(component, i) || isSink(component, i)) {
hasInteraction = true;
}
});
return hasInteraction;
}
export function timeOfDayDescriptor(schedule: pond.IInteractionSchedule) {
let timeOfDayStart = "00:00";
let timeOfDayEnd = "24:00";
if (schedule) {
if (schedule.timeOfDayStart) {
timeOfDayStart = schedule.timeOfDayStart;
}
if (schedule.timeOfDayEnd) {
timeOfDayEnd = schedule.timeOfDayEnd;
}
}
if (timeOfDayStart === "00:00" && timeOfDayEnd === "24:00") {
return "allDay";
} else if (timeOfDayStart === "00:00") {
return "before";
} else if (timeOfDayEnd === "24:00") {
return "after";
} else {
return "from";
}
}
export function friendlyTimeOfDayDescriptor(
descriptor: "allDay" | "before" | "after" | "from"
): string {
switch (descriptor) {
case "allDay":
return "all day";
default:
return descriptor;
}
}
const isWeekend = (day: string): boolean => {
return day.toLowerCase().startsWith("sat") || day.toLowerCase().startsWith("sun");
};
export function abbreviatedWeekdays(schedule: pond.IInteractionSchedule): string {
if (!schedule || !schedule.weekdays) {
return "";
}
let weekdays = schedule.weekdays;
let numDays = weekdays.length;
if (numDays === 1) {
return capitalize(weekdays[0]);
}
if (numDays === 2 && weekdays.every(day => isWeekend(day))) {
return "Weekends";
}
if (numDays === 5 && weekdays.every(day => !isWeekend(day))) {
return "Weekdays";
}
if (numDays === 7) {
return "Everyday";
}
return weekdaysShort()
.filter(abrev => weekdays.find(day => day.toLowerCase().startsWith(abrev.toLowerCase())))
.join(", ");
}
export const interactionResultText = (interaction: Interaction, sink?: Component) => {
let sinkName = sink ? sink.name() : "Unknown";
let result = interaction.settings.result;
let notifications = interaction.settings.notifications;
if (!result) {
return "Unknown when";
}
let value = result.value ? "on" : "off";
let notify = notifications && notifications.notify;
switch (result.type) {
case quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT:
return (notify ? "Send notifications" : "Reporting measurement") + " when";
case quack.InteractionResultType.INTERACTION_RESULT_TYPE_RUN:
return "Run " + sinkName + " when";
case quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET:
return "Set " + sinkName + " to " + value + " when";
case quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE:
return "Toggle " + sinkName + " " + value + " when";
}
return "Unknown when";
};
export const interactionConditionText = (
source: Component | undefined,
condition: pond.IInteractionCondition,
and: boolean,
user?: User
) => {
let comparison = "is exactly";
switch (condition.comparison) {
case quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO:
comparison = "is exactly";
break;
case quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN:
comparison = "is above";
break;
case quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN:
comparison = "is below";
break;
}
if (!condition.measurementType) return "Unknown " + comparison + " " + condition.value;
let sourceType = source && source.settings.type;
let sourceSubtype = source && source.settings.subtype;
let measurement = describeMeasurement(condition.measurementType, sourceType, sourceSubtype, undefined, user);
return (
(and ? "and " : "") +
measurement.label() +
" " +
comparison +
" " +
measurement.convertWithUnits(condition.value ? condition.value : 0)
);
};
const keyTranslator = new Map<keyof pond.InteractionSettings, string>([]);
// calibration_offset
// grain_type
// grain_filled_to
// Keys will be stringified by default if not found in the keyTranslator
export function TranslateKey(key: keyof pond.InteractionSettings): string {
let translatedKey = keyTranslator.get(key);
return translatedKey ? translatedKey : capitalize(key.toString());
}
const getComparisons = (conditions: pond.InteractionCondition[]) => {
let ret = " ";
conditions.forEach(cond => {
switch (cond.measurementType) {
case quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN:
ret = ret + "Bool";
break;
case quack.MeasurementType.MEASUREMENT_TYPE_ANALOG:
ret = ret + "Analog";
break;
case quack.MeasurementType.MEASUREMENT_TYPE_CAPACITANCE:
ret = ret + "Capacitance";
break;
case quack.MeasurementType.MEASUREMENT_TYPE_CONDUCTIVITY:
ret = ret + "Conductivity";
break;
case quack.MeasurementType.MEASUREMENT_TYPE_DISTANCE_CM:
ret = ret + "Distance(cm)";
break;
case quack.MeasurementType.MEASUREMENT_TYPE_LIGHT:
ret = ret + "Light";
break;
case quack.MeasurementType.MEASUREMENT_TYPE_ORP:
ret = ret + "ORP";
break;
case quack.MeasurementType.MEASUREMENT_TYPE_PERCENT:
ret = ret + "Percent";
break;
case quack.MeasurementType.MEASUREMENT_TYPE_PH:
ret = ret + "PH";
break;
case quack.MeasurementType.MEASUREMENT_TYPE_PPB:
ret = ret + "PPB";
break;
case quack.MeasurementType.MEASUREMENT_TYPE_PPM:
ret = ret + "PPM";
break;
case quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE:
ret = ret + "Pressure";
break;
case quack.MeasurementType.MEASUREMENT_TYPE_RAIN:
ret = ret + "Rain";
break;
case quack.MeasurementType.MEASUREMENT_TYPE_RSSI:
ret = ret + "RSSI";
break;
case quack.MeasurementType.MEASUREMENT_TYPE_SPEED:
ret = ret + "Speed";
break;
case quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE:
ret = ret + "Temperature";
break;
case quack.MeasurementType.MEASUREMENT_TYPE_VOLTAGE:
ret = ret + "Voltage";
break;
case quack.MeasurementType.MEASUREMENT_TYPE_WEIGHT:
ret = ret + "Weight";
break;
default:
ret = "Invalid Measurement Type";
break;
}
switch (cond.comparison) {
case quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO:
ret = ret + " equal to " + cond.value;
break;
case quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN:
ret = ret + " greater than " + cond.value;
break;
case quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN:
ret = ret + " less than " + cond.value;
break;
default:
ret = ret + " Invalid Operator";
}
});
return ret;
};
const resultTypeString = (result: pond.InteractionResult | null | undefined) => {
let ret = "No Result Found";
if (result) {
switch (result.type) {
case quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT:
ret = "Report";
break;
case quack.InteractionResultType.INTERACTION_RESULT_TYPE_RUN:
ret = "Run";
break;
case quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET:
ret = "Set";
break;
case quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE:
ret = "Toggle";
break;
default:
ret = "Invalid";
}
}
return ret;
};
// might not need this
const valueTranslator = new Map<
keyof pond.InteractionSettings,
(interaction: pond.InteractionSettings) => string
>([
[
"source",
interaction =>
or(
interaction.source?.type.toString() +
"-" +
interaction.source?.addressType.toString() +
"-" +
interaction.source?.address.toString(),
"Failed to Find Source"
)
],
["conditions", interaction => or(getComparisons(interaction.conditions), "")],
["result", interaction => or(resultTypeString(interaction.result), "")],
["notifications", interaction => (interaction.notifications?.notify ? "True" : "False")]
// ["schedule", interaction => "s"]
]);
// Values will be stringified by default if its key is not found in the valueTranslator
export function TranslateValue(
key: keyof pond.InteractionSettings,
interaction: pond.InteractionSettings
) {
let translatorFunc = valueTranslator.get(key);
let value: any = or(interaction[key], "");
let d: string;
d = or(value.toString(), "");
return translatorFunc ? translatorFunc(interaction) : interaction[key] ? capitalize(d) : "";
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~VALIDATORS~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
export function isInteractionValid(interaction: Interaction) {
return (
notNull(interaction.settings) &&
isSourceValid(or(interaction.settings.source, {} as quack.ComponentID)) &&
isSinkValid(or(interaction.settings.sink, {} as quack.ComponentID)) &&
areConditionsValid(interaction.settings.conditions) &&
isResultValid(or(interaction.settings.result, {} as pond.InteractionResult)) &&
isNotificationsValid(
or(interaction.settings.notifications, {} as pond.InteractionNotifications)
)
);
}
export function isSourceValid(source: quack.IComponentID | null | undefined) {
return source && JSON.stringify(source) !== JSON.stringify(emptyComponentId());
}
export function isSinkValid(sink: quack.IComponentID) {
return notNull(sink);
}
export function areConditionsValid(conditions: Array<pond.IInteractionCondition>) {
let conditionsValid = true;
conditions.forEach((condition: pond.IInteractionCondition) => {
if (!isConditionValid(condition)) {
conditionsValid = false;
}
});
return conditionsValid;
}
export function isConditionValid(condition: pond.IInteractionCondition) {
if (!notNull(condition)) {
return false;
}
return (
isMeasurementTypeValid(
or(condition.measurementType, quack.MeasurementType.MEASUREMENT_TYPE_INVALID)
) &&
isComparisonValid(
or(condition.comparison, quack.RelationalOperator.RELATIONAL_OPERATOR_INVALID)
) &&
isValueValid(or(condition.value, null))
);
}
export function isMeasurementTypeValid(measurementType: quack.MeasurementType) {
return (
notNull(measurementType) && measurementType !== quack.MeasurementType.MEASUREMENT_TYPE_INVALID
);
}
export function isComparisonValid(comparison: quack.RelationalOperator) {
return notNull(comparison) && comparison !== quack.RelationalOperator.RELATIONAL_OPERATOR_INVALID;
}
export function isValueValid(value: number) {
return notNull(value) && !isNaN(value);
}
export function isResultValid(result: pond.IInteractionResult) {
return (
notNull(result) &&
isResultTypeValid(
or(result.type, quack.InteractionResultType.INTERACTION_RESULT_TYPE_INVALID)
) &&
or(result.dutyCycle, 0) >= 0
);
}
export function isResultTypeValid(resultType: quack.InteractionResultType) {
return (
notNull(resultType) &&
resultType !== quack.InteractionResultType.INTERACTION_RESULT_TYPE_INVALID
);
}
export function isNotificationsValid(notifications: pond.IInteractionNotifications) {
return notNull(notifications);
}