Function node with a timer (stored in Flow variable to maintain state) - doesn't work

Am I correct to use Date.Now() to capture seconds since epoch?

In the last stanza of this code from my function node I check whether Date.Now() (for actual now!) minus Flow variable startTimer (Date.Now() from when the mode first changed to fanOn10), is more than the value in ms of the Flow variable fanTimerInterval. I can't get this to work and the mode always changes from fanOn10 to humidityTrigger as soon as the next sensor reading is published (about 10 secs). I presume I've missed something obvious but I can't work out what it is! Please help?

//each time through, set variables to reinstate previous values for
//these maintained attributes (or initially, their default values)

var requiredFanState    = String(flow.get('requiredFanState')) || "humidityTrigger";
var startTimer          = flow.get('startTimer') || 0;
var fanTimerInterval    = 60000*flow.get('fanOnTime') || 60000; //milliseconds (1 minute = 60000ms)
var fanSwitchSet        = flow.get('fanSwitchSet') || false;
var humidity            = 50;

//set defaults for messages: payload & topic
//FanSwitch is subscribed by the ESP8266 controlling the fan relay. It has valid values of 'ON' and 'OFF'
var FanSwitch           = {payload: 'OFF', topic: 'bathroom/fanSwitch'};

//FanStateNR is subscribed by the ESP8266 with the SHT30 sensor, 
//so NodeRed can pass Dashboard mode changes back to it
//to keep both Dashboard and ESP8266 in sync
var FanStateNR          = {payload: 'humiditytrigger', topic: 'esp/dht/b/fanStatetNR'};

//NodeRed will trigger a timer as soon as it detects a mode change to fanOn10. Despite the mode variable name it's not necessarily 10mins, as the Dashboard should update the flow variable to be the slider value x 60000 (1 min in ms)
var Resume              = {payload: null, topic: 'esp/dht/b/fanresume'};

//If the fan mode changes use this message to synchronize the Dashboard's mode dropdown
var DashSync            = {payload: null, topic:'dashSync'};

//message comes from humidity sensor
if (msg.topic == 'esp/dht/b/humid'){  
  humidity = msg.payload;
}

//message comes from mode switch
if ((msg.topic == 'esp/dht/b/fanState') || (msg.topic == 'esp/dht/b/fanStateNR')){  //the message is on the topic 'fanState' AND
    if (msg.payload != requiredFanState){ //the mode has changed, it should be actioned
        flow.set('fanSwitchSet', false);
        flow.set('requiredFanState', msg.payload); //store new mode state
        requiredFanState    = String(flow.get('requiredFanState')) // also update the variable used to make the following tests
        DashSync.payload    = requiredFanState;
    }
    //if the mode change hasn't been actioned yet run the mode appropriate action
    if ((requiredFanState == 'fanOff') && (fanSwitchSet == false)) {
        fanSwitchSet        =  true;
        FanSwitch.payload   = 'OFF';
        FanStateNR.payload  = 'fanOff';
        return [ FanStateNR, FanSwitch, null, DashSync ];
        
    }   else if ((requiredFanState == 'fanOn10') && (startTimer == 0) && (fanSwitchSet == false)) {
        fanSwitchSet        =  true;
        startTimer          =  Date.now();
        FanSwitch.payload   = 'ON';
        FanStateNR.payload  = 'fanOn10';
        return [ FanStateNR, FanSwitch, null, DashSync ];
        
    }   else if ((requiredFanState == 'humidityTrigger') && (fanSwitchSet == false)) {
        fanSwitchSet        =  true;
        if (humidity >= 90) {
                FanSwitch.payload = 'ON';
            } else {
                FanSwitch.payload = 'OFF';
            }
         FanStateNRpayload   = 'humidityTrigger';
         return [ FanStateNR, FanSwitch, null, DashSync ];
     }
}

//irrespective of the source of the message:
//If the timer has expired, reset the timer, 
//send a message to turn the fan off and change the persistent 
//flow variable to reflect the resumed default mode

if (requiredFanState == 'fanOn10') {
    if ((Date.now() - startTimer) >=  fanTimerInterval) {
        startTimer          = 0;
        FanSwitch.payload   = 'OFF';
        flow.set('requiredFanState', "humidityTrigger");
        Resume.payload      = 'resume';
        FanStateNR.payload  = "humidityTrigger";
        DashSync.payload    = requiredFanState +  ' - ' + String(Date.now()-startTimer);
        return [ FanStateNR, FanSwitch, Resume, DashSync ];
    }
}

No, it is milliseconds.

To debug the code you can insert node.warn() statements to display stuff in the debug pane so you can follow the code and work out where it is going wrong.
For example, after fetching startTimer from context you could insert
node.warn("startTimer: " + startTimer)
in order to check that it is picking up sensible values.

I notice that you have return calls inside the if statements in the first part, then you have code below it with comments starting //irrespective of the source of the message. That code will, of course, not be executed if you hit one of the return statements in the earlier code. If that is indeed part of the problem then it is an example of why many consider it a bad idea to have multiple return statements in one function.

Thanks Colin. I've made the suggested. changes and more (see below). Using the node.warn() technique it seems that the Date.Now() isn't working as I'd expect. On the debug console, I get:

13/01/2022, 00:31:10node: 5ab9213ea2458824function : (warn)
"topic: esp/dht/b/fanState"
13/01/2022, 00:31:10node: 5ab9213ea2458824function : (warn)
"payload: fanOn10"
13/01/2022, 00:31:10node: 5ab9213ea2458824function : (warn)
true
13/01/2022, 00:31:10node: 5ab9213ea2458824function : (warn)
"changed fan state detected"
13/01/2022, 00:31:10node: 5ab9213ea2458824function : (warn)
"Date.Now():1642033872047"
13/01/2022, 00:31:10node: 5ab9213ea2458824function : (warn)
"startTimer:1642032427034"
13/01/2022, 00:31:10node: 5ab9213ea2458824function : (warn)
"timer progress:1445014 to:300000"
13/01/2022, 00:31:10node: 5ab9213ea2458824function : (warn)
"Resetting timer!"

I note that the timestamp for the warnings I issued for the startTime and Date.Now() are BOTH 13/01/2022, 00:31:10 and yet the difference is 1445014 which is already considerably more than my threshold test of 5mins (300000ms)

Am I making another schoolboy error

My new code (in the function is:

//each time through, set variables to reinstate previous values for
//these maintained attibutes (or initially, their default values)

var requiredFanState    = String(flow.get('requiredFanState')) || "humidityTrigger";
var startTimer          = flow.get('startTimer') || 0;
var fanTimerInterval    = flow.get('fanOnTime') || 300000; //milliseconds (1 minute = 60000ms)
node.warn(flow.get('fanOnTime'));
var fanSwitchSet        = flow.get('fanSwitchSet') || false;
var humidity            = 50;
var humidityThreshold   = 90;

//set defaults for messages: payload & topic
//FanSwitch is subscribed by the ESP8266 controlling the fan relay. It has valid values of 'ON' and 'OFF'
var FanSwitch           = {payload: 'OFF', topic: 'bathroom/fanSwitch'};

//FanStateNR is subscribed by the ESP8266 with the SHT30 sensor, 
//so NodeRed can pass Dashboard mode changes back to it
//to keep both Dashboard and ESP8266 in sync
var FanStateNR          = {payload: 'humiditytrigger', topic: 'esp/dht/b/fanStatetNR'};

//NodeRed will trigger a timer as soon as it detects a mode change to fanOn10. It's not necessarily 10mins
var Resume              = {payload: null, topic: 'esp/dht/b/fanresume'};

//If the fan mode changes use this message to synchronize the Dashboard's mode dropdown
var DashSync            = {payload: null, topic:'dashSync'};

//debug output
node.warn('topic: '+msg.topic);
node.warn('payload: '+msg.payload);

//message comes from humidity sensor
if (msg.topic == 'esp/dht/b/humid'){  
  humidity = msg.payload;
  FanStateNR.payload    = null;
  FanSwitch.payload     = null;
  Resume.payload        = null;
  DashSync.payload      = null;
}

//message comes from dashboard timer slider
if (msg.topic == 'fanOnTime'){  
  flow.set('fanOnTime', msg.payload);
  FanStateNR.payload    = null;
  FanSwitch.payload     = null;
  Resume.payload        = null;
  DashSync.payload      = null;
}

//message comes from mode switch
//if ((msg.topic == 'esp/dht/b/fanState') || (msg.topic == 'esp/dht/b/fanStateNR')){  //the message is on the topic 'fanState' AND
if (msg.topic == 'esp/dht/b/fanState') {  //the message is on the topic 'fanState' AND
    node.warn(msg.payload != requiredFanState)
    if (msg.payload != requiredFanState){ //the mode has changed, it should be actioned
        fanSwitchSet        = 'false';
        flow.set('fanSwitchSet', false);
        flow.set('requiredFanState', msg.payload); //store new mode state
        requiredFanState    = String(flow.get('requiredFanState')) // also update the variable used to make the following tests
        DashSync.payload    = requiredFanState; //use this topic output to keep the Dashboard mode dropdown in sync
        node.warn('changed fan state detected');
    }
    //if the mode change hasn't been actioned yet run the mode appropriate action
    if ((requiredFanState == 'fanOff') && (fanSwitchSet == false)) {
        fanSwitchSet        =  true;
        flow.set('fanSwitchSet', fanSwitchSet);
        FanSwitch.payload   = 'OFF';
        FanStateNR.payload  = 'fanOff';
        node.warn('fan turned off');
        
    }   else if ((requiredFanState == 'fanOn10') && (startTimer == 0) && (fanSwitchSet == false)) {
        fanSwitchSet        =  true;
        flow.set('fanSwitchSet', true);
        startTimer          =  Date.now();
        flow.set('startTimer', Date.now());
        node.warn('startTimer: ' + startTimer);
        FanSwitch.payload   = 'ON';
        FanStateNR.payload  = 'fanOn10';
        node.warn('fan turned on and timer started at :'+ startTimer);

    }   else if ((requiredFanState == 'humidityTrigger') && (fanSwitchSet == false)) {
        node.warn('fancontrolled by humidity level :'+humidityThreshold);
        fanSwitchSet        =  true;
        flow.set('fanSwitchSet', true);
        if (humidity >= humidityThreshold) {
                FanSwitch.payload = 'ON';
            } else {
                FanSwitch.payload = 'OFF';
            }
         FanStateNRpayload   = 'humidityTrigger';
    }
    Resume.payload      = null;
}

//If the timer has expired, reset the timer, 
//send a message to turn the fan off and change the persistent 
//flow variable to reflect the resumed default mode

if (requiredFanState == 'fanOn10') {
        node.warn('Date.Now():'+ Date.now());
        node.warn('startTimer:'+ startTimer);
        var prog = (Date.now()-startTimer);
        node.warn('timer progress:'+ prog + 'to:'+ fanTimerInterval);
        
    if (((Date.now() - startTimer) >=  fanTimerInterval) && (startTimer > 0)) {
        node.warn('Resetting timer!');
        startTimer          = 0;
        FanSwitch.payload   = 'OFF';
        flow.set('requiredFanState', "humidityTrigger");
        Resume.payload      = 'resume';
        FanStateNR.payload  = "humidityTrigger";
        DashSync.payload    = requiredFanState;
    }
}

return [ FanStateNR, FanSwitch, Resume, DashSync ];

Hi. It's likely there is a flaw in your function but without sample data and a flow chart or well formed description of the intended logic, it is hard to assist.

To me, it looks like you want a dashboard entry for fan duration and if a humidity sensor rises above a certain value, run a fan for that duration? I'm likely missing some finer details and minor steps but that's partly my point ...

It's a low code environment, but you have gone down the classic route a novice on node-red goes - doing large chunks of logic in a function node (I did this myself in the early days - it's not a dig at your capabilities)

I would like to think, if you can list out exactly what you are trying to achieve, a relatively simple (code free/low code) flow ( that is visually easier to follow than some code buried in a function ) could be achieved.

1 Like

That is the time that it was displayed in the debug screen, not the timestamp represented by the value from node.warn().
You can use https://www.epochconverter.com/ for converting the ms values back to real time.
Date.Now():1642033872047 is Thursday, 13 January 2022 00:31:12.047 UTC
startTimer:1642032427034 is Thursday, 13 January 2022 00:07:07.034 UTC

You can make the warnings easier to read if you use something like
node.warn("startTimer: " + new Date(startTimer))

Thanks Colin.
I was merely illustrating that the stored epoch millis were significantly different (in the region of 24 seconds or 1445014ms), even though the values were stored and reported on, using node.warn() within a second of each other, which is why I quoted the timestamps. A message goes through the flow at least every 10 secs (humidity and temperature), so 1445014ms is an unexpectedly long time. I realise the stored values are in ms and I need that to find the difference and see if it exceeds the timer period (also ms).

Thanks Steve
I have an ESP8266 with attached

  • SHT30
  • button
  • red LED
  • green LED

Every 10s, this publishes temperature and humidity on esp/dht/b/temp and esp/dht/b/hum
Every time the button is pressed, there's logic that increments the fan mode
( fanOff | fanOn10 | humidityTriggered ) and publishes that on esp/dht/b/fanState . It also sets the LEDs to the correct state
( fanOff=red on | fanOn10=green on | humiditytriggered=both off )

It's also intended to subscribe to /esp/dht/b/fanStateNR, so that if the NR dashboard control for setting the fan mode changes, the ESP's mode and LEDs stay in sync

I have my NR flow which subscribes to:

  • esp/dht/b/temp - goes straight to the dashboard (instance and historical graphs)
  • esp/dht/b/hum - goes straight to the dashboard (instance and historical graphs) but also the function node
  • esp/dht/b/fanState - goes to the function node, which does the following:
    • is it a humidity message - if yes and the value is > than 90% publish "ON" on esp/dht/b/fanSwitch. Otherwise publish "OFF"
    • Is it a fanState message -
      • if state = fanOff, publish "OFF" on esp/dht/b/fanSwitch
      • if state = fanOn10, capture the current epoch time as startTime and publish "ON" on esp/dht/b/fanSwitch.
      • if state = humidityTriggered, publish "OFF" on esp/dht/b/fanSwitch - the next time through, the fan will be controlled by the humidity
  • for all messages I check if the mode is currently fanOn10 and if it is I check whether the time now, minus the startTime exceeds the interval period (in ms) and if it does, I change the mode back to humiditytrigger and reset the startTimer.

and publishes to:

  • esp/dht/b/fanStateNR - which will be subscribed to by the ESP + sensor (see above)
  • esp/dht/b/fanSwitch - which is subscribed to by the other ESP + relay, which controls the fan

I have an ESP8266 with attached:

  • Relay,

which subscribes to:

  • esp/dht/b/fanSwitch - which turns the extractor fan on or off

If you need it, I can also attach my flow but I'm not sure how to capture the messages in a useful way
would mosquitto_sub -h <MQTT server IP addr> -t esp/dht/b/# >> messages.txt do it?

You can use the debug side bar "copy value" with debug node set to show complete message. That will capture a perfect+complete msg (as JSON) which you can use in a function node to simulate your data - and thus provide a fully working demo.

E.g.Function node...

return { pasted msg JSON } ;

I don't know what you mean by that. The now timestamp is the current time, but the startTime is the time from the last time it was written to the context, which should be the last time you saw the output from this line in the debug
node.warn('fan turned on and timer started at :'+ startTimer)

I notice in the section of code below where it says
node.warn('Resetting timer!');
startTimer = 0;
you have not written startTimer out to context.

When writing such functions I usually flow.get() all the data at the start and flow.set() everything at the end, even if it may not have changed. That removes the possibility of updating the local variable and forgetting to save it.

By the way, if the context data is purely for use within the function you should use context.get() and context.set() rather than flow.

Working Flow:

[
    {
        "id": "8b72e4c9a2f439e5",
        "type": "tab",
        "label": "Bathroom",
        "disabled": false,
        "info": ""
    },
    {
        "id": "ef3ff83eaafada3b",
        "type": "mqtt in",
        "z": "8b72e4c9a2f439e5",
        "name": "Temperature",
        "topic": "esp/dht/b/temp",
        "qos": "1",
        "datatype": "json",
        "broker": "d232adcfdae4f31a",
        "nl": false,
        "rap": true,
        "rh": 0,
        "x": 390,
        "y": 80,
        "wires": [
            [
                "f84e0f488406be5a",
                "2e77932274aae2d4",
                "38469f49d714040b"
            ]
        ]
    },
    {
        "id": "f84e0f488406be5a",
        "type": "ui_gauge",
        "z": "8b72e4c9a2f439e5",
        "name": "BathroomTemp",
        "group": "1bc7dc824c2874aa",
        "order": 2,
        "width": "1",
        "height": "1",
        "gtype": "gage",
        "title": "",
        "label": "°C",
        "format": "{{value}}",
        "min": 0,
        "max": "30",
        "colors": [
            "#00b500",
            "#e6e600",
            "#ca3838"
        ],
        "seg1": "18",
        "seg2": "25",
        "className": "",
        "x": 620,
        "y": 80,
        "wires": []
    },
    {
        "id": "2e77932274aae2d4",
        "type": "ui_chart",
        "z": "8b72e4c9a2f439e5",
        "name": "BathroomTempHistory",
        "group": "1bc7dc824c2874aa",
        "order": 1,
        "width": 0,
        "height": 0,
        "label": "Temp",
        "chartType": "line",
        "legend": "false",
        "xformat": "HH:mm:ss",
        "interpolate": "linear",
        "nodata": "",
        "dot": false,
        "ymin": "0",
        "ymax": "35",
        "removeOlder": 1,
        "removeOlderPoints": "",
        "removeOlderUnit": "3600",
        "cutout": 0,
        "useOneColor": false,
        "useUTC": false,
        "colors": [
            "#1f77b4",
            "#aec7e8",
            "#ff7f0e",
            "#2ca02c",
            "#98df8a",
            "#d62728",
            "#ff9896",
            "#9467bd",
            "#c5b0d5"
        ],
        "outputs": 1,
        "useDifferentColor": false,
        "className": "",
        "x": 640,
        "y": 120,
        "wires": [
            []
        ]
    },
    {
        "id": "1e4ad1236e8a9363",
        "type": "mqtt in",
        "z": "8b72e4c9a2f439e5",
        "d": true,
        "name": "",
        "topic": "esp/dht/#",
        "qos": "2",
        "datatype": "auto",
        "broker": "d232adcfdae4f31a",
        "nl": false,
        "rap": true,
        "rh": 0,
        "x": 400,
        "y": 1100,
        "wires": [
            [
                "9a5951e05defcdf9"
            ]
        ]
    },
    {
        "id": "9a5951e05defcdf9",
        "type": "debug",
        "z": "8b72e4c9a2f439e5",
        "d": true,
        "name": "",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 930,
        "y": 1100,
        "wires": []
    },
    {
        "id": "38469f49d714040b",
        "type": "debug",
        "z": "8b72e4c9a2f439e5",
        "name": "Temperature",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 610,
        "y": 40,
        "wires": []
    },
    {
        "id": "c9570e0491311b68",
        "type": "ui_slider",
        "z": "8b72e4c9a2f439e5",
        "name": "Fan-on timer (mins)",
        "label": "slider",
        "tooltip": "",
        "group": "b19d6a6aef08844e",
        "order": 2,
        "width": 0,
        "height": 0,
        "passthru": true,
        "outs": "end",
        "topic": "fanTimerInterval",
        "topicType": "msg",
        "min": "1",
        "max": 10,
        "step": 1,
        "className": "",
        "x": 1270,
        "y": 620,
        "wires": [
            [
                "ab887a36f677fbad",
                "0f12394758bd3e82"
            ]
        ]
    },
    {
        "id": "d41385b7559b848f",
        "type": "comment",
        "z": "8b72e4c9a2f439e5",
        "name": "",
        "info": "Passes SHT30 sensor's temperature reading and shows it on the dashboard as a large historical trace and a small instance dial/value",
        "x": 220,
        "y": 80,
        "wires": []
    },
    {
        "id": "08b8d633b10ab20e",
        "type": "comment",
        "z": "8b72e4c9a2f439e5",
        "name": "",
        "info": "Passes SHT30 sensor's humidity reading and shows it on the dashboard as a large historical trace and a small instance dial/value\n\nThe function node measures the humidity against a threshold value (90%) and if the reading exceeds that it turns the fan on by publishing a FanState message (picked up by the ESP8266 that controls the fan)",
        "x": 220,
        "y": 240,
        "wires": []
    },
    {
        "id": "31d050f97f5c1129",
        "type": "comment",
        "z": "8b72e4c9a2f439e5",
        "name": "",
        "info": "The timer triggers every 5s to make the function check if the fan mode is 'fanOn10' and if so whether the time it's been on exceeds the interval value. If the time has been exceeded it sets the timer back to 0, the mode to 'humidityTrigger' and it publishes the mode on 'esp/dht/b/fanStateNR', so the ESP with the sensor can sync its mode and LED states with the dashboard\n\nThe interval has a default of 300k ms (5min). The slider on the Dashboard chan change this flow variable between 1 & 10 mins.",
        "x": 220,
        "y": 400,
        "wires": []
    },
    {
        "id": "ad1983652e476e88",
        "type": "comment",
        "z": "8b72e4c9a2f439e5",
        "name": "",
        "info": "The dashboard includes a 1-10 slider. The function node sets the flow interval variable with this value * 60000.",
        "x": 1080,
        "y": 620,
        "wires": []
    },
    {
        "id": "7b1515e4b537caab",
        "type": "mqtt in",
        "z": "8b72e4c9a2f439e5",
        "name": "Humidity",
        "topic": "esp/dht/b/humid",
        "qos": "0",
        "datatype": "json",
        "broker": "d232adcfdae4f31a",
        "nl": false,
        "rap": false,
        "rh": 0,
        "x": 400,
        "y": 240,
        "wires": [
            [
                "38986d99d372b31d",
                "e0729cfdcfcaa9a9",
                "ad7ba5c36dda50e6",
                "5a3b1c94b2f52243"
            ]
        ]
    },
    {
        "id": "480ff91b0d684a1c",
        "type": "mqtt out",
        "z": "8b72e4c9a2f439e5",
        "name": "FanSwitch",
        "topic": "bathroom/fanSwitch",
        "qos": "2",
        "retain": "false",
        "respTopic": "",
        "contentType": "text/plain",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "d232adcfdae4f31a",
        "x": 1030,
        "y": 280,
        "wires": []
    },
    {
        "id": "6a684d4e3c6d91d7",
        "type": "debug",
        "z": "8b72e4c9a2f439e5",
        "name": "FanSwitchReport",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1050,
        "y": 360,
        "wires": []
    },
    {
        "id": "38986d99d372b31d",
        "type": "debug",
        "z": "8b72e4c9a2f439e5",
        "name": "Humidity",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 600,
        "y": 200,
        "wires": []
    },
    {
        "id": "e0729cfdcfcaa9a9",
        "type": "ui_gauge",
        "z": "8b72e4c9a2f439e5",
        "name": "BathroomHumidity",
        "group": "e4779998dd3b61c9",
        "order": 2,
        "width": "1",
        "height": "1",
        "gtype": "gage",
        "title": "",
        "label": "%",
        "format": "{{value}}",
        "min": 0,
        "max": "100",
        "colors": [
            "#00b500",
            "#e6e600",
            "#ca3838"
        ],
        "seg1": "50",
        "seg2": "80",
        "className": "",
        "x": 630,
        "y": 240,
        "wires": []
    },
    {
        "id": "ad7ba5c36dda50e6",
        "type": "ui_chart",
        "z": "8b72e4c9a2f439e5",
        "name": "BathroomHumidityHistory",
        "group": "e4779998dd3b61c9",
        "order": 1,
        "width": 0,
        "height": 0,
        "label": "Humidity",
        "chartType": "line",
        "legend": "false",
        "xformat": "HH:mm:ss",
        "interpolate": "linear",
        "nodata": "",
        "dot": false,
        "ymin": "",
        "ymax": "",
        "removeOlder": 1,
        "removeOlderPoints": "",
        "removeOlderUnit": "3600",
        "cutout": 0,
        "useOneColor": false,
        "useUTC": false,
        "colors": [
            "#1f77b4",
            "#aec7e8",
            "#ff7f0e",
            "#2ca02c",
            "#98df8a",
            "#d62728",
            "#ff9896",
            "#9467bd",
            "#c5b0d5"
        ],
        "outputs": 1,
        "useDifferentColor": false,
        "className": "",
        "x": 650,
        "y": 280,
        "wires": [
            []
        ]
    },
    {
        "id": "360e440fcbb6dbd4",
        "type": "mqtt in",
        "z": "8b72e4c9a2f439e5",
        "name": "FanStateIn",
        "topic": "esp/dht/b/fanState",
        "qos": "2",
        "datatype": "auto",
        "broker": "d232adcfdae4f31a",
        "nl": false,
        "rap": true,
        "rh": 0,
        "x": 400,
        "y": 480,
        "wires": [
            [
                "f1c8b9956d494710",
                "92b01a2852bbd2c4"
            ]
        ]
    },
    {
        "id": "fca1aba52eac712a",
        "type": "debug",
        "z": "8b72e4c9a2f439e5",
        "name": "DashboardMode",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1550,
        "y": 400,
        "wires": []
    },
    {
        "id": "d279dabf5f459856",
        "type": "ui_dropdown",
        "z": "8b72e4c9a2f439e5",
        "name": "FanMode",
        "label": "Fan mode",
        "tooltip": "",
        "place": "Select option",
        "group": "b19d6a6aef08844e",
        "order": 2,
        "width": 0,
        "height": 0,
        "passthru": false,
        "multiple": false,
        "options": [
            {
                "label": "Turn fan off",
                "value": "fanOff",
                "type": "str"
            },
            {
                "label": "Turn fan on for a few mins",
                "value": "fanOn10",
                "type": "str"
            },
            {
                "label": "Fan on Auto (humidity > 90%)",
                "value": "humidityTrigger",
                "type": "str"
            }
        ],
        "payload": "",
        "topic": "DashSync",
        "topicType": "msg",
        "className": "",
        "x": 1260,
        "y": 400,
        "wires": [
            [
                "fca1aba52eac712a",
                "0f12394758bd3e82"
            ]
        ]
    },
    {
        "id": "2501240f4c2d1391",
        "type": "mqtt out",
        "z": "8b72e4c9a2f439e5",
        "name": "FanStateNR",
        "topic": "esp/dht/b/fanStateNR",
        "qos": "2",
        "retain": "false",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "d232adcfdae4f31a",
        "x": 1030,
        "y": 460,
        "wires": []
    },
    {
        "id": "f1c8b9956d494710",
        "type": "debug",
        "z": "8b72e4c9a2f439e5",
        "name": "ESPfanState",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 610,
        "y": 540,
        "wires": []
    },
    {
        "id": "ae18377fbc948a88",
        "type": "debug",
        "z": "8b72e4c9a2f439e5",
        "name": "FanStateNR",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1030,
        "y": 520,
        "wires": []
    },
    {
        "id": "ab887a36f677fbad",
        "type": "debug",
        "z": "8b72e4c9a2f439e5",
        "name": "TimerInterval(ms)",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1550,
        "y": 680,
        "wires": []
    },
    {
        "id": "62535dbc893fc724",
        "type": "comment",
        "z": "8b72e4c9a2f439e5",
        "name": "",
        "info": "The ESP8266 with the humidity sensor also has a button to toggle between turning the fan OFF, ON for a period or let it be controlled by the humidity readings. This node uses the FanState subscription to monitor which state the ESP8266 is in and reacts accordingly\n\nThe function looks for fan mode changes and really only reacts if it detects fanOn10 (timed mode).  In this case, it starts the timer flow variable",
        "x": 220,
        "y": 480,
        "wires": []
    },
    {
        "id": "02437886c7b3dc14",
        "type": "comment",
        "z": "8b72e4c9a2f439e5",
        "name": "",
        "info": "The function that checks expiry of the timer interval feeds the fan mode through to the dropdown mode selector to make sure it relects the current mode. Particularly where time has expired and the mode falls back from fanOn10 to humidityTrigger \n\nThe function sets the flow variable for the fan mode, when changed from the dashboard",
        "x": 1260,
        "y": 360,
        "wires": []
    },
    {
        "id": "92b01a2852bbd2c4",
        "type": "function",
        "z": "8b72e4c9a2f439e5",
        "name": "Fan state switch",
        "func": "var requiredFanState    = String(flow.get('requiredFanState')) || \"humidityTrigger\";\nvar startTimer          = flow.get('startTimer') || 0;\nvar prevFanState        = String(context.get('prevFanState')) ||  \"humidityTrigger\";\nvar fanStateNR          = {payload: null, topic: 'esp/dht/b/fanStateNR'};\nvar fanSwitch          = {payload: null, topic: 'esp/dht/b/fanSwitch'};\n\nrequiredFanState        = msg.payload; \nfanStateNR.payload      = requiredFanState;\n\nif ((requiredFanState == 'humidityTrigger') && (prevFanState != 'humidityTrigger')) {\n    prevFanState = requiredFanState;\n    fanSwitch.payload   = \"OFF\";\n} else if ((requiredFanState == 'fanOff') && (prevFanState != 'fanOff')) {\n    prevFanState = requiredFanState;\n    fanSwitch.payload   = \"OFF\";\n} else if ((requiredFanState == 'fanOn10') && (startTimer == 0)  && (prevFanState != 'fanOn10')) {\n    prevFanState = requiredFanState;\n    startTimer          =  Date.now();\n    flow.set('startTimer', Date.now()); \n    fanSwitch.payload   = \"ON\";\n}    \n\n//Set persistent variables (whether changed or not)\nflow.set('requiredFanState', requiredFanState);\nflow.set('startTimer', startTimer);\ncontext.set('prevFanState', prevFanState);\n\nreturn [ fanSwitch, fanStateNR ];",
        "outputs": 2,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 620,
        "y": 480,
        "wires": [
            [
                "480ff91b0d684a1c",
                "6a684d4e3c6d91d7",
                "d279dabf5f459856"
            ],
            [
                "2501240f4c2d1391",
                "ae18377fbc948a88"
            ]
        ]
    },
    {
        "id": "5a3b1c94b2f52243",
        "type": "function",
        "z": "8b72e4c9a2f439e5",
        "name": "Humidity threshold check",
        "func": "//Gather persistent variables\nvar humidity            = flow.get('humidity') || 50;  \nvar humidityThreshold   = flow.get('humidityThreshold') || 90;\nvar requiredFanState    = String(flow.get('requiredFanState')) || \"humidityTrigger\";\nvar startTimer          = flow.get('startTimer') || 0;\nvar prevFanState        = String(flow.get('prevFanState')) ||  \"humidityTrigger\";\nvar prevFanSwitch       = String(flow.get('prevFanSwitch')) || \"OFF\";\n\n//set defaults for messages: payload & topic\n//FanSwitch is subscribed by the ESP8266 controlling the fan relay. It has valid values of 'ON' and 'OFF'\nvar fanSwitch           = {payload: null, topic: 'bathroom/fanSwitch'};\n\nhumidity = msg.payload;\n//fanSwitch.payload = prevFanSwitch;\n\nif (requiredFanState == 'humidityTrigger') { //changed to humidity\n\n    if (requiredFanState != prevFanState){ //notify once per change only\n        node.warn('fan controlled by humidity level :'+ humidityThreshold+'%');\n        prevFanState = requiredFanState;\n    }\n    \n    //monitor threshold humidity\n    if ((humidity >= humidityThreshold) && (prevFanSwitch == \"OFF\")) { //only issue OFF\n        fanSwitch.payload = 'ON';\n        prevFanSwitch = \"ON\";\n    } else if ((humidity < humidityThreshold) && (prevFanSwitch == \"ON\")){\n        fanSwitch.payload = 'OFF';\n        prevFanSwitch = \"OFF\";\n    }\n    startTimer = 0;\n    \n} else if ((requiredFanState == 'fanOff') && (prevFanState != 'fanOff')) { //the FIRST fanOff message only\n    prevFanState = requiredFanState;\n    fanSwitch.payload   = 'OFF';\n    startTimer = 0;\n    \n} else if ((requiredFanState == 'fanOn10') && (startTimer == 0)  && (prevFanState != 'fanOn10')) { //The FIRST period message only\n    prevFanState = requiredFanState;\n    startTimer          =  Date.now();\n    fanSwitch.payload   = 'ON';\n}\n\n//Set persistent variables (whether changed or not)\nflow.set('humidity', humidity);  \nflow.set('humidityThreshold', humidityThreshold);\nflow.set('requiredFanState', requiredFanState);\nflow.set('startTimer', startTimer);\nflow.set('prevFanState', prevFanState);\nflow.set('prevFanSwitch', prevFanSwitch);\nif (fanSwitch.payload != null){\n    return fanSwitch;\n}",
        "outputs": 1,
        "noerr": 0,
        "initialize": "// Code added here will be run once\n// whenever the node is started.\nflow.set('humidityThreshold', 90);",
        "finalize": "",
        "libs": [],
        "x": 650,
        "y": 320,
        "wires": [
            [
                "480ff91b0d684a1c",
                "6a684d4e3c6d91d7"
            ]
        ]
    },
    {
        "id": "0f12394758bd3e82",
        "type": "function",
        "z": "8b72e4c9a2f439e5",
        "name": "Dashboard management",
        "func": "//message comes from dashboard timer slider or mode select DD\nvar fanState           = {payload: null, topic: 'esp/dht/b/fanState'};\n\nif (msg.payload * 1){  //it's a number from the timerslider\n  flow.set('fanTimerInterval', msg.payload*60000);\n} else  { //it's a string // it's a string from the mode dropdown\n  flow.set('requiredFanState', msg.payload);\n  fanState.payload = msg.payload;\n  return fanState; //Send the state into the fanStateSwitch function (as if it had come from the ESP+SHT30)\n}\n",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1570,
        "y": 620,
        "wires": [
            [
                "92b01a2852bbd2c4",
                "52e6fe2c7206f423"
            ]
        ]
    },
    {
        "id": "f271a7787eaad9c6",
        "type": "inject",
        "z": "8b72e4c9a2f439e5",
        "name": "Check timer",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "5",
        "crontab": "",
        "once": true,
        "onceDelay": 0.1,
        "topic": "",
        "payloadType": "date",
        "x": 390,
        "y": 400,
        "wires": [
            [
                "86ef298a4cc5b27f"
            ]
        ]
    },
    {
        "id": "86ef298a4cc5b27f",
        "type": "function",
        "z": "8b72e4c9a2f439e5",
        "name": "Fan Interval checker",
        "func": "//Gather persistent variables\nvar requiredFanState    = String(flow.get('requiredFanState')) || \"humidityTrigger\";\nvar startTimer          = flow.get('startTimer') || 0;\nvar fanTimerInterval    = flow.get('fanTimerInterval') || 300000; //milliseconds (1 minute = 60000ms)\nvar fanSwitch           = {payload: null, topic: 'esp/dht/b/fanSwitch'};\nvar fanStateNR          = {payload: null, topic: 'esp/dht/b/fanStateNR'};\nvar DashSync            = {payload: null, topic: 'DashSync'};\n//var prevFanSwitch       = String(flow.get('prevFanSwitch')) || \"OFF\";\n\n//If the timer has expired, reset the timer, \n//send a message to turn the fan off and change the persistent \n//flow variable to reflect the resumed default mode\n\nif (requiredFanState == 'fanOn10') {\n        var progress = (Date.now()-startTimer);\n        node.warn('timer progress:'+ progress + 'to:'+ fanTimerInterval);\n        \n    if ((progress >=  fanTimerInterval) && (startTimer > 0)) {\n        node.warn('Resetting timer! and changing mode to: humidityTrigger');\n        startTimer          = 0;\n        flow.set('startTimer',startTimer);\n        \n        fanSwitch.payload   = 'OFF';\n        \n        requiredFanState    = \"humidityTrigger\";\n        \n        fanStateNR.payload  = requiredFanState;\n        \n        DashSync.payload    = requiredFanState;\n        \n        flow.set('requiredFanState',requiredFanState); //set the flow params on exit, when there IS change\n        flow.set('fanTimerInterval',fanTimerInterval);\n        \n        node.warn(\"timer exceeded: fanSwitch = \"+fanSwitch.payload);\n        node.warn(\"timer exceeded: fanStateNR = \"+fanStateNR.payload);\n        node.warn(\"timer exceeded: DashSync = \"+DashSync.payload);\n        \n        return [ DashSync, fanSwitch, fanStateNR ];\n        \n    }\n    flow.set('requiredFanState',requiredFanState); //set the flow params on exit, even if there's no change\n    flow.set('fanTimerInterval',fanTimerInterval);\n    //flow.set('prevFanSwitch', fanSwitch.payload);\n    return [ DashSync, fanSwitch, fanStateNR ];\n} // else if (requiredFanState == 'humidityTrigger') {\n//     fanStateNR.payload  = requiredFanState;\n//     flow.set('requiredFanState',requiredFanState); //set the flow params on exit, when there IS change\n\n\n\n",
        "outputs": 3,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 640,
        "y": 400,
        "wires": [
            [
                "d279dabf5f459856"
            ],
            [
                "480ff91b0d684a1c"
            ],
            [
                "2501240f4c2d1391"
            ]
        ]
    },
    {
        "id": "52e6fe2c7206f423",
        "type": "debug",
        "z": "8b72e4c9a2f439e5",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1900,
        "y": 620,
        "wires": []
    },
    {
        "id": "d232adcfdae4f31a",
        "type": "mqtt-broker",
        "name": "Mosquitto",
        "broker": "192.168.7.245",
        "port": "1883",
        "clientid": "",
        "usetls": false,
        "protocolVersion": "5",
        "keepalive": "60",
        "cleansession": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willPayload": "",
        "willMsg": {},
        "sessionExpiry": ""
    },
    {
        "id": "1bc7dc824c2874aa",
        "type": "ui_group",
        "name": "Temp",
        "tab": "31c7884d06c62833",
        "order": 2,
        "disp": true,
        "width": "6",
        "collapse": false,
        "className": ""
    },
    {
        "id": "b19d6a6aef08844e",
        "type": "ui_group",
        "name": "Fan",
        "tab": "31c7884d06c62833",
        "order": 3,
        "disp": true,
        "width": "6",
        "collapse": false,
        "className": ""
    },
    {
        "id": "e4779998dd3b61c9",
        "type": "ui_group",
        "name": "Humidity",
        "tab": "31c7884d06c62833",
        "order": 1,
        "disp": true,
        "width": "6",
        "collapse": false,
        "className": ""
    },
    {
        "id": "31c7884d06c62833",
        "type": "ui_tab",
        "name": "Bathroom",
        "icon": "dashboard",
        "disabled": false,
        "hidden": false
    }
]

ESP+Humidity Sensor (SHT30)+mode switch+red LED+green LED


// #define & #include lines mustn't end with ;

#include "Wire.h" //I2C library
#include "SHT31.h" //I2C sensor library
#include <ESP8266WiFi.h>
#include <Ticker.h> // non-blocking delay library
#include <AsyncMqttClient.h>
#include <stdio.h>

#define WIFI_SSID "******"
#define WIFI_PASSWORD "*******"

// Raspberri Pi Mosquitto MQTT Broker
#define MQTT_HOST IPAddress(192, 168, 7, 245)
#define MQTT_PORT 1883
String hostname = "BathroomHumidTemp";

// Temperature & humidity MQTT topics
#define MQTT_PUB_TEMP "esp/dht/b/temp"
#define MQTT_PUB_HUM "esp/dht/b/humid"

// Fan control PUB
#define MQTT_PUB_FAN_STATE "esp/dht/b/fanState"
// Fan control SUB
#define MQTT_SUB_FAN_STATE "esp/dht/b/fanState"
#define MQTT_SUB_FAN_RESUME "esp/dht/b/fanresume"
#define MQTT_SUB_FAN_STATENR "esp/dht/b/fanStateNR"

// I2C & DHT
uint32_t start;
uint32_t stop;
// Initialize DHT sensor
SHT31 sht;

// pin assignments:
const int BUTTON_PIN = D7;  // the number of the pushbutton pin
const int RED_LED_PIN =  D6;   // the number of the RED LED pin
const int GREEN_LED_PIN =  D5;   // the number of the GREEN LED pin

int buttonState = 0;   // variable for reading the pushbutton status
int latchState = 2;
bool published = true; //don't keep bombarding the server once the message is sent
   
// Variables to hold sensor readings
float temp;
float hum;

AsyncMqttClient mqttClient;
Ticker mqttReconnectTimer;

WiFiEventHandler wifiConnectHandler;
WiFiEventHandler wifiDisconnectHandler;
Ticker wifiReconnectTimer;

unsigned long previousReportMillis = 0;   // Stores last time temperature & humidity were published
unsigned long previousDebounceMillis = 0; // Stores last time button was pressed
unsigned long previousPubMillis = 0;      // Stores last time button press was published
const long    reportInterval = 10000;     // reportInterval at which to publish sensor readings
const long    debounceInterval = 500;     // minimum button press time
const long    pubInterval = 1000;         // minimum button press report interval

void connectToWifi() {
  Serial.println("Connecting to Wi-Fi...");
  WiFi.setHostname(hostname.c_str());
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}

void onWifiConnect(const WiFiEventStationModeGotIP& event) {
  Serial.println("Connected to Wi-Fi.");
  connectToMqtt();
}

void onWifiDisconnect(const WiFiEventStationModeDisconnected& event) {
  Serial.println("Disconnected from Wi-Fi.");
  mqttReconnectTimer.detach(); // ensure we don't reconnect to MQTT while reconnecting to Wi-Fi
  wifiReconnectTimer.once(2, connectToWifi);
}

void connectToMqtt() {
  Serial.println("Connecting to MQTT...");
  mqttClient.connect();
}

void onMqttConnect(bool sessionPresent) {
  Serial.println("Connected to MQTT.");
  Serial.print("Session present: ");
  Serial.println(sessionPresent);

  uint16_t packetIdSub1 = mqttClient.subscribe(MQTT_SUB_FAN_RESUME, 2);

  Serial.print("Subscribing at QoS 2, packetId: ");
  Serial.println(packetIdSub1);
}

void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
  Serial.println("Disconnected from MQTT.");

  if (WiFi.isConnected()) {
    mqttReconnectTimer.once(2, connectToMqtt);
  }
}

void onMqttSubscribe(uint16_t packetId, uint8_t qos) {
  Serial.println("Subscribe acknowledged.");
  Serial.print("  packetId: ");
  Serial.println(packetId);
  Serial.print("  qos: ");
  Serial.println(qos);
}

void onMqttUnsubscribe(uint16_t packetId) {
  Serial.println("Unsubscribe acknowledged.");
  Serial.print("  packetId: ");
  Serial.println(packetId);
}

void onMqttPublish(uint16_t packetId) {
  Serial.print("Publish acknowledged.");
  Serial.print("  packetId: ");
  Serial.println(packetId);
}

void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {
  Serial.println("Publish received.");
  Serial.print("  topic: ");
  Serial.println(topic);
  Serial.print("  payload:  ");
  Serial.println(payload);
//  Serial.print("  qos: ");
//  Serial.println(properties.qos);
//  Serial.print("  dup: ");
//  Serial.println(properties.dup);
//  Serial.print("  retain: ");
//  Serial.println(properties.retain);
//  Serial.print("  len: ");
//  Serial.println(len);
//  Serial.print("  index: ");
//  Serial.println(index);
//  Serial.print("  total: ");
//  Serial.println(total);
  if (String(topic) == MQTT_SUB_FAN_STATENR){ //esp/dht/b/fanStateNR
    if (payload == 'fanOff'){
      latchState = 0;
      Serial.printf("Fan off. latchState: %i ", latchState);
      published = false;
      digitalWrite(RED_LED_PIN, HIGH); // turn off red LED
      digitalWrite(GREEN_LED_PIN, LOW); // turn off green LED
    } else if (String(payload) == 'fanOn10'){
      latchState = 1;
      Serial.printf("Fan on for a period. latchState: %i ", latchState);
      published = false;
      digitalWrite(RED_LED_PIN, LOW); // turn off red LED
      digitalWrite(GREEN_LED_PIN, HIGH); // turn off green LED
    } else if (String(payload) == 'humidityTrigger'){
      latchState = 2;
      Serial.printf("Fan controlled by humidity threshold. latchState: %i ", latchState);
      published = false;
      digitalWrite(RED_LED_PIN, LOW); // turn off red LED
      digitalWrite(GREEN_LED_PIN, LOW); // turn off green LED
    } 
  }
}


void stopFan(){
    // Publish an MQTT message on topic esp/dht/b/fanOff
    uint16_t packetIdPub2 = mqttClient.publish(MQTT_PUB_FAN_STATE, 1, true, String("fanOff").c_str());
    Serial.printf("Publishing on topic %s at QoS 1, packetId: %i ", MQTT_PUB_FAN_STATE, packetIdPub2);
    Serial.printf("Message: %s \n", "Fan OFF & red LED on. <");
    digitalWrite(RED_LED_PIN, HIGH); // turn on red LED
    digitalWrite(GREEN_LED_PIN, LOW); // turn off green LED
    published = true; 
}

void manFan(){
    // Publish an MQTT message on topic esp/dht/b/fanOn10
    uint16_t packetIdPub2 = mqttClient.publish(MQTT_PUB_FAN_STATE, 1, true, String("fanOn10").c_str());
    Serial.printf("Publishing on topic %s at QoS 1, packetId: %i ", MQTT_PUB_FAN_STATE, packetIdPub2);
    Serial.printf("Message: %s \n", "Fan ON for 10 minutes & green LED. <==");
    digitalWrite(GREEN_LED_PIN, HIGH); // turn on greenLED
    digitalWrite(RED_LED_PIN, LOW); // turn off red LED
    published = true;
}

void humidityFan(){
    // Publish an MQTT message on topic esp/dht/b/fanState
    uint16_t packetIdPub2 = mqttClient.publish(MQTT_PUB_FAN_STATE, 1, true, String("humidityTrigger").c_str());
    Serial.printf("Publishing on topic %s at QoS 1, packetId: %i ", MQTT_PUB_FAN_STATE, packetIdPub2);
    Serial.printf("Message: %s \n", "Fan resume humidity threshold switch & both LEDs off. <===");
    digitalWrite(RED_LED_PIN, LOW); // turn off red LED
    digitalWrite(GREEN_LED_PIN, LOW); // turn off green LED  
    published = true;
}

void setup() {
  Serial.begin(115200);
  // initialize the LED pins as an output:
  pinMode(RED_LED_PIN, OUTPUT);
  pinMode(GREEN_LED_PIN, OUTPUT);
  // initialize the pushbutton pin as an pull-up input:
  // the pull-up input pin will be HIGH when the switch is open and LOW when the switch is closed.
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  
  Serial.println();
  Wire.begin();
  sht.begin(0x44);    //Sensor I2C Address
  Wire.setClock(100000);
  uint16_t stat = sht.readStatus();
  Serial.print(stat, HEX);
  Serial.println();
  
  wifiConnectHandler = WiFi.onStationModeGotIP(onWifiConnect);
  wifiDisconnectHandler = WiFi.onStationModeDisconnected(onWifiDisconnect);

  mqttClient.onConnect(onMqttConnect);
  mqttClient.onDisconnect(onMqttDisconnect);
  mqttClient.onSubscribe(onMqttSubscribe);
  //mqttClient.onUnsubscribe(onMqttUnsubscribe);
  mqttClient.onMessage(onMqttMessage);
  mqttClient.onPublish(onMqttPublish);
  mqttClient.setServer(MQTT_HOST, MQTT_PORT);
  // If your broker requires authentication (username and password), set them below
  //mqttClient.setCredentials("REPlACE_WITH_YOUR_USER", "REPLACE_WITH_YOUR_PASSWORD");
  
  connectToWifi();
}

void loop() {
  unsigned long currentMillis = millis();

  if ((currentMillis - previousPubMillis >= pubInterval) && (published == true)){ //reset the 'published' flag after 1000ms
    previousPubMillis = currentMillis;
    published = false;
  }

  // read the state of the pushbutton value:
  buttonState = digitalRead(BUTTON_PIN);
  // cycle three states 0 = Fan OFF | 1 = Timed Fan ON | 2 = Humidity trigger (default)
  if((buttonState == LOW) && (currentMillis - previousDebounceMillis >= debounceInterval) && (published == false)) { // If button is being pressed
    // reset the debounce timer
    previousDebounceMillis = currentMillis;
    latchState += 1;
    if (latchState > 2) {
      latchState = 0;
    }

    if        (latchState == 0){  // 0 = Fan OFF
      stopFan();
    } else if (latchState == 1){  // 1 = Timed Fan ON (time controlled in NodeRed flow
      manFan();
    }else if  (latchState == 2){  // 2 = Humidity trigger (default)
      humidityFan();
    }
  }
    
   // Every X number of seconds (reportInterval = 10 seconds) 
  // it publishes a new MQTT message
  if (currentMillis - previousReportMillis >= reportInterval) {
    // Save the last time a new reading was published
    previousReportMillis = currentMillis;
    sht.read();
    // New DHT sensor readings
    hum = sht.getHumidity();
    // Read temperature as Celsius (the default)
    temp = sht.getTemperature();
    
    // Publish an MQTT message on topic esp/dht/b/temp
    uint16_t packetIdPub3 = mqttClient.publish(MQTT_PUB_TEMP, 1, true, String(temp).c_str());                            
    Serial.printf("Publishing on topic %s at QoS 1, packetId: %i ", MQTT_PUB_TEMP, packetIdPub3);
    Serial.printf("Message: %.2f \n", temp);

    // Publish an MQTT message on topic esp/dht/b/humid
    uint16_t packetIdPub4 = mqttClient.publish(MQTT_PUB_HUM, 1, true, String(hum).c_str());                            
    Serial.printf("Publishing on topic %s at QoS 1, packetId %i: ", MQTT_PUB_HUM, packetIdPub4);
    Serial.printf("Message: %.2f \n", hum);
  }
}

ESP8266+relay

#include <ESP8266WiFi.h>

#include <PubSubClient.h>

// Update these with values suitable for your network.
const char* ssid = "*****";
const char* password = "*****";
const char* mqtt_server = "192.168.7.245";
//const char* mqtt_port = "1883";

WiFiClient espClient;
PubSubClient client(espClient);
int FanRelay = 13;
String switch1;
String strTopic;
String strPayload;

void setup_wifi() {

  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) 
  {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void callback(char* topic, byte* payload, unsigned int length) {
  payload[length] = '\0';
  strTopic = String((char*)topic);
  Serial.print(strTopic);
  Serial.print("\n");
  if(strTopic == "bathroom/fanSwitch")
    {
    switch1 = String((char*)payload);
    //Serial.print(switch1+"\n");
    if(switch1 == "ON") 
      { 
        Serial.println("message = ON \nTurn relay on");
        digitalWrite(FanRelay, HIGH);
      }
    else if(switch1 == "OFF")
      {
        Serial.println("message = OFF \nTurn relay off");
        digitalWrite(FanRelay, LOW);
      }
    }
}
 
 
void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect("arduinoClient")) {
      Serial.println("connected");
      // Once connected, publish an announcement...
      client.subscribe("bathroom/fanSwitch");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}
 
void setup()
{
  Serial.begin(115200);
  setup_wifi(); 
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);

  pinMode(FanRelay, OUTPUT);
  digitalWrite(FanRelay, HIGH);
}
 
void loop()
{
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
}