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([]); // 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) { 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); }