Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge dev to main, release v3.0 #22

Merged
merged 3 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/arm/v7,linux/arm64
platforms: linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
2 changes: 1 addition & 1 deletion .github/workflows/pr_docker_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ jobs:
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/arm/v7,linux/arm64
platforms: linux/arm64
push: false
12 changes: 2 additions & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
FROM python:3.12.3-slim-bookworm AS builder
FROM ubuntu:noble-20240429

ARG TZ=America/New_York
RUN apt update && DEBIAN_FRONTEND=noninteractive apt -yq install gcc make
RUN pip install python-telegram-bot requests RPi.GPIO

FROM python:3.12.3-slim-bookworm

ARG TZ=America/New_York
ARG PYVER=3.12

COPY --from=builder /usr/local/lib/python$PYVER/site-packages/ /usr/local/lib/python$PYVER/site-packages/
RUN apt update && DEBIAN_FRONTEND=noninteractive apt -yq install python3-gpiozero python3-requests python3-python-telegram-bot

RUN mkdir /app

Expand Down
30 changes: 19 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,54 @@
# vibinator

This is a complementary app to my [plugmon app](https://github.com/jcostom/plugmon). I'm using plugmon to monitor the Etekcity smart plug that our washer is plugged into. This enables me to tell when the washer is done.
This is a complementary app to my [washerbot app](https://github.com/jcostom/washerbot). I'm using washerbot to monitor the Kasa smart plug that our washer is plugged into. By monitoring power use through the plug's API, I can tell when the washer finishes a load and then kick out a notification to the family.

Ordinarily, I'd just recycle the code and use the same to keep an eye on the dryer, but there's a problem. You see, we've got an electric dryer, and nobody makes a smart plug for a 240V 30A appliance like a dryer. If we got a gas dryer, this would be easy, but I'm not dropping that kind of cash just to get this done.
Ordinarily, I'd just recycle that code and just spin another instance of the same container to monitor the dryer, but there's a hitch. We've got an electric dryer, and nobody makes a 240V 30A smart plug. If we had a gas dryer I could do it, but I'm not about to buy a new dryer just to support notifications.

So, instead of monitoring voltage, we'll look at vibration. When the dryer is running, it's vibrating.
So, instead of monitoring voltage, we'll look at vibration. When the dryer is running, it's vibrating. I'm using an 801s vibration sensor, wiring up +5V DC, Ground, and Digital Output from the sensor to the Pi.

Update - March 2022 - I'm in the process of refactoring this code to run under Docker. It's only ever going to be built as armv7 and arm64 images, as it just doesn't make sense to build as amd64 images ever.
**Update**: As of v3.0, I'm rewriting some of the code here to migrate to the gpiozero Python module. Why do this? The Raspberry Pi 5 completely changed how GPIO works. Fortunately, the gpiozero module supports both old-style GPIO as well as the Pi 5! Also as of v3.0, I'm discontinuing support for non-64-bit ARM platforms.

Check out the example docker-compose file for how you should be launching this thing. Environment variables, with their default values follow:

* TZ: Your Time Zone, default is America/New_York*
* INTERVAL: your polling interval, default is 120s (internally, this is carved into 4 slices)
* SENSOR_PIN: which GPIO pin you're using for the sensor, default is pin 14
* AVG_THRESHOLD: above this value, you declare the dryer as being "on", used to prevent false positives if you're in a "noisy" environment. Default is 0.2
* LOGALL: logs more data during monitoring - useful for debugging monitor intervals and threshold levels, default is False. Set to True if you want more logs. Don't leave this on forever if you use a Pi with a flash card, as flash cards have a finite number of write ops.
* AVG_THRESHOLD: above this value, you declare the dryer as being "on", used to prevent false positives if you're in a "noisy" environment. Default is 0.4
* DEBUG: logs more data during monitoring - useful for debugging monitor intervals and threshold levels, default is False. Set to True if you want more logs. Don't leave this on forever if you use a Pi with a flash card, as flash cards have a finite number of write ops.

You should map the /dev/gpiomem device into the container as well. I believe you can also do a volume mount of /sys:/sys, but I wouldn't advise that for security reasons. Similarly, you could invoke the container as priviliged, but again, I wouldn't do that for security reasons.
You should run this container in privileged mode.

## Wiring and tuning your sensor

I'm using an 801s sensor, which definitely needed some tuning. Typically there's a little screw on the sensor, and you turn it with a screwdriver to tune. They can sometimes be fiddly. If you're tuning, consider dialing down the INTERVAL, and turn on DEBUG while you're tuning. Here's what mine looks like. You don't have to do the same, but I can at least say it works.

![wiring diagram](pi5-with-sensor.png)

## Notifications

As of v2.5 of the container, multiple notification types are supported. Yes, you can do multiple notification types simultaneously too!

## Setting up Telegram
### Setting up Telegram

There are a ton of tutorials out there to teach you how to create a Telegram Bot. Follow one and come back with your Chat ID and Token values. Set the USE_TELEGRAM variable to 1, and set the TELEGRAM_CHATID and TELEGRAM_TOKEN variables and you're set. The old variables of CHATID and MYTOKEN still work as well, but be a good citizen and update to the new variable names please.

## Setting up Pushover
### Setting up Pushover

1. Sign up for an account at the [Pushover](https://pushover.net/) website and install the app on your device(s). Make note of your User Key in the app. It's easy to find it in the settings.

2. Follow their [API Docs](https://pushover.net/api) to create yourself an app you intend to use.

3. Pass the variables USE_PUSHOVER (set this to 1!), PUSHOVER_APP_TOKEN, and PUSHOVER_USER_KEY into the container and magic will happen.

## Setting up Pushbullet
### Setting up Pushbullet

1. Sign up for an account at the Pushbullet website.

2. In the Settings > Account page, setup an API key.

3. Pass the variables USE_PUSHBULLET and PUSHBULLET_APIKEY to the container and wait for magic.

## Setting up Alexa Notifications
### Setting up Alexa Notifications

1. Add the "Notify Me" skill to your Alexa account

Expand Down
6 changes: 4 additions & 2 deletions example-docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ services:
vibinator:
image: jcostom/vibinator:latest
container_name: vibinator
devices:
- /dev/gpiomem:/dev/gpiomem
environment:
- USE_TELEGRAM=1
- TELEGRAM_CHATID=your-chatid-value
- TELEGRAM_TOKEN=your-token-name
- TZ=America/New_York
- DEBUG=0
- INTERVAL=60
- AVG_THRESHOLD=0.95
restart: unless-stopped
privileged: true
networks:
- containers

Expand Down
Binary file added pi5-with-sensor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 15 additions & 16 deletions vibinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import json
import requests
import telegram
import RPi.GPIO
from gpiozero import LineSensor
from time import sleep, strftime

# --- To be passed in to container ---
Expand All @@ -15,10 +15,10 @@
SENSOR_PIN = int(os.getenv('SENSOR_PIN', 14))
INTERVAL = int(os.getenv('INTERVAL', 120))
READINGS = int(os.getenv('READINGS', 1000000))
AVG_THRESHOLD = float(os.getenv('AVG_THRESHOLD', 0.2))
AVG_THRESHOLD = float(os.getenv('AVG_THRESHOLD', 0.8))
SLICES = int(os.getenv('SLICES', 4))
RAMP_UP_READINGS = int(os.getenv('RAMP_UP_READINGS', 4))
RAMP_DOWN_READINGS = int(os.getenv('RAMP_DOWN_READINGS', 4))
RAMP_UP_READINGS = int(os.getenv('RAMP_UP_READINGS', 3))
RAMP_DOWN_READINGS = int(os.getenv('RAMP_DOWN_READINGS', 3))

# Optional
DEBUG = int(os.getenv('DEBUG', 0))
Expand All @@ -43,7 +43,7 @@
ALEXA_ACCESSCODE = os.getenv('ALEXA_ACCESSCODE')

# Other Globals
VER = '2.5.4'
VER = '3.0'
USER_AGENT = f"vibinator.py/{VER}"

# Setup logger
Expand Down Expand Up @@ -96,36 +96,35 @@ def send_notifications(msg: str) -> None:
send_alexa(msg, ALEXA_ACCESSCODE)


def sensor_init(pin: int) -> None:
RPi.GPIO.setwarnings(False)
RPi.GPIO.setmode(RPi.GPIO.BCM)
RPi.GPIO.setup(pin, RPi.GPIO.IN, pull_up_down=RPi.GPIO.PUD_DOWN)
def sensor_init(pin: int) -> LineSensor:
s = LineSensor(pin=pin, pull_up=True)
return s


def take_reading(num_readings: int, pin: int) -> float:
def take_reading(num_readings: int, s: LineSensor) -> float:
total_readings = 0
for _ in range(num_readings):
total_readings += RPi.GPIO.input(pin)
total_readings += s.value
return (total_readings / num_readings)


def main() -> None:
sensor_init(SENSOR_PIN)
logger.info(f"Startup: {USER_AGENT}")
sensor = sensor_init(SENSOR_PIN)
is_running = 0
ramp_up = 0
ramp_down = 0
while True:
slice_sum = 0
for i in range(SLICES):
result = take_reading(READINGS, SENSOR_PIN)
result = take_reading(READINGS, sensor)
DEBUG and logger.debug(f"Slice result was: {result}")
slice_sum += result
sleep(INTERVAL/SLICES)
sleep(INTERVAL / SLICES)
slice_avg = slice_sum / SLICES
DEBUG and logger.debug(f"slice_avg was: {slice_avg}")
if is_running == 0:
if slice_avg >= AVG_THRESHOLD:
if slice_avg <= AVG_THRESHOLD:
ramp_up += 1
if ramp_up > RAMP_UP_READINGS:
is_running = 1
Expand All @@ -136,7 +135,7 @@ def main() -> None:
ramp_up = 0
DEBUG and logger.debug(f"Remains stopped: {slice_avg}")
else:
if slice_avg < AVG_THRESHOLD:
if slice_avg > AVG_THRESHOLD:
ramp_down += 1
if ramp_down > RAMP_DOWN_READINGS:
is_running = 0
Expand Down
Loading