Skip to main content

Raspberry Pi garage opener on k3s cluster

·5 mins

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
  1. add the views folder in the root directory and create a new index.pug file in the views folder
  2. wire up templating engine with application
  3. render index page on root / endpoint
  4. 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 &#xF62B;
  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 🔥");
  }
});