Raspberry Pi garage opener on k3s cluster
Table of Contents
Many articles are out there which demonstrate how to use a raspberry pi as a DIY garage door opener project. Few are outdated and not deployed using container images. I found a few reasonable solutions on google, but I couldn’t run them on the Kubernetes cluster due to older packages or insufficient information. I decided to build my solution from different sources of information I found
What we’ll cover in this post
- setup nodejs project to simulate a button
- create a container from nodejs application
- deploy the container on the Kubernetes cluster
Kudos! to the author for this excellent article for showing us how to connect the relay and magnetic switch to raspberry pi for our purpose.
Once finished wiring up all components. Let’s start setting up our Nodejs application.
Application setup #
Create an npm project
mkdir garage-pi && cd garage-pi
npm init -y
We’ll be using node-rpio package, which provides access to the Raspberry Pi GPIO interface
Install node-rpio
and express
packages
npm i rpio express -S
Create an express app that starts on port 8080
in server.js
file
"use strict";
const express = require("express");
const rpio = require("rpio");
const app = express();
const PORT = 8080;
app.use("/assets", express.static("assets"));
app.listen(PORT);
console.log("Running on http://localhost:" + PORT);
The below code lets you simulate a button press; In our case, PIN is 19
. First, we want to output to low and set the pin to high after 1000ms. Please refer to rpio
repo for more explanation
const openPin = process.env.OPEN_PIN || 19;
const relayPin = process.env.RELAY_PIN || 11;
app.get("/relay", function (req, res) {
// Simulate a button press
rpio.write(relayPin, rpio.LOW);
setTimeout(function () {
rpio.write(relayPin, rpio.HIGH);
res.send("done");
}, 1000);
});
To get the state of pin.
function getState() {
return {
open: !rpio.read(openPin),
};
}
app.get("/status", function (req, res) {
res.send(JSON.stringify(getState()));
});
Complete server.js
file
"use strict";
const express = require("express");
const rpio = require("rpio");
const app = express();
const PORT = 8080;
const openPin = process.env.OPEN_PIN || 19;
const relayPin = process.env.RELAY_PIN || 11;
app.use("/assets", express.static("assets"));
function getState() {
return {
open: !rpio.read(openPin),
};
}
app.get("/status", function (req, res) {
res.send(JSON.stringify(getState()));
});
app.get("/relay", function (req, res) {
// Simulate a button press
rpio.write(relayPin, rpio.LOW);
setTimeout(function () {
rpio.write(relayPin, rpio.HIGH);
res.send("done");
}, 1000);
});
app.listen(PORT);
console.log("Running on http://localhost:" + PORT);
Container image #
- Create a dockerfile with multi-stage builds using nodejs arm image
- Install the necessary python package for
rpio
- Build docker image
- Publish the docker image to your repo in docker hub
- Create new Kubernetes deployment and service
# Fetch node_modules for backend; nothing here except
# the node_modules dir ends up in the final image
FROM arm32v7/node:12.18-alpine as builder
RUN mkdir /app
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json /app/package.json
RUN apk add --no-cache make gcc g++ python && \
npm install --production --silent && \
apk del make gcc g++ python
RUN npm install
# Add the files to the arm image
FROM arm32v7/node:12.18-alpine
RUN mkdir /app
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
# Same as earlier, be specific or copy everything
ADD package.json /app/package.json
ADD package-lock.json /app/package-lock.json
ADD . /app
COPY --from=builder /app/node_modules /app/node_modules
ENV PORT=8080
EXPOSE 8080
CMD [ "npm", "start" ]
Docker buildx feature lets you build arm-based images on mac or windows system
docker buildx build --platform linux/arm64 -t <docker-username>/garage-pi .
docker push <docker-username>/garage-pi
Create a new deployment named garage-pi
that runs the earlier published image.
kind: Deployment
apiVersion: apps/v1
metadata:
name: garage-pi
labels:
app.kubernetes.io/name: garage-pi
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/instance: garage-pi
app.kubernetes.io/name: garage-pi
template:
metadata:
creationTimestamp: null
labels:
app.kubernetes.io/instance: garage-pi
app.kubernetes.io/name: garage-pi
spec:
volumes:
- name: dev-snd
hostPath:
path: /dev/mem
type: ''
containers:
- name: garage-pi
image: '<docker-username>/garage-pi:latest' ##update username here
ports:
- name: http
containerPort: 8080
protocol: TCP
resources: {}
volumeMounts:
- name: dev-snd
mountPath: /dev/mem
livenessProbe:
httpGet:
path: /
port: http
scheme: HTTP
readinessProbe:
httpGet:
path: /
port: http
scheme: HTTP
imagePullPolicy: Always
securityContext:
privileged: true
restartPolicy: Always
Create a service for a garage-pi
deployment, which serves on port 8080
and connects to the containers on port 8080
.
kubectl expose deployment nginx --port=8080 --target-port=8080
That’s it; you should be able to hit the endpoint and simulate button click!
Frontend #
For frontend of application, we’ll use pugjs templating engine
Install pug
package
npm i pug -S
- add the
views
folder in the root directory and create a newindex.pug
file in the views folder - wire up templating engine with application
- render index page on root
/
endpoint - integrate button with
/relay
endpoint, which will open/closed garage door
doctype html
head
meta(charset='utf-8')
meta(name='viewport', content='width=device-width, initial-scale=1, shrink-to-fit=no')
meta(name='description', content='')
meta(name='author', content='')
title Garage Opener
.text-center
form.form-signin(method='POST', action='/relay')
h1.h1.mb-2.font-weight-normal(style='color: #FFFFFF') Garage Door
.text-center
.form-signin
.text-center
#open
h1.h2.mb-3.font-weight-normal(style='color: #CF6679') The Door is Open 
button#mainButton.btn.btn-lg.btn-primary.btn-block(type='submit') Close
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
app.get("/", function (req, res) {
res.render("index", getState());
});
Twilio Integration (Optional) #
Install dotenv
, twilio
and node-schedule
packages
npm i dotenv node-schedule twilio -S
Create a .env
file at the root of the project and add your Twilio auth key, account sid, and phone number
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_PHONE_NUMBER=
Create a twilio.js
file, add the below code
require("dotenv").config();
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const sendSms = (phone, message) => {
const client = require("twilio")(accountSid, authToken);
client.messages
.create({
body: message,
from: process.env.TWILIO_PHONE_NUMBER,
to: phone,
})
.then((message) => console.log(message.sid));
};
module.exports = sendSms;
Schedule a job for every 15mins to check the state of the garage door. If the door is open, we’ll send a message
const sendSms = require("./twilio");
...
...
schedule.scheduleJob("*/15 * * * *", function () {
var status = JSON.parse(JSON.stringify(getState()));
if (status.open) {
sendSms("<YOUR-NUMBER>", "Garage door is open 🔥");
}
});