Skip to content
Open
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,17 @@ use the default user "pi" to run the calendar, and this user has sudo permission
17. `reboot` the pi
18. Log in to your Google account on the Pi
19. Put a bow on it. You're done!

## Adding a passive infra-red sensor
1. Get an HC-SR501 sensor module. Set its L/H option to "H", either with a jumper or a dot of solder.
2. Connect wires to pins 4, 6, and 16 on the top row of connectors. With the connector at the top left of the board,
the top leftmost pin is #2, and the top row is numbered by twos (the bottom row gets the ones). Pin 16 is GPIO 23.
If you change the scripts, you can change the pin.
3. With the HC-SR501 pins at the top and facing you, the leftmost pin is ground. Connect that to the wire from pin 6 on the Pi.
4. Connect the rightmost pin on the HC-SR501 to pin 4 from the Pi.
5. Connect the center pin on the HC-SR501 to pin 16 from the Pi.
6. Mount the HC-SR501 to your monitor, but away from the power LED or anything else that might mess with detection.
7. When you run the playbook (either by `ansible-playbook` or `calendar-install.run`), add a parameter 'DPMS_TIMEOUT_MIN=20'.
(Set it to your favorite value. I like 20 minutes.) For `ansible-playbook` execution, you'll want to prefix that with `--extra-vars `
8. The playbook installs a `pir-monitor` systemd service that wakes the display when motion is detected. Set `CEC_FALLBACK=true` if your monitor needs HDMI‑CEC to wake up. The service starts automatically on boot and can be checked with `systemctl status pir-monitor`.
9. Watch it go
5 changes: 4 additions & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,7 @@ grep -q localhost $kh || ssh-keyscan localhost >>$kh
# install Ansible if necessary
sudo dpkg -s ansible 2>&1 >/dev/null || sudo apt-get install -y ansible

ansible-playbook playbook.yml -i hosts-localhost -u pi
if [ $# -gt 0 ]; then
EXTRA_VARS="--extra-vars $*"
fi
ansible-playbook playbook.yml -i hosts-localhost -u pi $EXTRA_VARS
2 changes: 1 addition & 1 deletion release_number.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1
2
15 changes: 15 additions & 0 deletions roles/calendar/files/pir-monitor.service.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[Unit]
Description=PIR sensor monitor to wake display on motion
After=graphical.target

[Service]
Type=simple
User=pi
Group=pi
ExecStart=/usr/local/bin/pirMonitor
Environment=DISPLAY=:0.0
Environment=CEC_FALLBACK={{ '1' if CEC_FALLBACK | bool else '0' }}
Restart=always

[Install]
WantedBy=graphical.target
93 changes: 93 additions & 0 deletions roles/calendar/files/pirMonitor
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env python3

"""Monitor a PIR sensor and wake the display when motion is detected."""

import argparse
import logging
import os
import shutil
import subprocess
import time

import RPi.GPIO as GPIO


def display_is_on() -> bool:
"""Return True if the monitor appears to be on."""
try:
out = subprocess.check_output(["xset", "-q"], text=True, stderr=subprocess.DEVNULL)
except (FileNotFoundError, subprocess.CalledProcessError):
return True # Assume on if we cannot determine
for line in out.splitlines():
if "Monitor is" in line:
return "On" in line
return True


def wake_display(use_cec: bool) -> None:
"""Wake the display using DPMS and optionally HDMI-CEC."""
if os.getenv("XDG_SESSION_TYPE") == "wayland":
cmd = ["loginctl", "unlock-sessions"]
else:
cmd = ["xset", "dpms", "force", "on"]
subprocess.run(cmd, check=False)
time.sleep(2)
if use_cec and not display_is_on():
logging.info("Falling back to HDMI-CEC")
if shutil.which("cec-client"):
try:
subprocess.run(
["cec-client", "-s", "-d", "1"],
input="on 0\n",
text=True,
check=False,
)
except Exception as exc: # pragma: no cover - best effort
logging.debug("CEC command failed: %s", exc)
else:
logging.debug("cec-client not installed")


def main() -> None:
parser = argparse.ArgumentParser(description="PIR motion monitor")
parser.add_argument("--pin", type=int, default=23, help="GPIO pin number")
parser.add_argument(
"--idle",
type=int,
default=30,
help="minimum seconds between wake commands",
)
parser.add_argument("--display", default=":0.0", help="display for xset")
parser.add_argument(
"--cec-fallback",
action="store_true",
default=os.getenv("CEC_FALLBACK", "0") in ("1", "true", "True"),
help="use HDMI-CEC if DPMS fails",
)
args = parser.parse_args()

os.environ["DISPLAY"] = args.display
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")

GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(args.pin, GPIO.IN)

last_trigger = 0.0
logging.info("Monitoring GPIO pin %s", args.pin)
try:
while True:
GPIO.wait_for_edge(args.pin, GPIO.RISING)
now = time.time()
if now - last_trigger >= args.idle:
logging.info("Motion detected")
wake_display(args.cec_fallback)
last_trigger = now
except KeyboardInterrupt:
pass
finally:
GPIO.cleanup()


if __name__ == "__main__":
main()
4 changes: 3 additions & 1 deletion roles/calendar/files/startup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@
# [ $(lsusb | wc -l) -lt 4 ] && echo '1-1' | sudo tee /sys/bus/usb/drivers/usb/unbind

# Start the browser
chromium-browser --app=https://calendar.google.com/calendar/r/custom/7/d --user-agent="Mozilla/5.0 (Windows; U; Windows NT 6.1; rv:2.2) Gecko/20110201" --disable-infobars --disable-session-crashed-bubble --kiosk &
nohup chromium-browser --app=https://calendar.google.com/calendar/r/custom/7/d --user-agent="Mozilla/5.0 (Windows; U; Windows NT 6.1; rv:2.2) Gecko/20110201" --disable-infobars --disable-session-crashed-bubble --kiosk </dev/null 2>&1 >/dev/null &

# The pir-monitor systemd service handles keeping the display awake when blanking is enabled
72 changes: 59 additions & 13 deletions roles/calendar/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
- name: Fetch Raspberry pi model
slurp:
src: "/sys/firmware/devicetree/base/model"
register: rpi_model_data

- name: Set Raspberry Pi model
ansible.builtin.set_fact:
rpi_model: "{{ rpi_model_data['content'] | b64decode }}"

- name: Install Chromium
package:
name: chromium-browser
Expand All @@ -8,12 +17,12 @@
name: scrot
state: latest

- name: Install feh
- name: Install feh to display an image
package:
name: feh
state: latest

- name: Install wmctrl
- name: Install wmctrl to control windows
package:
name: wmctrl
state: latest
Expand All @@ -23,12 +32,18 @@
name: unclutter
state: latest

- name: Install cec-utils for HDMI-CEC control
package:
name: cec-utils
state: latest
when: CEC_FALLBACK | bool

- name: Install python-uinput to fake keystrokes in the browser
pip:
executable: /usr/bin/pip3
name: python-uinput

- name: Load the uinput module so we can fake keystrokes
- name: Load the uinput module to fake keystrokes
lineinfile:
dest: /etc/modules
create: true
Expand All @@ -48,7 +63,7 @@
mode: '0755'
owner: pi

- name: Install refresh_cal script
- name: Install refresh_cal script to scroll to the right spot on the calendar periodically
copy:
src: refresh_cal
dest: /home/pi/refresh_cal
Expand All @@ -75,7 +90,7 @@
job: "/home/pi/refresh_cal"
when: false

- name: Autostart folder
- name: Autostart folder for LXDE
file:
path: "/home/pi/.config/autostart"
state: directory
Expand All @@ -89,19 +104,50 @@
mode: '0644'
owner: pi

- name: Remove default screensaver config
- name: Remove default graphical screensaver config
lineinfile:
dest: /etc/xdg/lxsession/LXDE-pi/autostart
state: absent
line: "@xscreensaver -no-splash"

- name: Turn off screensaver in LXDE autostart
blockinfile:
template:
src: autostart-no-screensaver
dest: /etc/xdg/lxsession/LXDE-pi/autostart
create: true
mode: '0644'
owner: pi
group: root
when: ( DPMS_TIMEOUT_MIN | int ) <= 0

- name: Copy the script to monitor PIR sensor
copy:
src: pirMonitor
dest: /usr/local/bin/pirMonitor
mode: '0755'
owner: root
group: root
when: ( DPMS_TIMEOUT_MIN | int ) > 0

- name: Install systemd service for PIR monitor
template:
src: pir-monitor.service.j2
dest: /etc/systemd/system/pir-monitor.service
mode: '0644'
when: ( DPMS_TIMEOUT_MIN | int ) > 0

- name: Enable and start pir-monitor service
systemd:
name: pir-monitor.service
enabled: yes
state: started
daemon_reload: yes
when: ( DPMS_TIMEOUT_MIN | int ) > 0

- name: Turn on screensaver in LXDE autostart
template:
src: autostart-with-DPMS
dest: /etc/xdg/lxsession/LXDE-pi/autostart
mode: '0644'
owner: pi
block: |
@xset s noblank
@xset s off
@xset s -dpms
# for Pi 4, the last line should be "@xset dpms 0 0 0", but I haven't figured out how to detect the Pi 4 yet.
group: root
when: ( DPMS_TIMEOUT_MIN | int ) > 0
6 changes: 6 additions & 0 deletions roles/calendar/templates/autostart-no-screensaver
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@lxpanel --profile LXDE-pi
@pcmanfm --desktop --profile LXDE-pi
point-rpi
@xset s noblank
@xset s off
@xset s {% if rpi_model.startswith("Raspberry Pi 4") %}dpms 0 0 0{% else %}-dpms{% endif %}
6 changes: 6 additions & 0 deletions roles/calendar/templates/autostart-with-DPMS
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@lxpanel --profile LXDE-pi
@pcmanfm --desktop --profile LXDE-pi
point-rpi
@xset s blank
@xset s dpms
@xset s {{ ( DPMS_TIMEOUT_MIN | int ) * 60 }} {{ ( DPMS_TIMEOUT_MIN | int ) * 60 }}
6 changes: 6 additions & 0 deletions roles/calendar/vars/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
# Screensaver timeout in minutes - 0 means no timeout
# Configure a PIR sensor to show the display when necessary
DPMS_TIMEOUT_MIN: 0
# Enable HDMI-CEC fallback if DPMS does not wake the display
CEC_FALLBACK: false