436 lines
13 KiB
TypeScript
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);
|
|
}
|