Raspberry Pi garage opener on k3s cluster
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 -yWe’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 -SCreate 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-piCreate 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: AlwaysCreate 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=8080That’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
viewsfolder in the root directory and create a newindex.pugfile in the views folder - wire up templating engine with application
- render index page on root
/endpoint - integrate button with
/relayendpoint, 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') Closeapp.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 -SCreate 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 🔥");
}
});