Martin's corner on the web

DIY Internet of Things Fire Alarm

I purchased a battery operated smoke/fire alarm few days ago and it showed up today. It runs on 9V and will make a loud sound if smoke is detected. My intention was to hook it up with my home automation system so that I would receive alert if it would go off including SMS, pushbullet notification to my phone, email etc.

The Funky v1 is ideal for the purpose because it is really flat/tiny and would fit inside the alarm. It will tap into the piezo siren and sleep until the siren is activated. Upon activation, it will make a wireless transmission to my home automation system (Raspberry Pi running Node-Red) for further processing and alerting me on my phone.

The piezo siren is activated with a 200ms series of 6.5Khz 50% PWM at 9V so I had to create a small voltage divider + a capacitor to smoothen the signal out and bring it to 3V so the Funky v1 can handle it. A 4.7K:10K + 1 uF ceramic capacitor works well:

2014-07-19 16.10.18

2014-07-19 16.11.12

I then created a miniature PCB using 0603 sized SMD components for the voltage divider and low pass filter so that all can fit in the fire alarm sensor:

2014-07-19 16.23.12

The Funky v1 is equipped with a MCP1703 LDO that will supply 3.3V from the available 9V.

2014-07-19 16.33.53 2014-07-19 16.35.06

Everything is fit inside the original case.

The Funky is running the following code, basically it sleeps waiting for a pin-change interrupt from the piezo siren to kick in. It will wake every 30 min to provide heartbeat transmission and let the home automation system know it is alive and well.

//--------------------------------------------------------------------------------------
// harizanov.com
// GNU GPL V3
//--------------------------------------------------------------------------------------

#include <avr/sleep.h>

#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif

#include <JeeLib.h> // https://github.com/jcw/jeelib
#include "pins_arduino.h"

ISR(WDT_vect) { Sleepy::watchdogEvent(); } // interrupt handler for JeeLabs Sleepy power saving

#define myNodeID 13      // RF12 node ID in the range 1-30
#define network 210      // RF12 Network group
#define freq RF12_868MHZ // Frequency of RFM12B module

#define RETRY_PERIOD 5    // How soon to retry (in seconds) if ACK didn't come in
#define RETRY_LIMIT 5     // Maximum number of times to retry
#define ACK_TIME 10       // Number of milliseconds to wait for an ack


#define LEDpin 10


//########################################################################################################################
//Data Structure to be sent
//########################################################################################################################

 typedef struct {
  	  int state;	// State
 } Payload;

static Payload temptx;
static int gotPCI;
 
 ISR(PCINT0_vect) {
  temptx.state=bitRead(PINA,1);
  gotPCI=true;
 }

void setup() {

  pinMode(LEDpin,OUTPUT);
  digitalWrite(LEDpin,LOW);
  delay(1000);
  digitalWrite(LEDpin,HIGH);
 
  pinMode(9,INPUT);
  
  rf12_initialize(myNodeID,freq,network); // Initialize RFM12 with settings defined above 
  // Adjust low battery voltage to 2.2V
  rf12_control(0xC040);
  rf12_sleep(0);                          // Put the RFM12 to sleep
//  Serial.begin(9600);
  PRR = bit(PRTIM1); // only keep timer 0 going
  gotPCI=false;
}

void loop() {
  
  pinMode(LEDpin,OUTPUT);
  
  digitalWrite(LEDpin,LOW);   // LED on  
  Sleepy::loseSomeTime(100); 
  digitalWrite(LEDpin,HIGH);  // LED off
  
  if(gotPCI) {  // How did we get here? Thru PinChangeInterrupt or wait expired?)
    temptx.state=1;
    rfwrite(); // Send data via RF   
    Sleepy::loseSomeTime(65000); //JeeLabs power save function: enter low power mode for 60 seconds (valid range 16-65000 ms)
  }
  
  temptx.state=0;  // Set the doorbell status to off
  rfwrite(); // Send data via RF 
    
  Sleepy::loseSomeTime(10000);  //sleep some more to debounce
  
  sbi(GIMSK,PCIE0); // Turn on Pin Change interrupt
  sbi(PCMSK0,PCINT1); // Which pins are affected by the interrupt
  
  gotPCI=false;
  for(int j = 1; ((j < 30) && (gotPCI==false)); j++) {    // Sleep for 30 minutes
    Sleepy::loseSomeTime(60000); //JeeLabs power save function: enter low power mode for 60 seconds (valid range 16-65000 ms)
  }
  
//  set_sleep_mode(SLEEP_MODE_PWR_DOWN); // Set sleep mode
//  sleep_mode(); // System sleeps here

  cbi(GIMSK,PCIE0); // Turn off Pin Change interrupt

}

//--------------------------------------------------------------------------------------------------
// Send payload data via RF
//--------------------------------------------------------------------------------------------------
static void rfwrite(){
   bitClear(PRR, PRUSI); // enable USI h/w
   for (byte i = 0; i <= RETRY_LIMIT; ++i) {  // tx and wait for ack up to RETRY_LIMIT times
     rf12_sleep(-1);              // Wake up RF module
      while (!rf12_canSend())
        rf12_recvDone();
      rf12_sendStart(RF12_HDR_ACK, &temptx, sizeof temptx); 
      rf12_sendWait(2);           // Wait for RF to finish sending while in standby mode
      byte acked = waitForAck();
      rf12_sleep(0);              // Put RF module to sleep
      if (acked) { return; }
  
   Sleepy::loseSomeTime(RETRY_PERIOD * 1000);     // If no ack received wait and try again
   }   
   bitSet(PRR, PRUSI); // disable USI h/w
}

// Wait a few milliseconds for proper ACK
 static byte waitForAck() {
   MilliTimer ackTimer;
   while (!ackTimer.poll(ACK_TIME)) {
     if (rf12_recvDone() && rf12_crc == 0 &&
        rf12_hdr == (RF12_HDR_DST | RF12_HDR_CTL | myNodeID))
        return 1;
     }
   return 0;
 }

The receiving end is  Raspberry Pi with RFM2Pi board running a read-only Raspbian. All the business logic is implemented using a Node-Red flow:

Fire Alarm Flow

The flow consists of the following components
1) Parser that analyzes the raw payload
2) Data validator that makes sure the payload exists and contains the expected range of values
3) Heartbeat monitor to make sure we hear from the fire alarm often enough meaning it is up and running
4) Alarm activated notification
5) logging to EmonCMS

Here is the code for the flow:

[{"id":"ba386057.845d3","type":"mqtt-broker","broker":"localhost","port":"1883"},{"id":"35befc4f.ca4104","type":"mqtt in","name":"Fire Alarm 1 raw","topic":"home/fire/alarm1","broker":"ba386057.845d3","x":97.22221374511719,"y":1891.222255706787,"z":"31c82d7c.ce37d2","wires":[["c0c8d3dc.3f373"]]},{"id":"214c6848.deb398","type":"pushbullet","title":"","name":"","x":1112.2222137451172,"y":1981.222255706787,"z":"31c82d7c.ce37d2","wires":[]},{"id":"c0c8d3dc.3f373","type":"function","name":"Parse fire node payload","func":"var raw= JSON.parse(msg.payload);\nmsg.environment = new Object();\n\nmsg.environment.AlarmState= (raw[0]); \n\nreturn msg;","outputs":1,"x":153.2222137451172,"y":1945.222255706787,"z":"31c82d7c.ce37d2","wires":[["18d0cda9.e72f32"]]},{"id":"8d10e00f.72ef2","type":"delay","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"2","rateUnits":"day","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"x":980.2222137451172,"y":1926.222255706787,"z":"31c82d7c.ce37d2","wires":[["214c6848.deb398"]]},{"id":"18d0cda9.e72f32","type":"function","name":"Validate data","func":"\nif (\n (msg.environment.AlarmState!=null && !isNaN(msg.environment.AlarmState) )\n && (msg.environment.AlarmState >= 0) \n && (msg.environment.AlarmState < 2)\n )  \n {\n \tmsg.environment.valid=1\n }\n else {\n \tmsg.environment.valid=0\n }\n\nreturn msg;","outputs":1,"x":189.2222137451172,"y":2002.222255706787,"z":"31c82d7c.ce37d2","wires":[["3e25e8b5.c1da18"]]},{"id":"3e25e8b5.c1da18","type":"switch","name":"Is data valid?","property":"environment.valid","rules":[{"t":"eq","v":"1"}],"checkall":"true","outputs":1,"x":192.2222137451172,"y":2050.222255706787,"z":"31c82d7c.ce37d2","wires":[["d5f0b640.2a0f48","fb09e85c.04f618","2df65c73.d209a4"]]},{"id":"d5f0b640.2a0f48","type":"trigger","op1":"","op2":"1","op1type":"nul","op2type":"pay","duration":"45","extend":"true","units":"min","name":"Heartbeat monitor","x":451.2222137451172,"y":1874.222255706787,"z":"31c82d7c.ce37d2","wires":[["ced533d2.312ad"]]},{"id":"b1434385.4ebcc","type":"inject","name":"Initial heartbeat","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"x":258.2222137451172,"y":1856.222255706787,"z":"31c82d7c.ce37d2","wires":[["d5f0b640.2a0f48"]]},{"id":"fb09e85c.04f618","type":"switch","name":"Alarm activated?","property":"environment.AlarmState","rules":[{"t":"eq","v":"1"}],"checkall":"true","outputs":1,"x":445.2222137451172,"y":1940.222255706787,"z":"31c82d7c.ce37d2","wires":[["b7b3e435.484c18"]]},{"id":"ced533d2.312ad","type":"function","name":"Prepare 'node not available' alert","func":"msg.payload= \"Haven't heard from Alarm1 sensor node for a while..\";\nmsg.topic=\"Fire alarm 1 not available!\";\nreturn msg;\n","outputs":1,"x":746.2222137451172,"y":1930.222255706787,"z":"31c82d7c.ce37d2","wires":[["8d10e00f.72ef2"]]},{"id":"b7b3e435.484c18","type":"function","name":"Prepare 'Fire Alarm 1' alert","func":"msg.payload= \"Fire Alarm 1 sensor activated !!!\";\nmsg.topic=\"Fire Alarm 1!!!\";\nreturn msg;","outputs":1,"x":726.2222137451172,"y":2016.222255706787,"z":"31c82d7c.ce37d2","wires":[["6351719c.9cae9"]]},{"id":"2df65c73.d209a4","type":"function","name":"Route messages","func":"// The received message is stored in 'msg'\n// It will have at least a 'payload' property:\n//   console.log(msg.payload);\n// The 'context' object is available to store state\n// between invocations of the function\n//   context = {};\n//create json text\n\nif(msg.environment == null)\n{\n\t//no data - stop here\n\treturn null;\n}\n\njsonText = JSON.stringify(msg.environment);\n \n//var msg1 = {payload:JSON.stringify(msg.environment)};\nvar msg1 = msg.payload;\nvar msg2 = {payload:msg.environment.AlarmState};\nvar msg3 = {payload:Date.now()};\n\nreturn [msg1,msg2,msg3];","outputs":"3","x":440.2222137451172,"y":2078.222255706787,"z":"31c82d7c.ce37d2","wires":[["c616e74e.39e918"],["3526f324.cad90c"],["e4a17c3e.1b5e8"]]},{"id":"c616e74e.39e918","type":"mqtt out","name":"Send to EmonCMS","topic":"/home/emoncms/out/13","broker":"ba386057.845d3","x":712.2222137451172,"y":2090.222255706787,"z":"31c82d7c.ce37d2","wires":[]},{"id":"3526f324.cad90c","type":"mqtt out","name":"","topic":"home/alarm/fire/sensor1/state","broker":"ba386057.845d3","x":745.2222137451172,"y":2136.222255706787,"z":"31c82d7c.ce37d2","wires":[]},{"id":"e4a17c3e.1b5e8","type":"mqtt out","name":"","topic":"home/alarm/fire/sensor1/lastupdate","broker":"ba386057.845d3","x":764.2222137451172,"y":2178.222255706787,"z":"31c82d7c.ce37d2","wires":[]},{"id":"6351719c.9cae9","type":"delay","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","rateUnits":"minute","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"x":946.2222137451172,"y":2018.222255706787,"z":"31c82d7c.ce37d2","wires":[["214c6848.deb398"]]}]

I then closed up everything, added a ‘hacked’ sticker and tested it above a burning piece of paper. My phone was alerting me a second later.. neat.

Fire Alarm IoT

And a video of it in action:

As a conclusion this DIY solution costs fraction of the price of similar commercial products (like the Nest Protect for example) and is way more flexible as well.

 

 

11 thoughts on “DIY Internet of Things Fire Alarm

  1. Oliver

    As a conclusion this DIY solution costs fraction of the price of similar commercial products like the Nest Protect for example and is way more flexible as well.

    And… …well… it’s DIY, maybe the strongest point! 🙂

    1. Martin Post author

      DIY does give me great accomplishment satisfaction. IoT products for the “masses” are no good for people like me 🙂

    1. Martin Post author

      Glad you also thought of it, the plan is to capture these low battery chrips as well and send that sort of event as well. I can easily identify these based on their duration since the real alarm event produces 200ms long pulses.
      Will do that in a future upgrade, the battery should be good for a year now.

  2. Pingback: A Cheap DIY Smoke Detector that Can Save Lives

  3. Pingback: A Cheap DIY Smoke Detector that Can Save Lives | Hack The Planet

  4. Raj

    I just discovered your website, thanks for hackaday. Your experiments are very inspiring. One question though: I saw that a while ago you tried using the NRF245L01 module instead of the RFM12b. Have you continued on that path to nrf24l01 migration ? It seems like the nrf24l01+ are more energy efficient and can handle mesh networks than the rfm12b. I’d like to know what you think !

    1. Martin Post author

      I’m not sure that the nrf24l01 is more energy efficient, I did experiment a bit with those and find them to be of similar energy performance to the RFM12bs. Mesh on these is software implementation that has high power usage requirements, no good for a low-powered remote node.

  5. Pingback: Meteen maken: DIY rookmelder met sms alarm - Mancave

  6. Raul Fernandez

    Thanks for your great job!! I just bought a Ikea smoke detector and the chip is hidden. Where do you sense the output? Directly from the piezo? My piezo has 3 connections.
    Can you gives us some details about how you decode it in Raspberry? Is it rfm2pi-board-v2 ? Raspberry sends a message to Funky , no?
    Is it possible to get a Funky v1 board?
    Thanks, and sorry if my question are silly.

    1. Martin Post author

      Hi,
      Sensing is from the Piezo, they do have 3 leads, you need to tap into the input electrode, see here for diagram.
      Make sure to have a voltage divider+low pass filter or you will blow the microcontroller.

      I don’t have any Funky v1s for sale but you can gram a very similar micro from here: http://www.digitalsmarties.net/products/jeenode-micro

      The receiver is a rfm2pi v2 board, available for sale here: http://shop.openenergymonitor.com/rfm12pi-raspberry-pi-base-station-receiver-board/

      Processing is done by Node-RED flow.

      I hope this makes sense 🙂