diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index bf68b52..ec01acd 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -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 }} diff --git a/.github/workflows/pr_docker_test.yml b/.github/workflows/pr_docker_test.yml index df7254c..1fc4e88 100644 --- a/.github/workflows/pr_docker_test.yml +++ b/.github/workflows/pr_docker_test.yml @@ -21,5 +21,5 @@ jobs: uses: docker/build-push-action@v5 with: context: . - platforms: linux/arm/v7,linux/arm64 + platforms: linux/arm64 push: false diff --git a/Dockerfile b/Dockerfile index 940796a..1c9fa00 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index daac3d4..9c3906f 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,38 @@ # 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. @@ -32,7 +40,7 @@ There are a ton of tutorials out there to teach you how to create a Telegram Bot 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. @@ -40,7 +48,7 @@ There are a ton of tutorials out there to teach you how to create a Telegram Bot 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 diff --git a/example-docker-compose.yaml b/example-docker-compose.yaml index 2691d47..1e08517 100644 --- a/example-docker-compose.yaml +++ b/example-docker-compose.yaml @@ -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 diff --git a/pi5-with-sensor.png b/pi5-with-sensor.png new file mode 100644 index 0000000..a934312 Binary files /dev/null and b/pi5-with-sensor.png differ diff --git a/vibinator.py b/vibinator.py index 7224f28..7b10f0d 100755 --- a/vibinator.py +++ b/vibinator.py @@ -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 --- @@ -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)) @@ -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 @@ -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 @@ -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