RESTful server for (remote) GPIO control and HA integration examples

Inspired from this idea from Vex, I couldn’t help myself but implement a little RESTful server, that I can roll out to my (Rhasspy) satellites to control their GPIO-pins.

I will provide all my code here, not only for the server and the systemd-configuaration, but also the scripts for HA and even a full example how to control a simple light (on/off) in HA with the status always updated.

I worked on that all day, learned a lot, and provide it here in case someone finds it useful.

The RESTful-server Python-script:

#!/usr/bin/env python3

import RPi.GPIO as GPIO
from flask import Flask, request, make_response
from os import chdir

chdir("/tmp") # because of temp-files created by gpizero/RPi.GPIO/lgpio

GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)

chan_list = [17]
GPIO.setup(chan_list, GPIO.OUT, initial=GPIO.LOW)

application = Flask(__name__)

def create_response(message, http_status_code=200):
  resp = make_response(message, http_status_code)
  resp.headers['message'] = message
  return resp

@application.route('/gpio_status', methods=['GET', 'POST'])
def get_gpio_status():
  pin = request.args.get('pin', default=None, type = int)
  if pin == None:
    return create_response("Kein Pin angegeben!", 400)
  elif pin >=2 and pin <= 27:
    try:
      GPIO.input(pin)
    except RuntimeError as ex:
      return create_response(str(ex.args[0]), 400)

    if request.method == 'GET':
      return create_response("HIGH" if GPIO.input(pin) else "LOW")
    else:
      if not request.form and not request.data:
        return create_response("Es muss 'status=HIGH' oder 'status=LOW' als form- oder payload-Daten angegeben werden!", 400)
      elif     (len(request.form) != 1 or not request.form["status"] in ("HIGH", "LOW")) \
           and (request.data not in (b'status=HIGH', b'status=LOW')):
        return create_response("Es wird nur 'status=HIGH' oder 'status=LOW' als form- oder payload-Daten akzeptiert!", 400)
      else:
        status = None
        if request.data:
          if   request.data == b'status=HIGH':
            status = "HIGH"
          elif request.data == b'status=LOW':
            status = "LOW"
        elif request.form:
          if   request.form["status"] == "HIGH":
            status = "HIGH"
          elif request.form["status"] == "LOW":
            status = "LOW"

        if status == None:
          return create_response("Kein gueltiger Status in den Daten enthalten!", 400)

        GPIO.output(pin, GPIO.HIGH if (status == "HIGH") else GPIO.LOW)
        return create_response("HIGH" if GPIO.input(pin) else "LOW")
  else:
    return create_response("Ungueltiger Pin angegeben!")

if __name__ == '__main__':
    application.run(host="0.0.0.0")
    GPIO.cleanup()

The systemd.service:

[Unit]
Description=A minimal RESTful server for getting and setting the status of GPIO-pins
Requires=network.target
After=network.target

[Install]
WantedBy=multi-user.target

[Service]
User=pi
Group=pi
Type=simple
Restart=always
WorkingDirectory=/tmp
ExecStart=/usr/bin/env python3 /home/pi/scripts/rest_server_for_gpio.py

With that you can have an always running RESTful server that can control the GPIO-pins.
You can interact with it with wget, curl, a browser, or a RESTful client.


Now into HA:

In configuration.yaml define the corresponding rest_commands:

rest_command:
  get_gpio_status:
    url: http://{{ host }}:{{ port }}/gpio_status?pin={{ pin }}
    method: GET
  set_gpio_status:
    url: http://{{ host }}:{{ port }}/gpio_status?pin={{ pin }}
    method: POST
    payload: '{{ payload }}'
    headers:
      content-type: 'text/plain'

Than have a script in HA for conveniently using the rest_command:

sequence:
  - action: rest_command.set_gpio_status
    data:
      host: "{{ host }}"
      port: "{{ port }}"
      pin: "{{ pin }}"
      payload: |
        status={{ status }}
fields:
  host:
    selector:
      text: null
    name: Host
    required: true
  port:
    selector:
      number:
        min: 1
        max: 65534
    name: Port
    default: 5000
    required: true
  pin:
    selector:
      number:
        min: 2
        max: 27
    name: Pin
    required: true
  status:
    selector:
      select:
        options:
          - HIGH
          - LOW
    name: Status
    required: true
alias: "[GPIO] GPIO-Status setzen"
description: ""

With that, you can control every GPIO-pin on every host, that is running the RESTful-server script with ease from within HA.


Last but not least: An example for controling a simple light with always updated status:

First, let’s have a binary_sensor, that holds the state of the light. In configuration.yaml:

template:
  - trigger:
      - platform: homeassistant
        event: start
      - platform: time_pattern
        minutes: /1
      - platform: state
        entity_id: script.lights_deckenfluter_im_schlafzimmer_umschalten
        from: 'on'
        to: 'off'
    action:
      - service: rest_command.get_gpio_status
        data:
          host: rpi0-2W-3
          port: 5000
          pin: 17
        response_variable: response
    binary_sensor:
      - name: Deckenfluter im Schlafzimmer
        unique_id: deckenfluter_im_schlafzimmer
        device_class: light
        state: "{{ response['content'] == 'HIGH' }}"
        icon: mdi:lightbulb

And another script, that controls exactly this light (the one GPIO-pin on the one host) - just for convenience:

sequence:
  - action: script.gpio_gpio_status_setzen
    data:
      port: 5000
      host: rpi0-2W-3
      pin: 17
      status: |-
        {% if status | length > 0 %}
          {% if status == 'AUS' %}
            LOW
          {% else %}
            HIGH
          {% endif %}
        {% else %}
          {% if is_state('binary_sensor.deckenfluter_im_schlafzimmer', 'on') %}
            LOW
          {% else %}
            HIGH
          {% endif %}
        {% endif %}
fields:
  status:
    selector:
      select:
        options:
          - AN
          - AUS
    name: Status
alias: "[Lights] Deckenfluter im Schlafzimmer umschalten"
description: ""

And finally: A button in the Lovelace-dashboard for switching the light on and off:

show_name: true
show_icon: true
type: button
tap_action:
  action: perform-action
  perform_action: script.lights_deckenfluter_im_schlafzimmer_umschalten
entity: binary_sensor.deckenfluter_im_schlafzimmer
icon_height: 20px

There is much that could be optimised, or simplified.
But for now I’m both happy and exhausted, so I won’t change a running system anymore.

So long.

Out of interest, is there a reason you wouldn’t use the remote GPIO integration?

Well, this was, what started all that…
See here:

I thought, that it was deprecated, not maintained anymore, not working anymore…
Now I had another look and see, that it is still in the source code of HA core.

But it “doesn’t work”… if I add a simple switch to configuration.yaml, it never shows up:

switch:
  - platform: remote_rpi_gpio
    host: 192.168.0.233
    ports:
      17: DeckenfluterSchlafz

So, if you tell me, how to use it, I will give it a try and perhaps burst into tears for all the unnecessary time and energy spent.

But if I see something like that:

than perhaps, it’s just better to not use it…

It’s neat, what you’ve done.

Personally, I’ve only experimented with it quite long ago. I thought maybe you tried it too.

It may well be that this integration is somewhat stale too. No idea if it works on an RPi5, e.g. The one that was deprecated and removed previously was the native integration (i.e. the one you’d use if HA was running on the Pi).

Thx.

Yes, I tried it indeed. It was my first approach to all this and I was so happy, that it existed - because naturally I thougth that this is exactly what I needed.
But with it not working (anymore) - and that really is a shame! - my journey began…

I will stick to my solution.
It also has nearly zero CPU consumption - in contrast to pigpiod, which consumes 6-10% CPU constantly.
But it has a noticeable memory footprint, which is not optimal on the Zero 2Ws (2.2% of the 512 MB), but can’t do anything against that, I guess.

1 Like

Hey sic6SaNdMaN,

Great work! Seems like an simple and effective approach, more simple than I made :blush:. Sinds about the beginning of 2022 I am working every now and then on home automation via pi’s and bumped into the same issue: remote GPIO does note recover its connection when Home Assistant reboots (and I even thought that the issue exists when the remote pi reboots, though I left that road quite soon so I cannot remember). Every now and then searching for the issue still to exist, and so it apparently does.

As mentioned, I am working on a more extensive python package. I think it is nearly ready to post. I still need to factor out some personal stuff like Dutch text, ip addresses and Home Assistant token. The token is needed as I use the homeassistant_api package to update an input GPIO instantly to Home Assistant when (de)activated. This refactoring may take tough a couple of weeks or more (its spare time project …)

I was wondering why RPI.GPIO? Only for the CPU usage? I have seen such arguments elsewhere against some other packages. Though RPI.GPIO seems to get more and more outdated. I noted it is not compatible with bookworm, and thus seems to be an issue in long term support. As I am working for about 3 years on home automation, I notices several issues when creating new pi’s with the ancestor’s of my more-and-more generic package. For now, I switched to the recommended gpiozero with LGPIOFactory (lgpio is rather arbitrarily chosen), though it also supports other factories like rpigpio (which seems RPI.GPIO 24. API - Pins — gpiozero 2.0.1 Documentation). Did you consider this? Are there any alternatives?

Jeroen

Hi.

THX :slight_smile:
Sounds interesting! Keep me posted about this, I will definitively try it, when it’s ready and perhaps switch permanently to it, when it fulfills my needs.

About “why RPI.GPIO?”:
I’m using the package “python3-rpi-lgpio”, which provides a modernized interface for the new pis.
It definitively works with bookworm, because that’s what all my pis are on.
It provides (nearly) the same API: RPI.GPIO.
https://rpi-lgpio.readthedocs.io/en/latest/

I don’t have another/a specific reason for using it.
I wanted to use “python3-rpi.gpio”, because I only knew about that - and also don’t know, why NOT to use it.
Than I learned, that “python3-rpi-lgpio” is now the way to go and I didn’t have to change my code… so…

But isn’t that the same LGPIO you referred to with “LGPIOFactory”?
OK, you’re using gpiozero, when I’m reading your post correcly.
Well, I just flew over the docs. It seems the more extensive Lib.
I read about the Lib whilst my researches.
I can’t exactly remember, why I didn’t use/prefer it… perhaps simply because of the fact, that I didn’t (and still don’t) know the differeneces, pros and cons.

But from the docs it should be easy to switch to a DigitalOutputDevice that controls my relais:
https://gpiozero.readthedocs.io/en/stable/api_output.html#gpiozero.DigitalOutputDevice
Correct me, if I’m wrong.

So the gpiozero-Lib ist just the better (and perhaps log term) supported one?
If so, I will definitively consider moving to it.

Nice, I will keep you posted.

I tried rpi-lgpio aswel, very confusing all those different packages, rpi-lgpio seems newer, not sure anymore why I left it …

Yes, I use gpiozero with LGPIO, it may seem more extensive, but so far I only use DigitalOutputDevice and DigitalInputDevice. I found it very easy to switch.

As a downside, I thought I read somewhere “it” (gpiozero or one of its factories) creates a relative large overhead in cpu and/or number of threads. But I accepted it, hoping to last longer in support :wink:

Hey sic6SaNdMaN, you should be able to find the package here: JOTD / GPIOpinAPI · GitLab.