Compare commits

...

33 Commits

Author SHA1 Message Date
8c225bbe06 Stop the kiosk coming up blank after every reboot
A reboot kills Chromium rather than closing it, so it writes "exit_type":"Crashed" into
its profile. On the next boot it came up with a restore prompt and a tab stuck for ever
on a blank "Loading..." page - navigation never committed, navigator.serviceWorker was
still undefined, nothing on screen. On a device with a monitor and no keyboard that is
permanent, and the backend was healthy throughout: / answered in 5 ms and /api/library
returned 812 kB the whole time.

--disable-session-crashed-bubble hides the prompt but not the state behind it, and here
the prompt showed anyway. .xinitrc now rewrites exit_type and exited_cleanly before
launching, which is the usual remedy for a kiosk that gets powered off rather than shut
down, and adds --hide-crash-restore-bubble (the current name for the flag).

Verified by rebooting and leaving it alone: the page comes up on its own with its title
and its content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 17:31:52 +02:00
831a1b50a4 Run the kiosk on Chromium, and take the stray media server off musicdolphin
Chromium: same page, same machine, idle, Firefox 24.2% of a core against Chromium's
11.4%. The build comes from archive.raspberrypi.com (a "+rpt" version) and is patched
for this board's V3D GPU; Debian ships a much older chromium, so the archive matters.
The whole userland here is 32-bit armhf on a 64-bit kernel, which Firefox handles worse
than Chromium does. --use-gl=egl, --enable-gpu-rasterization, --ignore-gpu-blocklist
and --enable-zero-copy are what keep rasterization on the GPU: Chromium's blocklist
does not recognise this driver and silently falls back to software without them.
pi_kiosk_browser: firefox still works, as the fallback if an update ever regresses.

Debug mode also gains xdotool, scrot and --remote-debugging-port=9222 (on 127.0.0.1),
because firing XTEST key events at a window lands about half the time and a benchmark
you cannot verify the state of is worse than none.

pi_squeezeserver gains an absent path, and mediapis.yml derives its state from
mediapi_has_squeezeserver, which defaults to false for the whole group. false means
"actively remove", not "skip": musicdolphin has been running a Logitech Media Server
that no playbook installs and nothing talks to - every squeezelite in the fleet points
at pi_squeezelite_squeezeserver (192.168.178.80, the server) - while costing ~30 MB
resident and ~47 MB of swap on a 2 GB Pi that also drives the kiosk. The absent path
stops it, purges the package, unregisters it from sysdweb, drops the port 80 -> 9000
redirect and deletes its database and logs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 15:13:16 +02:00
0cb199c403 Point musicdolphin's kiosk at the front-end's Pi profile
?pi=1 is the low-power profile added in the musicmouse repo (web/src/lib/lowPower.ts):
the ambient canvas at a quarter of the pixels and 30fps, the progress bar re-rendering
ten times a second rather than sixty, and no backdrop-filter blur. The parameter is
the whole switch, so dropping it in the address bar compares the two on one machine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 13:16:01 +02:00
a2ed74093a Give the kiosk a window manager, and a debug mode to go with it
The role ran no window manager on the theory that Firefox is the only X client and
--kiosk makes it full-screen by itself. It doesn't: --kiosk *asks* to be full-screen
over EWMH, and with no WM running nobody answers. Firefox kept its default window
size in the top-left corner - on musicdolphin's 1920x1080 monitor, a 1280x972 window
with black bands down the right edge and along the bottom, which looks exactly like
an overscan problem and isn't one. Openbox answers the request and does nothing else.

pi_kiosk_mode=debug then turns the attached screen into something workable: Firefox
as an ordinary window that is not relaunched when you close it, a visible pointer,
Openbox as the session leader with a root menu offering a terminal, and xterm,
x11-utils and mesa-utils installed. musicdolphin is set to debug for now.

The templates notify the getty handler, so a mode switch restarts the session on its
own - Xorg lives in that unit's cgroup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 13:03:14 +02:00
b3f1ff69b1 Drop the forced display mode, and let the kiosk user own its own cache
The screen was a badly seated cable - the enclosure was fouling the connector,
so the DDC and hotplug pins never mated while the video pairs partly did. That
is why it read 0 bytes of EDID on both ports and why forcing a mode "fixed" it.
With the cable seated properly HDMI-A-1 reports connected, hands over 256 bytes
of EDID and identifies itself as a DELL P2419H, so the force has nothing left to
do. Remove it and let EDID decide, which also means the mode is no longer
hardcoded to something that happened to match.

Separately, and the reason the screen then showed "Your Firefox profile cannot
be loaded": Xorg runs as root under Debian's wrapper with HOME pointing at the
kiosk user's, so the first mesa shader cache write created ~/.cache owned by
root and mode 0700. Firefox could not create ~/.cache/mozilla inside it and
never got as far as writing a profile - .mozilla/firefox held only Crash Reports
and Pending Pings, with no profiles.ini. Create ~/.cache and ~/.mozilla up front
owned by the user, so root only ever adds subdirectories to a directory the user
already owns.

Keeping the getty@tty1 enable and the userconfig.service mask: those were real
faults, not workarounds for this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 12:26:51 +02:00
a973bc8d75 Make the kiosk autologin survive a reboot, and allow forcing a display mode
Two problems on musicdolphin, found after its first reboot.

getty@tty1 was never enabled. The role installed the autologin drop-in and the
handler restarted the unit, which works until the next boot - these images do
not enable getty@tty1 themselves, getty.target pulls in getty-static.service and
nothing else, so after a reboot nobody logs in and the kiosk never starts. Enable
it as well as starting it.

And the attached screen reads back 0 bytes of EDID on both micro-HDMI ports while
reporting "disconnected", so the kernel offers no modes, no framebuffer is
created, and X exits with "no screens found". That is the signature of a cable or
adapter whose DDC/HPD lines are broken but whose video pairs are fine. Forcing
the connector on is not enough on its own - with no EDID there is still no mode
to set - so allow a `video=` kernel parameter, which forces the output and names
the mode. hdmi_force_hotplug=1 is no use here; vc4-kms-v3d ignores it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 12:10:34 +02:00
5d853cbb8f Gate the whole of GEOlino Spezial behind the Welt 3 reward
It pointed at a 2020 episode, which locked the first handful and left the rest
of the show open - and the podcast retention cap has since deleted that episode
anyway, so the reward resolved to nothing at all.

An episode reward unlocks every episode up to and including the one named,
chronologically, so naming the newest covers the show: 356 of 356 episodes
locked until the lesson is passed, 0 after. Checked against the real library.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 10:24:21 +02:00
804cb1bfaf Give musicdolphin real sound through the HifiBerry amp
It was set up display-only, with alsa_device "simulate", so the UI showed a
working player that made no noise - but the Pi has a HifiBerry amp and
pi_alsasetup already put the softvol_effects slot on it. Point the backend at
it, the same way musicmouse does: a softvol control feeding dmix, so the card
stays shared with squeezelite and shairport instead of being locked.

Carry musicmouse's volume clamp over with it. Below roughly 23 the softvol
attenuation leaves nothing audible, and the ceiling keeps a UI slider from
putting the amp to full. Those numbers are tuned to musicmouse's speakers and
this is a different room, so they are a starting point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 21:53:50 +02:00
c8f19ed918 Only restart samba when its config changes
state: restarted is unconditional, so every run reported a change and bounced
smbd - which is the share the music is copied over, so a run during a transfer
would interrupt it. Start/enable it as a task, restart it from a handler the
config copy notifies.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 21:41:50 +02:00
1b5a5e2c1d Clean up the uv installs this role used to make
It managed them, it no longer needs them, and a stray uv on PATH is a confusing
thing to find when working out how the venv got built. Nothing else on these
hosts put them there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 21:29:18 +02:00
9bd23467f0 Build the venv from the distribution's Python instead of uv
The backend now targets 3.11, which is what Raspberry Pi OS ships, so there is
nothing left for uv to solve. It was only ever there to obtain a 3.13 the distro
does not have, and it brought its own problems: the download had to be matched
to the Pi's 32-bit userland by hand, and no armv7 wheel exists for a 3.13 ABI,
so Pillow was compiled from source and needed image headers installed alongside.

apt for the interpreter, python3 -m venv, pip for the rest - and piwheels then
supplies prebuilt armhf wheels for the native dependencies.

A venv cannot be migrated between interpreters (it holds absolute paths into the
one that made it), and `creates:` would keep the old one forever, so check what
built it and rebuild when it does not match the current python3. That is what
carries a device off the uv-installed 3.13 without hand-holding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 21:21:12 +02:00
6fcc345a26 Only rebuild the venv when the deployed commit changes
The previous fix made the install convergent but not idempotent: it keyed
"changed" off uv printing "Installed", and uv prints that every single run.
musicmouse is a local path dependency, so uv rebuilds and reinstalls it every
time regardless. Every ansible run therefore restarted the service and made it
rescan the whole library for nothing.

Decide from the device instead: the commit the checkout landed on, recorded in
the venv, plus whether the venv can import the package at all. That keeps the
convergence the handler lacked - an empty venv reinstalls even when the commit
matches - without touching anything on a run where nothing moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 21:13:00 +02:00
340eeb5433 Install the backend on every run, not on a checkout change
The role left musicdolphin with an empty venv and a service crash-looping on
ModuleNotFoundError while ansible reported failed=0, because installing the
backend hung off a handler notified by the git checkout:

  - the run where the checkout changed failed later on, so handlers never
    flushed and the notification was dropped;
  - the next run found the checkout already current, notified nothing, and
    installed nothing.

Nothing ever converges from there. Make it an ordinary task that runs every
time - uv is fast when there is nothing to do, and its output says whether it
actually installed anything, so a restart is still only notified on a real
change. Give the frontend the same treatment with a cheap stat, since the build
chain has the identical hole and a device with no dist serves no UI.

Also add libjpeg-dev and zlib1g-dev: Raspberry Pi OS has a 32-bit userland,
Pillow publishes no armv7 wheel, and uv therefore builds it from source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 20:58:05 +02:00
edca1e47dc Stop the delegated frontend build from sudoing on the control machine
pi_standard_setup's defaults set `ansible_become: true`. Role defaults are
play-wide host variables, and in Ansible's precedence the `ansible_become`
variable outranks the `become` keyword - so the `become: false` on every
pi_musicmouse task delegated to localhost was silently overridden and each one
tried to sudo on the machine running ansible:

    Premature end of stream waiting for become success or become password prompt

Those defaults earn their keep bootstrapping a fresh Pi, where you connect as
`pi` and become root to enable root login, so override the variable on the
delegated tasks rather than removing them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 20:46:07 +02:00
ca0271ba44 Disable the Raspberry Pi first-boot user wizard
Removing the SSH banner only hid the symptom. userconfig.service is a
Type=oneshot unit that runs

    whiptail --inputbox "Which user would you like to rename:" 20 60 pi

on tty8 and waits for an answer, which on a headless Pi never comes - it had
been sitting in "activating" for four and a half hours on musicdolphin. It is
WantedBy=multi-user.target, so that target never finished activating either, and
the boot job for it was still queued from boot. Anything ordered
After=multi-user.target then gets a start job that queues behind it and hangs
forever: musicmouse.service never started, and the ansible task that starts it
blocked until killed.

The pi user already exists on these images, so the wizard has nothing to do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 20:41:07 +02:00
331df52f0b Download the uv build the Pi can actually run
Raspberry Pi OS ships a 64-bit kernel with a 32-bit userland, so uname -m - and
therefore ansible_architecture - reports aarch64 on a box whose /bin/ls is
ELF32 ARM and which has no /lib/ld-linux-aarch64.so.1. The aarch64 uv tarball
unpacked happily and then failed three tasks later with

    /usr/local/bin/uv: No such file or directory

which is the dynamic loader missing, not the file, and points at entirely the
wrong thing. Pick the target triple from ansible_userspace_bits instead, which
is the fact that tells the truth, and key the install directory on the triple as
well as the version - otherwise `creates:` would keep a wrong-architecture
binary in place forever. Then run `uv --version` right after installing it, so a
bad download fails where the cause is visible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 19:59:43 +02:00
3557fe3f2b Skip the layout guard under --check
Under --check the git task reports "changed" without writing anything, so on a
host that has not been deployed yet there is no checkout to stat and the guard
fired on a perfectly fine configuration - making `just check mediapis.yml
musicdolphin` fail before the first real run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 19:10:44 +02:00
5763df90f8 Pin each MusicMouse host to a deploy branch, and freeze musicmouse
musicmouse_version pointed both hosts at rearchitect-backend, a branch that only
ever existed on a laptop - so the previous commit's rework could never actually
have run. Point each host at a deploy/<host> branch on the remote instead. Those
branches are the deployment record: what is installed on that box right now,
moved with a force-push when you mean to deploy.

musicmouse (Kinderzimmer) is the reason this matters. It is in daily use and
still runs the pre-rearchitecture backend out of /opt/musicmouse_venv and
espmusicmouse/host_driver/main.py. That commit has no python-backend/
pyproject.toml, no python-backend/musicmouse.service and no web/ at all, so this
role cannot install it - and a run would have force-checked-out incompatible
code over a working device first and failed afterwards. Set
mediapi_install_kidsmusic: false there and leave it alone;
deploy/musicmouse records the commit that is installed, for rollback. Add a
stat + fail right after the checkout so the same mistake stops with an
explanation instead of failing three tasks deeper.

Also in the role:

- Install the typing game's curriculum next to config.yml. A config with a
  tippen: section refuses to start without it - curriculum_file is validated as
  must-exist - so musicdolphin's new config needs it present.

- Stop clobbering config.yml. The app writes to it at runtime: parent mode
  patches the volume keys and the remote-control page rewrites the remote:
  block. A plain copy: undid that on every run. Install-once by default, with
  -e pi_musicmouse_force_config=true to push a change deliberately.

- Fill out config-musicdolphin.yml against the current schema: library root and
  cache, an absolute static_dir (../web/dist would resolve to /media/web/dist
  from /media/musicmouse and silently serve nothing), tippen, and the volume
  range for a screen-driven instance. ha: is left commented out until the
  Wohnzimmer-oben entity ids are picked - the ones in the dev config are
  Arbeitszimmer. No mqtt: (it would publish a device of dead entities on a
  simulate-only host) and no lirc:/remote: (this host runs irserver, a serial
  daemon, not the lircd this speaks TCP to).

The curriculum is a verbatim copy of app content, so exclude it from
ansible-lint rather than reformat it away from its source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 19:04:57 +02:00
9b1d01115b Deploy rearchitected MusicMouse backend + web frontend
The app grew a python-backend/ and a web/ frontend, so pi_musicmouse no
longer matches it. Rework the role:

- Install a pinned uv and build the venv with Python 3.13; Raspbian
  Bookworm's python3 is 3.11 and the backend uses PEP 695 syntax.
- pip install python-backend/ instead of a requirements.txt, and take
  the systemd unit straight from the checkout so it can't drift.
- Build web/ on the control machine (no npm on the Pi) and rsync
  web/dist over. Chain the checkout -> install -> build -> sync -> restart
  steps through handlers so a run with no repo change does nothing.
- Per-host config files (config-<host>.yml); the schema now differs
  between a host with a real mouse and a display-only one.
- Version to deploy is per host (musicmouse_version), no default.

Add pi_kiosk: autologin user running startx with Firefox in kiosk mode
and no window manager, for a Pi with a monitor attached. Enabled by
mediapi_has_monitor, defaulting off.

Turn musicdolphin into a display-only instance: serial and audio
simulated, driven entirely through the kiosk page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 17:58:51 +02:00
2443a75871 Keep /etc/hosts 127.0.1.1 in sync with the hostname
raspi-config's do_hostname rewrites the 127.0.1.1 line with
  sed "s/127\.0\.1\.1.*$CURRENT_HOSTNAME/127.0.1.1\t$NEW_HOSTNAME/"
which only matches if that line already holds the *old* hostname. Ours
still said "raspberrypi", so the substitution has been silently doing
nothing and the box could not resolve its own hostname.

Set the line explicitly instead, after the hostname is re-read, so a
rename and this update happen in the same run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 16:31:12 +02:00
ddda28be44 Remove dead host esszimmerradio
The Pi is gone; the name no longer resolves. It was still picked up by
the "hosts: all" playbooks (update-packages.yml, test_keepass.yml),
where it showed up as unreachable on every fleet run.

It was also the only host pointing pi_squeezelite_squeezeserver at
192.168.178.100 (musicdolphin) rather than the central server at .80,
so nothing depends on musicdolphin's LMS instance any more.

The "esszimmer" rows in roles/bluetooth_monitor/other/*.csv and the
analysis notebook are room labels in historical sensor training data,
not this host, and are left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 16:28:00 +02:00
9c66b7f665 Rename musikserverwohnzimmeroben -> musicdolphin
The Pi's OS hostname was changed first (raspi-config do_hostname +
reboot), since no mediapi sets ansible_host and Ansible resolves these
by hostname via mDNS/router DNS. Host now answers as musicdolphin.local
at 192.168.178.100.

The motd file must match the live OS hostname (pi_standard_setup reads
it off the device), the host_vars filename and the pi_musicmouse config
filename must match the inventory key.

sensor_room_name/sensor_room_name_ascii stay WohnzimmerOben - they name
the physical room for MQTT topics, not the host, and renaming them would
orphan the Home Assistant history.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 16:23:00 +02:00
07e3c2797e Fix pi_standard_setup under --check
Skipped command tasks return stdout: "" rather than being undefined, so
"src: motd/{{ ...stdout }}" collapsed to "motd/" - a directory - and copy
switched to recursive mode, failing with "/etc/motd/<file>: Not a
directory". The empty boot-target stdout likewise never equals
"multi-user.target", so "Set boot mode to CLI" always reported changed.

Run the three read-only commands in check mode. They are all
changed_when: false already, so this is safe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 16:22:50 +02:00
85a26db46b single playbook for mediapis 2026-09-10 21:33:19 +02:00
dae2470e67 use same shell setup as in nix home manager 2026-09-09 08:17:48 +02:00
3d1675528b Fix tasks that always report changed regardless of actual state
Several roles reported "changed" on every playbook run even when
nothing on the target had drifted, making real config drift
indistinguishable from noise:

- 7 systemd tasks across 6 roles used state:restarted, which always
  issues a restart and always reports changed. Switched to
  state:started plus notify-driven handlers that only restart when
  the underlying unit file, script, or config actually changes.
- pi_standard_setup's boot mode, timezone, and locale tasks shelled
  out to raspi-config with changed_when:true hardcoded. Boot mode now
  checks systemctl get-default first; timezone/locale now use the
  natively idempotent community.general.timezone/locale_gen modules.
- The pi account password task computed password_hash('sha512')
  without a seed, generating a new random salt (and thus an
  apparently different hash) on every run. Added a stable seed so the
  hash only changes when the underlying secret does.

Also renamed a mislabeled task in pi_squeezelite_custom and fixed a
typo in pi_standard_setup while those files were already touched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 21:28:36 +02:00
c49e4db304 Modern tools (rg, fd...) 2026-09-08 18:26:53 +02:00
f043606466 Add deliberate update playbook and unattended-upgrades role
Regular playbooks now use state: present, so they no longer upgrade
packages as a side effect. This adds two separate, explicit mechanisms
to keep the fleet patched instead:

- update-packages.yml: ad hoc / to-be-scheduled fleet-wide upgrade
  (safe by default, dist available via -e), plus
  update-packages-pinned-example.yml as a template for pinning or
  bumping a single package outside that.
- roles/unattended_upgrades: automatic security-only patching via
  unattended-upgrades, with a scheduled reboot window and mail
  left disabled pending a configured MTA. Applied to every host in
  full.yml and server.yml.

Also removes a leftover `upgrade: yes` apt task from pi_standard_setup
and server_basic_environment that was still doing a full upgrade on
every routine run, defeating the point of the state: present switch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 18:02:41 +02:00
37b75ecf81 Fix remaining ansible-lint violations: file permissions, var-naming, package pinning
- risky-file-permissions (32): add explicit mode: to copy/template/file tasks,
  matching the umask-derived permissions they already had (0644 for configs
  and systemd units, 0755 for created directories) — no functional change.
- var-naming (28): prefix role-scoped vars with their role name across
  pi_dhtsensor, pi_dhtsensor_circuitpython, pi_shairport, pi_squeezelite,
  pi_squeezelite_custom, pi_sispmctl, pi_standard_setup, and pi_sysdweb's
  sysdweb_name (shared by 9 consuming roles). Updated every dependent
  template, task reference, and matching inventory.yml override, and
  verified resolved values with ansible-inventory before/after.
- Fixes a latent bug found while renaming: pi_standard_setup's "Get/Change
  WiFi country" tasks reused the name wifi_country for both the role default
  and a register, so the register silently clobbered the default before
  do_wifi_country ever read it. Split into distinct names so the intended
  default value is used.
- package-latest (2): pin docker-ce/docker-compose-plugin installs in
  server_debian_docker to state: present.
- no-handler (1): move pi_lirc's "Reboot if boot overlay changed" into a
  proper handler notified by the boot-overlay task.

ansible-lint now passes clean at the production profile.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 17:28:12 +02:00
bb72eaec10 Untrack local .ansible runtime cache and fix stale gitignore path
The .ansible directory is a local ansible-galaxy/ansible-core cache
that got swept into the previous commit by git add -A. Also update
the pi-squeezeserver backup ignore path to match the role rename.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 17:13:39 +02:00
ab9763ec49 Fix ansible-lint violations: FQCN, formatting, bugs, role renames
- Auto-fix FQCN, YAML formatting, jinja spacing, and free-form module
  syntax via ansible-lint --fix
- Fix comments misplaced inside module args by the auto-fixer
  (bluetooth-monitor, pi_standard_setup, pi_musicmouse)
- Fix notify: references left stale (lowercase) after handler names
  were re-cased, which would have silently broken reboot/restart
  handlers (pi_disable_onboard_bluetooth, pi_hifiberry_amp,
  pi_squeezelite, pi_standard_setup)
- Fix a task in pis/debmatic-install.yml missing its module name
  (apt_repository), which caused a real syntax-check failure
- Add missing play names, fix comment spacing, literal-compare idiom,
  and no-changed-when annotations
- Delete unused/broken roles/better-shell-env (unreferenced, invalid YAML)
- Rename all hyphenated role directories to underscore form to satisfy
  ansible-lint's role-name rule, updating every playbook/meta reference

Remaining lint findings (var-naming, package-latest, risky-file-permissions,
no-handler) intentionally left for follow-up per user decision.
2026-09-08 17:13:00 +02:00
f79c106437 Automated repo cleanup with claude 2026-09-08 16:52:46 +02:00
5642424697 before cleanup 2026-09-08 16:32:02 +02:00
220 changed files with 3480 additions and 1729 deletions

7
.ansible-lint Normal file
View File

@@ -0,0 +1,7 @@
---
exclude_paths:
# A verbatim copy of the app repo's tippen-curriculum.yml. It is content the game
# reads, not ansible YAML, and reformatting it here to satisfy a line-length rule
# would make this copy diverge from the file it is copied from. Its schema is checked
# by the backend at startup, which is a stronger check than anything lint does.
- roles/pi_musicmouse/files/tippen-curriculum.yml

9
.gitignore vendored
View File

@@ -1,8 +1,5 @@
ve_*
/server/scripts/docker-images/tagspace/tagspaces
/server/scripts/docker-images/tagspace/*.zip
*.img
/music
__pycache__
/roles/pi-squeezeserver/backup
venv*
/roles/pi_squeezeserver/backup
.ansible/
venv*

80
README.md Normal file
View File

@@ -0,0 +1,80 @@
# ansible
Personal Ansible setup for provisioning and maintaining a fleet of Raspberry
Pis (audio players, sensors, music mouse, etc.) plus one home server.
## Layout
- `inventory.yml` — hosts and group vars (Pis under `iot`, plus `server`).
`group_vars/` and `host_vars/` hold vars for the `mediapis` group (the four
parallel audio-player Pis) and their host-specific overrides.
- `mediapis.yml`, `working.yml`, `server.yml`, `newrpi-provisioning.yml`,
`octopisetup.yml` — top-level playbooks. `mediapis.yml` is the canonical
playbook for the `mediapis` group (`musicdolphin`, `kitchenpi`,
`bedroompi`, `musicmouse`); the others are narrower/ad-hoc runs kept around
for specific hosts or one-off tasks.
- `roles/` — one role per piece of functionality (audio backends, sensors,
bluetooth monitoring, server basics, etc.). Each has a short `README.md`.
- `update-packages.yml` — deliberate, fleet-wide package update (see
"Keeping packages up to date" below). `update-packages-pinned-example.yml`
is a template for pinning or bumping a single package outside that.
- `lookup_plugins/keepass.py` — custom lookup plugin that fetches secrets
(device passwords, wifi passphrase) from a running KeePassXC instance via
its browser-integration protocol, instead of storing them in the repo.
- `pis/` — loose config files/scripts used when provisioning Pis by hand.
- `scripts/` — standalone helper scripts (Raspbian image creation, a network
logger) that aren't Ansible roles.
- `archive/` — retired setups kept for reference (not actively maintained).
## Setup
```
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
Secrets are pulled from KeePassXC at run time via the `keepass` lookup
plugin — see the header of `lookup_plugins/keepass.py` for how to enable
Browser Integration in KeePassXC. Freshly-flashed Raspberry Pis are reached
first with the OS-default `pi`/`raspberry` credentials (see
`roles/pi_standard_setup`), which the role then rotates to a KeePassXC-managed
password.
## Running a playbook
```
ansible-playbook mediapis.yml --limit <host>
```
`ansible.cfg` points Ansible at `inventory.yml` and `roles/` by default, so
no extra flags are needed for those.
## Keeping packages up to date
Regular playbook runs (`mediapis.yml`, `server.yml`, etc.) use `state: present`
for packages, so they only install what's missing — they never upgrade
anything as a side effect of an unrelated config change. Two separate,
deliberate mechanisms handle upgrades instead:
**Security patches — automatic.** The `unattended_upgrades` role (applied
to every host in `mediapis.yml`/`server.yml`) configures `unattended-upgrades`
to install security-origin updates automatically, with a scheduled reboot
window (default 03:00, see `roles/unattended_upgrades/defaults/main.yml`)
for patches that need one. Not scoped to full dist-upgrades.
**Everything else — deliberate, ad hoc.**
```
just update <host_or_group> # e.g. `just update kitchenpi` or `just update`
venv/bin/ansible-playbook update-packages.yml --limit <host> --check --diff # dry run first
```
Defaults to `upgrade: safe` (upgrades in place, never installs/removes
packages to resolve dependencies — see the comment header in
`update-packages.yml` for the tradeoff against `dist`). There is no cron/
schedule wired up for this yet; run it ad hoc when you want the fleet
updated, or add a crontab entry yourself (a starting point is documented in
`update-packages.yml`'s header).
To pin a package to an exact version, or deliberately bump one named
package to latest outside this schedule, copy the pattern in
`update-packages-pinned-example.yml`.

6
ansible.cfg Normal file
View File

@@ -0,0 +1,6 @@
[defaults]
inventory = inventory.yml
roles_path = roles
lookup_plugins = lookup_plugins
#strategy = free
serial = 5

View File

@@ -1,61 +0,0 @@
---
#- hosts: esszimmerradio
# roles:
# - pi-standard-setup
# - pi-squeezelite-custom
# - pi-shairport
# - pi-lirc
# - pi-sispmctl
#
- hosts: musikserverwohnzimmeroben
roles:
- pi-standard-setup
- pi-hifiberry-amp
- pi-squeezelite-custom
- pi-shairport
- pi-irserver
#- pi-dhtsensor
- pi-squeezeserver
- hosts: kitchenpi
roles:
- pi-standard-setup
- pi-hifiberry-amp
- pi-squeezelite-custom
- pi-shairport
- pi-lirc
- pi-dhtsensor
- pi-disable-onboard-bluetooth
- bluetooth-monitor
- hosts: bedroompi
roles:
- pi-standard-setup
- pi-squeezelite-custom
- pi-shairport
- pi-lirc
- pi-dhtsensor
- pi-disable-onboard-bluetooth
- bluetooth-monitor
- hosts: musicmouse
roles:
- pi-standard-setup
- pi-hifiberry-amp
- pi-musicmouse
- pi-squeezelite-custom
- pi-shairport
- pi-lirc
- bluetooth-monitor
#- hosts: octopi
# roles:
# - pi-dhtsensor
#- hosts: newrpi
# roles:
# - pi-standard-setup
# - pi-lirc

15
group_vars/mediapis.yml Normal file
View File

@@ -0,0 +1,15 @@
---
mediapi_has_hifiberry_amp: false
mediapi_ir_control: lirc
mediapi_has_dhtsensor: false
mediapi_disable_onboard_bluetooth: false
mediapi_install_bluetooth_monitor: false
mediapi_install_kidsmusic: false
mediapi_has_monitor: false
# No media pi runs its own Logitech Media Server. Every squeezelite in the fleet is
# pointed at pi_squeezelite_squeezeserver (the server, 192.168.178.80) by inventory.yml,
# so a server on a pi would have no clients. false here does not mean "skip the role" -
# it means "actively remove it", which is how the stray one on musicdolphin goes away
# and stays away. See roles/pi_squeezeserver/README.md.
mediapi_has_squeezeserver: false

13
host_vars/bedroompi.yml Normal file
View File

@@ -0,0 +1,13 @@
---
squeezelite_name: BedroomPi
pi_shairport_name: BedroomPi
alsa_card_name: Codec
sensor_room_name_ascii: schlafzimmer
sensor_room_name: Schlafzimmer
my_bt_monitor_watchdog_seconds: 600
my_btmonitor_restart_ble_interface: hci0
main_user: root
mediapi_has_dhtsensor: true
mediapi_disable_onboard_bluetooth: true
mediapi_install_bluetooth_monitor: true

13
host_vars/kitchenpi.yml Normal file
View File

@@ -0,0 +1,13 @@
---
squeezelite_name: KitchenPi
pi_shairport_name: KitchenPi
alsa_card_name: 0
sensor_room_name_ascii: kueche
sensor_room_name: Küche
hifiberry_overlay: hifiberry-amp
main_user: root
mediapi_has_hifiberry_amp: true
mediapi_has_dhtsensor: true
mediapi_disable_onboard_bluetooth: true
mediapi_install_bluetooth_monitor: true

View File

@@ -0,0 +1,26 @@
---
squeezelite_name: Wohnzimmer
pi_shairport_name: _Oben_Wohnzimmer
alsa_card_name: sndrpihifiberry
sensor_room_name_ascii: wohnzimmeroben
sensor_room_name: WohnzimmerOben
hifiberry_overlay: hifiberry-dacplus
main_user: root
mediapi_has_hifiberry_amp: true
mediapi_ir_control: irserver
mediapi_has_dhtsensor: true
# Display-only MusicMouse instance: no physical mouse attached, shown on the monitor
# via pi_kiosk instead. See roles/pi_musicmouse/files/config-musicdolphin.yml.
mediapi_install_kidsmusic: true
mediapi_has_monitor: true
# Temporarily a debuggable desktop rather than a locked-down kiosk: Openbox root menu,
# visible pointer, Firefox in a normal window. Set back to "kiosk" when done.
pi_kiosk_mode: debug
# ?pi=1 is the front-end's Raspberry Pi profile (web/src/lib/lowPower.ts): the ambient
# canvas at a quarter of the pixels and 30fps, the progress bar re-rendering ten times
# a second instead of sixty, and no backdrop blur. Drop the parameter in the address
# bar to compare against the full-fat version on the same machine.
pi_kiosk_url: "http://localhost:8080/?pi=1"
musicmouse_version: deploy/musicdolphin

25
host_vars/musicmouse.yml Normal file
View File

@@ -0,0 +1,25 @@
---
squeezelite_name: MusicMouse
pi_shairport_name: MusicMouse
alsa_card_name: sndrpihifiberry
hifiberry_overlay: hifiberry-dacplus
sensor_room_name: Kinderzimmer
sensor_room_name_ascii: kinderzimmer
main_user: root
mediapi_has_hifiberry_amp: true
mediapi_install_bluetooth_monitor: true
# The Kinderzimmer device still runs the pre-rearchitecture backend (deploy/musicmouse
# == b2c060f): /opt/musicmouse_venv and espmusicmouse/host_driver/main.py, installed by
# an older version of this role. The current pi_musicmouse only knows the new
# python-backend/ + web/ layout, so it is deliberately off here - a run would
# force-check-out incompatible code over a device the kids use every day, and would
# fail anyway (that commit has no python-backend/pyproject.toml, no
# python-backend/musicmouse.service and no web/ at all).
#
# deploy/musicmouse stays recorded below as the rollback marker for what is actually
# installed. Turning this back on means migrating the device at the same time: move
# /media/musicmouse/<figure>/ into Figuren/ and switch to the new config schema -
# files/config-musicmouse.yml is already written for that.
mediapi_install_kidsmusic: false
musicmouse_version: deploy/musicmouse

View File

@@ -1,67 +1,25 @@
---
all:
hosts:
server:
ansible_host: home.bauer.tech
ansible_port: 22187
server2:
children:
iot:
hosts:
octopi:
sensor_room_name_ascii: prusaprinter
sensor_room_name: prusaprinter
dht_pin: 26
main_user: root
bedroompi:
squeezelite_name: BedroomPi
shairport_name: BedroomPi
alsa_card_name: Codec
sensor_room_name_ascii: schlafzimmer
sensor_room_name: Schlafzimmer
my_bt_monitor_watchdog_seconds: 600
my_btmonitor_restart_ble_interface: hci0
main_user: root
kitchenpi:
squeezelite_name: KitchenPi
shairport_name: KitchenPi
alsa_card_name: 0
sensor_room_name_ascii: kueche
sensor_room_name: Küche
hifiberry_overlay: hifiberry-amp
main_user: root
esszimmerradio: # oben, eltern
squeezelite_name: Esszimmer
shairport_name: _Oben_Esszimmer
#alsa_card_name: Device
squeezeserver: 192.168.178.100
configure_wifi: true
alsa_card_name: 1
main_user: root
musikserverwohnzimmeroben: # oben, eltern
squeezelite_name: Wohnzimmer
shairport_name: _Oben_Wohnzimmer
alsa_card_name: 0
squeezeserver: 192.168.178.100
sensor_room_name_ascii: wohnzimmeroben
sensor_room_name: WohnzimmerOben
hifiberry_overlay: hifiberry-dacplus
main_user: root
musicmouse:
squeezelite_name: MusicMouse
shairport_name: MusicMouse
alsa_card_name: 1
hifiberry_overlay: hifiberry-dacplus
sensor_room_name: Kinderzimmer
sensor_room_name_ascii: kinderzimmer
pi_dhtsensor_dht_pin: 26
main_user: root
newrpi:
squeezelite_name: MyTestRaspberry
shairport_name: MyTestRaspberry
pi_shairport_name: MyTestRaspberry
alsa_card_name: 0
sensor_room_name_ascii: testraum
sensor_room_name: Test Raum
heatingpi:
heatingpi:
server:
sensor_room_name: Arbeitszimmer
sensor_room_name_ascii: arbeitszimmer
@@ -71,14 +29,23 @@ all:
homeassistant:
sensor_room_name: Anschlussraum
sensor_room_name_ascii: anschlussraum
children:
mediapis:
hosts:
musicdolphin:
kitchenpi:
bedroompi:
musicmouse:
vars:
ansible_user: root
ansible_python_interpreter: /usr/bin/python3
squeezeserver: 192.168.178.80
ansible_ssh_common_args: "-o ForwardAgent=yes"
pi_squeezelite_squeezeserver: 192.168.178.80
router_ip: 192.168.178.1
home_assistant_url: https://ha.bauer.tech
home_assistant_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkM2QxYjAwYjkxZjY0MWVhYjA4YmZhMDYwYTg3YjRhNyIsImlhdCI6MTcwNDI3MDU5MSwiZXhwIjoyMDE5NjMwNTkxfQ.dzvejgEQd9hf-Yftzd7NkR5pv76GaLFczeOy-a2pa1o
home_assistant_token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkM2QxYjAwYjkxZjY0MWVhYjA4YmZhMDYwYTg3YjRhNyIsImlhdCI6MTcwNDI3MDU5MSwiZXhwIjoyMDE5NjMwNTkxfQ.dzvejgEQd9hf-Yftzd7NkR5pv76GaLFczeOy-a2pa1o
configure_wifi: false
wifi_ssid: BauerWLAN
pi_standard_setup_wifi_ssid: BauerWLAN
my_btmonitor_mqtt_username: my_btmonitor
my_btmonitor_mqtt_password: 8aBIAC14jaKKbla

55
justfile Normal file
View File

@@ -0,0 +1,55 @@
venv_bin := "venv/bin"
# List available commands
default:
@just --list
# Create the Python virtualenv used to run ansible
venv:
python3 -m venv venv
{{venv_bin}}/pip install --upgrade pip
# Install Python deps (requirements.txt) and Galaxy collections (requirements.yml)
install:
{{venv_bin}}/pip install -r requirements.txt
{{venv_bin}}/ansible-galaxy collection install -r requirements.yml < /dev/null
# Ping all hosts, or a group/host, e.g. `just ping kitchenpi`
ping group="all":
{{venv_bin}}/ansible {{group}} -m ping
# Show the resolved inventory as a tree
inventory:
{{venv_bin}}/ansible-inventory --graph
# Syntax-check a playbook, e.g. `just syntax mediapis.yml`
syntax playbook:
{{venv_bin}}/ansible-playbook {{playbook}} --syntax-check
# Dry-run a playbook against a host/group without making changes
check playbook limit="all":
{{venv_bin}}/ansible-playbook {{playbook}} --limit {{limit}} --check --diff
# Run a playbook against a host/group, e.g. `just run mediapis.yml kitchenpi`
run playbook limit="all":
{{venv_bin}}/ansible-playbook {{playbook}} --limit {{limit}}
# Run mediapis.yml (pi roles) against one host/group, e.g. `just mediapis kitchenpi`
mediapis limit="all":
just run mediapis.yml {{limit}}
# Run server.yml against the server host
server:
just run server.yml server
# Run the deliberate fleet package-update playbook, e.g. `just update kitchenpi`
update limit="all":
just run update-packages.yml {{limit}}
# List installed Galaxy collections
collections:
{{venv_bin}}/ansible-galaxy collection list
# Lint playbooks and roles
lint:
{{venv_bin}}/ansible-lint

View File

@@ -1,63 +1,191 @@
# Copy this to ".ansible/plugins/lookup"
# Copy this file to ~/.ansible/plugins/lookup/keepass.py
#
# Prerequisites:
# pip install keepassxc-proxy-client
#
# KeePassXC setup:
# Tools > Settings > Browser Integration > Enable browser integration
# The plugin will prompt you to approve the association in KeePassXC on first run.
# Association credentials are saved to ~/.keepassxc_ansible_assoc (override with
# env var KEEPASSXC_ASSOC_FILE).
#
# Usage:
# {{ lookup('keepass', 'https://example.com', 'password') }}
# {{ lookup('keepass', 'https://example.com', 'username') }}
# {{ lookup('keepass', 'https://example.com') }} # defaults to password
#
# Terms:
# terms[0] URL (or title) to search for — matched against KeePassXC entries
# terms[1] Attribute to return: username / user / password / pass / passwd
# Defaults to 'password' if omitted.
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import json
import os
from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
from keepasshttplib import keepasshttplib, encrypter
import requests
DOCUMENTATION = """
DOCUMENTATION = r"""
lookup: keepass
author: Martin Bauer <bauer_martin@gmx.de>
version_added: '0.2'
short_description: fetch data from KeePass over KeePassHTTP
short_description: Fetch credentials from KeePassXC via the browser proxy protocol
description:
- This lookup returns a username or password queried by the URL of the keepass entry
- Connects to a running KeePassXC instance over its browser-integration Unix socket
(or Windows named pipe) and retrieves credentials for a given URL.
- On first use the plugin associates itself with KeePassXC; you will see an
approval dialog in KeePassXC. The association is saved to disk so subsequent
runs are silent.
options:
_terms:
description:
- first is the URL to search for
- second is a property name of the entry, e.g. username or password
required: True
description:
- First term is the URL (or entry title URL) to look up.
- Second term (optional) is the attribute to return.
Accepted values are username, user, password, pass, passwd.
Defaults to password.
required: true
notes:
- https://github.com/viczem/ansible-keepass
example:
- "{{ lookup('keepass', 'urlOfEntry', 'password') }}"
- KeePassXC must be running with Browser Integration enabled.
- Requires the Python package keepassxc-proxy-client.
"""
EXAMPLES = r"""
- name: Get password for an entry
debug:
msg: "{{ lookup('keepass', 'https://example.com') }}"
- name: Get username for an entry
debug:
msg: "{{ lookup('keepass', 'https://example.com', 'username') }}"
- name: Set pi account password from KeePassXC
user:
name: pi
password: "{{ lookup('keepass', 'default_rpi_password') | password_hash('sha512') }}"
"""
ASSOC_FILE_ENV = "KEEPASSXC_ASSOC_FILE"
ASSOC_FILE_DEFAULT = os.path.expanduser("~/.keepassxc_ansible_assoc")
ASSOC_NAME = "ansible-keepassxc"
USERNAME_ATTRS = {"username", "user"}
PASSWORD_ATTRS = {"password", "pass", "passwd"}
def _get_assoc_file():
return os.environ.get(ASSOC_FILE_ENV, ASSOC_FILE_DEFAULT)
def _load_assoc(connection):
"""Load a previously saved association from disk into *connection*. Returns True on success."""
path = _get_assoc_file()
if not os.path.exists(path):
return False
try:
with open(path, "r") as fh:
data = json.load(fh)
connection.load_associate(data["name"], bytes(data["public_key"]))
return True
except (KeyError, ValueError, OSError):
return False
def _save_assoc(connection):
"""Persist the current association to disk."""
path = _get_assoc_file()
assoc_id, public_key = connection.dump_associate()
data = {"name": assoc_id, "public_key": list(public_key)}
with open(path, "w") as fh:
json.dump(data, fh)
os.chmod(path, 0o600)
def _connect():
"""Return an authenticated, test-verified Connection to KeePassXC."""
try:
from keepassxc_proxy_client.protocol import Connection, ResponseUnsuccesfulException
except ImportError:
raise AnsibleError(
"keepassxc-proxy-client is not installed. "
"Run: pip install keepassxc-proxy-client"
)
conn = Connection()
try:
conn.connect()
except Exception as exc:
raise AnsibleError(
"Cannot connect to KeePassXC. Is it running with Browser Integration enabled? "
"Error: {}".format(exc)
)
conn.change_public_keys()
# Try to reuse an existing association
if _load_assoc(conn):
try:
conn.test_associate(trigger_unlock=True)
return conn # existing association is still valid
except Exception:
pass # fall through and re-associate
# First run (or stale association) — ask KeePassXC to authorise us
try:
conn.associate()
except Exception as exc:
raise AnsibleError(
"KeePassXC association failed. "
"Please approve the request in the KeePassXC dialog. "
"Error: {}".format(exc)
)
try:
conn.test_associate(trigger_unlock=True)
except Exception as exc:
raise AnsibleError("KeePassXC association test failed: {}".format(exc))
_save_assoc(conn)
return conn
class LookupModule(LookupBase):
def __init__(self, *args, **kwargs):
super(LookupModule, self).__init__(*args, **kwargs)
self.k = keepasshttplib.Keepasshttplib()
def run(self, terms, variables=None, **kwargs):
if not terms or len(terms) > 2:
raise AnsibleError('Keepass wrong request format')
if len(terms) == 1:
entry_path, entry_attr = terms[0], 'password'
else:
entry_path, entry_attr = terms[0], terms[1]
#if not self._test_connection():
# raise AnsibleError('Keepass is closed!')
try:
auth = self.k.get_credentials(entry_path)
except Exception as e:
raise AnsibleError('Keepass error obtaining entry {}: {}'.format(entry_path, e))
if auth:
if entry_attr not in ('username', 'user', 'pass', 'passwd', 'password'):
raise AnsibleError("Keepass wrong entry")
ret = auth[0] if entry_attr.startswith('user') else auth[1]
return [ret]
raise AnsibleError(
"keepass lookup expects 1 or 2 arguments: "
"lookup('keepass', '<url>', '<attribute>')"
)
url = terms[0]
attr = terms[1].lower() if len(terms) == 2 else "password"
if attr not in USERNAME_ATTRS | PASSWORD_ATTRS:
raise AnsibleError(
"keepass: unsupported attribute '{}'. "
"Use one of: username, user, password, pass, passwd".format(attr)
)
conn = _connect()
def _test_connection(self):
key = self.k.get_key_from_keyring()
if key is None:
key = encrypter.generate_key()
id_ = self.k.get_id_from_keyring()
try:
return self.k.test_associate(key, id_)
except requests.exceptions.ConnectionError as e:
raise AnsibleError('Keepass Connection Error: {}'.format(e))
entries = conn.get_logins(url)
except Exception as exc:
raise AnsibleError(
"keepass: failed to retrieve logins for '{}': {}".format(url, exc)
)
if not entries:
raise AnsibleError(
"keepass: no entries found in KeePassXC for URL '{}'".format(url)
)
# Return the first matching entry (KeePassXC already filters by URL)
entry = entries[0]
if attr in USERNAME_ATTRS:
return [entry.get("login", "")]
else:
return [entry.get("password", "")]

28
mediapis.yml Normal file
View File

@@ -0,0 +1,28 @@
---
- name: Media Pis (squeezelite/shairport audio players)
hosts: mediapis
roles:
- unattended_upgrades
- pi_modern_shell_env
- pi_standard_setup
- role: pi_hifiberry_amp
when: mediapi_has_hifiberry_amp
- role: pi_musicmouse
when: mediapi_install_kidsmusic
- role: pi_kiosk
when: mediapi_has_monitor
- pi_squeezelite_custom
- role: pi_squeezeserver
vars:
pi_squeezeserver_state: "{{ 'present' if mediapi_has_squeezeserver else 'absent' }}"
- pi_shairport
- role: pi_irserver
when: mediapi_ir_control == 'irserver'
- role: pi_lirc
when: mediapi_ir_control == 'lirc'
- role: pi_dhtsensor
when: mediapi_has_dhtsensor
- role: pi_disable_onboard_bluetooth
when: mediapi_disable_onboard_bluetooth
- role: bluetooth_monitor
when: mediapi_install_bluetooth_monitor

View File

@@ -1,8 +1,9 @@
---
- hosts: musicmouse
- name: Musicmouse
hosts: musicmouse
roles:
- pi-standard-setup
- pi-hifiberry-amp
- pi-squeezelite
- pi-shairport
- pi-lirc
- pi_standard_setup
- pi_hifiberry_amp
- pi_squeezelite
- pi_shairport
- pi_lirc

View File

@@ -1,3 +1,5 @@
- hosts: octopi
---
- name: Octopi
hosts: octopi
roles:
- pi-dhtsensor
- pi_dhtsensor

View File

@@ -1,18 +1,19 @@
---
- hosts: kitchenpi
- name: Install Debmatic
hosts: kitchenpi
tasks:
- name: Add Key for Debmatic Repository
apt_key:
ansible.builtin.apt_key:
url: https://www.debmatic.de/debmatic/public.key
state: present
- name: Add Debmatic Repository
repo: deb https://www.debmatic.de/debmatic stable main
state: present
filename: debmatic
ansible.builtin.apt_repository:
repo: deb https://www.debmatic.de/debmatic stable main
state: present
filename: debmatic
- name: Install Pre-requesites
apt:
update_cache: yes
ansible.builtin.apt:
update_cache: true
name:
- build-essential
- bison
@@ -21,8 +22,8 @@
- raspberrypi-kernel-headers
- pivccu-modules-dkms
- name: Reboot
reboot:
ansible.builtin.reboot:
- name: Install Debmatic package
apt:
update_cache: no
ansible.builtin.apt:
update_cache: false
name: debmatic

7
requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
# Python dependencies for the Ansible *control node* (the machine running
# ansible-playbook). Dependencies needed only on managed hosts (Pis/server)
# are installed by the relevant roles themselves via apt/pip tasks.
ansible-core>=2.14
ansible-lint
keepassxc-proxy-client
passlib

4
requirements.yml Normal file
View File

@@ -0,0 +1,4 @@
---
collections:
- name: ansible.posix
- name: community.general

View File

@@ -1,3 +0,0 @@
function dot -w git -d "Manages dotfiles"
git --git-dir=$HOME/.dot --work-tree=$HOME $argv
end

View File

@@ -1,47 +0,0 @@
---
#
- name: Install packages
apt:
name:
- bat
- broot
- duf
- fd-find
- fish
- fzf
- git
- glances
- lsd
- neovim
- ripgrep
- tmux
- zoxide
# TODO: Dust, btm
block:
become: true
become_user: {{main_user}}
- name: Create bin folder
ansible.builtin.file:
path: ~/bin
state: directory
mode: '0755'
# Oh-my-fish
- name: get oh-my-fish repo
git:
repo: 'https://github.com/oh-my-fish/oh-my-fish.git'
dest: ~/.local/share/git/oh-my-fish
- name: install oh-my-fish
shell:
cmd: "bin/install --offline --noninteractive"
executable: /usr/bin/fish
chdir: ~/.local/share/git/oh-my-fish
creates:
- ~/.local/share/omf
- ~/.config/omf
- copy:
content: "bobthefish"
dst: "~/.config/omf/theme"

View File

@@ -1,22 +0,0 @@
- name: Apt install bluez, firmware and Python requirements
apt:
name:
- bluez
- bluez-firmware
- firmware-realtek
- firmware-realtek-rtl8723cs-bt
- python3-pycryptodome
- python3-bleak
- python3-asyncio-mqtt
- python3-numpy
- name: Copy monitor script
template: src=my_btmonitor.py dest=/usr/bin/my_btmonitor owner=root mode=u+rwx
- name: Install systemd service file
copy: src=my_btmonitor.service dest=/etc/systemd/system/
- name: Add script to autostart and start now
systemd: name=my_btmonitor state=restarted enabled=yes daemon_reload=yes
#- name: Add to sysdweb
# include_role:
# name: pi-sysdweb
# vars:
# sysdweb_name: my_btmonitor

View File

@@ -0,0 +1,12 @@
# bluetooth_monitor
Installs `my_btmonitor.py` (BLE scanning via `bleak`) as a systemd service
that watches for nearby devices, publishes state over MQTT, and can restart
the local BLE interface on a watchdog timeout.
**Key vars:** `my_bt_monitor_watchdog_seconds`, `my_btmonitor_restart_ble_interface`,
`my_btmonitor_mqtt_username`, `my_btmonitor_mqtt_password`
`other/` is not part of the deployed role — it's a separate, standalone
data-analysis project (Jupyter notebook, Dockerfile, collected CSV data) used
to analyze data captured by the monitor.

View File

@@ -0,0 +1,5 @@
---
- name: Restart my_btmonitor
ansible.builtin.service:
name: my_btmonitor
state: restarted

View File

@@ -0,0 +1,36 @@
---
- name: Apt install bluez, firmware and Python requirements
ansible.builtin.apt:
name:
- bluez
- bluez-firmware
- firmware-realtek
- firmware-realtek-rtl8723cs-bt
- python3-pycryptodome
- python3-bleak
- python3-asyncio-mqtt
- python3-numpy
- name: Copy monitor script
ansible.builtin.template:
src: my_btmonitor.py
dest: /usr/bin/my_btmonitor
owner: root
mode: u+rwx
notify: Restart my_btmonitor
- name: Install systemd service file
ansible.builtin.copy:
src: my_btmonitor.service
dest: /etc/systemd/system/
mode: "0644"
notify: Restart my_btmonitor
- name: Add script to autostart and start now
ansible.builtin.systemd:
name: my_btmonitor
state: started
enabled: "yes"
daemon_reload: "yes"
# - name: Add to sysdweb
# include_role:
# name: pi_sysdweb
# vars:
# pi_sysdweb_name: my_btmonitor

View File

@@ -10,6 +10,7 @@ import json
from datetime import datetime
import os
import time
import subprocess
# ------------------- Config ----------------------------------------------------------------
@@ -78,6 +79,12 @@ async def ble_scan():
await stop_event.wait()
except Exception as e:
print("Error", e)
try:
subprocess.run(["hciconfig", "hci0", "reset"], check=True)
except Exception as reset_err:
print(f"Reset failed: {reset_err}")
await asyncio.sleep(3)
print("Starting again")
@@ -94,4 +101,4 @@ if __name__ == "__main__":
os.system(f"hciconfig {restart_interface} up")
time.sleep(3)
print("Done")
asyncio.run(main())
asyncio.run(main())

View File

@@ -1,3 +0,0 @@
---
- name: Sync alsa config
template: src=asound.conf dest=/etc/asound.conf

View File

@@ -1,3 +0,0 @@
---
dht_pin: "D12"
dht_polling_sleep_time_seconds: 20

View File

@@ -1,20 +0,0 @@
---
- name: apt install libgpiod2
apt: name=libgpiod2 cache_valid_time=7200 state=present
- name: pip install adafruit-circuitpython-dht
pip:
name: adafruit-circuitpython-dht
executable: pip3
- name: Install script config
template: src=dht22_sensing.json dest=/etc/dht22_sensing.json
- name: Install script
copy: src=dht22_sensing.py dest=/usr/bin/dht22_sensing owner=root mode=u+rwx
- name: Install systemd service file
copy: src=dht22_sensing.service dest=/lib/systemd/system/
- name: Add script to autostart and start now
systemd: name=dht22_sensing state=started enabled=yes daemon_reload=yes
- name: Add to sysdweb
include_role:
name: pi-sysdweb
vars:
sysdweb_name: dht22_sensing

View File

@@ -1,3 +0,0 @@
---
dht_pin: 12
dht_polling_sleep_time_seconds: 20

View File

@@ -1,31 +0,0 @@
---
# Use the deprecated version here instead, not the new circuitpython adafruit-circuitpython-dht version
# the new version needs a lot of CPU time and doesn't work correctly on old raspi 1
# a copy of the deprecated repo is downloaded as zip (if it goes away)
- name: Uninstall libgpiod2 (circuitpython) if present
apt: name=libgpiod2 cache_valid_time=7200 state=absent
- name: pip uninstall adafruit-circuitpython-dht
pip:
name: adafruit-circuitpython-dht
executable: pip3
state: absent
extra_args: "--break-system-packages"
- name: pip install adafruit-dht
pip:
name: adafruit-dht
executable: pip3
extra_args: "--break-system-packages"
- name: Install script config
template: src=dht22_sensing.json dest=/etc/dht22_sensing.json
- name: Install script
copy: src=dht22_sensing.py dest=/usr/bin/dht22_sensing owner=root mode=u+rwx
- name: Install systemd service file
copy: src=dht22_sensing.service dest=/etc/systemd/system/
- name: Add script to autostart and start now
systemd: name=dht22_sensing state=restarted enabled=yes daemon_reload=yes
- name: Add to sysdweb
include_role:
name: pi-sysdweb
vars:
sysdweb_name: dht22_sensing

View File

@@ -1,4 +0,0 @@
---
- name: reboot
reboot:

View File

@@ -1,4 +0,0 @@
---
- name: reboot
reboot:

View File

@@ -1,3 +0,0 @@
---
dependencies:
- role: pi-alsasetup

View File

@@ -1,16 +0,0 @@
---
- name: Copy irserver
copy: src=irserver dest=/usr/bin/irserver mode=u+rx
- name: Make config dir for remotes
file: path=/usr/bin/remotes state=directory
- name: Copy hauppauge remote
copy: src=hauppauge.rem dest=/usr/bin/remotes/
- name: Copy irserver systemd file
copy: src=irserver.service dest=/lib/systemd/system/
- name: Enable irserver autostart
systemd: name=irserver state=restarted enabled=yes daemon_reload=yes
- name: Add irserver to sysdweb
include_role:
name: pi-sysdweb
vars:
sysdweb_name: irserver

View File

@@ -1,26 +0,0 @@
---
- name: Install lirc
apt:
name: lirc
- name: Install config file lirc_options.conf
copy: src=lirc_options.conf dest=/etc/lirc/lirc_options.conf
- name: Install config file lircd.conf
copy: src=lircd.conf dest=/etc/lirc/lircd.conf
- name: Install remote file
copy: src=hauppauge.conf dest=/etc/lirc/hauppauge.conf
- name: Activate overlay in boot config
lineinfile:
path: /boot/firmware/config.txt
regexp: "^#?dtoverlay=gpio-ir"
line: "dtoverlay=gpio-ir,gpio_pin=17"
register: boot_overlay
- name: Restart lircd
systemd: name=lircd state=started enabled=yes daemon_reload=yes
- name: Reboot if boot overlay changed
reboot:
when: boot_overlay.changed
- name: Add to sysdweb
include_role:
name: pi-sysdweb
vars:
sysdweb_name: lircd

View File

@@ -1,40 +0,0 @@
general:
alsa_device: softvol_effects
mqtt:
user: musicmouse
password: KNLEFLZF94yA6Zhj141
server: homeassistant
hass_url: https://ha.bauer.tech
hass_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI4N2ExMzM2ZmQ4ZWQ0ZDgzOWZhMjU3NmZjYTg1NWQ1ZiIsImlhdCI6MTcwNDAyOTUyOSwiZXhwIjoyMDE5Mzg5NTI5fQ.vx9L5bTSey98uc8TwodShaEXSMr-hjXugPsNviR_fEw"
button_leds_brightness: 0.5
volume_increment: 5
min_volume: 23
max_volume: 70
serial_port: "/dev/ttyUSB0"
figures:
elefant:
id: "88041174e9"
colors: ["#ffff00", "#00c8ff", "#094b46", "#c20099"]
fuchs:
id: "8804ce7230"
colors: ["#F4D35E", "#F95738", "#F95738", "#083d77"]
eule:
id: "88040d71f0"
colors: ["#e5a200", "#f8e300", "w33", "w99"]
omnom:
id: "88043c6ede"
colors: ["#005102", "#fec800", "#005102", "#3bc405"]
eichhoernchen:
id: "88040b78ff"
colors: ["#ff0ada", "#4BC6B9", "#69045a", "#4BC6B9"]
hund:
id: "8804bc7444"
colors: ["#ffff00", "#00c8ff", "#094b46", "#c20099"]
hase:
id: "88044670ba"
colors: ["#ffff00", "#00c8ff", "#094b46", "#c20099"]
schneemann:
id: "88043f71c2"
colors: ["#ff0ada", "#4BC6B9", "#69045a", "#4BC6B9"]

View File

@@ -1,11 +0,0 @@
[Unit]
Description=MusicMouse Player
After=network.target
[Service]
Type=simple
Restart=always
ExecStart=/opt/musicmouse_venv/bin/python /opt/musicmouse/espmusicmouse/host_driver/main.py /media/musicmouse/
[Install]
WantedBy=multi-user.target

View File

@@ -1,38 +0,0 @@
---
- name: Packages
apt:
name:
- python3
- python3-pip
- python3-vlc # also in venv - but this installs vlc + deps
- samba
- name: Checkout Musicmouse repo
ansible.builtin.git:
repo: 'ssh://git@git.bauer.tech:2222/martin/musicmouse.git'
dest: '/opt/musicmouse'
version: 'release/1.1'
accept_hostkey: true
- name: Create and update virtual env
ansible.builtin.pip:
requirements: /opt/musicmouse/espmusicmouse/host_driver/requirements.txt
virtualenv: /opt/musicmouse_venv
virtualenv_command: "/usr/bin/python3 -m venv"
- name: Create media directory
file:
path: /media/musicmouse
state: directory
- name: Install config file
copy: src=config.yml dest=/media/musicmouse/config.yml
- name: Install systemd service file
copy: src=musicmouse.service dest=/etc/systemd/system/
- name: Add script to autostart and start now
systemd: name=musicmouse state=restarted enabled=yes daemon_reload=yes
- name: Samba setup
copy: src=smb.conf dest=/etc/samba/
- name: Restart samba
systemd: name=smbd state=restarted enabled=yes
# manual steps:
# - set samba passwords with smbpasswd
# - copy music into /media/musicmouse (or via samba share)
# - upload host driver?
# - manual patching of hassclient necessary :( loop has to be passed in, to not use running_event_loop

View File

@@ -1,3 +0,0 @@
---
shairport_sync_version: "3.3.5"
shairport_name: Unnamed Raspberry with shairport

View File

@@ -1,3 +0,0 @@
---
dependencies:
- role: pi-alsasetup

View File

@@ -1,21 +0,0 @@
---
- name: Check if sispmctl already exists
stat: path=/usr/bin/sispmctl
register: sispmctl_file
- name: Install dependencies
apt: name="libusb-dev" cache_valid_time=7200 state=present
- name: Copy sispmctl sources
unarchive: src=sispmctl-4.7.tar.gz dest=/tmp
when: sispmctl_file.stat.exists == false
- name: Build and install
shell: cd /tmp/sispmctl*/ && ./configure --prefix=/usr && make install
when: sispmctl_file.stat.exists == false
- name: Install systemd service file
copy: src=sispmctl.service dest=/lib/systemd/system/
- name: Add script to autostart and start now
systemd: name=sispmctl state=started enabled=yes daemon_reload=yes
- name: Add to sysdweb
include_role:
name: pi-sysdweb
vars:
sysdweb_name: sispmctl

View File

@@ -1,31 +0,0 @@
---
- name: Uninstall system package of squeezelite
apt: name=squeezelite state=absent
- name: Install dependencies
apt:
name:
- libmad0
- libmpg123-0
- libflac12
- libvorbisfile3
- libfaad2
# for building libssl-dev, libasound2-dev, libflac-dev, libvorbis-dev, libsoxr-dev, libfaad-dev, libmad0-dev, libmpg123-dev
state: present
cache_valid_time: 7200
- name: Remove old config file if present
file: path=/etc/default/squeezelite state=absent
- name: Copy over custom compile version of squeezelite
copy: src=squeezelite dest=/opt/squeezelite mode=700
- name: Install systemd service file
template: src=squeezelite.service dest=/lib/systemd/system/
- name: Enable sysdweb autostart
systemd: name=squeezelite state=restarted enabled=yes daemon_reload=yes
- name: Add to sysdweb
include_role:
name: pi-sysdweb
vars:
sysdweb_name: squeezelite
# build with adapted Makefile.rpi:
#OPTS = -DRESAMPLE -DDSD -DUSE_SSL -DLINKALL -I./include -I./include/opus -I./include/alac -I/usr/local/include -s -mfpu=vfp -mfloat-abi=hard -march=armv6zk -mtune=arm1176jzf-s

View File

@@ -1,2 +0,0 @@
---
squeezeserver: 192.168.178.80

View File

@@ -1,3 +0,0 @@
---
- name: restart-squeezelite
systemd: name=squeezelite state=restarted enabled=yes daemon_reload=yes

View File

@@ -1,3 +0,0 @@
---
dependencies:
- role: pi-alsasetup

View File

@@ -1,13 +0,0 @@
---
- name: Apt install squeezelite package
apt: name=squeezelite cache_valid_time=7200 state=present
notify: restart-squeezelite
- name: Install config file
template: src=squeezelite.cfg dest=/etc/default/squeezelite
notify: restart-squeezelite
- name: Add to sysdweb
include_role:
name: pi-sysdweb
vars:
sysdweb_name: squeezelite

View File

@@ -1,13 +0,0 @@
---
wifi_ssid: "" # put SSID here to configure wifi
ansible_user: "root" # "User to connect with, put in 'pi' here if you connect the first time, else leave empty"
new_hostname: "" # set this to change the hostname
timezone: "Europe/Berlin"
wifi_country: "DE"
wifi_pass_url: "bauer_wifi" # has to be in keepass with url "wifi_pass_url"
ansible_ssh_pass: "raspberry"
ansible_become_password: "raspberry"
ansible_become: yes

View File

@@ -1,12 +0,0 @@
EsszimmerRadio Eltern
________
/______ |
| | | _
| ===== | | | |
| ===== | | o o
| | | |~
| .-. | | o o o
| ' . ' | | |~ |_|
..'| '._.' | | o
.' |_______|/

View File

@@ -1,10 +0,0 @@
Musik Server Wohnzimmer oben
|~~~~~~~~~~~~~~~|
|~~~~~~~~~~~~~~~|
| |
/~~\| /~~\|
\__/ \__/

View File

@@ -1,7 +0,0 @@
#!/bin/bash
echo none > /sys/class/leds/led0/trigger
echo none > /sys/class/leds/led1/trigger
echo 0 >/sys/class/leds/led0/brightness
echo 0 >/sys/class/leds/led1/brightness

View File

@@ -1,8 +0,0 @@
---
- name: restart sshd
service:
name: sshd
state: restarted
- name: reboot
reboot:

View File

@@ -1,102 +0,0 @@
---
- name: Do apt update/upgrade
apt: upgrade=yes update_cache=yes cache_valid_time=7200
- name: Detect Raspi Model
slurp: src=/sys/firmware/devicetree/base/model
register: raspberry_model
- name: Show Raspi Model
debug: msg={{ raspberry_model.content | b64decode }}
- name: Add authorized SSH key to root account
authorized_key:
user: root
key: "{{ lookup('file', 'sshkey.pub') }}"
state: present
- name: Activate root login with key
lineinfile:
path: /etc/ssh/sshd_config
regexp: "^#?PermitRootLogin"
line: "PermitRootLogin prohibit-password"
notify: restart sshd
- name: Deactive SSH accepting locale vars (leads to warnings)
lineinfile:
path: /etc/ssh/sshd_config
regexp: "^#?AcceptEnv LANG LC_*"
line: "#AcceptEnv LANG LC_*"
notify: restart sshd
- name: Get hostname
command: "raspi-config nonint get_hostname"
register: pi_hostname
changed_when: False
- name: Change hostname {{ new_hostname }}
command: "raspi-config nonint do_hostname {{ new_hostname }}"
when: new_hostname | bool and pi_hostname.stdout != new_hostname
register: set_hostname
notify: reboot
- name: Get hostname
command: "raspi-config nonint get_hostname"
register: pi_hostname
changed_when: False
- name: set boot mode to CLI
command: "raspi-config nonint do_boot_behaviour B1"
#I2 Change Timezone
- name: Change timezone
command: "raspi-config nonint do_change_timezone {{ timezone }}"
- name: Change locale
command: "raspi-config nonint do_change_locale en_US.UTF-8"
- name: Change password of default pi account
user:
name: pi
update_password: always
password: "{{ lookup('keepass', 'default_rpi_password') | password_hash('sha512') }}"
- name: Install Packages (vim, git, basic python stuff)
apt:
name:
- vim
- git
- python3
- python3-pip
- python3-wheel
- telnet
cache_valid_time: 7200
state: present
- name: Copy vim config
copy: src=vimrc dest=/root/.vimrc
- name: Copy git config
copy: src=gitconfig dest=/root/.gitconfig
# Wifi
- name: Get WiFi country
command: "raspi-config nonint get_wifi_country"
register: wifi_country
changed_when: False
ignore_errors: yes #to avoid error when WiFi is not present
- name: Change WiFi country
command: "raspi-config nonint do_wifi_country {{ wifi_country }}"
when: configure_wifi
- name: Set WiFi credentials
command: "raspi-config nonint do_wifi_ssid_passphrase {{ wifi_ssid }} {{ lookup('keepass', 'bauer_wifi') }}"
when: configure_wifi
- name: Install watchdog
apt: name=watchdog cache_valid_time=7200 state=present
when: not wifi_ssid is defined
- name: Configure watchdog
blockinfile:
path: /etc/watchdog.conf
block: |
interface = wlan0
retry-timeout = 90
ping = {{router_ip}}
interval = 15
when: configure_wifi
- name: Start watchdog
systemd: name=watchdog state=started enabled=yes daemon_reload=yes # state=restarted not working, also not manually
when: configure_wifi
# Message of the day
- name: Set Message of the day
copy: src=motd/{{ pi_hostname.stdout }} dest=/etc/motd
# LED off script
- name: Copy led off script
copy: src=raspi-leds-off.sh dest=/usr/sbin/raspi-leds-off.sh mode="u+rwx"
- name: Copy led off service
copy: src=raspi-leds-off.service dest=/lib/systemd/system/
- name: Activate led off servic
systemd: name=raspi-leds-off state=restarted enabled=yes daemon_reload=yes

View File

@@ -1,39 +0,0 @@
---
- name: Apt install python3-pip
apt:
name: python3-pip
cache_valid_time: 7200
state: present
- name: Install sysdweb
pip:
name: sysdweb
executable: pip3
extra_args: "--break-system-packages"
- name: sysdweb user
user:
name: sysdweb
shell: /usr/bin/nologin
password: "$6$TcTD23xOXln$RxN3Kd0vJRaxffoyKqjoBJM0Q5Va6REBVZ6BOgmGXs3fTAWc7voSW5QcN35t9pfro2do0LeSaeGsrMLbArZ.2."
update_password: always
- name: Configure sysdweb user
blockinfile:
path: /etc/sysdweb.conf
create: true
marker: "# {mark} ansible user"
block : |
[DEFAULT]
users = sysdweb
- name: Configure sysdweb
blockinfile:
path: /etc/sysdweb.conf
create: true
marker: "# {mark} ansible managed for {{sysdweb_name}}"
block: |
[{{sysdweb_name}}]
title = {{sysdweb_name}}
unit = {{sysdweb_name}}.service
- name: Install systemd service file
copy: src=sysdweb-system.service dest=/etc/systemd/system/
- name: Enable sysdweb autostart
systemd: name=sysdweb-system state=restarted enabled=yes daemon_reload=yes

View File

@@ -0,0 +1,3 @@
# pi_alsasetup
Syncs the ALSA configuration file to the host.

View File

@@ -0,0 +1,6 @@
---
- name: Sync alsa config
ansible.builtin.template:
src: asound.conf
dest: /etc/asound.conf
mode: "0644"

View File

@@ -0,0 +1,7 @@
# pi_dhtsensor
Installs the DHT22 temperature/humidity sensor polling script based on the
legacy `Adafruit_DHT` library (removing the circuitpython variant/
`libgpiod2` if present — mutually exclusive with `pi_dhtsensor_circuitpython`).
**Key vars:** `pi_dhtsensor_dht_pin`, `pi_dhtsensor_dht_polling_sleep_time_seconds`

View File

@@ -0,0 +1,3 @@
---
pi_dhtsensor_dht_pin: 12
pi_dhtsensor_dht_polling_sleep_time_seconds: 20

View File

@@ -0,0 +1,5 @@
---
- name: Restart dht22_sensing
ansible.builtin.service:
name: dht22_sensing
state: restarted

View File

@@ -0,0 +1,55 @@
---
# Use the deprecated version here instead, not the new circuitpython adafruit-circuitpython-dht version
# the new version needs a lot of CPU time and doesn't work correctly on old raspi 1
# a copy of the deprecated repo is downloaded as zip (if it goes away)
- name: Uninstall libgpiod2 (circuitpython) if present
ansible.builtin.apt:
name: libgpiod2
cache_valid_time: "7200"
state: absent
- name: Pip uninstall adafruit-circuitpython-dht
ansible.builtin.pip:
name: adafruit-circuitpython-dht
executable: pip3
state: absent
extra_args: "--break-system-packages"
- name: Pip install adafruit-dht
ansible.builtin.pip:
name: adafruit-dht
executable: pip3
# Modern Raspberry Pi OS kernels no longer expose a "Hardware" line in
# /proc/cpuinfo, which breaks this package's built-in Pi auto-detection.
# Force it explicitly since we only ever target Raspberry Pis.
extra_args: '--break-system-packages --global-option="--force-pi"'
notify: Restart dht22_sensing
- name: Install script config
ansible.builtin.template:
src: dht22_sensing.json
dest: /etc/dht22_sensing.json
mode: "0644"
notify: Restart dht22_sensing
- name: Install script
ansible.builtin.copy:
src: dht22_sensing.py
dest: /usr/bin/dht22_sensing
owner: root
mode: u+rwx
notify: Restart dht22_sensing
- name: Install systemd service file
ansible.builtin.copy:
src: dht22_sensing.service
dest: /etc/systemd/system/
mode: "0644"
notify: Restart dht22_sensing
- name: Add script to autostart and start now
ansible.builtin.systemd:
name: dht22_sensing
state: started
enabled: "yes"
daemon_reload: "yes"
- name: Add to sysdweb
ansible.builtin.include_role:
name: pi_sysdweb
vars:
pi_sysdweb_name: dht22_sensing

View File

@@ -1,8 +1,8 @@
{
"ha_url": "{{home_assistant_url}}",
"token": "{{home_assistant_token}}",
"dht_pin": "{{dht_pin}}",
"polling_sleep_time_seconds": "{{dht_polling_sleep_time_seconds}}",
"dht_pin": "{{pi_dhtsensor_dht_pin}}",
"polling_sleep_time_seconds": "{{pi_dhtsensor_dht_polling_sleep_time_seconds}}",
"ha_temp_sensor_name": "{{sensor_room_name_ascii|lower}}_dht22_temperatur",
"ha_temp_friendly_name": "{{sensor_room_name}} Temperatur",

View File

@@ -0,0 +1,8 @@
# pi_dhtsensor_circuitpython
Installs the DHT22 temperature/humidity sensor polling script based on
`adafruit-circuitpython-dht` (requires `libgpiod2`) — the newer alternative
to the `pi_dhtsensor` role's legacy `Adafruit_DHT` library. Mutually
exclusive with `pi_dhtsensor`.
**Key vars:** `pi_dhtsensor_circuitpython_dht_pin`, `pi_dhtsensor_circuitpython_dht_polling_sleep_time_seconds`

View File

@@ -0,0 +1,3 @@
---
pi_dhtsensor_circuitpython_dht_pin: "D12"
pi_dhtsensor_circuitpython_dht_polling_sleep_time_seconds: 20

View File

@@ -0,0 +1,37 @@
---
- name: Apt install libgpiod2
ansible.builtin.apt:
name: libgpiod2
cache_valid_time: "7200"
state: present
- name: Pip install adafruit-circuitpython-dht
ansible.builtin.pip:
name: adafruit-circuitpython-dht
executable: pip3
- name: Install script config
ansible.builtin.template:
src: dht22_sensing.json
dest: /etc/dht22_sensing.json
mode: "0644"
- name: Install script
ansible.builtin.copy:
src: dht22_sensing.py
dest: /usr/bin/dht22_sensing
owner: root
mode: u+rwx
- name: Install systemd service file
ansible.builtin.copy:
src: dht22_sensing.service
dest: /lib/systemd/system/
mode: "0644"
- name: Add script to autostart and start now
ansible.builtin.systemd:
name: dht22_sensing
state: started
enabled: "yes"
daemon_reload: "yes"
- name: Add to sysdweb
ansible.builtin.include_role:
name: pi_sysdweb
vars:
pi_sysdweb_name: dht22_sensing

View File

@@ -1,8 +1,8 @@
{
"ha_url": "{{home_assistant_url}}",
"token": "{{home_assistant_token}}",
"dht_pin": "{{dht_pin}}",
"polling_sleep_time_seconds": "{{dht_polling_sleep_time_seconds}}",
"dht_pin": "{{pi_dhtsensor_circuitpython_dht_pin}}",
"polling_sleep_time_seconds": "{{pi_dhtsensor_circuitpython_dht_polling_sleep_time_seconds}}",
"ha_temp_sensor_name": "{{sensor_room_name_ascii|lower}}_dht22_temperatur",
"ha_temp_friendly_name": "{{sensor_room_name}} Temperatur",

View File

@@ -0,0 +1,4 @@
# pi_disable_onboard_bluetooth
Disables the Pi's onboard Bluetooth radio via `dtoverlay=disable-bt` in the
boot config, for hosts that use an external BT/audio dongle instead.

View File

@@ -0,0 +1,3 @@
---
- name: Reboot
ansible.builtin.reboot:

View File

@@ -1,7 +1,7 @@
---
- name: Deactivate onboard bluetooth
lineinfile:
ansible.builtin.lineinfile:
path: /boot/firmware/config.txt
regexp: "^#?dtoverlay=disable-bt"
line: "dtoverlay=disable-bt"
notify: reboot
notify: Reboot

View File

@@ -0,0 +1,6 @@
# pi_hifiberry_amp
Deactivates the Pi's onboard analog audio and activates a HiFiBerry
amp/DAC overlay instead.
**Key vars:** `hifiberry_overlay`

View File

@@ -0,0 +1,3 @@
---
- name: Reboot
ansible.builtin.reboot:

View File

@@ -0,0 +1,3 @@
---
dependencies:
- role: pi_alsasetup

View File

@@ -1,13 +1,13 @@
---
- name: Deactivate normal audio
lineinfile:
ansible.builtin.lineinfile:
path: /boot/config.txt
regexp: "^#?dtparam=audio=on"
line: "#dtparam=audio=on"
notify: reboot
notify: Reboot
- name: Activate Hifiberry
lineinfile:
ansible.builtin.lineinfile:
path: /boot/firmware/config.txt
regexp: "^#?dtoverlay=hifiberry-amp"
line: "dtoverlay={{hifiberry_overlay}}"
notify: reboot
line: "dtoverlay={{ hifiberry_overlay }}"
notify: Reboot

View File

@@ -0,0 +1,5 @@
# pi_irserver
Installs and configures `irserver` (with a Hauppauge remote config) as an
autostarted systemd service for IR remote control, and registers it with
`pi_sysdweb`.

View File

@@ -0,0 +1,5 @@
---
- name: Restart irserver
ansible.builtin.service:
name: irserver
state: restarted

View File

@@ -0,0 +1,35 @@
---
- name: Copy irserver
ansible.builtin.copy:
src: irserver
dest: /usr/bin/irserver
mode: u+rx
notify: Restart irserver
- name: Make config dir for remotes
ansible.builtin.file:
path: /usr/bin/remotes
state: directory
mode: "0755"
- name: Copy hauppauge remote
ansible.builtin.copy:
src: hauppauge.rem
dest: /usr/bin/remotes/
mode: "0644"
notify: Restart irserver
- name: Copy irserver systemd file
ansible.builtin.copy:
src: irserver.service
dest: /lib/systemd/system/
mode: "0644"
notify: Restart irserver
- name: Enable irserver autostart
ansible.builtin.systemd:
name: irserver
state: started
enabled: "yes"
daemon_reload: "yes"
- name: Add irserver to sysdweb
ansible.builtin.include_role:
name: pi_sysdweb
vars:
pi_sysdweb_name: irserver

Some files were not shown because too many files have changed in this diff Show More