@@ -187,6 +187,19 @@ static size_t strlcpy(char *dst, const char *src, size_t size) {
187187#define MIN_ALARM_INTERVAL_SECONDS 300 // Minimum 5 minutes between same alarm type
188188#endif
189189
190+ // Digital sensor (float switch) constants
191+ #ifndef DIGITAL_SWITCH_THRESHOLD
192+ #define DIGITAL_SWITCH_THRESHOLD 0 .5f // Threshold to determine activated vs not-activated state
193+ #endif
194+
195+ #ifndef DIGITAL_SENSOR_ACTIVATED_VALUE
196+ #define DIGITAL_SENSOR_ACTIVATED_VALUE 1 .0f // Value returned when switch is activated
197+ #endif
198+
199+ #ifndef DIGITAL_SENSOR_NOT_ACTIVATED_VALUE
200+ #define DIGITAL_SENSOR_NOT_ACTIVATED_VALUE 0 .0f // Value returned when switch is not activated
201+ #endif
202+
190203static const uint8_t NOTECARD_I2C_ADDRESS = 0x17 ;
191204static const uint32_t NOTECARD_I2C_FREQUENCY = 400000UL ;
192205
@@ -235,6 +248,8 @@ struct TankConfig {
235248 uint8_t relayMask; // Bitmask of relays to trigger (bit 0=relay 1, etc.)
236249 RelayTrigger relayTrigger; // Which alarm type triggers the relay (any, high, low)
237250 RelayMode relayMode; // How long relay stays on (momentary, until_clear, manual_reset)
251+ // Digital sensor (float switch) specific settings
252+ char digitalTrigger[16 ]; // 'activated' or 'not_activated' - when to trigger alarm for digital sensors
238253};
239254
240255struct ClientConfig {
@@ -615,6 +630,7 @@ static void createDefaultConfig(ClientConfig &cfg) {
615630 cfg.tanks [0 ].relayMask = 0 ; // No relays triggered by default
616631 cfg.tanks [0 ].relayTrigger = RELAY_TRIGGER_ANY; // Default: trigger on any alarm
617632 cfg.tanks [0 ].relayMode = RELAY_MODE_MOMENTARY; // Default: momentary 30 min activation
633+ cfg.tanks [0 ].digitalTrigger [0 ] = ' \0 ' ; // Not a digital sensor by default
618634}
619635
620636static bool loadConfigFromFlash (ClientConfig &cfg) {
@@ -740,6 +756,9 @@ static bool loadConfigFromFlash(ClientConfig &cfg) {
740756 } else {
741757 cfg.tanks [i].relayMode = RELAY_MODE_MOMENTARY;
742758 }
759+ // Load digital sensor trigger state (for float switches)
760+ const char *digitalTriggerStr = t[" digitalTrigger" ].as <const char *>();
761+ strlcpy (cfg.tanks [i].digitalTrigger , digitalTriggerStr ? digitalTriggerStr : " " , sizeof (cfg.tanks [i].digitalTrigger ));
743762 }
744763
745764 return true ;
@@ -804,6 +823,10 @@ static bool saveConfigToFlash(const ClientConfig &cfg) {
804823 case RELAY_MODE_MANUAL_RESET: t[" relayMode" ] = " manual_reset" ; break ;
805824 default : t[" relayMode" ] = " momentary" ; break ;
806825 }
826+ // Save digital sensor trigger state (for float switches)
827+ if (cfg.tanks [i].digitalTrigger [0 ] != ' \0 ' ) {
828+ t[" digitalTrigger" ] = cfg.tanks [i].digitalTrigger ;
829+ }
807830 }
808831
809832 #if defined(ARDUINO_OPTA) || defined(ARDUINO_ARCH_MBED)
@@ -1230,6 +1253,13 @@ static void applyConfigUpdate(const JsonDocument &doc) {
12301253 gConfig .tanks [i].relayMode = RELAY_MODE_MOMENTARY;
12311254 }
12321255 }
1256+ // Handle digital sensor trigger state (for float switches)
1257+ if (t.containsKey (" digitalTrigger" )) {
1258+ const char *digitalTriggerStr = t[" digitalTrigger" ].as <const char *>();
1259+ strlcpy (gConfig .tanks [i].digitalTrigger ,
1260+ digitalTriggerStr ? digitalTriggerStr : " " ,
1261+ sizeof (gConfig .tanks [i].digitalTrigger ));
1262+ }
12331263 }
12341264 }
12351265
@@ -1377,11 +1407,16 @@ static float readTankSensor(uint8_t idx) {
13771407
13781408 switch (cfg.sensorType ) {
13791409 case SENSOR_DIGITAL: {
1380- // Use explicit bounds check for pin
1410+ // Float switch sensor - returns activated/not-activated state
1411+ // This implementation assumes normally-open (NO) float switches with INPUT_PULLUP:
1412+ // - Default state is HIGH (switch open, no fluid)
1413+ // - When fluid is present, switch closes and pulls pin LOW
1414+ // For normally-closed (NC) switches, invert the trigger condition in config
13811415 int pin = (cfg.primaryPin >= 0 && cfg.primaryPin < 255 ) ? cfg.primaryPin : (2 + idx);
13821416 pinMode (pin, INPUT_PULLUP);
13831417 int level = digitalRead (pin);
1384- return level == HIGH ? cfg.maxValue : 0 .0f ;
1418+ // Return activated value when switch is closed (LOW), not-activated when open (HIGH)
1419+ return (level == LOW) ? DIGITAL_SENSOR_ACTIVATED_VALUE : DIGITAL_SENSOR_NOT_ACTIVATED_VALUE;
13851420 }
13861421 case SENSOR_ANALOG: {
13871422 // Use explicit bounds check for channel (A0602 has channels 0-7)
@@ -1520,7 +1555,71 @@ static void evaluateAlarms(uint8_t idx) {
15201555 return ;
15211556 }
15221557
1523- // Apply hysteresis: use different thresholds for triggering vs clearing
1558+ // Handle digital sensors (float switches) differently
1559+ if (cfg.sensorType == SENSOR_DIGITAL) {
1560+ // For digital sensors, currentInches is either DIGITAL_SENSOR_ACTIVATED_VALUE (1.0) or DIGITAL_SENSOR_NOT_ACTIVATED_VALUE (0.0)
1561+ bool isActivated = (state.currentInches > DIGITAL_SWITCH_THRESHOLD);
1562+ bool shouldAlarm = false ;
1563+ bool triggerOnActivated = true ; // Track what condition triggers the alarm
1564+
1565+ // Determine if we should alarm based on trigger configuration
1566+ if (cfg.digitalTrigger [0 ] != ' \0 ' ) {
1567+ if (strcmp (cfg.digitalTrigger , " activated" ) == 0 ) {
1568+ shouldAlarm = isActivated; // Alarm when switch is activated
1569+ triggerOnActivated = true ;
1570+ } else if (strcmp (cfg.digitalTrigger , " not_activated" ) == 0 ) {
1571+ shouldAlarm = !isActivated; // Alarm when switch is NOT activated
1572+ triggerOnActivated = false ;
1573+ }
1574+ } else {
1575+ // Legacy behavior: use highAlarm/lowAlarm thresholds
1576+ // Only one of these should be configured for a digital sensor
1577+ // highAlarm = 1 means trigger when reading is 1.0 (switch activated)
1578+ // lowAlarm = 0 means trigger when reading is 0.0 (switch not activated)
1579+ bool hasHighAlarm = (cfg.highAlarmThreshold >= DIGITAL_SENSOR_ACTIVATED_VALUE);
1580+ bool hasLowAlarm = (cfg.lowAlarmThreshold == DIGITAL_SENSOR_NOT_ACTIVATED_VALUE);
1581+
1582+ if (hasHighAlarm && !hasLowAlarm) {
1583+ shouldAlarm = isActivated;
1584+ triggerOnActivated = true ;
1585+ } else if (hasLowAlarm && !hasHighAlarm) {
1586+ shouldAlarm = !isActivated;
1587+ triggerOnActivated = false ;
1588+ } else if (hasHighAlarm) {
1589+ // Default to high alarm behavior if both are set
1590+ shouldAlarm = isActivated;
1591+ triggerOnActivated = true ;
1592+ }
1593+ }
1594+
1595+ // Handle alarm state with debouncing
1596+ if (shouldAlarm && !state.highAlarmLatched ) {
1597+ state.highAlarmDebounceCount ++;
1598+ state.clearDebounceCount = 0 ;
1599+ if (state.highAlarmDebounceCount >= ALARM_DEBOUNCE_COUNT) {
1600+ state.highAlarmLatched = true ;
1601+ state.highAlarmDebounceCount = 0 ;
1602+ // Send alarm with descriptive type based on configured trigger condition
1603+ const char *alarmType = triggerOnActivated ? " triggered" : " not_triggered" ;
1604+ sendAlarm (idx, alarmType, state.currentInches );
1605+ }
1606+ } else if (!shouldAlarm && state.highAlarmLatched ) {
1607+ state.clearDebounceCount ++;
1608+ state.highAlarmDebounceCount = 0 ;
1609+ if (state.clearDebounceCount >= ALARM_DEBOUNCE_COUNT) {
1610+ state.highAlarmLatched = false ;
1611+ state.clearDebounceCount = 0 ;
1612+ sendAlarm (idx, " clear" , state.currentInches );
1613+ }
1614+ } else if (!shouldAlarm) {
1615+ state.highAlarmDebounceCount = 0 ;
1616+ } else {
1617+ state.clearDebounceCount = 0 ;
1618+ }
1619+ return ; // Skip the standard analog threshold evaluation
1620+ }
1621+
1622+ // Standard analog/current loop sensor alarm evaluation with hysteresis
15241623 float highTrigger = cfg.highAlarmThreshold ;
15251624 float highClear = cfg.highAlarmThreshold - cfg.hysteresisValue ;
15261625 float lowTrigger = cfg.lowAlarmThreshold ;
@@ -1598,9 +1697,19 @@ static void sendTelemetry(uint8_t idx, const char *reason, bool syncNow) {
15981697 doc[" label" ] = cfg.name ;
15991698 doc[" tank" ] = cfg.tankNumber ;
16001699 doc[" id" ] = String (cfg.id );
1601- doc[" maxValue" ] = cfg.maxValue ;
1602- doc[" levelInches" ] = state.currentInches ;
1603- doc[" percent" ] = (cfg.maxValue > 0 .1f ) ? (state.currentInches / cfg.maxValue * 100 .0f ) : 0 .0f ;
1700+
1701+ // Handle digital sensors differently in telemetry
1702+ if (cfg.sensorType == SENSOR_DIGITAL) {
1703+ doc[" sensorType" ] = " digital" ;
1704+ bool activated = (state.currentInches > DIGITAL_SWITCH_THRESHOLD);
1705+ doc[" activated" ] = activated; // Boolean state: true = switch activated
1706+ doc[" levelInches" ] = state.currentInches ; // 1.0 or 0.0
1707+ doc[" percent" ] = activated ? 100 .0f : 0 .0f ; // 100% when activated, 0% when not
1708+ } else {
1709+ doc[" maxValue" ] = cfg.maxValue ;
1710+ doc[" levelInches" ] = state.currentInches ;
1711+ doc[" percent" ] = (cfg.maxValue > 0 .1f ) ? (state.currentInches / cfg.maxValue * 100 .0f ) : 0 .0f ;
1712+ }
16041713 doc[" reason" ] = reason;
16051714 doc[" time" ] = currentEpoch ();
16061715
0 commit comments