Hardware components | ||||||
![]() |
| × | 1 | |||
| × | 3 | ||||
![]() |
| × | 3 | |||
![]() |
| × | 3 | |||
| × | 1 | ||||
| × | 1 | ||||
Hand tools and fabrication machines | ||||||
![]() |
|
When my parents set up our smart home system, they put different devices on different networks and some weren't implemented properly at all. This is why I decided to make a smart home station that is reliant on remotes and API Calls.
To control my lights and fan, I ripped open two remotes (one for lights and one for fan), soldered connections to the two poles of the switch and hooked it up to relays to simulate a button press. To control Kasa Smart Wi-Fi Plugs, I used Kasa's API. In this project I am going to be detailing the hardware component of the setup, and the scheduling.
ElectronicsI first started by tearing apart the remote to my lights and soldering leads where the button used to be:
This made it so that when the wires touched, my lights would turn on and off based on which wires were being touched.
I then made a simple circuit so that I could choose when to run an action:
The buttons would allow you to cycle between lights on and off, and also allow you to select the action.
I realized that the five buttons could simply be replaced with a joystick, giving me the exact functionality I needed:
I finally ended up soldering the whole thing onto a perf board:
This project can be replicated with almost any remote as the relay completes the circuit just as if the button was depressed.
Wiring the Joystick and the LCD screen

Full code for the Particle Argon
C/C++#include "Particle.h"
#include <LiquidCrystal_I2C_Spark.h>
SYSTEM_THREAD(ENABLED);
//LCD Info ==
LiquidCrystal_I2C *lcd; // Initialize variable for LCD
const String textToDisplay[] = {"Lights On", "Lights Off", "Toggle Fan", "Schedule Task", "Screen Off", "Power off"};
const int ACTIONSLEN = arraySize(textToDisplay);
int startTime = 0;
// Relays ==
const int RELAY_LIGHTS_ON_PIN = 7;
const int RELAY_LIGHTS_OFF_PIN = 8;
const int TOGGLE_FAN_PIN = 6;
// Joystick ==
const int JOYSTICK_CLICK_PIN = 13;
const int JOYSTICK_Y_PIN = A4;
const int JOYSTICK_X_PIN = A2;
// Joystick offsets ==
const int JOYSTICK_OFFSET = 400;
const int JOYSTICK_CENTER_Y = 1200;
const int JOYSTICK_CENTER_X = 1200;
// Joystick action nums ==
const int JOYSTICK_LOCATION_CENTER = 0;
const int JOYSTICK_LOCATION_LEFT = 1;
const int JOYSTICK_LOCATION_RIGHT = 2;
const int JOYSTICK_LOCATION_UP = 3;
const int JOYSTICK_LOCATION_DOWN = 4;
const int JOYSTICK_ACTION_CLICK = 5;
// Running state
int state = 0;
// Task Scheduler ==
retained int targetHour = 0; // 0 - 12
retained int targetMinute = 0; // 0 - 60
retained bool isAM = true;
retained bool screenOff = false;
int currHour = 0;
int compMinute = 0;
int cursorPos = 0;
retained bool isSCHEDULED = false;
retained int scheduledState = 0;
bool SCHEDULEMODE = false;
String centeredText;
SystemSleepConfiguration config;
int lastMinute = 0;
void setup() {
System.enableFeature(FEATURE_RETAINED_MEMORY);
Watchdog.init(WatchdogConfiguration().capabilities(WatchdogCap::DEBUG_RUNNING | WatchdogCap::RESET).timeout(30s));
Watchdog.start();
Watchdog.refresh();
lcd = new LiquidCrystal_I2C(0x27, 20, 4);
lcd->init();
lcd->backlight();
lcd->display();
pinMode(RELAY_LIGHTS_OFF_PIN, OUTPUT);
pinMode(RELAY_LIGHTS_ON_PIN, OUTPUT);
pinMode(TOGGLE_FAN_PIN, OUTPUT);
pinMode(JOYSTICK_CLICK_PIN, INPUT_PULLUP);
pinMode(JOYSTICK_Y_PIN, INPUT);
pinMode(JOYSTICK_X_PIN, INPUT);
Time.zone(-5);
lastMinute = Time.minute();
updateDisplay();
}
void loop(void) {
if((isAM == Time.isAM()) && (targetMinute == Time.minute()) && (targetHour == Time.hourFormat12()) && isSCHEDULED){
runTask(scheduledState, true);
isSCHEDULED = false;
}
if (lastMinute != Time.minute()) {
lastMinute = Time.minute();
updateDisplay();
}
if(screenOff){
runTask(8, false);
}
switch (checkJoystickState()) {
case JOYSTICK_LOCATION_UP:
state = (state + 1) % (ACTIONSLEN);
delay(250);
updateDisplay();
break;
case JOYSTICK_LOCATION_DOWN:
state = (state + (ACTIONSLEN - 1)) % (ACTIONSLEN);
delay(250);
updateDisplay();
break;
case JOYSTICK_ACTION_CLICK:
runTask(state, false);
break;
default:
break;
}
Watchdog.refresh();
}
void displayFinishedTask(String text){
clearLine(1);
lcd->setCursor(0,1);
lcd->print(padText(text));
delay(1500);
updateDisplay();
}
void relayToggle(int pin){
digitalWrite(pin, HIGH);
delay(300);
digitalWrite(pin, LOW);
}
void runTask(int state, bool wasScheduled){
switch(state){
case 0:
relayToggle(RELAY_LIGHTS_ON_PIN);
displayFinishedTask("Light turned on!");
break;
case 1:
relayToggle(RELAY_LIGHTS_OFF_PIN);
displayFinishedTask("Light turned off!");
break;
case 2:
relayToggle(TOGGLE_FAN_PIN);
displayFinishedTask("Fan Toggled!");
break;
case 3:
delay(300);
enterScheduleMode();
break;
case 4:
lcd->clear();
print4L("Powering off...", "Goodbye!", "Click Joystick to", "Power On!");
delay(2000);
lcd->noBacklight();
lcd->noDisplay();
screenOff = true;
delay(2000);
while(checkJoystickState() != JOYSTICK_ACTION_CLICK){
Watchdog.refresh();
if((isAM == Time.isAM()) && (targetMinute == Time.minute()) && (targetHour == Time.hourFormat12()) && isSCHEDULED){
runTask(scheduledState, true);
isSCHEDULED = false;
}
}
screenOff = false;
delay(200);
lcd->display();
lcd->backlight();
state = 0;
updateDisplay();
break;
case 5:
lcd->clear();
print4L("Powering off...", "Goodbye!", "Click Joystick to", "Power On!");
delay(2000);
lcd->noBacklight();
lcd->noDisplay();
delay(2000);
config.mode(SystemSleepMode::HIBERNATE)
.gpio(JOYSTICK_CLICK_PIN, FALLING);
System.sleep(config);
break;
default:
clearLine(1);
lcd->setCursor(0,1);
lcd->print("*****ERROR*****");
break;
}
String data = "[\"" + textToDisplay[state] + "\", \"" + String(wasScheduled) + "\"]";
Particle.publish("pushToSheet", data, PRIVATE);
}
void print4L(String line1, String line2, String line3, String line4){
lcd->clear();
lcd->setCursor(0, 0);
lcd->print(line1);
lcd->setCursor(0, 1);
lcd->print(line2);
lcd->setCursor(0, 2);
lcd->print(line3);
lcd->setCursor(0, 3);
lcd->print(line4);
}
String padText(String text) {
uint16_t displayWidth = 20;
int textLength = text.length();
if (textLength >= displayWidth) {
return text.substring(0, displayWidth);
}
int padding = (displayWidth - textLength) / 2;
centeredText = "";
for (int i = 0; i < padding; i++) {
centeredText += ' ';
}
centeredText += text;
while (centeredText.length() < displayWidth) {
centeredText += ' ';
}
return centeredText;
}
void updateDisplay(){
String currTime = (String(Time.hourFormat12()) + ":" + (String(Time.minute()).length() < 2 ? ("0" + String(Time.minute())) : String(Time.minute())) + " " + (Time.isAM() ? "AM" : "PM"));
String schedhTime = isSCHEDULED ? (String(targetHour) + ":" + (String(targetMinute).length() < 2 ? ("0" + String(targetMinute)) : String(targetMinute)) + " " + (isAM ? "AM" : "PM")) : "Pending";
int schedhTimeLen = schedhTime.length();
int state2 = (state + 1) % (ACTIONSLEN);
print4L
(
(String(Time.hourFormat12()) + ":" + (String(Time.minute()).length() < 2 ? ("0" + String(Time.minute())) : String(Time.minute())) + " " + (Time.isAM() ? "AM" : "PM")),
"--------------------",
padText(textToDisplay[state]),
padText(textToDisplay[state2])
);
lcd->setCursor(20 - schedhTimeLen, 0);
lcd->print(schedhTime);
lcd->setCursor(0, 2);
lcd->print(">");
lcd->setCursor(19, 2);
lcd->print("<");
}
void clearLine(int line) {
lcd->setCursor(0, line);
lcd->print(" ");
}
void updateDisplaySchedule() {
uint8_t minLen = String(targetMinute).length();
uint8_t minHour = String(targetHour).length();
String targMinStr = minLen < 2 ? "0" + String(targetMinute) : String(targetMinute);
String targHrStr = minHour < 2 ? "0" + String(targetHour): String(targetHour);
lcd->clear();
lcd->setCursor(4, 0);
lcd->print(targHrStr + ":" + targMinStr + " ");
lcd->print(isAM ? "AM" : "PM");
lcd->setCursor((cursorPos * 3) + 4, 1);
lcd->print("^");
}
void enterScheduleMode() {
SCHEDULEMODE = true;
clearLine(0);
clearLine(1);
updateDisplaySchedule();
bool loopE = true;
while(loopE){
Watchdog.refresh();
if (checkJoystickState() == JOYSTICK_LOCATION_RIGHT) {
cursorPos = (cursorPos + 1) % 3;
updateDisplaySchedule();
delay(100);
}
if (checkJoystickState() == JOYSTICK_LOCATION_LEFT) {
cursorPos = (cursorPos + 2) % 3;
updateDisplaySchedule();
delay(100);
}
if (checkJoystickState() == JOYSTICK_LOCATION_DOWN) {
if (cursorPos == 0) {
targetHour = (targetHour + 1) % 13;
} else if (cursorPos == 1) {
targetMinute = (targetMinute + 1) % 61;
} else if (cursorPos == 2) {
isAM = !isAM;
}
updateDisplaySchedule();
delay(100);
}
if (checkJoystickState() == JOYSTICK_LOCATION_UP) {
if (cursorPos == 0) {
targetHour = (targetHour + 12) % 13;
} else if (cursorPos == 1) {
targetMinute = (targetMinute + 60) % 61;
} else if (cursorPos == 2) {
isAM = !isAM;
}
updateDisplaySchedule();
delay(100);
}
if (checkJoystickState() == JOYSTICK_ACTION_CLICK){
loopE = false;
SCHEDULEMODE = false;
}
}
lcd->clear();
delay(500);
lcd->setCursor(0,0);
lcd->print(" Pick a Task!");
delay(2000);
bool loopB = true;
state = 0;
updateDisplay();
while(loopB){
Watchdog.refresh();
if(checkJoystickState() == JOYSTICK_LOCATION_UP){
scheduledState = (scheduledState + 1) % (ACTIONSLEN - 2);
state = (state + 1) % (ACTIONSLEN - 2);
delay(250);
updateDisplay();
}
if(checkJoystickState() == JOYSTICK_LOCATION_DOWN){
scheduledState = (scheduledState + (ACTIONSLEN - 3)) % (ACTIONSLEN - 2);
state = (state + (ACTIONSLEN - 3)) % (ACTIONSLEN - 2);
delay(250);
updateDisplay();
}
if (checkJoystickState() == JOYSTICK_ACTION_CLICK){
loopB = false;
}
}
lcd->clear();
lcd->setCursor(0, 0);
lcd->print(" Scheduled for:");
lcd->setCursor(3, 1);
uint8_t minLen = String(targetMinute).length();
uint8_t minHour = String(targetHour).length();
String taskArr[3] = {"Lights On", "Lights Off", "Toggle Fan"};
String taskSelected = taskArr[scheduledState];
String targMinStr = minLen < 2 ? "0" + String(targetMinute) : String(targetMinute);
String targHrStr = minHour < 2 ? "0" + String(targetHour): String(targetHour);
lcd->print(targHrStr + ":" + targMinStr + " ");
lcd->print(isAM ? "AM" : "PM");
isSCHEDULED = true;
delay(2000);
lcd->clear();
lcd->setCursor(0, 0);
lcd->print("Scheduled Task:");
lcd->setCursor(0, 1);
lcd->print(padText(taskSelected));
delay(2000);
state = 0;
lcd->clear();
updateDisplay();
}
int checkJoystickState(){
int yValue = analogRead(JOYSTICK_Y_PIN);
int xValue = analogRead(JOYSTICK_X_PIN);
if (digitalRead(JOYSTICK_CLICK_PIN) == LOW){
return JOYSTICK_ACTION_CLICK;
}
if (yValue < JOYSTICK_CENTER_Y - JOYSTICK_OFFSET) {
return JOYSTICK_LOCATION_DOWN;
}
else if (yValue > JOYSTICK_CENTER_Y + JOYSTICK_OFFSET) {
return JOYSTICK_LOCATION_UP;
}
else if (xValue < JOYSTICK_CENTER_X - JOYSTICK_OFFSET) {
return JOYSTICK_LOCATION_LEFT;
}
else if (xValue > JOYSTICK_CENTER_X + JOYSTICK_OFFSET) {
return JOYSTICK_LOCATION_RIGHT;
}
else {
return JOYSTICK_LOCATION_CENTER;
}
}
Comments