Project tutorial
Smart Pool: Alexa Controlled Pool Manager

Smart Pool: Alexa Controlled Pool Manager © GPL3+

Build your own high-quality water sensor to monitor your pool or spa, using the Amazon Alexa to check on the status of your pool.

  • 2,740 views
  • 3 comments
  • 9 respects

Components and supplies

Apps and online services

About this project

For this project I created a device that will really save me a lot of money and time. Measuring my pool's chemical balance has been a big hassle for me as it involves taking a sample of my pool's water to the local pool shop every week to test, then adding in the chemical myself. This process is both time consuming and costly as small problems prolonged over the week can amplify to big problems, which means adding more chemicals and costing more money. Also I want confidence that my pool is always clear and safe to swim in, without agitating my eyes or skin. That's why I created my own pool monitoring system to tell me exactly when my pool needs to be fixed, and how to fix it. This ensures that my pool is always chemically balanced, looking crystal clear, and perfectly safe to swim in. Additionally, I added a chlorine dispenser device that adds chlorine to my pool through voice commands on the Amazon Echo so I don't even have to go outside to fix my pool!

This project works in connection with my Pool Manager Alexa Skill which should be already published on the Amazon Alexa Store.

Demo Video

Project Overview

The flow for my project is pretty complex:

  • It starts with the Pool monitoring device collecting data from the pool.
  • This data is then sent over the Sigfox Network and configured into an AWS DynamoDB table (Further details on this later).
  • Once the table is populated with data, the user can initiate the Alexa Skill from their Amazon Echo Device.
  • This skill calls a Lambda function that reads the pool data from the DynamoDB table, processes it, then gives the outcome to the user.
  • Also, the Alexa skill is able to initiate a different part of the Lambda function that triggers a IFTTT Applet.
  • This IFTT Applet then triggers the Chlorine dispensing device (further details later) which begins to add Chlorine into the pool.
  • The user is able to control all these functions while engaging in conversation with the Amazon Echo device.

Project Outline

I am going to divide this project into 5 seperate parts to make it easier to follow through and replicate;

  • Adding Sigfox Connectivity
  • Creating the Pool Monitoring Device which pushing data to Sigfox Console
  • Pushing data from Sigfox to AWS Dynamodb table through a Lambda Function
  • Creating the Chlorine Dispensing Device which is activated by an Alexa Skill
  • Setting up an Alexa Skill to control the whole process

Step 1: Adding Sigfox Connectivity

Alright, we are going to need to connect our Arduino board to the Sigfox network which allows Low Powered Signals to be sent all around Australia (Similar network is set up in America just need to make sure you buy the Sigfox device with the right connectivity).

So with our Thinxtra xKit we need to power the device and check if we can receive a signal on the Sigfox Network, but first we need make a Sigfox account and register our device.

For this step you will need:

  • Thinxtra xKit Development Kit
  • Sigfox Account

Setting up a Sigfox Account and Registering the Device

First you need to navigate to https://backend.sigfox.com/activate.

On this page you will see development kit providers, you will need to scroll down and click on Thinxtra.

On this page you will have to select your country, so I would pick Thinxtra in Australia (in America you would need to scroll down and chose Sigfox in America).

Now you need to add the Device ID and PAC of your device. Your Sigfox ID and PAC can be found in the package from the Thinxtra xKit.

Then you need to fill out your account details, and click subscribe.

After this you should now be in the Sigfox developer portal, and you will see your device in the device list.

Run the Demo Application on your Device

To run the demo app connect the xKit to the Arduino board as the application is already pre-configured into the Arduino board.

Connect the external antenna to the xKit.

You will need to configure the jumpers on the xKit to how you would like to power the device. For this example I will be powering the device from the Arduino board. (You can find the other configurations in the xKit Instruction Guide)

Once you powered the device, you should see a red light from the Arduino board, and you should see the Thinxtra board flash a blue light when it sends a signal out.

In the demo app, the Thinxtra board will send a signal every 10 minutes, but if you don't see the blue flashing shortly after powering the device, try resetting the device a few times by pressing the red button on the Arduino board.

Also you can send out a signal by pressing the black button on the Thinxtra board.

  • For more information on the Demo App view the xKit Instruction Guide.

Check Sigfox Connectivity

Now go back to the Sigfox development portal: https://backend.sigfox.com/auth/login.

Log in with your account details. Click on the Device Tab in the top left corner.

Click on your Device's ID. On the Left Column, click on Messages

You should be able to see your device's messages, if not you need to run the Demo Application on your device again and make sure it sends a signal visible through blue flashes on the xKit board

Awesome, you have just completed adding Sigfox connectivity to your device. If you struggled to complete this, please lookat the xKit Instruction Guide for more info.

Step 2: Create the Pool Monitor Device which pushes Data to our Sigfox Console

Alright now we will work on collecting the data from all the sensor probes, compile that into one payload and send it to the Sigfox Network to use later.

The flow chart for the Pool Monitor to Sigfox Console works like this:

  • The pool sensors gather data from the pool.
  • This data is then collated and packaged into a payload by the Arduino board.
  • The Thixtra Sheild then sends the payload out to the Sigfox Network.
  • The Sigfox Network picks up the data and adds it to your Sigfox Console in the Cloud.
  • You can now access your data in the Sigfox Console.

For this step you will need:

  • Thinxtra xKit
  • Arduino Sensor Expansion Shield
  • pH Probe and Module
  • DFRobot ORP Probe and Module
  • DFRobot EC Probe and Module
  • DFRobot Temperature Probe and Module(Came with the EC Probe)
  • Arduino IDE
  • 9V Battery + Connector
  • Jumper cables (male to male, male to female and female to female)

Installing Arduino IDE

Download and install the Arduino IDE for your operating system: https://www.arduino.cc/en/Main/Software. After successful installation, open the IDE.

In the menu bar, click on Tools-> Board and select Arduino/Genuino Uno.

Also in the menu bar, click on Tools-> Programmer and select AVRISP mkll. Download the xKit Arduino firmware as a zip file from https://github.com/Thinxtra/Xkit-Sample and unzip the file.

In the folder, click on libraries and copy the contents.

Paste these contents into: C:\Users\<yourusername>\Documents\Arduino\libraries on Windows, /Users/<yourusername>/Documents/Arduino/libraries on OS X.

Restart Arduino IDE.

For more information on Installing the Arduino IDE please look at the xKit Development Kit for Arduino.

Configuring the pH Probe

We will be calibrating the pH probe so first disconnect the xKit board from the Arduino board. Then connect the pH module to the Arduino board using the jumper cables (male to female) in this configuration:

  • PO – PH analog output ==> Arduino A0
  • Gnd – Gnd for pH probe ==> Arduino GND
  • Gnd – Gnd for board ==> Arduino GND
  • VCC – 5V DC ==> Arduino 5V pin

Now you need to control the offset of the pH probe. As the board will have pH 7 set to 0V by default, when it reads acidic values, the voltage will be a negative which cannot be read by the Arduino analog pin. Therefore you will need to offset the pH probe at measure pH 7 as 2.5V (halfway between 0 and 5V) to ensure constant readings.

The offset is pretty simple, you will need to short circuit the BNC connector by attaching a piece of wire from the inside to the outside. This will allow a neutral reading.

I achieved this by stripping down a piece of wire and tapping it to the inside and outside of the BNC connector.

void setup() { 
// initialize serial communication at 9600 bits per second: 
Serial.begin(9600);}
// the loop routine runs over and over showing the voltage on A0
void loop() { 
// read the input on analog pin 0: 
int sensorValue = analogRead(A0); 
// Convert the analog reading (which goes from 0 - 1023) to a voltage (0 - 5V): 
float voltage = sensorValue * (5.0 / 1023.0); 
// print out the value you read: 
Serial.println(voltage); delay(300);}

Upload that sketch to your Arduino board and open up the Serial Monitor from the menu bar in Tools-> Serial Monitor.

You should see a voltage reading, and you will need to turn the offset pot with a screw driver until the voltage reads 2.5V (Offset pot is the blue pot closest to the BNC connector).

Once you have achieved this, take off the short circuiting wire and connect the pH probe to the BNC connector and upload the the calibration sketch to the Arduino board.

float calibration = 0; //change this value to calibrate
const int analogInPin = A0; 
int sensorValue = 0; 
unsigned long int avgValue; 
float b;
int buf[10],temp;
void setup() {
Serial.begin(9600);
}
void loop() {
for(int i=0;i<10;i++) 
{ 
buf[i]=analogRead(analogInPin);
delay(30);
}
for(int i=0;i<9;i++)
{
for(int j=i+1;j<10;j++)
{
if(buf[i]>buf[j])
{
temp=buf[i];
buf[i]=buf[j];
buf[j]=temp;
}
}
}
avgValue=0;
for(int i=2;i<8;i++)
avgValue+=buf[i];
float pHVol=(float)avgValue*5.0/1024/6;
float phValue = -5.70 * pHVol + calibration;
Serial.print("sensor = ");
Serial.println(phValue);
delay(500);
}

Place your probe into a pH buffer solution 7 and let it stabilise for at least 2 minutes. Then open up the serial monitor and take down the pH measurement. Using the measurement figure out the difference between the buffer pH and measured pH, and add this to the calibration variable. For example if I used the pH 7 buffer solution, and my pH probe read a pH of -13.28, I would set the calibration variable to 20.28 and re-upload the sketch to the Arduino board. The probe should now read pH 7.

Perfect, you are now done calibrating the pH probe.

Calibrating the ORP Sensor

To calibrate the ORP Sensor, first you need to disconnect the pH Module from the Arduino board, and connect the ORP Module to the Arduino board according to this diagram.

Once connected, upload this sketch and open the serial terminal.

#define VOLTAGE 5.00    //system voltage
#define OFFSET 0        //zero drift voltage
#define LED 13         //operating instructions
double orpValue;
#define ArrayLenth  40    //times of collection
#define orpPin 1          //orp meter output
int orpArray[ArrayLenth];
int orpArrayIndex=0;
double avergearray(int* arr, int number){
 int i;
 int max,min;
 double avg;
 long amount=0;
 if(number<=0){
   printf("Error number for the array to avraging!/n");
   return 0;
 }
 if(number<5){   //less than 5, calculated directly statistics
   for(i=0;i<number;i++){
     amount+=arr[i];
   }
   avg = amount/number;
   return avg;
 }else{
   if(arr[0]<arr[1]){
     min = arr[0];max=arr[1];
   }
   else{
     min=arr[1];max=arr[0];
   }
   for(i=2;i<number;i++){
     if(arr[i]<min){
       amount+=min;        //arr<min
       min=arr[i];
     }else {
       if(arr[i]>max){
         amount+=max;    //arr>max
         max=arr[i];
       }else{
         amount+=arr[i]; //min<=arr<=max
       }
     }//if
   }//for
   avg = (double)amount/(number-2);
 }//if
 return avg;
}
void setup(void) {
 Serial.begin(9600);
 pinMode(LED,OUTPUT);
}
void loop(void) {
 static unsigned long orpTimer=millis();   //analog sampling interval
 static unsigned long printTime=millis();
 if(millis() >= orpTimer)
 {
   orpTimer=millis()+20;
   orpArray[orpArrayIndex++]=analogRead(orpPin); //read analog value every 20ms
   if (orpArrayIndex==ArrayLenth) {
     orpArrayIndex=0;
   }   
   orpValue=((30*(double)VOLTAGE*1000)-(75*avergearray(orpArray, ArrayLenth)*VOLTAGE*1000/1024))/75-OFFSET;   //convert the analog value to orp
 }
 if(millis() >= printTime)   //Every 800 milliseconds, print a numerical
 {
	printTime=millis()+800;
	Serial.print("ORP: ");
	Serial.print((int)orpValue);
       Serial.println("mV");
       digitalWrite(LED,1-digitalRead(LED));
 }
} 

Disconnect the ORP probe from the BNC connector and press and hold the calibration button.

You should see a ORP Value printed on the serial monitor and this number will become your OFFSET number, so if you see 22mV, you will change the code to #define OFFSET 22.

You have now completed your ORP probe calibration, for more information visit the product wiki page: https://www.dfrobot.com/wiki/index.php/Analog_ORP_Meter(SKU:SEN0165)

Calibrating the Electrical Sensor and Temperature Sensor

Disconnect the ORP Module and connect the EC and Temperature Module as shown in the diagram.

Download the OneWire Library by going to the menu on Arduino and clicking Sketch->Include Library-> Manage Libraries then search for and install OneWire.

Upload the sample code to the Arduino board.

#include <OneWire.h>
#define StartConvert 0
#define ReadTemperature 1
const byte numReadings = 20;     //the number of sample times
byte ECsensorPin = A1;  //EC Meter analog output,pin on analog 1
byte DS18B20_Pin = 2; //DS18B20 signal, pin on digital 2
unsigned int AnalogSampleInterval=25,printInterval=700,tempSampleInterval=850;  //analog sample interval;serial print interval;temperature sample interval
unsigned int readings[numReadings];      // the readings from the analog input
byte index = 0;                  // the index of the current reading
unsigned long AnalogValueTotal = 0;                  // the running total
unsigned int AnalogAverage = 0,averageVoltage=0;                // the average
unsigned long AnalogSampleTime,printTime,tempSampleTime;
float temperature,ECcurrent; 
//Temperature chip i/o
OneWire ds(DS18B20_Pin);  // on digital pin 2
void setup() {
// initialize serial communication with computer:
 Serial.begin(9600);
 // initialize all the readings to 0:
 for (byte thisReading = 0; thisReading < numReadings; thisReading++)
   readings[thisReading] = 0;
 TempProcess(StartConvert);   //let the DS18B20 start the convert
 AnalogSampleTime=millis();
 printTime=millis();
 tempSampleTime=millis();
}
void loop() {
 /*
  Every once in a while,sample the analog value and calculate the average.
 */
 if(millis()-AnalogSampleTime>=AnalogSampleInterval)  
 {
   AnalogSampleTime=millis();
    // subtract the last reading:
   AnalogValueTotal = AnalogValueTotal - readings[index];
   // read from the sensor:
   readings[index] = analogRead(ECsensorPin);
   // add the reading to the total:
   AnalogValueTotal = AnalogValueTotal + readings[index];
   // advance to the next position in the array:
   index = index + 1;
   // if we're at the end of the array...
   if (index >= numReadings)
   // ...wrap around to the beginning:
   index = 0;
   // calculate the average:
   AnalogAverage = AnalogValueTotal / numReadings;
 }
 /*
  Every once in a while,MCU read the temperature from the DS18B20 and then let the DS18B20 start the convert.
  Attention:The interval between start the convert and read the temperature should be greater than 750 millisecond,or the temperature is not accurate!
 */
  if(millis()-tempSampleTime>=tempSampleInterval) 
 {
   tempSampleTime=millis();
   temperature = TempProcess(ReadTemperature);  // read the current temperature from the  DS18B20
   TempProcess(StartConvert);                   //after the reading,start the convert for next reading
 }
  /*
  Every once in a while,print the information on the serial monitor.
 */
 if(millis()-printTime>=printInterval)
 {
   printTime=millis();
   averageVoltage=AnalogAverage*(float)5000/1024;
   Serial.print("Analog value:");
   Serial.print(AnalogAverage);   //analog average,from 0 to 1023
   Serial.print("    Voltage:");
   Serial.print(averageVoltage);  //millivolt average,from 0mv to 4995mV
   Serial.print("mV    ");
   Serial.print("temp:");
   Serial.print(temperature);    //current temperature
   Serial.print("^C     EC:");
   float TempCoefficient=1.0+0.0185*(temperature-25.0);    //temperature compensation formula: fFinalResult(25^C) = fFinalResult(current)/(1.0+0.0185*(fTP-25.0));
   float CoefficientVolatge=(float)averageVoltage/TempCoefficient;   
   if(CoefficientVolatge<150)Serial.println("No solution!");   //25^C 1413us/cm<-->about 216mv  if the voltage(compensate)<150,that is <1ms/cm,out of the range
   else if(CoefficientVolatge>3300)Serial.println("Out of the range!");  //>20ms/cm,out of the range
   else
   { 
     if(CoefficientVolatge<=448)ECcurrent=6.84*CoefficientVolatge-64.32;   //1ms/cm<EC<=3ms/cm
     else if(CoefficientVolatge<=1457)ECcurrent=6.98*CoefficientVolatge-127;  //3ms/cm<EC<=10ms/cm
     else ECcurrent=5.3*CoefficientVolatge+2278;                           //10ms/cm<EC<20ms/cm
     ECcurrent/=1000;    //convert us/cm to ms/cm
     Serial.print(ECcurrent,2);  //two decimal
     Serial.println("ms/cm");
   }
 }
}
/*
ch=0,let the DS18B20 start the convert;ch=1,MCU read the current temperature from the DS18B20.
*/
float TempProcess(bool ch)
{
 //returns the temperature from one DS18B20 in DEG Celsius
 static byte data[12];
 static byte addr[8];
 static float TemperatureSum;
 if(!ch){
         if ( !ds.search(addr)) {
             Serial.println("no more sensors on chain, reset search!");
             ds.reset_search();
             return 0;
         }      
         if ( OneWire::crc8( addr, 7) != addr[7]) {
             Serial.println("CRC is not valid!");
             return 0;
         }        
         if ( addr[0] != 0x10 && addr[0] != 0x28) {
             Serial.print("Device is not recognized!");
             return 0;
         }      
         ds.reset();
         ds.select(addr);
         ds.write(0x44,1); // start conversion, with parasite power on at the end
 }
 else{  
         byte present = ds.reset();
         ds.select(addr);    
         ds.write(0xBE); // Read Scratchpad            
         for (int i = 0; i < 9; i++) { // we need 9 bytes
           data[i] = ds.read();
         }         
         ds.reset_search();           
         byte MSB = data[1];
         byte LSB = data[0];        
         float tempRead = ((MSB << 8) | LSB); //using two's compliment
         TemperatureSum = tempRead / 16;
   }
         return TemperatureSum;  
}

Open up the serial monitor and check you are receiving the EC and Temperature values.

Since the probe comes pre-configured you shouldn't need to calibrate it, and as it is a long a lengthy process I will not explain it here, but you can check the product wiki to learn more: https://www.dfrobot.com/wiki/index.php/Analog_EC_Meter_SKU:DFR0300

Amazing, you have completed the calibration of all the probes. Now we just need to connect them all together on the Arduino board, and send the data to the Sigfox Console.

Connecting the Sensors to the Arduino board and Sending Data

To make things easier, I have connected all the sensors using the Arduino Sensor Expansion Shield which I have placed on-top of the Arduino board.

Here is a picture how I connected all the sensors to the sensor expansion board.

  • ORP Cables go to A0 Slot
  • pH Cables go to A2 Slot
  • EC Cables go to A3 Slot
  • Temperature cables go to Digital 5 Slot (modified some of the jumper cables to connect the temperature sensor to allow for the Thinxtra shield to be placed on top).

Once you have connected the sensors to the board and connect it to the Arduino board, place the Thinxtra board on-top of the expansion shield.

Since the expansion shield doesn't connect to every single pin, I have modified some jumper cables (male to female) to connect the remaining pins from the Thinxtra board to the Arduino board.

Upload this code to the Arduino board.

//Initilallising the libraries needed
#include <OneWire.h>
#include <Tsensors.h>
#include <Wire.h>
#include <Isigfox.h>
#include <WISOL.h>
#include <math.h>
#include <avr/wdt.h>
// Set up the protocol for Sigfox and the Sensors on board the xKit
Isigfox *Isigfox = new WISOL();
Tsensors *tSensors = new Tsensors();
// Defining the unions for the payload package
typedef union{
   float number;
   uint8_t bytes[4];
} FLOATUNION_t;
typedef union{
   uint16_t number;
   uint8_t bytes[2];
} UINT16_t;
typedef union{
   int16_t number;
   uint8_t bytes[2];
} INT16_t;
//pH Probe Setup
float calibration = 20.86; //change this value to calibrate
const int analogInPin = A2; 
int sensorValue = 0; 
unsigned long int avgValue; 
float b;
int buf[10],temp;
int ph;
int ecValue;
int orp;
//EC Probe Setup
#define StartConvert 0
#define ReadTemperature 1
const byte numReadings = 20;     //the number of sample times
byte ECsensorPin = A3;  //EC Meter analog output,pin on analog 1
byte DS18B20_Pin = 5; //DS18B20 signal, pin on digital 2
unsigned int AnalogSampleInterval=25,printInterval=700,tempSampleInterval=850;  //analog sample interval;serial print interval;temperature sample interval
unsigned int readings[numReadings];      // the readings from the analog input
byte index = 0;                  // the index of the current reading
unsigned long AnalogValueTotal = 0;                  // the running total
unsigned int AnalogAverage = 0,averageVoltage=0;                // the average
unsigned long AnalogSampleTime,printTime,tempSampleTime;
float temperature,ECcurrent; 
//Temperature chip i/o
OneWire ds(DS18B20_Pin);
//ORP Probe Setup
#define VOLTAGE 5.00    //system voltage
#define OFFSET 22        //OFFSET from calibration
#define LED 13         
double orpValue;
#define ArrayLenth  40    //times of collection
#define orpPin 0          //Analog Pin ORP Module is connected
int orpArray[ArrayLenth];
int orpArrayIndex=0;
double avergearray(int* arr, int number){
 int i;
 int max,min;
 double avg;
 long amount=0;
 if(number<=0){
   printf("Error number for the array to avraging!/n");
   return 0;
 }
 if(number<5){   //less than 5, calculated directly statistics
   for(i=0;i<number;i++){
     amount+=arr[i];
   }
   avg = amount/number;
   return avg;
 }else{
   if(arr[0]<arr[1]){
     min = arr[0];max=arr[1];
   }
   else{
     min=arr[1];max=arr[0];
   }
   for(i=2;i<number;i++){
     if(arr[i]<min){
 amount+=min;        //arr<min
       min=arr[i];
     }else {
       if(arr[i]>max){
         amount+=max;    //arr>max
         max=arr[i];
       }else{
         amount+=arr[i]; //min<=arr<=max
       }
     }//if
   }//for
   avg = (double)amount/(number-2);
 }//if
 return avg;
}
void setup() {
 Serial.begin(9600);
// Initialising and testing the Sigfox protcall
 Isigfox->initSigfox();
 Isigfox->testComms();
// Init sensors on Thinxtra Module
 tSensors->initSensors();
 // Init an interruption on the button of the Xkit
 tSensors->setButton(buttonIR);
// Setting up EC Sensor
for (byte thisReading = 0; thisReading < numReadings; thisReading++)
   readings[thisReading] = 0;
 TempProcess(StartConvert);   //let the DS18B20 start the convert
 AnalogSampleTime=millis();
 printTime=millis();
 tempSampleTime=millis();
}
void loop() {
 // The data from ORP and EC probes are constanlty monitored
 static unsigned long orpTime=millis(); 
 int orpValue = update(); // Gather the ORP value
 int ecValue = getEC(); // Gather the Conductivity and the Temperature
 if(millis() >= orpTime)  // Every 10 minutes, all the data is sent to Sigfox through the Send_Data function
 {
 orpTime=millis()+600000;
 Serial.print("ORP: ");
 Serial.print((int)orpValue);
 Serial.println("mV");
 int ph = getPh();
 Serial.print("Sending pH: "); Serial.println(ph);
  Serial.print("EC Value: "); Serial.println(ecValue);
  Serial.print("Temperature: "); Serial.println(temperature); 
  Send_Data(); // Prepare the payload to send to Sigfox    
 }
}
// Function to collect ORP Value
int update() {
static unsigned long orpTimer=millis();   
 if(millis() >= orpTimer)
 {
   orpTimer=millis()+20;
   orpArray[orpArrayIndex++]=analogRead(orpPin);    //read an analog value every 20ms
   if (orpArrayIndex==ArrayLenth) {
     orpArrayIndex=0;
   }   
   orpValue=((30*(double)VOLTAGE*1000)-(75*avergearray(orpArray, ArrayLenth)*VOLTAGE*1000/1024))/75-OFFSET;   //convert the analog value to orp according the circuit
 }
return orpValue;
}
// Function to collect pH Value
int getPh() {
 for(int i=0;i<10;i++) 
{ 
buf[i]=analogRead(analogInPin);
delay(30);
}
for(int i=0;i<9;i++)
{
for(int j=i+1;j<10;j++)
{
if(buf[i]>buf[j])
{
temp=buf[i];
buf[i]=buf[j];
buf[j]=temp;
}
}
}
avgValue=0;
for(int i=2;i<8;i++)
avgValue+=buf[i];
float pHVol=(float)avgValue*5.0/1024/6;
float phValue = -5.70 * pHVol + calibration;
int ph = phValue*100; // pH value has to be sent as an integer so we muliply by 100, then divide by 100 on the Sigfox Console
return ph;
 }
// Function to collect EC value
int getEC(){
if(millis()-AnalogSampleTime>=AnalogSampleInterval)  
 {
   AnalogSampleTime=millis();
    // subtract the last reading:
   AnalogValueTotal = AnalogValueTotal - readings[index];
   // read from the sensor:
   readings[index] = analogRead(ECsensorPin);
   // add the reading to the total:
   AnalogValueTotal = AnalogValueTotal + readings[index];
   // advance to the next position in the array:
   index = index + 1;
   // if we're at the end of the array...
   if (index >= numReadings)
   // ...wrap around to the beginning:
   index = 0;
   // calculate the average:
   AnalogAverage = AnalogValueTotal / numReadings;
 }
  if(millis()-tempSampleTime>=tempSampleInterval) 
 {
   tempSampleTime=millis();
   temperature = TempProcess(ReadTemperature);  // read the current temperature from the  DS18B20
   TempProcess(StartConvert);                   //after the reading,start the convert for next reading
 }
 if(millis()-printTime>=printInterval)
 {
   printTime=millis();
   averageVoltage=AnalogAverage*(float)5000/1024;
   float TempCoefficient=1.0+0.0185*(temperature-25.0);    //temperature compensation formula: fFinalResult(25^C) = fFinalResult(current)/(1.0+0.0185*(fTP-25.0));
   float CoefficientVolatge=(float)averageVoltage/TempCoefficient;   
   if(CoefficientVolatge>3300)Serial.println("Out of the range!");  //>20ms/cm,out of the range
   else
   { 
     if(CoefficientVolatge<=448)ECcurrent=6.84*CoefficientVolatge-64.32;   //1ms/cm<EC<=3ms/cm
     else if(CoefficientVolatge<=1457)ECcurrent=6.98*CoefficientVolatge-127;  //3ms/cm<EC<=10ms/cm
     else ECcurrent=5.3*CoefficientVolatge+2278;                           //10ms/cm<EC<20ms/cm
     ECcurrent/=10;    //convert us/cm to ms/cm
  ecValue = ECcurrent;
   }
 }
return ecValue;
return temperature;
}
// Proccesses the Temperature Value
float TempProcess(bool ch)
{
 //returns the temperature from one DS18B20 in DEG Celsius
 static byte data[12];
 static byte addr[8];
 static float TemperatureSum;
 if(!ch){
         if ( !ds.search(addr)) {
             Serial.println("no more sensors on chain, reset search!");
             ds.reset_search();
             return 0;
         }      
         if ( OneWire::crc8( addr, 7) != addr[7]) {
             Serial.println("CRC is not valid!");
             return 0;
         }        
         if ( addr[0] != 0x10 && addr[0] != 0x28) {
             Serial.print("Device is not recognized!");
             return 0;
         }      
         ds.reset();
         ds.select(addr);
         ds.write(0x44,1); // start conversion, with parasite power on at the end
 }
 else{  
         byte present = ds.reset();
         ds.select(addr);    
         ds.write(0xBE); // Read Scratchpad            
         for (int i = 0; i < 9; i++) { // we need 9 bytes
           data[i] = ds.read();
         }         
         ds.reset_search();           
         byte MSB = data[1];
         byte LSB = data[0];        
         float tempRead = ((MSB << 8) | LSB); //using two's compliment
         TemperatureSum = tempRead / 16;
   }
         return TemperatureSum;  
}
// Function that sends payload to Sigfox
void Send_Pload(uint8_t *sendData, const uint8_t len) {
 recvMsg *RecvMsg;
 RecvMsg = (recvMsg *)malloc(sizeof(recvMsg));
 Isigfox->sendPayload(sendData, len, 0, RecvMsg);
 for (int i = 0; i < RecvMsg->len; i++) {
   Serial.print(RecvMsg->inData[i]);
 }
 Serial.println("");
 free(RecvMsg);
}
//Function that packs the raw data into a payload
void Send_Data(){
 UINT16_t ph, orpValue, ecValue, temperature; 
 ph.number = (uint16_t)getPh();
 Serial.println(ph.number);
 orpValue.number = (uint16_t)update();
 Serial.println(orpValue.number);
 ecValue.number = (uint16_t)getEC();
 Serial.println(ecValue.number);
 temperature.number = (uint16_t)getTemperature();
 Serial.println(temperature.number);
const uint8_t payloadSize = 8; //Number of bytes of the payload, every value takes 2 bytes
//  byte* buf_str = (byte*) malloc (payloadSize);
 uint8_t buf_str[payloadSize];
// Packs the data into the payload
 buf_str[0] = ph.bytes[0];
 buf_str[1] = ph.bytes[1];
 buf_str[2] = orpValue.bytes[0];
 buf_str[3] = orpValue.bytes[1];
 buf_str[4] = ecValue.bytes[0];
 buf_str[5] = ecValue.bytes[1];
 buf_str[6] = temperature.bytes[0];
 buf_str[7] = temperature.bytes[1];
//Initilises the payload of data to be sent to Sigfox 
Send_Pload(buf_str, payloadSize);
 }
// Function to get the Temperature for the payload
int getTemperature(){
temperature = temperature * 10;
 return temperature;
 }
// Button on Thinxtra board that will initilise the sending of data
void buttonIR(){
  Send_Data(); 
}

*** When uploading to the Arduino board with the Thinxtra connected, you must remove the P9 jumpers to allow for the upload, then reattach after to allow the module to function***

I will go over the main parts of the file to give you an understanding of what is going on.

  • The start of the file sets up connection with Sigfox and configures the probes.
  • In the void loop() we are constantly monitoring the ORP and Conductivity levels, and then every 10 minutes the data from all the sensors are prepared to be sent to Sigfox through the Send_Data() function. (The only way the ORP and EC sensors worked properly were to constantly monitor them.)
  • In the Send_Data() function, we work to package all the data into a payload which is able to be sent over the Sigfox network. This function initialises the Send_Pload() function which sends the payload over the Sigfox network.

Collecting the Data on the Sigfox Console

Check your device is sending the payloads by heading to the Sigfox Backend Console https://backend.sigfox.com/device/list, click on your Device's ID, then on the left column click on Messages.

New messages should now show on the page. Head back to the Device page by clicking on Device in the top menu.

Now click on the device's type name.

In the left column, click on the Callback option.

To start off with we will create an email callback, so click the new button in the right hand corner.

Click on Custom Callback.

  • Select type: Data, Uplink
  • Channel: Email
  • Custom payload config: ph::uint:16:little-endian orpValue::uint:16:little-endian ecValue::uint:16:little-endian temperature::uint:16:little-endian
  • Recipient: Your Email
  • Subject: Whatever you like it to be
  • Message:
Device Information
device ID = {device}
       data = {data}
       time = {time}
pH Value = {customData#ph}
Orp Value = {customData#orpValue}
EC Value = {customData#ecValue}
Temperature = {customData#temperature}

Now whenever your xKit sends a message over the Sigfox network, you should receive a email with all the data values.

Congratulations, you have create a working Water Monitor that is connected to the Sigfox Network. Now the next step is to get this data into a DynamoDB Database so it can be accessed by Alexa later.

Step 3: Pushing data from Sigfox to DynamoDB using a Lambda function

The flow chart for the Sigfox to DynamoDB Table works like this:

  • The Sigfox Console pushes the data to a AWS Stack.
  • This triggers a function in the AWS IOT Console which then pushes to data to the Lambda function.
  • The Lambda function process the data then pushes it into a DynamoDB table, writing over the old entry (this makes it simpler for the Alexa Skill to read).
  • Now your data is able to be viewed in the Dynamodb Table.

Sending Data to AWS IOT

Now we are going to have to transfer this data from Sigfox into AWS, but first we are going to have to set up a AWS account.

Go to https://portal.aws.amazon.com/billing/signup#/start and fill out the details to create an AWS account. When you create your account make sure your location is N. Virginia (top right), and keep checking it is here throughout the tutorial.

Once you created the account, head back to the Sigfox Backend Console https://backend.sigfox.com/device/list, click on your device's type name and then click the Callback option on the left hand column.

In the top right hand corner click on new callback. Click on AWS IOT.

Now copy the External ID (we are going to use it later).

Select: CROSSACCOUNT then click Launch Stack.

Keep the default settings then click next.

Create a name for your stack.

Put in your AWS Account ID (find this on the Settings Page by clicking My Account button in the menu).

Put in the External ID (copied from before).

  • Region: us-east-1
  • Topic Name: sigfox

Click next page. Keep all the default setting and click next page. Acknowledge the capabilities, then create the stack.

After the stack has been created select it, then select outputs and copy the ARNRole.

Head back to the Sigfox page where you were creating the stack and paste the ARN Role.

  • Enter the topic: sigfox
  • Region: US East (N. Virginia)
  • Custom Payload: ph::uint:16:little-endian orp::uint:16:little-endian ec::uint:16:little-endian temp::uint:16:little-endian
  • Json Body:
{
"deviceId" : "{device}",
"time" : "{time}",
"data": {
"ph" : "{customData#ph}",
"orp" : "{customData#orp}",
"conductivity" : "{customData#ec}",
"temperature" : "{customData#temp}"
}
}

Receiving Data in the AWS IOT

Now open the AWS IOT console (you may have to find it in the Services search).

In the left hand column, click Test. On this page, enter 'sigfox' in the subscribe to topic, then click the subscribe button

Now send messages on your Thinxtra xKit and they should come up on this page if everything is working.

Creating an IAM Role

First open the IAM console (you may have to find it in the Services search).

Click on Roles on the left side menu and Create New role.

Select Lambda. Attach the following policies:

Press next, name the role, then click create.

Now go into your account tab in the upper right corner, click on My Security Credentials, click on Access Keys and then Create a new access key.

Save your access key and keep it safe as you will need it later.

Computing your IOT Endpoint

You will need to compute your IOT Endpoint for your Lambda function so you will need to download the CommandLine Interface corresponding to your running OS.

Follow the steps outlined in this tutorial

Now that your AWS Command Line Interface is set up, go into your console/terminal and type in:

aws configure

Your console will ask you for your access key. Add it and press enter.

Your console will then ask you for your Secret Access Key. Add it and press enter.

Your console will then ask you for your Region. Add it and press enter.

Type in the following:

aws iot describe-endpoint

Copy your AWS IoT endpoint.

Creating the Lambda Function

Now open the Lambda console (you may have to find it in the Services search).

Create New Function -> Author from Scratch.

  • Name: prasing
  • Runtime: Node js 4.3
  • Set role as “Choose an existing role”, choose the IAM role name you’ve created previously.

Create function. Add this in your Function Code.

console.log('Loading event'); 
var AWS = require('aws-sdk');
var dynamodb = new AWS.DynamoDB();
var iotData = new AWS.IotData({endpoint: 'Your AWS End Point'});
//make sure to add your actual AWS End Point
exports.handler = function(event, context) {
   console.log("Request received:\n", JSON.stringify(event));
   console.log("Context received:\n", JSON.stringify(context));
   var tableName = event.deviceId;
//Receives the Data
   var time = event.time;
   var ph = event.data.ph;
   var orp = event.data.orp;
   var conductivity = event.data.conductivity;
   var temperature = event.data.temperature;
ph = ph/100; //Converts back from when we multiplied by 100
temperature = temperature/10
   function returnTime(value){
       return new Date((value)*1000 + 39600000); 
   }
   //replace 39600000 with the Unix time offset according to your timezone. Here it is set to Sydney time offset. 
   timedecode = '' +returnTime(time).toLocaleString();
   var payloadObj={ "state": {
                   "reported": {
                           "device": event.device,
                           "time": '1518681912', //Keep the time constant to overwrite the data in the table which allows easy access from the Alexa Skill 
                           "timedecode": timedecode,
                           "ph": ph,
                            "orp": orp,
                             "conductivity": conductivity,
                              "temperature": temperature,
                               },
                           }
               };
   var paramsUpdate = {
       thingName : event.deviceId,
       payload : JSON.stringify(payloadObj)
   };
   //This function will Update the Device Shadow State
   iotData.updateThingShadow(paramsUpdate, function(err, data) {
     if (err){
       console.log("Error in updating the Thing Shadow");
       console.log(err, err.stack);
           }
   });
//Next function will store the message in a dynamoDB table
   dynamodb.putItem({
           "TableName": event.deviceId,
           "Item": {
               "deviceId": {
                   "S": event.deviceId
               }, 
               "time": {
                   "S": '1518681912'
               },
               "timedecode": {
                   "S": timedecode.toString()
               },
                "ph": {
                   "S": ph.toString()
               },
               "orp": {
                   "S": orp.toString()
               },
               "conductivity": {
                   "S": conductivity.toString()
               },
               "temperature": {
                   "S": temperature.toString() + "°C"
               }
           }
       },
       function(err, data){
           if (err) {
               context.fail('ERROR: Dynamo failed: ' + err);
           } else {
       console.log('Dynamo Success: ' + JSON.stringify(data, null, ' '));
               context.succeed('SUCCESS');
           }
       }); 
};

Add your AWS Endpoint to the code at the top.

In advanced settings change your timeout to 5 min.

Add a Trigger for Lambda Function

Open up the AWS IOT console again.

Now on the left hand column click Act.

Create a new rule.

  • Name: prasing
  • Attribute: *
  • Topic Filter: sigfox

Add action -> Click Invokes a Lambda Function passing on the message -> Configure action.

  • Function name-> prasing

Add action. Then click create rule.

Create DynamoDB Table

Now open the DynamoDB console (you may have to find it in the Services search).

Click create table.

  • Table Name: has to be your Device ID (find it on your Sigfox Backend Console).
  • Primary Key: devieId
  • Add sort key: time
  • Create Table, then click on it and go into the items Tab

Now when you send a message on your xKit, your table should populate with new data.

Amazing work, you have now pushed data from your Water Monitoring Device through Sigfox to a DynamoDB table hosted on the Amazon Cloud. Your water monitoring device set up is completed, now you just need to set up your Alexa Skill.

*If you need more information on phrasing data from Sigfox to DynamoDB table, have a look through the xKit AWS guide.

Step 4: Voice Activated Chlorine Dispenser

We will move on to my favourite device of this project and it is the Voice Activated Chlorine Dispenser.

The flow chart for the Alexa Skill to Chlorine Dispenser works like this:

  • The Alexa Skill initiates a Lambda function.
  • This Lambda Function processes a http Get function, calling a URL which triggers the Webhook.
  • This Webhook then triggers the IFTTT Applet which adds a value to the Adafruit feed.
  • Through the MQTT Protocol, the ESP8266 Wifi Module collects the value from the Adafruit feed which triggers the opening of the Relay Module.
  • With the Relay Module open, power is allowed to flow from the 9V battery to the Solenoid Valve which opens the valve.
  • With the open valve, the chlorine is able to flow from a container into the pool.

For this step you will need:

  • Arduino Uno board
  • Breadboard
  • Esp8266 wifi module
  • 12V DC Plastic water solenoid valve
  • Jumper cables (male to female, male to male)
  • 9V battery + conenctor
  • Liquid Container
  • Strong stand
  • Tubes and garden hose adapters
  • IFTTT Account
  • Adafruit account

Set up your IFTTT Account and Adafruit Account

First set up your Adafruit account, so head to https://io.adafruit.com/ and create an account and head to the dashboard.

On the dashboard, on the left menu click on feeds, then create a new feed called 'ChlorineOnOff'.

Also on the left menu click see AIO key, and copy Active key down somewhere as we will need that later

Now head to the IFTTT homepage https://ifttt.com/discover and create an account.

Once you create the account, search up Webhooks and click on the Webhooks service, then click connect.

Also search up Adafruit and connect to their service.

Now click on My Applets in the top menu, then click new Applet.

Click on the +if, and in the service search for Webhooks -> Click Receive a web request-> and name the event 'add_chlorine'-> Click create trigger.

Now click on the +That, and in services search for adafruit -> Click Send data to Adafruit IO -> Feed name 'ChlorineOnOff' and data to save '0' -> Review and click finish.

You can test to see if it is running by going to My Applets-> Services-> Webhooks.

Click Documentation and write down your key somewhere.

Now in the URL type: https://maker.ifttt.com/trigger/add_chlorine/with/key/{WebhookKey}

Now when you go to your Adafruit Dashboard, and check your ChlorineOnOff feed, a 0 value should be created.

Set up the Wifi Module

You will need to connect the Esp8266 Wifi Module to the Arduino board in this configuration.

  • TXO -> TX Pin on Arduino board
  • CHPD -> 3.3V Supply on breadboard
  • 3V -> 3.3V Supply on breadboard
  • GND -> GND supply on breadboard
  • GPIO0 -> GND supply on breadboard
  • RXT - RX on Arduino board

In Arduino IDE, head to preferences in the menu bar and add in http://arduino.esp8266.com/stable/package_esp8266com_index.json into the additional board managers.

Now go to tools -> Boards-> Boards Manager -> Search for esp8266 and install that board.

Now in Tools-> Boards select the Generic Esp8266 board.

We also have to install the adafruit MQTT libraries so go to https://learn.adafruit.com/mqtt-adafruit-io-and-you/arduino-plus-library-setup download the libraries and add them to your Arduino Libraries folder.

Now create a new sketch, and upload this code to the sketch.

#include <ESP8266WiFi.h>
#include "Adafruit_MQTT.h"
#include "Adafruit_MQTT_Client.h"
// the OnOff button feed turns this RELAY on/off
#define RELAY 2  // EPS8266 GPIO2
/************************* WiFi Access Point *********************************/
#define WLAN_SSID       "Network Name"
#define WLAN_PASS       "Password"
/************************* Adafruit.io Setup *********************************/
#define AIO_SERVER      "io.adafruit.com"
#define AIO_SERVERPORT  8883                   // 1883 for http 8883 for https
#define AIO_USERNAME    "{Your username}"
#define AIO_KEY         "{Your Key}"
/************ Global State (you don't need to change this!) ******************/
// Create an ESP8266 WiFiClient class to connect to the MQTT server.
//WiFiClient client;  // Must set AIO_SERVERPORT to 1883
// or... use WiFiFlientSecure for SSL
WiFiClientSecure client;  // Must set AIO_SERVERPORT to 8883
// Setup the MQTT client class by passing in the WiFi client and MQTT server and login details.
Adafruit_MQTT_Client mqtt(&client, AIO_SERVER, AIO_SERVERPORT, AIO_USERNAME, AIO_USERNAME, AIO_KEY);
/****************************** Feeds ***************************************/
// Notice MQTT paths for AIO follow the form: <username>/feeds/<feedname>
Adafruit_MQTT_Subscribe OnOffbutton = Adafruit_MQTT_Subscribe(&mqtt, AIO_USERNAME "/feeds/ChlorineOnOff");
/*************************** Sketch Code ************************************/
// Bug workaround for Arduino 1.6.6, it seems to need a function declaration
// for some reason (only affects ESP8266, likely an arduino-builder bug).
void MQTT_connect();
void setup() {
 pinMode(RELAY, OUTPUT);
 Serial.begin(115200);
 delay(10);
 Serial.println(F("Adafruit MQTT demo"));
 // Connect to WiFi access point.
 Serial.println(); Serial.println();
 Serial.print("Connecting to ");
 Serial.println(WLAN_SSID);
WiFi.mode(WIFI_STA);
 WiFi.begin(WLAN_SSID, WLAN_PASS);
 while (WiFi.status() != WL_CONNECTED) {
   delay(500);
   Serial.print(".");
 }
 Serial.println();
 Serial.println("WiFi connected");
 Serial.println("IP address: "); Serial.println(WiFi.localIP());
 // Setup MQTT subscription for OnOff feed.
 mqtt.subscribe(&OnOffbutton);
}
uint32_t x=0;
void loop() {
 // Ensure the connection to the MQTT server is alive (this will make the first
 // connection and automatically reconnect when disconnected).
 MQTT_connect();
 // this is our 'wait for incoming subscription packets' busy subloop
 Adafruit_MQTT_Subscribe *subscription;
 while ((subscription = mqtt.readSubscription(5000))) {
   // Check if its the OnOff button feed
   if (subscription == &OnOffbutton) {
     Serial.print(F("On-Off button: "));
     Serial.println((char *)OnOffbutton.lastread);
     if (strcmp((char *)OnOffbutton.lastread, "0") == 0) {
       Serial.print("Adding Chlorine");
 digitalWrite(RELAY, HIGH); 
     }
     if (strcmp((char *)OnOffbutton.lastread, "1") == 0) {
       Serial.print("Stopping Chlorine");
       digitalWrite(RELAY, LOW); 
     }
   }
 }
 // ping the server to keep the mqtt connection alive
 if(! mqtt.ping()) {
   mqtt.disconnect();
 }
}
void addChlorine(){
 Serial.print("Adding Chlorine");
 digitalWrite(RELAY, HIGH); 
 delay(600000);
 digitalWrite(RELAY, LOW); 
 }
// Function to connect and reconnect as necessary to the MQTT server.
// Should be called in the loop function and it will take care if connecting.
void MQTT_connect() {
 int8_t ret;
 // Stop if already connected.
 if (mqtt.connected()) {
   return;
 }
 Serial.print("Connecting to MQTT... ");
 uint8_t retries = 3;
 while ((ret = mqtt.connect()) != 0) { // connect will return 0 for connected
      Serial.println(mqtt.connectErrorString(ret));
      Serial.println("Retrying MQTT connection in 5 seconds...");
      mqtt.disconnect();
      delay(5000);  // wait 5 seconds
      retries--;
      if (retries == 0) {
        // basically die and wait for WDT to reset me
        while (1);
      }
 }
 Serial.println("MQTT Connected!");
}

Make sure you change the Wifi username and password to your own.

#define WLAN_SSID       "Network Name"
#define WLAN_PASS       "Password"

Also you will have to add in your Adafruit username and AIO Key.

#define AIO_USERNAME    "{Adafruit Username}"
#define AIO_KEY         "{Your Key}"

Now you need to upload this code to your Esp8266 module, just make sure your settings in tools are set to Debug port-> Serial, Reset Method-> nodemcu, Upload Speed -> 115200.

After you upload the sketch, you should see success in the Serial menu, and whenever you trigger the Webhook Feed it should print out "Adding Chlorine".

Connect up the Solenoid Valve

Now get a new breadboard, and connect your Arduino board to your Solenoid and Relay in this configuration.

  • Signal Pin -> Digital Pin 2 on the Arduino board
  • Voltage -> 5V Pin on the Arduino board
  • Ground -> Ground supply on the Breadboard
  • NO -> 9V power supply on the Arduino board (By connect the Vin Pin from the Arduino board to the Breadboard we can access the 9V from the Battery)
  • C -> Refer to Diagram
  • Connect a Diode Rectifier between the Solenoid Voltage and Ground Pins

Upload this code to your Arduino board to test it works. (Make sure you select the Arduino Uno board in the list of boards.)

int solenoidPin = 2;    //This is the output pin on the Arduino we are using
void setup() {
 // put your setup code here, to run once:
 pinMode(solenoidPin, OUTPUT);           //Sets the pin as an output
}
void loop() {
 // put your main code here, to run repeatedly:  
 digitalWrite(solenoidPin, HIGH);    //Switch Solenoid ON
 delay(2000);                      //Wait 1 Second
 digitalWrite(solenoidPin, LOW);     //Switch Solenoid OFF
 delay(2000);                      //Wait 1 Second
}

After upload this code, the relay board should switch on and off very two seconds (shown by an LED light). If you connect your solenoid to a water source using the garden hose adapters and tubes, this should start and stop the water flow every 2 seconds.

Create the Automatic Chlorine Dispensing Device

You are now ready to create your working Chlorine dispensing device, so connect up all the parts together like in this diagram.

ESP8266 Wifi Module

  • TXO -> TX Pin on Arduino board
  • CHPD -> 3.3V Supply on Breadboard
  • 3V -> 3.3V Supply on Breadboard
  • GND -> GND supply on Breadboard
  • GPI00 -> GND supply on Breadboard
  • GPIO2 -> Signal Pin on Relay board Module
  • RXT - RX on Arduino board

Relay Board Module

  • Signal Pin -> GPIO2 of the Wifi Module
  • Voltage -> 5V Pin on the Arduino board
  • Ground -> Ground supply on the Breadboard
  • NO -> 9V power supply on the Arduino board (by connecting the Vin Pin from the Arduino board to the breadboard, we can access the 9V from the battery)
  • C -> Refer to Diagram
  • Connect a Diode Rectifier between the Solenoid Voltage and Ground Pins

Re-upload the Wifi sketch to the Esp8266 again with some minor changes. (You will have to reselect the Generic ESP8266 board.)

if (strcmp((char *)OnOffbutton.lastread, "0") == 0) {
       
         addChlorine(); 
     }
     if (strcmp((char *)OnOffbutton.lastread, "1") == 0) {
       Serial.print("Stopping Chlorine");
       digitalWrite(RELAY, LOW); 
     }

In the Adafruit Subscription function change digital write (RELAY, HIGH) to addChlorine()

void addChlorine(){
 Serial.print("Adding Chlorine");
 digitalWrite(RELAY, HIGH); 
 delay(600000);
 digitalWrite(RELAY, LOW); 
 }

Now in the addChlorine function, change the delay time according to the amount of liquid chlorine you would need to your pool. For my 40,000L pool, I need 200ml of stabilised Liquid Chlorine per 10,000L, so I need to add 800ml when my chlorine becomes deficient. Measuring the rate of flow from my Chlorine Dispensing device I found it did 800ml in around 10 minutes, so therefore I set the delay for 10 minutes. This will open the valve for 10 minutes before closing it again, adding the required amount of chlorine.

Once you upload the sketch to the ESP8266 Wifi module, you will need to change to the GPIO0 pin from Ground, to 3V Power when re-powering the device to allow normal functionality.

Connect the device to the strong stand (I put the device in a little container and hung it).

Connect the solenoid to a chlorine source which you should also connect to the stand (I used a milk bottle with a hole and tube at the bottom to allow the chlorine to flow through).

Add a tube to the bottom of the solenoid valve which will leak into the pool.

Now your chlorine should flow for 10 minutes when you trigger the Webhook through the URL. Also you can manually stop the flow of Chlorine by adding a 1 value in your Adafruit feed.

Here's a video showing how it is set up and working with the Alexa Skill.

Almost there, now we just got to set up your devices with an Alexa Skill.

Step 5: Set Up Alexa Skill

Now for the final step, and that is to set up your Alexa Skill.

For this you will just need a Amazon Developer Account and a Amazon Echo Device.

Go the the Amazon Developer Console and register an account: https://developer.amazon.com/home.html

In the menu, click on the Alexa tab.

On the Alexa Skills Kit, click get started.

Click Add new skill.

  • Skill Type: Custom Skill
  • Language: (Language of your Amazon Echo so mine was English Australia)
  • Name: Pool Manager
  • Invocation Name: Pool Manager
  • Global Fields: Select No for all of them

Click Next

In the Interaction Model, on the left menu click Code editor and upload the following code.

{
 "languageModel": {
   "intents": [
     {
       "name": "actionNeeded",
       "samples": [
         "what needs to be done",
         "how do i fix it",
         "what are the actions",
         "what are the problems"
       ],
       "slots": []
     },
     {
       "name": "addChlorine",
       "samples": [
         "add chlorine",
         "yes"
       ],
       "slots": []
     },
     {
       "name": "AMAZON.CancelIntent",
       "samples": []
     },
     {
       "name": "AMAZON.HelpIntent",
       "samples": []
     },
     {
       "name": "AMAZON.StopIntent",
       "samples": []
     },
     {
       "name": "poolHealth",
       "samples": [
         "how is my pool going",
         "can i go for a swim"
       ],
       "slots": []
     },
     {
       "name": "temperature",
       "samples": [
         "what is the temperature"
       ],
       "slots": []
     }
   ],
   "invocationName": "pool manager"
 }
}

In the top menu select Save Model, then Build Model.

Now in the top menu select Configuration. On this page under your skill name, copy down your skill ID.

In another tab, open up your AWS console and search for the Lambda function.

In the top right-hand corner make sure your Region is set to N. Virginia. Click create new function.

Select Author from scratch.

  • Name: alexaPoolSkill
  • Role: Chose Existing Role
  • Existing Role: Lambda (we create it earlier)

Create function.

Under the designer tab, click add trigger.

Select Alexa Skills Kit.

Paste in your Alexa Skills ID.

Click back on your alexaPoolSkill.

Upload this code to your code editor:

//Variables for the Skills
var callback;
var myRequest = 'Skill';
const AWSregion = 'us-east-1';  // us-east-1
// Dynamo Table Name and Keys, change the Table Name and Device ID to your Device ID from the Sigfox Configuration
const params = {
   TableName: '{Your Dynamo Table Name}',
   Key:{ "deviceId": '{Sigfox Device ID}', "time":	'1518681912'}
};
//Set up for the ALexa Skill
const Alexa = require('alexa-sdk');
const AWS = require('aws-sdk');
AWS.config.update({
   region: AWSregion
});
exports.handler = function(event, context, callback) {
   var alexa = Alexa.handler(event, context);
   alexa.appId = '{Alexa Skill ID}'; //Add in your Alexa Skill ID
   alexa.registerHandlers(handlers);
   alexa.execute();
};
const handlers = {
   //Message when we Initiate the Skill
   'LaunchRequest': function () {
       this.response.speak('Greetings Ben. I am your Pool Manager and I will constantly monitor your pools health. Ask me a question like, can i go for a swim today?').listen('try again');
       this.emit(':responseReady');
   },
//Message when we Ask about the Pool's Health
   'poolHealth': function () {
       //Function feeds data from our DynamoTable
       readDynamoItem(params, myResult=>{
           var say = '';
           var temperature = myResult[0]
           var ph = myResult[1]
           var conductivity = myResult[2]
           var orp = myResult[3]
           var numberOfActions = 0
           //Parameters to Check the Health of the Pool
           if (ph < 7.2 || ph > 7.6){
               numberOfActions = numberOfActions + 1;
           } 
           if (orp < 650){
               numberOfActions = numberOfActions + 1;
           }
           if (conductivity > 10000){
               numberOfActions = numberOfActions + 1;
           }
           //Messages according to the Health of the Pool
           if (numberOfActions == 0){
               say = "Good news. Your pool is perfectly balanced, and The temperature is " + temperature + ". It is a great time for a swim";
               this.response.speak(say);
           } else if (numberOfActions == 1) {
               say = "Sorry, your pool is not chemically balanced. You have one item that needs to be fixed before you can start swimming.";
               this.response.speak(say).listen('try again');
           }else if (numberOfActions > 1) {
               say = "Sorry, your pool is not chemically balanced. You have " + numberOfActions + " items that need to be fixed before you can start swimming. ";
               this.response.speak(say).listen('try again');
           }
           this.emit(':responseReady');
       });
   },
   //Initisiated when action is need on the pool
   'actionNeeded': function () {
       //Fuction feeds data from the Dynamo Table
           readDynamoItem(params, myResult=>{
           var say = '';
           var problem = '';
           var solution = '';
           var problem2 = '';
           var solution2 = '';
           var problem3 = '';
           var solution3 = '';
           var ph = myResult[1];
           var conductivity = myResult[2];
           var orp = myResult[3];
           var numberOfActions = 0;
           var phActive = false;
           var orpActive = false;
           var ecActive = false;
           //Parameters to Check the Health of the Pool
           if (ph < 7.2 || ph > 7.6){
               numberOfActions = numberOfActions + 1;
               phActive = true;
           } 
           if (orp < 650){
               numberOfActions = numberOfActions + 1;
               orpActive = true;
           }
           if (conductivity > 10000){
               numberOfActions = numberOfActions + 1;
               ecActive = true;
           }
           //Parameters to Identify the problem and offer a solution for the pool
           if (ph < 6.8){
               problem = "your pH level is too low"
               solution = "Adding 100g of pH Up to your pool"
           } else if (ph < 7.0) {
               problem = "your pH level is too low"
               solution = "Adding 80g of pH Up to your pool"
           } else if (ph < 7.2) {
               problem = "your pH level is too low"
               solution = "Adding 40g of pH Up to your pool"
           }
           if (ph > 8.4){
               problem = "your pH level is too high"
               solution = "Adding 180g of pH Minus to your pool"
           } else if (ph > 8.0) {
               problem = "your pH level is too high"
               solution = "Adding 120g of pH Minus to your pool"
           } else if (ph > 7.6) {
               problem = "your pH level is too high"
               solution = "Adding 60g of pH Minus to your pool"
           }
            //Parameters to Identify the problem and offer a solution for the pool
           if (numberOfActions == 0){
           say = "Your pool has no problems. It is perfect to swim in"; 
           }
           if (numberOfActions == 1){
               if (orpActive == true){
                    problem = "your chlorine level is too low"
                    solution = "Adding 800mils of Liquid Chlorine to your pool"
               }else if (ecActive == true){
                   problem = "your pools salt levels are too high"
                   solution = "topping up your pool with fresh water"
               }
           say = "In your pool, the problem is "+ problem + ". You can fix this by " + solution; 
           }
           if (numberOfActions == 2){
               if (orpActive == true && phActive == true){
                    problem2 = "your chlorine level is too low"
                    solution2 = "Adding 800mils of Liquid Chlorine to your pool"
               } else if (orpActive == true && ecActive == true){
                   problem2 = "your chlorine level is too low"
                   solution2 = "Adding 800mils of Liquid Chlorine to your pool"
                   problem = "your pools salt levels are too high"
                   solution = "topping up your pool with fresh water"
               } else if (ecActive == true && phActive == true) {
                   problem2 = "your pools salt levels are too high"
                   solution2 = "topping up your pool with fresh water"
               }
             say = "In your pool, the first problem is "+ problem + ". You can fix this by " + solution + ". Secondly, "+ problem2 + ". You can fix this by " + solution2;  
           }
           if (numberOfActions == 3){
               problem3 = " your chlorine level is too low"
                    solution3 = "Adding 800mils of Liquid Chlorine to your pool"
                problem2 = " your pools salt levels are too high"
                   solution2 = "topping up your pool with fresh water"
           say = "In your pool, the first problem is "+ problem + ". You can fix this by " + solution + ". Also, your second problem is "+ problem2 + ". You can fix this by " + solution2 + ". Furthermore, your third problem is "+ problem3 + ". You can fix this by " + solution3;
           }
           //Parameters to check if Pool Manager should offer to Add Chlorine Automatically
           if (orpActive == true){
               this.response.speak(say+ ". , . Amazingly, I have gained the ability to add the liquid chlorine, so, would you like me to add the Chlorine to your pool for you?").listen('try again');
           }else if (orpActive == false){
               this.response.speak(say);
           }
           //Initiates Alexa to speak
           this.emit(':responseReady');
           });
   },
   //Function to check  the temperature of the pool 
   'temperature': function () {
           readDynamoItem(params, myResult=>{
           var say = '';
           var temperature = myResult[0];
          say = "The Temperature in your pool is " + temperature; 
           this.response.speak(say);
           // Alexa responds with the Temperature of the pool
           this.emit(':responseReady');
           });
   },
   //Function to initiate the addition of the chlorine when invoked
    'addChlorine': function () {
   httpsGet(myRequest,  (myResult) => {
               this.response.speak('Great. I have started to add 800mils of liquid chlorine to your pool. Your pool should be ready soon');
               this.emit(':responseReady');
           }
       );
   },
   'AMAZON.HelpIntent': function () {
       this.response.speak('Ask me how your pool is going').listen('try again');
       this.emit(':responseReady');
   },
   'AMAZON.CancelIntent': function () {
       this.response.speak('Goodbye!');
       this.emit(':responseReady');
   },
   'AMAZON.StopIntent': function () {
       this.response.speak('Goodbye!');
       this.emit(':responseReady');
   }
};
// Function Gathers the Data from the Dynamo Table
function readDynamoItem(params, callback) {
   var AWS = require('aws-sdk');
   AWS.config.update({region: AWSregion});
   var docClient = new AWS.DynamoDB.DocumentClient();
   console.log('reading item from DynamoDB table');
   docClient.get(params, (err, data) => {
       if (err) {
           console.error("Unable to read item. Error JSON:", JSON.stringify(err, null, 2));
       } else {
           console.log("GetItem succeeded:", JSON.stringify(data, null, 2));
var temp = data.Item.temperature
var ph = data.Item.ph
var EC = data.Item.conductivity
var ORP = data.Item.orp
//Place all the data into an arrary
var dataArray = [temp, ph, EC, ORP]
           callback(dataArray);  // Sends the array back to the calling function
       }
   });
}
//Function Sends a http GET request which triggers the Webhook for your IFTTT Function to add Chlorine
var https = require('https');
function httpsGet(myData, callback) {
   var options = {
       host: 'maker.ifttt.com',
       port: 443,
       path: '/trigger/add_chlorine/with/key/{Webhook Key}',
       method: 'GET',
   };
   var req = https.request(options, res => {
       res.setEncoding('utf8');
       var returnData = "";
       res.on('data', chunk => {
           returnData = returnData + chunk;
       });
callback();
   });
   req.end();
}

In the above code, you need to change the following items:

// Dynamo Table Name and Keys, change the Table Name and Device ID to your Device ID from the Sigfox Configuration
const params = {
   TableName: '{Your Dynamo Table Name}',
   Key:{ "deviceId": '{Sigfox Device ID}', "time":	'1518681912'}
};

Add in your Dynamo Table name and Sigfox Device ID.

exports.handler = function(event, context, callback) {
   var alexa = Alexa.handler(event, context);
   alexa.appId = '{Alexa Skill ID}'; //Add in your Alexa Skill ID
   alexa.registerHandlers(handlers);
   alexa.execute();
};

Enter in your Alexa Skill ID.

var https = require('https');
function httpsGet(myData, callback) {
   var options = {
       host: 'maker.ifttt.com',
       port: 443,
       path: '/trigger/add_chlorine/with/key/{Webhook Key}',
       method: 'GET',
   };
   var req = https.request(options, res => {
       res.setEncoding('utf8');
       var returnData = "";
       res.on('data', chunk => {
           returnData = returnData + chunk;
       });
callback();
   });
   req.end();
}

Enter in your Webhook Key.

In basic settings, set timeout to 5 minutes.

At the top of the page, copy your ARN.

Head back to your Amazon Developer Console page where you left off, and select AWS Lambda Endpoint, then paste in your ARN you just copied.

Check no for the rest of the options, click require any permissions, then click next.

Enable the interactive Model and test out your Alexa Skill with the following phrases, then test them out on your Echo Device.

  • Alexa, begin Pool Manager.
  • Alexa, ask Pool Manager can I go for a Swim Today.
  • Alexa, ask Pool Manager what are the problems.
  • Alexa, ask Pool Manager to Add Chlorine. (Check your Adafruit IO feed and make sure this creates a 0 value.)
  • Alexa, ask Pool Manager what is the temperature of my pool.

You are now done!

Set up all and turn on your devices next to your pool and you now have a working Pool Assistant controlled through the Alexa Echo!

Code

Combined_Sensors.inoArduino
//Initilallising the libraries needed
#include <OneWire.h>
#include <Tsensors.h>
#include <Wire.h>
#include <Isigfox.h>
#include <WISOL.h>
#include <math.h>
#include <avr/wdt.h>

// Set up the protocol for Sigfox and the Sensors on board the xKit
Isigfox *Isigfox = new WISOL();
Tsensors *tSensors = new Tsensors();

// Defining the unions for the payload package
typedef union{
    float number;
    uint8_t bytes[4];
} FLOATUNION_t;

typedef union{
    uint16_t number;
    uint8_t bytes[2];
} UINT16_t;

typedef union{
    int16_t number;
    uint8_t bytes[2];
} INT16_t;



//pH Probe Setup
float calibration = 20.86; //change this value to calibrate
const int analogInPin = A2; 
int sensorValue = 0; 
unsigned long int avgValue; 
float b;
int buf[10],temp;
int ph;
int ecValue;
int orp;


//EC Probe Setup
#define StartConvert 0
#define ReadTemperature 1

const byte numReadings = 20;     //the number of sample times
byte ECsensorPin = A3;  //EC Meter analog output,pin on analog 1
byte DS18B20_Pin = 5; //DS18B20 signal, pin on digital 2
unsigned int AnalogSampleInterval=25,printInterval=700,tempSampleInterval=850;  //analog sample interval;serial print interval;temperature sample interval
unsigned int readings[numReadings];      // the readings from the analog input
byte index = 0;                  // the index of the current reading
unsigned long AnalogValueTotal = 0;                  // the running total
unsigned int AnalogAverage = 0,averageVoltage=0;                // the average
unsigned long AnalogSampleTime,printTime,tempSampleTime;
float temperature,ECcurrent; 
 
//Temperature chip i/o
OneWire ds(DS18B20_Pin);


//ORP Probe Setup
#define VOLTAGE 5.00    //system voltage
#define OFFSET 22        //OFFSET from calibration
#define LED 13         

double orpValue;

#define ArrayLenth  40    //times of collection
#define orpPin 0          //Analog Pin ORP Module is connected
int orpArray[ArrayLenth];
int orpArrayIndex=0;

double avergearray(int* arr, int number){
  int i;
  int max,min;
  double avg;
  long amount=0;
  if(number<=0){
    printf("Error number for the array to avraging!/n");
    return 0;
  }
  if(number<5){   //less than 5, calculated directly statistics
    for(i=0;i<number;i++){
      amount+=arr[i];
    }
    avg = amount/number;
    return avg;
  }else{
    if(arr[0]<arr[1]){
      min = arr[0];max=arr[1];
    }
    else{
      min=arr[1];max=arr[0];
    }
    for(i=2;i<number;i++){
      if(arr[i]<min){
      
  amount+=min;        //arr<min
        min=arr[i];
      }else {
        if(arr[i]>max){
          amount+=max;    //arr>max
          max=arr[i];
        }else{
          amount+=arr[i]; //min<=arr<=max
        }
      }//if
    }//for
    avg = (double)amount/(number-2);
  }//if
  return avg;
}



void setup() {
  Serial.begin(9600);

// Initialising and testing the Sigfox protcall
  Isigfox->initSigfox();
  Isigfox->testComms();

// Init sensors on Thinxtra Module
  tSensors->initSensors();

  // Init an interruption on the button of the Xkit
  tSensors->setButton(buttonIR);
  
// Setting up EC Sensor
 for (byte thisReading = 0; thisReading < numReadings; thisReading++)
    readings[thisReading] = 0;
  TempProcess(StartConvert);   //let the DS18B20 start the convert
  AnalogSampleTime=millis();
  printTime=millis();
  tempSampleTime=millis();
}



void loop() {
  // The data from ORP and EC probes are constanlty monitored
  static unsigned long orpTime=millis(); 
  int orpValue = update(); // Gather the ORP value
  int ecValue = getEC(); // Gather the Conductivity and the Temperature
  if(millis() >= orpTime)  // Every 10 minutes, all the data is sent to Sigfox through the Send_Data function
  {
  orpTime=millis()+600000;
  Serial.print("ORP: ");
  Serial.print((int)orpValue);
  Serial.println("mV");
  int ph = getPh();
  Serial.print("Sending pH: "); Serial.println(ph);
   Serial.print("EC Value: "); Serial.println(ecValue);
   Serial.print("Temperature: "); Serial.println(temperature); 
   Send_Data(); // Prepare the payload to send to Sigfox    
  }
}

// Function to collect ORP Value
int update() {
static unsigned long orpTimer=millis();   
  
  if(millis() >= orpTimer)
  {
    orpTimer=millis()+20;
    orpArray[orpArrayIndex++]=analogRead(orpPin);    //read an analog value every 20ms
    if (orpArrayIndex==ArrayLenth) {
      orpArrayIndex=0;
    }   
    orpValue=((30*(double)VOLTAGE*1000)-(75*avergearray(orpArray, ArrayLenth)*VOLTAGE*1000/1024))/75-OFFSET;   //convert the analog value to orp according the circuit
  }
 return orpValue;
}

// Function to collect pH Value
int getPh() {
  for(int i=0;i<10;i++) 
 { 
 buf[i]=analogRead(analogInPin);
 delay(30);
 }
 for(int i=0;i<9;i++)
 {
 for(int j=i+1;j<10;j++)
 {
 if(buf[i]>buf[j])
 {
 temp=buf[i];
 buf[i]=buf[j];
 buf[j]=temp;
 }
 }
 }
 avgValue=0;
 for(int i=2;i<8;i++)
 avgValue+=buf[i];
 float pHVol=(float)avgValue*5.0/1024/6;
 float phValue = -5.70 * pHVol + calibration;
 int ph = phValue*100; // pH value has to be sent as an integer so we muliply by 100, then divide by 100 on the Sigfox Console

return ph;
  }


// Function to collect EC value
int getEC(){

if(millis()-AnalogSampleTime>=AnalogSampleInterval)  
  {
    AnalogSampleTime=millis();
     // subtract the last reading:
    AnalogValueTotal = AnalogValueTotal - readings[index];
    // read from the sensor:
    readings[index] = analogRead(ECsensorPin);
    // add the reading to the total:
    AnalogValueTotal = AnalogValueTotal + readings[index];
    // advance to the next position in the array:
    index = index + 1;
    // if we're at the end of the array...
    if (index >= numReadings)
    // ...wrap around to the beginning:
    index = 0;
    // calculate the average:
    AnalogAverage = AnalogValueTotal / numReadings;
  }

   if(millis()-tempSampleTime>=tempSampleInterval) 
  {
    tempSampleTime=millis();
    temperature = TempProcess(ReadTemperature);  // read the current temperature from the  DS18B20
    TempProcess(StartConvert);                   //after the reading,start the convert for next reading
  }
 
  if(millis()-printTime>=printInterval)
  {
    printTime=millis();
    averageVoltage=AnalogAverage*(float)5000/1024;
    
    float TempCoefficient=1.0+0.0185*(temperature-25.0);    //temperature compensation formula: fFinalResult(25^C) = fFinalResult(current)/(1.0+0.0185*(fTP-25.0));
    float CoefficientVolatge=(float)averageVoltage/TempCoefficient;   
     
    if(CoefficientVolatge>3300)Serial.println("Out of the range!");  //>20ms/cm,out of the range
    else
    { 
      if(CoefficientVolatge<=448)ECcurrent=6.84*CoefficientVolatge-64.32;   //1ms/cm<EC<=3ms/cm
      else if(CoefficientVolatge<=1457)ECcurrent=6.98*CoefficientVolatge-127;  //3ms/cm<EC<=10ms/cm
      else ECcurrent=5.3*CoefficientVolatge+2278;                           //10ms/cm<EC<20ms/cm
      ECcurrent/=10;    //convert us/cm to ms/cm
   ecValue = ECcurrent;
    }
  }
return ecValue;
return temperature;
}


// Proccesses the Temperature Value
float TempProcess(bool ch)
{
  //returns the temperature from one DS18B20 in DEG Celsius
  static byte data[12];
  static byte addr[8];
  static float TemperatureSum;
  if(!ch){
          if ( !ds.search(addr)) {
              Serial.println("no more sensors on chain, reset search!");
              ds.reset_search();
              return 0;
          }      
          if ( OneWire::crc8( addr, 7) != addr[7]) {
              Serial.println("CRC is not valid!");
              return 0;
          }        
          if ( addr[0] != 0x10 && addr[0] != 0x28) {
              Serial.print("Device is not recognized!");
              return 0;
          }      
          ds.reset();
          ds.select(addr);
          ds.write(0x44,1); // start conversion, with parasite power on at the end
  }
  else{  
          byte present = ds.reset();
          ds.select(addr);    
          ds.write(0xBE); // Read Scratchpad            
          for (int i = 0; i < 9; i++) { // we need 9 bytes
            data[i] = ds.read();
          }         
          ds.reset_search();           
          byte MSB = data[1];
          byte LSB = data[0];        
          float tempRead = ((MSB << 8) | LSB); //using two's compliment
          TemperatureSum = tempRead / 16;
    }
          return TemperatureSum;  
}



// Function that sends payload to Sigfox
void Send_Pload(uint8_t *sendData, const uint8_t len) {
  recvMsg *RecvMsg;

  RecvMsg = (recvMsg *)malloc(sizeof(recvMsg));
  Isigfox->sendPayload(sendData, len, 0, RecvMsg);
  for (int i = 0; i < RecvMsg->len; i++) {
    Serial.print(RecvMsg->inData[i]);
  }
  Serial.println("");
  free(RecvMsg);
}

//Function that packs the raw data into a payload
void Send_Data(){
  UINT16_t ph, orpValue, ecValue, temperature; 

  ph.number = (uint16_t)getPh();
  Serial.println(ph.number);
  orpValue.number = (uint16_t)update();
  Serial.println(orpValue.number);
  ecValue.number = (uint16_t)getEC();
  Serial.println(ecValue.number);
  temperature.number = (uint16_t)getTemperature();
  Serial.println(temperature.number);

const uint8_t payloadSize = 8; //Number of bytes of the payload, every value takes 2 bytes
//  byte* buf_str = (byte*) malloc (payloadSize);
  uint8_t buf_str[payloadSize];

// Packs the data into the payload
  buf_str[0] = ph.bytes[0];
  buf_str[1] = ph.bytes[1];
  buf_str[2] = orpValue.bytes[0];
  buf_str[3] = orpValue.bytes[1];
  buf_str[4] = ecValue.bytes[0];
  buf_str[5] = ecValue.bytes[1];
  buf_str[6] = temperature.bytes[0];
  buf_str[7] = temperature.bytes[1];
  
//Initilises the payload of data to be sent to Sigfox 
Send_Pload(buf_str, payloadSize);
  
  }

// Function to get the Temperature for the payload
int getTemperature(){
 temperature = temperature * 10;
  return temperature;
  }

// Button on Thinxtra board that will initilise the sending of data
void buttonIR(){

   Send_Data(); 
   

}

Schematics

Xkit Development Guide For Arduino
Xkit Instructions Guide
Xkit AWS Guide
Chlorine Dispenser
chlorne_dispenser_sketch_xcnFWukrAa.fzz

Comments

Similar projects you might like

Alexa BBQ/Kitchen Thermometer with IoT Arduino and e-Paper

Project tutorial by Roger Theriault

  • 2,385 views
  • 0 comments
  • 9 respects

Alexa Based Smart Home Monitoring

Project tutorial by Adithya TG

  • 16,777 views
  • 19 comments
  • 47 respects

Animated Smart Light with Alexa and Arduino

Project tutorial by Bruno Portaluri

  • 3,652 views
  • 9 comments
  • 23 respects

Wise Shower Driven by Alexa Skill

Project in progress by Virgilio Enrique Aray Arteaga

  • 2,193 views
  • 0 comments
  • 3 respects

Hygge Home - Alexa Smart Bath

Project tutorial by J Howard

  • 5,355 views
  • 2 comments
  • 18 respects

Secure Package Delivery Trunk for Your Front Porch

Project tutorial by Team Castle Locker

  • 2,515 views
  • 1 comment
  • 15 respects
Add projectSign up / Login