Rampiot Smart Lock is a smart device connected to the Internet that allows you to turn your simple door into a smart door. This device is integrated with Alexa for control and customization of his behavior providing a personalized experience.
This smart lock allows basic functions as Lock/Unlock, but it comes with another cool features for personalize user experience, all features are:
- Rules creation by voice: You can say to Alexa how this device should behave and react to events, for example: "Lock door and notify me when door is being closed for 10 seconds", "Notify me when door is unlocked but between 7 a.m to 6 p.m".
- Scheduled Actions: You can say to Alexa that lock/unlock/security check the door on specific day of the week and in a specific hour of the day, for example: "On friday, lock door at 7 p.m", "Everyday, unlock door at 6 p.m", "Everyday, at 11 p.m security check door".
- Lock: You can lock the door using Alexa Smart Home Skill, Rampiot device app or simply with hand.
- Unlock: Alexa Smart Home Skill can't unlock or disarm devices, so this feature is implemented with an Custom Skill. Unlock action can done by hand, voice pin like "3490" and the new fingerprint unlock using the smartphone sensor.
I have split the demo video into several videos by feature. First I will show the basic functionality of lock/unlock, then creation of rules, scheduled actions, status reading by Alexa Smart Home Skill and by Rampiot Smart Home Custom Skill.
I have added the application screen to the right side of the video to show how I enable fingerprint authentication and how fingerprint authentication works.
- Basic Lock/Unlock features using voice pin and smartphone fingerprint sensor.
- Rule creation: This feature allows to define what actions the door should take in response to events such as locking, unlocking, opening and closing. It also allows defining which days and in what range of hours this rule should be applied.
Example Rule 1: I want that my door automatically lock when this is being closed for 10 seconds.
Example Rule 2: I want that my door send me a push notification and email when it's unlocked.
- Scheduled Action: This feature allows you to schedule actions such as lock, unlock and security check. For example, we could tell you at our door that every day at 8 a.m. unlock the door or that every day at 10 p.m. do a security check, this check basically verifies the state of the door, if it is open, then sends a notification, if it is closed but not locked, then it is automatically locked.
Example Scheduled Action 1: I want that my door automatically lock every friday at 2:21 a.m.
Example Scheduled Action 2: I want that my door automatically unlocks every friday at 2:30 a.m. (Unlock action require a confirmation for security.)
Example Scheduled Security Check: This check basically verifies the state of the door, if it is open, then sends a notification, if it is closed but not locked, then it is automatically locked. In this example will going to see security check when door is open. Security check is a merge between preloaded rules and scheduled action.
- Status Reporting: The status of the door can be queried from the Alexa Smart Home Skill as well as using the Rampiot Custom Skill.
- Rampiot MQTT Server and REST API:
I'm using my own iot platform called rampiot, this is in the development phase, my purpose of developing and use it is to understand a bit how these platforms work and how I can give my own added value to these, besides being fun.
This platform is built on AWS services such as AWS Lambda, AWS API Gateway, SNS, SQS, EC2, Elastic Cache and Cloudwatch Rules.
For this guide it is not necessary to do some deployment on the server side because all the services and endpoints used in this guide are public available.
I am currently working on documentation of services, architecture and others.
Devices Software (ESP01 + Arduino NANO)
This software component has divided into three layers:
1- Shield Layer (Runs on ESP01)
All the responsibility of access to the network, saving access point keys, network configuration, and common interaction with the iot rampiot platform such as:
- Device login
- Device signup
- Connect to MQTT Server
- Deliver MQTT messages to Arduino NANO
- Allow to Arduino NANO publish messages to MQTT topics using serial.
It is managed by RampIOTESPShield, this software component runs on the ESP01 establishing serial communication with the Arduino NANO to notify it when a message arrives and also to allow the Arduino to send messages to the MQTT server.
2- Arduino Rampiot Client Layer (Runs on Arduino NANO as Lib)
This layer has the responsibility to encapsulate the serial communication with the RampIOTESPShield and expose the methods to principal Sketch to interact with the platform.
This is handled by rampiot-arduino-lib which basically allows the Arduino sketch to send and receive messages to and from the MQTT server.
3- Door Business (Runs on NANO as principal Sketch)
This is where all the logic of our smart door is, here the position of the servo is read, the opening sensor is read and it is also decided what to do when a message arrives from the MQTT server. Here it also notifies the MQTT server when an open / close / lock / unlock event is triggered.
#include "RampIOTControl.h"
#include <Servo.h>
#include <SoftwareSerial.h>
SoftwareSerial cameraAddonSerial(12, 11);
/*This type is mandatory for smart door implementation*/
#define THING_TYPE "SMART_DOOR"
/*This id must be unique*/
#define THING_ID 2396641
/*Storage Slots*/
#define EEPROM_LOCK_MOV_SLOT 1
#define EEPROM_UNLOCK_MOV_SLOT 2
#define PIN_SLOT 3
/*End Storage Slots*/
/*Pins definition*/
#define SERVO_POS_PIN A0
#define DOOR_OPCL_PIN 3
#define CONF_LOCK_PIN 6
#define CONF_UNLOCK_PIN 7
#define SERVO_PIN 9
#define RESET_PIN 10
/*End Pins definition*/
#define LOCK_EVENT 1
#define UNLOCK_EVENT 2
#define DELTA_MOV 12
#define POSITION_THRESHOLD 20
#define OC_DELAY 1000
#define SERVO_MIN 36
#define SERVO_MAX 509
#define ANGLE_MIN 0
#define ANGLE_MAX 180
#define ERR_MARGIN 5
RampIOTControl rampiotControl;
Servo locker;
bool movingServo = false;
bool configured = false;
bool doorOpen = false;
bool doorLock = false;
uint8_t currentEvent = 0;
uint8_t lockLimit = 0;
uint8_t unlockLimit = 0;
unsigned long lastOCEventTime = 0;
char fireUserId[15];
char pin[6];
/*This method read the internal servo potentiometer and converts it on servo angle using arduino map function*/
uint16_t getServoPosition(){
uint16_t servoF = analogRead(SERVO_POS_PIN);
servoF = servoF < SERVO_MIN ? SERVO_MIN : servoF > SERVO_MAX ? SERVO_MAX : servoF;
servoF = map(servoF, SERVO_MIN, SERVO_MAX, ANGLE_MIN, ANGLE_MAX);
servoF = ANGLE_MAX - servoF;
return servoF > ERR_MARGIN ? servoF-ERR_MARGIN : servoF;
}
void handleConfiguration(){
/*When user configure locker manually, then when press lock button it saves lock position on EEPROM*/
if( !configured && digitalRead(CONF_LOCK_PIN) == LOW ){
Storage storage;
lockLimit = getServoPosition();
char lockPositionStr[5];
itoa(lockLimit, lockPositionStr, 10);
storage.saveData(EEPROM_LOCK_MOV_SLOT, lockPositionStr);
delay(500);
}
/*When user configure locker manually, then when press unlock button it saves unlock position on EEPROM*/
else if( !configured && digitalRead(CONF_UNLOCK_PIN) == LOW ){
Storage storage;
unlockLimit = getServoPosition();
char unlockPositionStr[5];
itoa(unlockLimit, unlockPositionStr, 10);
storage.saveData(EEPROM_UNLOCK_MOV_SLOT, unlockPositionStr);
delay(500);
}
}
/*Reading and notifying sensors status like door open/close sensor and servo lock/unlock position*/
void handleSensors(){
uint8_t doorOC = digitalRead(DOOR_OPCL_PIN);
if( (lastOCEventTime == 0 || millis()-lastOCEventTime > OC_DELAY) &&
((doorOpen && doorOC == LOW) || (!doorOpen && doorOC == HIGH)) ){
doorOpen = !doorOpen;
lastOCEventTime = millis();
DynamicJsonBuffer jsonBuffer;
JsonObject& status = jsonBuffer.createObject();
status["state"] = doorOpen ? "opened" : doorLock ? "closed_locked": "closed_unlocked";
status["event"] = doorOpen ? "open" : "close";
rampiotControl.publishEvent(status, "MANUAL");
}
if( configured && (getServoPosition() == lockLimit || getServoPosition() == lockLimit-POSITION_THRESHOLD
|| getServoPosition() == lockLimit+POSITION_THRESHOLD) && !doorLock ){
doorLock = true;
DynamicJsonBuffer jsonBuffer;
JsonObject& status = jsonBuffer.createObject();
status["state"] = "closed_locked";
status["event"] = "lock";
rampiotControl.publishEvent(status, fireUserId[0] != '\0' ? fireUserId : "MANUAL");
fireUserId[0] = '\0';
}
else if( configured && (getServoPosition() == unlockLimit || getServoPosition() == unlockLimit-POSITION_THRESHOLD
|| getServoPosition() == unlockLimit+POSITION_THRESHOLD) && doorLock ){
doorLock = false;
DynamicJsonBuffer jsonBuffer;
JsonObject& status = jsonBuffer.createObject();
status["state"] = "closed_unlocked";
status["event"] = "unlock";
rampiotControl.publishEvent(status, fireUserId[0] != '\0' ? fireUserId : "MANUAL");
fireUserId[0] = '\0';
}
}
void sendDoorOpenStatus(){
DynamicJsonBuffer jsonBuffer;
JsonObject& status = jsonBuffer.createObject();
status["state"] = "opened";
status["event"] = "open";
rampiotControl.publishEvent(status, "MANUAL");
}
void handleLockUnlockFireEvent(){
switch(currentEvent){
case LOCK_EVENT:
/*If door is open then locker can't lock/unlock*/
if( doorOpen ){
currentEvent = 0;
sendDoorOpenStatus();
return;
}
/*When servo lock movement is completed, then detach servo for allow manually lock/unlock*/
if( getServoPosition() == lockLimit ||
(unlockLimit < lockLimit && getServoPosition()+ERR_MARGIN >= lockLimit) ||
(unlockLimit > lockLimit && getServoPosition()-ERR_MARGIN <= lockLimit) ){
movingServo = false;
locker.detach();
currentEvent = 0;
}
else if( !movingServo ){
movingServo = true;
locker.attach(SERVO_PIN);
locker.write(lockLimit);
}
break;
case UNLOCK_EVENT:
/*If door is open then locker can't lock/unlock*/
if( doorOpen ){
currentEvent = 0;
sendDoorOpenStatus();
return;
}
/*When servo unlock movement is completed, then detach servo for allow manually lock/unlock*/
if( getServoPosition() == unlockLimit ||
(unlockLimit > lockLimit && getServoPosition()+ERR_MARGIN >= unlockLimit) ||
(unlockLimit < lockLimit && getServoPosition()-ERR_MARGIN <= unlockLimit) ){
movingServo = false;
locker.detach();
currentEvent = 0;
}
else if( !movingServo ){
movingServo = true;
locker.attach(SERVO_PIN);
locker.write(unlockLimit);
}
break;
}
}
/*Reset callback handler, after send reset signal to RampIOTShield,
then clear local storage for allow configure lock and unlock limits*/
void onReset(){
Storage storage;
storage.clearAll();
}
/*Message callback handler, when message arrives from MQTT server,
then this method will be called with message received*/
void onMessage(const char* topic, JsonObject& json, const char* fUserId){
const char* event = json["event"];
if( fUserId ){
strcpy(fireUserId, fUserId);
}
if( strcmp(event, "lock") == 0 ){
currentEvent = LOCK_EVENT;
}
/*If user unlock using pin number*/
else if( strcmp(event, "pin_unlock") == 0 ){
const char* _pin = json["pin"];
/*Only unlock if user pin is equals to device pin*/
if( _pin && strcmp(pin, _pin) == 0 ){
currentEvent = UNLOCK_EVENT;
}
}
else if( strcmp(event, "unlock") == 0 || strcmp(event, "face_unlock") == 0 ){
currentEvent = UNLOCK_EVENT;
}
}
/*
* This callback method will be called when device login and every time that user update PIN number on App
*/
void onProperties(JsonObject& properties){
const char* _pin = properties["pin"];
if( _pin ){
/*Updating device pin*/
strcpy(pin, _pin);
}
}
void setup() {
Storage storage;
configured = !storage.isEmpty(EEPROM_LOCK_MOV_SLOT) &&
!storage.isEmpty(EEPROM_UNLOCK_MOV_SLOT);
if( configured ){
char lockMoveStr[5];
char unlockMoveStr[5];
storage.getData(EEPROM_LOCK_MOV_SLOT, lockMoveStr);
storage.getData(EEPROM_UNLOCK_MOV_SLOT, unlockMoveStr);
lockLimit = atoi(lockMoveStr);
unlockLimit = atoi(unlockMoveStr);
}
pinMode(DOOR_OPCL_PIN, INPUT_PULLUP);
pinMode(CONF_LOCK_PIN, INPUT_PULLUP);
pinMode(CONF_UNLOCK_PIN, INPUT_PULLUP);
/*Begin serial for camera addon*/
cameraAddonSerial.begin(9600);
/*Must be pull up because if it's DOWN then reset*/
pinMode(RESET_PIN, INPUT_PULLUP);
rampiotControl.begin(
&Serial, //HardwareSerial
RESET_PIN, //Reset PIN
THING_ID, //Thing ID
THING_TYPE //Thing type
);
rampiotControl.setMessageCallback(onMessage);
rampiotControl.setResetCallback(onReset);
rampiotControl.setPropertiesCallback(onProperties);
}
String camBuff = "";
void handleCamera(){
while (cameraAddonSerial.available()) {
char data = (char)cameraAddonSerial.read();
if( data == '\n' ){
char status[camBuff.length()+1];
camBuff.toCharArray(status, camBuff.length()+1);
rampiotControl.publishEvent(status, "CAMERA");
camBuff = "";
}else{
camBuff.concat(data);
}
}
}
void loop() {
if( !configured ){
handleConfiguration();
}else{
handleLockUnlockFireEvent();
handleSensors();
handleCamera();
}
rampiotControl.handleThing();
}
How Rules Works
Alexa calls to rule creation service in URL:
URL: https://api.rampiot.com/v1/things/{thingId}/rules
Method: POST
Headers:
Content-Type - application/json
Authorization - Bearer AuthToken....
With an body payload that respects the following JSON scheme:
{
"type": "object",
"required": ["name", "conditions", "actions"],
"properties":{
"name":{
"type": "string"
},
"conditions": {
"type": "array",
"required": true,
"items":{
"type": "object",
"properties":{
"operator": {
"enum": ["==", "<=", ">=", "!=", ">", "<"],
"required": true
},
"property":{
"type": "string",
"required": true
},
"value":{
"type": "any",
"required": true
},
"condOperator": { "enum": ["&&", "||"]}
}
}
},
"restrictions":{
"type": "object",
"properties":{
"dayOfWeek":{
"type": "array",
"items":{
"types": "number",
"minimum": 0,
"maximum": 6
}
},
"hoursRange":{
"type": "array",
"items":{
"type": "object",
"required": ["min", "max"],
"properties":{
"min":{
"type": "object",
"required": ["hours", "minutes"],
"properties":{
"hours":{
"type": "number"
},
"minutes":{
"type": "number"
}
}
},
"max":{
"type": "object",
"required": ["hours", "minutes"],
"properties":{
"hours":{
"type": "number"
},
"minutes":{
"type": "number"
}
}
}
}
}
}
}
},
"timerWatchValue":{
"type": "object",
"properties":{
"hours":{
"type": "number",
"minimum": 0,
"maximum": 23
},
"minutes":{
"type": "number",
"minimum": 0,
"maximum": 59
},
"seconds":{
"type": "number",
"minimum": 0,
"maximum": 59
}
}
},
"actions":{
"type": "array",
"items":{
"type": "object",
"properties":{
"name":{
"type": "string",
"required": true
},
"content":{
"type": "any",
"required": true
}
}
}
}
}
}
For example:
{
"code": "security_check",
"rule":[{
"name": "Notify if is open",
"conditions": [
{
"property": "connected",
"operator": "==",
"value": true
},
{
"condOperator": "&&",
"property": "status.state",
"operator": "==",
"value": "opened"
}
],
"actions":[
{
"name": "EMAIL",
"content":{
"to": "destination@domain.com",
"subject": "Door Event",
"message": "The door is opened !"
}
}
]
}
So, when door sends an event with status.state == "opened" to MQTT server, this going to call an SNS topic that executes an Lambda function that search a rule that matches with its status, if it's found, then resolve "actions" element of rule.
How Scheduled actions works:
Alexa calls to schedule action creation service in URL:
URL: https://api.rampiot.com/v1/things/{thingId}/scheduled
Method: POST
Headers:
Content-Type - application/json
Authorization - Bearer AuthToken....
With an body payload that respects the following JSON scheme:
{
"type": "object",
"properties":{
"config":{
"type": "object",
"properties":{
"every":{
"type": "number",
"minimum": 1,
"maximum": 59
},
"freq":{
"enum": ["hourly", "daily", "monthly", "yearly", "weekly"]
},
"minutes":{
"type": "string"
},
"hours":{
"type": "string"
},
"dom":{
"type": "string"
},
"mon":{
"type": "string"
},
"dow":{
"type": "string"
},
"year":{
"type": "string"
}
}
}
}
}
For Example:
{
"userId": "rmesino",
"name": "On friday, unlock Door at 2:30 AM",
"description": "At 02:30 AM, only on Friday",
"thingId": "XXXXXXX",
"config": {
"minutes": "30",
"hours": "2",
"dom": "*",
"mon": "*",
"dow": "5",
"year": "*"
},
"fire": {
"status": {
"event": "unlock"
}
}
}
After this is saved in the database, a AWS Cloudwatch Rule with the expression cron is created and with a target lambda that will send a message to the MQTT server with the specified action (Lock / Unlock / Security Check).
When this Cloudwatch Rule is triggered then a lambda function sends the scheduled message to the Door.
Rampiot Smart Door JSON Schema
The smart door must respect SMART_DOOR JSON schema for connect and interact with the MQTT Server and REST API.
JSON Schema:
{
"_id": "SMART_DOOR",
"name": "Smart door",
"description": "Thing that allows lock, unlock door and query if it is opened or closed",
"readOnly": false,
"requiresConfirmation": [
{
"event": "unlock"
}
],
"propertiesSchema": {
"type": "object",
"properties": {
"pin": {
"title": "PIN",
"type": "number"
},
"appAuth": {
"title": "App. Authorization",
"type": "boolean"
}
}
},
"statusSchema": {
"type": "object",
"properties": {
"state": {
"enum": [
"opened",
"closed_unlocked",
"closed_locked"
]
},
"event": {
"enum": [
"open",
"close",
"lock",
"unlock",
"pin_unlock"
]
},
"pin": {
"type": "number"
}
},
"required": [
"event"
]
}
}
The most important JSON schema properties are:
- requiresConfirmation: This property is an array of possible door status that requires confirmation by the door owner, this confirmation is made in the application by fingerprint, in this case, if an unlock action is performed the MQTT server based on the JSON schema will send a push to the device of the door owner requesting the confirmation.
- propertiesSchema: This property is a subschema that defines a set of properties that must be filled by the user in the mobile application, this scheme is transformed into a form by the application. The door has two properties that are: PIN: Defines a four-digit code and its function is to validate the access to unlock the door. App Auhorization: If it is true then when user fire an unlock then confirmation by fingerprint will be requested.
- statusSchema: This property is a subschema that describes the possible status that the door may have. For example, when Alexa requests to block the door using a pin, the status message sent to the door will be as follows:
{
"event": "pin_unlock",
"pin": 3562
}
And the status answer of the door would be:
{
"state": "closed_unlocked",
"event": "unlock"
}
More info about JSON Schema here.
Alexa Skills:This project is compatible with Alexa Smart Home Skill and has an Custom Skill implementation for unlock, rules and scheduled actions.
Alexa Smart Home Skill
Install: https://goo.gl/nYYYh7
This skill implements:
- Alexa.LockController.
- Alexa.Discovery.
- StateReport.
- Alexa Event Gateway.
When a door lock is requested, the skill delivers a deferred response, and start a Lambda function that subscribes to the MQTT event topic waiting for the door to notify the locked state and then give the definitive response to the Alexa event gateway.
More information about the Alexa Event Gateway here, and here for Alexa Smart Home Skill.
Alexa Event Gateway Token Refresher
This functions connects with AWS LWA and request/refresh/save the access token used for call to Alexa Event Gateway.
See this:
Event gateway reporter
This lambda function is the one that finally reports to the Alexa Event Gateway the final state of the door (locked/unlocked).
VUI Diagram
Rampiot Custom Skill
Note: This Skill is actually in certification process.
This Skill handle:
- Unlock Door
- Door status
- Rule creation
- Scheduled action creation
Unlock Door:
This Custom Skill receives the Unlock voice command and gives the user a progressive response while connecting to the MQTT server waiting for the door Unlock event confirmation and finally delivering the final Successful/Failed response.
Fingerprint authorization is only available for Android, here an debug apk (Android 6+ recommended), you can download the source code here and build it for Android/iOS platform following this instructions.
Utterances:
- Alexa, tell rampiot unlock Door
- Alexa, tell rampiot secure unlock
- Alexa, tell rampiot secure unlock Door
- Alexa, tell rampiot smart unlock
- Alexa, tell rampiot smart unlock Door
You can download from here the complete interaction model.
VUI Diagram
Door Status:
This Custom Skill receives the status voice command and query to rampiot rest api the current Door status.
Utterances:
- Alexa, tell rampiot Door status
- Alexa, tell rampiot device status
VUI Diagram
Rule:
This Custom Skill interacts with user for fill all necessary slots for create the rule.
Utterances:
- Alexa, start rampiot
- Alexa, tell rampiot new rule
- Alexa, tell rampiot create rule
- Alexa, tell rampiot rule when opened
VUI Diagram
Scheduled action:
This Custom Skill interacts with user for fill all necessary slots for create the scheduled action.
Utterances:
- Alexa, start rampiot
- Alexa, tell rampiot schedule action
- Alexa, tell rampiot create schedule action
- Alexa, tell rampiot schedule lock
- Alexa, tell rampiot schedule unlock
VUI Diagram
Components connections
Printed PCBs
Hardware Connection
- Installing required software
1- Shield Layer (Runs on ESP01)
Installation Steps:
1- Clone this repository
2- Download the Arduino IDE from here
3- Connect the FT232R to ESP01
4- On Arduino preferences section "Additional Boards Manager URLs" add this http://arduino.esp8266.com/stable/package_esp8266com_index.json and then OK.
5- Open Boards Manager (Tools/Board/Boards Manager...) and install esp8266 2.3.0 version.
6- Set board configuration on Tools/Board
7- Copy to cloned project into Arduino library folder
8- Open Sketch ShieldSketch.ino in Arduino IDE
9- Compile and upload
2- Arduino Rampiot Client Layer (Runs on Arduino NANO as Lib)
1- Clone this repository
2- Copy cloned folder into Arduino library folder
3- Door Business (Runs on NANO as principal Sketch)
1- Connect Arduino NANO to computer using USB cable.
2- Open downloaded Arduino IDE
3- Select Arduino nano board from Tools/Board
4- Clone this repository
5- Modify this line #define THING_ID 2396641
with your own id
5- Compile and upload
- Print Smart Door Parts
- Converting High torque D772 Solar Servo into Feedback Servo
1- Open servo and find the potentiometer
2- Solder an cable to middle pin of potentiometer
- Cut and solder 3 pin female pin connector to magnetic switch
- Solder 2 cables to 3 pin male pin
- Glue the cable with the male pins to the printed piece
- Solder 3 wires to another male 3-pin connector
- Add arm to servo and move it to 90 degrees
- Change the position of the arm but keep the servo in the 90 degrees position
- Divide the 3 cables of the servo motor
This device by default start as Access Point with name RAMPIOT+<DeviceId>. This exposes an POST URL that received the configuration parameters like:
- SSID
- Password
- Thing Name
These configuration properties are provided from the rampiot application.
Here an demonstration video:
Lock and Unlock ConfigurationBefore using this device you must configure the Lock and Unlock movements, you can see how it is done in the following video:
Reset Wifi / Lock / Unlock ConfigurationBefore this step is completed you need reconfigure all again.
Next Steps and enhancements- I'm working on integrating this with a raspberry pi with camera to create rules based on facial recognition, package delivery, etc. Using AWS Rekognition, this device already has 3 pins available at the bottom for the serial connection with Rpi.
- I'm going to merge the part that holds the servo with the base so that I do not have to stick them, when it unstick it make strange movements when locking and unlocking
- I'm going to improve battery consumption, I get the impression that the servo is consuming more than it should, even when doing detach, I need to investigate and improve it.
- I am just in the process of sending the skills for certification, once they are published I will be modifying this story.
Comments